kuaifan 0a97039d75 refactor(ai): 重构 AI 建议功能并完善向量搜索
1. 重构 task__ai_apply 接口:移除业务逻辑,仅负责状态更新和日志记录,
   返回建议数据由前端调用现有接口处理(taskUpdate/taskAddSub)

2. 实现 searchSimilarByEmbedding 向量搜索:
   - 使用 ManticoreBase::taskVectorSearch 进行向量搜索
   - 按 project_id 过滤同项目任务
   - 排除当前任务及其子任务
   - 设置 0.7 相似度阈值,最多返回 5 个结果

3. 更新 AI 助手头像:将文字 "AI" 替换为 SVG 图标

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-01-21 15:30:06 +00:00

299 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div @click="onCLick" class="markdown-body" v-html="html"></div>
</template>
<script>
import '../../../../sass/pages/components/dialog-markdown/markdown.less'
import {MarkdownConver} from "../../../utils/markdown";
export default {
name: "DialogMarkdown",
props: {
text: {
type: String,
default: ''
},
// 导航前回调(如关闭弹窗)
beforeNavigate: {
type: Function,
default: null
},
},
data() {
return {
mdi: null,
}
},
mounted() {
this.copyCodeBlock()
},
updated() {
this.copyCodeBlock()
},
computed: {
html({text}) {
return MarkdownConver(text)
}
},
methods: {
copyCodeBlock() {
const codeBlockWrapper = this.$el.querySelectorAll('.code-block-wrapper')
codeBlockWrapper.forEach((wrapper) => {
const copyBtn = wrapper.querySelector('.code-block-header__copy')
const codeBlock = wrapper.querySelector('.code-block-body')
if (copyBtn && codeBlock && copyBtn.getAttribute("data-copy") !== "click") {
copyBtn.setAttribute("data-copy", "click")
copyBtn.addEventListener('click', () => {
if (navigator.clipboard?.writeText)
navigator.clipboard.writeText(codeBlock.textContent ?? '')
else
this.copyContent({text: codeBlock.textContent ?? '', origin: true})
})
}
})
},
copyContent(options) {
const props = {origin: true, ...options}
let input
if (props.origin)
input = document.createElement('textarea')
else
input = document.createElement('input')
input.setAttribute('readonly', 'readonly')
input.value = props.text
document.body.appendChild(input)
input.select()
if (document.execCommand('copy'))
document.execCommand('copy')
document.body.removeChild(input)
},
onCLick(e) {
const target = e.target;
if (target.tagName === 'A') {
const href = target.getAttribute('href');
if (href && href.startsWith('dootask://')) {
e.preventDefault();
e.stopPropagation();
this.handleDooTaskLink(href);
return;
}
}
this.$emit('click', e)
},
/**
* 处理 dootask:// 协议链接
* 格式: dootask://type/id 或 dootask://type/id1/id2
* 文件链接支持: dootask://file/123 (数字ID) 或 dootask://file/OSwxLHY3ZlN2R245 (base64编码)
* AI 建议链接: dootask://ai-apply/{type}/{task_id}/{msg_id} 或 dootask://ai-dismiss/...
*/
handleDooTaskLink(href) {
// 优先处理 AI 建议链接(格式与其他类型不同)
if (href.startsWith('dootask://ai-apply/')) {
this.handleAiApply(href);
return;
}
if (href.startsWith('dootask://ai-dismiss/')) {
this.handleAiDismiss(href);
return;
}
const match = href.match(/^dootask:\/\/(\w+)\/([^/]+)(?:\/(\d+))?$/);
if (!match) {
return;
}
const [, type, id, id2] = match;
const isNumericId = /^\d+$/.test(id);
const numId = isNumericId ? parseInt(id, 10) : null;
const numId2 = id2 ? parseInt(id2, 10) : null;
switch (type) {
case 'task':
this.$store.dispatch('openTask', { id: (numId2 && numId2 > 0) ? numId2 : numId });
break;
case 'project':
this.beforeNavigate?.();
this.goForward({ name: 'manage-project', params: { projectId: numId } });
break;
case 'file':
if (isNumericId) {
// 数字ID跳转到文件列表并高亮
this.beforeNavigate?.();
this.goForward({ name: 'manage-file', params: { folderId: 0, fileId: null, shakeId: numId } });
this.$store.state.fileShakeId = numId;
setTimeout(() => {
this.$store.state.fileShakeId = 0;
}, 600);
} else {
// 非数字ID如base64编码打开新窗口预览
window.open($A.mainUrl('single/file/' + id));
}
break;
case 'contact':
this.$store.dispatch('openDialogUserid', numId).catch(({ msg }) => {
$A.modalError(msg);
});
break;
case 'message':
this.$store.dispatch('openDialog', numId).then(() => {
if (numId2) {
this.$store.state.dialogSearchMsgId = numId2;
}
}).catch(({ msg }) => {
$A.modalError(msg);
});
break;
}
},
/**
* 处理 AI 建议采纳
* 格式: dootask://ai-apply/{type}/{task_id}/{msg_id}?{params}
*/
handleAiApply(href) {
const match = href.match(/^dootask:\/\/ai-apply\/(\w+)\/(\d+)\/(\d+)(?:\?(.*))?$/);
if (!match) {
return;
}
const [, type, taskId, msgId, queryString] = match;
const params = new URLSearchParams(queryString || '');
// 先调用接口标记为已采纳,获取建议数据
this.$store.dispatch('applyAiSuggestion', {
task_id: parseInt(taskId, 10),
msg_id: parseInt(msgId, 10),
type,
}).then(({data}) => {
// 根据类型调用对应的业务接口
this.applyAiSuggestionByType(data.type, data.task_id, data.result, params);
}).catch(({msg}) => {
$A.modalError(msg);
});
},
/**
* 根据类型执行对应的业务操作
*/
applyAiSuggestionByType(type, taskId, result, params) {
switch (type) {
case 'description':
// 更新任务描述
this.$store.dispatch('taskUpdate', {
task_id: taskId,
content: result.content,
}).then(() => {
$A.messageSuccess(this.$L('应用成功'));
}).catch(({msg}) => {
$A.modalError(msg);
});
break;
case 'subtasks':
// 批量创建子任务
this.createSubtasksSequentially(taskId, result.content || []);
break;
case 'assignee':
// 指派负责人
const userid = params.get('userid');
if (!userid || isNaN(parseInt(userid, 10))) {
$A.modalError(this.$L('请选择负责人'));
return;
}
this.$store.dispatch('taskUpdate', {
task_id: taskId,
owner: [parseInt(userid, 10)],
}).then(() => {
$A.messageSuccess(this.$L('应用成功'));
}).catch(({msg}) => {
$A.modalError(msg);
});
break;
case 'similar':
// 相似任务关联(当前功能未启用)
$A.messageSuccess(this.$L('应用成功'));
break;
default:
$A.modalError(this.$L('未知的建议类型'));
}
},
/**
* 顺序创建子任务
*/
createSubtasksSequentially(taskId, subtasks) {
if (!subtasks || subtasks.length === 0) {
$A.modalError(this.$L('没有有效的子任务'));
return;
}
let completed = 0;
const total = subtasks.length;
const createNext = (index) => {
if (index >= total) {
$A.messageSuccess(this.$L('应用成功'));
return;
}
const name = subtasks[index];
if (!name || typeof name !== 'string' || !name.trim()) {
createNext(index + 1);
return;
}
this.$store.dispatch('taskAddSub', {
task_id: taskId,
name: name.trim(),
}).then(() => {
completed++;
createNext(index + 1);
}).catch(({msg}) => {
// 单个失败不影响后续创建
console.warn(`创建子任务失败: ${name}`, msg);
createNext(index + 1);
});
};
createNext(0);
},
/**
* 处理 AI 建议忽略
* 格式: dootask://ai-dismiss/{type}/{task_id}/{msg_id}
*/
handleAiDismiss(href) {
const match = href.match(/^dootask:\/\/ai-dismiss\/(\w+)\/(\d+)\/(\d+)$/);
if (!match) {
return;
}
const [, type, taskId, msgId] = match;
this.$store.dispatch('dismissAiSuggestion', {
task_id: parseInt(taskId, 10),
msg_id: parseInt(msgId, 10),
type,
}).then(() => {
$A.messageSuccess(this.$L('已忽略'));
}).catch(({msg}) => {
$A.modalError(msg);
});
}
}
}
</script>