diff --git a/Plugin/AICodeWorker/AICodeWorker.js b/Plugin/AICodeWorker/AICodeWorker.js index 19387ea69..1d18de545 100644 --- a/Plugin/AICodeWorker/AICodeWorker.js +++ b/Plugin/AICodeWorker/AICodeWorker.js @@ -1,12 +1,22 @@ "use strict"; -// AICodeWorker - VCP 插件主入口 -// 让 VCP Agent 可以安全调度本地 opencode / mimocode 执行代码分析和 patch 生成。 -// 插件类型: synchronous / stdio。run 命令后台启动 runner.js 并立即返回 jobId。 +// AICodeWorker - VCP 插件主入口 v1.6.0 +// 让 VCP Agent 可以安全调度本地 opencode 执行代码分析和 patch 生成。 +// 插件类型: synchronous / stdio。 +// +// v1.5 核心升级:规范化报告输出 +// - 三种模式前缀末尾加入固定报告规范(文件清单 + 执行结果摘要锚点) +// - buildResult 优先提取 【执行结果摘要】 锚点,新增 fileReadList 字段 +// - opencode 工作质量与上报质量双保证 const { spawn } = require("child_process"); const fs = require("fs"); const path = require("path"); +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); + +const BACKOFF_RUN_WAIT = [2, 3, 5, 10, 15, 20, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]; +const BACKOFF_QUERY = [5, 10, 15, 20, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]; + // ─── 配置加载 ───────────────────────────────────────────────────────────────── function loadConfig() { @@ -27,8 +37,9 @@ function loadConfig() { enableOpencode: (raw.ENABLE_OPENCODE || "true") !== "false", enableMimocode: (raw.ENABLE_MIMOCODE || "false") !== "false", opencodeBin: raw.OPENCODE_BIN || "opencode", - opencodeBaseUrl: raw.OPENCODE_BASE_URL || "http://127.0.0.1:6005", + opencodeBaseUrl: raw.OPENCODE_BASE_URL || "", opencodeApiKey: raw.OPENCODE_API_KEY || process.env.ANTHROPIC_API_KEY || "", + opencodeModel: raw.OPENCODE_MODEL || "", allowedRoots: (raw.ALLOWED_PROJECT_ROOTS || "/app/VCPToolBox_new,/app/ZhongZhuan,/app/claud") .split(",").map(s => s.trim()).filter(Boolean), jobRoot: raw.JOB_ROOT || path.join(__dirname, "jobs"), @@ -36,20 +47,23 @@ function loadConfig() { defaultTimeout: parseInt(raw.DEFAULT_TIMEOUT_SEC || "600", 10), allowDangerSkip: (raw.ALLOW_DANGEROUS_SKIP_PERMISSIONS || "false") !== "false", redactSecrets: (raw.REDACT_SECRETS || "true") !== "false", + projectContext: raw.PROJECT_CONTEXT ? raw.PROJECT_CONTEXT.replace(/\\n/g, "\n") : "", + fileSizeWarnKB: parseInt(raw.FILE_SIZE_WARN_KB || "200", 10), }; } const CFG = loadConfig(); +let _ocVersionCache = null; // ─── Job 路径 ───────────────────────────────────────────────────────────────── function jobPaths(jobId) { return { - output: path.join(CFG.jobRoot, "output", `${jobId}.txt`), - log: path.join(CFG.jobRoot, "logs", `${jobId}.log`), - patch: path.join(CFG.jobRoot, "patches",`${jobId}.patch`), - meta: path.join(CFG.jobRoot, "meta", `${jobId}.json`), - args: path.join(CFG.jobRoot, "meta", `${jobId}.args.json`), + output: path.join(CFG.jobRoot, "output", `${jobId}.txt`), + log: path.join(CFG.jobRoot, "logs", `${jobId}.log`), + patch: path.join(CFG.jobRoot, "patches", `${jobId}.patch`), + meta: path.join(CFG.jobRoot, "meta", `${jobId}.json`), + args: path.join(CFG.jobRoot, "meta", `${jobId}.args.json`), }; } @@ -62,8 +76,9 @@ function ensureJobDirs() { function generateJobId() { const n = new Date(); - const p = (x) => String(x).padStart(2, "0"); - return `job_${n.getFullYear()}${p(n.getMonth()+1)}${p(n.getDate())}_${p(n.getHours())}${p(n.getMinutes())}${p(n.getSeconds())}_${process.pid}`; + const p = x => String(x).padStart(2, "0"); + const rand = String(Math.floor(Math.random() * 9000) + 1000); + return `job_${n.getFullYear()}${p(n.getMonth()+1)}${p(n.getDate())}_${p(n.getHours())}${p(n.getMinutes())}${p(n.getSeconds())}_${rand}`; } function readMeta(jobId) { @@ -76,8 +91,6 @@ function saveMeta(jobId, meta) { fs.writeFileSync(jobPaths(jobId).meta, JSON.stringify(meta, null, 2), "utf8"); } -// ─── 进程存活检测 ───────────────────────────────────────────────────────────── - function isProcessRunning(pid) { if (!pid) return false; try { process.kill(Number(pid), 0); return true; } catch { return false; } @@ -86,10 +99,12 @@ function isProcessRunning(pid) { // ─── 密钥脱敏 ───────────────────────────────────────────────────────────────── const SECRET_RE = [ - /(?:ANTHROPIC_API_KEY|OPENAI_API_KEY|x-api-key|Authorization)[^\n"',]*/gi, + // 要求 key 名后面有 =: 分隔符 + 实际值(≥10字符),避免误伤源码变量名/正则 + /(?:ANTHROPIC_API_KEY|OPENAI_API_KEY|x-api-key)\s*[=:]\s*["']?[A-Za-z0-9\-_.+/]{10,}["']?/gi, + /Authorization\s*:\s*\S{10,}/gi, /Bearer\s+[A-Za-z0-9\-_.~+/]+=*/g, /sk-[A-Za-z0-9]{20,}/g, - /(?:password|token|secret)[=:\s]+\S+/gi, + /\b(?:password|token|secret)\b\s*[=:]\s*\S{6,}/gi, ]; function redact(text) { @@ -102,9 +117,8 @@ function redact(text) { // ─── 路径白名单验证 ─────────────────────────────────────────────────────────── function validatePath(projectPath) { - if (!projectPath || typeof projectPath !== "string") { + if (!projectPath || typeof projectPath !== "string") return "projectPath 是必填参数。"; - } const resolved = path.resolve(projectPath); for (const root of CFG.allowedRoots) { const r = path.resolve(root); @@ -113,54 +127,356 @@ function validatePath(projectPath) { return `projectPath "${resolved}" 不在白名单内。允许的路径: ${CFG.allowedRoots.join(", ")}`; } +// ─── Preset 快捷任务库(v1.6)──────────────────────────────────────────────── +// 低算力模型只需传 preset + targetPath(+ 少量附加参数),插件自动生成任务书和 mode。 +// projectPath 在未提供时从 targetPath 自动推导。 + +const PRESETS = { + index: { + mode: "analyze", + required: ["targetPath"], + desc: "列出文件的所有函数/方法索引(行号·名称·功能)", + generate: (p) => + `请读取 ${p.targetPath},列出所有函数/方法的完整索引。\n` + + `每行格式:行号 | 函数名 | 功能描述(≤20字)\n` + + `不修改任何文件。`, + }, + read: { + mode: "analyze", + required: ["targetPath"], + desc: "读取文件完整内容并原文输出", + generate: (p) => + `请读取 ${p.targetPath} 的完整内容并原文输出。不修改任何文件。`, + }, + scan: { + mode: "analyze", + required: ["targetPath"], + desc: "扫描目录结构,列出文件树 + 用途说明", + generate: (p) => + `请扫描 ${p.targetPath}` + + (p.depth ? `(最多 ${p.depth} 层)` : "") + + `,列出目录/文件树形结构,每个文件附一句用途说明。不修改任何文件。`, + }, + bug: { + mode: "analyze", + required: ["targetPath", "error"], + desc: "分析文件中某个错误的根本原因", + generate: (p) => + `请分析 ${p.targetPath} 中以下错误的根本原因:\n` + + `错误信息:${p.error}\n` + + (p.detail ? `附加上下文:${p.detail}\n` : "") + + `只输出分析报告,不修改任何文件。`, + }, + set: { + mode: "write", + required: ["targetPath", "key", "value"], + desc: "修改文件中某个配置项/变量的值", + generate: (p) => + `请修改 ${p.targetPath},将 ${p.key} 的值改为 ${p.value}。\n` + + `约束:只改这一处,禁止修改其他内容或其他文件。\n` + + `验证:修改后用 grep 搜索 "${p.key}" 并在报告中附输出。`, + }, + append: { + mode: "write", + required: ["targetPath", "content"], + desc: "在文件末尾(或指定位置)追加内容", + generate: (p) => + `请在 ${p.targetPath} 的${p.position || "末尾"}追加以下内容:\n` + + `${p.content}\n` + + `约束:只追加,禁止修改已有内容,禁止操作其他文件。\n` + + `验证:追加后读取文件末尾 20 行并在报告中附输出。`, + }, + create: { + mode: "write", + required: ["targetPath", "what"], + desc: "创建或覆写一个文件", + generate: (p) => + `请在 ${p.targetPath} 创建(或覆写)文件,内容要求如下:\n` + + `${p.what}\n` + + `约束:只操作这一个文件,禁止修改其他文件。\n` + + `验证:写入完成后读取文件前 30 行并在报告中附输出。`, + }, +}; + +/** + * 处理 preset 快捷参数。 + * 成功返回新的 input 对象(task/mode/projectPath 已填充); + * 验证失败返回 {status:"error"} 对象; + * 未传 preset 返回 null。 + */ +function applyPreset(input) { + const preset = (input.preset || "").trim().toLowerCase(); + if (!preset) return null; + + const def = PRESETS[preset]; + if (!def) { + const list = Object.entries(PRESETS) + .map(([k, v]) => ` ${k}:${v.desc}`) + .join("\n"); + return { status: "error", error: `未知预设 "${input.preset}"。可用预设:\n${list}` }; + } + + const missing = def.required.filter(k => !input[k]); + if (missing.length > 0) { + return { + status: "error", + error: `预设 "${preset}" 缺少必填参数:${missing.join(", ")}。` + + `\n该预设说明:${def.desc}` + + `\n必填:${def.required.join(", ")}`, + }; + } + + // projectPath 未提供时从 targetPath 自动推导 + let projectPath = input.projectPath; + if (!projectPath && input.targetPath) { + try { + const stat = fs.statSync(input.targetPath); + projectPath = stat.isDirectory() ? input.targetPath : path.dirname(input.targetPath); + } catch { + projectPath = path.dirname(input.targetPath); + } + } + + return { + ...input, + task: def.generate(input), + mode: input.mode || def.mode, + projectPath: projectPath || input.projectPath, + }; +} + +// ─── 报告规范尾部(三种模式共用)───────────────────────────────────────────── +// v1.5 核心:强制 opencode 在报告末尾输出固定格式锚点 +// buildResult 优先提取【执行结果摘要】,VCP AI 无需重读全文 + +const REPORT_FOOTER_ANALYZE = ` + +【报告输出规范 - 必须严格遵守,这是最后输出的内容】 +① 报告正文用 ▍01 · 标题 / ▍02 · 标题 格式分节,每节标题清晰 +② 发现关键风险/坑点/异常时用 ⚠️ 明显标出 +③ 结论基于推断而非直接读取时,必须注明:「此处基于推断,未直读源文件」 +④ 报告最后必须输出以下两行(格式固定,不得省略): +【读取文件清单】已读:<逗号分隔的文件路径列表> | 跳过/未读:<文件及原因,无则写"无"> +【执行结果摘要】<60字以内一句话:做了什么 · 发现了什么 · 结论是什么>`; + +const REPORT_FOOTER_PATCH = ` + +【报告输出规范 - 必须严格遵守,这是最后输出的内容】 +① 每个 diff 块前说明:修改原因 + 影响范围 +② diff 格式:标准 unified diff,上下文保留 3 行,行号必须准确 +③ 若某处修改有风险,用 ⚠️ 标注并说明原因 +④ 报告最后必须输出以下三行(格式固定,不得省略): +【读取文件清单】已读:<文件列表> | 跳过:<文件及原因,无则写"无"> +【变更摘要】共 N 处修改 | 涉及文件:<列表> | 风险点:<若有则列出,无则写"无"> +【执行结果摘要】<60字以内一句话:生成了什么补丁 · 解决了什么问题 · 是否有风险>`; + +const REPORT_FOOTER_WRITE = ` + +【报告输出规范 - 必须严格遵守,这是最后输出的内容】 +① 每次修改文件前说明:修改哪个文件、改了什么、为什么 +② 修改完成后必须读取文件确认写入成功(ls -la 或 cat 关键行) +③ 发现与预期不符时立即停止并说明,不要强行继续 +④ 报告最后必须输出以下三行(格式固定,不得省略): +【读取文件清单】已读:<列表> | 已修改:<列表> | 已新增:<列表> | 已删除:<列表,无则写"无"> +【变更摘要】<逐文件一行描述:路径 → 做了什么变更> +【执行结果摘要】<60字以内一句话:改了什么 · 验证结果如何 · 是否完全成功>`; + // ─── 安全前缀 ───────────────────────────────────────────────────────────────── -const PREFIX_ANALYZE = `【VCP AICodeWorker - 安全约束,必须严格遵守】 +const PREFIX_ANALYZE = `【VCP AICodeWorker - analyze 模式,安全约束必须严格遵守】 你作为只读代码分析 Worker 执行此任务: -- 只允许读取和分析文件 -- 禁止修改、删除、移动、创建任何文件 -- 禁止安装依赖(npm install / pip install 等) -- 禁止重启或停止服务 +- 只允许读取和分析文件,禁止修改、删除、移动、创建任何文件 +- 禁止安装依赖(npm install / pip install 等),禁止重启或停止服务 - 如需提出修改建议,以 diff/patch 格式输出,不得直接落盘 - 禁止在输出中包含 API Key、密码、Token 等敏感信息 【任务内容】 `; -const PREFIX_PATCH = `【VCP AICodeWorker - 安全约束,必须严格遵守】 +const PREFIX_PATCH = `【VCP AICodeWorker - patch 模式,安全约束必须严格遵守】 你作为 patch 生成 Worker 执行此任务: - 可以读取文件进行分析 - 必须以 unified diff 格式输出修改建议,每处修改单独一个 diff 块 -- 禁止直接写入、修改、删除任何文件 -- 禁止安装依赖、重启服务 +- 禁止直接写入、修改、删除任何文件,禁止安装依赖、重启服务 - 禁止在输出中包含敏感信息 -- 输出结尾附上:「已生成 N 处修改建议,等待审查确认后落盘。」 【任务内容】 `; -const PREFIX_WRITE = `【VCP AICodeWorker - write 模式,必须严格遵守以下约束】 +const PREFIX_WRITE = `【VCP AICodeWorker - write 模式,安全约束必须严格遵守】 你作为代码修改 Worker 执行此任务: - 可以读取文件进行分析 - 可以修改/新增文件,但只能操作 task 中明确指定或直接相关的文件 - 禁止删除文件(除非 task 明确要求删除且说明原因) - 禁止修改配置文件(*.env, config.env, .env.* 等) -- 禁止安装依赖(npm install / pip install 等) -- 禁止重启或停止任何服务 +- 禁止安装依赖(npm install / pip install 等),禁止重启或停止任何服务 - 禁止在输出或文件内容中写入 API Key、密码、Token 等敏感信息 -- 每次修改文件前先说明:修改哪个文件、改了什么、为什么 -- 全部完成后输出变更摘要:列出所有被修改/新增的文件路径及变更简述 【任务内容】 `; +// ─── 任务书预检 ─────────────────────────────────────────────────────────────── + +const VAGUE_VERBS = /看一下|处理一下|优化一下|整理一下|随便|帮我看看|感觉|好像|试试|弄一下|搞一下|清理一下(?!.{0,30}\/)/; +const HAS_ABS_PATH = /\/[a-zA-Z0-9_一-龥]/; +const HAS_CONSTRAINT = /禁止|只改|不要|仅|只有|排除|不能|不得|不允许/; +const HAS_VERIFY = /验证|ls |ls$|cat |check|确认|ENOENT|\$\?/; +const DANGER_OPS = /\brm\b|删除|清空|移动|\bmv\b|truncate|unlink/; + +function preflightCheck(task, mode) { + const warnings = []; + if (VAGUE_VERBS.test(task)) + warnings.push({ level: "warn", message: "任务描述含模糊动词(看一下/处理一下等),opencode 可能偏离意图;建议改为明确动作动词。" }); + if ((mode === "write" || mode === "patch") && !HAS_ABS_PATH.test(task)) + warnings.push({ level: "error", message: "write/patch 模式未检测到绝对路径(/开头),建议改用绝对路径防止工作目录歧义。" }); + if (mode === "write" && !HAS_CONSTRAINT.test(task)) + warnings.push({ level: "warn", message: "write 模式未包含操作约束(禁止/只改/不要等),opencode 可能顺手修改无关文件。" }); + if (mode === "write" && DANGER_OPS.test(task) && !HAS_VERIFY.test(task)) + warnings.push({ level: "error", message: "任务含删除/移动操作但未要求验证步骤,建议加:'操作前后必须 ls -la 验证并在报告中附输出'。" }); + return warnings; +} + +// ─── 大文件预检 ─────────────────────────────────────────────────────────────── + +const FILE_PATH_RE = /(?:^|[\s"'`((])(\/[^\s"'`)\n)]{3,})/gm; + +function checkFileSizes(task) { + const warnings = []; + const seen = new Set(); + let m; + FILE_PATH_RE.lastIndex = 0; + while ((m = FILE_PATH_RE.exec(task)) !== null) { + const fp = m[1].replace(/[,。、))】]+$/, ""); + if (seen.has(fp)) continue; + seen.add(fp); + const isAllowed = CFG.allowedRoots.some(r => fp.startsWith(path.resolve(r))); + if (!isAllowed) continue; + try { + const stat = fs.statSync(fp); + if (!stat.isFile()) continue; + const kb = stat.size / 1024; + if (kb > CFG.fileSizeWarnKB) { + warnings.push({ + level: "warn", + message: `文件 ${fp} 约 ${Math.round(kb)}KB(超过 ${CFG.fileSizeWarnKB}KB 阈值),opencode 全量读取可能超时或质量下降;建议缩小范围(指定函数名/行号,或先 grep 过滤)。` + }); + } + } catch {} + } + return warnings; +} + +// ─── 危险操作自动补丁 ───────────────────────────────────────────────────────── + +const DANGER_VERIFY_PATCH = ` + +【AICodeWorker 安全补丁 - 自动注入】 +检测到删除/移动操作,强制执行三步验证协议: +① 操作前:ls -la <目标路径> 确认目标存在 +② 执行操作 +③ 操作后:ls -la <目标路径> 验证结果(删除则确认 ENOENT/No such file) +最终报告必须包含每步的实际命令输出,不允许只写"已完成"。`; + +// ─── 任务包装(注入前缀 + 项目上下文 + 报告规范尾部)──────────────────────── + function wrapTask(task, mode) { - if (mode === "patch") return PREFIX_PATCH + task; - if (mode === "write") return PREFIX_WRITE + task; - return PREFIX_ANALYZE + task; + const ctx = CFG.projectContext + ? `\n【项目上下文 - 自动注入,供 Worker 参考】\n${CFG.projectContext}\n\n` + : ""; + if (mode === "patch") { + return PREFIX_PATCH + ctx + task + REPORT_FOOTER_PATCH; + } + if (mode === "write") { + const needsPatch = DANGER_OPS.test(task) && !HAS_VERIFY.test(task); + return PREFIX_WRITE + ctx + task + (needsPatch ? DANGER_VERIFY_PATCH : "") + REPORT_FOOTER_WRITE; + } + return PREFIX_ANALYZE + ctx + task + REPORT_FOOTER_ANALYZE; } -// ─── Commands ───────────────────────────────────────────────────────────────── +// ─── 结果构建 ───────────────────────────────────────────────────────────────── +// v1.5:新增 fileReadList 字段;summary 优先提取【执行结果摘要】固定锚点 -async function cmdCapabilities() { - const ocOk = await new Promise(resolve => { +function buildResult(jobId, meta) { + const p = jobPaths(jobId); + + let output = ""; + if (fs.existsSync(p.output)) { + const raw = fs.readFileSync(p.output, "utf8"); + const masked = redact(raw); + output = masked.length > 50000 + ? "[输出已截断,仅显示最后 50000 字符]\n" + masked.slice(-50000) + : masked; + } + + let logSummary = ""; + if (fs.existsSync(p.log)) { + const rawLog = fs.readFileSync(p.log, "utf8"); + const ml = redact(rawLog); + logSummary = ml.length > 5000 ? "[日志已截断]\n" + ml.slice(-5000) : ml; + } + + // 提取【读取文件清单】→ fileReadList 字段,让 VCP AI 知道 opencode 读了哪些文件 + let fileReadList = ""; + if (output) { + const frm = output.match(/【读取文件清单】[^\n]*/); + if (frm) fileReadList = frm[0].trim(); + } + + // 摘要提取优先级:① summaryHint → ② 【执行结果摘要】锚点 → ③ 变更摘要等关键词 → ④ 尾部截取 + let summary = ""; + if (output) { + const hint = meta && meta.summaryHint; + if (hint) { + const escaped = hint.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + // 非贪婪匹配,遇到空行、【新节】或字符串结尾即停,避免截入无关内容 + const hm = output.match(new RegExp(escaped + "[\\s\\S]{0,250}?(?=\\n{2,}|【|$)", "i")); + if (hm) summary = hm[0].slice(0, 220).trim(); + } + if (!summary) { + // 优先提取固定锚点(v1.5 报告规范强制输出) + const anchor = output.match(/【执行结果摘要】[^\n]{1,200}/); + if (anchor) summary = anchor[0].trim(); + } + if (!summary) { + const summaryMatch = output.match(/(?:变更摘要|执行结果|完成|总结|验证结果)[\s\S]{0,800}/i); + summary = summaryMatch + ? summaryMatch[0].slice(0, 400).trim() + : output.replace(/^[\s\S]*?\n\n/, "").slice(-300).trim(); + } + } + + return { + status: "success", + jobId, + state: meta.state, + exitCode: meta.exitCode, + startedAt: meta.startedAt, + completedAt: meta.completedAt, + projectPath: meta.projectPath, + mode: meta.mode, + fileReadList, // v1.5 新增:opencode 读了哪些文件 + summary, // 优先锚点提取,比 v1.4 更准确 + output, + logSummary, + outputFile: p.output, + logFile: p.log, + patchFile: fs.existsSync(p.patch) ? p.patch : null, + }; +} + +function checkAndMarkDead(meta, jobId, source) { + if (meta.state === "running" && meta.pid && !isProcessRunning(meta.pid)) { + meta.state = "failed"; + meta.completedAt = new Date().toISOString(); + meta.exitReason = `runner 进程意外退出(${source} 检测)`; + saveMeta(jobId, meta); + } + return meta; +} + +// ─── 探活缓存 ───────────────────────────────────────────────────────────────── + +async function checkOcVersion() { + if (_ocVersionCache && (Date.now() - _ocVersionCache.ts) < 300000) + return _ocVersionCache; + const result = await new Promise(resolve => { const p = spawn(CFG.opencodeBin, ["--version"], { env: process.env, stdio: ["ignore", "pipe", "ignore"] }); @@ -169,6 +485,14 @@ async function cmdCapabilities() { p.on("close", code => resolve({ ok: code === 0, ver: ver.trim() })); p.on("error", () => resolve({ ok: false, ver: "" })); }); + _ocVersionCache = { ...result, ts: Date.now() }; + return _ocVersionCache; +} + +// ─── Commands ───────────────────────────────────────────────────────────────── + +async function cmdCapabilities() { + const ocOk = await checkOcVersion(); return { status: "success", workers: [ @@ -183,20 +507,24 @@ async function cmdCapabilities() { ? "auto-approve 已启用,opencode 不会等待权限提示" : "auto-approve 未启用,若 opencode 出现权限提示可能阻塞直到超时" }, - { - name: "mimocode", - available: false, - note: "adapter 预留,暂未实现" - } + { name: "mimocode", available: false, note: "adapter 预留,暂未实现" } ] }; } async function cmdRun(input) { + // v1.6: preset 快捷方式 — 自动生成 task / mode / projectPath + if (input.preset) { + const presetResult = applyPreset(input); + if (presetResult && presetResult.status === "error") return presetResult; + if (presetResult) input = presetResult; + } + const { worker = "opencode", projectPath, task, mode = "analyze", - sessionId, attachments = [], timeoutSec } = input; + sessionId, attachments = [], timeoutSec, summaryHint } = input; - if (!task) return { status: "error", error: "task 是必填参数。" }; + if (!task) + return { status: "error", error: "task 是必填参数。若要快速上手可使用 preset 参数,例如:preset=index, targetPath=/path/to/file.js" }; if (task.length > CFG.maxTaskChars) return { status: "error", error: `task 超出最大长度 ${CFG.maxTaskChars} 字符。` }; @@ -208,58 +536,49 @@ async function cmdRun(input) { if (!CFG.enableOpencode) return { status: "error", error: "opencode 已被禁用(ENABLE_OPENCODE=false)。" }; - // 检测 opencode 是否可用 - const ocOk = await new Promise(resolve => { - const p = spawn(CFG.opencodeBin, ["--version"], { - env: process.env, stdio: ["ignore", "pipe", "ignore"] - }); - p.on("close", code => resolve(code === 0)); - p.on("error", () => resolve(false)); - }); - if (!ocOk) + const ocOk = await checkOcVersion(); + if (!ocOk.ok) return { status: "error", error: `找不到 opencode(OPENCODE_BIN=${CFG.opencodeBin}),请确认已安装。` }; ensureJobDirs(); const jobId = generateJobId(); const p = jobPaths(jobId); - // 构造 opencode 参数数组(严格 spawn args,不拼 shell 字符串) const finalTask = wrapTask(task, mode); - const ocArgs = ["run", "--format", "json", finalTask]; - if (sessionId) ocArgs.push("--session", String(sessionId)); + const ocArgs = ["run", "--format", "json"]; + if (CFG.opencodeModel) ocArgs.push("-m", CFG.opencodeModel); + if (sessionId) ocArgs.push("--session", String(sessionId)); for (const f of attachments) { if (typeof f === "string" && f.trim()) ocArgs.push("-f", f.trim()); } - // write 模式自动开启 skip-permissions(opencode 需要此标志才能在非交互模式下写文件) if (mode === "write" || CFG.allowDangerSkip) ocArgs.push("--dangerously-skip-permissions"); + ocArgs.push(finalTask); - // runner 参数写入 args 文件(避免敏感信息出现在进程列表里) const runnerArgs = { - jobId, - jobRoot: CFG.jobRoot, - opencodeBin: CFG.opencodeBin, - opencodeBaseUrl: CFG.opencodeBaseUrl, - // API Key 通过 runner.js 读取 config.env,不在 args 文件中 + jobId, jobRoot: CFG.jobRoot, + opencodeBin: CFG.opencodeBin, opencodeBaseUrl: CFG.opencodeBaseUrl, projectPath: path.resolve(projectPath), ocArgs, - timeoutSec: Number(timeoutSec) || CFG.defaultTimeout, + timeoutSec: Number(timeoutSec) || CFG.defaultTimeout, redactSecrets: CFG.redactSecrets, }; fs.writeFileSync(p.args, JSON.stringify(runnerArgs), "utf8"); - // 初始 meta + const warnings = [...preflightCheck(task, mode), ...checkFileSizes(task)]; + const meta = { jobId, worker, mode, - projectPath: path.resolve(projectPath), - sessionId: sessionId || null, - startedAt: new Date().toISOString(), + projectPath: path.resolve(projectPath), + sessionId: sessionId || null, + summaryHint: summaryHint || null, + startedAt: new Date().toISOString(), state: "running", pid: null, exitCode: null, completedAt: null, + warnings, ...p }; saveMeta(jobId, meta); - // 写 header 到输出文件 fs.writeFileSync(p.output, [ "=== AICodeWorker Job ===", `Job ID : ${jobId}`, @@ -270,96 +589,86 @@ async function cmdRun(input) { "===================" ].join("\n") + "\n\n", "utf8"); - // 后台启动 runner.js(detached + unref,主进程退出后继续运行) const runner = spawn(process.execPath, [path.join(__dirname, "runner.js"), p.args], { - detached: true, - stdio: "ignore", - env: process.env + detached: true, stdio: "ignore", env: process.env }); - meta.pid = runner.pid; saveMeta(jobId, meta); runner.unref(); return { - status: "success", - jobId, - state: "running", - pid: runner.pid, - outputFile: p.output, - logFile: p.log, - patchFile: p.patch, + status: "success", jobId, state: "running", pid: runner.pid, + warnings, outputFile: p.output, logFile: p.log, patchFile: p.patch, message: `任务已提交。使用 query 命令查询进度:command=query, jobId=${jobId}` }; } +async function cmdRunAndWait(input) { + const runResult = await cmdRun(input); + if (runResult.status === "error") return runResult; + const { jobId } = runResult; + + for (const sec of BACKOFF_RUN_WAIT) { + await sleep(sec * 1000); + let meta = readMeta(jobId); + if (!meta) return { status: "error", error: `Job "${jobId}" 元数据丢失。` }; + meta = checkAndMarkDead(meta, jobId, "run_and_wait"); + if (meta.state !== "running") { + const result = buildResult(jobId, meta); + result.warnings = meta.warnings || []; + return result; + } + } + + const meta = readMeta(jobId); + return { + status: "success", jobId, state: "running", + warnings: meta?.warnings || [], + startedAt: meta?.startedAt, suggestedWaitSec: 0, + hint: "任务超过 7 分钟仍在运行,请立即调用一次 query(query 内部同样会等待约 7 分钟,无需频繁调用)" + }; +} + async function cmdQuery(input) { const { jobId } = input; if (!jobId) return { status: "error", error: "jobId 是必填参数。" }; - const meta = readMeta(jobId); + let meta = readMeta(jobId); if (!meta) return { status: "error", error: `Job "${jobId}" 不存在。` }; - - const p = jobPaths(jobId); - - // 进程存活检测(runner.js 退出 = job 完成) - if (meta.state === "running" && meta.pid && !isProcessRunning(meta.pid)) { - meta.state = "completed"; - meta.completedAt = meta.completedAt || new Date().toISOString(); - saveMeta(jobId, meta); + if (meta.state !== "running") return buildResult(jobId, meta); + + for (const sec of BACKOFF_QUERY) { + await sleep(sec * 1000); + meta = readMeta(jobId); + if (!meta) break; + meta = checkAndMarkDead(meta, jobId, "query"); + if (meta.state !== "running") break; } - - // 读输出(截断超大文件) - let output = ""; - if (fs.existsSync(p.output)) { - const raw = fs.readFileSync(p.output, "utf8"); - const masked = redact(raw); - output = masked.length > 50000 - ? "[输出已截断,仅显示最后 50000 字符]\n" + masked.slice(-50000) - : masked; - } - - let logSummary = ""; - if (fs.existsSync(p.log)) { - const rawLog = fs.readFileSync(p.log, "utf8"); - const ml = redact(rawLog); - logSummary = ml.length > 5000 ? "[日志已截断]\n" + ml.slice(-5000) : ml; + meta = readMeta(jobId) || meta; + + if (meta.state === "running") { + return { + status: "success", jobId, state: "running", + warnings: meta.warnings || [], + startedAt: meta.startedAt, suggestedWaitSec: 0, + hint: "任务仍在运行,请再调用一次 query(query 会自动内部等待,无需频繁调用)" + }; } - - return { - status: "success", - jobId, - state: meta.state, - exitCode: meta.exitCode, - startedAt: meta.startedAt, - completedAt: meta.completedAt, - projectPath: meta.projectPath, - mode: meta.mode, - output, - logSummary, - outputFile: p.output, - logFile: p.log, - patchFile: fs.existsSync(p.patch) ? p.patch : null, - }; + return buildResult(jobId, meta); } async function cmdListJobs(input) { ensureJobDirs(); const metaDir = path.join(CFG.jobRoot, "meta"); const limit = Math.min(parseInt(input.limit || "10", 10), 50); - const files = fs.readdirSync(metaDir) .filter(f => f.endsWith(".json") && !f.endsWith(".args.json")) .sort().reverse().slice(0, limit); - const jobs = []; for (const file of files) { try { - const m = JSON.parse(fs.readFileSync(path.join(metaDir, file), "utf8")); - if (m.state === "running" && m.pid && !isProcessRunning(m.pid)) { - m.state = "completed"; - saveMeta(m.jobId, m); - } + let m = JSON.parse(fs.readFileSync(path.join(metaDir, file), "utf8")); + m = checkAndMarkDead(m, m.jobId, "listJobs"); jobs.push({ jobId: m.jobId, state: m.state, worker: m.worker, mode: m.mode, projectPath: m.projectPath, @@ -373,21 +682,18 @@ async function cmdListJobs(input) { async function cmdCancel(input) { const { jobId } = input; if (!jobId) return { status: "error", error: "jobId 是必填参数。" }; - const meta = readMeta(jobId); if (!meta) return { status: "error", error: `Job "${jobId}" 不存在。` }; if (meta.state !== "running") return { status: "error", error: `Job "${jobId}" 状态为 "${meta.state}",不是运行中。` }; if (!meta.pid) return { status: "error", error: `Job "${jobId}" 无 PID 记录,无法取消。` }; - try { process.kill(Number(meta.pid), "SIGTERM"); meta.state = "cancelled"; meta.completedAt = new Date().toISOString(); saveMeta(jobId, meta); - const p = jobPaths(jobId); - try { fs.appendFileSync(p.output, `\n=== 任务已手动取消 (${meta.completedAt}) ===\n`); } catch {} + try { fs.appendFileSync(jobPaths(jobId).output, `\n=== 任务已手动取消 (${meta.completedAt}) ===\n`); } catch {} return { status: "success", jobId, message: `Job "${jobId}" 已发送 SIGTERM。` }; } catch (err) { return { status: "error", error: `终止 PID ${meta.pid} 失败: ${err.message}` }; @@ -400,7 +706,6 @@ async function main() { let raw = ""; process.stdin.setEncoding("utf8"); for await (const chunk of process.stdin) raw += chunk; - let input; try { input = JSON.parse(raw.replace(/^/, "")); @@ -408,27 +713,22 @@ async function main() { process.stdout.write(JSON.stringify({ status: "error", error: "stdin 不是合法 JSON。" })); return; } - const cmd = (input.command || "").trim().toLowerCase(); let result; try { switch (cmd) { - case "capabilities": result = await cmdCapabilities(); break; - case "run": result = await cmdRun(input); break; - case "query": result = await cmdQuery(input); break; - case "listjobs": result = await cmdListJobs(input); break; - case "cancel": result = await cmdCancel(input); break; + case "capabilities": result = await cmdCapabilities(); break; + case "run": result = await cmdRun(input); break; + case "run_and_wait": result = await cmdRunAndWait(input); break; + case "query": result = await cmdQuery(input); break; + case "listjobs": result = await cmdListJobs(input); break; + case "cancel": result = await cmdCancel(input); break; default: - result = { - status: "error", - error: `未知命令 "${cmd}"。支持: capabilities, run, query, listJobs, cancel` - }; + result = { status: "error", error: `未知命令 "${cmd}"。支持: capabilities, run, run_and_wait, query, listJobs, cancel` }; } } catch (err) { result = { status: "error", error: `插件内部错误: ${err.message}` }; } - - // VCP 协议要求: 成功 → {status:"success", result:{...}}, 错误 → {status:"error", error:"..."} if (result.status === "error") { process.stdout.write(JSON.stringify(result)); } else { diff --git a/Plugin/AICodeWorker/README.md b/Plugin/AICodeWorker/README.md index 741319553..27e8e1691 100644 --- a/Plugin/AICodeWorker/README.md +++ b/Plugin/AICodeWorker/README.md @@ -1,3 +1,4 @@ + # AICodeWorker - AI 代码工程 Worker 让 VCP Agent 可以安全调度服务器本地的 [opencode](https://opencode.ai),作为下游代码分析、patch 生成、文件修改 Worker。支持三种模式,采用"同步外壳 + 异步 runner"架构,主插件立即返回 jobId,实际工作在后台 runner.js 中执行。 @@ -57,7 +58,6 @@ task:「始」请分析 Plugin/AICodeWorker/AICodeWorker.js 的整体结构, mode:「始」analyze「末」 <<<[END_TOOL_REQUEST]>>> ``` - ### 2. 查询结果(query) ```text diff --git a/Plugin/AICodeWorker/config.env.example b/Plugin/AICodeWorker/config.env.example index 24b0a56b6..896e469c2 100644 --- a/Plugin/AICodeWorker/config.env.example +++ b/Plugin/AICodeWorker/config.env.example @@ -25,7 +25,7 @@ OPENCODE_MODEL= # ── 安全配置 ──────────────────────────────────────── # 允许操作的项目根目录白名单(逗号分隔),projectPath 必须在其中 -ALLOWED_PROJECT_ROOTS=/app/VCPToolBox_new,/app/ZhongZhuan,/app/claud +ALLOWED_PROJECT_ROOTS=/app/VCPToolBox_new,/app/ZhongZhuan,/app/claud,/app,/home/kasm_user # 任务输出目录(output/logs/patches/meta 均在此目录下) JOB_ROOT=/app/VCPToolBox_new/Plugin/AICodeWorker/jobs diff --git a/Plugin/AICodeWorker/plugin-manifest.json b/Plugin/AICodeWorker/plugin-manifest.json index a5bc3ed12..a05d5993e 100644 --- a/Plugin/AICodeWorker/plugin-manifest.json +++ b/Plugin/AICodeWorker/plugin-manifest.json @@ -1,9 +1,9 @@ { "manifestVersion": "1.0.0", "name": "AICodeWorker", - "version": "1.1.0", + "version": "1.6.0", "displayName": "AI 代码工程 Worker", - "description": "让 VCP Agent 可以安全调度服务器本地的 opencode,作为下游代码分析、patch 生成、文件修改 Worker。", + "description": "AICodeWorker v1.5 — 规范化报告输出:三模式强制输出【读取文件清单】+【执行结果摘要】锚点,buildResult 精准提取,新增 fileReadList 字段。省 token 与工作质量双保证。", "author": "Local", "pluginType": "synchronous", "entryPoint": { @@ -12,25 +12,66 @@ }, "communication": { "protocol": "stdio", - "timeout": 30000 + "timeout": 540000 }, "configSchema": { - "ENABLE_OPENCODE": { "type": "boolean", "default": true }, - "OPENCODE_BIN": { "type": "string", "default": "opencode" }, - "OPENCODE_BASE_URL": { "type": "string", "default": "" }, - "OPENCODE_API_KEY": { "type": "string", "default": "" }, - "ALLOWED_PROJECT_ROOTS": { "type": "string", "default": "/app/VCPToolBox_new" }, - "DEFAULT_TIMEOUT_SEC": { "type": "integer", "default": 600 }, - "MAX_TASK_CHARS": { "type": "integer", "default": 20000 }, - "ALLOW_DANGEROUS_SKIP_PERMISSIONS": { "type": "boolean", "default": false }, - "REDACT_SECRETS": { "type": "boolean", "default": true } + "ENABLE_OPENCODE": { + "type": "boolean", + "default": true + }, + "OPENCODE_BIN": { + "type": "string", + "default": "opencode" + }, + "OPENCODE_BASE_URL": { + "type": "string", + "default": "" + }, + "OPENCODE_API_KEY": { + "type": "string", + "default": "" + }, + "OPENCODE_MODEL": { + "type": "string", + "default": "" + }, + "ALLOWED_PROJECT_ROOTS": { + "type": "string", + "default": "/app/VCPToolBox_new" + }, + "DEFAULT_TIMEOUT_SEC": { + "type": "integer", + "default": 600 + }, + "MAX_TASK_CHARS": { + "type": "integer", + "default": 20000 + }, + "ALLOW_DANGEROUS_SKIP_PERMISSIONS": { + "type": "boolean", + "default": false + }, + "REDACT_SECRETS": { + "type": "boolean", + "default": true + }, + "PROJECT_CONTEXT": { + "type": "string", + "default": "", + "description": "项目上下文,自动注入每条任务书,省去 VCP AI 重复描述项目背景" + }, + "FILE_SIZE_WARN_KB": { + "type": "number", + "default": 200, + "description": "大文件预检阈值(KB),超过时 warnings 提醒缩小范围" + } }, "capabilities": { "invocationCommands": [ { "commandIdentifier": "AICodeWorker", - "description": "═══════════════════════════════════════════\n【AICodeWorker 工具使用手册 v1.1】\n═══════════════════════════════════════════\n\n这个工具让你可以把代码任务委托给服务器本地的 opencode(AI 代码引擎)执行。\n你是调度者和审查者,opencode 是执行层。\n\n▌支持的三种工作模式\n\n analyze(默认):只读分析。opencode 读取代码,输出分析报告,不修改任何文件。\n patch:生成修改建议。opencode 以 unified diff 格式输出修改方案,不落盘。你审查后用 ServerFileOperator 应用。\n write:直接修改文件。opencode 读取并修改/新增文件,完成后输出变更摘要。用于你已明确要求某项修改的场景。\n\n▌核心概念:任务是异步的\n 第一步:run(提交任务)→ 立刻拿到 jobId\n 第二步:query(查询结果)→ 反复查直到 state = completed\n\n⚠️ 铁律:\n - run 后必须 query,不能跳过\n - state = running 时等几秒再查\n - patch 模式输出的 diff 只是建议,必须你确认后再落盘\n - write 模式 opencode 会直接改文件,提交前确认 task 描述准确\n - 不要在 task 里写 API Key 或密码\n\n═══════════════════════════════════════════\n【模式选择指南】\n═══════════════════════════════════════════\n\n 用 analyze:我想了解这段代码是怎么工作的 / 有没有 bug / 代码质量怎么样\n 用 patch:我知道要改什么,先让 opencode 出方案,我审查后再决定\n 用 write:我已经明确要加/改某个功能,直接让 opencode 实现\n\n═══════════════════════════════════════════\n【完整工作流示例】\n═══════════════════════════════════════════\n\n─── 示例A:analyze(只读分析)───\n\n第一步:提交\n<<<[TOOL_REQUEST]>>>\ntool_name:「始」AICodeWorker「末」,\ncommand:「始」run「末」,\nworker:「始」opencode「末」,\nprojectPath:「始」/app/VCPToolBox_new「末」,\ntask:「始」请分析 Plugin/AICodeWorker/AICodeWorker.js 文件的整体结构,说明主要函数的作用,不要修改任何文件。「末」,\nmode:「始」analyze「末」\n<<<[END_TOOL_REQUEST]>>>\n\n第二步:查询(等 state=completed)\n<<<[TOOL_REQUEST]>>>\ntool_name:「始」AICodeWorker「末」,\ncommand:「始」query「末」,\njobId:「始」job_20260620_001910_172286「末」\n<<<[END_TOOL_REQUEST]>>>\n\n─── 示例B:patch(生成 diff,人工审查后落盘)───\n\n第一步:提交\n<<<[TOOL_REQUEST]>>>\ntool_name:「始」AICodeWorker「末」,\ncommand:「始」run「末」,\nworker:「始」opencode「末」,\nprojectPath:「始」/app/VCPToolBox_new「末」,\ntask:「始」请检查 Plugin/AICodeWorker/runner.js 的错误处理逻辑,如发现遗漏,以 unified diff 格式输出修改建议,不要直接修改文件。「末」,\nmode:「始」patch「末」\n<<<[END_TOOL_REQUEST]>>>\n\n第二步:query 拿到 diff 内容后,展示给用户确认,用户批准后再用 ServerFileOperator 应用 patch。\n\n─── 示例C:write(opencode 直接修改文件)───\n\n⚠️ write 模式会直接改文件,提交前确认 task 描述准确!\n\n第一步:提交\n<<<[TOOL_REQUEST]>>>\ntool_name:「始」AICodeWorker「末」,\ncommand:「始」run「末」,\nworker:「始」opencode「末」,\nprojectPath:「始」/app/VCPToolBox_new「末」,\ntask:「始」请在 Plugin/AICodeWorker/AICodeWorker.js 的 generateJobId 函数中,把 pid 替换为4位随机数,避免暴露进程ID。只修改这一处,其他代码不动。完成后输出变更摘要。「末」,\nmode:「始」write「末」\n<<<[END_TOOL_REQUEST]>>>\n\n第二步:query 拿到变更摘要后,汇报给用户哪些文件被修改了。\n\n═══════════════════════════════════════════\n【五个命令速查】\n═══════════════════════════════════════════\n\n命令1:capabilities(查询可用 Worker)\n无参数。确认 opencode 版本和可用状态。\n<<<[TOOL_REQUEST]>>>\ntool_name:「始」AICodeWorker「末」,\ncommand:「始」capabilities「末」\n<<<[END_TOOL_REQUEST]>>>\n\n命令2:run(提交任务)\n worker 必填 opencode\n projectPath 必填 白名单内的项目根目录(/app/VCPToolBox_new, /app/ZhongZhuan, /app/claud)\n task 必填 任务描述\n mode 选填 analyze(默认)/ patch / write\n timeoutSec 选填 超时秒数,默认 600\n\n命令3:query(查询结果)\n jobId 必填 run 返回的 jobId\n state 含义:running=进行中 / completed=成功 / failed=失败 / timeout=超时 / cancelled=已取消\n<<<[TOOL_REQUEST]>>>\ntool_name:「始」AICodeWorker「末」,\ncommand:「始」query「末」,\njobId:「始」job_20260620_001910_172286「末」\n<<<[END_TOOL_REQUEST]>>>\n\n命令4:listJobs(列出历史任务)\n limit 选填 最多返回条数,默认10\n<<<[TOOL_REQUEST]>>>\ntool_name:「始」AICodeWorker「末」,\ncommand:「始」listJobs「末」,\nlimit:「始」5「末」\n<<<[END_TOOL_REQUEST]>>>\n\n命令5:cancel(取消任务)\n jobId 必填 要取消的任务ID\n<<<[TOOL_REQUEST]>>>\ntool_name:「始」AICodeWorker「末」,\ncommand:「始」cancel「末」,\njobId:「始」job_20260620_001910_172286「末」\n<<<[END_TOOL_REQUEST]>>>\n\n═══════════════════════════════════════════\n【write 模式后处理流程】\n═══════════════════════════════════════════\n\n1. run(mode=write)→ 拿到 jobId\n2. query → 等 state=completed\n3. 读取 output 中的变更摘要\n4. 向用户汇报:修改了哪些文件、改了什么\n5. 如用户要求回滚,使用 ServerFileOperator 恢复(前提:任务前已备份或有 git)\n\n═══════════════════════════════════════════\n【patch 模式后处理流程】\n═══════════════════════════════════════════\n\n1. run(mode=patch)→ 拿到 jobId\n2. query → 等 state=completed\n3. 读取 output 中的 unified diff 内容\n4. 向用户展示 diff,解释每处变更\n5. 用户确认后 → 用 ServerFileOperator 逐文件应用修改\n6. 用户拒绝 → 丢弃,不做任何改动\n\n═══════════════════════════════════════════\n【常见错误处理】\n═══════════════════════════════════════════\n\nprojectPath 不在白名单 → 换成白名单内路径\nstate = running → 等几秒再 query,不要停\nstate = timeout → 下次 run 时加 timeoutSec(如 1200),或拆小任务\noutput 被截断 → 用 ServerFileOperator 读 outputFile 字段的完整文件" + "description": "## AICodeWorker — 让免费 AI 干活,让付费模型做决策\n\n**核心用途**:把耗 Token 的代码读写任务交给本地免费的 opencode 执行,VCP 模型只需下命令和看结果。\n\n---\n\n## 🚀 最快上手:用 preset 参数(低算力模型推荐)\n\n只需填两行,插件自动生成任务书:\n\n```\ncommand: run_and_wait\npreset: [预设名]\ntargetPath: [文件或目录的绝对路径]\nprojectPath: [可选,不填时从 targetPath 自动推导]\n```\n\n### 7 个内置预设速查表\n\n| preset | 说明 | 必填参数 | 可选参数 |\n|--------|------|---------|---------|\n| index | 列出文件所有函数索引(行号·名称·功能) | targetPath | — |\n| read | 读取文件完整内容并原文输出 | targetPath | — |\n| scan | 扫描目录树 + 每个文件用途说明 | targetPath | depth(层数) |\n| bug | 分析某个错误的根本原因 | targetPath, error | detail(补充上下文) |\n| set | 修改文件中某个配置项/变量的值 | targetPath, key, value | — |\n| append | 在文件末尾追加内容 | targetPath, content | position(位置描述) |\n| create | 创建或覆写一个文件 | targetPath, what | — |\n\n### preset 调用示例\n\n**看文件函数索引(index)**:\n```\ncommand:「始」run_and_wait「末」,\npreset:「始」index「末」,\ntargetPath:「始」/app/VCPToolBox_new/Plugin/AICodeWorker/AICodeWorker.js「末」\n```\n\n**读取配置文件(read)**:\n```\ncommand:「始」run_and_wait「末」,\npreset:「始」read「末」,\ntargetPath:「始」/app/VCPToolBox_new/config.env「末」\n```\n\n**扫描目录(scan)**:\n```\ncommand:「始」run_and_wait「末」,\npreset:「始」scan「末」,\ntargetPath:「始」/app/VCPToolBox_new/Plugin「末」,\ndepth:「始」2「末」\n```\n\n**分析错误(bug)**:\n```\ncommand:「始」run_and_wait「末」,\npreset:「始」bug「末」,\ntargetPath:「始」/app/VCPToolBox_new/Plugin/AICodeWorker/AICodeWorker.js「末」,\nerror:「始」TypeError: Cannot read property 'trim' of undefined at line 402「末」\n```\n\n**改配置值(set)**:\n```\ncommand:「始」run_and_wait「末」,\npreset:「始」set「末」,\ntargetPath:「始」/app/VCPToolBox_new/Plugin/AICodeWorker/config.env「末」,\nkey:「始」DEFAULT_TIMEOUT_SEC「末」,\nvalue:「始」900「末」\n```\n\n**追加一行内容(append)**:\n```\ncommand:「始」run_and_wait「末」,\npreset:「始」append「末」,\ntargetPath:「始」/app/VCPToolBox_new/Plugin/AICodeWorker/config.env「末」,\ncontent:「始」NEW_PARAM=hello「末」\n```\n\n**创建新文件(create)**:\n```\ncommand:「始」run_and_wait「末」,\npreset:「始」create「末」,\ntargetPath:「始」/app/VCPToolBox_new/Plugin/AICodeWorker/notes.md「末」,\nwhat:「始」记录 AICodeWorker 版本历史,格式:## vX.Y.Z 换行 - 变更说明「末」\n```\n\n---\n\n## ⚡ 白话触发词对照表\n\n当用户用自然语言说以下话时,选对应 preset:\n\n| 用户说的话 | 选这个 preset | 还需要的参数 |\n|-----------|-------------|------------|\n| \"看看这个文件/函数/代码\" | index | targetPath |\n| \"读一下/把内容给我看\" | read | targetPath |\n| \"扫一下目录/看看有哪些文件\" | scan | targetPath |\n| \"帮我查一下这个报错/错误原因\" | bug | targetPath + error |\n| \"把XXX改成YYY/修改配置\" | set | targetPath + key + value |\n| \"在文件末尾加一行/追加\" | append | targetPath + content |\n| \"帮我创建一个文件/新建\" | create | targetPath + what |\n| \"三方工具/牛马/opencode 去分析\" | 自写 task,mode=analyze | projectPath + task |\n| \"三方工具去改/修复\" | 自写 task,mode=write | projectPath + task |\n\n---\n\n## 📝 进阶:自己写任务书(复杂任务)\n\n当预设满足不了需求时,手写 task 参数。\n\n### 三种模式\n\n| mode | 行为 | 适用场景 |\n|------|------|---------|\n| analyze(默认)| 只读,禁止修改文件 | 理解代码、排查 bug、读取内容 |\n| patch | 输出 diff,不直接落盘 | 需人工审查再决定是否改 |\n| write | 直接修改/新增文件 | 需求明确、已确认要改 |\n\n### 填空模板(复制后替换 [] 内容)\n\n**分析/读取类**:\n```\ncommand:「始」run_and_wait「末」,\nprojectPath:「始」[项目目录绝对路径]「末」,\ntask:「始」请分析 [文件绝对路径] 的 [具体内容],[输出要求],不修改任何文件。「末」,\nmode:「始」analyze「末」\n```\n\n**修改单个值**:\n```\ncommand:「始」run_and_wait「末」,\nprojectPath:「始」[项目目录绝对路径]「末」,\ntask:「始」请修改 [文件绝对路径],将 [目标内容] 从 [旧值] 改为 [新值]。约束:只改这一处,禁止修改其他文件。验证:修改后 grep [关键词] 并在报告中附输出。「末」,\nmode:「始」write「末」\n```\n\n**创建/写入文件**:\n```\ncommand:「始」run_and_wait「末」,\nprojectPath:「始」[项目目录绝对路径]「末」,\ntask:「始」请在 [文件绝对路径] 创建文件,内容要求:[详细描述内容和格式]。约束:只操作这一个文件。验证:写入后读取前30行并在报告中附输出。「末」,\nmode:「始」write「末」\n```\n\n### 任务书六要素\n\n```\nWhat 做什么?动词明确(分析/修改/新增/删除)\nWhere 操作哪个绝对路径?\nDon't 禁止动什么?(禁改其他文件/禁装依赖)\nProve 怎么验证?(ls / cat / grep 看输出)\nReport 报告格式?(附命令输出/输出摘要)\nIf Fail 失败怎么办?(回滚/保留原文件)\n```\n\n---\n\n## 📋 完整参数参考\n\n### run / run_and_wait 参数\n\n| 参数 | 必填 | 说明 |\n|------|------|------|\n| preset | 否 | 预设名,填了可省略 task/projectPath |\n| targetPath | preset时必填 | 目标文件/目录路径 |\n| projectPath | 否* | opencode 工作目录;preset 时可省略 |\n| task | 否* | 任务描述;填了 preset 可省略 |\n| mode | 否 | analyze(默认)/ patch / write |\n| timeoutSec | 否 | 超时秒数,默认 600 |\n| summaryHint | 否 | 指定从哪个关键词后提取摘要 |\n| worker | 否 | 默认 opencode |\n\n### 返回字段\n\n| 字段 | 说明 |\n|------|------|\n| warnings | 任务书预检告警,含 level: error/warn |\n| fileReadList | opencode 实际读取的文件清单 |\n| summary | 从【执行结果摘要】锚点提取的结论(≤60字) |\n| output | opencode 完整输出(超 50KB 自动截断) |\n| outputFile | 完整输出文件路径(截断时用 ServerFileOperator 读) |\n| state | completed / failed / running / timeout / cancelled |\n\n### 其他命令\n\n- query:查询任务进度(参数:jobId)\n- listJobs:列出历史任务(参数:limit,默认10)\n- cancel:取消任务(参数:jobId)\n- capabilities:查看插件能力和 opencode 版本" } ] } -} +} \ No newline at end of file