kcode-pi 0.1.34 → 0.1.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +10 -10
  2. package/dist/cli/kcode.js +3 -3
  3. package/dist/context/project-context.js +4 -5
  4. package/dist/harness/prompt-policy.d.ts +19 -0
  5. package/dist/harness/prompt-policy.js +206 -0
  6. package/dist/harness/types.d.ts +74 -0
  7. package/dist/harness/types.js +16 -0
  8. package/dist/product/profile.d.ts +20 -0
  9. package/dist/product/profile.js +103 -0
  10. package/docs/CHANGELOG.md +90 -2
  11. package/docs/COMMAND_REFERENCE.md +27 -12
  12. package/docs/DEVELOPMENT.md +3 -3
  13. package/docs/EVIDENCE_AND_GATES.md +15 -8
  14. package/docs/HARNESS_WORKFLOW.md +32 -12
  15. package/docs/KCODE_DISTRIBUTION.md +7 -7
  16. package/docs/PRODUCT_PROFILE.md +9 -9
  17. package/docs/TROUBLESHOOTING.md +8 -8
  18. package/docs/USER_GUIDE.md +10 -10
  19. package/extensions/kingdee-harness.ts +141 -86
  20. package/extensions/kingdee-header.ts +1 -1
  21. package/extensions/kingdee-subagents.ts +1 -1
  22. package/extensions/kingdee-tools.ts +44 -44
  23. package/package.json +1 -1
  24. package/src/cli/kcode.ts +3 -3
  25. package/src/context/project-context.ts +4 -5
  26. package/src/harness/artifacts.ts +6 -7
  27. package/src/harness/data-source-policy.ts +346 -0
  28. package/src/harness/delegation.ts +28 -23
  29. package/src/harness/gates.ts +16 -1
  30. package/src/harness/messages.ts +65 -11
  31. package/src/harness/path-policy.ts +1 -0
  32. package/src/harness/plan-steps.ts +3 -3
  33. package/src/harness/prompt-policy.ts +227 -0
  34. package/src/harness/prompt.ts +12 -16
  35. package/src/harness/question-memory.ts +220 -0
  36. package/src/harness/repair.ts +18 -3
  37. package/src/harness/state.ts +93 -6
  38. package/src/harness/types.ts +19 -0
  39. package/src/official/kingdee-skills.ts +4 -4
  40. package/src/product/profile.ts +2 -2
  41. package/src/rules/checker.ts +27 -27
  42. package/src/tools/build-debug.ts +5 -5
  43. package/src/tools/sdk-signature.ts +4 -4
@@ -8,8 +8,8 @@
8
8
 
9
9
  - Node.js `>=22.19.0`
10
10
  - npm
11
- - Windows 推荐使用 Windows Terminal
12
- - 不需要安装 Python。KCode 随包工具使用 Node/TypeScript 实现;只有业务项目或外部脚本明确依赖 Python 时才需要另行安装。
11
+ - Windows 环境使用 Windows Terminal
12
+ - 无需安装 Python。KCode 随包工具使用 Node/TypeScript 实现;只有业务项目或外部脚本明确依赖 Python 时才另行安装。
13
13
 
14
14
  全局安装:
15
15
 
@@ -25,7 +25,7 @@ kcode
25
25
 
26
26
  ## 初始化业务项目
27
27
 
28
- 先在终端进入你的实际业务项目根目录,不是 KCode 源码目录。
28
+ 在终端进入实际业务项目根目录,不是 KCode 源码目录。
29
29
 
30
30
  初始化项目级配置:
31
31
 
@@ -73,7 +73,7 @@ kcode version
73
73
  kcode start
74
74
  ```
75
75
 
76
- `kcode start` 会先确保 `.pi/settings.json` 已登记当前安装的 KCode package,然后启动随包 Pi CLI。
76
+ `kcode start` 会确保 `.pi/settings.json` 已登记当前安装的 KCode package,然后启动随包 Pi CLI。
77
77
 
78
78
  `start` 后面的参数会原样透传给 Pi CLI:
79
79
 
@@ -83,7 +83,7 @@ kcode start --provider openai --model gpt-4o
83
83
 
84
84
  ## 配置模型
85
85
 
86
- 首次启动 Pi 时,如果看到:
86
+ 首次启动 Pi 时出现以下信息:
87
87
 
88
88
  ```text
89
89
  Warning: No models available.
@@ -107,7 +107,7 @@ DeepSeek
107
107
  Gemini
108
108
  ```
109
109
 
110
- 也可以在 PowerShell 中设置环境变量后再启动:
110
+ 支持在 PowerShell 中设置环境变量后再启动:
111
111
 
112
112
  ```powershell
113
113
  $env:OPENAI_API_KEY="sk-..."
