kcode-pi 0.1.14 → 0.1.15
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 +27 -25
- package/dist/context/project-context.js +20 -20
- package/extensions/kingdee-harness.ts +80 -80
- package/extensions/kingdee-header.ts +16 -16
- package/extensions/kingdee-tools.ts +82 -82
- 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 +47 -47
- package/src/harness/format.ts +12 -12
- package/src/harness/gates.ts +5 -5
- package/src/harness/plan-steps.ts +3 -3
- 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
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
KCode 是面向金蝶开发的 Pi 工作环境启动器。安装后会提供全局命令 `kcode`,自动携带 Pi CLI,并在当前业务项目中加载金蝶专属工具、skills、prompts、themes 和知识库。
|
|
4
4
|
|
|
5
|
+
KCode 默认面向中文用户:README、`kcode help`、Pi 内 `/kd-*` 命令说明、工具参数说明、内置 prompts 和 Harness 阶段文档模板均使用中文。命令名和产品标识仍保持英文,例如 `/kd-start`、`kd_search`、`flagship`。
|
|
6
|
+
|
|
5
7
|
KCode 不要求你单独安装全局 `pi` 命令,也不会修改用户全局 Pi 配置。它只维护当前项目的:
|
|
6
8
|
|
|
7
9
|
```text
|
|
@@ -289,17 +291,17 @@ kcode start --provider openai --model gpt-4o
|
|
|
289
291
|
进入 `kcode start` 后,可以使用以下 KCode 命令:
|
|
290
292
|
|
|
291
293
|
```text
|
|
292
|
-
/kd-start [--product
|
|
293
|
-
/kd-product
|
|
294
|
+
/kd-start [--product 产品] [--version 版本] <需求>
|
|
295
|
+
/kd-product <产品> [--version 版本]
|
|
294
296
|
/kd-status
|
|
295
297
|
/kd-runs
|
|
296
298
|
/kd-switch <run-id>
|
|
297
299
|
/kd-resume [补充说明]
|
|
298
300
|
/kd-finish
|
|
299
301
|
/kd-gate
|
|
300
|
-
/kd-advance [
|
|
301
|
-
/kd-artifact [
|
|
302
|
-
/kd-answer Q-001
|
|
302
|
+
/kd-advance [阶段]
|
|
303
|
+
/kd-artifact [阶段] [内容]
|
|
304
|
+
/kd-answer Q-001 <答案>
|
|
303
305
|
```
|
|
304
306
|
|
|
305
307
|
典型开始方式:
|
|
@@ -393,8 +395,8 @@ ship 汇总变更、验证证据、风险和后续事项
|
|
|
393
395
|
|
|
394
396
|
- 未进入 `execute` 阶段时,KCode 会阻止写入 Java/XML/SQL 等产品代码。
|
|
395
397
|
- 进入 `execute` 前必须已有 `PLAN.md` 和必要证据。
|
|
396
|
-
- `PLAN.md` 必须包含 `##
|
|
397
|
-
- `PLAN.md` 必须包含 `## TDD /
|
|
398
|
+
- `PLAN.md` 必须包含 `## 执行步骤`,并用 `- [ ] STEP-001:...` 格式拆分可跟踪步骤。
|
|
399
|
+
- `PLAN.md` 必须包含 `## TDD / 红绿检查`,声明红灯证据、绿灯证据和测试/检查命令。
|
|
398
400
|
- 红绿检查不等于必须写 JUnit。金蝶项目不要为了满足门禁引入额外 jar 或测试框架。
|
|
399
401
|
- 写生产源码前必须已有 `evidence/tdd-red.md`,内容可以是 API/基类/方法签名检查、元数据检查、编译检查、既有测试框架或外部接口最小验证的失败输出。
|
|
400
402
|
- 进入 `execute` 后,只允许写入 `PLAN.md` 明确列出的源码文件;如果临时发现要改新文件,必须先回到 plan 更新 `PLAN.md`。
|
|
@@ -405,36 +407,36 @@ ship 汇总变更、验证证据、风险和后续事项
|
|
|
405
407
|
示例计划步骤:
|
|
406
408
|
|
|
407
409
|
```markdown
|
|
408
|
-
##
|
|
410
|
+
## 执行步骤
|
|
409
411
|
|
|
410
|
-
- [ ] STEP-001
|
|
411
|
-
- [ ] STEP-002
|
|
412
|
-
- [ ] STEP-003
|
|
413
|
-
- [ ] STEP-004
|
|
414
|
-
- [ ] STEP-005
|
|
412
|
+
- [ ] STEP-001:检查现有插件基类、包名和目标字段。
|
|
413
|
+
- [ ] STEP-002:运行 API/元数据/编译等红灯检查并记录证据。
|
|
414
|
+
- [ ] STEP-003:修改 `code/scm/plugin/src/main/java/.../PurchaseOrderPlugin.java`。
|
|
415
|
+
- [ ] STEP-004:运行同一类绿灯检查并记录证据。
|
|
416
|
+
- [ ] STEP-005:记录变更文件和验证证据。
|
|
415
417
|
```
|
|
416
418
|
|
|
417
419
|
示例红绿检查:
|
|
418
420
|
|
|
419
421
|
```markdown
|
|
420
|
-
## TDD /
|
|
422
|
+
## TDD / 红绿检查
|
|
421
423
|
|
|
422
|
-
-
|
|
423
|
-
-
|
|
424
|
-
-
|
|
425
|
-
-
|
|
424
|
+
- 红灯证据:evidence/tdd-red.md
|
|
425
|
+
- 绿灯证据:evidence/tdd-green.md
|
|
426
|
+
- 红绿检查命令或工具:kd_sdk_signature / kd_cosmic_metadata / kd_check / build
|
|
427
|
+
- 不要为了满足门禁引入第三方测试 jar 或测试框架。
|
|
426
428
|
```
|
|
427
429
|
|
|
428
430
|
示例执行记录:
|
|
429
431
|
|
|
430
432
|
```markdown
|
|
431
|
-
##
|
|
433
|
+
## 步骤结果
|
|
432
434
|
|
|
433
|
-
- [x] STEP-001
|
|
434
|
-
- [x] STEP-002
|
|
435
|
-
- [x] STEP-003
|
|
436
|
-
- [x] STEP-004
|
|
437
|
-
- [x] STEP-005
|
|
435
|
+
- [x] STEP-001:已完成。证据:evidence/step-001.md
|
|
436
|
+
- [x] STEP-002:已完成。证据:evidence/step-002.md
|
|
437
|
+
- [x] STEP-003:已完成。证据:evidence/step-003.md
|
|
438
|
+
- [x] STEP-004:已完成。证据:evidence/tdd-green.md
|
|
439
|
+
- [x] STEP-005:已完成。证据:evidence/step-005.md
|
|
438
440
|
```
|
|
439
441
|
|
|
440
442
|
如果 LLM 跳过步骤、没有记录 evidence,或只是口头声明完成,KCode 不允许进入 `verify`。
|
|
@@ -449,7 +451,7 @@ ship 汇总变更、验证证据、风险和后续事项
|
|
|
449
451
|
|
|
450
452
|
得到回答后,再继续问触发时机、库存条件、弹窗内容或插件位置。
|
|
451
453
|
|
|
452
|
-
在 `kcode start` 的交互式 TUI 中,`kd_question` 会弹出 KCode
|
|
454
|
+
在 `kcode start` 的交互式 TUI 中,`kd_question` 会弹出 KCode 问题选择/输入对话,并在用户选择或输入后自动记录答案。非交互模式或 UI 不可用时,问题会显示在对话里,用户直接回复;Agent 读取回复后再记录答案。也可以手动执行:
|
|
453
455
|
|
|
454
456
|
```text
|
|
455
457
|
/kd-answer Q-001 采购订单
|
|
@@ -47,33 +47,33 @@ export function generateProjectContext(cwd) {
|
|
|
47
47
|
const buildFiles = scan.files.filter((file) => BUILD_FILE_NAMES.has(basename(file).toLowerCase()) || isSolutionOrProject(file));
|
|
48
48
|
const sourceSamples = scan.files.filter((file) => SOURCE_EXTENSIONS.has(extname(file).toLowerCase())).slice(0, MAX_SOURCE_SAMPLES);
|
|
49
49
|
return [
|
|
50
|
-
"# KCode
|
|
50
|
+
"# KCode 项目上下文",
|
|
51
51
|
"",
|
|
52
|
-
`-
|
|
53
|
-
`-
|
|
54
|
-
`-
|
|
52
|
+
`- 项目根目录:${cwd}`,
|
|
53
|
+
`- 项目名称:${basename(cwd)}`,
|
|
54
|
+
`- 生成时间:${new Date().toISOString()}`,
|
|
55
55
|
"",
|
|
56
|
-
"##
|
|
56
|
+
"## 持久规则",
|
|
57
57
|
"",
|
|
58
|
-
"-
|
|
59
|
-
"-
|
|
60
|
-
"-
|
|
61
|
-
"-
|
|
62
|
-
"-
|
|
63
|
-
"-
|
|
58
|
+
"- 本文件是 KCode 的项目记忆,计划或编辑代码前必须读取。",
|
|
59
|
+
"- 业务需求不得生成 demo/sample/scaffold 代码。",
|
|
60
|
+
"- 不要假设模块结构。必须基于下方真实路径,并在编辑前确认目标文件。",
|
|
61
|
+
"- 调用文件工具时优先使用项目相对路径。在 Windows 中,不要把路径改写为 /mnt/<drive>/... 或 /<drive>/...;只有确需绝对路径时才使用 Windows 路径。",
|
|
62
|
+
"- 如果本文件过期,计划前先运行 `kcode context --refresh` 重新生成。",
|
|
63
|
+
"- 只有 Harness 进入 `execute` 且 PLAN.md 写明真实目标路径后,才能写产品代码。",
|
|
64
64
|
"",
|
|
65
|
-
"##
|
|
65
|
+
"## 项目结构摘要",
|
|
66
66
|
"",
|
|
67
|
-
`-
|
|
68
|
-
`-
|
|
69
|
-
`-
|
|
70
|
-
`-
|
|
67
|
+
`- 是否存在 code 目录:${codeDir ? "是" : "否"}`,
|
|
68
|
+
`- 可能的源码根:${formatList(likelyRoots)}`,
|
|
69
|
+
`- 构建文件:${formatList(buildFiles)}`,
|
|
70
|
+
`- 识别到的模块:${formatList(modules)}`,
|
|
71
71
|
"",
|
|
72
|
-
"##
|
|
72
|
+
"## 源码样例",
|
|
73
73
|
"",
|
|
74
74
|
formatBlockList(sourceSamples),
|
|
75
75
|
"",
|
|
76
|
-
"##
|
|
76
|
+
"## 顶层目录",
|
|
77
77
|
"",
|
|
78
78
|
formatBlockList(scan.directories.filter((dir) => !dir.includes("/") && !dir.includes("\\")).sort()),
|
|
79
79
|
"",
|
|
@@ -181,11 +181,11 @@ function isSolutionOrProject(file) {
|
|
|
181
181
|
return [".sln", ".csproj"].includes(extname(file).toLowerCase());
|
|
182
182
|
}
|
|
183
183
|
function formatList(values) {
|
|
184
|
-
return values.length > 0 ? values.join(", ") : "
|
|
184
|
+
return values.length > 0 ? values.join(", ") : "未识别";
|
|
185
185
|
}
|
|
186
186
|
function formatBlockList(values) {
|
|
187
187
|
if (values.length === 0)
|
|
188
|
-
return "-
|
|
188
|
+
return "- 未识别";
|
|
189
189
|
return values.map((value) => `- ${value}`).join("\n");
|
|
190
190
|
}
|
|
191
191
|
function normalize(path) {
|
|
@@ -84,7 +84,7 @@ function sendWorkflowPrompt(pi: ExtensionAPI, ctx: ExtensionContext, run: NonNul
|
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
86
|
pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
87
|
-
if (ctx.hasUI) ctx.ui.notify("KCode
|
|
87
|
+
if (ctx.hasUI) ctx.ui.notify("KCode 工作流消息已排队。", "info");
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
function workflowPromptForRun(cwd: string, run: NonNullable<ReturnType<typeof readActiveRun>>, userText: string): string {
|
|
@@ -163,8 +163,8 @@ function codeWriteBlockReason(cwd: string, path: string | undefined): string | u
|
|
|
163
163
|
|
|
164
164
|
const kdPlanStatusTool = defineTool({
|
|
165
165
|
name: "kd_plan_status",
|
|
166
|
-
label: "KD
|
|
167
|
-
description: "
|
|
166
|
+
label: "KD 状态",
|
|
167
|
+
description: "查看当前金蝶 Harness run、当前阶段、必需文档和门禁状态。",
|
|
168
168
|
parameters: Type.Object({}),
|
|
169
169
|
|
|
170
170
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
@@ -178,24 +178,24 @@ const kdPlanStatusTool = defineTool({
|
|
|
178
178
|
|
|
179
179
|
const kdQuestionTool = defineTool({
|
|
180
180
|
name: "kd_question",
|
|
181
|
-
label: "KD
|
|
181
|
+
label: "KD 问题",
|
|
182
182
|
description:
|
|
183
|
-
"
|
|
183
|
+
"创建、回答或列出金蝶 Harness 结构化问题。每次只能问一个最阻塞的短问题,不要批量列清单。",
|
|
184
184
|
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: "
|
|
185
|
+
action: Type.Optional(Type.String({ description: "操作类型:ask、answer 或 list,默认 ask。" })),
|
|
186
|
+
id: Type.Optional(Type.String({ description: "回答问题时的问题编号,例如 Q-001。" })),
|
|
187
|
+
question: Type.Optional(Type.String({ description: "提问内容。只能是一个短问题,不要写编号清单或多个问题。" })),
|
|
188
|
+
answer: Type.Optional(Type.String({ description: "用户答案,action=answer 时使用。" })),
|
|
189
|
+
reason: Type.Optional(Type.String({ description: "说明为什么这个问题会阻塞当前阶段。" })),
|
|
190
|
+
choices: Type.Optional(Type.Array(Type.String(), { description: "可选项,最多 3 个简短选项。" })),
|
|
191
|
+
blocking: Type.Optional(Type.Boolean({ description: "是否阻塞阶段推进,默认 true。" })),
|
|
192
192
|
}),
|
|
193
193
|
|
|
194
194
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
195
195
|
const run = readActiveRun(ctx.cwd);
|
|
196
196
|
if (!run) {
|
|
197
197
|
return {
|
|
198
|
-
content: [{ type: "text", text: "
|
|
198
|
+
content: [{ type: "text", text: "当前没有 active Kingdee Harness run。请先使用 /kd-start <需求> 创建。" }],
|
|
199
199
|
details: { error: "no-active-run" },
|
|
200
200
|
};
|
|
201
201
|
}
|
|
@@ -209,34 +209,34 @@ const kdQuestionTool = defineTool({
|
|
|
209
209
|
if (action === "answer") {
|
|
210
210
|
if (!params.id || !params.answer) {
|
|
211
211
|
return {
|
|
212
|
-
content: [{ type: "text", text: "kd_question action=answer
|
|
212
|
+
content: [{ type: "text", text: "kd_question action=answer 需要同时提供 id 和 answer。" }],
|
|
213
213
|
details: { error: "missing-answer-params" },
|
|
214
214
|
};
|
|
215
215
|
}
|
|
216
216
|
const answered = answerQuestion(ctx.cwd, run, params.id, params.answer);
|
|
217
217
|
if (!answered) {
|
|
218
218
|
return {
|
|
219
|
-
content: [{ type: "text", text:
|
|
219
|
+
content: [{ type: "text", text: `未找到问题:${params.id}` }],
|
|
220
220
|
details: { error: "question-not-found", id: params.id },
|
|
221
221
|
};
|
|
222
222
|
}
|
|
223
|
-
appendQuestionEventToArtifact(ctx.cwd, run, [`-
|
|
223
|
+
appendQuestionEventToArtifact(ctx.cwd, run, [`- 已回答 ${answered.id}:${answered.answer}`]);
|
|
224
224
|
return {
|
|
225
|
-
content: [{ type: "text", text:
|
|
225
|
+
content: [{ type: "text", text: `已记录 ${answered.id} 的答案,并刷新门禁。` }],
|
|
226
226
|
details: { question: answered, gate: readActiveRun(ctx.cwd)?.gate },
|
|
227
227
|
};
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
if (action !== "ask") {
|
|
231
231
|
return {
|
|
232
|
-
content: [{ type: "text", text:
|
|
232
|
+
content: [{ type: "text", text: `未知 kd_question action:${params.action}。请使用 ask、answer 或 list。` }],
|
|
233
233
|
details: { error: "unknown-action", action: params.action },
|
|
234
234
|
};
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
if (!params.question?.trim()) {
|
|
238
238
|
return {
|
|
239
|
-
content: [{ type: "text", text: "kd_question action=ask
|
|
239
|
+
content: [{ type: "text", text: "kd_question action=ask 需要提供 question。" }],
|
|
240
240
|
details: { error: "missing-question" },
|
|
241
241
|
};
|
|
242
242
|
}
|
|
@@ -272,15 +272,15 @@ const kdQuestionTool = defineTool({
|
|
|
272
272
|
if (interactiveAnswer) {
|
|
273
273
|
const answered = answerQuestion(ctx.cwd, run, question.id, interactiveAnswer);
|
|
274
274
|
if (answered) {
|
|
275
|
-
appendQuestionEventToArtifact(ctx.cwd, run, [`-
|
|
275
|
+
appendQuestionEventToArtifact(ctx.cwd, run, [`- 已回答 ${answered.id}:${answered.answer}`]);
|
|
276
276
|
return {
|
|
277
|
-
content: [{ type: "text", text:
|
|
277
|
+
content: [{ type: "text", text: `用户已回答 ${answered.id}:${answered.answer}` }],
|
|
278
278
|
details: { question: answered, gate: readActiveRun(ctx.cwd)?.gate, answered: true },
|
|
279
279
|
};
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
const text = `${formatQuestionCard(question)}\n\
|
|
283
|
+
const text = `${formatQuestionCard(question)}\n\n未获得交互式答案;在用户回答前,该问题会保持 open。`;
|
|
284
284
|
return { content: [{ type: "text", text }], details: { question, gate: readActiveRun(ctx.cwd)?.gate, answered: false } };
|
|
285
285
|
},
|
|
286
286
|
});
|
|
@@ -309,7 +309,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
309
309
|
if (!run && shouldStartHarnessFromInput(event.text)) {
|
|
310
310
|
run = createActiveRun(ctx.cwd, event.text);
|
|
311
311
|
if (ctx.hasUI) {
|
|
312
|
-
ctx.ui.notify(
|
|
312
|
+
ctx.ui.notify(`已启动 Kingdee Harness run:${run.id}(${run.profile?.product}/${run.profile?.techStack})`, "info");
|
|
313
313
|
}
|
|
314
314
|
}
|
|
315
315
|
|
|
@@ -341,14 +341,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
341
341
|
});
|
|
342
342
|
|
|
343
343
|
pi.registerCommand("kd-status", {
|
|
344
|
-
description: "
|
|
344
|
+
description: "查看当前 Kingdee Harness run 和门禁状态",
|
|
345
345
|
handler: async (_args, ctx) => {
|
|
346
346
|
ctx.ui.notify(formatStatus(ctx.cwd, readActiveRun(ctx.cwd)), "info");
|
|
347
347
|
},
|
|
348
348
|
});
|
|
349
349
|
|
|
350
350
|
pi.registerCommand("kd-runs", {
|
|
351
|
-
description: "
|
|
351
|
+
description: "列出当前项目的 Kingdee Harness run",
|
|
352
352
|
handler: async (_args, ctx) => {
|
|
353
353
|
const active = readActiveRun(ctx.cwd);
|
|
354
354
|
ctx.ui.notify(formatRuns(listRuns(ctx.cwd), active?.id), "info");
|
|
@@ -356,46 +356,46 @@ export default function (pi: ExtensionAPI) {
|
|
|
356
356
|
});
|
|
357
357
|
|
|
358
358
|
pi.registerCommand("kd-gate", {
|
|
359
|
-
description: "
|
|
359
|
+
description: "刷新并查看当前 Kingdee Harness 门禁",
|
|
360
360
|
handler: async (_args, ctx) => {
|
|
361
361
|
const run = requireRun(ctx.cwd);
|
|
362
362
|
if (!run) {
|
|
363
|
-
ctx.ui.notify("
|
|
363
|
+
ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求>。", "error");
|
|
364
364
|
return;
|
|
365
365
|
}
|
|
366
366
|
const refreshed = refreshGate(ctx.cwd, run);
|
|
367
|
-
ctx.ui.notify(refreshed.gate.passed ? "
|
|
367
|
+
ctx.ui.notify(refreshed.gate.passed ? "门禁通过" : `门禁阻塞:${refreshed.gate.reason}`, "info");
|
|
368
368
|
},
|
|
369
369
|
});
|
|
370
370
|
|
|
371
371
|
pi.registerCommand("kd-start", {
|
|
372
|
-
description: "
|
|
372
|
+
description: "启动一个 Kingdee Harness run:/kd-start [--product 产品] [--version 版本] <需求>",
|
|
373
373
|
handler: async (args, ctx) => {
|
|
374
374
|
const parsed = parseStartArgs(args);
|
|
375
375
|
const goal = parsed.goal;
|
|
376
376
|
if (!goal) {
|
|
377
|
-
ctx.ui.notify("
|
|
377
|
+
ctx.ui.notify("用法:/kd-start [--product 产品] [--version 版本] <需求>", "error");
|
|
378
378
|
return;
|
|
379
379
|
}
|
|
380
380
|
|
|
381
381
|
const run = createActiveRun(ctx.cwd, goal, parsed.product, parsed.version);
|
|
382
|
-
ctx.ui.notify(
|
|
382
|
+
ctx.ui.notify(`已启动 Kingdee Harness run:${run.id}(${run.profile?.product}/${run.profile?.techStack})`, "info");
|
|
383
383
|
sendWorkflowPrompt(pi, ctx, run, `继续 KCode Harness run ${run.id}:${goal}`);
|
|
384
384
|
},
|
|
385
385
|
});
|
|
386
386
|
|
|
387
387
|
pi.registerCommand("kd-resume", {
|
|
388
|
-
description: "
|
|
388
|
+
description: "接续当前 Kingdee Harness run,并注入已落盘上下文",
|
|
389
389
|
handler: async (args, ctx) => {
|
|
390
390
|
const run = requireRun(ctx.cwd);
|
|
391
391
|
if (!run) {
|
|
392
|
-
ctx.ui.notify("
|
|
392
|
+
ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求> 或 /kd-runs。", "error");
|
|
393
393
|
return;
|
|
394
394
|
}
|
|
395
395
|
|
|
396
396
|
const note = args.trim();
|
|
397
397
|
const message = [
|
|
398
|
-
`继续 KCode Harness run ${run.id}:${run.goal ?? "
|
|
398
|
+
`继续 KCode Harness run ${run.id}:${run.goal ?? "未知需求"}`,
|
|
399
399
|
note ? `用户补充:${note}` : undefined,
|
|
400
400
|
]
|
|
401
401
|
.filter(Boolean)
|
|
@@ -405,64 +405,64 @@ export default function (pi: ExtensionAPI) {
|
|
|
405
405
|
});
|
|
406
406
|
|
|
407
407
|
pi.registerCommand("kd-switch", {
|
|
408
|
-
description: "
|
|
408
|
+
description: "切换当前 active Kingdee Harness run:/kd-switch <run-id>",
|
|
409
409
|
handler: async (args, ctx) => {
|
|
410
410
|
const id = args.trim();
|
|
411
411
|
if (!id) {
|
|
412
|
-
ctx.ui.notify("
|
|
412
|
+
ctx.ui.notify("用法:/kd-switch <run-id>", "error");
|
|
413
413
|
return;
|
|
414
414
|
}
|
|
415
415
|
|
|
416
416
|
const run = switchActiveRun(ctx.cwd, id);
|
|
417
417
|
if (!run) {
|
|
418
|
-
ctx.ui.notify(
|
|
418
|
+
ctx.ui.notify(`未找到 Kingdee Harness run:${id}。请用 /kd-runs 查看列表。`, "error");
|
|
419
419
|
return;
|
|
420
420
|
}
|
|
421
421
|
|
|
422
|
-
ctx.ui.notify(
|
|
422
|
+
ctx.ui.notify(`已切换 active Kingdee Harness run:${run.id}(${run.phase})`, "info");
|
|
423
423
|
},
|
|
424
424
|
});
|
|
425
425
|
|
|
426
426
|
pi.registerCommand("kd-finish", {
|
|
427
|
-
description: "
|
|
427
|
+
description: "标记当前 Kingdee Harness run 完成,并清除 active run",
|
|
428
428
|
handler: async (_args, ctx) => {
|
|
429
429
|
const run = requireRun(ctx.cwd);
|
|
430
430
|
if (!run) {
|
|
431
|
-
ctx.ui.notify("
|
|
431
|
+
ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求>。", "error");
|
|
432
432
|
return;
|
|
433
433
|
}
|
|
434
434
|
|
|
435
435
|
const finished = finishActiveRun(ctx.cwd, run);
|
|
436
|
-
ctx.ui.notify(
|
|
436
|
+
ctx.ui.notify(`已完成 Kingdee Harness run:${finished.id}。下一个功能点请使用 /kd-start <需求>。`, "info");
|
|
437
437
|
},
|
|
438
438
|
});
|
|
439
439
|
|
|
440
440
|
pi.registerCommand("kd-product", {
|
|
441
|
-
description: "
|
|
441
|
+
description: "设置当前金蝶产品画像:/kd-product <flagship|cosmic|xinghan|cangqiong|enterprise> [--version 版本]",
|
|
442
442
|
handler: async (args, ctx) => {
|
|
443
443
|
const run = requireRun(ctx.cwd);
|
|
444
444
|
if (!run) {
|
|
445
|
-
ctx.ui.notify("
|
|
445
|
+
ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求>。", "error");
|
|
446
446
|
return;
|
|
447
447
|
}
|
|
448
448
|
|
|
449
449
|
const parsed = parseProductArgs(args);
|
|
450
450
|
if (!parsed) {
|
|
451
|
-
ctx.ui.notify("
|
|
451
|
+
ctx.ui.notify("用法:/kd-product <flagship|cosmic|xinghan|cangqiong|enterprise> [--version 版本]", "error");
|
|
452
452
|
return;
|
|
453
453
|
}
|
|
454
454
|
|
|
455
455
|
const updated = updateProductProfile(ctx.cwd, run, parsed.product, parsed.version);
|
|
456
|
-
ctx.ui.notify(
|
|
456
|
+
ctx.ui.notify(`产品画像:${updated.profile?.product}/${updated.profile?.techStack}/${updated.profile?.language}`, "info");
|
|
457
457
|
},
|
|
458
458
|
});
|
|
459
459
|
|
|
460
460
|
pi.registerCommand("kd-advance", {
|
|
461
|
-
description:
|
|
461
|
+
description: `推进 Kingdee run 到下一阶段,或指定阶段:${PHASE_ORDER.join("|")}`,
|
|
462
462
|
handler: async (args, ctx) => {
|
|
463
463
|
const run = requireRun(ctx.cwd);
|
|
464
464
|
if (!run) {
|
|
465
|
-
ctx.ui.notify("
|
|
465
|
+
ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求>。", "error");
|
|
466
466
|
return;
|
|
467
467
|
}
|
|
468
468
|
|
|
@@ -470,7 +470,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
470
470
|
let requested: KdPhase | undefined;
|
|
471
471
|
if (requestedText) {
|
|
472
472
|
if (!isKdPhase(requestedText)) {
|
|
473
|
-
ctx.ui.notify(
|
|
473
|
+
ctx.ui.notify(`未知阶段 "${requestedText}"。可用阶段:${PHASE_ORDER.join(", ")}`, "error");
|
|
474
474
|
return;
|
|
475
475
|
}
|
|
476
476
|
requested = requestedText;
|
|
@@ -482,67 +482,67 @@ export default function (pi: ExtensionAPI) {
|
|
|
482
482
|
});
|
|
483
483
|
|
|
484
484
|
pi.registerCommand("kd-artifact", {
|
|
485
|
-
description: "
|
|
485
|
+
description: "创建或更新阶段文档:/kd-artifact [阶段] [内容]",
|
|
486
486
|
handler: async (args, ctx) => {
|
|
487
487
|
const run = requireRun(ctx.cwd);
|
|
488
488
|
if (!run) {
|
|
489
|
-
ctx.ui.notify("
|
|
489
|
+
ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求>。", "error");
|
|
490
490
|
return;
|
|
491
491
|
}
|
|
492
492
|
|
|
493
493
|
const parsed = parseArtifactArgs(args, run.phase);
|
|
494
494
|
if (!parsed) {
|
|
495
|
-
ctx.ui.notify("
|
|
495
|
+
ctx.ui.notify("用法:/kd-artifact [阶段] [内容]", "error");
|
|
496
496
|
return;
|
|
497
497
|
}
|
|
498
498
|
|
|
499
499
|
const path = parsed.content
|
|
500
500
|
? updatePhaseArtifact(ctx.cwd, run, parsed.phase, parsed.content)
|
|
501
501
|
: ensurePhaseArtifact(ctx.cwd, run, parsed.phase);
|
|
502
|
-
ctx.ui.notify(
|
|
502
|
+
ctx.ui.notify(`阶段文档已就绪:${path}`, "info");
|
|
503
503
|
},
|
|
504
504
|
});
|
|
505
505
|
|
|
506
506
|
pi.registerCommand("kd-answer", {
|
|
507
|
-
description: "
|
|
507
|
+
description: "回答一个阻塞中的 Kingdee Harness 问题:/kd-answer Q-001 <答案>",
|
|
508
508
|
handler: async (args, ctx) => {
|
|
509
509
|
const run = requireRun(ctx.cwd);
|
|
510
510
|
if (!run) {
|
|
511
|
-
ctx.ui.notify("
|
|
511
|
+
ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求>。", "error");
|
|
512
512
|
return;
|
|
513
513
|
}
|
|
514
514
|
const [id, ...answerParts] = args.trim().split(/\s+/);
|
|
515
515
|
const answer = answerParts.join(" ").trim();
|
|
516
516
|
if (!id || !answer) {
|
|
517
|
-
ctx.ui.notify("
|
|
517
|
+
ctx.ui.notify("用法:/kd-answer Q-001 <答案>", "error");
|
|
518
518
|
return;
|
|
519
519
|
}
|
|
520
520
|
const answered = answerQuestion(ctx.cwd, run, id, answer);
|
|
521
521
|
if (!answered) {
|
|
522
|
-
ctx.ui.notify(
|
|
522
|
+
ctx.ui.notify(`未找到问题:${id}`, "error");
|
|
523
523
|
return;
|
|
524
524
|
}
|
|
525
|
-
appendQuestionEventToArtifact(ctx.cwd, run, [`-
|
|
526
|
-
ctx.ui.notify(
|
|
525
|
+
appendQuestionEventToArtifact(ctx.cwd, run, [`- 已回答 ${answered.id}:${answered.answer}`]);
|
|
526
|
+
ctx.ui.notify(`已记录 ${answered.id} 的答案`, "info");
|
|
527
527
|
},
|
|
528
528
|
});
|
|
529
529
|
}
|
|
530
530
|
|
|
531
531
|
function formatQuestions(run: NonNullable<ReturnType<typeof readActiveRun>>): string {
|
|
532
532
|
const questions = run.questions ?? [];
|
|
533
|
-
if (questions.length === 0) return "
|
|
533
|
+
if (questions.length === 0) return "尚未记录 Harness 问题。";
|
|
534
534
|
return questions.map(formatQuestionCard).join("\n\n");
|
|
535
535
|
}
|
|
536
536
|
|
|
537
537
|
function formatRuns(runs: NonNullable<ReturnType<typeof readActiveRun>>[], activeId?: string): string {
|
|
538
|
-
if (runs.length === 0) return "
|
|
538
|
+
if (runs.length === 0) return "当前项目没有 Kingdee Harness run。请使用 /kd-start <需求> 创建。";
|
|
539
539
|
return runs
|
|
540
540
|
.map((run) =>
|
|
541
541
|
[
|
|
542
542
|
`${run.id === activeId ? "*" : "-"} ${run.id}`,
|
|
543
|
-
`
|
|
544
|
-
`
|
|
545
|
-
`
|
|
543
|
+
` 需求:${run.goal ?? "未知"}`,
|
|
544
|
+
` 状态:${run.status ?? "active"} | 阶段:${run.phase} | 产品:${run.profile?.product ?? run.product ?? "unknown"}`,
|
|
545
|
+
` 更新时间:${run.updatedAt ?? "未知"}`,
|
|
546
546
|
].join("\n"),
|
|
547
547
|
)
|
|
548
548
|
.join("\n\n");
|
|
@@ -550,14 +550,14 @@ function formatRuns(runs: NonNullable<ReturnType<typeof readActiveRun>>[], activ
|
|
|
550
550
|
|
|
551
551
|
function formatQuestionCard(question: NonNullable<NonNullable<ReturnType<typeof readActiveRun>>["questions"]>[number]): string {
|
|
552
552
|
const lines = [
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
question.reason ?
|
|
557
|
-
question.choices?.length ?
|
|
558
|
-
question.answer ?
|
|
553
|
+
`问题 ${question.id} [${question.status}${question.blocking ? ", blocking" : ""}]`,
|
|
554
|
+
`阶段:${question.phase}`,
|
|
555
|
+
`问题:${question.question}`,
|
|
556
|
+
question.reason ? `原因:${question.reason}` : undefined,
|
|
557
|
+
question.choices?.length ? `选项:${question.choices.join(" | ")}` : undefined,
|
|
558
|
+
question.answer ? `答案:${question.answer}` : undefined,
|
|
559
559
|
question.status === "open"
|
|
560
|
-
?
|
|
560
|
+
? `如果有弹窗,请直接回答;否则请在对话中回复,并用 kd_question action=answer id=${question.id} answer=<答案> 记录。`
|
|
561
561
|
: undefined,
|
|
562
562
|
];
|
|
563
563
|
return lines.filter(Boolean).join("\n");
|
|
@@ -589,12 +589,12 @@ async function askQuestionInteractively(
|
|
|
589
589
|
function questionBatchProblem(question: string, choices?: string[]): string | undefined {
|
|
590
590
|
const text = question.trim();
|
|
591
591
|
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
|
|
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 拒绝了过多选项。最多提供 3 个简短选项。";
|
|
596
596
|
if (choices?.some((choice) => choice.length > 40 || /[\r\n??]/.test(choice))) {
|
|
597
|
-
return "kd_question
|
|
597
|
+
return "kd_question 拒绝了复杂选项。选项必须是短标签,不能嵌套问题。";
|
|
598
598
|
}
|
|
599
599
|
return undefined;
|
|
600
600
|
}
|
|
@@ -602,14 +602,14 @@ function questionBatchProblem(question: string, choices?: string[]): string | un
|
|
|
602
602
|
function formatQuestionArtifactLines(question: NonNullable<NonNullable<ReturnType<typeof readActiveRun>>["questions"]>[number]): string[] {
|
|
603
603
|
return [
|
|
604
604
|
`- ${question.id} [${question.blocking ? "blocking" : "non-blocking"}] ${question.question}`,
|
|
605
|
-
question.reason ? ` -
|
|
606
|
-
question.choices?.length ? ` -
|
|
607
|
-
" -
|
|
605
|
+
question.reason ? ` - 原因:${question.reason}` : undefined,
|
|
606
|
+
question.choices?.length ? ` - 选项:${question.choices.join(" | ")}` : undefined,
|
|
607
|
+
" - 状态:open",
|
|
608
608
|
].filter(Boolean) as string[];
|
|
609
609
|
}
|
|
610
610
|
|
|
611
611
|
function appendQuestionEventToArtifact(cwd: string, run: NonNullable<ReturnType<typeof readActiveRun>>, lines: string[]): void {
|
|
612
612
|
const existing = readArtifact(cwd, run, run.phase) ?? "";
|
|
613
|
-
const section = ["", "## Harness
|
|
614
|
-
updatePhaseArtifact(cwd, run, run.phase, existing.includes("## Harness
|
|
613
|
+
const section = ["", "## Harness 问题", "", ...lines, ""].join("\n");
|
|
614
|
+
updatePhaseArtifact(cwd, run, run.phase, existing.includes("## Harness 问题") ? `${existing.trimEnd()}\n${lines.join("\n")}\n` : `${existing.trimEnd()}${section}`);
|
|
615
615
|
}
|