kcode-pi 0.1.14 → 0.1.16
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.
- package/README.md +31 -25
- package/dist/context/project-context.js +20 -20
- package/extensions/kingdee-harness.ts +89 -82
- package/extensions/kingdee-header.ts +16 -16
- package/extensions/kingdee-tools.ts +112 -83
- package/package.json +2 -2
- package/prompts/kd-discuss.md +4 -4
- package/prompts/kd-execute.md +4 -5
- package/prompts/kd-plan.md +4 -5
- package/prompts/kd-ship.md +4 -5
- package/prompts/kd-spec.md +4 -5
- package/prompts/kd-verify.md +4 -5
- package/src/context/project-context.ts +20 -20
- package/src/harness/artifacts.ts +52 -47
- package/src/harness/format.ts +12 -12
- package/src/harness/gates.ts +23 -13
- package/src/harness/plan-steps.ts +3 -3
- package/src/harness/sdk-policy.ts +37 -0
- package/src/harness/state.ts +4 -4
- package/src/harness/tdd-policy.ts +2 -2
- package/src/knowledge/format.ts +11 -12
- package/src/product/profile.ts +16 -16
- package/src/tools/build-debug.ts +35 -35
- package/src/tools/sdk-signature.ts +10 -10
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "../src/harness/state.ts";
|
|
19
19
|
import { readArtifact } from "../src/harness/artifacts.ts";
|
|
20
20
|
import { flagshipWriteBlockReason, isSourceLikePath, planWriteBlockReason } from "../src/harness/path-policy.ts";
|
|
21
|
+
import { sdkSignatureProductionWriteBlockReason } from "../src/harness/sdk-policy.ts";
|
|
21
22
|
import { tddProductionWriteBlockReason } from "../src/harness/tdd-policy.ts";
|
|
22
23
|
import { readProjectContext } from "../src/context/project-context.ts";
|
|
23
24
|
import { windowsPathHint } from "../src/platform/path.ts";
|
|
@@ -84,7 +85,7 @@ function sendWorkflowPrompt(pi: ExtensionAPI, ctx: ExtensionContext, run: NonNul
|
|
|
84
85
|
return;
|
|
85
86
|
}
|
|
86
87
|
pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
87
|
-
if (ctx.hasUI) ctx.ui.notify("KCode
|
|
88
|
+
if (ctx.hasUI) ctx.ui.notify("KCode 工作流消息已排队。", "info");
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
function workflowPromptForRun(cwd: string, run: NonNullable<ReturnType<typeof readActiveRun>>, userText: string): string {
|
|
@@ -123,9 +124,11 @@ function workflowPromptForRun(cwd: string, run: NonNullable<ReturnType<typeof re
|
|
|
123
124
|
"",
|
|
124
125
|
phaseGuidance[run.phase],
|
|
125
126
|
"必须先理解当前业务项目已有目录、模块、包名、基类和本地封装,再决定文件位置和实现方式。",
|
|
127
|
+
"禁止凭记忆、模型知识或随包知识库直接编写 SDK 方法调用。Java/C# 代码中出现的 SDK 类、方法、构造器、枚举和属性,必须来自 kd_sdk_signature 对当前项目 jar/dll 的成功结果、项目构建输出或官方元数据证据。",
|
|
128
|
+
"kd_search、kd_cosmic_api 和随包知识只能用于找线索;没有 evidence/sdk-signature.md 或明确构建证据时,不得进入 execute,也不得写生产源码。",
|
|
126
129
|
"路径规则:在 Windows 工作区内,优先使用项目相对路径;如需绝对路径必须使用 `D:\\...` 这类 Windows 路径,禁止把路径改写成 `/mnt/d/...`、`/d/...` 等 WSL/MSYS 风格路径。",
|
|
127
130
|
"execute 阶段只能写 PLAN.md 明确列出的源码文件;如果目标文件不在计划内,必须先回到 plan 更新 PLAN.md。",
|
|
128
|
-
"写生产源码前必须先有红灯证据 evidence/tdd-red.md
|
|
131
|
+
"写生产源码前必须先有红灯证据 evidence/tdd-red.md;Java/C# 还必须有 SDK 签名证据 evidence/sdk-signature.md。红绿证据可以是 kd_sdk_signature 本地 SDK 签名、API/基类/方法签名、元数据、编译、既有测试框架或外部接口最小验证,不要为了测试引入额外 jar。",
|
|
129
132
|
].join("\n");
|
|
130
133
|
}
|
|
131
134
|
|
|
@@ -158,13 +161,17 @@ function codeWriteBlockReason(cwd: string, path: string | undefined): string | u
|
|
|
158
161
|
return `当前 KCode 阶段是 ${run.phase},不能写产品代码。请先完成 discuss -> spec -> plan 并进入 execute。`;
|
|
159
162
|
}
|
|
160
163
|
|
|
161
|
-
return
|
|
164
|
+
return (
|
|
165
|
+
sdkSignatureProductionWriteBlockReason(cwd, run, path) ??
|
|
166
|
+
tddProductionWriteBlockReason(cwd, run, path) ??
|
|
167
|
+
planWriteBlockReason(cwd, run, path, readArtifact(cwd, run, "plan") ?? "")
|
|
168
|
+
);
|
|
162
169
|
}
|
|
163
170
|
|
|
164
171
|
const kdPlanStatusTool = defineTool({
|
|
165
172
|
name: "kd_plan_status",
|
|
166
|
-
label: "KD
|
|
167
|
-
description: "
|
|
173
|
+
label: "KD 状态",
|
|
174
|
+
description: "查看当前金蝶 Harness run、当前阶段、必需文档和门禁状态。",
|
|
168
175
|
parameters: Type.Object({}),
|
|
169
176
|
|
|
170
177
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
@@ -178,24 +185,24 @@ const kdPlanStatusTool = defineTool({
|
|
|
178
185
|
|
|
179
186
|
const kdQuestionTool = defineTool({
|
|
180
187
|
name: "kd_question",
|
|
181
|
-
label: "KD
|
|
188
|
+
label: "KD 问题",
|
|
182
189
|
description:
|
|
183
|
-
"
|
|
190
|
+
"创建、回答或列出金蝶 Harness 结构化问题。每次只能问一个最阻塞的短问题,不要批量列清单。",
|
|
184
191
|
parameters: Type.Object({
|
|
185
|
-
action: Type.Optional(Type.String({ description: "ask
|
|
186
|
-
id: Type.Optional(Type.String({ description: "
|
|
187
|
-
question: Type.Optional(Type.String({ description: "
|
|
188
|
-
answer: Type.Optional(Type.String({ description: "
|
|
189
|
-
reason: Type.Optional(Type.String({ description: "
|
|
190
|
-
choices: Type.Optional(Type.Array(Type.String(), { description: "
|
|
191
|
-
blocking: Type.Optional(Type.Boolean({ description: "
|
|
192
|
+
action: Type.Optional(Type.String({ description: "操作类型:ask、answer 或 list,默认 ask。" })),
|
|
193
|
+
id: Type.Optional(Type.String({ description: "回答问题时的问题编号,例如 Q-001。" })),
|
|
194
|
+
question: Type.Optional(Type.String({ description: "提问内容。只能是一个短问题,不要写编号清单或多个问题。" })),
|
|
195
|
+
answer: Type.Optional(Type.String({ description: "用户答案,action=answer 时使用。" })),
|
|
196
|
+
reason: Type.Optional(Type.String({ description: "说明为什么这个问题会阻塞当前阶段。" })),
|
|
197
|
+
choices: Type.Optional(Type.Array(Type.String(), { description: "可选项,最多 3 个简短选项。" })),
|
|
198
|
+
blocking: Type.Optional(Type.Boolean({ description: "是否阻塞阶段推进,默认 true。" })),
|
|
192
199
|
}),
|
|
193
200
|
|
|
194
201
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
195
202
|
const run = readActiveRun(ctx.cwd);
|
|
196
203
|
if (!run) {
|
|
197
204
|
return {
|
|
198
|
-
content: [{ type: "text", text: "
|
|
205
|
+
content: [{ type: "text", text: "当前没有 active Kingdee Harness run。请先使用 /kd-start <需求> 创建。" }],
|
|
199
206
|
details: { error: "no-active-run" },
|
|
200
207
|
};
|
|
201
208
|
}
|
|
@@ -209,34 +216,34 @@ const kdQuestionTool = defineTool({
|
|
|
209
216
|
if (action === "answer") {
|
|
210
217
|
if (!params.id || !params.answer) {
|
|
211
218
|
return {
|
|
212
|
-
content: [{ type: "text", text: "kd_question action=answer
|
|
219
|
+
content: [{ type: "text", text: "kd_question action=answer 需要同时提供 id 和 answer。" }],
|
|
213
220
|
details: { error: "missing-answer-params" },
|
|
214
221
|
};
|
|
215
222
|
}
|
|
216
223
|
const answered = answerQuestion(ctx.cwd, run, params.id, params.answer);
|
|
217
224
|
if (!answered) {
|
|
218
225
|
return {
|
|
219
|
-
content: [{ type: "text", text:
|
|
226
|
+
content: [{ type: "text", text: `未找到问题:${params.id}` }],
|
|
220
227
|
details: { error: "question-not-found", id: params.id },
|
|
221
228
|
};
|
|
222
229
|
}
|
|
223
|
-
appendQuestionEventToArtifact(ctx.cwd, run, [`-
|
|
230
|
+
appendQuestionEventToArtifact(ctx.cwd, run, [`- 已回答 ${answered.id}:${answered.answer}`]);
|
|
224
231
|
return {
|
|
225
|
-
content: [{ type: "text", text:
|
|
232
|
+
content: [{ type: "text", text: `已记录 ${answered.id} 的答案,并刷新门禁。` }],
|
|
226
233
|
details: { question: answered, gate: readActiveRun(ctx.cwd)?.gate },
|
|
227
234
|
};
|
|
228
235
|
}
|
|
229
236
|
|
|
230
237
|
if (action !== "ask") {
|
|
231
238
|
return {
|
|
232
|
-
content: [{ type: "text", text:
|
|
239
|
+
content: [{ type: "text", text: `未知 kd_question action:${params.action}。请使用 ask、answer 或 list。` }],
|
|
233
240
|
details: { error: "unknown-action", action: params.action },
|
|
234
241
|
};
|
|
235
242
|
}
|
|
236
243
|
|
|
237
244
|
if (!params.question?.trim()) {
|
|
238
245
|
return {
|
|
239
|
-
content: [{ type: "text", text: "kd_question action=ask
|
|
246
|
+
content: [{ type: "text", text: "kd_question action=ask 需要提供 question。" }],
|
|
240
247
|
details: { error: "missing-question" },
|
|
241
248
|
};
|
|
242
249
|
}
|
|
@@ -272,15 +279,15 @@ const kdQuestionTool = defineTool({
|
|
|
272
279
|
if (interactiveAnswer) {
|
|
273
280
|
const answered = answerQuestion(ctx.cwd, run, question.id, interactiveAnswer);
|
|
274
281
|
if (answered) {
|
|
275
|
-
appendQuestionEventToArtifact(ctx.cwd, run, [`-
|
|
282
|
+
appendQuestionEventToArtifact(ctx.cwd, run, [`- 已回答 ${answered.id}:${answered.answer}`]);
|
|
276
283
|
return {
|
|
277
|
-
content: [{ type: "text", text:
|
|
284
|
+
content: [{ type: "text", text: `用户已回答 ${answered.id}:${answered.answer}` }],
|
|
278
285
|
details: { question: answered, gate: readActiveRun(ctx.cwd)?.gate, answered: true },
|
|
279
286
|
};
|
|
280
287
|
}
|
|
281
288
|
}
|
|
282
289
|
|
|
283
|
-
const text = `${formatQuestionCard(question)}\n\
|
|
290
|
+
const text = `${formatQuestionCard(question)}\n\n未获得交互式答案;在用户回答前,该问题会保持 open。`;
|
|
284
291
|
return { content: [{ type: "text", text }], details: { question, gate: readActiveRun(ctx.cwd)?.gate, answered: false } };
|
|
285
292
|
},
|
|
286
293
|
});
|
|
@@ -309,7 +316,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
309
316
|
if (!run && shouldStartHarnessFromInput(event.text)) {
|
|
310
317
|
run = createActiveRun(ctx.cwd, event.text);
|
|
311
318
|
if (ctx.hasUI) {
|
|
312
|
-
ctx.ui.notify(
|
|
319
|
+
ctx.ui.notify(`已启动 Kingdee Harness run:${run.id}(${run.profile?.product}/${run.profile?.techStack})`, "info");
|
|
313
320
|
}
|
|
314
321
|
}
|
|
315
322
|
|
|
@@ -341,14 +348,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
341
348
|
});
|
|
342
349
|
|
|
343
350
|
pi.registerCommand("kd-status", {
|
|
344
|
-
description: "
|
|
351
|
+
description: "查看当前 Kingdee Harness run 和门禁状态",
|
|
345
352
|
handler: async (_args, ctx) => {
|
|
346
353
|
ctx.ui.notify(formatStatus(ctx.cwd, readActiveRun(ctx.cwd)), "info");
|
|
347
354
|
},
|
|
348
355
|
});
|
|
349
356
|
|
|
350
357
|
pi.registerCommand("kd-runs", {
|
|
351
|
-
description: "
|
|
358
|
+
description: "列出当前项目的 Kingdee Harness run",
|
|
352
359
|
handler: async (_args, ctx) => {
|
|
353
360
|
const active = readActiveRun(ctx.cwd);
|
|
354
361
|
ctx.ui.notify(formatRuns(listRuns(ctx.cwd), active?.id), "info");
|
|
@@ -356,46 +363,46 @@ export default function (pi: ExtensionAPI) {
|
|
|
356
363
|
});
|
|
357
364
|
|
|
358
365
|
pi.registerCommand("kd-gate", {
|
|
359
|
-
description: "
|
|
366
|
+
description: "刷新并查看当前 Kingdee Harness 门禁",
|
|
360
367
|
handler: async (_args, ctx) => {
|
|
361
368
|
const run = requireRun(ctx.cwd);
|
|
362
369
|
if (!run) {
|
|
363
|
-
ctx.ui.notify("
|
|
370
|
+
ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求>。", "error");
|
|
364
371
|
return;
|
|
365
372
|
}
|
|
366
373
|
const refreshed = refreshGate(ctx.cwd, run);
|
|
367
|
-
ctx.ui.notify(refreshed.gate.passed ? "
|
|
374
|
+
ctx.ui.notify(refreshed.gate.passed ? "门禁通过" : `门禁阻塞:${refreshed.gate.reason}`, "info");
|
|
368
375
|
},
|
|
369
376
|
});
|
|
370
377
|
|
|
371
378
|
pi.registerCommand("kd-start", {
|
|
372
|
-
description: "
|
|
379
|
+
description: "启动一个 Kingdee Harness run:/kd-start [--product 产品] [--version 版本] <需求>",
|
|
373
380
|
handler: async (args, ctx) => {
|
|
374
381
|
const parsed = parseStartArgs(args);
|
|
375
382
|
const goal = parsed.goal;
|
|
376
383
|
if (!goal) {
|
|
377
|
-
ctx.ui.notify("
|
|
384
|
+
ctx.ui.notify("用法:/kd-start [--product 产品] [--version 版本] <需求>", "error");
|
|
378
385
|
return;
|
|
379
386
|
}
|
|
380
387
|
|
|
381
388
|
const run = createActiveRun(ctx.cwd, goal, parsed.product, parsed.version);
|
|
382
|
-
ctx.ui.notify(
|
|
389
|
+
ctx.ui.notify(`已启动 Kingdee Harness run:${run.id}(${run.profile?.product}/${run.profile?.techStack})`, "info");
|
|
383
390
|
sendWorkflowPrompt(pi, ctx, run, `继续 KCode Harness run ${run.id}:${goal}`);
|
|
384
391
|
},
|
|
385
392
|
});
|
|
386
393
|
|
|
387
394
|
pi.registerCommand("kd-resume", {
|
|
388
|
-
description: "
|
|
395
|
+
description: "接续当前 Kingdee Harness run,并注入已落盘上下文",
|
|
389
396
|
handler: async (args, ctx) => {
|
|
390
397
|
const run = requireRun(ctx.cwd);
|
|
391
398
|
if (!run) {
|
|
392
|
-
ctx.ui.notify("
|
|
399
|
+
ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求> 或 /kd-runs。", "error");
|
|
393
400
|
return;
|
|
394
401
|
}
|
|
395
402
|
|
|
396
403
|
const note = args.trim();
|
|
397
404
|
const message = [
|
|
398
|
-
`继续 KCode Harness run ${run.id}:${run.goal ?? "
|
|
405
|
+
`继续 KCode Harness run ${run.id}:${run.goal ?? "未知需求"}`,
|
|
399
406
|
note ? `用户补充:${note}` : undefined,
|
|
400
407
|
]
|
|
401
408
|
.filter(Boolean)
|
|
@@ -405,64 +412,64 @@ export default function (pi: ExtensionAPI) {
|
|
|
405
412
|
});
|
|
406
413
|
|
|
407
414
|
pi.registerCommand("kd-switch", {
|
|
408
|
-
description: "
|
|
415
|
+
description: "切换当前 active Kingdee Harness run:/kd-switch <run-id>",
|
|
409
416
|
handler: async (args, ctx) => {
|
|
410
417
|
const id = args.trim();
|
|
411
418
|
if (!id) {
|
|
412
|
-
ctx.ui.notify("
|
|
419
|
+
ctx.ui.notify("用法:/kd-switch <run-id>", "error");
|
|
413
420
|
return;
|
|
414
421
|
}
|
|
415
422
|
|
|
416
423
|
const run = switchActiveRun(ctx.cwd, id);
|
|
417
424
|
if (!run) {
|
|
418
|
-
ctx.ui.notify(
|
|
425
|
+
ctx.ui.notify(`未找到 Kingdee Harness run:${id}。请用 /kd-runs 查看列表。`, "error");
|
|
419
426
|
return;
|
|
420
427
|
}
|
|
421
428
|
|
|
422
|
-
ctx.ui.notify(
|
|
429
|
+
ctx.ui.notify(`已切换 active Kingdee Harness run:${run.id}(${run.phase})`, "info");
|
|
423
430
|
},
|
|
424
431
|
});
|
|
425
432
|
|
|
426
433
|
pi.registerCommand("kd-finish", {
|
|
427
|
-
description: "
|
|
434
|
+
description: "标记当前 Kingdee Harness run 完成,并清除 active run",
|
|
428
435
|
handler: async (_args, ctx) => {
|
|
429
436
|
const run = requireRun(ctx.cwd);
|
|
430
437
|
if (!run) {
|
|
431
|
-
ctx.ui.notify("
|
|
438
|
+
ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求>。", "error");
|
|
432
439
|
return;
|
|
433
440
|
}
|
|
434
441
|
|
|
435
442
|
const finished = finishActiveRun(ctx.cwd, run);
|
|
436
|
-
ctx.ui.notify(
|
|
443
|
+
ctx.ui.notify(`已完成 Kingdee Harness run:${finished.id}。下一个功能点请使用 /kd-start <需求>。`, "info");
|
|
437
444
|
},
|
|
438
445
|
});
|
|
439
446
|
|
|
440
447
|
pi.registerCommand("kd-product", {
|
|
441
|
-
description: "
|
|
448
|
+
description: "设置当前金蝶产品画像:/kd-product <flagship|cosmic|xinghan|cangqiong|enterprise> [--version 版本]",
|
|
442
449
|
handler: async (args, ctx) => {
|
|
443
450
|
const run = requireRun(ctx.cwd);
|
|
444
451
|
if (!run) {
|
|
445
|
-
ctx.ui.notify("
|
|
452
|
+
ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求>。", "error");
|
|
446
453
|
return;
|
|
447
454
|
}
|
|
448
455
|
|
|
449
456
|
const parsed = parseProductArgs(args);
|
|
450
457
|
if (!parsed) {
|
|
451
|
-
ctx.ui.notify("
|
|
458
|
+
ctx.ui.notify("用法:/kd-product <flagship|cosmic|xinghan|cangqiong|enterprise> [--version 版本]", "error");
|
|
452
459
|
return;
|
|
453
460
|
}
|
|
454
461
|
|
|
455
462
|
const updated = updateProductProfile(ctx.cwd, run, parsed.product, parsed.version);
|
|
456
|
-
ctx.ui.notify(
|
|
463
|
+
ctx.ui.notify(`产品画像:${updated.profile?.product}/${updated.profile?.techStack}/${updated.profile?.language}`, "info");
|
|
457
464
|
},
|
|
458
465
|
});
|
|
459
466
|
|
|
460
467
|
pi.registerCommand("kd-advance", {
|
|
461
|
-
description:
|
|
468
|
+
description: `推进 Kingdee run 到下一阶段,或指定阶段:${PHASE_ORDER.join("|")}`,
|
|
462
469
|
handler: async (args, ctx) => {
|
|
463
470
|
const run = requireRun(ctx.cwd);
|
|
464
471
|
if (!run) {
|
|
465
|
-
ctx.ui.notify("
|
|
472
|
+
ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求>。", "error");
|
|
466
473
|
return;
|
|
467
474
|
}
|
|
468
475
|
|
|
@@ -470,7 +477,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
470
477
|
let requested: KdPhase | undefined;
|
|
471
478
|
if (requestedText) {
|
|
472
479
|
if (!isKdPhase(requestedText)) {
|
|
473
|
-
ctx.ui.notify(
|
|
480
|
+
ctx.ui.notify(`未知阶段 "${requestedText}"。可用阶段:${PHASE_ORDER.join(", ")}`, "error");
|
|
474
481
|
return;
|
|
475
482
|
}
|
|
476
483
|
requested = requestedText;
|
|
@@ -482,67 +489,67 @@ export default function (pi: ExtensionAPI) {
|
|
|
482
489
|
});
|
|
483
490
|
|
|
484
491
|
pi.registerCommand("kd-artifact", {
|
|
485
|
-
description: "
|
|
492
|
+
description: "创建或更新阶段文档:/kd-artifact [阶段] [内容]",
|
|
486
493
|
handler: async (args, ctx) => {
|
|
487
494
|
const run = requireRun(ctx.cwd);
|
|
488
495
|
if (!run) {
|
|
489
|
-
ctx.ui.notify("
|
|
496
|
+
ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求>。", "error");
|
|
490
497
|
return;
|
|
491
498
|
}
|
|
492
499
|
|
|
493
500
|
const parsed = parseArtifactArgs(args, run.phase);
|
|
494
501
|
if (!parsed) {
|
|
495
|
-
ctx.ui.notify("
|
|
502
|
+
ctx.ui.notify("用法:/kd-artifact [阶段] [内容]", "error");
|
|
496
503
|
return;
|
|
497
504
|
}
|
|
498
505
|
|
|
499
506
|
const path = parsed.content
|
|
500
507
|
? updatePhaseArtifact(ctx.cwd, run, parsed.phase, parsed.content)
|
|
501
508
|
: ensurePhaseArtifact(ctx.cwd, run, parsed.phase);
|
|
502
|
-
ctx.ui.notify(
|
|
509
|
+
ctx.ui.notify(`阶段文档已就绪:${path}`, "info");
|
|
503
510
|
},
|
|
504
511
|
});
|
|
505
512
|
|
|
506
513
|
pi.registerCommand("kd-answer", {
|
|
507
|
-
description: "
|
|
514
|
+
description: "回答一个阻塞中的 Kingdee Harness 问题:/kd-answer Q-001 <答案>",
|
|
508
515
|
handler: async (args, ctx) => {
|
|
509
516
|
const run = requireRun(ctx.cwd);
|
|
510
517
|
if (!run) {
|
|
511
|
-
ctx.ui.notify("
|
|
518
|
+
ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求>。", "error");
|
|
512
519
|
return;
|
|
513
520
|
}
|
|
514
521
|
const [id, ...answerParts] = args.trim().split(/\s+/);
|
|
515
522
|
const answer = answerParts.join(" ").trim();
|
|
516
523
|
if (!id || !answer) {
|
|
517
|
-
ctx.ui.notify("
|
|
524
|
+
ctx.ui.notify("用法:/kd-answer Q-001 <答案>", "error");
|
|
518
525
|
return;
|
|
519
526
|
}
|
|
520
527
|
const answered = answerQuestion(ctx.cwd, run, id, answer);
|
|
521
528
|
if (!answered) {
|
|
522
|
-
ctx.ui.notify(
|
|
529
|
+
ctx.ui.notify(`未找到问题:${id}`, "error");
|
|
523
530
|
return;
|
|
524
531
|
}
|
|
525
|
-
appendQuestionEventToArtifact(ctx.cwd, run, [`-
|
|
526
|
-
ctx.ui.notify(
|
|
532
|
+
appendQuestionEventToArtifact(ctx.cwd, run, [`- 已回答 ${answered.id}:${answered.answer}`]);
|
|
533
|
+
ctx.ui.notify(`已记录 ${answered.id} 的答案`, "info");
|
|
527
534
|
},
|
|
528
535
|
});
|
|
529
536
|
}
|
|
530
537
|
|
|
531
538
|
function formatQuestions(run: NonNullable<ReturnType<typeof readActiveRun>>): string {
|
|
532
539
|
const questions = run.questions ?? [];
|
|
533
|
-
if (questions.length === 0) return "
|
|
540
|
+
if (questions.length === 0) return "尚未记录 Harness 问题。";
|
|
534
541
|
return questions.map(formatQuestionCard).join("\n\n");
|
|
535
542
|
}
|
|
536
543
|
|
|
537
544
|
function formatRuns(runs: NonNullable<ReturnType<typeof readActiveRun>>[], activeId?: string): string {
|
|
538
|
-
if (runs.length === 0) return "
|
|
545
|
+
if (runs.length === 0) return "当前项目没有 Kingdee Harness run。请使用 /kd-start <需求> 创建。";
|
|
539
546
|
return runs
|
|
540
547
|
.map((run) =>
|
|
541
548
|
[
|
|
542
549
|
`${run.id === activeId ? "*" : "-"} ${run.id}`,
|
|
543
|
-
`
|
|
544
|
-
`
|
|
545
|
-
`
|
|
550
|
+
` 需求:${run.goal ?? "未知"}`,
|
|
551
|
+
` 状态:${run.status ?? "active"} | 阶段:${run.phase} | 产品:${run.profile?.product ?? run.product ?? "unknown"}`,
|
|
552
|
+
` 更新时间:${run.updatedAt ?? "未知"}`,
|
|
546
553
|
].join("\n"),
|
|
547
554
|
)
|
|
548
555
|
.join("\n\n");
|
|
@@ -550,14 +557,14 @@ function formatRuns(runs: NonNullable<ReturnType<typeof readActiveRun>>[], activ
|
|
|
550
557
|
|
|
551
558
|
function formatQuestionCard(question: NonNullable<NonNullable<ReturnType<typeof readActiveRun>>["questions"]>[number]): string {
|
|
552
559
|
const lines = [
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
question.reason ?
|
|
557
|
-
question.choices?.length ?
|
|
558
|
-
question.answer ?
|
|
560
|
+
`问题 ${question.id} [${question.status}${question.blocking ? ", blocking" : ""}]`,
|
|
561
|
+
`阶段:${question.phase}`,
|
|
562
|
+
`问题:${question.question}`,
|
|
563
|
+
question.reason ? `原因:${question.reason}` : undefined,
|
|
564
|
+
question.choices?.length ? `选项:${question.choices.join(" | ")}` : undefined,
|
|
565
|
+
question.answer ? `答案:${question.answer}` : undefined,
|
|
559
566
|
question.status === "open"
|
|
560
|
-
?
|
|
567
|
+
? `如果有弹窗,请直接回答;否则请在对话中回复,并用 kd_question action=answer id=${question.id} answer=<答案> 记录。`
|
|
561
568
|
: undefined,
|
|
562
569
|
];
|
|
563
570
|
return lines.filter(Boolean).join("\n");
|
|
@@ -589,12 +596,12 @@ async function askQuestionInteractively(
|
|
|
589
596
|
function questionBatchProblem(question: string, choices?: string[]): string | undefined {
|
|
590
597
|
const text = question.trim();
|
|
591
598
|
const numberedItems = text.split(/\r?\n/).filter((line) => /^\s*(\d+[\.\)、)]|[-*]\s+)/.test(line)).length;
|
|
592
|
-
if (numberedItems >= 2) return "kd_question
|
|
593
|
-
if ((text.match(/[??]/g) ?? []).length > 1) return "kd_question
|
|
594
|
-
if (text.length > 220) return "kd_question
|
|
595
|
-
if ((choices?.length ?? 0) > 3) return "kd_question
|
|
599
|
+
if (numberedItems >= 2) return "kd_question 拒绝了批量清单式问题。";
|
|
600
|
+
if ((text.match(/[??]/g) ?? []).length > 1) return "kd_question 拒绝了一次包含多个问题的提问。";
|
|
601
|
+
if (text.length > 220) return "kd_question 拒绝了过长问题。请只问当前最小的阻塞问题。";
|
|
602
|
+
if ((choices?.length ?? 0) > 3) return "kd_question 拒绝了过多选项。最多提供 3 个简短选项。";
|
|
596
603
|
if (choices?.some((choice) => choice.length > 40 || /[\r\n??]/.test(choice))) {
|
|
597
|
-
return "kd_question
|
|
604
|
+
return "kd_question 拒绝了复杂选项。选项必须是短标签,不能嵌套问题。";
|
|
598
605
|
}
|
|
599
606
|
return undefined;
|
|
600
607
|
}
|
|
@@ -602,14 +609,14 @@ function questionBatchProblem(question: string, choices?: string[]): string | un
|
|
|
602
609
|
function formatQuestionArtifactLines(question: NonNullable<NonNullable<ReturnType<typeof readActiveRun>>["questions"]>[number]): string[] {
|
|
603
610
|
return [
|
|
604
611
|
`- ${question.id} [${question.blocking ? "blocking" : "non-blocking"}] ${question.question}`,
|
|
605
|
-
question.reason ? ` -
|
|
606
|
-
question.choices?.length ? ` -
|
|
607
|
-
" -
|
|
612
|
+
question.reason ? ` - 原因:${question.reason}` : undefined,
|
|
613
|
+
question.choices?.length ? ` - 选项:${question.choices.join(" | ")}` : undefined,
|
|
614
|
+
" - 状态:open",
|
|
608
615
|
].filter(Boolean) as string[];
|
|
609
616
|
}
|
|
610
617
|
|
|
611
618
|
function appendQuestionEventToArtifact(cwd: string, run: NonNullable<ReturnType<typeof readActiveRun>>, lines: string[]): void {
|
|
612
619
|
const existing = readArtifact(cwd, run, run.phase) ?? "";
|
|
613
|
-
const section = ["", "## Harness
|
|
614
|
-
updatePhaseArtifact(cwd, run, run.phase, existing.includes("## Harness
|
|
620
|
+
const section = ["", "## Harness 问题", "", ...lines, ""].join("\n");
|
|
621
|
+
updatePhaseArtifact(cwd, run, run.phase, existing.includes("## Harness 问题") ? `${existing.trimEnd()}\n${lines.join("\n")}\n` : `${existing.trimEnd()}${section}`);
|
|
615
622
|
}
|
|
@@ -32,7 +32,7 @@ function readActiveRun(cwd: string): ActiveRun | undefined {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
function formatProduct(run: ActiveRun | undefined): string {
|
|
35
|
-
if (!run) return "
|
|
35
|
+
if (!run) return "未选择";
|
|
36
36
|
const product = run.profile?.product ?? run.product ?? "unknown";
|
|
37
37
|
const techStack = run.profile?.techStack ?? "unknown";
|
|
38
38
|
const language = run.profile?.language ?? "unknown";
|
|
@@ -40,13 +40,13 @@ function formatProduct(run: ActiveRun | undefined): string {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function formatPhase(phase: ActiveRun["phase"]): string {
|
|
43
|
-
return phase ?? "
|
|
43
|
+
return phase ?? "空闲";
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function formatGate(run: ActiveRun | undefined): string {
|
|
47
|
-
if (!run?.gate) return "
|
|
48
|
-
if (run.gate.passed) return "
|
|
49
|
-
return
|
|
47
|
+
if (!run?.gate) return "门禁:待检查";
|
|
48
|
+
if (run.gate.passed) return "门禁:通过";
|
|
49
|
+
return `门禁:阻塞${run.gate.reason ? ` - ${run.gate.reason}` : ""}`;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
function padOrTrim(text: string, width: number): string {
|
|
@@ -60,7 +60,7 @@ function logoLines(theme: Theme): string[] {
|
|
|
60
60
|
const muted = (text: string) => theme.fg("muted", text);
|
|
61
61
|
|
|
62
62
|
return [
|
|
63
|
-
`${accent("KCode")} ${muted("
|
|
63
|
+
`${accent("KCode")} ${muted("金蝶 Pi Harness")}`,
|
|
64
64
|
`${accent("=====")} ${muted("discuss -> spec -> plan -> execute -> verify -> ship")}`,
|
|
65
65
|
];
|
|
66
66
|
}
|
|
@@ -76,15 +76,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
76
76
|
const phase = formatPhase(run?.phase);
|
|
77
77
|
const product = formatProduct(run);
|
|
78
78
|
const gate = formatGate(run);
|
|
79
|
-
const risk = run?.risk ?? "
|
|
80
|
-
const runId = run?.id ?? "
|
|
79
|
+
const risk = run?.risk ?? "未知";
|
|
80
|
+
const runId = run?.id ?? "无";
|
|
81
81
|
|
|
82
82
|
const status = [
|
|
83
|
-
theme.fg("muted", "
|
|
83
|
+
theme.fg("muted", "阶段:"),
|
|
84
84
|
theme.fg("accent", phase),
|
|
85
|
-
theme.fg("muted", " |
|
|
85
|
+
theme.fg("muted", " | 产品:"),
|
|
86
86
|
theme.fg("text", product),
|
|
87
|
-
theme.fg("muted", " |
|
|
87
|
+
theme.fg("muted", " | 风险:"),
|
|
88
88
|
theme.fg(risk === "high" ? "error" : risk === "medium" ? "warning" : "success", risk),
|
|
89
89
|
theme.fg("muted", " | "),
|
|
90
90
|
theme.fg(run?.gate?.passed === false ? "error" : "muted", gate),
|
|
@@ -94,7 +94,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
94
94
|
"",
|
|
95
95
|
...logoLines(theme).map((line) => padOrTrim(line, width)),
|
|
96
96
|
padOrTrim(status, width),
|
|
97
|
-
padOrTrim(theme.fg("dim", `run
|
|
97
|
+
padOrTrim(theme.fg("dim", `run:${runId}`), width),
|
|
98
98
|
"",
|
|
99
99
|
];
|
|
100
100
|
},
|
|
@@ -104,19 +104,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
pi.registerCommand("kcode-header", {
|
|
107
|
-
description: "
|
|
107
|
+
description: "恢复 KCode 金蝶标题栏",
|
|
108
108
|
handler: async (_args, ctx) => {
|
|
109
109
|
if (ctx.mode !== "tui") return;
|
|
110
|
-
ctx.ui.notify("
|
|
110
|
+
ctx.ui.notify("请重启会话或重新加载扩展以恢复 KCode 标题栏。", "info");
|
|
111
111
|
},
|
|
112
112
|
});
|
|
113
113
|
|
|
114
114
|
pi.registerCommand("builtin-header", {
|
|
115
|
-
description: "
|
|
115
|
+
description: "恢复 Pi 内置标题栏",
|
|
116
116
|
handler: async (_args, ctx) => {
|
|
117
117
|
if (ctx.mode !== "tui") return;
|
|
118
118
|
ctx.ui.setHeader(undefined);
|
|
119
|
-
ctx.ui.notify("
|
|
119
|
+
ctx.ui.notify("已恢复 Pi 内置标题栏", "info");
|
|
120
120
|
},
|
|
121
121
|
});
|
|
122
122
|
}
|