diff --git a/app/Tasks/BotReceiveMsgTask.php b/app/Tasks/BotReceiveMsgTask.php
index b860a42a1..7aa221569 100644
--- a/app/Tasks/BotReceiveMsgTask.php
+++ b/app/Tasks/BotReceiveMsgTask.php
@@ -675,6 +675,20 @@ class BotReceiveMsgTask extends AbstractTask
$prompt[] = implode("\n", $contextLines);
}
+ // 4、追加条件性格式指南(放在最后,优先级最低)
+ $prompt[] = <<<'EOF'
+
+ 当你的回答中包含 DooTask 系统资源(任务、项目、文件等)时,建议使用以下链接格式使其可点击:
+ - 任务: [任务名称](dootask://task/{task_id}/{parent_id}),其中 parent_id 为主任务ID,主任务时为 0
+ - 项目: [项目名称](dootask://project/{project_id})
+ - 文件: [文件名称](dootask://file/{file_id})
+ - 联系人: [用户名](dootask://contact/{userid})
+ - 消息: [消息预览](dootask://message/{dialog_id}/{msg_id})
+
+ 注意:此格式指南不影响正常对话,仅在涉及上述资源时参考。如果与当前对话无关,请忽略。
+
+ EOF;
+
$extras['system_message'] = implode("\n----\n", array_filter($prompt));
}
diff --git a/resources/assets/js/components/AIAssistant.vue b/resources/assets/js/components/AIAssistant.vue
index 18c637682..50c8e3e35 100644
--- a/resources/assets/js/components/AIAssistant.vue
+++ b/resources/assets/js/components/AIAssistant.vue
@@ -84,7 +84,7 @@
v-if="response.rawOutput"
class="ai-assistant-output-markdown no-dark-content"
:text="response.displayOutput || response.rawOutput"
- @click="onContentClick"/>
+ :before-navigate="() => { showModal = false }"/>
{{ response.status === 'error' ? (response.error || $L('发送失败')) : $L('等待 AI 回复...') }}
@@ -900,71 +900,6 @@ export default {
return distance <= threshold;
},
- /**
- * 处理内容区域的点击事件
- */
- onContentClick(e) {
- const target = e.target;
- if (target.tagName !== 'A') {
- return;
- }
-
- const href = target.getAttribute('href');
- if (!href || !href.startsWith('dootask://')) {
- return;
- }
-
- e.preventDefault();
- e.stopPropagation();
-
- // 解析 dootask:// 协议链接
- // 格式: dootask://type/id 或 dootask://type/id1/id2 例如 dootask://task/123 或 dootask://message/789/1234
- const match = href.match(/^dootask:\/\/(\w+)\/(\d+)(?:\/(\d+))?$/);
- if (!match) {
- return;
- }
-
- const [, type, id, id2] = match;
- const numId = parseInt(id, 10);
- 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.showModal = false;
- this.goForward({name: 'manage-project', params: {projectId: numId}});
- break;
-
- case 'file':
- this.showModal = false;
- this.goForward({name: 'manage-file', params: {folderId: 0, fileId: null, shakeId: numId}});
- this.$store.state.fileShakeId = numId;
- setTimeout(() => {
- this.$store.state.fileShakeId = 0;
- }, 600);
- break;
-
- case 'contact':
- this.$store.dispatch('openDialogUserid', numId).catch(({msg}) => {
- $A.modalError(msg || this.$L('打开会话失败'));
- });
- break;
-
- case 'message':
- this.$store.dispatch('openDialog', numId).then(() => {
- if (numId2) {
- this.$store.state.dialogSearchMsgId = numId2;
- }
- }).catch(({msg}) => {
- $A.modalError(msg || this.$L('打开会话失败'));
- });
- break;
- }
- },
-
// ==================== 会话管理方法 ====================
/**
diff --git a/resources/assets/js/components/SearchBox.vue b/resources/assets/js/components/SearchBox.vue
index 23afaf403..1858aeffa 100755
--- a/resources/assets/js/components/SearchBox.vue
+++ b/resources/assets/js/components/SearchBox.vue
@@ -94,6 +94,7 @@
import {mapState} from "vuex";
import emitter from "../store/events";
import transformEmojiToHtml from "../utils/emoji";
+import {SEARCH_AI_SYSTEM_PROMPT} from "../utils/ai";
export default {
name: 'SearchBox',
@@ -593,33 +594,8 @@ export default {
},
handleAISearchBeforeSend(context = []) {
- const systemPrompt = [
- '你是一个智能搜索助手,负责帮助用户在 DooTask 系统中搜索和整理信息。',
- '你可以使用 intelligent_search 工具来搜索任务、项目、文件和联系人。',
- '',
- '请根据用户的搜索需求:',
- '1. 调用搜索工具获取相关结果',
- '2. 对搜索结果进行分类整理',
- '3. 以清晰的格式呈现给用户',
- '4. 如有需要,可以进行多次搜索以获取更全面的结果',
- '',
- '## 链接格式要求',
- '在返回结果时,请使用以下格式创建可点击的链接:',
- '- 任务: [任务名称](dootask://task/任务ID/主任务ID)',
- '- 项目: [项目名称](dootask://project/项目ID)',
- '- 文件: [文件名称](dootask://file/文件ID)',
- '- 联系人: [联系人名称](dootask://contact/用户ID)',
- '- 消息: [消息内容预览](dootask://message/对话ID/消息ID)',
- '',
- '示例:',
- '- [完成项目报告](dootask://task/123/0)(主任务)',
- '- [编写测试用例](dootask://task/456/123)(子任务)',
- '- [产品开发项目](dootask://project/456)',
- '- [关于报销的讨论](dootask://message/789/1234)',
- ].join('\n');
-
const prepared = [
- ['system', systemPrompt]
+ ['system', SEARCH_AI_SYSTEM_PROMPT]
];
if (context.length > 0) {
diff --git a/resources/assets/js/pages/manage/components/DialogMarkdown.vue b/resources/assets/js/pages/manage/components/DialogMarkdown.vue
index 9a074c015..cee3e1034 100644
--- a/resources/assets/js/pages/manage/components/DialogMarkdown.vue
+++ b/resources/assets/js/pages/manage/components/DialogMarkdown.vue
@@ -13,6 +13,11 @@ export default {
type: String,
default: ''
},
+ // 导航前回调(如关闭弹窗)
+ beforeNavigate: {
+ type: Function,
+ default: null
+ },
},
data() {
return {
@@ -72,7 +77,68 @@ export default {
},
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
+ */
+ handleDooTaskLink(href) {
+ const match = href.match(/^dootask:\/\/(\w+)\/(\d+)(?:\/(\d+))?$/);
+ if (!match) {
+ return;
+ }
+
+ const [, type, id, id2] = match;
+ const numId = parseInt(id, 10);
+ 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':
+ 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);
+ 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;
+ }
}
}
}
diff --git a/resources/assets/js/utils/ai.js b/resources/assets/js/utils/ai.js
index befbf0c49..5bdbd6c69 100644
--- a/resources/assets/js/utils/ai.js
+++ b/resources/assets/js/utils/ai.js
@@ -1,60 +1,8 @@
import {languageList, languageName} from "../language";
-const withLanguagePreferencePrompt = (prompt) => {
- if (typeof prompt !== 'string' || !prompt) {
- return prompt;
- }
- const label = languageList[languageName] || languageName || '';
- if (!label) {
- return prompt;
- }
- return `${prompt}\n\n输出语言策略:\n- 默认使用 ${label} 输出。\n- 即使上下文或引用包含其他语言,也保持 ${label} 输出。\n- 仅当我明确指定其他语言时,才切换到该语言。`;
-};
-
-const AIModelNames = (str) => {
- const lines = str.split('\n').filter(line => line.trim());
-
- return lines.map(line => {
- const [value, label] = line.split('|').map(s => s.trim());
-
- return {
- value,
- label: label || value
- };
- }, []).filter(item => item.value);
-}
-
-const AINormalizeJsonContent = (content) => {
- if (!content) {
- return null;
- }
- const raw = String(content).trim();
- if (!raw) {
- return null;
- }
- const candidates = [raw];
- const block = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
- if (block && block[1]) {
- candidates.push(block[1].trim());
- }
- const start = raw.indexOf('{');
- const end = raw.lastIndexOf('}');
- if (start !== -1 && end !== -1 && end > start) {
- candidates.push(raw.slice(start, end + 1));
- }
- for (const candidate of candidates) {
- if (!candidate) {
- continue;
- }
- try {
- return JSON.parse(candidate);
- } catch (e) {
- // continue
- }
- }
- return null;
-}
-
+/**
+ * AI 服务商标识与显示名映射
+ */
const AIBotMap = {
openai: "ChatGPT",
claude: "Claude",
@@ -67,6 +15,9 @@ const AIBotMap = {
wenxin: "文心一言",
}
+/**
+ * AI 系统配置表单与平台配置
+ */
const AISystemConfig = {
fields: [
{
@@ -259,6 +210,9 @@ const AISystemConfig = {
}
}
+/**
+ * 即时消息生成系统提示词
+ */
const MESSAGE_AI_SYSTEM_PROMPT = `你是一名专业的沟通助手,协助用户编写得体、清晰且具行动指向的即时消息。
写作要求:
@@ -272,6 +226,9 @@ const MESSAGE_AI_SYSTEM_PROMPT = `你是一名专业的沟通助手,协助用
- 仅返回可直接发送的消息内容
- 禁止在内容前后添加额外说明、标签或引导语`;
+/**
+ * 任务生成系统提示词
+ */
const TASK_AI_SYSTEM_PROMPT = `你是一个专业的任务管理专家,擅长将想法和需求转化为清晰、可执行的项目任务。
任务生成要求:
@@ -315,6 +272,9 @@ const TASK_AI_SYSTEM_PROMPT = `你是一个专业的任务管理专家,擅长
- 如果涉及设计,要说明设计要求和期望效果
- 如果涉及测试,要明确测试范围和验收标准`;
+/**
+ * 项目创建系统提示词
+ */
const PROJECT_AI_SYSTEM_PROMPT = `你是一名资深的项目规划顾问,帮助团队快速搭建符合需求的项目。
生成要求:
@@ -335,6 +295,9 @@ const PROJECT_AI_SYSTEM_PROMPT = `你是一名资深的项目规划顾问,帮
- 列表名称应当互不重复且语义明确
- 若上下文包含已有名称或列表,请在此基础上迭代优化`;
+/**
+ * 周报/日报整理系统提示词
+ */
const REPORT_AI_SYSTEM_PROMPT = `你是一名资深团队管理教练,需要根据提供的周报/日报草稿进行整理。
工作目标:
@@ -349,6 +312,9 @@ const REPORT_AI_SYSTEM_PROMPT = `你是一名资深团队管理教练,需要
- 若原文包含数据或里程碑,保留并突出这些数字
- 若某一章节没有信息,请输出“暂无”而非留空`;
+/**
+ * 汇报分析系统提示词
+ */
const REPORT_ANALYSIS_SYSTEM_PROMPT = `你是一名经验丰富的团队管理顾问,擅长阅读和分析员工提交的工作汇报,能够快速提炼重点并给出可执行建议。
输出要求:
@@ -358,9 +324,112 @@ const REPORT_ANALYSIS_SYSTEM_PROMPT = `你是一名经验丰富的团队管理
4. 语气保持专业、客观、中立,不过度夸赞或批评
5. 控制在 200-400 字之间,可视内容复杂度略微增减,但保持紧凑`;
+/**
+ * 智能搜索系统提示词
+ */
+const SEARCH_AI_SYSTEM_PROMPT = `你是一个智能搜索助手,负责帮助用户在 DooTask 系统中搜索和整理信息。
+你可以使用 intelligent_search 工具来搜索任务、项目、文件和联系人。
+
+请根据用户的搜索需求:
+1. 调用搜索工具获取相关结果
+2. 对搜索结果进行分类整理
+3. 以清晰的格式呈现给用户
+4. 如有需要,可以进行多次搜索以获取更全面的结果`;
+
+/**
+ * DooTask 资源格式指南(条件性提示词)
+ * 仅在 AI 返回 DooTask 资源时生效,不影响普通对话
+ */
+const DOOTASK_RESOURCE_FORMAT_GUIDE = `
+
+当你的回答中包含 DooTask 系统资源(任务、项目、文件等)时,建议使用以下链接格式使其可点击:
+- 任务: [任务名称](dootask://task/{task_id}/{parent_id}),其中 parent_id 为主任务ID,主任务时为 0
+- 项目: [项目名称](dootask://project/{project_id})
+- 文件: [文件名称](dootask://file/{file_id})
+- 联系人: [用户名](dootask://contact/{userid})
+- 消息: [消息预览](dootask://message/{dialog_id}/{msg_id})
+
+注意:此格式指南不影响正常对话,仅在涉及上述资源时参考。如果与当前对话无关,请忽略。
+
+`.trim();
+
+/**
+ * 输出语言偏好提示
+ * 用于引导 AI 按指定语言输出
+ */
+const LANGUAGE_PREFERENCE_PROMPT = (label) => `输出语言策略:
+- 默认使用 ${label} 输出。
+- 即使上下文或引用包含其他语言,也保持 ${label} 输出。
+- 仅当我明确指定其他语言时,才切换到该语言。`;
+
+/**
+ * 注入语言偏好提示与资源格式指南
+ * 返回拼接后的完整提示词
+ */
+const withLanguagePreferencePrompt = (prompt) => {
+ if (typeof prompt !== 'string' || !prompt) {
+ return prompt;
+ }
+ const label = languageList[languageName] || languageName || '';
+ if (!label) {
+ return prompt;
+ }
+ return `${prompt}\n\n${LANGUAGE_PREFERENCE_PROMPT(label)}\n\n${DOOTASK_RESOURCE_FORMAT_GUIDE}`;
+};
+
+/**
+ * 解析模型列表文本为选项数组
+ * 支持以 "|" 分隔显示名
+ */
+const AIModelNames = (str) => {
+ const lines = str.split('\n').filter(line => line.trim());
+
+ return lines.map(line => {
+ const [value, label] = line.split('|').map(s => s.trim());
+
+ return {
+ value,
+ label: label || value
+ };
+ }, []).filter(item => item.value);
+}
+
+/**
+ * 尝试从内容中提取并解析 JSON
+ * 支持代码块与裸 JSON
+ */
+const AINormalizeJsonContent = (content) => {
+ if (!content) {
+ return null;
+ }
+ const raw = String(content).trim();
+ if (!raw) {
+ return null;
+ }
+ const candidates = [raw];
+ const block = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
+ if (block && block[1]) {
+ candidates.push(block[1].trim());
+ }
+ const start = raw.indexOf('{');
+ const end = raw.lastIndexOf('}');
+ if (start !== -1 && end !== -1 && end > start) {
+ candidates.push(raw.slice(start, end + 1));
+ }
+ for (const candidate of candidates) {
+ if (!candidate) {
+ continue;
+ }
+ try {
+ return JSON.parse(candidate);
+ } catch (e) {
+ // continue
+ }
+ }
+ return null;
+}
+
export {
- AIModelNames,
- AINormalizeJsonContent,
AIBotMap,
AISystemConfig,
MESSAGE_AI_SYSTEM_PROMPT,
@@ -368,5 +437,8 @@ export {
PROJECT_AI_SYSTEM_PROMPT,
REPORT_AI_SYSTEM_PROMPT,
REPORT_ANALYSIS_SYSTEM_PROMPT,
+ SEARCH_AI_SYSTEM_PROMPT,
withLanguagePreferencePrompt,
+ AIModelNames,
+ AINormalizeJsonContent,
}