@@ -124,7 +124,7 @@ KCode 在 Pi 内注册的 `/kd-*` 命令见 [命令参考](COMMAND_REFERENCE.md#
124
124
 
125
125
  ## 自定义模型供应商
126
126
 
127
- 如果你的模型服务是企业网关、代理服务、Ollama、LM Studio、vLLM,或其他 OpenAI-compatible endpoint,不需要改 KCode。Pi 原生支持通过用户级配置文件添加自定义供应商:
127
+ 模型服务为企业网关、代理服务、Ollama、LM Studio、vLLM 或其他 OpenAI-compatible endpoint 时,无需修改 KCode。Pi 原生支持通过用户级配置文件添加自定义供应商:
128
128
 
129
129
  ```powershell
130
130
  notepad $env:USERPROFILE\.pi\agent\models.json
@@ -171,7 +171,7 @@ $env:CORP_AI_API_KEY="sk-..."
171
171
  kcode start
172
172
  ```
173
173
 
174
- 也可以直接指定:
174
+ 支持直接指定:
175
175
 
176
176
  ```powershell
177
177
  kcode start --provider corp-ai --model corp-coder
@@ -199,11 +199,11 @@ npm install -g kcode-pi@latest
199
199
  kcode version
200
200
  ```
201
201
 
202
- 升级后建议在业务项目根目录执行:
202
+ 升级后在业务项目根目录执行:
203
203
 
204
204
  ```powershell
205
205
  kcode init
206
206
  kcode doctor --deep
207
207
  ```
208
208
 
209
- 如果升级失败或仍加载旧版本,见 [故障排查](TROUBLESHOOTING.md)
209
+ 升级失败或仍加载旧版本时,按 [故障排查](TROUBLESHOOTING.md) 处理。
@@ -11,8 +11,10 @@ import {
11
11
  ensurePhaseArtifact,
12
12
  finishActiveRun,
13
13
  listRuns,
14
+ openBlockingQuestions,
14
15
  readActiveRun,
15
16
  refreshGate,
17
+ reviseFact,
16
18
  switchActiveRun,
17
19
  updateProductProfile,
18
20
  updatePhaseArtifact,
@@ -22,11 +24,13 @@ import type { KdRisk } from "../src/harness/types.ts";
22
24
  import { readArtifact } from "../src/harness/artifacts.ts";
23
25
  import { flagshipWriteBlockReason, isSourceLikePath, planWriteBlockReason } from "../src/harness/path-policy.ts";
24
26
  import { sdkSignatureProductionWriteBlockReason } from "../src/harness/sdk-policy.ts";
27
+ import { dataSourceProductionWriteBlockReason } from "../src/harness/data-source-policy.ts";
25
28
  import { tddProductionWriteBlockReason } from "../src/harness/tdd-policy.ts";
26
29
  import { windowsPathHint } from "../src/platform/path.ts";
27
30
  import { repairPromptForRun, workflowPromptForRun } from "../src/harness/prompt.ts";
28
31
  import { recordVerifyResult, type VerifyResultOutcome } from "../src/harness/repair.ts";
29
32
  import { isSubagentChild, subagentRoleFromEnv, subagentToolCallBlockReason } from "../src/harness/delegation.ts";
33
+ import { questionAskBlockReason } from "../src/harness/question-memory.ts";
30
34
 
31
35
  function requireRun(cwd: string): ReturnType<typeof readActiveRun> {
32
36
  return readActiveRun(cwd);
@@ -137,14 +141,15 @@ function codeWriteBlockReason(cwd: string, path: string | undefined): string | u
137
141
 
138
142
  const run = readActiveRun(cwd);
139
143
  if (!run) {
140
- return "KCode 工作流未启动,不能直接写产品代码。下一步:先用自然语言说明需求让 KCode 自动进入 discuss,或手动运行 /kd-start <需求> 创建 run;完成 discuss -> spec -> plan -> execute 后再写生产源码。";
144
+ return "KCode 工作流未启动,禁止直接写产品代码。执行指令:使用自然语言需求启动 discuss,或执行 /kd-start <需求> 创建 run;完成 discuss -> spec -> plan -> execute 后再写生产源码。";
141
145
  }
142
146
 
143
147
  if (run.phase !== "execute") {
144
- return `当前 KCode 阶段是 ${run.phase},不能写产品代码。下一步:先完成当前阶段文档和门禁,用 /kd-advance 推进到 execute;如果发现计划不完整,更新 PLAN.md,而不是直接写代码。`;
148
+ return `当前 KCode 阶段是 ${run.phase},禁止写产品代码。执行指令:完成当前阶段文档和门禁,使用 /kd-advance 推进到 execute;计划不完整时更新 PLAN.md,禁止直接写代码。`;
145
149
  }
146
150
 
147
151
  return (
152
+ dataSourceProductionWriteBlockReason(cwd, run, path) ??
148
153
  sdkSignatureProductionWriteBlockReason(cwd, run, path) ??
149
154
  tddProductionWriteBlockReason(cwd, run, path) ??
150
155
  planWriteBlockReason(cwd, run, path, readArtifact(cwd, run, "plan") ?? "")
@@ -154,7 +159,7 @@ function codeWriteBlockReason(cwd: string, path: string | undefined): string | u
154
159
  const kdPlanStatusTool = defineTool({
155
160
  name: "kd_plan_status",
156
161
  label: "KD 状态",
157
- description: "查看当前金蝶 Harness run、当前阶段、必需文档和门禁状态。",
162
+ description: "输出当前金蝶 Harness run、当前阶段、必需文档和门禁状态。",
158
163
  parameters: Type.Object({}),
159
164
 
160
165
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
@@ -170,22 +175,24 @@ const kdQuestionTool = defineTool({
170
175
  name: "kd_question",
171
176
  label: "KD 问题",
172
177
  description:
173
- "创建、回答或列出金蝶 Harness 结构化问题。每次只记录一个最阻塞的短问题。",
178
+ "创建、回答、修订或列出金蝶 Harness 结构化问题。每次必须只记录一个最阻塞的短问题。",
174
179
  parameters: Type.Object({
175
- action: Type.Optional(Type.String({ description: "操作类型:ask、answer 或 list,默认 ask。" })),
180
+ action: Type.Optional(Type.String({ description: "操作类型:ask、answer、revise 或 list,默认 ask。" })),
176
181
  id: Type.Optional(Type.String({ description: "回答问题时的问题编号,例如 Q-001。" })),
177
- question: Type.Optional(Type.String({ description: "提问内容。使用一个短问题。" })),
182
+ question: Type.Optional(Type.String({ description: "提问内容;必须是一个短问题。" })),
178
183
  answer: Type.Optional(Type.String({ description: "用户答案,action=answer 时使用。" })),
179
- reason: Type.Optional(Type.String({ description: "说明为什么这个问题会阻塞当前阶段。" })),
180
- choices: Type.Optional(Type.Array(Type.String(), { description: "可选项,最多 3 个简短选项。" })),
181
- blocking: Type.Optional(Type.Boolean({ description: "是否阻塞阶段推进,默认 true。" })),
184
+ reason: Type.Optional(Type.String({ description: "当前阶段阻塞原因。" })),
185
+ factLabel: Type.Optional(Type.String({ description: "该问题要补齐的结构化事实标签,例如 目标 FormId/单据或表单标识。" })),
186
+ proposedFactValue: Type.Optional(Type.String({ description: "确认题中的候选事实值;用户回答是/yes 时写入 factLabel。" })),
187
+ choices: Type.Optional(Type.Array(Type.String(), { description: "候选答案;最多 3 个简短选项。" })),
188
+ blocking: Type.Optional(Type.Boolean({ description: "控制问题是否阻塞阶段推进,默认 true。" })),
182
189
  }),
183
190
 
184
191
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
185
192
  const run = readActiveRun(ctx.cwd);
186
193
  if (!run) {
187
194
  return {
188
- content: [{ type: "text", text: "当前没有 active Kingdee Harness run。请先使用 /kd-start <需求> 创建。" }],
195
+ content: [{ type: "text", text: "当前没有 active Kingdee Harness run。使用 /kd-start <需求> 创建。" }],
189
196
  details: { error: "no-active-run" },
190
197
  };
191
198
  }
@@ -197,17 +204,24 @@ const kdQuestionTool = defineTool({
197
204
  }
198
205
 
199
206
  if (action === "answer") {
200
- if (!params.id || !params.answer) {
207
+ if (!params.id || !params.answer?.trim()) {
201
208
  return {
202
- content: [{ type: "text", text: "kd_question action=answer 需要同时提供 id 和 answer。" }],
209
+ content: [{ type: "text", text: "kd_question action=answer 必须同时提供 id 和 answer。" }],
203
210
  details: { error: "missing-answer-params" },
204
211
  };
205
212
  }
213
+ const existingQuestion = (run.questions ?? []).find((question) => question.id === params.id);
214
+ if (existingQuestion?.status === "answered") {
215
+ return {
216
+ content: [{ type: "text", text: `${params.id} 已回答,禁止重复覆盖。用户明确更正事实时使用 kd_question action=revise factLabel="<事实标签>" answer="<新值>" reason="<更正原因>"。` }],
217
+ details: { error: "question-already-answered", question: existingQuestion },
218
+ };
219
+ }
206
220
  const answered = answerQuestion(ctx.cwd, run, params.id, params.answer);
207
221
  if (!answered) {
208
222
  return {
209
- content: [{ type: "text", text: `未找到问题:${params.id}` }],
210
- details: { error: "question-not-found", id: params.id },
223
+ content: [{ type: "text", text: existingQuestion ? `未能记录 ${params.id} 的答案;答案不能为空或占位内容。` : `未找到问题:${params.id}` }],
224
+ details: { error: existingQuestion ? "invalid-answer" : "question-not-found", id: params.id },
211
225
  };
212
226
  }
213
227
  appendQuestionEventToArtifact(ctx.cwd, run, [`- 已回答 ${answered.id}:${answered.answer}`]);
@@ -218,16 +232,42 @@ const kdQuestionTool = defineTool({
218
232
  };
219
233
  }
220
234
 
235
+ if (action === "revise") {
236
+ if (!params.factLabel?.trim() || !params.answer?.trim()) {
237
+ return {
238
+ content: [{ type: "text", text: "kd_question action=revise 必须同时提供 factLabel 和 answer。" }],
239
+ details: { error: "missing-revise-params" },
240
+ };
241
+ }
242
+ const fact = reviseFact(ctx.cwd, run, {
243
+ factLabel: params.factLabel,
244
+ value: params.answer,
245
+ reason: params.reason,
246
+ });
247
+ if (!fact) {
248
+ return {
249
+ content: [{ type: "text", text: "未能修订结构化事实;factLabel 和 answer 不能为空。" }],
250
+ details: { error: "invalid-revision" },
251
+ };
252
+ }
253
+ appendQuestionEventToArtifact(ctx.cwd, run, [`- 已修订事实 ${fact.label}:${fact.value}`]);
254
+ const auto = autoAdvanceTool(ctx.cwd, run);
255
+ return {
256
+ content: [{ type: "text", text: `已修订事实 ${fact.label}:${fact.value}。${auto.advanced ? auto.message : `门禁仍阻塞:${auto.message}`}` }],
257
+ details: { fact, gate: readActiveRun(ctx.cwd)?.gate, autoAdvance: auto },
258
+ };
259
+ }
260
+
221
261
  if (action !== "ask") {
222
262
  return {
223
- content: [{ type: "text", text: `未知 kd_question action:${params.action}。请使用 ask、answerlist。` }],
263
+ content: [{ type: "text", text: `未知 kd_question action:${params.action}。有效值:ask、answer、revise、list。` }],
224
264
  details: { error: "unknown-action", action: params.action },
225
265
  };
226
266
  }
227
267
 
228
268
  if (!params.question?.trim()) {
229
269
  return {
230
- content: [{ type: "text", text: "kd_question action=ask 需要提供 question。" }],
270
+ content: [{ type: "text", text: "kd_question action=ask 必须提供 question。" }],
231
271
  details: { error: "missing-question" },
232
272
  };
233
273
  }
@@ -240,39 +280,43 @@ const kdQuestionTool = defineTool({
240
280
  text: [
241
281
  batchProblem,
242
282
  "",
243
- "kd_question 一次只登记一个当前最阻塞的问题。",
244
- "请先问第一个必须确认的问题,例如:",
283
+ "kd_question 一次必须只登记一个当前最阻塞的问题。",
284
+ "登记第一个必须确认的问题,例如:",
245
285
  "kd_question action=ask question=\"采购入库单 Form ID 是否为 pur_receivebill?\" choices=[\"是\", \"不是\"]",
246
286
  "",
247
- "注意:交互模式下会弹出选择/输入对话并自动记录;非交互模式下用户在对话中回答后再用 kd_question action=answer 记录。",
287
+ "问题登记后保持 open;用户在下一条消息中回答,或使用 kd_question action=answer 记录。",
248
288
  ].join("\n"),
249
289
  },
250
290
  ],
251
291
  details: { error: "batched-question" },
252
292
  };
253
293
  }
294
+ const askBlockReason = questionAskBlockReason(run, { factLabel: params.factLabel });
295
+ if (askBlockReason) {
296
+ return {
297
+ content: [{ type: "text", text: askBlockReason }],
298
+ details: { error: "duplicate-fact-question", factLabel: params.factLabel },
299
+ };
300
+ }
254
301
 
302
+ const existingQuestionIds = new Set((run.questions ?? []).map((question) => question.id));
255
303
  const question = addQuestion(ctx.cwd, run, {
256
304
  question: params.question,
257
305
  reason: params.reason,
306
+ factLabel: params.factLabel,
307
+ proposedFactValue: params.proposedFactValue,
258
308
  choices: params.choices,
259
309
  blocking: params.blocking,
260
310
  });
261
- appendQuestionEventToArtifact(ctx.cwd, run, formatQuestionArtifactLines(question));
262
- const interactiveAnswer = await askQuestionInteractively(ctx, question.question, question.choices);
263
- if (interactiveAnswer) {
264
- const answered = answerQuestion(ctx.cwd, run, question.id, interactiveAnswer);
265
- if (answered) {
266
- appendQuestionEventToArtifact(ctx.cwd, run, [`- 已回答 ${answered.id}:${answered.answer}`]);
267
- const auto = autoAdvanceTool(ctx.cwd, run);
268
- return {
269
- content: [{ type: "text", text: `用户已回答 ${answered.id}:${answered.answer}。${auto.advanced ? auto.message : `门禁仍阻塞:${auto.message}`}` }],
270
- details: { question: answered, gate: readActiveRun(ctx.cwd)?.gate, answered: true, autoAdvance: auto },
271
- };
272
- }
311
+ if (existingQuestionIds.has(question.id)) {
312
+ return {
313
+ content: [{ type: "text", text: `${question.id} 已是 open 问题,禁止重复登记。等待用户回答,或使用 kd_question action=answer id=${question.id} answer=<答案> 记录。` }],
314
+ details: { question, gate: readActiveRun(ctx.cwd)?.gate, duplicate: true },
315
+ };
273
316
  }
317
+ appendQuestionEventToArtifact(ctx.cwd, run, formatQuestionArtifactLines(question));
274
318
 
275
- const text = `${formatQuestionCard(question)}\n\n未获得交互式答案;在用户回答前,该问题会保持 open。`;
319
+ const text = `${formatQuestionCard(question)}\n\n问题已登记为 open。等待用户在下一条消息中回答,或执行 kd_question action=answer id=${question.id} answer=<答案>。`;
276
320
  return { content: [{ type: "text", text }], details: { question, gate: readActiveRun(ctx.cwd)?.gate, answered: false } };
277
321
  },
278
322
  });
@@ -281,7 +325,7 @@ function createKdVerifyResultTool(pi: ExtensionAPI) {
281
325
  return defineTool({
282
326
  name: "kd_verify_result",
283
327
  label: "KD 验证结果",
284
- description: "记录当前 verify 命令结果。失败时自动写失败证据并回到 execute 修复;成功时记录通过证据并尝试推进。",
328
+ description: "记录当前 verify 命令结果。失败时自动写失败证据并回到 execute 修复;成功时记录通过证据并推进门禁。",
285
329
  parameters: Type.Object({
286
330
  command: Type.String({ description: "实际执行的验证命令。" }),
287
331
  exitCode: Type.Number({ description: "验证命令退出码。" }),
@@ -294,7 +338,7 @@ function createKdVerifyResultTool(pi: ExtensionAPI) {
294
338
  const run = readActiveRun(ctx.cwd);
295
339
  if (!run) {
296
340
  return {
297
- content: [{ type: "text", text: "当前没有 active Kingdee Harness run。请先使用 /kd-start <需求> 创建。" }],
341
+ content: [{ type: "text", text: "当前没有 active Kingdee Harness run。使用 /kd-start <需求> 创建。" }],
298
342
  details: { error: "no-active-run" },
299
343
  };
300
344
  }
@@ -326,7 +370,7 @@ export default function (pi: ExtensionAPI) {
326
370
  [
327
371
  `发现未完成 KCode run:${run.goal ?? run.id}`,
328
372
  `阶段:${run.phase},产品:${run.profile?.product ?? run.product ?? "unknown"}`,
329
- "输入 /kd-resume 可接续;输入 /kd-runs 查看其他需求或需求组。",
373
+ "执行 /kd-resume 接续;执行 /kd-runs 输出其他需求或需求组。",
330
374
  ].join("\n"),
331
375
  "info",
332
376
  );
@@ -342,13 +386,27 @@ export default function (pi: ExtensionAPI) {
342
386
  if (ctx.hasUI) {
343
387
  ctx.ui.notify(`已启动 Kingdee Harness run:${run.id}(${run.profile?.product}/${run.profile?.techStack})`, "info");
344
388
  if (run.profile?.product === "unknown") {
345
- ctx.ui.notify("产品画像未识别。下一步先执行 /kd-product <flagship|xinghan|cangqiong|enterprise>。", "warning");
389
+ ctx.ui.notify("产品画像未识别。执行指令:执行 /kd-product <flagship|xinghan|cangqiong|enterprise>。", "warning");
346
390
  }
347
391
  }
348
392
  }
349
393
 
350
394
  if (!run) return { action: "continue" };
351
395
 
396
+ const openQuestions = openBlockingQuestions(run);
397
+ if (openQuestions.length === 1 && shouldAutoRecordQuestionAnswer(event.text)) {
398
+ const answered = answerQuestion(ctx.cwd, run, openQuestions[0].id, event.text);
399
+ const current = readActiveRun(ctx.cwd) ?? run;
400
+ if (answered) {
401
+ appendQuestionEventToArtifact(ctx.cwd, current, [`- 已回答 ${answered.id}:${answered.answer}`]);
402
+ if (ctx.hasUI) ctx.ui.notify(`已记录 ${answered.id} 的答案`, "info");
403
+ return {
404
+ action: "transform",
405
+ text: workflowPromptForRun(ctx.cwd, current, `用户已回答 ${answered.id}:${answered.answer}`),
406
+ };
407
+ }
408
+ }
409
+
352
410
  return {
353
411
  action: "transform",
354
412
  text: workflowPromptForRun(ctx.cwd, run, event.text),
@@ -360,7 +418,7 @@ export default function (pi: ExtensionAPI) {
360
418
  const path = typeof input.path === "string" ? input.path : undefined;
361
419
  const hint = path ? windowsPathHint(path) : undefined;
362
420
  if (hint && ["read", "write", "edit"].includes(event.toolName)) {
363
- const reason = `当前是 Windows 工作区,路径 ${path} 不是本项目使用的路径形式。下一步:改用项目相对路径;如果必须使用绝对路径,使用 Windows 路径 ${hint}。`;
421
+ const reason = `当前是 Windows 工作区,路径 ${path} 不是本项目使用的路径形式。执行指令:改用项目相对路径;必须使用绝对路径时,使用 Windows 路径 ${hint}。`;
364
422
  if (ctx.hasUI) ctx.ui.notify(reason, "warning");
365
423
  return { block: true, reason };
366
424
  }
@@ -369,7 +427,7 @@ export default function (pi: ExtensionAPI) {
369
427
  if (event.toolName === "read" && path) {
370
428
  const ext = path.toLowerCase().split(".").pop();
371
429
  if (ext === "pdf" || ext === "docx" || ext === "doc" || ext === "xlsx" || ext === "xls" || ext === "csv") {
372
- const reason = `read 工具无法解析 .${ext} 二进制格式。下一步:改用 kd_doc_read 工具读取此文件,参数 path="${path}"。`;
430
+ const reason = `read 工具无法解析 .${ext} 二进制格式。执行指令:改用 kd_doc_read 工具读取此文件,参数 path="${path}"。`;
373
431
  if (ctx.hasUI) ctx.ui.notify(reason, "info");
374
432
  return { block: true, reason };
375
433
  }
@@ -379,6 +437,7 @@ export default function (pi: ExtensionAPI) {
379
437
  if (subagentRole) {
380
438
  const run = readActiveRun(ctx.cwd);
381
439
  const sourceWriteBlock =
440
+ dataSourceProductionWriteBlockReason(ctx.cwd, run, path) ??
382
441
  sdkSignatureProductionWriteBlockReason(ctx.cwd, run, path) ??
383
442
  tddProductionWriteBlockReason(ctx.cwd, run, path) ??
384
443
  planWriteBlockReason(ctx.cwd, run, path, run ? (readArtifact(ctx.cwd, run, "plan") ?? "") : "") ??
@@ -408,14 +467,14 @@ export default function (pi: ExtensionAPI) {
408
467
  });
409
468
 
410
469
  pi.registerCommand("kd-status", {
411
- description: "查看当前 Kingdee Harness run 和门禁状态",
470
+ description: "输出当前 Kingdee Harness run 和门禁状态",
412
471
  handler: async (_args, ctx) => {
413
472
  ctx.ui.notify(formatStatus(ctx.cwd, readActiveRun(ctx.cwd)), "info");
414
473
  },
415
474
  });
416
475
 
417
476
  pi.registerCommand("kd-runs", {
418
- description: "列出当前项目的 Kingdee Harness run",
477
+ description: "输出当前项目的 Kingdee Harness run 列表",
419
478
  handler: async (_args, ctx) => {
420
479
  const active = readActiveRun(ctx.cwd);
421
480
  ctx.ui.notify(formatRuns(listRuns(ctx.cwd), active?.id), "info");
@@ -423,11 +482,11 @@ export default function (pi: ExtensionAPI) {
423
482
  });
424
483
 
425
484
  pi.registerCommand("kd-gate", {
426
- description: "刷新并查看当前 Kingdee Harness 门禁",
485
+ description: "刷新并输出当前 Kingdee Harness 门禁",
427
486
  handler: async (_args, ctx) => {
428
487
  const run = requireRun(ctx.cwd);
429
488
  if (!run) {
430
- ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求>。", "error");
489
+ ctx.ui.notify("当前没有 active Kingdee Harness run。使用 /kd-start <需求>。", "error");
431
490
  return;
432
491
  }
433
492
  const refreshed = refreshGate(ctx.cwd, run);
@@ -436,7 +495,7 @@ export default function (pi: ExtensionAPI) {
436
495
  });
437
496
 
438
497
  pi.registerCommand("kd-start", {
439
- description: "启动一个 Kingdee Harness run:/kd-start [--product 产品] [--version 版本] <需求或需求组>",
498
+ description: "创建 Kingdee Harness run:/kd-start [--product 产品] [--version 版本] <需求或需求组>",
440
499
  handler: async (args, ctx) => {
441
500
  const parsed = parseStartArgs(args);
442
501
  const goal = parsed.goal;
@@ -448,7 +507,7 @@ export default function (pi: ExtensionAPI) {
448
507
  const run = createActiveRun(ctx.cwd, goal, parsed.product, parsed.version);
449
508
  ctx.ui.notify(`已启动 Kingdee Harness run:${run.id}(${run.profile?.product}/${run.profile?.techStack})`, "info");
450
509
  if (run.profile?.product === "unknown") {
451
- ctx.ui.notify("产品画像未识别。下一步先执行 /kd-product <flagship|xinghan|cangqiong|enterprise>。", "warning");
510
+ ctx.ui.notify("产品画像未识别。执行指令:执行 /kd-product <flagship|xinghan|cangqiong|enterprise>。", "warning");
452
511
  }
453
512
  sendWorkflowPrompt(pi, ctx, run, `继续 KCode Harness run ${run.id}:${goal}`);
454
513
  },
@@ -459,7 +518,7 @@ export default function (pi: ExtensionAPI) {
459
518
  handler: async (args, ctx) => {
460
519
  const run = requireRun(ctx.cwd);
461
520
  if (!run) {
462
- ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求> 或 /kd-runs。", "error");
521
+ ctx.ui.notify("当前没有 active Kingdee Harness run。使用 /kd-start <需求> 或 /kd-runs。", "error");
463
522
  return;
464
523
  }
465
524
 
@@ -475,7 +534,7 @@ export default function (pi: ExtensionAPI) {
475
534
  });
476
535
 
477
536
  pi.registerCommand("kd-switch", {
478
- description: "切换当前 active Kingdee Harness run:/kd-switch <run-id>",
537
+ description: "设置当前 active Kingdee Harness run:/kd-switch <run-id>",
479
538
  handler: async (args, ctx) => {
480
539
  const id = args.trim();
481
540
  if (!id) {
@@ -485,7 +544,7 @@ export default function (pi: ExtensionAPI) {
485
544
 
486
545
  const run = switchActiveRun(ctx.cwd, id);
487
546
  if (!run) {
488
- ctx.ui.notify(`未找到 Kingdee Harness run:${id}。请用 /kd-runs 查看列表。`, "error");
547
+ ctx.ui.notify(`未找到 Kingdee Harness run:${id}。使用 /kd-runs 查看列表。`, "error");
489
548
  return;
490
549
  }
491
550
 
@@ -498,12 +557,12 @@ export default function (pi: ExtensionAPI) {
498
557
  handler: async (_args, ctx) => {
499
558
  const run = requireRun(ctx.cwd);
500
559
  if (!run) {
501
- ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求>。", "error");
560
+ ctx.ui.notify("当前没有 active Kingdee Harness run。使用 /kd-start <需求>。", "error");
502
561
  return;
503
562
  }
504
563
 
505
564
  const finished = finishActiveRun(ctx.cwd, run);
506
- ctx.ui.notify(`已完成 Kingdee Harness run:${finished.id}。下一个需求或需求组请使用 /kd-start <需求>。`, "info");
565
+ ctx.ui.notify(`已完成 Kingdee Harness run:${finished.id}。下一个需求或需求组使用 /kd-start <需求>。`, "info");
507
566
  },
508
567
  });
509
568
 
@@ -512,7 +571,7 @@ export default function (pi: ExtensionAPI) {
512
571
  handler: async (args, ctx) => {
513
572
  const run = requireRun(ctx.cwd);
514
573
  if (!run) {
515
- ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求>。", "error");
574
+ ctx.ui.notify("当前没有 active Kingdee Harness run。使用 /kd-start <需求>。", "error");
516
575
  return;
517
576
  }
518
577
 
@@ -533,7 +592,7 @@ export default function (pi: ExtensionAPI) {
533
592
  handler: async (args, ctx) => {
534
593
  const run = requireRun(ctx.cwd);
535
594
  if (!run) {
536
- ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求>。", "error");
595
+ ctx.ui.notify("当前没有 active Kingdee Harness run。使用 /kd-start <需求>。", "error");
537
596
  return;
538
597
  }
539
598
 
@@ -554,7 +613,7 @@ export default function (pi: ExtensionAPI) {
554
613
  handler: async (args, ctx) => {
555
614
  const run = requireRun(ctx.cwd);
556
615
  if (!run) {
557
- ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求>。", "error");
616
+ ctx.ui.notify("当前没有 active Kingdee Harness run。使用 /kd-start <需求>。", "error");
558
617
  return;
559
618
  }
560
619
 
@@ -562,7 +621,7 @@ export default function (pi: ExtensionAPI) {
562
621
  let requested: KdPhase | undefined;
563
622
  if (requestedText) {
564
623
  if (!isKdPhase(requestedText)) {
565
- ctx.ui.notify(`未知阶段 "${requestedText}"。可用阶段:${PHASE_ORDER.join(", ")}`, "error");
624
+ ctx.ui.notify(`未知阶段 "${requestedText}"。有效阶段:${PHASE_ORDER.join(", ")}`, "error");
566
625
  return;
567
626
  }
568
627
  requested = requestedText;
@@ -574,11 +633,11 @@ export default function (pi: ExtensionAPI) {
574
633
  });
575
634
 
576
635
  pi.registerCommand("kd-artifact", {
577
- description: "创建阶段文档,或显式覆盖阶段文档:/kd-artifact [阶段] [内容] [--replace]",
636
+ description: "创建阶段文档,或使用 --replace 显式覆盖阶段文档:/kd-artifact [阶段] [内容] [--replace]",
578
637
  handler: async (args, ctx) => {
579
638
  const run = requireRun(ctx.cwd);
580
639
  if (!run) {
581
- ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求>。", "error");
640
+ ctx.ui.notify("当前没有 active Kingdee Harness run。使用 /kd-start <需求>。", "error");
582
641
  return;
583
642
  }
584
643
 
@@ -589,7 +648,7 @@ export default function (pi: ExtensionAPI) {
589
648
  }
590
649
 
591
650
  if (parsed.content && readArtifact(ctx.cwd, run, parsed.phase) !== undefined && !parsed.replace) {
592
- ctx.ui.notify(`拒绝覆盖 ${parsed.phase} 阶段文档。若确认要整体替换,请追加 --replace;追加内容应让 KCode 更新具体章节。`, "warning");
651
+ ctx.ui.notify(`拒绝覆盖 ${parsed.phase} 阶段文档。确认整体替换时追加 --replace;追加内容必须让 KCode 更新具体章节。`, "warning");
593
652
  return;
594
653
  }
595
654
 
@@ -606,7 +665,7 @@ export default function (pi: ExtensionAPI) {
606
665
  handler: async (args, ctx) => {
607
666
  const run = requireRun(ctx.cwd);
608
667
  if (!run) {
609
- ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求>。", "error");
668
+ ctx.ui.notify("当前没有 active Kingdee Harness run。使用 /kd-start <需求>。", "error");
610
669
  return;
611
670
  }
612
671
  const [id, ...answerParts] = args.trim().split(/\s+/);
@@ -615,9 +674,14 @@ export default function (pi: ExtensionAPI) {
615
674
  ctx.ui.notify("用法:/kd-answer Q-001 <答案>", "error");
616
675
  return;
617
676
  }
677
+ const existingQuestion = (run.questions ?? []).find((question) => question.id === id);
678
+ if (existingQuestion?.status === "answered") {
679
+ ctx.ui.notify(`${id} 已回答,禁止重复覆盖;用户更正事实时使用 kd_question action=revise。`, "warning");
680
+ return;
681
+ }
618
682
  const answered = answerQuestion(ctx.cwd, run, id, answer);
619
683
  if (!answered) {
620
- ctx.ui.notify(`未找到问题:${id}`, "error");
684
+ ctx.ui.notify(existingQuestion ? `未能记录 ${id} 的答案;答案不能为空、占位内容或单独确认/否定。` : `未找到问题:${id}`, "error");
621
685
  return;
622
686
  }
623
687
  appendQuestionEventToArtifact(ctx.cwd, run, [`- 已回答 ${answered.id}:${answered.answer}`]);
@@ -631,7 +695,7 @@ export default function (pi: ExtensionAPI) {
631
695
  handler: async (args, ctx) => {
632
696
  const run = requireRun(ctx.cwd);
633
697
  if (!run) {
634
- ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求>。", "error");
698
+ ctx.ui.notify("当前没有 active Kingdee Harness run。使用 /kd-start <需求>。", "error");
635
699
  return;
636
700
  }
637
701
  const [exitCodeText, ...commandParts] = args.trim().split(/\s+/);
@@ -665,7 +729,7 @@ function formatQuestions(run: NonNullable<ReturnType<typeof readActiveRun>>): st
665
729
  }
666
730
 
667
731
  function formatRuns(runs: NonNullable<ReturnType<typeof readActiveRun>>[], activeId?: string): string {
668
- if (runs.length === 0) return "当前项目没有 Kingdee Harness run。请使用 /kd-start <需求> 创建。";
732
+ if (runs.length === 0) return "当前项目没有 Kingdee Harness run。使用 /kd-start <需求> 创建。";
669
733
  return runs
670
734
  .map((run) =>
671
735
  [
@@ -684,55 +748,46 @@ function formatQuestionCard(question: NonNullable<NonNullable<ReturnType<typeof
684
748
  `阶段:${question.phase}`,
685
749
  `问题:${question.question}`,
686
750
  question.reason ? `原因:${question.reason}` : undefined,
751
+ question.factLabel ? `事实标签:${question.factLabel}` : undefined,
752
+ question.proposedFactValue ? `候选事实值:${question.proposedFactValue}` : undefined,
687
753
  question.choices?.length ? `选项:${question.choices.join(" | ")}` : undefined,
688
754
  question.answer ? `答案:${question.answer}` : undefined,
689
755
  question.status === "open"
690
- ? `如果有弹窗,请直接回答;否则请在对话中回复,并用 kd_question action=answer id=${question.id} answer=<答案> 记录。`
756
+ ? `回答方式:用户在下一条消息中回答,或使用 kd_question action=answer id=${question.id} answer=<答案> 记录。`
691
757
  : undefined,
692
758
  ];
693
759
  return lines.filter(Boolean).join("\n");
694
760
  }
695
761
 
696
- async function askQuestionInteractively(
697
- ctx: ExtensionContext,
698
- question: string,
699
- choices: string[] | undefined,
700
- ): Promise<string | undefined> {
701
- if (!ctx.hasUI) return undefined;
702
-
703
- const normalizedChoices = choices?.map((choice) => choice.trim()).filter(Boolean) ?? [];
704
- try {
705
- if (normalizedChoices.length === 0) {
706
- return (await ctx.ui.input(question, "请输入答案"))?.trim() || undefined;
707
- }
708
-
709
- const customChoice = "其他,自定义输入";
710
- const selected = await ctx.ui.select(question, [...normalizedChoices, customChoice]);
711
- if (!selected) return undefined;
712
- if (selected !== customChoice) return selected;
713
- return (await ctx.ui.input(question, "请输入答案"))?.trim() || undefined;
714
- } catch {
715
- return undefined;
716
- }
717
- }
718
-
719
762
  function questionBatchProblem(question: string, choices?: string[]): string | undefined {
720
763
  const text = question.trim();
721
764
  const numberedItems = text.split(/\r?\n/).filter((line) => /^\s*(\d+[\.\)、)]|[-*]\s+)/.test(line)).length;
722
765
  if (numberedItems >= 2) return "kd_question 拒绝了批量清单式问题。";
723
766
  if ((text.match(/[??]/g) ?? []).length > 1) return "kd_question 拒绝了一次包含多个问题的提问。";
724
- if (text.length > 220) return "kd_question 拒绝了过长问题。请只问当前最小的阻塞问题。";
725
- if ((choices?.length ?? 0) > 3) return "kd_question 拒绝了过多选项。最多提供 3 个简短选项。";
767
+ if (text.length > 220) return "kd_question 拒绝了过长问题。问题必须聚焦当前最小阻塞项。";
768
+ if ((choices?.length ?? 0) > 3) return "kd_question 拒绝了过多选项。最多 3 个简短选项。";
726
769
  if (choices?.some((choice) => choice.length > 40 || /[\r\n??]/.test(choice))) {
727
770
  return "kd_question 拒绝了复杂选项。选项必须是短标签,不能嵌套问题。";
728
771
  }
729
772
  return undefined;
730
773
  }
731
774
 
775
+ function shouldAutoRecordQuestionAnswer(text: string): boolean {
776
+ const trimmed = text.trim();
777
+ if (!trimmed || trimmed.startsWith("/")) return false;
778
+ if (trimmed.length > 300) return false;
779
+ if (/[??]/.test(trimmed)) return false;
780
+ if (/^(先|暂停|等等|不用|不要|继续|开始|实现|修改|修复|提交|push|发布)\b/i.test(trimmed)) return false;
781
+ if (/^(先|暂停|等等|不用|不要|继续|开始|实现|修改|修复|提交|发布)/.test(trimmed)) return false;
782
+ return true;
783
+ }
784
+
732
785
  function formatQuestionArtifactLines(question: NonNullable<NonNullable<ReturnType<typeof readActiveRun>>["questions"]>[number]): string[] {
733
786
  return [
734
787
  `- ${question.id} [${question.blocking ? "blocking" : "non-blocking"}] ${question.question}`,
735
788
  question.reason ? ` - 原因:${question.reason}` : undefined,
789
+ question.factLabel ? ` - 事实标签:${question.factLabel}` : undefined,
790
+ question.proposedFactValue ? ` - 候选事实值:${question.proposedFactValue}` : undefined,
736
791
  question.choices?.length ? ` - 选项:${question.choices.join(" | ")}` : undefined,
737
792
  " - 状态:open",
738
793
  ].filter(Boolean) as string[];
@@ -186,7 +186,7 @@ export default function (pi: ExtensionAPI) {
186
186
  description: "恢复 KCode 金蝶标题栏",
187
187
  handler: async (_args, ctx) => {
188
188
  if (ctx.mode !== "tui") return;
189
- ctx.ui.notify("请重启会话或重新加载扩展以恢复 KCode 标题栏。", "info");
189
+ ctx.ui.notify("重启会话或重新加载扩展以恢复 KCode 标题栏。", "info");
190
190
  },
191
191
  });
192
192
 
@@ -246,7 +246,7 @@ async function runChainedAgents(
246
246
 
247
247
  function formatModeResult(mode: "single" | "parallel" | "chain", results: ChildAgentResult[], limit: number): string {
248
248
  const succeeded = results.filter((result) => result.exitCode === 0).length;
249
- const header = `子 agent ${mode} 完成:${succeeded}/${results.length} succeeded`;
249
+ const header = `子 agent ${mode} 完成:${succeeded}/${results.length} 成功`;
250
250
  const sections = results.map((result, index) => {
251
251
  const output = truncateOutput(result.output || result.stderr || "(子 agent 无输出)", limit);
252
252
  return [