v0.12.11: Plan Mode 格式标准化 + 图片附件感知 + 弹窗UI优化

feat: Plan Mode 强制输出格式模板(任务分析/执行计划/预期结果)
feat: 支持无需工具步骤,纯分析推理任务不再强制工具调用
feat: 反幻觉铁律新增3条(已上传附件不查找、read_file不能看图、Plan不规划查找步骤)
fix: 上传图片后消息文本明确提示AI图片已随消息提供
fix: Plan Mode确认弹窗UI优化(步骤卡片样式/宽度/滚动)
style: extractPlanSteps 双策略解析,兼容多种模型输出格式
This commit is contained in:
紫影233
2026-06-24 11:40:18 +08:00
parent d7275b08e8
commit c7aa5df81c
9 changed files with 143 additions and 52 deletions
+72 -19
View File
@@ -537,10 +537,34 @@ async function handleInit(
if (ctx.mode === 'plan') {
systemPromptParts.push(`[Plan Mode 执行规则]
你当前处于 Plan Mode(先规划后执行)。重要规则:
1. 第一轮回复必须是执行计划,不要直接调用工具。列出步骤、涉及工具和预期结果后,等待用户批准。
2. 计划批准后,系统会自动追踪工具执行进度——你每完成一个步骤对应的工具调用,系统会自动标记该步骤为完成。
3. 你也可以手动调用 plan_track(action='mark_done', step_index=N) 来更新进度
4. 所有步骤完成后直接给出最终回答。不需要手动调用 plan_track 来标记全部完成——系统会自动检测。`);
**输出格式(必须严格遵守)**
你的第一轮回复必须是一个清晰的执行计划,使用以下格式。不要输出其他无关内容
## 任务分析
[1-2 句话概述任务,展示你的理解]
## 执行计划
1. **步骤名称** — 工具: tool_name — 简要描述这一步做什么,完成后预期结果是什么
2. **步骤名称** — 无需工具 — 如果这一步只需推理/分析/总结,标注"无需工具"并描述分析要点
...(根据任务复杂度,通常 2-6 步)
## 预期结果
[完成所有步骤后的最终产出]
**关键规则**
- 需要调用工具的步骤:必须写"工具: xxx"(如 工具: web_search、工具: write_file
- 纯分析/推理/总结的步骤:写"无需工具",描述你要分析和思考的要点
- 禁止模糊描述如"分析需求"——即使是分析步骤,也要写清楚分析什么、关注什么
- 如果用户已上传图片或文件,第一步必须是分析附件(写"无需工具"即可,图片你直接能看到)
- 如果整个任务完全不需要工具(如纯翻译、润色、总结对话),可以全部步骤都是"无需工具"
- 输出计划后等待用户批准,**不要**在输出计划的同时调用工具
**计划批准后**
- 需要工具的步骤:调用对应工具完成任务
- 无需工具的步骤:直接基于已有信息给出分析结论
- 系统会自动追踪工具执行进度
- 所有步骤完成后直接给出最终回答。`);
// 将用户原始任务描述固化到系统提示词中(不会被压缩或清理)
const taskDesc = userContent?.slice(0, 200) || '';
@@ -589,6 +613,9 @@ Shell: ${osInfo.shell}
3. **搜索必须实际执行**:说"搜索结果显示"、"根据搜索结果"之前,必须先调用 web_search。搜索结果的 snippet 不可信,必须用 web_fetch 获取完整内容后才能引用。
4. **信息不足时如实报告**:如果工具调用失败或返回了意外的结果,必须如实报告,不能编造替代信息。
5. **完成标志**:当所有必需的工具调用已经实际执行完毕,且获得了足够的信息后,才能给出最终回答。最终回答中不要编造"文件已生成"等声明——除非你真的调用了对应工具。
6. **用户上传的图片和文件已在当前消息中**:如果用户消息中包含图片附件标记(如 [已上传 N 张图片: ...])或文件标记(如 [文件: xxx]),说明这些资源已随消息作为附件提供,你可以直接"看到"和分析它们。**严禁再用 read_file / search_files 等工具去磁盘上查找这些已上传的图片和文件**——它们不在磁盘上,就在当前消息里。不要浪费轮次去做无意义的文件搜索。
7. **read_file 不能看图片**read_file 返回的是文本或 base64 编码字符串,视觉模型无法处理。如果你需要分析图片内容,只有用户通过上传功能提供的图片才是可见的。不要尝试用 read_file mode=binary 去"读取"图片文件。
8. **禁止在 Plan 模式下为已上传的附件规划文件查找步骤**:如果用户消息中已经有图片或文件附件,执行计划的第一步应该是直接分析这些附件,而不是"搜索工作空间中的图片文件"或"查找相关文件"。
违反以上任何一条都是不可接受的错误。请逐条对照检查你的每一次回复。`);
@@ -656,7 +683,7 @@ Shell: ${osInfo.shell}
if (ctx.mode === 'plan' && ctx.loopCount === 0) {
ctx.messages.push({
role: 'user' as const,
content: `[Plan Mode] 请先分析任务,列出执行计划(包括需要的工具、步骤和预期结果),然后等待我的确认。不要直接执行工具调用`,
content: `[Plan Mode] 请按照 Plan Mode 执行规则中指定的格式输出执行计划。不要直接执行工具调用,先输出计划等待用户批准`,
ephemeral: true,
});
}
@@ -674,23 +701,49 @@ function truncateByTokenBudget(text: string, maxTokens: number): string {
return text.slice(0, cutAt) + '\n\n... (已截断以控制 Token 预算)';
}
/** 从 Plan Mode 输出中提取步骤列表 */
/** 从 Plan Mode 输出中提取步骤列表(兼容多种模型格式) */
function extractPlanSteps(content: string): string[] {
const steps: string[] = [];
const stepRegex = /(?:^|\n)\s*(?:\d+[\.\)、]\s*|[-*]\s+)(.+)/g;
// 过滤项:阶段标题、HTML 标记、纯格式描述
const filterPatterns = [/^\*\*.*阶段.*\*\*/, /\*\*第[一二三]|\*\*Phase/, /^```/, /^<\/?/, /^Stage/i];
// 动作动词:步骤必须包含至少一个表示"要做什么"的词
const ACTION_VERBS = /搜索|查找|检索|抓取|fetch|写入|write|创建|生成|运行|执行|打开|浏览|读取|下载|提交|推送|编译|压缩|解压|截图|提取|设计|构建|编码|HTML|CSS|组织|整理|归类|搜集|汇总/;
let match;
while ((match = stepRegex.exec(content)) !== null) {
const step = match[1].trim();
if (step.length < 5 || step.length > 200) continue;
if (filterPatterns.some(p => p.test(step))) continue;
if (!ACTION_VERBS.test(step)) continue; // 纯描述无动词,跳过
steps.push(step);
// 策略1: 精确匹配 "## 执行计划" 区块中的编号行
const planSection = content.match(/##\s*执行计划\s*\n([\s\S]*?)(?=\n##|\n---|\n\*\*关键|$)/);
if (planSection) {
const lines = planSection[1].split('\n');
for (const line of lines) {
// 匹配: 1. **步骤名** — 工具: xxx — 描述
const match = line.match(/^\d+[\.\)、]\s*(?:\*\*)?(.+?)(?:\*\*)?(?:\s*[—\-]\s*)/);
if (match) {
const step = match[0].trim();
if (step.length >= 5 && step.length <= 200) {
steps.push(step);
continue;
}
}
// 回退: 匹配任何编号行
const looseMatch = line.match(/^\d+[\.\)、]\s+(.+)/);
if (looseMatch) {
const step = looseMatch[1].trim();
if (step.length >= 5 && step.length <= 200) {
steps.push(step);
}
}
}
}
return steps.length > 0 ? steps.slice(0, 6) : ['执行任务计划(详见上方描述)'];
// 策略2: 回退 — 全局匹配编号行(松动匹配,接受"无需工具"的步骤)
if (steps.length === 0) {
const stepRegex = /(?:^|\n)\s*(?:\d+[\.\)、]\s*|[-*]\s+)(.+)/g;
const filterPatterns = [/^\*\*.*阶段.*\*\*/, /\*\*第[一二三]|\*\*Phase/, /^```/, /^<\/?/, /^Stage/i];
let match;
while ((match = stepRegex.exec(content)) !== null) {
const step = match[1].trim();
if (step.length < 5 || step.length > 200) continue;
if (filterPatterns.some(p => p.test(step))) continue;
steps.push(step);
}
}
return steps.length > 0 ? steps.slice(0, 8) : ['执行任务计划(详见上方描述)'];
}
/**