kcode-pi 0.1.30 → 0.1.34
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/extensions/kingdee-harness.ts +10 -0
- package/extensions/kingdee-header.ts +93 -9
- package/extensions/kingdee-tools.ts +130 -2
- package/package.json +6 -2
- package/src/harness/gates.ts +10 -0
- package/src/harness/messages.ts +5 -0
- package/src/harness/state.ts +57 -0
|
@@ -365,6 +365,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
365
365
|
return { block: true, reason };
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
// Redirect read tool to kd_doc_read for office document formats
|
|
369
|
+
if (event.toolName === "read" && path) {
|
|
370
|
+
const ext = path.toLowerCase().split(".").pop();
|
|
371
|
+
if (ext === "pdf" || ext === "docx" || ext === "doc" || ext === "xlsx" || ext === "xls" || ext === "csv") {
|
|
372
|
+
const reason = `read 工具无法解析 .${ext} 二进制格式。下一步:改用 kd_doc_read 工具读取此文件,参数 path="${path}"。`;
|
|
373
|
+
if (ctx.hasUI) ctx.ui.notify(reason, "info");
|
|
374
|
+
return { block: true, reason };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
368
378
|
const subagentRole = isSubagentChild() ? subagentRoleFromEnv() : undefined;
|
|
369
379
|
if (subagentRole) {
|
|
370
380
|
const run = readActiveRun(ctx.cwd);
|
|
@@ -4,6 +4,96 @@ import { readActiveRun } from "../src/harness/state.ts";
|
|
|
4
4
|
import type { ActiveRun, GateResult } from "../src/harness/types.ts";
|
|
5
5
|
import { formatProductProfile } from "../src/product/profile.ts";
|
|
6
6
|
|
|
7
|
+
/** ANSI escape sequence pattern: CSI, OSC, APC. */
|
|
8
|
+
const ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]|\x1b\][^\x07]*\x07|\x1b_[^\x1b]*\x1b\\/g;
|
|
9
|
+
|
|
10
|
+
/** Visible width of a string (strips ANSI codes; CJK/wide chars = 2 columns). */
|
|
11
|
+
function visibleWidth(str: string): number {
|
|
12
|
+
let width = 0;
|
|
13
|
+
const clean = str.replace(ANSI_RE, "");
|
|
14
|
+
for (const ch of clean) {
|
|
15
|
+
const code = ch.codePointAt(0)!;
|
|
16
|
+
width +=
|
|
17
|
+
code >= 0x1100 &&
|
|
18
|
+
!(code >= 0x00a0 && code <= 0x00ff) &&
|
|
19
|
+
((code >= 0x1100 && code <= 0x115f) ||
|
|
20
|
+
(code >= 0x2329 && code <= 0x232a) ||
|
|
21
|
+
(code >= 0x2e80 && code <= 0x303e) ||
|
|
22
|
+
(code >= 0x3040 && code <= 0x3247) ||
|
|
23
|
+
(code >= 0x3250 && code <= 0x4dbf) ||
|
|
24
|
+
(code >= 0x4e00 && code <= 0xa4c6) ||
|
|
25
|
+
(code >= 0xa960 && code <= 0xa97c) ||
|
|
26
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
27
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
28
|
+
(code >= 0xfe10 && code <= 0xfe19) ||
|
|
29
|
+
(code >= 0xfe30 && code <= 0xfe6b) ||
|
|
30
|
+
(code >= 0xff01 && code <= 0xff60) ||
|
|
31
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
32
|
+
(code >= 0x1f300 && code <= 0x1f9ff) ||
|
|
33
|
+
(code >= 0x20000 && code <= 0x2fffd))
|
|
34
|
+
? 2
|
|
35
|
+
: 1;
|
|
36
|
+
}
|
|
37
|
+
return width;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* If the line's visible width exceeds `maxWidth`, truncate visible characters
|
|
42
|
+
* and append `>` so the result fits. Preserves ANSI codes in the kept portion
|
|
43
|
+
* and appends SGR reset before the `>`. No padding — pi-tui only requires
|
|
44
|
+
* visibleWidth <= width.
|
|
45
|
+
*/
|
|
46
|
+
export function clipLine(text: string, maxWidth: number): string {
|
|
47
|
+
if (maxWidth <= 0) return "";
|
|
48
|
+
const vw = visibleWidth(text);
|
|
49
|
+
if (vw <= maxWidth) return text;
|
|
50
|
+
|
|
51
|
+
const targetW = maxWidth - 1; // reserve 1 col for ">"
|
|
52
|
+
let result = "";
|
|
53
|
+
let visibleSoFar = 0;
|
|
54
|
+
let i = 0;
|
|
55
|
+
|
|
56
|
+
while (i < text.length && visibleSoFar < targetW) {
|
|
57
|
+
// Preserve ANSI escape sequences
|
|
58
|
+
if (text[i] === "\x1b") {
|
|
59
|
+
const m = text.slice(i).match(/^\x1b(?:\[[0-9;]*[A-Za-z]|\][^\x07]*\x07|_[^\x1b]*\x1b\\)/);
|
|
60
|
+
if (m) {
|
|
61
|
+
result += m[0];
|
|
62
|
+
i += m[0].length;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const ch = text[i];
|
|
68
|
+
const code = ch.codePointAt(0)!;
|
|
69
|
+
const wide =
|
|
70
|
+
(code >= 0x1100 && code <= 0x115f) ||
|
|
71
|
+
(code >= 0x2329 && code <= 0x232a) ||
|
|
72
|
+
(code >= 0x2e80 && code <= 0x303e) ||
|
|
73
|
+
(code >= 0x3040 && code <= 0x3247) ||
|
|
74
|
+
(code >= 0x3250 && code <= 0x4dbf) ||
|
|
75
|
+
(code >= 0x4e00 && code <= 0xa4c6) ||
|
|
76
|
+
(code >= 0xa960 && code <= 0xa97c) ||
|
|
77
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
78
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
79
|
+
(code >= 0xfe10 && code <= 0xfe19) ||
|
|
80
|
+
(code >= 0xfe30 && code <= 0xfe6b) ||
|
|
81
|
+
(code >= 0xff01 && code <= 0xff60) ||
|
|
82
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
83
|
+
(code >= 0x1f300 && code <= 0x1f9ff) ||
|
|
84
|
+
(code >= 0x20000 && code <= 0x2fffd);
|
|
85
|
+
const charW = wide ? 2 : 1;
|
|
86
|
+
|
|
87
|
+
if (visibleSoFar + charW > targetW) break;
|
|
88
|
+
|
|
89
|
+
result += ch;
|
|
90
|
+
visibleSoFar += charW;
|
|
91
|
+
i++;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return result + "\x1b[0m>";
|
|
95
|
+
}
|
|
96
|
+
|
|
7
97
|
function formatProduct(run: ActiveRun | undefined): string {
|
|
8
98
|
if (!run) return "未选择";
|
|
9
99
|
if (run.profile?.product === "unknown") return "未确认";
|
|
@@ -43,12 +133,6 @@ function riskColor(risk: string): "error" | "warning" | "muted" | "success" {
|
|
|
43
133
|
return "success";
|
|
44
134
|
}
|
|
45
135
|
|
|
46
|
-
function padOrTrim(text: string, width: number): string {
|
|
47
|
-
if (width <= 0) return "";
|
|
48
|
-
if (text.length > width) return text.slice(0, Math.max(0, width - 1)) + ">";
|
|
49
|
-
return text + " ".repeat(width - text.length);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
136
|
function logoLines(theme: Theme): string[] {
|
|
53
137
|
const accent = (text: string) => theme.fg("accent", text);
|
|
54
138
|
const muted = (text: string) => theme.fg("muted", text);
|
|
@@ -87,9 +171,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
87
171
|
|
|
88
172
|
return [
|
|
89
173
|
"",
|
|
90
|
-
...logoLines(theme).map((line) =>
|
|
91
|
-
|
|
92
|
-
|
|
174
|
+
...logoLines(theme).map((line) => clipLine(line, width)),
|
|
175
|
+
clipLine(status, width),
|
|
176
|
+
clipLine(theme.fg("dim", `run:${runId}`), width),
|
|
93
177
|
"",
|
|
94
178
|
];
|
|
95
179
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { dirname, join } from "node:path";
|
|
1
|
+
import { dirname, join, extname } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
3
|
+
import { readFileSync, readFile as fsReadFile } from "node:fs";
|
|
4
4
|
import { Type } from "@earendil-works/pi-ai";
|
|
5
5
|
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { formatSearchResults, formatTableSchema } from "../src/knowledge/format.ts";
|
|
@@ -430,6 +430,133 @@ const kdDebugTool = defineTool({
|
|
|
430
430
|
},
|
|
431
431
|
});
|
|
432
432
|
|
|
433
|
+
const kdDocReadTool = defineTool({
|
|
434
|
+
name: "kd_doc_read",
|
|
435
|
+
label: "KD 文档读取",
|
|
436
|
+
description:
|
|
437
|
+
"读取 PDF、Word (.docx/.doc)、Excel (.xlsx/.xls) 或 CSV 文件并提取文本内容。对于 .pdf、.docx、.doc、.xlsx、.xls、.csv 文件,必须使用此工具而非 read 工具,因为 read 无法解析这些二进制格式。PDF 目前只支持可复制文本抽取;扫描型 PDF 需要另行提供图片或 OCR 结果。",
|
|
438
|
+
parameters: Type.Object({
|
|
439
|
+
path: Type.String({ description: "文档文件路径,支持 .pdf、.docx、.doc、.xlsx、.xls、.csv。" }),
|
|
440
|
+
sheet: Type.Optional(Type.String({ description: "Excel 工作表名或序号(从 1 开始)。默认读取第一个工作表。" })),
|
|
441
|
+
maxRows: Type.Optional(Type.Number({ description: "Excel/CSV 最大返回行数。默认 200。" })),
|
|
442
|
+
maxPages: Type.Optional(Type.Number({ description: "PDF 最大提取页数。默认 50。" })),
|
|
443
|
+
}),
|
|
444
|
+
|
|
445
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
446
|
+
const filePath = resolveWorkspacePath(ctx.cwd, params.path);
|
|
447
|
+
const ext = extname(filePath).toLowerCase();
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
let text: string;
|
|
451
|
+
|
|
452
|
+
if (ext === ".pdf") {
|
|
453
|
+
text = await extractPdf(filePath, params.maxPages ?? 50);
|
|
454
|
+
} else if (ext === ".docx") {
|
|
455
|
+
text = await extractDocx(filePath);
|
|
456
|
+
} else if (ext === ".doc") {
|
|
457
|
+
text = await extractDoc(filePath);
|
|
458
|
+
} else if (ext === ".xlsx" || ext === ".xls") {
|
|
459
|
+
text = extractXlsx(filePath, params.sheet, params.maxRows ?? 200);
|
|
460
|
+
} else if (ext === ".csv") {
|
|
461
|
+
text = extractCsv(filePath, params.maxRows ?? 200);
|
|
462
|
+
} else {
|
|
463
|
+
return {
|
|
464
|
+
content: [{ type: "text", text: `不支持的文件格式:${ext}。支持 .pdf、.docx、.doc、.xlsx、.xls、.csv。` }],
|
|
465
|
+
details: { error: "unsupported-format", ext },
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
content: [{ type: "text", text }],
|
|
471
|
+
details: { path: params.path, format: ext },
|
|
472
|
+
};
|
|
473
|
+
} catch (error) {
|
|
474
|
+
return {
|
|
475
|
+
content: [{ type: "text", text: `读取文档失败:${error instanceof Error ? error.message : String(error)}` }],
|
|
476
|
+
details: { error: "doc-read-failed", path: params.path },
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
async function extractPdf(filePath: string, maxPages: number): Promise<string> {
|
|
483
|
+
const { PDFParse } = await import("pdf-parse");
|
|
484
|
+
const buffer = readFileSync(filePath);
|
|
485
|
+
const parser = new PDFParse({ data: new Uint8Array(buffer) });
|
|
486
|
+
const result = await parser.getText({ last: maxPages });
|
|
487
|
+
const pages = result.total;
|
|
488
|
+
const text = result.text.trim();
|
|
489
|
+
return `[PDF] ${pages} 页\n\n${text}`;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function extractDocx(filePath: string): Promise<string> {
|
|
493
|
+
const mammoth = await import("mammoth");
|
|
494
|
+
const buffer = readFileSync(filePath);
|
|
495
|
+
const result = await mammoth.extractRawText({ buffer });
|
|
496
|
+
return `[Word .docx]\n\n${result.value.trim()}`;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async function extractDoc(filePath: string): Promise<string> {
|
|
500
|
+
const word = await import("word");
|
|
501
|
+
const doc = await word.readFile(filePath);
|
|
502
|
+
const text = word.to_text(doc);
|
|
503
|
+
return `[Word .doc]\n\n${text.trim()}`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function extractXlsx(filePath: string, sheetNameOrIndex?: string, maxRows?: number): string {
|
|
507
|
+
const XLSX = require("xlsx");
|
|
508
|
+
const workbook = XLSX.readFile(filePath);
|
|
509
|
+
const sheetNames = workbook.SheetNames;
|
|
510
|
+
|
|
511
|
+
let sheetName: string;
|
|
512
|
+
if (sheetNameOrIndex) {
|
|
513
|
+
const idx = Number(sheetNameOrIndex);
|
|
514
|
+
if (!isNaN(idx) && idx >= 1 && idx <= sheetNames.length) {
|
|
515
|
+
sheetName = sheetNames[idx - 1];
|
|
516
|
+
} else if (sheetNames.includes(sheetNameOrIndex)) {
|
|
517
|
+
sheetName = sheetNameOrIndex;
|
|
518
|
+
} else {
|
|
519
|
+
return `[Excel] 工作表 "${sheetNameOrIndex}" 不存在。可用:${sheetNames.join(", ")}`;
|
|
520
|
+
}
|
|
521
|
+
} else {
|
|
522
|
+
sheetName = sheetNames[0];
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const sheet = workbook.Sheets[sheetName];
|
|
526
|
+
const rows: Record<string, unknown>[] = XLSX.utils.sheet_to_json(sheet, { defval: "" });
|
|
527
|
+
const limit = Math.min(rows.length, maxRows ?? 200);
|
|
528
|
+
|
|
529
|
+
if (rows.length === 0) return `[Excel] 工作表 "${sheetName}" 为空。`;
|
|
530
|
+
|
|
531
|
+
const headers = Object.keys(rows[0]);
|
|
532
|
+
const lines: string[] = [
|
|
533
|
+
`[Excel] 工作表:${sheetName}(${sheetNames.length} 个:${sheetNames.join(", ")})`,
|
|
534
|
+
`行数:${rows.length}${rows.length > limit ? `(显示前 ${limit} 行)` : ""}`,
|
|
535
|
+
"",
|
|
536
|
+
"| " + headers.join(" | ") + " |",
|
|
537
|
+
"| " + headers.map(() => "---").join(" | ") + " |",
|
|
538
|
+
];
|
|
539
|
+
|
|
540
|
+
for (let i = 0; i < limit; i++) {
|
|
541
|
+
const row = rows[i];
|
|
542
|
+
lines.push("| " + headers.map((h) => String(row[h] ?? "")).join(" | ") + " |");
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return lines.join("\n");
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function extractCsv(filePath: string, maxRows: number): string {
|
|
549
|
+
const XLSX = require("xlsx");
|
|
550
|
+
const workbook = XLSX.readFile(filePath, { raw: true });
|
|
551
|
+
const sheetName = workbook.SheetNames[0];
|
|
552
|
+
const sheet = workbook.Sheets[sheetName];
|
|
553
|
+
const csv = XLSX.utils.sheet_to_csv(sheet);
|
|
554
|
+
const lines = csv.split("\n");
|
|
555
|
+
const limit = Math.min(lines.length, maxRows + 1); // +1 for header
|
|
556
|
+
const truncated = lines.length > limit;
|
|
557
|
+
return `[CSV] ${lines.length - 1} 行${truncated ? `(显示前 ${limit - 1} 行)` : ""}\n\n${lines.slice(0, limit).join("\n")}`;
|
|
558
|
+
}
|
|
559
|
+
|
|
433
560
|
export default function (pi: ExtensionAPI) {
|
|
434
561
|
pi.registerTool(kdSearchTool);
|
|
435
562
|
pi.registerTool(kdTableTool);
|
|
@@ -441,6 +568,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
441
568
|
pi.registerTool(kdKsqlLintTool);
|
|
442
569
|
pi.registerTool(kdBuildTool);
|
|
443
570
|
pi.registerTool(kdDebugTool);
|
|
571
|
+
pi.registerTool(kdDocReadTool);
|
|
444
572
|
}
|
|
445
573
|
|
|
446
574
|
function writeSdkSignatureEvidence(cwd: string, content: string): string | undefined {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kcode-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.34",
|
|
4
4
|
"description": "面向金蝶开发的 Pi Coding Agent 启动器、工具包和 Harness 工作流",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -47,7 +47,11 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@earendil-works/pi-ai": "^0.78.1",
|
|
50
|
-
"@earendil-works/pi-coding-agent": "^0.78.1"
|
|
50
|
+
"@earendil-works/pi-coding-agent": "^0.78.1",
|
|
51
|
+
"mammoth": "^1.12.0",
|
|
52
|
+
"pdf-parse": "^2.4.5",
|
|
53
|
+
"word": "^0.4.0",
|
|
54
|
+
"xlsx": "^0.18.5"
|
|
51
55
|
},
|
|
52
56
|
"devDependencies": {
|
|
53
57
|
"@types/node": "^25.9.2",
|
package/src/harness/gates.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
missingForTargetReason,
|
|
17
17
|
missingMarkerReason,
|
|
18
18
|
openQuestionsReason,
|
|
19
|
+
repairBlockedReason,
|
|
19
20
|
unknownProductReason,
|
|
20
21
|
unknownRiskReason,
|
|
21
22
|
} from "./messages.ts";
|
|
@@ -71,6 +72,7 @@ function collectGateProblems(cwd: string, run: ActiveRun, phase: KdPhase, mode:
|
|
|
71
72
|
const stepProblem = inspectStepState(cwd, run, phase);
|
|
72
73
|
const evidenceProblem = mode === "inspect" ? inspectEvidence(cwd, run, phase) : undefined;
|
|
73
74
|
const questionProblem = inspectOpenQuestions(run);
|
|
75
|
+
const repairProblem = inspectRepairState(run);
|
|
74
76
|
|
|
75
77
|
if (flagshipPathProblem) {
|
|
76
78
|
reasonParts.push(flagshipPathProblem);
|
|
@@ -87,6 +89,9 @@ function collectGateProblems(cwd: string, run: ActiveRun, phase: KdPhase, mode:
|
|
|
87
89
|
if (questionProblem) {
|
|
88
90
|
reasonParts.push(questionProblem);
|
|
89
91
|
}
|
|
92
|
+
if (repairProblem) {
|
|
93
|
+
reasonParts.push(repairProblem);
|
|
94
|
+
}
|
|
90
95
|
|
|
91
96
|
if (mode === "enter") {
|
|
92
97
|
missing.push(...missingEvidenceForPhase(cwd, run, phase));
|
|
@@ -148,6 +153,11 @@ function inspectOpenQuestions(run: ActiveRun): string | undefined {
|
|
|
148
153
|
return openQuestionsReason(open);
|
|
149
154
|
}
|
|
150
155
|
|
|
156
|
+
function inspectRepairState(run: ActiveRun): string | undefined {
|
|
157
|
+
if (run.repair?.status !== "blocked") return undefined;
|
|
158
|
+
return repairBlockedReason(run.repair.attempts, run.repair.maxAttempts, run.repair.lastFailureEvidence);
|
|
159
|
+
}
|
|
160
|
+
|
|
151
161
|
function inspectStepState(cwd: string, run: ActiveRun, phase: KdPhase): string | undefined {
|
|
152
162
|
if (phase === "execute") {
|
|
153
163
|
const plan = readArtifact(cwd, run, "plan") ?? "";
|
package/src/harness/messages.ts
CHANGED
|
@@ -19,6 +19,11 @@ export function openQuestionsReason(questions: Array<{ id: string; question: str
|
|
|
19
19
|
].join("。");
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export function repairBlockedReason(attempts: number, maxAttempts: number, evidence?: string): string {
|
|
23
|
+
const evidenceText = evidence ? `,最近失败证据:${evidence}` : "";
|
|
24
|
+
return `自动修复已停止:验证失败 ${attempts}/${maxAttempts}${evidenceText}。下一步:回到 plan 调整范围,或重新创建/回答修复问题选择“继续修复”。`;
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
export function missingMarkerReason(phase: KdPhase, missing: string[]): string {
|
|
23
28
|
return `${PHASE_ARTIFACTS[phase]} 缺少必需章节:${missing.join(", ")}。下一步:更新 ${PHASE_ARTIFACTS[phase]},补齐这些章节并写入真实内容。`;
|
|
24
29
|
}
|
package/src/harness/state.ts
CHANGED
|
@@ -123,6 +123,7 @@ export function answerQuestion(cwd: string, run: ActiveRun, id: string, answer:
|
|
|
123
123
|
question.status = "answered";
|
|
124
124
|
question.answer = answer.trim();
|
|
125
125
|
question.answeredAt = new Date().toISOString();
|
|
126
|
+
applyRepairQuestionAnswer(cwd, run, question);
|
|
126
127
|
run.gate = inspectGate(cwd, run);
|
|
127
128
|
writeActiveRun(cwd, run);
|
|
128
129
|
return question;
|
|
@@ -216,6 +217,62 @@ function writeRunState(cwd: string, run: ActiveRun): void {
|
|
|
216
217
|
writeFileSync(runStatePath(cwd, run), `${JSON.stringify(run, null, 2)}\n`, "utf8");
|
|
217
218
|
}
|
|
218
219
|
|
|
220
|
+
function applyRepairQuestionAnswer(cwd: string, run: ActiveRun, question: KdQuestion): void {
|
|
221
|
+
if (!isRepairLimitQuestion(question)) return;
|
|
222
|
+
const answer = question.answer?.trim() ?? "";
|
|
223
|
+
const now = new Date().toISOString();
|
|
224
|
+
const attempts = run.repair?.attempts ?? 0;
|
|
225
|
+
const maxAttempts = run.repair?.maxAttempts ?? 3;
|
|
226
|
+
const extendedMaxAttempts = attempts + maxAttempts;
|
|
227
|
+
|
|
228
|
+
if (/^继续修复\b|^继续|continue/i.test(answer)) {
|
|
229
|
+
run.phase = "execute";
|
|
230
|
+
ensureArtifact(cwd, run, "execute", defaultArtifactContent("execute", run.goal, run.profile));
|
|
231
|
+
run.repair = {
|
|
232
|
+
attempts,
|
|
233
|
+
maxAttempts: extendedMaxAttempts,
|
|
234
|
+
lastFailureEvidence: run.repair?.lastFailureEvidence,
|
|
235
|
+
lastFailureSignature: run.repair?.lastFailureSignature,
|
|
236
|
+
status: "repairing",
|
|
237
|
+
updatedAt: now,
|
|
238
|
+
};
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (/^回到\s*plan\b|^回到计划|^返回\s*plan\b|^重新计划/i.test(answer)) {
|
|
243
|
+
run.phase = "plan";
|
|
244
|
+
ensureArtifact(cwd, run, "plan", defaultArtifactContent("plan", run.goal, run.profile));
|
|
245
|
+
run.repair = {
|
|
246
|
+
attempts,
|
|
247
|
+
maxAttempts: extendedMaxAttempts,
|
|
248
|
+
lastFailureEvidence: run.repair?.lastFailureEvidence,
|
|
249
|
+
lastFailureSignature: run.repair?.lastFailureSignature,
|
|
250
|
+
status: "idle",
|
|
251
|
+
updatedAt: now,
|
|
252
|
+
};
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (/^停止|^stop\b/i.test(answer)) {
|
|
257
|
+
run.repair = {
|
|
258
|
+
attempts: run.repair?.attempts ?? maxAttempts,
|
|
259
|
+
maxAttempts,
|
|
260
|
+
lastFailureEvidence: run.repair?.lastFailureEvidence,
|
|
261
|
+
lastFailureSignature: run.repair?.lastFailureSignature,
|
|
262
|
+
status: "blocked",
|
|
263
|
+
updatedAt: now,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function isRepairLimitQuestion(question: KdQuestion): boolean {
|
|
269
|
+
return (
|
|
270
|
+
question.phase === "verify" &&
|
|
271
|
+
/验证失败已达到/.test(question.question) &&
|
|
272
|
+
(Array.isArray(question.choices) ? question.choices.includes("继续修复") : true)
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
219
276
|
function hydrateRun(parsed: ActiveRun): ActiveRun | undefined {
|
|
220
277
|
if (!parsed || typeof parsed !== "object") return undefined;
|
|
221
278
|
if (typeof parsed.id !== "string" || !parsed.id.trim()) return undefined;
|