mirror of
https://github.com/kuaifan/dootask.git
synced 2026-01-26 20:48:12 +00:00
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>
299 lines
10 KiB
Vue
299 lines
10 KiB
Vue
<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>
|