import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { formatStatus } from "../src/harness/format.ts"; import { HARNESS_MODES, PHASE_ORDER, isKdHarnessMode, isKdPhase, phaseOrderForRun, type KdPhase } from "../src/harness/types.ts"; import { advanceRun, advanceRunIfReady, answerQuestion, createActiveRun, ensurePhaseArtifact, finishActiveRun, listRuns, openBlockingQuestions, readActiveRun, recordActionCommit, recordRunContext, refreshGate, switchActiveRun, updateHarnessMode, updateProductProfile, updatePhaseArtifact, updateRisk, } from "../src/harness/state.ts"; import { readArtifact } from "../src/harness/artifacts.ts"; import { buildControlFrame } from "../src/harness/control-frame.ts"; import { parseArtifactArgs, parseProductArgs, parseRiskArgs, parseStartArgs } from "../src/harness/extension-args.ts"; import { formatRuns } from "../src/harness/extension-format.ts"; import { isPlainAdvanceRequest } from "../src/harness/action-router.ts"; import { executeDeterministicNext } from "../src/harness/demand-scheduler.ts"; import { repairPromptForRun, workflowPromptForRun } from "../src/harness/prompt.ts"; import { recordVerifyResult, type VerifyResultOutcome } from "../src/harness/repair.ts"; import { isSubagentChild } from "../src/harness/delegation.ts"; import { parseStructuredQuestionAnswers } from "../src/harness/question-interaction.ts"; import { appendQuestionEventToArtifact, shouldAutoRecordQuestionAnswer } from "../src/harness/question-artifacts.ts"; import { formatLedgerEvents, readLedgerEvents } from "../src/harness/ledger.ts"; import { recordAutoAnsweredInput, recordInputReceived } from "../src/harness/observation.ts"; import { buildHandoffCapsule } from "../src/harness/handoff-capsule.ts"; import { admitPromptDispatch } from "../src/harness/prompt-dispatch.ts"; import { registerHarnessToolEvents } from "./kingdee-harness-tool-events.ts"; import { registerKingdeeHarnessTools } from "./kingdee-harness-tools.ts"; function shouldStartHarnessFromInput(text: string): boolean { if (!text.trim() || text.trim().startsWith("/")) return false; if (isPlainAdvanceRequest(text)) return false; return true; } function sendWorkflowPrompt(pi: ExtensionAPI, ctx: ExtensionContext, run: NonNullable>, userText: string): void { const committed = commitActionForPrompt(ctx.cwd, run, userText, "command"); const prompt = workflowPromptForRun(ctx.cwd, committed, userText); const dispatch = admitPromptDispatch({ cwd: ctx.cwd, runId: committed.id, phase: committed.phase, userText, }); if (!dispatch.admitted) { if (ctx.hasUI) ctx.ui.notify(dispatch.reason ?? "重复 KCode workflow prompt 已跳过。", "warning"); return; } if (ctx.isIdle()) { pi.sendUserMessage(prompt); return; } pi.sendUserMessage(prompt, { deliverAs: "followUp" }); if (ctx.hasUI) ctx.ui.notify("KCode 工作流消息已排队。", "info"); } function commitActionForPrompt( cwd: string, run: NonNullable>, userText: string, source: "input" | "command" | "tool" | "system", ): NonNullable> { const frame = buildControlFrame(cwd, run, userText); recordActionCommit(cwd, run, { focus: frame.focus, intent: frame.turnInputPolicy.actionRoute.intent, allowed: frame.turnInputPolicy.actionRoute.allowed, reason: frame.turnInputPolicy.actionRoute.reason, requiredAction: frame.turnInputPolicy.actionRoute.requiredAction, source, }); return readActiveRun(cwd) ?? run; } function autoAdvanceCommand( pi: ExtensionAPI, ctx: ExtensionContext, run: NonNullable>, reason: string, ): ReturnType { const result = advanceRunIfReady(ctx.cwd, run); if (result.advanced) { if (ctx.hasUI) ctx.ui.notify(result.message, "info"); sendWorkflowPrompt(pi, ctx, result.run, `${reason}\n继续处理需求:${result.run.goal ?? result.run.id}`); } else if (result.run.gate.passed === false || !result.message.includes("最终阶段")) { if (ctx.hasUI) ctx.ui.notify(`还不能继续:${result.message}`, "warning"); } return result; } async function handleUnifiedKdCommand(pi: ExtensionAPI, args: string, ctx: ExtensionContext): Promise { const trimmed = args.trim(); const [verb = "", ...rest] = trimmed.split(/\s+/); const command = verb.toLowerCase(); const tail = rest.join(" ").trim(); const run = readActiveRun(ctx.cwd); if (!trimmed) { if (run) continueTask(pi, ctx, run, ""); else ctx.ui.notify(formatKdHelp(undefined), "info"); return; } if (command === "status" || command === "s" || command === "状态") { ctx.ui.notify(run ? formatStatus(ctx.cwd, run, { detail: isDetailStatusRequest(tail) }) : formatKdHelp(undefined), "info"); return; } if (command === "help" || command === "h" || command === "?" || command === "帮助") { ctx.ui.notify(formatKdHelp(run), "info"); return; } if (command === "list" || command === "runs" || command === "ls" || command === "列表") { ctx.ui.notify(formatRuns(listRuns(ctx.cwd), run?.id), "info"); return; } if (command === "new" || command === "start" || command === "开始" || command === "新建") { startTaskFromArgs(pi, tail, ctx); return; } if (!run) { startTaskFromArgs(pi, trimmed, ctx); return; } if (command === "log" || command === "ledger" || command === "日志") { const limit = Math.max(1, Math.min(100, Number(tail) || 30)); ctx.ui.notify(formatLedgerEvents(readLedgerEvents(ctx.cwd, run), limit), "info"); return; } if (command === "handoff" || command === "capsule" || command === "交接") { ctx.ui.notify(buildHandoffCapsule({ cwd: ctx.cwd, run, audience: "resume", task: tail || undefined }), "info"); return; } if (command === "doc" || command === "artifact" || command === "文档") { updateTaskArtifact(pi, ctx, run, tail); return; } if (command === "next" || command === "go" || command === "continue" || command === "resume" || command === "继续" || command === "下一步") { continueTask(pi, ctx, run, tail); return; } if (command === "check" || command === "gate" || command === "检查") { const refreshed = refreshGate(ctx.cwd, run); if (refreshed.gate.passed) { ctx.ui.notify("检查通过,继续处理。", "info"); continueTask(pi, ctx, refreshed, ""); } else { ctx.ui.notify(`还不能继续:${refreshed.gate.reason}`, "info"); } return; } if (command === "done" || command === "finish" || command === "结束" || command === "完成") { const finished = finishActiveRun(ctx.cwd, run); ctx.ui.notify(`已结束:${finished.goal ?? finished.id}。下一个需求用 /kd <需求>。`, "info"); return; } if (command === "use" || command === "switch" || command === "切换") { switchTask(ctx, tail); return; } if (command === "mode" || command === "模式") { updateTaskMode(pi, ctx, run, tail); return; } if (command === "product" || command === "产品") { updateTaskProduct(pi, ctx, run, tail); return; } if (command === "risk" || command === "风险") { updateTaskRisk(pi, ctx, run, tail); return; } if (command === "answer" || command === "a" || command === "回答") { answerTaskQuestion(pi, ctx, run, tail); return; } if (command === "verify" || command === "验证") { recordTaskVerifyResult(pi, ctx, run, tail); return; } if (command === "phase" || command === "advance" || command === "阶段") { advanceTask(ctx, run, tail); return; } if (answerCurrentQuestionFromShortcut(pi, ctx, run, trimmed)) { return; } if (applyTaskSettingsShortcut(pi, ctx, run, trimmed)) { return; } startTaskFromArgs(pi, trimmed, ctx); } function startTaskFromArgs(pi: ExtensionAPI, args: string, ctx: ExtensionContext): void { const parsed = parseStartArgs(args); if (parsed.modeError) { ctx.ui.notify(`${parsed.modeError}。有效值:${HARNESS_MODES.join(", ")}`, "error"); return; } if (!parsed.goal) { ctx.ui.notify("直接输入:/kd <需求>。例如:/kd 销售订单保存前校验插件", "error"); return; } const run = createActiveRun(ctx.cwd, parsed.goal, parsed.product, parsed.version, parsed.mode); ctx.ui.notify(`已开始:${parsed.goal}(${run.mode},${run.profile?.product}/${run.profile?.techStack})`, "info"); if (run.profile?.product === "unknown") { ctx.ui.notify("还没识别出金蝶产品。可用 /kd product cangqiong 指定。", "warning"); } const outcome = executeDeterministicNext(ctx.cwd, readActiveRun(ctx.cwd) ?? run, "继续"); for (const notice of outcome.notices) ctx.ui.notify(notice.message, notice.level); sendWorkflowPrompt(pi, ctx, outcome.run, outcome.promptText ?? `继续处理需求:${parsed.goal}`); } function continueTask(pi: ExtensionAPI, ctx: ExtensionContext, run: NonNullable>, note: string): void { const current = readActiveRun(ctx.cwd) ?? run; const userText = note || "继续"; const outcome = executeDeterministicNext(ctx.cwd, current, userText); for (const notice of outcome.notices) ctx.ui.notify(notice.message, notice.level); if (outcome.promptText) { sendWorkflowPrompt(pi, ctx, outcome.run, outcome.promptText); return; } if (outcome.handled) return; const frame = buildControlFrame(ctx.cwd, outcome.run, userText); const message = [ `继续处理需求:${outcome.run.goal ?? outcome.run.id}`, note ? `用户补充:${note}` : undefined, `当前下一步:${frame.nextAction.instruction}`, ].filter(Boolean).join("\n"); sendWorkflowPrompt(pi, ctx, outcome.run, message); } function answerCurrentQuestionFromShortcut(pi: ExtensionAPI, ctx: ExtensionContext, run: NonNullable>, text: string): boolean { const openQuestions = openBlockingQuestions(run); if (openQuestions.length !== 1 || !shouldAutoRecordQuestionAnswer(text)) return false; const answered = answerQuestion(ctx.cwd, run, openQuestions[0].id, text); if (!answered) { ctx.ui.notify(`未能记录 ${openQuestions[0].id} 的答案;请提供具体、可核验的答案。`, "warning"); return true; } appendQuestionEventToArtifact(ctx.cwd, readActiveRun(ctx.cwd) ?? run, [`- 已回答 ${answered.id}:${answered.answer}`]); ctx.ui.notify(`已记录 ${answered.id} 的答案`, "info"); autoAdvanceCommand(pi, ctx, readActiveRun(ctx.cwd) ?? run, `${answered.id} 已回答。`); return true; } function applyTaskSettingsShortcut(pi: ExtensionAPI, ctx: ExtensionContext, run: NonNullable>, args: string): boolean { const parsed = parseStartArgs(args); if (parsed.modeError) { ctx.ui.notify(`${parsed.modeError}。有效值:${HARNESS_MODES.join(", ")}`, "error"); return true; } if (parsed.goal || (!parsed.product && !parsed.mode)) return false; let current = readActiveRun(ctx.cwd) ?? run; let changed = false; if (parsed.product) { current = updateProductProfile(ctx.cwd, current, parsed.product, parsed.version); ctx.ui.notify(`产品:${current.profile?.product}/${current.profile?.techStack}/${current.profile?.language}`, "info"); changed = true; } if (parsed.mode) { const result = updateHarnessMode(ctx.cwd, current, parsed.mode); ctx.ui.notify(result.message, result.changed ? "info" : "warning"); current = result.run; changed = changed || result.changed; } if (changed) autoAdvanceCommand(pi, ctx, current, "需求配置已更新。"); return true; } function switchTask(ctx: ExtensionContext, id: string): void { if (!id) { ctx.ui.notify("用法:/kd use 。先用 /kd list 查看 id。", "error"); return; } const run = switchActiveRun(ctx.cwd, id); if (!run) { ctx.ui.notify(`没找到需求:${id}。用 /kd list 查看列表。`, "error"); return; } ctx.ui.notify(`已切换到:${run.goal ?? run.id}(${run.phase})`, "info"); } function updateTaskMode(pi: ExtensionAPI, ctx: ExtensionContext, run: NonNullable>, mode: string): void { if (!isKdHarnessMode(mode)) { ctx.ui.notify("用法:/kd mode lite|standard|strict", "error"); return; } const result = updateHarnessMode(ctx.cwd, run, mode); ctx.ui.notify(result.message, result.changed ? "info" : "warning"); if (result.changed) autoAdvanceCommand(pi, ctx, result.run, "流程强度已更新。"); } function updateTaskProduct(pi: ExtensionAPI, ctx: ExtensionContext, run: NonNullable>, args: string): void { const parsed = parseProductArgs(args); if (!parsed) { ctx.ui.notify("用法:/kd product cangqiong。可选:--version 版本", "error"); return; } const updated = updateProductProfile(ctx.cwd, run, parsed.product, parsed.version); ctx.ui.notify(`产品:${updated.profile?.product}/${updated.profile?.techStack}/${updated.profile?.language}`, "info"); autoAdvanceCommand(pi, ctx, updated, "产品已更新。"); } function updateTaskRisk(pi: ExtensionAPI, ctx: ExtensionContext, run: NonNullable>, args: string): void { const risk = parseRiskArgs(args); if (!risk) { ctx.ui.notify("用法:/kd risk low|medium|high <原因>", "error"); return; } const updated = updateRisk(ctx.cwd, run, risk.risk, risk.reason); ctx.ui.notify(`风险:${updated.riskAssessment?.level}(${updated.riskAssessment?.reason})`, "info"); autoAdvanceCommand(pi, ctx, updated, "风险已更新。"); } function advanceTask(ctx: ExtensionContext, run: NonNullable>, phaseText: string): void { let requested: KdPhase | undefined; if (phaseText) { if (!isKdPhase(phaseText)) { ctx.ui.notify(`未知阶段 "${phaseText}"。有效阶段:${PHASE_ORDER.join(", ")}`, "error"); return; } requested = phaseText; } if (requested && !phaseOrderForRun(run).includes(requested)) { ctx.ui.notify(`当前流程不包含 ${requested}。可选阶段:${phaseOrderForRun(run).join(", ")}`, "error"); return; } const result = advanceRun(ctx.cwd, run, requested); ctx.ui.notify(result.message, result.run.gate.passed ? "info" : "warning"); } function answerTaskQuestion(pi: ExtensionAPI, ctx: ExtensionContext, run: NonNullable>, args: string): void { const [id, ...answerParts] = args.trim().split(/\s+/); const answer = answerParts.join(" ").trim(); if (!id || !answer) { ctx.ui.notify("用法:/kd answer Q-001 <答案>", "error"); return; } const existingQuestion = (run.questions ?? []).find((question) => question.id === id); if (existingQuestion?.status === "answered") { ctx.ui.notify(`${id} 已回答。如需更正事实,用 /kd product、/kd risk 或 kd_answer_user action=revise。`, "warning"); return; } const answered = answerQuestion(ctx.cwd, run, id, answer); if (!answered) { ctx.ui.notify(existingQuestion ? `未能记录 ${id} 的答案;答案不能为空、占位内容或单独确认/否定。` : `未找到问题:${id}`, "error"); return; } appendQuestionEventToArtifact(ctx.cwd, run, [`- 已回答 ${answered.id}:${answered.answer}`]); ctx.ui.notify(`已记录 ${answered.id} 的答案`, "info"); autoAdvanceCommand(pi, ctx, readActiveRun(ctx.cwd) ?? run, `${answered.id} 已回答。`); } function updateTaskArtifact(pi: ExtensionAPI, ctx: ExtensionContext, run: NonNullable>, args: string): void { const parsed = parseArtifactArgs(args, run.phase); if (!parsed) { ctx.ui.notify("用法:/kd doc [阶段] [内容] [--replace]", "error"); return; } if (parsed.content && readArtifact(ctx.cwd, run, parsed.phase) !== undefined && !parsed.replace) { ctx.ui.notify(`拒绝覆盖 ${parsed.phase} 阶段文档。确认整体替换时追加 --replace;追加内容必须让 KCode 更新具体章节。`, "warning"); return; } const path = parsed.content ? updatePhaseArtifact(ctx.cwd, run, parsed.phase, parsed.content) : ensurePhaseArtifact(ctx.cwd, run, parsed.phase); ctx.ui.notify(`阶段文档已就绪:${path}`, "info"); autoAdvanceCommand(pi, ctx, readActiveRun(ctx.cwd) ?? run, `${parsed.phase} 阶段文档已更新。`); } function recordTaskVerifyResult(pi: ExtensionAPI, ctx: ExtensionContext, run: NonNullable>, args: string): void { const [exitCodeText, ...commandParts] = args.trim().split(/\s+/); const exitCode = Number(exitCodeText); const command = commandParts.join(" ").trim(); if (!Number.isFinite(exitCode) || !command) { ctx.ui.notify("用法:/kd verify ", "error"); return; } const outcome = recordVerifyResult(ctx.cwd, run, { command, exitCode }); ctx.ui.notify(outcome.message, outcome.status === "passed" ? "info" : "warning"); handleVerifyOutcome(pi, ctx, outcome); } function formatKdHelp(run: ReturnType): string { const current = run ? [`当前:${run.goal ?? run.id}`, `阶段:${run.phase},模式:${run.mode}`] : ["当前没有正在处理的需求。"]; return [ ...current, "", "核心命令:", "- /kd:继续当前需求", "- /kd <需求>:创建新需求并进入执行流程", "- /kd cangqiong 或 /kd strict:更新当前需求的产品或流程强度", "- /kd status:查看阶段、检查结果和下一步", "- /kd status --detail:查看需求ID、模式、运行目录等诊断信息", "- /kd answer Q-001 <答案>:记录澄清答案或业务事实", "- /kd check:重新执行阶段准入检查", "- /kd done:完成并归档当前需求", "", "配置与审计:/kd product cangqiong、/kd mode lite、/kd risk medium <原因>、/kd list、/kd use 、/kd log、/kd handoff", ].join("\n"); } function isDetailStatusRequest(args: string): boolean { return /(^|\s)(--detail|-d|detail|详情)(\s|$)/i.test(args); } export default function (pi: ExtensionAPI) { registerKingdeeHarnessTools(pi); registerHarnessToolEvents(pi); pi.registerCommand("kd", { description: "KCode 需求执行入口:继续当前需求,或用 /kd <需求> 创建新需求", handler: async (args, ctx) => { await handleUnifiedKdCommand(pi, args, ctx); }, }); pi.on("session_start", async (_event, ctx) => { const run = readActiveRun(ctx.cwd); if (!run || !ctx.hasUI) return; ctx.ui.notify( [ `发现未完成的需求:${run.goal ?? run.id}`, `阶段:${run.phase},产品:${run.profile?.product ?? run.product ?? "unknown"}`, "继续当前需求用 /kd;新需求直接用 /kd <需求>。", ].join("\n"), "info", ); }); pi.on("input", async (event, ctx) => { if (event.source === "extension") return { action: "continue" }; if (isSubagentChild()) return { action: "continue" }; let run = readActiveRun(ctx.cwd); if (!run && shouldStartHarnessFromInput(event.text)) { run = createActiveRun(ctx.cwd, event.text); if (ctx.hasUI) { ctx.ui.notify(`已开始:${run.goal ?? run.id}(${run.mode},${run.profile?.product}/${run.profile?.techStack})`, "info"); if (run.profile?.product === "unknown") { ctx.ui.notify("还没识别出金蝶产品。可用 /kd product cangqiong 指定。", "warning"); } } } if (!run) return { action: "continue" }; recordInputReceived(ctx.cwd, run, { text: event.text, source: event.source }); if (event.text.trim() && !event.text.trim().startsWith("/")) { recordRunContext(ctx.cwd, run, { text: event.text, kind: "user-input" }); run = readActiveRun(ctx.cwd) ?? run; } const openQuestions = openBlockingQuestions(run); if (openQuestions.length > 1) { const parsedAnswers = parseStructuredQuestionAnswers(openQuestions, event.text); if (parsedAnswers.length > 0) { const answered = []; const rejected = []; for (const parsed of parsedAnswers) { const result = answerQuestion(ctx.cwd, readActiveRun(ctx.cwd) ?? run, parsed.id, parsed.answer); const current = readActiveRun(ctx.cwd) ?? run; if (result) { answered.push(result); appendQuestionEventToArtifact(ctx.cwd, current, [`- 已回答 ${result.id}:${result.answer}`]); recordAutoAnsweredInput(ctx.cwd, current, { question: result, answer: parsed.answer }); } else { rejected.push(parsed.id); } } const current = readActiveRun(ctx.cwd) ?? run; if (ctx.hasUI) { const message = rejected.length > 0 ? `已记录 ${answered.map((item) => item.id).join(", ")};未能记录 ${rejected.join(", ")}。` : `已记录 ${answered.map((item) => item.id).join(", ")}。`; ctx.ui.notify(message, rejected.length > 0 ? "warning" : "info"); } const committed = commitActionForPrompt( ctx.cwd, current, `用户已回答问题组:${answered.map((item) => `${item.id}=${item.answer}`).join(";")}${rejected.length > 0 ? `;未记录:${rejected.join(", ")}` : ""}`, "input", ); return { action: "transform", text: workflowPromptForRun(ctx.cwd, committed, `用户已回答问题组:${answered.map((item) => `${item.id}=${item.answer}`).join(";")}`), }; } } if (openQuestions.length === 1 && shouldAutoRecordQuestionAnswer(event.text)) { const answered = answerQuestion(ctx.cwd, run, openQuestions[0].id, event.text); const current = readActiveRun(ctx.cwd) ?? run; if (answered) { appendQuestionEventToArtifact(ctx.cwd, current, [`- 已回答 ${answered.id}:${answered.answer}`]); recordAutoAnsweredInput(ctx.cwd, current, { question: answered, answer: event.text }); if (ctx.hasUI) ctx.ui.notify(`已记录 ${answered.id} 的答案`, "info"); const committed = commitActionForPrompt(ctx.cwd, current, `用户已回答 ${answered.id}:${answered.answer}`, "input"); return { action: "transform", text: workflowPromptForRun(ctx.cwd, committed, `用户已回答 ${answered.id}:${answered.answer}`), }; } } if (openQuestions.length === 0 && isPlainAdvanceRequest(event.text)) { const outcome = executeDeterministicNext(ctx.cwd, readActiveRun(ctx.cwd) ?? run, event.text); for (const notice of outcome.notices) { if (ctx.hasUI) ctx.ui.notify(notice.message, notice.level); } if (outcome.promptText) { const committed = commitActionForPrompt(ctx.cwd, outcome.run, outcome.promptText, "input"); return { action: "transform", text: workflowPromptForRun(ctx.cwd, committed, outcome.promptText), }; } if (outcome.handled) return { action: "continue" }; } const committed = commitActionForPrompt(ctx.cwd, run, event.text, "input"); return { action: "transform", text: workflowPromptForRun(ctx.cwd, committed, event.text), }; }); } function handleVerifyOutcome(pi: ExtensionAPI, ctx: ExtensionContext, outcome: VerifyResultOutcome): void { if (outcome.status === "passed") { autoAdvanceCommand(pi, ctx, outcome.run, "验证结果已通过。"); return; } if (outcome.status === "repairing") { sendWorkflowPrompt(pi, ctx, outcome.run, repairPromptForRun(outcome.run)); } }