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.
- package/README.md +10 -10
- package/dist/cli/kcode.js +3 -3
- package/dist/context/project-context.js +4 -5
- package/dist/harness/prompt-policy.d.ts +19 -0
- package/dist/harness/prompt-policy.js +206 -0
- package/dist/harness/types.d.ts +74 -0
- package/dist/harness/types.js +16 -0
- package/dist/product/profile.d.ts +20 -0
- package/dist/product/profile.js +103 -0
- package/docs/CHANGELOG.md +90 -2
- package/docs/COMMAND_REFERENCE.md +27 -12
- package/docs/DEVELOPMENT.md +3 -3
- package/docs/EVIDENCE_AND_GATES.md +15 -8
- package/docs/HARNESS_WORKFLOW.md +32 -12
- package/docs/KCODE_DISTRIBUTION.md +7 -7
- package/docs/PRODUCT_PROFILE.md +9 -9
- package/docs/TROUBLESHOOTING.md +8 -8
- package/docs/USER_GUIDE.md +10 -10
- package/extensions/kingdee-harness.ts +141 -86
- package/extensions/kingdee-header.ts +1 -1
- package/extensions/kingdee-subagents.ts +1 -1
- package/extensions/kingdee-tools.ts +44 -44
- package/package.json +1 -1
- package/src/cli/kcode.ts +3 -3
- package/src/context/project-context.ts +4 -5
- package/src/harness/artifacts.ts +6 -7
- package/src/harness/data-source-policy.ts +346 -0
- package/src/harness/delegation.ts +28 -23
- package/src/harness/gates.ts +16 -1
- package/src/harness/messages.ts +65 -11
- package/src/harness/path-policy.ts +1 -0
- package/src/harness/plan-steps.ts +3 -3
- package/src/harness/prompt-policy.ts +227 -0
- package/src/harness/prompt.ts +12 -16
- package/src/harness/question-memory.ts +220 -0
- package/src/harness/repair.ts +18 -3
- package/src/harness/state.ts +93 -6
- package/src/harness/types.ts +19 -0
- package/src/official/kingdee-skills.ts +4 -4
- package/src/product/profile.ts +2 -2
- package/src/rules/checker.ts +27 -27
- package/src/tools/build-debug.ts +5 -5
- package/src/tools/sdk-signature.ts +4 -4
package/docs/USER_GUIDE.md
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
- Node.js `>=22.19.0`
|
|
10
10
|
- npm
|
|
11
|
-
- Windows
|
|
12
|
-
-
|
|
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
|
-
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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}
|
|
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: "
|
|
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
|
-
"
|
|
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
|
-
|
|
181
|
-
|
|
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
|
|
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
|
|
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}
|
|
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
|
|
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
|
-
"
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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("
|
|
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}
|
|
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}
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
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: "
|
|
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("
|
|
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
|
|
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: "
|
|
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}
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
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
|
|
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}"
|
|
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: "
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
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
|
|
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
|
-
?
|
|
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
|
|
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("
|
|
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}
|
|
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 [
|