kcode-pi 0.1.39 → 0.1.40
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/dist/cli/kcode.js +24 -0
- package/dist/harness/prompt-policy.d.ts +2 -0
- package/dist/harness/prompt-policy.js +19 -1
- package/docs/CHANGELOG.md +21 -0
- package/docs/COMMAND_REFERENCE.md +1 -0
- package/docs/TROUBLESHOOTING.md +33 -0
- package/extensions/kingdee-harness.ts +15 -4
- package/extensions/kingdee-tools.ts +101 -0
- package/package.json +1 -1
- package/src/cli/kcode.ts +27 -0
- package/src/harness/prompt-policy.ts +21 -1
- package/src/harness/question-memory.ts +34 -2
- package/src/tools/file-search.ts +107 -0
package/dist/cli/kcode.js
CHANGED
|
@@ -73,6 +73,7 @@ export function doctor(cwd, args = []) {
|
|
|
73
73
|
lines.push(`KCode package:${packageRoot}`);
|
|
74
74
|
lines.push(`项目配置:${existsSync(settingsPath) ? settingsPath : "未创建,运行 kcode init"}`);
|
|
75
75
|
lines.push(`项目上下文:${existsSync(projectContextPath) ? projectContextPath : "未创建,运行 kcode context"}`);
|
|
76
|
+
lines.push(`Windows bash:${process.platform === "win32" ? formatWindowsBashStatus(readSettingsSafe(settingsPath)) : "不适用"}`);
|
|
76
77
|
if (existsSync(settingsPath)) {
|
|
77
78
|
const settingsResult = readSettingsSafe(settingsPath);
|
|
78
79
|
if (!settingsResult.ok) {
|
|
@@ -109,6 +110,10 @@ export function doctor(cwd, args = []) {
|
|
|
109
110
|
warnings++;
|
|
110
111
|
lines.push("[WARN] 项目上下文不存在,运行 kcode context --refresh 或 kcode repair。");
|
|
111
112
|
}
|
|
113
|
+
if (process.platform === "win32" && !findWindowsBash(readSettingsSafe(settingsPath))) {
|
|
114
|
+
warnings++;
|
|
115
|
+
lines.push("[WARN] Pi 内置 bash 在当前 Windows 环境不可用;KCode 已覆盖 bash 为 PowerShell,并提供 kd_find_file/kd_list_dir。安装 Git for Windows 可恢复 Pi 原生 bash。");
|
|
116
|
+
}
|
|
112
117
|
const npmPrefix = spawnSync("npm", ["prefix", "-g"], { encoding: "utf8" });
|
|
113
118
|
const npmRoot = spawnSync("npm", ["root", "-g"], { encoding: "utf8" });
|
|
114
119
|
lines.push(`npm prefix -g:${npmPrefix.status === 0 ? npmPrefix.stdout.trim() : "不可用"}`);
|
|
@@ -305,3 +310,22 @@ function helpText() {
|
|
|
305
310
|
" kcode start 初始化项目配置后启动 KCode 工作环境",
|
|
306
311
|
].join("\n");
|
|
307
312
|
}
|
|
313
|
+
function formatWindowsBashStatus(settingsResult) {
|
|
314
|
+
const bash = findWindowsBash(settingsResult);
|
|
315
|
+
return bash ? `可用(${bash})` : "不可用;KCode 将使用 PowerShell 覆盖工具和 kd_find_file/kd_list_dir";
|
|
316
|
+
}
|
|
317
|
+
function findWindowsBash(settingsResult) {
|
|
318
|
+
const configured = settingsResult?.ok && typeof settingsResult.settings.shellPath === "string" ? settingsResult.settings.shellPath : undefined;
|
|
319
|
+
if (configured && existsSync(configured))
|
|
320
|
+
return configured;
|
|
321
|
+
const candidates = [
|
|
322
|
+
process.env.ProgramFiles ? join(process.env.ProgramFiles, "Git", "bin", "bash.exe") : undefined,
|
|
323
|
+
process.env["ProgramFiles(x86)"] ? join(process.env["ProgramFiles(x86)"], "Git", "bin", "bash.exe") : undefined,
|
|
324
|
+
...pathEntries().map((entry) => join(entry, "bash.exe")),
|
|
325
|
+
].filter((item) => Boolean(item));
|
|
326
|
+
return candidates.find((candidate) => existsSync(candidate));
|
|
327
|
+
}
|
|
328
|
+
function pathEntries() {
|
|
329
|
+
const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path") ?? "PATH";
|
|
330
|
+
return (process.env[pathKey] ?? "").split(";").filter(Boolean);
|
|
331
|
+
}
|
|
@@ -15,5 +15,7 @@ export declare const PHASE_GUIDANCE: Record<KdPhase, string>;
|
|
|
15
15
|
export declare const PLAN_REQUIRED_CHECK_LINES: string[];
|
|
16
16
|
export declare function formatPromptLines(lines: string[]): string[];
|
|
17
17
|
export declare function fieldLabels(fields: ContractField[]): string[];
|
|
18
|
+
export declare function allFactLabels(): string[];
|
|
19
|
+
export declare function formatFactLabelCatalog(): string;
|
|
18
20
|
export declare function questionForMissingLabel(label: string): string | undefined;
|
|
19
21
|
export declare function canonicalFactLabel(label: string): string | undefined;
|
|
@@ -145,6 +145,7 @@ export const PROJECT_PERSISTENT_RULES = [
|
|
|
145
145
|
"企业版 Python 插件通常没有本地构建可替代验证;BOS 注册、外部系统操作、人工功能测试和生产环境验证必须由用户提供可核验证据并记录来源。",
|
|
146
146
|
"提示语集中管理为正式工程指令;禁止口语化、闲聊式、鼓励式提示词进入运行时规则。",
|
|
147
147
|
"工具使用必须匹配当前会话实际可用工具和操作系统;Windows 默认使用 PowerShell、`rg`、`Get-ChildItem`、`Get-Content`,禁止假设 bash 可用。",
|
|
148
|
+
"Windows 查找文件和目录时优先使用 `kd_find_file`、`kd_list_dir` 或 PowerShell;禁止用 Linux find/ls 语法反复调用 Pi 内置 bash。",
|
|
148
149
|
"文件定位必须使用真实搜索或目录读取结果;工具不可用时明确说明阻塞原因和需要用户执行的命令,禁止猜测路径、反复自述工具失败或用 kd_subagent 代替基础文件搜索。",
|
|
149
150
|
"不做旧状态迁移兼容或旧问题答案推断;坏状态只过滤,缺失事实必须重新提问确认。",
|
|
150
151
|
];
|
|
@@ -169,7 +170,7 @@ export const CORE_WORKFLOW_CONSTRAINTS = [
|
|
|
169
170
|
"evidence 必须记录命令、Exit 和关键输出;命令无法运行时记录阻塞原因。",
|
|
170
171
|
"外部系统操作、BOS 注册、人工功能测试和生产环境验证不能由 LLM 代办;必须要求用户提供验证结果或可核验证据,并记录证据来源。",
|
|
171
172
|
"Windows 路径规则:项目相对路径为默认;绝对路径使用 D:\\... 形式。",
|
|
172
|
-
"工具规则:按当前会话实际 shell 和工具执行;Windows 不假设 bash
|
|
173
|
+
"工具规则:按当前会话实际 shell 和工具执行;Windows 不假设 bash,优先用 kd_find_file/kd_list_dir/PowerShell;禁止猜路径或用 kd_subagent 代替基础搜索失败。",
|
|
173
174
|
];
|
|
174
175
|
export const PHASE_GUIDANCE = {
|
|
175
176
|
discuss: "梳理需求来源、范围、已知事实;如缺通用实现契约、数据源或第三方接口关键事实,使用 kd_question 登记一个最阻塞问题。",
|
|
@@ -195,6 +196,23 @@ export function formatPromptLines(lines) {
|
|
|
195
196
|
export function fieldLabels(fields) {
|
|
196
197
|
return fields.map((field) => field.label);
|
|
197
198
|
}
|
|
199
|
+
export function allFactLabels() {
|
|
200
|
+
return fieldLabels([...IMPLEMENTATION_CONTRACT_FIELDS, ...DATA_SOURCE_CONTEXT_FIELDS, ...INTEGRATION_CONTEXT_FIELDS]);
|
|
201
|
+
}
|
|
202
|
+
export function formatFactLabelCatalog() {
|
|
203
|
+
return [
|
|
204
|
+
"## 实现契约事实标签",
|
|
205
|
+
...formatPromptLines(fieldLabels(IMPLEMENTATION_CONTRACT_FIELDS)),
|
|
206
|
+
"",
|
|
207
|
+
"## 数据源上下文事实标签",
|
|
208
|
+
...formatPromptLines(fieldLabels(DATA_SOURCE_CONTEXT_FIELDS)),
|
|
209
|
+
"",
|
|
210
|
+
"## 第三方对接事实标签",
|
|
211
|
+
...formatPromptLines(fieldLabels(INTEGRATION_CONTEXT_FIELDS)),
|
|
212
|
+
"",
|
|
213
|
+
"产品类型/产品画像不是 factLabel;使用 /kd-product <产品> 或先登记无 factLabel 的产品确认问题。",
|
|
214
|
+
].join("\n");
|
|
215
|
+
}
|
|
198
216
|
export function questionForMissingLabel(label) {
|
|
199
217
|
const fields = [...IMPLEMENTATION_CONTRACT_FIELDS, ...DATA_SOURCE_CONTEXT_FIELDS, ...INTEGRATION_CONTEXT_FIELDS];
|
|
200
218
|
return fields.find((field) => field.label === label)?.question;
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,27 @@
|
|
|
6
6
|
|
|
7
7
|
- 暂无。
|
|
8
8
|
|
|
9
|
+
## 0.1.40 - 2026-06-07
|
|
10
|
+
|
|
11
|
+
### 修复
|
|
12
|
+
|
|
13
|
+
- Windows 下覆盖 Pi 内置 `bash` 工具为 PowerShell 执行,避免未安装 Git Bash 时反复搜索 `C:\Program Files\Git\bin\bash.exe`。
|
|
14
|
+
- 新增 `kd_find_file` 和 `kd_list_dir`,用于不依赖 shell 的文件查找和目录探索。
|
|
15
|
+
- `kcode doctor --deep` 增加 Windows bash 状态诊断,并提示 KCode 的 PowerShell 覆盖和文件工具。
|
|
16
|
+
- 持久规则明确 Windows 文件定位优先使用 `kd_find_file`、`kd_list_dir` 或 PowerShell。
|
|
17
|
+
- `kd_question action=labels` 可列出合法 `factLabel`,未知标签错误会返回完整标签目录,避免 LLM 反复猜标签。
|
|
18
|
+
- `kd_question` 拒绝把产品类型和 FormId/字段/插件事件等多个事实塞进同一个 `factLabel`。
|
|
19
|
+
|
|
20
|
+
### 验证
|
|
21
|
+
|
|
22
|
+
- `npm run check`
|
|
23
|
+
- `npm run smoke:harness`
|
|
24
|
+
- `npm run smoke:kcode-command`
|
|
25
|
+
- `npm run smoke:package`
|
|
26
|
+
- `npm run smoke:official`
|
|
27
|
+
- `npm run build:cli`
|
|
28
|
+
- `git diff --check`
|
|
29
|
+
|
|
9
30
|
## 0.1.39 - 2026-06-07
|
|
10
31
|
|
|
11
32
|
### 修复
|
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -133,3 +133,36 @@ ENOENT: no such file or directory, access 'D:\mnt\d\projects\xxx\src\main\java\F
|
|
|
133
133
|
```powershell
|
|
134
134
|
kcode context --refresh
|
|
135
135
|
```
|
|
136
|
+
|
|
137
|
+
## Windows 下 bash 工具一直搜索 Git Bash
|
|
138
|
+
|
|
139
|
+
现象:
|
|
140
|
+
|
|
141
|
+
```text
|
|
142
|
+
Searched Git Bash in:
|
|
143
|
+
C:\Program Files\Git\bin\bash.exe
|
|
144
|
+
C:\Program Files (x86)\Git\bin\bash.exe
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
原因是 Pi 内置 `bash` 工具在 Windows 上要求 Git Bash、Cygwin 或 MSYS2。KCode 在 Windows 下会覆盖该工具为 PowerShell,并提供 `kd_find_file`、`kd_list_dir` 用于文件查找和目录探索。
|
|
148
|
+
|
|
149
|
+
处理:
|
|
150
|
+
|
|
151
|
+
```powershell
|
|
152
|
+
kcode repair
|
|
153
|
+
kcode doctor --deep
|
|
154
|
+
kcode start
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
在 KCode 会话中查找文件时使用:
|
|
158
|
+
|
|
159
|
+
```text
|
|
160
|
+
kd_find_file name=MesApiClient.cs root=C:\Users\Administrator\Desktop\lf-project
|
|
161
|
+
kd_list_dir path=C:\Users\Administrator\Desktop\lf-project
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
如果需要 Pi 原生 bash 行为,安装 Git for Windows 后重新运行:
|
|
165
|
+
|
|
166
|
+
```powershell
|
|
167
|
+
kcode doctor --deep
|
|
168
|
+
```
|
|
@@ -30,7 +30,8 @@ import { windowsPathHint } from "../src/platform/path.ts";
|
|
|
30
30
|
import { repairPromptForRun, workflowPromptForRun } from "../src/harness/prompt.ts";
|
|
31
31
|
import { recordVerifyResult, type VerifyResultOutcome } from "../src/harness/repair.ts";
|
|
32
32
|
import { isSubagentChild, subagentRoleFromEnv, subagentToolCallBlockReason } from "../src/harness/delegation.ts";
|
|
33
|
-
import { questionAskBlockReason } from "../src/harness/question-memory.ts";
|
|
33
|
+
import { questionAskBlockReason, questionFactLabelMismatchReason } from "../src/harness/question-memory.ts";
|
|
34
|
+
import { formatFactLabelCatalog } from "../src/harness/prompt-policy.ts";
|
|
34
35
|
|
|
35
36
|
function requireRun(cwd: string): ReturnType<typeof readActiveRun> {
|
|
36
37
|
return readActiveRun(cwd);
|
|
@@ -175,9 +176,9 @@ const kdQuestionTool = defineTool({
|
|
|
175
176
|
name: "kd_question",
|
|
176
177
|
label: "KD 问题",
|
|
177
178
|
description:
|
|
178
|
-
"
|
|
179
|
+
"创建、回答、修订、列出金蝶 Harness 结构化问题,或列出合法 factLabel。每次必须只记录一个最阻塞的短问题。",
|
|
179
180
|
parameters: Type.Object({
|
|
180
|
-
action: Type.Optional(Type.String({ description: "操作类型:ask、answer、revise 或
|
|
181
|
+
action: Type.Optional(Type.String({ description: "操作类型:ask、answer、revise、list 或 labels,默认 ask。" })),
|
|
181
182
|
id: Type.Optional(Type.String({ description: "回答问题时的问题编号,例如 Q-001。" })),
|
|
182
183
|
question: Type.Optional(Type.String({ description: "提问内容;必须是一个短问题。" })),
|
|
183
184
|
answer: Type.Optional(Type.String({ description: "用户答案,action=answer 时使用。" })),
|
|
@@ -198,6 +199,9 @@ const kdQuestionTool = defineTool({
|
|
|
198
199
|
}
|
|
199
200
|
|
|
200
201
|
const action = (params.action ?? "ask").toLowerCase();
|
|
202
|
+
if (action === "labels") {
|
|
203
|
+
return { content: [{ type: "text", text: formatFactLabelCatalog() }], details: { action: "labels" } };
|
|
204
|
+
}
|
|
201
205
|
if (action === "list") {
|
|
202
206
|
const text = formatQuestions(run);
|
|
203
207
|
return { content: [{ type: "text", text }], details: { questions: run.questions ?? [] } };
|
|
@@ -260,7 +264,7 @@ const kdQuestionTool = defineTool({
|
|
|
260
264
|
|
|
261
265
|
if (action !== "ask") {
|
|
262
266
|
return {
|
|
263
|
-
content: [{ type: "text", text: `未知 kd_question action:${params.action}。有效值:ask、answer、revise、list。` }],
|
|
267
|
+
content: [{ type: "text", text: `未知 kd_question action:${params.action}。有效值:ask、answer、revise、list、labels。` }],
|
|
264
268
|
details: { error: "unknown-action", action: params.action },
|
|
265
269
|
};
|
|
266
270
|
}
|
|
@@ -291,6 +295,13 @@ const kdQuestionTool = defineTool({
|
|
|
291
295
|
details: { error: "batched-question" },
|
|
292
296
|
};
|
|
293
297
|
}
|
|
298
|
+
const mismatchReason = questionFactLabelMismatchReason({ question: params.question, factLabel: params.factLabel });
|
|
299
|
+
if (mismatchReason) {
|
|
300
|
+
return {
|
|
301
|
+
content: [{ type: "text", text: mismatchReason }],
|
|
302
|
+
details: { error: "fact-label-question-mismatch", factLabel: params.factLabel },
|
|
303
|
+
};
|
|
304
|
+
}
|
|
294
305
|
const askBlockReason = questionAskBlockReason(run, { factLabel: params.factLabel });
|
|
295
306
|
if (askBlockReason) {
|
|
296
307
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dirname, join, extname } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { readFileSync, readFile as fsReadFile } from "node:fs";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
4
5
|
import { Type } from "@earendil-works/pi-ai";
|
|
5
6
|
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
7
|
import { formatSearchResults, formatTableSchema } from "../src/knowledge/format.ts";
|
|
@@ -25,6 +26,7 @@ import { resolveWorkspacePath } from "../src/platform/path.ts";
|
|
|
25
26
|
import { advanceRunIfReady, readActiveRun } from "../src/harness/state.ts";
|
|
26
27
|
import { writeEvidenceFile } from "../src/harness/evidence.ts";
|
|
27
28
|
import { SDK_SIGNATURE_EVIDENCE } from "../src/harness/sdk-policy.ts";
|
|
29
|
+
import { findFiles, formatFileSearchResults, listDirectory } from "../src/tools/file-search.ts";
|
|
28
30
|
|
|
29
31
|
const extensionDir = dirname(fileURLToPath(import.meta.url));
|
|
30
32
|
const knowledgePath = join(extensionDir, "..", "knowledge");
|
|
@@ -479,6 +481,58 @@ const kdDocReadTool = defineTool({
|
|
|
479
481
|
},
|
|
480
482
|
});
|
|
481
483
|
|
|
484
|
+
const kdFindFileTool = defineTool({
|
|
485
|
+
name: "kd_find_file",
|
|
486
|
+
label: "KD 文件查找",
|
|
487
|
+
description: "在 Windows/PowerShell 环境下按文件名递归查找文件,不依赖 Pi 内置 bash。查找源码文件时优先使用此工具。",
|
|
488
|
+
parameters: Type.Object({
|
|
489
|
+
name: Type.String({ description: "文件名或通配符,例如 MesApiClient.cs 或 *.cs。" }),
|
|
490
|
+
root: Type.Optional(Type.String({ description: "搜索根目录;支持项目相对路径或 Windows 绝对路径。默认当前项目根。" })),
|
|
491
|
+
maxResults: Type.Optional(Type.Number({ description: "最大结果数,默认 50,最多 500。" })),
|
|
492
|
+
}),
|
|
493
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
494
|
+
try {
|
|
495
|
+
const results = findFiles(ctx.cwd, { root: params.root, name: params.name, maxResults: params.maxResults });
|
|
496
|
+
return { content: [{ type: "text", text: formatFileSearchResults(results) }], details: { count: results.length, root: params.root ?? ".", name: params.name } };
|
|
497
|
+
} catch (error) {
|
|
498
|
+
return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], details: { error: "find-file-failed" } };
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
const kdListDirTool = defineTool({
|
|
504
|
+
name: "kd_list_dir",
|
|
505
|
+
label: "KD 目录列表",
|
|
506
|
+
description: "列出目录内容,不读取目录为文件,不依赖 Pi 内置 bash。探索项目结构时优先使用此工具。",
|
|
507
|
+
parameters: Type.Object({
|
|
508
|
+
path: Type.Optional(Type.String({ description: "目录路径;支持项目相对路径或 Windows 绝对路径。默认当前项目根。" })),
|
|
509
|
+
maxEntries: Type.Optional(Type.Number({ description: "最大返回条目数,默认 50,最多 500。" })),
|
|
510
|
+
}),
|
|
511
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
512
|
+
try {
|
|
513
|
+
const results = listDirectory(ctx.cwd, { path: params.path, maxEntries: params.maxEntries });
|
|
514
|
+
return { content: [{ type: "text", text: formatFileSearchResults(results) }], details: { count: results.length, path: params.path ?? "." } };
|
|
515
|
+
} catch (error) {
|
|
516
|
+
return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], details: { error: "list-dir-failed" } };
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
const windowsPowerShellBashTool = defineTool({
|
|
522
|
+
name: "bash",
|
|
523
|
+
label: "PowerShell",
|
|
524
|
+
description:
|
|
525
|
+
"Windows KCode shell override. Execute a PowerShell command in the current working directory. Do not use Linux/bash syntax; use PowerShell commands such as Get-ChildItem, Select-String, Get-Content, and dotnet.",
|
|
526
|
+
promptSnippet: "Execute Windows PowerShell commands. Use PowerShell syntax, not bash syntax.",
|
|
527
|
+
parameters: Type.Object({
|
|
528
|
+
command: Type.String({ description: "PowerShell command to execute. Use PowerShell syntax, not bash syntax." }),
|
|
529
|
+
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds." })),
|
|
530
|
+
}),
|
|
531
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
532
|
+
return runPowerShell(params.command, ctx.cwd, params.timeout, signal);
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
|
|
482
536
|
async function extractPdf(filePath: string, maxPages: number): Promise<string> {
|
|
483
537
|
const { PDFParse } = await import("pdf-parse");
|
|
484
538
|
const buffer = readFileSync(filePath);
|
|
@@ -558,6 +612,7 @@ function extractCsv(filePath: string, maxRows: number): string {
|
|
|
558
612
|
}
|
|
559
613
|
|
|
560
614
|
export default function (pi: ExtensionAPI) {
|
|
615
|
+
if (process.platform === "win32") pi.registerTool(windowsPowerShellBashTool);
|
|
561
616
|
pi.registerTool(kdSearchTool);
|
|
562
617
|
pi.registerTool(kdTableTool);
|
|
563
618
|
pi.registerTool(kdCheckTool);
|
|
@@ -569,6 +624,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
569
624
|
pi.registerTool(kdBuildTool);
|
|
570
625
|
pi.registerTool(kdDebugTool);
|
|
571
626
|
pi.registerTool(kdDocReadTool);
|
|
627
|
+
pi.registerTool(kdFindFileTool);
|
|
628
|
+
pi.registerTool(kdListDirTool);
|
|
572
629
|
}
|
|
573
630
|
|
|
574
631
|
function writeSdkSignatureEvidence(cwd: string, content: string): string | undefined {
|
|
@@ -593,3 +650,47 @@ function writeSdkSignatureEvidence(cwd: string, content: string): string | undef
|
|
|
593
650
|
{ kind: "sdk-signature", command: "kd_sdk_signature", exitCode: 0 },
|
|
594
651
|
);
|
|
595
652
|
}
|
|
653
|
+
|
|
654
|
+
function runPowerShell(command: string, cwd: string, timeout: number | undefined, signal: AbortSignal | undefined): Promise<{ content: Array<{ type: "text"; text: string }>; details: Record<string, unknown> }> {
|
|
655
|
+
return new Promise((resolve) => {
|
|
656
|
+
const shell = process.env.ComSpec?.replace(/cmd\.exe$/i, "WindowsPowerShell\\v1.0\\powershell.exe") || "powershell.exe";
|
|
657
|
+
const child = spawn(shell, ["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command], {
|
|
658
|
+
cwd,
|
|
659
|
+
windowsHide: true,
|
|
660
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
661
|
+
});
|
|
662
|
+
let stdout = "";
|
|
663
|
+
let stderr = "";
|
|
664
|
+
let timedOut = false;
|
|
665
|
+
const timeoutHandle =
|
|
666
|
+
timeout && timeout > 0
|
|
667
|
+
? setTimeout(() => {
|
|
668
|
+
timedOut = true;
|
|
669
|
+
child.kill();
|
|
670
|
+
}, timeout * 1000)
|
|
671
|
+
: undefined;
|
|
672
|
+
const onAbort = () => child.kill();
|
|
673
|
+
if (signal) signal.addEventListener("abort", onAbort, { once: true });
|
|
674
|
+
child.stdout?.on("data", (data) => {
|
|
675
|
+
stdout += data.toString();
|
|
676
|
+
});
|
|
677
|
+
child.stderr?.on("data", (data) => {
|
|
678
|
+
stderr += data.toString();
|
|
679
|
+
});
|
|
680
|
+
child.on("error", (error) => {
|
|
681
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
682
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
683
|
+
resolve({ content: [{ type: "text", text: `PowerShell 启动失败:${error.message}` }], details: { shell: "powershell", error: "spawn-failed" } });
|
|
684
|
+
});
|
|
685
|
+
child.on("close", (exitCode) => {
|
|
686
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
687
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
688
|
+
const output = [stdout.trimEnd(), stderr.trimEnd()].filter(Boolean).join("\n");
|
|
689
|
+
const suffix = timedOut ? `\n\n[PowerShell timeout after ${timeout}s]` : "";
|
|
690
|
+
resolve({
|
|
691
|
+
content: [{ type: "text", text: `${output || "(no output)"}${suffix}` }],
|
|
692
|
+
details: { shell: "powershell", exitCode, timedOut, command },
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
}
|
package/package.json
CHANGED
package/src/cli/kcode.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface KcodeCliResult {
|
|
|
12
12
|
|
|
13
13
|
interface PiSettings {
|
|
14
14
|
packages?: string[];
|
|
15
|
+
shellPath?: string;
|
|
15
16
|
[key: string]: unknown;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -100,6 +101,7 @@ export function doctor(cwd: string, args: string[] = []): KcodeCliResult {
|
|
|
100
101
|
lines.push(`KCode package:${packageRoot}`);
|
|
101
102
|
lines.push(`项目配置:${existsSync(settingsPath) ? settingsPath : "未创建,运行 kcode init"}`);
|
|
102
103
|
lines.push(`项目上下文:${existsSync(projectContextPath) ? projectContextPath : "未创建,运行 kcode context"}`);
|
|
104
|
+
lines.push(`Windows bash:${process.platform === "win32" ? formatWindowsBashStatus(readSettingsSafe(settingsPath)) : "不适用"}`);
|
|
103
105
|
|
|
104
106
|
if (existsSync(settingsPath)) {
|
|
105
107
|
const settingsResult = readSettingsSafe(settingsPath);
|
|
@@ -135,6 +137,10 @@ export function doctor(cwd: string, args: string[] = []): KcodeCliResult {
|
|
|
135
137
|
warnings++;
|
|
136
138
|
lines.push("[WARN] 项目上下文不存在,运行 kcode context --refresh 或 kcode repair。");
|
|
137
139
|
}
|
|
140
|
+
if (process.platform === "win32" && !findWindowsBash(readSettingsSafe(settingsPath))) {
|
|
141
|
+
warnings++;
|
|
142
|
+
lines.push("[WARN] Pi 内置 bash 在当前 Windows 环境不可用;KCode 已覆盖 bash 为 PowerShell,并提供 kd_find_file/kd_list_dir。安装 Git for Windows 可恢复 Pi 原生 bash。");
|
|
143
|
+
}
|
|
138
144
|
const npmPrefix = spawnSync("npm", ["prefix", "-g"], { encoding: "utf8" });
|
|
139
145
|
const npmRoot = spawnSync("npm", ["root", "-g"], { encoding: "utf8" });
|
|
140
146
|
lines.push(`npm prefix -g:${npmPrefix.status === 0 ? npmPrefix.stdout.trim() : "不可用"}`);
|
|
@@ -351,3 +357,24 @@ function helpText(): string {
|
|
|
351
357
|
" kcode start 初始化项目配置后启动 KCode 工作环境",
|
|
352
358
|
].join("\n");
|
|
353
359
|
}
|
|
360
|
+
|
|
361
|
+
function formatWindowsBashStatus(settingsResult: ReturnType<typeof readSettingsSafe>): string {
|
|
362
|
+
const bash = findWindowsBash(settingsResult);
|
|
363
|
+
return bash ? `可用(${bash})` : "不可用;KCode 将使用 PowerShell 覆盖工具和 kd_find_file/kd_list_dir";
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function findWindowsBash(settingsResult?: ReturnType<typeof readSettingsSafe>): string | undefined {
|
|
367
|
+
const configured = settingsResult?.ok && typeof settingsResult.settings.shellPath === "string" ? settingsResult.settings.shellPath : undefined;
|
|
368
|
+
if (configured && existsSync(configured)) return configured;
|
|
369
|
+
const candidates = [
|
|
370
|
+
process.env.ProgramFiles ? join(process.env.ProgramFiles, "Git", "bin", "bash.exe") : undefined,
|
|
371
|
+
process.env["ProgramFiles(x86)"] ? join(process.env["ProgramFiles(x86)"]!, "Git", "bin", "bash.exe") : undefined,
|
|
372
|
+
...pathEntries().map((entry) => join(entry, "bash.exe")),
|
|
373
|
+
].filter((item): item is string => Boolean(item));
|
|
374
|
+
return candidates.find((candidate) => existsSync(candidate));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function pathEntries(): string[] {
|
|
378
|
+
const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path") ?? "PATH";
|
|
379
|
+
return (process.env[pathKey] ?? "").split(";").filter(Boolean);
|
|
380
|
+
}
|
|
@@ -157,6 +157,7 @@ export const PROJECT_PERSISTENT_RULES = [
|
|
|
157
157
|
"企业版 Python 插件通常没有本地构建可替代验证;BOS 注册、外部系统操作、人工功能测试和生产环境验证必须由用户提供可核验证据并记录来源。",
|
|
158
158
|
"提示语集中管理为正式工程指令;禁止口语化、闲聊式、鼓励式提示词进入运行时规则。",
|
|
159
159
|
"工具使用必须匹配当前会话实际可用工具和操作系统;Windows 默认使用 PowerShell、`rg`、`Get-ChildItem`、`Get-Content`,禁止假设 bash 可用。",
|
|
160
|
+
"Windows 查找文件和目录时优先使用 `kd_find_file`、`kd_list_dir` 或 PowerShell;禁止用 Linux find/ls 语法反复调用 Pi 内置 bash。",
|
|
160
161
|
"文件定位必须使用真实搜索或目录读取结果;工具不可用时明确说明阻塞原因和需要用户执行的命令,禁止猜测路径、反复自述工具失败或用 kd_subagent 代替基础文件搜索。",
|
|
161
162
|
"不做旧状态迁移兼容或旧问题答案推断;坏状态只过滤,缺失事实必须重新提问确认。",
|
|
162
163
|
];
|
|
@@ -183,7 +184,7 @@ export const CORE_WORKFLOW_CONSTRAINTS = [
|
|
|
183
184
|
"evidence 必须记录命令、Exit 和关键输出;命令无法运行时记录阻塞原因。",
|
|
184
185
|
"外部系统操作、BOS 注册、人工功能测试和生产环境验证不能由 LLM 代办;必须要求用户提供验证结果或可核验证据,并记录证据来源。",
|
|
185
186
|
"Windows 路径规则:项目相对路径为默认;绝对路径使用 D:\\... 形式。",
|
|
186
|
-
"工具规则:按当前会话实际 shell 和工具执行;Windows 不假设 bash
|
|
187
|
+
"工具规则:按当前会话实际 shell 和工具执行;Windows 不假设 bash,优先用 kd_find_file/kd_list_dir/PowerShell;禁止猜路径或用 kd_subagent 代替基础搜索失败。",
|
|
187
188
|
];
|
|
188
189
|
|
|
189
190
|
export const PHASE_GUIDANCE: Record<KdPhase, string> = {
|
|
@@ -214,6 +215,25 @@ export function fieldLabels(fields: ContractField[]): string[] {
|
|
|
214
215
|
return fields.map((field) => field.label);
|
|
215
216
|
}
|
|
216
217
|
|
|
218
|
+
export function allFactLabels(): string[] {
|
|
219
|
+
return fieldLabels([...IMPLEMENTATION_CONTRACT_FIELDS, ...DATA_SOURCE_CONTEXT_FIELDS, ...INTEGRATION_CONTEXT_FIELDS]);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function formatFactLabelCatalog(): string {
|
|
223
|
+
return [
|
|
224
|
+
"## 实现契约事实标签",
|
|
225
|
+
...formatPromptLines(fieldLabels(IMPLEMENTATION_CONTRACT_FIELDS)),
|
|
226
|
+
"",
|
|
227
|
+
"## 数据源上下文事实标签",
|
|
228
|
+
...formatPromptLines(fieldLabels(DATA_SOURCE_CONTEXT_FIELDS)),
|
|
229
|
+
"",
|
|
230
|
+
"## 第三方对接事实标签",
|
|
231
|
+
...formatPromptLines(fieldLabels(INTEGRATION_CONTEXT_FIELDS)),
|
|
232
|
+
"",
|
|
233
|
+
"产品类型/产品画像不是 factLabel;使用 /kd-product <产品> 或先登记无 factLabel 的产品确认问题。",
|
|
234
|
+
].join("\n");
|
|
235
|
+
}
|
|
236
|
+
|
|
217
237
|
export function questionForMissingLabel(label: string): string | undefined {
|
|
218
238
|
const fields = [...IMPLEMENTATION_CONTRACT_FIELDS, ...DATA_SOURCE_CONTEXT_FIELDS, ...INTEGRATION_CONTEXT_FIELDS];
|
|
219
239
|
return fields.find((field) => field.label === label)?.question;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ActiveRun, KdFact, KdQuestion } from "./types.ts";
|
|
2
|
-
import { canonicalFactLabel } from "./prompt-policy.ts";
|
|
2
|
+
import { allFactLabels, canonicalFactLabel, formatFactLabelCatalog } from "./prompt-policy.ts";
|
|
3
3
|
|
|
4
4
|
export function formatQuestionMemory(run: ActiveRun): string {
|
|
5
5
|
const questions = Array.isArray(run.questions) ? run.questions : [];
|
|
@@ -98,7 +98,7 @@ export function questionAskBlockReason(run: ActiveRun, input: { factLabel?: stri
|
|
|
98
98
|
const rawLabel = input.factLabel?.trim();
|
|
99
99
|
if (!rawLabel) return undefined;
|
|
100
100
|
const label = canonicalFactLabel(rawLabel);
|
|
101
|
-
if (!label) return `未知 factLabel:${rawLabel}。必须使用 KCode
|
|
101
|
+
if (!label) return [`未知 factLabel:${rawLabel}。必须使用 KCode 集中定义的实现契约、数据源上下文或第三方对接事实标签。`, "", "执行 `kd_question action=labels` 查看完整合法标签。", "", formatFactLabelCatalog()].join("\n");
|
|
102
102
|
const key = factKey(label);
|
|
103
103
|
const open = (Array.isArray(run.questions) ? run.questions : []).find(
|
|
104
104
|
(question) => question.status === "open" && question.blocking && question.factLabel && factKey(question.factLabel) === key,
|
|
@@ -113,6 +113,26 @@ export function questionAskBlockReason(run: ActiveRun, input: { factLabel?: stri
|
|
|
113
113
|
return undefined;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
export function questionFactLabelMismatchReason(input: { question: string; factLabel?: string }): string | undefined {
|
|
117
|
+
const question = input.question.trim();
|
|
118
|
+
const label = input.factLabel?.trim() ? canonicalFactLabel(input.factLabel) : undefined;
|
|
119
|
+
if (!label) return undefined;
|
|
120
|
+
const asksProduct = /产品类型|产品画像|金蝶产品|技术栈|苍穹|星瀚|星空旗舰版|旗舰版|企业版|Cloud\s*BOS|Cosmic|BOS/i.test(question);
|
|
121
|
+
const asksOtherFact = allFactLabels().some((candidate) => candidate !== label && labelAppearsInQuestion(question, candidate));
|
|
122
|
+
const conjunction = /以及|和|并|同时|还有|及|\/|、|,|,/.test(question);
|
|
123
|
+
if (asksProduct && (asksOtherFact || conjunction)) {
|
|
124
|
+
return [
|
|
125
|
+
`问题文本同时要求确认产品类型和 ${label},但 factLabel 只能记录一个结构化事实。`,
|
|
126
|
+
"产品类型/产品画像不是 factLabel;先使用 /kd-product <产品>,或登记一个无 factLabel 的产品确认问题。",
|
|
127
|
+
`确认产品后,再单独登记 ${label}。`,
|
|
128
|
+
].join("\n");
|
|
129
|
+
}
|
|
130
|
+
if (asksOtherFact && conjunction) {
|
|
131
|
+
return `问题文本包含多个结构化事实,但 factLabel 只能是一个:${label}。请拆成多个问题,每次只登记当前最阻塞的一个。`;
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
116
136
|
export function invalidFactValueReason(value: string): string | undefined {
|
|
117
137
|
return isInvalidFactValue(value) ? "事实值不是可核验内容,不能使用待确认、未知、按实际环境、TODO/TBD、单独确认/否定或口头确认语解除门禁。" : undefined;
|
|
118
138
|
}
|
|
@@ -218,3 +238,15 @@ function normalizePersistedFact(fact: KdFact): KdFact[] {
|
|
|
218
238
|
if (!label || !value || invalidFactValueReason(value)) return [];
|
|
219
239
|
return [{ ...fact, key: factKey(label), label, value }];
|
|
220
240
|
}
|
|
241
|
+
|
|
242
|
+
function labelAppearsInQuestion(question: string, label: string): boolean {
|
|
243
|
+
const normalizedQuestion = question.toLowerCase().replace(/\s+/g, "");
|
|
244
|
+
const normalizedLabel = label.toLowerCase().replace(/\s+/g, "");
|
|
245
|
+
if (normalizedQuestion.includes(normalizedLabel)) return true;
|
|
246
|
+
if (label === "目标 FormId/单据或表单标识") return /formid|formid|单据标识|表单标识|目标单据|目标表单/i.test(question);
|
|
247
|
+
if (label === "插件类型和触发事件") return /插件类型|触发事件|生命周期|挂载点|审核事件|保存事件/i.test(question);
|
|
248
|
+
if (label === "字段/实体/分录标识") return /字段标识|实体标识|分录标识|字段|实体|单据体/i.test(question);
|
|
249
|
+
if (label === "数据读取写入方式") return /读取方式|写入方式|读写|取数|数据访问|SQL|KSQL/i.test(question);
|
|
250
|
+
if (label === "SQL/KSQL 表名和数据库字段名") return /表名|数据库字段名|SQL字段|KSQL字段/i.test(question);
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, join, relative } from "node:path";
|
|
3
|
+
import { resolveWorkspacePath } from "../platform/path.ts";
|
|
4
|
+
|
|
5
|
+
const IGNORED_DIRS = new Set([".git", ".idea", ".pi", ".tmp", ".vscode", "bin", "build", "dist", "node_modules", "obj", "out", "target"]);
|
|
6
|
+
const DEFAULT_MAX_RESULTS = 50;
|
|
7
|
+
const MAX_ENTRIES = 50000;
|
|
8
|
+
|
|
9
|
+
export interface FileSearchResult {
|
|
10
|
+
path: string;
|
|
11
|
+
type: "file" | "directory";
|
|
12
|
+
size?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function findFiles(cwd: string, input: { root?: string; name: string; maxResults?: number }): FileSearchResult[] {
|
|
16
|
+
const root = resolveWorkspacePath(cwd, input.root || ".");
|
|
17
|
+
const pattern = input.name.trim();
|
|
18
|
+
if (!pattern) throw new Error("kd_find_file requires a non-empty name.");
|
|
19
|
+
if (!existsSync(root) || !statSync(root).isDirectory()) throw new Error(`Search root is not a directory: ${root}`);
|
|
20
|
+
|
|
21
|
+
const maxResults = clampLimit(input.maxResults);
|
|
22
|
+
const matcher = wildcardMatcher(pattern);
|
|
23
|
+
const results: FileSearchResult[] = [];
|
|
24
|
+
const queue = [root];
|
|
25
|
+
let visited = 0;
|
|
26
|
+
|
|
27
|
+
while (queue.length > 0 && results.length < maxResults && visited < MAX_ENTRIES) {
|
|
28
|
+
const current = queue.shift();
|
|
29
|
+
if (!current) break;
|
|
30
|
+
|
|
31
|
+
let children;
|
|
32
|
+
try {
|
|
33
|
+
children = readdirSync(current, { withFileTypes: true });
|
|
34
|
+
} catch {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const child of children) {
|
|
39
|
+
if (results.length >= maxResults || visited >= MAX_ENTRIES) break;
|
|
40
|
+
const fullPath = join(current, child.name);
|
|
41
|
+
visited++;
|
|
42
|
+
|
|
43
|
+
if (child.isDirectory()) {
|
|
44
|
+
if (!IGNORED_DIRS.has(child.name)) queue.push(fullPath);
|
|
45
|
+
if (matcher(child.name)) results.push({ path: displayPath(cwd, fullPath), type: "directory" });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (child.isFile() && matcher(child.name)) {
|
|
50
|
+
let size: number | undefined;
|
|
51
|
+
try {
|
|
52
|
+
size = statSync(fullPath).size;
|
|
53
|
+
} catch {
|
|
54
|
+
size = undefined;
|
|
55
|
+
}
|
|
56
|
+
results.push({ path: displayPath(cwd, fullPath), type: "file", size });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return results;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function listDirectory(cwd: string, input: { path?: string; maxEntries?: number }): FileSearchResult[] {
|
|
65
|
+
const dir = resolveWorkspacePath(cwd, input.path || ".");
|
|
66
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) throw new Error(`Path is not a directory: ${dir}`);
|
|
67
|
+
const maxEntries = clampLimit(input.maxEntries);
|
|
68
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
69
|
+
.slice(0, maxEntries)
|
|
70
|
+
.map((entry) => {
|
|
71
|
+
const fullPath = join(dir, entry.name);
|
|
72
|
+
if (entry.isDirectory()) return { path: displayPath(cwd, fullPath), type: "directory" as const };
|
|
73
|
+
let size: number | undefined;
|
|
74
|
+
try {
|
|
75
|
+
size = statSync(fullPath).size;
|
|
76
|
+
} catch {
|
|
77
|
+
size = undefined;
|
|
78
|
+
}
|
|
79
|
+
return { path: displayPath(cwd, fullPath), type: "file" as const, size };
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function formatFileSearchResults(results: FileSearchResult[]): string {
|
|
84
|
+
if (results.length === 0) return "未找到匹配项。";
|
|
85
|
+
return results.map((item) => `- ${item.type === "directory" ? "[dir]" : "[file]"} ${item.path}${item.size === undefined ? "" : ` (${item.size} bytes)`}`).join("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function clampLimit(value: number | undefined): number {
|
|
89
|
+
return Math.max(1, Math.min(Math.trunc(value ?? DEFAULT_MAX_RESULTS), 500));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function displayPath(cwd: string, fullPath: string): string {
|
|
93
|
+
const rel = relative(cwd, fullPath);
|
|
94
|
+
if (rel && !rel.startsWith("..") && !/^[a-zA-Z]:/.test(rel)) return rel.replace(/\//g, "\\");
|
|
95
|
+
return fullPath;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function wildcardMatcher(pattern: string): (name: string) => boolean {
|
|
99
|
+
const normalized = pattern.trim();
|
|
100
|
+
if (!/[*?]/.test(normalized)) {
|
|
101
|
+
const target = normalized.toLowerCase();
|
|
102
|
+
return (name) => basename(name).toLowerCase() === target;
|
|
103
|
+
}
|
|
104
|
+
const escaped = normalized.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
105
|
+
const regex = new RegExp(`^${escaped}$`, "i");
|
|
106
|
+
return (name) => regex.test(basename(name));
|
|
107
|
+
}
|