kcode-pi 0.1.7 → 0.1.9
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 +55 -4
- package/docs/DEVELOPMENT.md +2 -0
- package/extensions/kingdee-harness.ts +271 -2
- package/extensions/kingdee-tools.ts +43 -1
- package/package.json +2 -1
- package/skills/kd-check/SKILL.md +1 -2
- package/skills/kd-cosmic-dev/SKILL.md +3 -3
- package/skills/kd-cosmic-review/SKILL.md +2 -2
- package/skills/kd-cosmic-unittest/SKILL.md +2 -2
- package/skills/kd-debug/SKILL.md +1 -2
- package/skills/kd-discuss/SKILL.md +1 -0
- package/skills/kd-execute/SKILL.md +2 -2
- package/skills/kd-gen/SKILL.md +2 -1
- package/skills/kd-plan/SKILL.md +3 -3
- package/skills/kd-spec/SKILL.md +1 -2
- package/src/harness/format.ts +2 -0
- package/src/harness/gates.ts +14 -1
- package/src/harness/paths.ts +5 -1
- package/src/harness/state.ts +113 -11
- package/src/harness/types.ts +19 -0
- package/src/tools/sdk-signature.ts +309 -0
package/README.md
CHANGED
|
@@ -274,9 +274,13 @@ kcode start --provider openai --model gpt-4o
|
|
|
274
274
|
/kd-start [--product product] [--version version] <goal>
|
|
275
275
|
/kd-product <product> [--version version]
|
|
276
276
|
/kd-status
|
|
277
|
+
/kd-runs
|
|
278
|
+
/kd-switch <run-id>
|
|
279
|
+
/kd-finish
|
|
277
280
|
/kd-gate
|
|
278
281
|
/kd-advance [phase]
|
|
279
282
|
/kd-artifact [phase] [content]
|
|
283
|
+
/kd-answer Q-001 <answer>
|
|
280
284
|
```
|
|
281
285
|
|
|
282
286
|
典型开始方式:
|
|
@@ -309,12 +313,30 @@ discuss -> spec -> plan -> execute -> verify -> ship
|
|
|
309
313
|
|
|
310
314
|
KCode 会把金蝶开发需求纳入 Harness 工作流。你可以直接输入自然语言需求;如果当前项目还没有 active run,KCode 会自动创建 run 并进入 `discuss` 阶段,而不是直接生成代码。
|
|
311
315
|
|
|
316
|
+
一个功能点对应一个 run。每个 run 都有独立阶段、计划、执行记录和证据;做另一个功能点时,不要复用上一个功能点的阶段。
|
|
317
|
+
|
|
318
|
+
```text
|
|
319
|
+
/kd-start 实现采购订单保存校验
|
|
320
|
+
/kd-finish
|
|
321
|
+
/kd-start 实现采购订单列表字段展示
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
查看和切换当前项目里的功能点 run:
|
|
325
|
+
|
|
326
|
+
```text
|
|
327
|
+
/kd-runs
|
|
328
|
+
/kd-switch <run-id>
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
如果当前 active run 处于 `execute` 或 `verify`,但你提出的是另一个需求,KCode 会要求先创建新 run 或切换 run,避免把新需求塞进旧功能点的 `PLAN.md` 和阶段门禁。
|
|
332
|
+
|
|
312
333
|
每次进入 Harness 时,KCode 会附带 `.pi/kd/PROJECT_CONTEXT.md`。这份上下文不会替代计划阶段的实际文件检查,但会让 Agent 在暂停、恢复、重新打开终端后仍知道当前项目的源码根、模块线索和禁止猜路径规则。
|
|
313
334
|
|
|
314
335
|
每个需求 run 的设计和执行文档会落到本地:
|
|
315
336
|
|
|
316
337
|
```text
|
|
317
338
|
.pi/kd/active-run.json
|
|
339
|
+
.pi/kd/runs/<run-id>/RUN.json
|
|
318
340
|
.pi/kd/runs/<run-id>/CONTEXT.md
|
|
319
341
|
.pi/kd/runs/<run-id>/SPEC.md
|
|
320
342
|
.pi/kd/runs/<run-id>/PLAN.md
|
|
@@ -324,7 +346,7 @@ KCode 会把金蝶开发需求纳入 Harness 工作流。你可以直接输入
|
|
|
324
346
|
.pi/kd/runs/<run-id>/evidence/
|
|
325
347
|
```
|
|
326
348
|
|
|
327
|
-
下次重新 `kcode start` 时,KCode 会读取当前项目的 active run
|
|
349
|
+
下次重新 `kcode start` 时,KCode 会读取当前项目的 active run、项目常驻上下文和已生成的阶段文档,再继续当前功能点的当前阶段。已完成或暂停的功能点仍保留在 `.pi/kd/runs/<run-id>/`,可用 `/kd-runs` 查看,用 `/kd-switch <run-id>` 切回。
|
|
328
350
|
|
|
329
351
|
阶段含义:
|
|
330
352
|
|
|
@@ -369,7 +391,7 @@ ship 汇总变更、验证证据、风险和后续事项
|
|
|
369
391
|
|
|
370
392
|
- Red evidence: evidence/tdd-red.md
|
|
371
393
|
- Green evidence: evidence/tdd-green.md
|
|
372
|
-
- Red/green command or tool:
|
|
394
|
+
- Red/green command or tool: kd_sdk_signature / kd_cosmic_metadata / kd_check / build
|
|
373
395
|
- Do not add third-party test jars or frameworks only for this gate.
|
|
374
396
|
```
|
|
375
397
|
|
|
@@ -387,11 +409,28 @@ ship 汇总变更、验证证据、风险和后续事项
|
|
|
387
409
|
|
|
388
410
|
如果 LLM 跳过步骤、没有记录 evidence,或只是口头声明完成,KCode 不允许进入 `verify`。
|
|
389
411
|
|
|
412
|
+
结构化问题也会进入门禁。Agent 需要确认产品、目标单据、插件位置、高风险 SQL 或方案选择时,会使用 `kd_question` 记录问题;未回答的阻断问题会阻止进入下一阶段。
|
|
413
|
+
|
|
414
|
+
`kd_question` 一次只允许问一个当前最阻塞的问题,不能把多个问题打包成编号清单。比如应先问:
|
|
415
|
+
|
|
416
|
+
```text
|
|
417
|
+
采购入库单 Form ID 是否为 pur_receivebill?
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
得到回答后,再继续问触发时机、库存条件、弹窗内容或插件位置。
|
|
421
|
+
|
|
422
|
+
在 `kcode start` 的交互式 TUI 中,`kd_question` 会弹出 KCode Question 选择/输入对话,并在用户选择或输入后自动记录答案。非交互模式或 UI 不可用时,问题会显示在对话里,用户直接回复;Agent 读取回复后再记录答案。也可以手动执行:
|
|
423
|
+
|
|
424
|
+
```text
|
|
425
|
+
/kd-answer Q-001 采购订单
|
|
426
|
+
```
|
|
427
|
+
|
|
390
428
|
运行状态保存在当前业务项目:
|
|
391
429
|
|
|
392
430
|
```text
|
|
393
431
|
.pi/kd/active-run.json
|
|
394
432
|
.pi/kd/runs/<run-id>/
|
|
433
|
+
.pi/kd/runs/<run-id>/RUN.json
|
|
395
434
|
```
|
|
396
435
|
|
|
397
436
|
## 内置金蝶工具
|
|
@@ -400,12 +439,14 @@ KCode 会向 Pi 注册这些金蝶工具:
|
|
|
400
439
|
|
|
401
440
|
```text
|
|
402
441
|
kd_plan_status 查看当前 Harness run、阶段、产物和门禁
|
|
442
|
+
kd_question 记录/回答/查看阻断问题,未回答时阻止阶段推进
|
|
403
443
|
kd_search 搜索内置金蝶 SDK 知识和代码模式
|
|
404
444
|
kd_table 查询内置金蝶表结构
|
|
405
445
|
kd_check 检查金蝶 Java/C# 插件代码
|
|
406
446
|
kd_cosmic_config 运行官方 ok-cosmic 配置预检
|
|
407
447
|
kd_cosmic_metadata 查询官方 Cosmic 表单/单据元数据
|
|
408
|
-
kd_cosmic_api
|
|
448
|
+
kd_cosmic_api 查询随包 Cosmic API 知识线索
|
|
449
|
+
kd_sdk_signature 从当前项目实际 SDK jar/dll 中读取类和方法签名
|
|
409
450
|
kd_ksql_lint 运行官方 ok-ksql SQL/KSQL lint
|
|
410
451
|
kd_build 按产品画像执行或 dry-run 构建
|
|
411
452
|
kd_debug 分析金蝶日志和堆栈
|
|
@@ -416,7 +457,17 @@ kd_debug 分析金蝶日志和堆栈
|
|
|
416
457
|
- `kd_ksql_lint` 是内置 Node 静态检查器。
|
|
417
458
|
- `kd_cosmic_config` 使用 Node 校验 Cosmic 官方能力配置;项目没有 `ok-cosmic.json` 时会自动使用 KCode 随包默认配置。
|
|
418
459
|
- `kd_cosmic_metadata` 使用统一路由 API 查询真实单据/表单元数据,并在当前项目 `.pi/kd/official-skills/` 下维护 JSON 缓存。
|
|
419
|
-
- `
|
|
460
|
+
- `kd_sdk_signature` 优先从当前业务项目的实际 SDK jar/dll 中读取类型和方法签名。Cosmic Java 依赖 `javap`,Enterprise C# 依赖 PowerShell 读取 DLL 元数据。
|
|
461
|
+
- `kd_cosmic_api` 查询随包金蝶知识库,只作为 API 线索和兜底;精确方法签名优先使用 `kd_sdk_signature`、当前项目 SDK 或编译输出确认。
|
|
462
|
+
|
|
463
|
+
示例:
|
|
464
|
+
|
|
465
|
+
```text
|
|
466
|
+
kd_sdk_signature product=flagship query=QueryServiceHelper method=loadSingle path=lib
|
|
467
|
+
kd_sdk_signature product=enterprise query=DynamicObject method=GetDynamicObject path=bin
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
`method` 只是在已匹配的类/类型里过滤方法或属性;如果不知道类名,先用 `query` 搜类型关键词,再结合项目源码和构建文件缩小范围。
|
|
420
471
|
|
|
421
472
|
`ok-cosmic.json` 是可选的 KCode 官方能力覆盖配置,不是苍穹工程模板里的 `cosmic.json`。业务项目里的 `cosmic.json` 通常只包含开发者标识、工程标识、MC 资源地址等运行环境信息,不能替代 `ok-cosmic.json`。
|
|
422
473
|
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -32,6 +32,7 @@ npm run smoke:checker
|
|
|
32
32
|
npm run smoke:harness
|
|
33
33
|
npm run smoke:official
|
|
34
34
|
npm run smoke:build-debug
|
|
35
|
+
npm run smoke:sdk-signature
|
|
35
36
|
npm run smoke:package
|
|
36
37
|
npm run smoke:kcode-cli
|
|
37
38
|
```
|
|
@@ -45,6 +46,7 @@ npm run smoke:kcode-cli
|
|
|
45
46
|
- Harness 阶段和门禁。
|
|
46
47
|
- 官方能力 Node 适配器。
|
|
47
48
|
- 构建/调试诊断。
|
|
49
|
+
- 当前项目 SDK jar/dll 签名读取。
|
|
48
50
|
- package manifest、skills、vendor 文件完整性。
|
|
49
51
|
- `kcode` 项目级入口逻辑。
|
|
50
52
|
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { Type } from "@earendil-works/pi-ai";
|
|
2
|
-
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { defineTool, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { formatStatus } from "../src/harness/format.ts";
|
|
4
4
|
import { PHASE_ORDER, isKdPhase, type KdPhase } from "../src/harness/types.ts";
|
|
5
5
|
import {
|
|
6
6
|
advanceRun,
|
|
7
|
+
addQuestion,
|
|
8
|
+
answerQuestion,
|
|
7
9
|
createActiveRun,
|
|
8
10
|
ensurePhaseArtifact,
|
|
11
|
+
finishActiveRun,
|
|
12
|
+
listRuns,
|
|
9
13
|
readActiveRun,
|
|
10
14
|
refreshGate,
|
|
15
|
+
switchActiveRun,
|
|
11
16
|
updateProductProfile,
|
|
12
17
|
updatePhaseArtifact,
|
|
13
18
|
} from "../src/harness/state.ts";
|
|
@@ -103,11 +108,14 @@ function workflowPromptForRun(cwd: string, run: NonNullable<ReturnType<typeof re
|
|
|
103
108
|
"KCode 本次工作流本地文档:",
|
|
104
109
|
memory,
|
|
105
110
|
"",
|
|
111
|
+
`当前 active run 只属于这个功能点:${run.goal ?? run.id}。如果用户提出的是另一个功能点或无关新需求,必须停止沿用当前阶段,要求用户运行 /kd-start <新需求> 创建新 run,或 /kd-switch <run-id> 切换已有 run。`,
|
|
112
|
+
"需要用户确认时,kd_question 一次只能问一个当前最阻塞的问题;不要把 FormId、触发时机、库存条件、弹窗内容、插件位置等打包成清单。选项最多 3 个;交互模式下会弹出选择/输入对话并自动记录答案。",
|
|
113
|
+
"",
|
|
106
114
|
phaseGuidance[run.phase],
|
|
107
115
|
"必须先理解当前业务项目已有目录、模块、包名、基类和本地封装,再决定文件位置和实现方式。",
|
|
108
116
|
"路径规则:在 Windows 工作区内,优先使用项目相对路径;如需绝对路径必须使用 `D:\\...` 这类 Windows 路径,禁止把路径改写成 `/mnt/d/...`、`/d/...` 等 WSL/MSYS 风格路径。",
|
|
109
117
|
"execute 阶段只能写 PLAN.md 明确列出的源码文件;如果目标文件不在计划内,必须先回到 plan 更新 PLAN.md。",
|
|
110
|
-
"写生产源码前必须先有红灯证据 evidence/tdd-red.md;红绿证据可以是 API/基类/方法签名、元数据、编译、既有测试框架或外部接口最小验证,不要为了测试引入额外 jar。",
|
|
118
|
+
"写生产源码前必须先有红灯证据 evidence/tdd-red.md;红绿证据可以是 kd_sdk_signature 本地 SDK 签名、API/基类/方法签名、元数据、编译、既有测试框架或外部接口最小验证,不要为了测试引入额外 jar。",
|
|
111
119
|
].join("\n");
|
|
112
120
|
}
|
|
113
121
|
|
|
@@ -158,8 +166,118 @@ const kdPlanStatusTool = defineTool({
|
|
|
158
166
|
},
|
|
159
167
|
});
|
|
160
168
|
|
|
169
|
+
const kdQuestionTool = defineTool({
|
|
170
|
+
name: "kd_question",
|
|
171
|
+
label: "KD Question",
|
|
172
|
+
description:
|
|
173
|
+
"Create, answer, or list structured Kingdee Harness questions. Ask exactly one short blocking question at a time; do not batch a checklist.",
|
|
174
|
+
parameters: Type.Object({
|
|
175
|
+
action: Type.Optional(Type.String({ description: "ask, answer, or list. Defaults to ask." })),
|
|
176
|
+
id: Type.Optional(Type.String({ description: "Question id when action=answer, for example Q-001." })),
|
|
177
|
+
question: Type.Optional(Type.String({ description: "One short question to ask when action=ask. No numbered lists or multiple questions." })),
|
|
178
|
+
answer: Type.Optional(Type.String({ description: "User answer when action=answer." })),
|
|
179
|
+
reason: Type.Optional(Type.String({ description: "Why this question blocks or matters." })),
|
|
180
|
+
choices: Type.Optional(Type.Array(Type.String(), { description: "Optional concrete choices for the user, maximum 3 short labels." })),
|
|
181
|
+
blocking: Type.Optional(Type.Boolean({ description: "Whether the question blocks phase advancement. Defaults to true." })),
|
|
182
|
+
}),
|
|
183
|
+
|
|
184
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
185
|
+
const run = readActiveRun(ctx.cwd);
|
|
186
|
+
if (!run) {
|
|
187
|
+
return {
|
|
188
|
+
content: [{ type: "text", text: "No active Kingdee harness run. Start one with /kd-start <goal> first." }],
|
|
189
|
+
details: { error: "no-active-run" },
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const action = (params.action ?? "ask").toLowerCase();
|
|
194
|
+
if (action === "list") {
|
|
195
|
+
const text = formatQuestions(run);
|
|
196
|
+
return { content: [{ type: "text", text }], details: { questions: run.questions ?? [] } };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (action === "answer") {
|
|
200
|
+
if (!params.id || !params.answer) {
|
|
201
|
+
return {
|
|
202
|
+
content: [{ type: "text", text: "kd_question action=answer requires id and answer." }],
|
|
203
|
+
details: { error: "missing-answer-params" },
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const answered = answerQuestion(ctx.cwd, run, params.id, params.answer);
|
|
207
|
+
if (!answered) {
|
|
208
|
+
return {
|
|
209
|
+
content: [{ type: "text", text: `Question not found: ${params.id}` }],
|
|
210
|
+
details: { error: "question-not-found", id: params.id },
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
appendQuestionEventToArtifact(ctx.cwd, run, [`- Answered ${answered.id}: ${answered.answer}`]);
|
|
214
|
+
return {
|
|
215
|
+
content: [{ type: "text", text: `Recorded answer for ${answered.id}. Gate refreshed.` }],
|
|
216
|
+
details: { question: answered, gate: readActiveRun(ctx.cwd)?.gate },
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (action !== "ask") {
|
|
221
|
+
return {
|
|
222
|
+
content: [{ type: "text", text: `Unknown kd_question action: ${params.action}. Use ask, answer, or list.` }],
|
|
223
|
+
details: { error: "unknown-action", action: params.action },
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!params.question?.trim()) {
|
|
228
|
+
return {
|
|
229
|
+
content: [{ type: "text", text: "kd_question action=ask requires question." }],
|
|
230
|
+
details: { error: "missing-question" },
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const batchProblem = questionBatchProblem(params.question, params.choices);
|
|
234
|
+
if (batchProblem) {
|
|
235
|
+
return {
|
|
236
|
+
content: [
|
|
237
|
+
{
|
|
238
|
+
type: "text",
|
|
239
|
+
text: [
|
|
240
|
+
batchProblem,
|
|
241
|
+
"",
|
|
242
|
+
"kd_question 一次只登记一个当前最阻塞的问题。",
|
|
243
|
+
"请先问第一个必须确认的问题,例如:",
|
|
244
|
+
"kd_question action=ask question=\"采购入库单 Form ID 是否为 pur_receivebill?\" choices=[\"是\", \"不是\"]",
|
|
245
|
+
"",
|
|
246
|
+
"注意:Pi 当前不会弹出表单;问题会显示在对话里,用户在对话中回答后再用 kd_question action=answer 记录。",
|
|
247
|
+
].join("\n"),
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
details: { error: "batched-question" },
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const question = addQuestion(ctx.cwd, run, {
|
|
255
|
+
question: params.question,
|
|
256
|
+
reason: params.reason,
|
|
257
|
+
choices: params.choices,
|
|
258
|
+
blocking: params.blocking,
|
|
259
|
+
});
|
|
260
|
+
appendQuestionEventToArtifact(ctx.cwd, run, formatQuestionArtifactLines(question));
|
|
261
|
+
const interactiveAnswer = await askQuestionInteractively(ctx, question.question, question.choices);
|
|
262
|
+
if (interactiveAnswer) {
|
|
263
|
+
const answered = answerQuestion(ctx.cwd, run, question.id, interactiveAnswer);
|
|
264
|
+
if (answered) {
|
|
265
|
+
appendQuestionEventToArtifact(ctx.cwd, run, [`- Answered ${answered.id}: ${answered.answer}`]);
|
|
266
|
+
return {
|
|
267
|
+
content: [{ type: "text", text: `User answered ${answered.id}: ${answered.answer}` }],
|
|
268
|
+
details: { question: answered, gate: readActiveRun(ctx.cwd)?.gate, answered: true },
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const text = `${formatQuestionCard(question)}\n\nInteractive answer was not provided; keep this question open until the user answers.`;
|
|
274
|
+
return { content: [{ type: "text", text }], details: { question, gate: readActiveRun(ctx.cwd)?.gate, answered: false } };
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
|
|
161
278
|
export default function (pi: ExtensionAPI) {
|
|
162
279
|
pi.registerTool(kdPlanStatusTool);
|
|
280
|
+
pi.registerTool(kdQuestionTool);
|
|
163
281
|
|
|
164
282
|
pi.on("input", async (event, ctx) => {
|
|
165
283
|
if (event.source === "extension") return { action: "continue" };
|
|
@@ -206,6 +324,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
206
324
|
},
|
|
207
325
|
});
|
|
208
326
|
|
|
327
|
+
pi.registerCommand("kd-runs", {
|
|
328
|
+
description: "List Kingdee harness runs for this project",
|
|
329
|
+
handler: async (_args, ctx) => {
|
|
330
|
+
const active = readActiveRun(ctx.cwd);
|
|
331
|
+
ctx.ui.notify(formatRuns(listRuns(ctx.cwd), active?.id), "info");
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
|
|
209
335
|
pi.registerCommand("kd-gate", {
|
|
210
336
|
description: "Refresh and show the active Kingdee harness gate",
|
|
211
337
|
handler: async (_args, ctx) => {
|
|
@@ -234,6 +360,39 @@ export default function (pi: ExtensionAPI) {
|
|
|
234
360
|
},
|
|
235
361
|
});
|
|
236
362
|
|
|
363
|
+
pi.registerCommand("kd-switch", {
|
|
364
|
+
description: "Switch active Kingdee harness run: /kd-switch <run-id>",
|
|
365
|
+
handler: async (args, ctx) => {
|
|
366
|
+
const id = args.trim();
|
|
367
|
+
if (!id) {
|
|
368
|
+
ctx.ui.notify("Usage: /kd-switch <run-id>", "error");
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const run = switchActiveRun(ctx.cwd, id);
|
|
373
|
+
if (!run) {
|
|
374
|
+
ctx.ui.notify(`Kingdee harness run not found: ${id}. Use /kd-runs to list runs.`, "error");
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
ctx.ui.notify(`Switched active Kingdee harness run: ${run.id} (${run.phase})`, "info");
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
pi.registerCommand("kd-finish", {
|
|
383
|
+
description: "Mark the active Kingdee harness run as done and clear active run",
|
|
384
|
+
handler: async (_args, ctx) => {
|
|
385
|
+
const run = requireRun(ctx.cwd);
|
|
386
|
+
if (!run) {
|
|
387
|
+
ctx.ui.notify("No active Kingdee harness run. Use /kd-start <goal>.", "error");
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const finished = finishActiveRun(ctx.cwd, run);
|
|
392
|
+
ctx.ui.notify(`Finished Kingdee harness run: ${finished.id}. Start the next feature with /kd-start <goal>.`, "info");
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
|
|
237
396
|
pi.registerCommand("kd-product", {
|
|
238
397
|
description: "Set the active Kingdee product profile: /kd-product <flagship|cosmic|xinghan|cangqiong|enterprise> [--version version]",
|
|
239
398
|
handler: async (args, ctx) => {
|
|
@@ -299,4 +458,114 @@ export default function (pi: ExtensionAPI) {
|
|
|
299
458
|
ctx.ui.notify(`Artifact ready: ${path}`, "info");
|
|
300
459
|
},
|
|
301
460
|
});
|
|
461
|
+
|
|
462
|
+
pi.registerCommand("kd-answer", {
|
|
463
|
+
description: "Answer a blocking Kingdee harness question: /kd-answer Q-001 <answer>",
|
|
464
|
+
handler: async (args, ctx) => {
|
|
465
|
+
const run = requireRun(ctx.cwd);
|
|
466
|
+
if (!run) {
|
|
467
|
+
ctx.ui.notify("No active Kingdee harness run. Use /kd-start <goal>.", "error");
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const [id, ...answerParts] = args.trim().split(/\s+/);
|
|
471
|
+
const answer = answerParts.join(" ").trim();
|
|
472
|
+
if (!id || !answer) {
|
|
473
|
+
ctx.ui.notify("Usage: /kd-answer Q-001 <answer>", "error");
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const answered = answerQuestion(ctx.cwd, run, id, answer);
|
|
477
|
+
if (!answered) {
|
|
478
|
+
ctx.ui.notify(`Question not found: ${id}`, "error");
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
appendQuestionEventToArtifact(ctx.cwd, run, [`- Answered ${answered.id}: ${answered.answer}`]);
|
|
482
|
+
ctx.ui.notify(`Recorded answer for ${answered.id}`, "info");
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function formatQuestions(run: NonNullable<ReturnType<typeof readActiveRun>>): string {
|
|
488
|
+
const questions = run.questions ?? [];
|
|
489
|
+
if (questions.length === 0) return "No harness questions recorded.";
|
|
490
|
+
return questions.map(formatQuestionCard).join("\n\n");
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function formatRuns(runs: NonNullable<ReturnType<typeof readActiveRun>>[], activeId?: string): string {
|
|
494
|
+
if (runs.length === 0) return "No Kingdee harness runs in this project. Start one with /kd-start <goal>.";
|
|
495
|
+
return runs
|
|
496
|
+
.map((run) =>
|
|
497
|
+
[
|
|
498
|
+
`${run.id === activeId ? "*" : "-"} ${run.id}`,
|
|
499
|
+
` Goal: ${run.goal ?? "unknown"}`,
|
|
500
|
+
` Status: ${run.status ?? "active"} | Phase: ${run.phase} | Product: ${run.profile?.product ?? run.product ?? "unknown"}`,
|
|
501
|
+
` Updated: ${run.updatedAt ?? "unknown"}`,
|
|
502
|
+
].join("\n"),
|
|
503
|
+
)
|
|
504
|
+
.join("\n\n");
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function formatQuestionCard(question: NonNullable<NonNullable<ReturnType<typeof readActiveRun>>["questions"]>[number]): string {
|
|
508
|
+
const lines = [
|
|
509
|
+
`Question ${question.id} [${question.status}${question.blocking ? ", blocking" : ""}]`,
|
|
510
|
+
`Phase: ${question.phase}`,
|
|
511
|
+
`Question: ${question.question}`,
|
|
512
|
+
question.reason ? `Reason: ${question.reason}` : undefined,
|
|
513
|
+
question.choices?.length ? `Choices: ${question.choices.join(" | ")}` : undefined,
|
|
514
|
+
question.answer ? `Answer: ${question.answer}` : undefined,
|
|
515
|
+
question.status === "open"
|
|
516
|
+
? `Answer the dialog if available, or reply in chat and record it using kd_question action=answer id=${question.id} answer=<answer>.`
|
|
517
|
+
: undefined,
|
|
518
|
+
];
|
|
519
|
+
return lines.filter(Boolean).join("\n");
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function askQuestionInteractively(
|
|
523
|
+
ctx: ExtensionContext,
|
|
524
|
+
question: string,
|
|
525
|
+
choices: string[] | undefined,
|
|
526
|
+
): Promise<string | undefined> {
|
|
527
|
+
if (!ctx.hasUI) return undefined;
|
|
528
|
+
|
|
529
|
+
const normalizedChoices = choices?.map((choice) => choice.trim()).filter(Boolean) ?? [];
|
|
530
|
+
try {
|
|
531
|
+
if (normalizedChoices.length === 0) {
|
|
532
|
+
return (await ctx.ui.input("KCode Question", question))?.trim() || undefined;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const customChoice = "其他,自定义输入";
|
|
536
|
+
const selected = await ctx.ui.select("KCode Question", [...normalizedChoices, customChoice]);
|
|
537
|
+
if (!selected) return undefined;
|
|
538
|
+
if (selected !== customChoice) return selected;
|
|
539
|
+
return (await ctx.ui.input("KCode Question", question))?.trim() || undefined;
|
|
540
|
+
} catch {
|
|
541
|
+
return undefined;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function questionBatchProblem(question: string, choices?: string[]): string | undefined {
|
|
546
|
+
const text = question.trim();
|
|
547
|
+
const numberedItems = text.split(/\r?\n/).filter((line) => /^\s*(\d+[\.\)、)]|[-*]\s+)/.test(line)).length;
|
|
548
|
+
if (numberedItems >= 2) return "kd_question rejected a batched checklist question.";
|
|
549
|
+
if ((text.match(/[??]/g) ?? []).length > 1) return "kd_question rejected multiple questions in one prompt.";
|
|
550
|
+
if (text.length > 220) return "kd_question rejected an overlong question. Ask only the smallest blocking question first.";
|
|
551
|
+
if ((choices?.length ?? 0) > 3) return "kd_question rejected too many choices. Provide at most 3 concise choices.";
|
|
552
|
+
if (choices?.some((choice) => choice.length > 40 || /[\r\n??]/.test(choice))) {
|
|
553
|
+
return "kd_question rejected complex choices. Choices must be short labels, not nested questions.";
|
|
554
|
+
}
|
|
555
|
+
return undefined;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function formatQuestionArtifactLines(question: NonNullable<NonNullable<ReturnType<typeof readActiveRun>>["questions"]>[number]): string[] {
|
|
559
|
+
return [
|
|
560
|
+
`- ${question.id} [${question.blocking ? "blocking" : "non-blocking"}] ${question.question}`,
|
|
561
|
+
question.reason ? ` - Reason: ${question.reason}` : undefined,
|
|
562
|
+
question.choices?.length ? ` - Choices: ${question.choices.join(" | ")}` : undefined,
|
|
563
|
+
" - Status: open",
|
|
564
|
+
].filter(Boolean) as string[];
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function appendQuestionEventToArtifact(cwd: string, run: NonNullable<ReturnType<typeof readActiveRun>>, lines: string[]): void {
|
|
568
|
+
const existing = readArtifact(cwd, run, run.phase) ?? "";
|
|
569
|
+
const section = ["", "## Harness Questions", "", ...lines, ""].join("\n");
|
|
570
|
+
updatePhaseArtifact(cwd, run, run.phase, existing.includes("## Harness Questions") ? `${existing.trimEnd()}\n${lines.join("\n")}\n` : `${existing.trimEnd()}${section}`);
|
|
302
571
|
}
|
|
@@ -9,6 +9,7 @@ import type { Edition, KnowledgeScope } from "../src/knowledge/types.ts";
|
|
|
9
9
|
import { resolveProductProfile, type ProductProfile } from "../src/product/profile.ts";
|
|
10
10
|
import { checkCode, formatCheckResults, type CheckLanguage } from "../src/rules/checker.ts";
|
|
11
11
|
import { analyzeDebugText, formatDebugFindings, planBuild, readDebugInput, runBuild } from "../src/tools/build-debug.ts";
|
|
12
|
+
import { formatSdkSignatureResult, inspectSdkSignature, type SdkSignatureLanguage } from "../src/tools/sdk-signature.ts";
|
|
12
13
|
import {
|
|
13
14
|
cosmicApiCommand,
|
|
14
15
|
cosmicConfigCommand,
|
|
@@ -64,6 +65,13 @@ function languageForProfile(profile: ProductProfile, value: string | undefined):
|
|
|
64
65
|
return "java";
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
function sdkLanguageForProfile(profile: ProductProfile, value: string | undefined): SdkSignatureLanguage {
|
|
69
|
+
if (value === "csharp" || value === "cs") return "csharp";
|
|
70
|
+
if (value === "java") return "java";
|
|
71
|
+
if (profile.platform === "enterprise-csharp") return "csharp";
|
|
72
|
+
return "java";
|
|
73
|
+
}
|
|
74
|
+
|
|
67
75
|
function rejectNonCosmic(profile: ProductProfile): string | undefined {
|
|
68
76
|
if (isCosmicFamily(profile)) return undefined;
|
|
69
77
|
if (profile.product === "unknown") return "Provide a Cosmic-family product first: cangqiong, xinghan, flagship, or cosmic.";
|
|
@@ -265,7 +273,7 @@ const kdCosmicMetadataTool = defineTool({
|
|
|
265
273
|
const kdCosmicApiTool = defineTool({
|
|
266
274
|
name: "kd_cosmic_api",
|
|
267
275
|
label: "KD Cosmic API",
|
|
268
|
-
description: "Query
|
|
276
|
+
description: "Query bundled Cosmic API knowledge for class and method clues. Use kd_sdk_signature or build output for final local SDK facts.",
|
|
269
277
|
parameters: Type.Object({
|
|
270
278
|
product: Type.String({ description: "Cosmic-family product: cangqiong, xinghan, flagship, or cosmic." }),
|
|
271
279
|
mode: Type.String({ description: "search, search-method, or detail." }),
|
|
@@ -295,6 +303,39 @@ const kdCosmicApiTool = defineTool({
|
|
|
295
303
|
},
|
|
296
304
|
});
|
|
297
305
|
|
|
306
|
+
const kdSdkSignatureTool = defineTool({
|
|
307
|
+
name: "kd_sdk_signature",
|
|
308
|
+
label: "KD SDK Signature",
|
|
309
|
+
description:
|
|
310
|
+
"Inspect method/type signatures from SDK jars or dlls actually present in the current project. Prefer this over bundled knowledge for factual API signatures.",
|
|
311
|
+
parameters: Type.Object({
|
|
312
|
+
product: Type.Optional(Type.String({ description: "Kingdee product. Used to derive Java or C# when language is omitted." })),
|
|
313
|
+
language: Type.Optional(Type.String({ description: "java or csharp. Defaults from product profile." })),
|
|
314
|
+
query: Type.Optional(Type.String({ description: "Class/type keyword to search, for example QueryServiceHelper or DynamicObject." })),
|
|
315
|
+
className: Type.Optional(Type.String({ description: "Fully qualified Java/C# type name when known." })),
|
|
316
|
+
method: Type.Optional(Type.String({ description: "Optional method/property name filter within the matched class/type. Does not scan all methods globally." })),
|
|
317
|
+
path: Type.Optional(Type.String({ description: "Optional SDK lib/bin directory or specific dependency root. Defaults to current project." })),
|
|
318
|
+
limit: Type.Optional(Type.Number({ description: "Maximum jars/dlls/classes to inspect. Defaults to 20 result classes and 200 files." })),
|
|
319
|
+
}),
|
|
320
|
+
|
|
321
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
322
|
+
const profile = resolveToolProfile(params.product, undefined);
|
|
323
|
+
const language = sdkLanguageForProfile(profile, params.language);
|
|
324
|
+
const result = await inspectSdkSignature(ctx.cwd, {
|
|
325
|
+
language,
|
|
326
|
+
query: params.query,
|
|
327
|
+
className: params.className,
|
|
328
|
+
method: params.method,
|
|
329
|
+
path: params.path,
|
|
330
|
+
limit: params.limit,
|
|
331
|
+
});
|
|
332
|
+
return {
|
|
333
|
+
content: [{ type: "text", text: formatSdkSignatureResult(result) }],
|
|
334
|
+
details: { product: profile.product, ...result },
|
|
335
|
+
};
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
|
|
298
339
|
const kdKsqlLintTool = defineTool({
|
|
299
340
|
name: "kd_ksql_lint",
|
|
300
341
|
label: "KD KSQL Lint",
|
|
@@ -377,6 +418,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
377
418
|
pi.registerTool(kdCosmicConfigTool);
|
|
378
419
|
pi.registerTool(kdCosmicMetadataTool);
|
|
379
420
|
pi.registerTool(kdCosmicApiTool);
|
|
421
|
+
pi.registerTool(kdSdkSignatureTool);
|
|
380
422
|
pi.registerTool(kdKsqlLintTool);
|
|
381
423
|
pi.registerTool(kdBuildTool);
|
|
382
424
|
pi.registerTool(kdDebugTool);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kcode-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Kingdee-specific package and harness for Pi Coding Agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"smoke:harness": "tsx scripts/smoke-harness.ts",
|
|
62
62
|
"smoke:official": "tsx scripts/smoke-official-tools.ts",
|
|
63
63
|
"smoke:build-debug": "tsx scripts/smoke-build-debug.ts",
|
|
64
|
+
"smoke:sdk-signature": "tsx scripts/smoke-sdk-signature.ts",
|
|
64
65
|
"smoke:package": "tsx scripts/smoke-package.ts",
|
|
65
66
|
"smoke:kcode-cli": "tsx scripts/smoke-kcode-cli.ts",
|
|
66
67
|
"kcode": "tsx scripts/kcode.ts",
|
package/skills/kd-check/SKILL.md
CHANGED
|
@@ -12,7 +12,7 @@ Checklist:
|
|
|
12
12
|
- Magic values: status codes, operation names, field keys, table names, numbers, and business strings.
|
|
13
13
|
- Naming: Java/C# naming conventions and project-local naming.
|
|
14
14
|
- Lifecycle: correct plugin base class and lifecycle method for the target behavior.
|
|
15
|
-
- SDK usage: no invented API names; uncertain APIs must be verified through `kd_search
|
|
15
|
+
- SDK usage: no invented API names; uncertain Java/C# APIs must be verified through `kd_sdk_signature` against current project jars/dlls, or by compile/build evidence. Use `kd_search` only for guidance.
|
|
16
16
|
- Database access: no DB query or save operation inside loops unless explicitly justified.
|
|
17
17
|
- Exception handling: no empty catch blocks; errors should be logged or converted to business exceptions as appropriate.
|
|
18
18
|
- Resource cleanup: close or dispose resources where the SDK requires it.
|
|
@@ -23,4 +23,3 @@ Output expectations:
|
|
|
23
23
|
- Lead with concrete findings and file/line references when reviewing code.
|
|
24
24
|
- Separate errors, warnings, and improvement suggestions.
|
|
25
25
|
- State which checks were not possible because context or tools were missing.
|
|
26
|
-
|
|
@@ -46,7 +46,7 @@ description: 金蝶 Cosmic 体系 Java 插件开发技能,适用于苍穹、
|
|
|
46
46
|
|
|
47
47
|
3. 通过官方适配器验证产品事实。
|
|
48
48
|
- 代码引用字段、表单 ID、单据名称、枚举/下拉值、实体 ID、操作编码、SQL 字段时,使用 `kd_cosmic_metadata`。
|
|
49
|
-
- 类名、方法名、方法签名、继承关系、Override
|
|
49
|
+
- 类名、方法名、方法签名、继承关系、Override 签名不确定时,优先使用 `kd_sdk_signature` 从当前项目实际 SDK jar 中验证;`kd_cosmic_api` 只作为随包知识线索和兜底。
|
|
50
50
|
- 多个表单或字段尽量合并到一次元数据查询。
|
|
51
51
|
- 元数据查询和 API 签名查询互不依赖时并行执行。
|
|
52
52
|
|
|
@@ -63,7 +63,7 @@ description: 金蝶 Cosmic 体系 Java 插件开发技能,适用于苍穹、
|
|
|
63
63
|
|
|
64
64
|
## 硬性规则
|
|
65
65
|
|
|
66
|
-
- 不臆造字段 key、表单 ID、操作名、枚举值、表名或 SDK
|
|
66
|
+
- 不臆造字段 key、表单 ID、操作名、枚举值、表名或 SDK 方法;本地 SDK 查不到且编译未验证时,不把知识库结果当作最终签名事实。
|
|
67
67
|
- 不把 Enterprise C# 命名空间或生命周期假设用于 Cosmic Java。
|
|
68
68
|
- 星空旗舰版不允许猜目录或写 demo/sample;如果无法判断真实代码位置,先询问或更新计划,不要直接创建新目录。
|
|
69
69
|
- 不在数据绑定前的初始化阶段操作 UI 控件。
|
|
@@ -78,7 +78,7 @@ description: 金蝶 Cosmic 体系 Java 插件开发技能,适用于苍穹、
|
|
|
78
78
|
完成执行工作时说明:
|
|
79
79
|
|
|
80
80
|
- 修改了哪些文件。
|
|
81
|
-
-
|
|
81
|
+
- 使用了哪些检查:配置、元数据、本地 SDK 签名、API 知识、编译。
|
|
82
82
|
- 哪些事实仍无法验证。
|
|
83
83
|
- 验证命令和结果。
|
|
84
84
|
- `EXECUTION.md` 中记录相对 `PLAN.md` 的偏差。
|
|
@@ -17,7 +17,7 @@ description: 金蝶 Cosmic 体系 Java 插件和 KSQL 代码审查技能,按
|
|
|
17
17
|
|
|
18
18
|
- 存在 harness run 时,先用 `kd_plan_status` 查看产品画像。
|
|
19
19
|
- 用 `kd_search` 查询 Cosmic 审查清单、生命周期、平台约束、KSQL 和单测指导。
|
|
20
|
-
- SDK
|
|
20
|
+
- SDK 签名或生命周期方法不确定时,优先用 `kd_sdk_signature` 从当前项目实际 SDK jar 验证;查不到时再用 `kd_cosmic_api` 获取知识库线索,并要求编译或人工证据兜底。
|
|
21
21
|
- 变更中用到字段、操作、枚举值、表名、数据库列时,用 `kd_cosmic_metadata` 验证。
|
|
22
22
|
- 先运行 `kd_check` 做基础静态检查,再按下方清单深入审查。
|
|
23
23
|
- 对新增或修改的 SQL/KSQL 文件运行 `kd_ksql_lint`。
|
|
@@ -84,7 +84,7 @@ description: 金蝶 Cosmic 体系 Java 插件和 KSQL 代码审查技能,按
|
|
|
84
84
|
随后说明:
|
|
85
85
|
|
|
86
86
|
- 已运行和未运行的检查。
|
|
87
|
-
-
|
|
87
|
+
- 已验证的产品、元数据、本地 SDK 签名和 API 线索。
|
|
88
88
|
- 剩余风险或缺失上下文。
|
|
89
89
|
|
|
90
90
|
如果没有发现问题,明确说明未发现问题,并列出剩余测试或证据缺口。
|
|
@@ -25,7 +25,7 @@ description: 金蝶 Cosmic 体系 Java 单元测试生成和审查技能,适
|
|
|
25
25
|
- 阅读被测类和最近的测试基类。
|
|
26
26
|
- 新增测试风格前,先搜索同模块同包下已有测试。
|
|
27
27
|
- 用 `kd_search` 查询 Cosmic 单测指导和模块测试模式。
|
|
28
|
-
- 仅在测试或被测代码使用的 SDK
|
|
28
|
+
- 仅在测试或被测代码使用的 SDK 方法签名不确定时,优先使用 `kd_sdk_signature` 从当前项目实际 SDK jar 验证;查不到时再用 `kd_cosmic_api` 获取线索,并以编译结果兜底。
|
|
29
29
|
|
|
30
30
|
## 测试模式选择
|
|
31
31
|
|
|
@@ -56,7 +56,7 @@ POJO 或简单枚举场景可以简要说明后直接实现。
|
|
|
56
56
|
## 测试质量规则
|
|
57
57
|
|
|
58
58
|
- 使用项目已有 JUnit 和 Mockito 版本。
|
|
59
|
-
- 如果项目没有既有且允许使用的单测框架,不要新增 JUnit、Mockito 或额外 jar;改用 Harness 的红绿验证门禁,例如
|
|
59
|
+
- 如果项目没有既有且允许使用的单测框架,不要新增 JUnit、Mockito 或额外 jar;改用 Harness 的红绿验证门禁,例如 `kd_sdk_signature` 本地 SDK 签名、元数据、编译或最小外部接口验证。
|
|
60
60
|
- import 保持显式;项目风格允许时也不要使用宽泛静态通配导入。
|
|
61
61
|
- 每个测试方法必须有真实断言或交互验证。
|
|
62
62
|
- 避免 `assertTrue(true)`、`assertEquals(x, x)` 或只执行不验证的测试。
|
package/skills/kd-debug/SKILL.md
CHANGED
|
@@ -19,7 +19,7 @@ Analysis approach:
|
|
|
19
19
|
- permission or authorization failure
|
|
20
20
|
- null dynamic object or missing entry rows
|
|
21
21
|
- repeated DB calls in loops causing slow saves/submits
|
|
22
|
-
- Use `
|
|
22
|
+
- Use `kd_sdk_signature` for uncertain SDK types/methods against current project jars/dlls, `kd_search` for guidance, and `kd_table`/metadata tools for table assumptions.
|
|
23
23
|
|
|
24
24
|
Output expectations:
|
|
25
25
|
|
|
@@ -27,4 +27,3 @@ Output expectations:
|
|
|
27
27
|
- Provide the next concrete inspection step.
|
|
28
28
|
- Suggest a minimal fix, then verification evidence needed to prove it.
|
|
29
29
|
- If evidence is insufficient, say exactly what log, stack trace, metadata, or code file is missing.
|
|
30
|
-
|
|
@@ -20,5 +20,6 @@ Rules:
|
|
|
20
20
|
- Do not propose implementation before the context is clear.
|
|
21
21
|
- Do not invent SDK APIs, table names, or lifecycle methods.
|
|
22
22
|
- If the product/version, tech stack, or target object is unknown, mark it as an open question.
|
|
23
|
+
- Ask unresolved questions one at a time. When using `kd_question`, ask only the single most blocking question and use at most 3 short choices; never submit a numbered checklist of questions.
|
|
23
24
|
- Never apply enterprise C# rules to Java products, or Java/Cosmic plugin rules to enterprise C#.
|
|
24
25
|
- Keep context concise; detailed design belongs in `SPEC.md`.
|