kuaifan 3911af7b51 fix(ai): 修复描述格式和负责人重复问题
1. 描述建议:AI 返回 Markdown,前端用 MarkdownConver 转 HTML
2. 负责人推荐:排除已分配的任务成员
3. 解析负责人推荐时去重,防止 AI 返回重复用户

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:07 +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':
// 更新任务描述Markdown 转 HTML
this.$store.dispatch('taskUpdate', {
task_id: taskId,
content: MarkdownConver(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>