minimal-agent 0.12.0 → 0.13.0
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/package.json +3 -2
- package/plugins/workflow-runner/src/expressions.js +29 -8
- package/plugins/workflow-runner/src/expressions.ts +28 -10
- package/plugins/workflow-runner/src/index.js +45 -17
- package/plugins/workflow-runner/src/index.ts +44 -18
- package/plugins/workflow-runner/src/recovery.js +9 -2
- package/plugins/workflow-runner/src/recovery.ts +9 -2
- package/plugins/workflow-runner/src/runner.js +31 -5
- package/plugins/workflow-runner/src/runner.ts +33 -5
- package/plugins/workflow-runner/src/stepExecutors/assert.js +2 -1
- package/plugins/workflow-runner/src/stepExecutors/assert.ts +3 -1
- package/plugins/workflow-runner/src/stepExecutors/tool.js +29 -10
- package/plugins/workflow-runner/src/stepExecutors/tool.ts +29 -11
- package/plugins/workflow-runner/src/stepExecutors/verify.js +6 -3
- package/plugins/workflow-runner/src/stepExecutors/verify.ts +7 -3
- package/plugins/workflow-runner/src/taskIdentity.js +197 -21
- package/plugins/workflow-runner/src/taskIdentity.ts +201 -20
- package/plugins/workflow-runner/src/types.js +113 -7
- package/plugins/workflow-runner/src/types.ts +111 -7
- package/plugins/workflow-runner/src/workflowState.js +455 -54
- package/plugins/workflow-runner/src/workflowState.ts +508 -63
- package/plugins/workflow-runner/test/cli.e2e.test.ts +20 -0
- package/plugins/workflow-runner/test/e2eNewLayout.test.ts +314 -0
- package/plugins/workflow-runner/test/engineRobustness.test.ts +9 -1
- package/plugins/workflow-runner/test/expressions.test.ts +42 -0
- package/plugins/workflow-runner/test/illustrate.load.test.ts +2 -2
- package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +19 -0
- package/plugins/workflow-runner/test/ppt.load.test.ts +15 -15
- package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +3 -2
- package/plugins/workflow-runner/test/research.load.test.ts +328 -0
- package/plugins/workflow-runner/test/runnerPreflight.test.ts +68 -11
- package/plugins/workflow-runner/test/stateLifecycle.test.ts +60 -12
- package/plugins/workflow-runner/test/taskArbitration.test.ts +19 -3
- package/plugins/workflow-runner/test/taskLifecycle.test.ts +61 -7
- package/plugins/workflow-runner/test/webppt.load.test.ts +4 -4
- package/plugins/workflow-runner/test/word.load.test.ts +9 -9
- package/skills/skill-creator/scripts/run_eval.py +3 -2
- package/src/cli/print.js +17 -0
- package/src/cli/print.ts +25 -0
- package/src/config/configService.js +24 -0
- package/src/config/configService.ts +23 -0
- package/src/infra/verification.js +25 -18
- package/src/infra/verification.ts +50 -18
- package/src/plugins/pluginRunner.js +18 -7
- package/src/plugins/pluginRunner.ts +20 -6
- package/src/prompts/policyRegistry.js +28 -1
- package/src/prompts/policyRegistry.ts +28 -1
- package/src/tools/advisor/advisor.js +76 -9
- package/src/tools/advisor/advisor.ts +79 -9
- package/src/tools/bash/bash.js +51 -6
- package/src/tools/bash/bash.ts +56 -7
- package/src/tools/powershell/powershell.js +18 -1
- package/src/tools/powershell/powershell.ts +18 -2
- package/src/tools/read/read.js +9 -4
- package/src/tools/read/read.ts +9 -4
- package/src/tools/shared/permanentDeleteGuard.js +34 -3
- package/src/tools/shared/permanentDeleteGuard.ts +43 -6
- package/src/tools/webbrowser/browserControl.js +5 -1
- package/src/tools/webbrowser/browserControl.ts +7 -1
- package/src/tools/webbrowser/webbrowser.js +8 -4
- package/src/tools/webbrowser/webbrowser.ts +8 -4
- package/src/types.ts +10 -4
- package/workflows/adaptive-research.yaml +5 -5
- package/workflows/book-review-short.yaml +5 -5
- package/workflows/e2e-write-greet.yaml +2 -2
- package/workflows/illustrate.yaml +12 -12
- package/workflows/plan.yaml +18 -18
- package/workflows/ppt.yaml +201 -201
- package/workflows/ralph-loop.yaml +3 -1
- package/workflows/research.md +449 -0
- package/workflows/research.yaml +873 -0
- package/workflows/webppt.yaml +47 -47
- package/workflows/word.yaml +80 -80
- package/workflows/youtube-shorts.yaml +25 -25
|
@@ -67,9 +67,10 @@ describe('任务身份生命周期(L2 completed / L3 new-task 清产物)', (
|
|
|
67
67
|
writeFileSync(join(tmp, 'PLAN.md'), '# plan');
|
|
68
68
|
await s1.markDone();
|
|
69
69
|
await s1.archive();
|
|
70
|
-
//
|
|
71
|
-
expect(existsSync(join(tmp, '.minimal-agent-workflow', 'plan'))).toBe(
|
|
72
|
-
expect(existsSync(
|
|
70
|
+
// 归档后活动实例被改名 .done(workflow 级父目录保留——里面可能有多个实例)
|
|
71
|
+
expect(existsSync(join(tmp, '.minimal-agent-workflow', 'plan'))).toBe(true);
|
|
72
|
+
expect(existsSync(s1.dir + '.done')).toBe(true);
|
|
73
|
+
expect(existsSync(s1.dir)).toBe(false);
|
|
73
74
|
|
|
74
75
|
// 第二次:同 goal 重跑 → completed(经 .done 检测),报真实产物
|
|
75
76
|
const s2 = new WorkflowState(tmp, 'plan');
|
|
@@ -104,8 +105,9 @@ describe('任务身份生命周期(L2 completed / L3 new-task 清产物)', (
|
|
|
104
105
|
const s2 = new WorkflowState(tmp, 'plan');
|
|
105
106
|
const r2 = await s2.init('plan', { goal: '完全不相关的目标B' }, STEPS, 1, ['PLAN.md']);
|
|
106
107
|
expect(r2.mode).toBe('new-task');
|
|
107
|
-
expect(r2.cleanedArtifacts).toContain('PLAN.md');
|
|
108
108
|
expect(existsSync(join(tmp, 'PLAN.md'))).toBe(false);
|
|
109
|
+
expect(r2.archivedTo).toBeDefined();
|
|
110
|
+
expect(existsSync(join(tmp, r2.archivedTo as string, 'PLAN.md'))).toBe(true);
|
|
109
111
|
});
|
|
110
112
|
|
|
111
113
|
it('legacy 状态文件(无 artifacts 字段)+ 同名 workflow 换 goal → new-task 用本次声明清单兜底清理', async () => {
|
|
@@ -128,8 +130,9 @@ describe('任务身份生命周期(L2 completed / L3 new-task 清产物)', (
|
|
|
128
130
|
const r = await s.init('plan', { goal: '全新目标' }, STEPS, 1, ['PLAN.md']);
|
|
129
131
|
expect(r.mode).toBe('new-task');
|
|
130
132
|
// 兜底清理生效:同名 workflow 的声明产物语义稳定,按本次声明清单清掉残留
|
|
131
|
-
expect(r.cleanedArtifacts).toContain('PLAN.md');
|
|
132
133
|
expect(existsSync(join(tmp, 'PLAN.md'))).toBe(false);
|
|
134
|
+
expect(r.archivedTo).toBeDefined();
|
|
135
|
+
expect(existsSync(join(tmp, r.archivedTo as string, 'PLAN.md'))).toBe(true);
|
|
133
136
|
});
|
|
134
137
|
|
|
135
138
|
it('/new 后孤儿产物:无任何状态但声明产物已存在 → fresh 启动时清理(全新任务从干净状态开始)', async () => {
|
|
@@ -141,8 +144,59 @@ describe('任务身份生命周期(L2 completed / L3 new-task 清产物)', (
|
|
|
141
144
|
const r = await s.init('plan', { goal: '目标A' }, STEPS, 1, ['PLAN.md']);
|
|
142
145
|
expect(r.mode).toBe('fresh');
|
|
143
146
|
expect(existsSync(join(tmp, 'PLAN.md'))).toBe(false);
|
|
144
|
-
|
|
145
|
-
expect(r.
|
|
147
|
+
expect(r.archivedTo).toBeDefined();
|
|
148
|
+
expect(existsSync(join(tmp, r.archivedTo as string, 'PLAN.md'))).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('A4:归档保留相对路径结构,只归档声明产物、不碰未声明文件', async () => {
|
|
152
|
+
const { mkdirSync } = await import('node:fs');
|
|
153
|
+
mkdirSync(join(tmp, 'out'), { recursive: true });
|
|
154
|
+
const s1 = new WorkflowState(tmp, 'plan');
|
|
155
|
+
await s1.init('plan', { goal: '目标A' }, STEPS, 1, ['out/PLAN.md']);
|
|
156
|
+
writeFileSync(join(tmp, 'out', 'PLAN.md'), '# A');
|
|
157
|
+
writeFileSync(join(tmp, 'out', 'USER-OWN.md'), '# 用户自己的文件');
|
|
158
|
+
|
|
159
|
+
const s2 = new WorkflowState(tmp, 'plan');
|
|
160
|
+
const r2 = await s2.init('plan', { goal: '目标B' }, STEPS, 1, ['out/PLAN.md']);
|
|
161
|
+
expect(r2.mode).toBe('new-task');
|
|
162
|
+
expect(existsSync(join(tmp, 'out', 'PLAN.md'))).toBe(false);
|
|
163
|
+
expect(existsSync(join(tmp, 'out', 'USER-OWN.md'))).toBe(true);
|
|
164
|
+
expect(existsSync(join(tmp, r2.archivedTo as string, 'out', 'PLAN.md'))).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('B8:声明产物支持 glob(out/*.md),new-task 清理时展开为实际文件归档', async () => {
|
|
168
|
+
const { mkdirSync } = await import('node:fs');
|
|
169
|
+
mkdirSync(join(tmp, 'out'), { recursive: true });
|
|
170
|
+
const s1 = new WorkflowState(tmp, 'research');
|
|
171
|
+
await s1.init('research', { goal: '目标A' }, STEPS, 1, ['out/deep-dive-*.md']);
|
|
172
|
+
writeFileSync(join(tmp, 'out', 'deep-dive-a.md'), '# A');
|
|
173
|
+
writeFileSync(join(tmp, 'out', 'deep-dive-b.md'), '# B');
|
|
174
|
+
writeFileSync(join(tmp, 'out', 'notes.txt'), '# 不匹配 md');
|
|
175
|
+
|
|
176
|
+
const s2 = new WorkflowState(tmp, 'research');
|
|
177
|
+
const r2 = await s2.init('research', { goal: '目标B' }, STEPS, 1, ['out/deep-dive-*.md']);
|
|
178
|
+
expect(r2.mode).toBe('new-task');
|
|
179
|
+
expect(existsSync(join(tmp, 'out', 'deep-dive-a.md'))).toBe(false);
|
|
180
|
+
expect(existsSync(join(tmp, 'out', 'deep-dive-b.md'))).toBe(false);
|
|
181
|
+
expect(existsSync(join(tmp, 'out', 'notes.txt'))).toBe(true);
|
|
182
|
+
const trash = join(tmp, r2.archivedTo as string);
|
|
183
|
+
expect(existsSync(join(trash, 'out', 'deep-dive-a.md'))).toBe(true);
|
|
184
|
+
expect(existsSync(join(trash, 'out', 'deep-dive-b.md'))).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('B8:越界 glob(../x*)不展开不清理(fail-closed)', async () => {
|
|
188
|
+
writeFileSync(join(tmp, 'PLAN.md'), '# inside');
|
|
189
|
+
const outside = mkdtempSync(join(tmpdir(), 'wf-outside-'));
|
|
190
|
+
try {
|
|
191
|
+
writeFileSync(join(outside, 'secret.md'), '# outside');
|
|
192
|
+
const s = new WorkflowState(tmp, 'plan');
|
|
193
|
+
const r = await s.init('plan', { goal: '目标A' }, STEPS, 1, ['../x*.md', 'PLAN.md']);
|
|
194
|
+
expect(r.mode).toBe('fresh');
|
|
195
|
+
expect(existsSync(join(outside, 'secret.md'))).toBe(true);
|
|
196
|
+
expect(existsSync(join(tmp, 'PLAN.md'))).toBe(false);
|
|
197
|
+
} finally {
|
|
198
|
+
rmSync(outside, { recursive: true, force: true });
|
|
199
|
+
}
|
|
146
200
|
});
|
|
147
201
|
|
|
148
202
|
it('resume 恢复点用最后完成步的【索引】而非 checkpoint 计数(when 跳过步不再被重复执行)', async () => {
|
|
@@ -70,11 +70,11 @@ describe('workflows/webppt.yaml', () => {
|
|
|
70
70
|
expect(String(s.title)).toContain('/11');
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
// 收尾报告 echo;artifacts 只列最终交付物、不在被 rm 的
|
|
73
|
+
// 收尾报告 echo;artifacts 只列最终交付物、不在被 rm 的 out/webppt/${session}/build 下
|
|
74
74
|
expect(def.steps.find((s) => s.id === 'final_report')?.type).toBe('echo');
|
|
75
75
|
const declared = def.artifacts ?? [];
|
|
76
|
-
expect(declared).toContain('
|
|
77
|
-
for (const a of declared) expect(a.startsWith('
|
|
76
|
+
expect(declared).toContain('out/webppt/${session}/deck.pptx');
|
|
77
|
+
for (const a of declared) expect(a.startsWith('out/webppt/${session}/build')).toBe(false);
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
it('需求分析步:style 为空时必问视觉样式(VI 选项 + 自定义输入),AskUserQuestion 可用', async () => {
|
|
@@ -199,7 +199,7 @@ describe('workflows/webppt.yaml', () => {
|
|
|
199
199
|
// 配图分流闸门:image_plan_lint 的 --outline 直接吃 design.json
|
|
200
200
|
const gate = def.steps.find((s) => s.id === 'stage_img_route_gate');
|
|
201
201
|
expect(String(gate?.agent)).toContain('image_plan_lint.py');
|
|
202
|
-
expect(String(gate?.agent)).toContain('--outline
|
|
202
|
+
expect(String(gate?.agent)).toContain('--outline out/webppt/${session}/build/design.json');
|
|
203
203
|
expect(String(gate?.done_when)).toContain('img_route_ok');
|
|
204
204
|
// 顺序:build_loop → img_route_gate → gen_images
|
|
205
205
|
expect(ids.indexOf('stage_img_route_gate')).toBeGreaterThan(ids.indexOf('stage_build_loop'));
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* ------------------------------------------------------------
|
|
4
4
|
* 锁定 workflows/word.yaml(素材驱动知乎深度长文 Word 工作流)的
|
|
5
5
|
* 「可解析 + 关键骨架」:
|
|
6
|
-
* - name=word,最终产物是
|
|
6
|
+
* - name=word,最终产物是 out/word/${session}/article.docx
|
|
7
7
|
* - inputs 支持 topic(greedy) / source / out / image_intensity / quality / debug_keep_build
|
|
8
8
|
* - Markdown-first:material → outline → article.md → image_map/img_plan → docx skill → OPC
|
|
9
9
|
* - 客户图必须使用;需要解释型视觉时积极文生图,提示词必须结合上下文
|
|
@@ -32,8 +32,8 @@ describe('workflows/word.yaml(素材驱动·知乎深度长文 Word)', () =>
|
|
|
32
32
|
expect(def.description).toContain('知乎深度文章');
|
|
33
33
|
|
|
34
34
|
const declared = def.artifacts ?? [];
|
|
35
|
-
expect(declared).toContain('
|
|
36
|
-
for (const a of declared) expect(a.startsWith('
|
|
35
|
+
expect(declared).toContain('out/word/${session}/article.docx');
|
|
36
|
+
for (const a of declared) expect(a.startsWith('out/word/${session}/build')).toBe(false);
|
|
37
37
|
|
|
38
38
|
const inputNames = (def.inputs ?? []).map((i) => i.name);
|
|
39
39
|
expect(inputNames).toEqual(
|
|
@@ -49,7 +49,7 @@ describe('workflows/word.yaml(素材驱动·知乎深度长文 Word)', () =>
|
|
|
49
49
|
expect(inputNames[0]).toBe('topic');
|
|
50
50
|
expect((def.inputs ?? []).find((i) => i.name === 'topic')?.greedy).toBe(true);
|
|
51
51
|
expect((def.inputs ?? []).find((i) => i.name === 'out')?.default).toBe(
|
|
52
|
-
'
|
|
52
|
+
'out/word/${session}/article.docx',
|
|
53
53
|
);
|
|
54
54
|
});
|
|
55
55
|
|
|
@@ -163,11 +163,11 @@ describe('workflows/word.yaml(素材驱动·知乎深度长文 Word)', () =>
|
|
|
163
163
|
const extract = def.steps.find((s) => s.id === 'stage_extract');
|
|
164
164
|
const command = String(extract?.args?.command ?? '');
|
|
165
165
|
|
|
166
|
-
expect(command).toContain('
|
|
167
|
-
expect(command).toContain('rm -rf
|
|
168
|
-
expect(command).toContain('mkdir -p
|
|
169
|
-
expect(command).toContain('
|
|
170
|
-
expect(command).toContain('
|
|
166
|
+
expect(command).toContain('out/word/${session}/build/source.md');
|
|
167
|
+
expect(command).toContain('rm -rf out/word/${session}/build/assets');
|
|
168
|
+
expect(command).toContain('mkdir -p out/word/${session}/build/assets');
|
|
169
|
+
expect(command).toContain('out/word/${session}/build/word_topic_image_gate.json');
|
|
170
|
+
expect(command).toContain('out/word/${session}/build/word_topic_image_gate.md');
|
|
171
171
|
});
|
|
172
172
|
|
|
173
173
|
it('生成配图失败时会在导出前硬失败,不会把渐变占位图交付进 Word', async () => {
|
|
@@ -20,14 +20,15 @@ from scripts.utils import parse_skill_md
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def find_project_root() -> Path:
|
|
23
|
-
"""Find the project root by walking up from cwd looking for .
|
|
23
|
+
"""Find the project root by walking up from cwd looking for .kakadeai/
|
|
24
|
+
(this repo's project-config dir) or .claude/ (upstream convention).
|
|
24
25
|
|
|
25
26
|
Mimics how Claude Code discovers its project root, so the command file
|
|
26
27
|
we create ends up where claude -p will look for it.
|
|
27
28
|
"""
|
|
28
29
|
current = Path.cwd()
|
|
29
30
|
for parent in [current, *current.parents]:
|
|
30
|
-
if (parent / ".claude").is_dir():
|
|
31
|
+
if (parent / ".kakadeai").is_dir() or (parent / ".claude").is_dir():
|
|
31
32
|
return parent
|
|
32
33
|
return current
|
|
33
34
|
|
package/src/cli/print.js
CHANGED
|
@@ -108,8 +108,16 @@ export function extractImageOptions(args) {
|
|
|
108
108
|
* stop_reason = end_turn | max_turns | stalled | error | interrupted | config_error
|
|
109
109
|
* num_turns = assistant_message 事件计数
|
|
110
110
|
* error = 失败文案(成功时 null)
|
|
111
|
+
* warnings = 本轮所有非致命警告(与 stderr 同源;宿主要结构化的那一份)
|
|
111
112
|
* session_file(可选)= 当前对话落盘路径;拿得到才填,拿不到省略该键
|
|
112
113
|
*
|
|
114
|
+
* completionStatus = 本层从 stop_reason + deliverables 重算:跑完了没、产物验没验
|
|
115
|
+
* delivery_status(可选)= 内核 turn_done 的交付判定,**回答的是另一个问题**:
|
|
116
|
+
* completed / delivery_incomplete / delivery_unverified / claims_unverified。
|
|
117
|
+
* 两者并列而非替代 —— `ok:true` 配 `delivery_status:'claims_unverified'` 是
|
|
118
|
+
* 完全合法的一行:Agent 确实正常收尾了,但它宣称的「测试已通过」没有证据支撑。
|
|
119
|
+
* 判断「任务是否真做成」的宿主应当读它,而不是只看 ok。
|
|
120
|
+
*
|
|
113
121
|
* 导出供单测断言。
|
|
114
122
|
*/
|
|
115
123
|
export function emitJsonResult(result, sessionFile) {
|
|
@@ -136,6 +144,7 @@ export function emitJsonResult(result, sessionFile) {
|
|
|
136
144
|
num_turns: result.numTurns,
|
|
137
145
|
error: result.error,
|
|
138
146
|
completionStatus,
|
|
147
|
+
...(result.deliveryStatus ? { delivery_status: result.deliveryStatus } : {}),
|
|
139
148
|
deliverables,
|
|
140
149
|
verification: allVerified ? 'passed' : deliverables.length > 0 ? 'not_run' : 'none',
|
|
141
150
|
warnings,
|
|
@@ -399,11 +408,19 @@ export function handleEvent(event, result, verbose, outputFormat = 'text') {
|
|
|
399
408
|
// 通用非致命警告(如 workflow 多余参数、pause 被自动跳过等)始终走 stderr,
|
|
400
409
|
// 不受 verbose 控制 —— 这是用户必须看到的信号,避免静默丢弃语义被忽略。
|
|
401
410
|
process.stderr.write(`${event.message}\n`);
|
|
411
|
+
// 同时收进结局契约:stderr 是给人看的,spawn 它的宿主要的是结构化的一条。
|
|
412
|
+
// (此前 PrintResult.warnings 声明了、读了、也输出了,就是没有任何地方写它 ——
|
|
413
|
+
// json 那行的 warnings 恒为空数组。)
|
|
414
|
+
(result.warnings ??= []).push(event.message);
|
|
402
415
|
return {};
|
|
403
416
|
case 'turn_done':
|
|
404
417
|
// text 模式:非 verbose 时这里一次性把累积答案打到 stdout(旧行为,逐字节不变)。
|
|
405
418
|
// json 模式:只记 stopReason='end_turn',stdout 保持静默。
|
|
406
419
|
result.stopReason = 'end_turn';
|
|
420
|
+
// 内核的交付判定必须过进程边界:模型谎称「已验证」时,若不记这一笔,
|
|
421
|
+
// 宿主收到的 JSON 与一切正常逐字节相同,唯一线索只剩 stderr 上一行中文。
|
|
422
|
+
if (event.completionStatus)
|
|
423
|
+
result.deliveryStatus = event.completionStatus;
|
|
407
424
|
if (!isJson && !verbose)
|
|
408
425
|
console.log(result.buffer);
|
|
409
426
|
return {};
|
package/src/cli/print.ts
CHANGED
|
@@ -98,6 +98,15 @@ export interface PrintResult {
|
|
|
98
98
|
deliverables?: DeliverableManifest['files'];
|
|
99
99
|
warnings?: string[];
|
|
100
100
|
harnessDiagnostics?: HarnessDiagnostics;
|
|
101
|
+
/**
|
|
102
|
+
* 内核 `turn_done` 给出的交付判定(loop.ts 的四态词表)。
|
|
103
|
+
*
|
|
104
|
+
* 与下面 json 里那个从 stopReason+deliverables 重算的 `completionStatus` **不是**
|
|
105
|
+
* 一回事:那个只回答「跑完了没、产物验没验」,这个回答「模型宣称的事到底做没做」。
|
|
106
|
+
* 尤其 `claims_unverified` —— 模型说「测试已全部通过」但本轮证据台账里没有一条
|
|
107
|
+
* 成功的验证记录 —— 是重不重要全看它能不能过进程边界。
|
|
108
|
+
*/
|
|
109
|
+
deliveryStatus?: string;
|
|
101
110
|
}
|
|
102
111
|
|
|
103
112
|
/** handleEvent 返回的结构 */
|
|
@@ -191,8 +200,16 @@ export function extractImageOptions(args: string[]): {
|
|
|
191
200
|
* stop_reason = end_turn | max_turns | stalled | error | interrupted | config_error
|
|
192
201
|
* num_turns = assistant_message 事件计数
|
|
193
202
|
* error = 失败文案(成功时 null)
|
|
203
|
+
* warnings = 本轮所有非致命警告(与 stderr 同源;宿主要结构化的那一份)
|
|
194
204
|
* session_file(可选)= 当前对话落盘路径;拿得到才填,拿不到省略该键
|
|
195
205
|
*
|
|
206
|
+
* completionStatus = 本层从 stop_reason + deliverables 重算:跑完了没、产物验没验
|
|
207
|
+
* delivery_status(可选)= 内核 turn_done 的交付判定,**回答的是另一个问题**:
|
|
208
|
+
* completed / delivery_incomplete / delivery_unverified / claims_unverified。
|
|
209
|
+
* 两者并列而非替代 —— `ok:true` 配 `delivery_status:'claims_unverified'` 是
|
|
210
|
+
* 完全合法的一行:Agent 确实正常收尾了,但它宣称的「测试已通过」没有证据支撑。
|
|
211
|
+
* 判断「任务是否真做成」的宿主应当读它,而不是只看 ok。
|
|
212
|
+
*
|
|
196
213
|
* 导出供单测断言。
|
|
197
214
|
*/
|
|
198
215
|
export function emitJsonResult(result: PrintResult, sessionFile?: string): void {
|
|
@@ -220,6 +237,7 @@ export function emitJsonResult(result: PrintResult, sessionFile?: string): void
|
|
|
220
237
|
num_turns: result.numTurns,
|
|
221
238
|
error: result.error,
|
|
222
239
|
completionStatus,
|
|
240
|
+
...(result.deliveryStatus ? { delivery_status: result.deliveryStatus } : {}),
|
|
223
241
|
deliverables,
|
|
224
242
|
verification: allVerified ? 'passed' : deliverables.length > 0 ? 'not_run' : 'none',
|
|
225
243
|
warnings,
|
|
@@ -509,12 +527,19 @@ export function handleEvent(
|
|
|
509
527
|
// 通用非致命警告(如 workflow 多余参数、pause 被自动跳过等)始终走 stderr,
|
|
510
528
|
// 不受 verbose 控制 —— 这是用户必须看到的信号,避免静默丢弃语义被忽略。
|
|
511
529
|
process.stderr.write(`${event.message}\n`);
|
|
530
|
+
// 同时收进结局契约:stderr 是给人看的,spawn 它的宿主要的是结构化的一条。
|
|
531
|
+
// (此前 PrintResult.warnings 声明了、读了、也输出了,就是没有任何地方写它 ——
|
|
532
|
+
// json 那行的 warnings 恒为空数组。)
|
|
533
|
+
(result.warnings ??= []).push(event.message);
|
|
512
534
|
return {};
|
|
513
535
|
|
|
514
536
|
case 'turn_done':
|
|
515
537
|
// text 模式:非 verbose 时这里一次性把累积答案打到 stdout(旧行为,逐字节不变)。
|
|
516
538
|
// json 模式:只记 stopReason='end_turn',stdout 保持静默。
|
|
517
539
|
result.stopReason = 'end_turn';
|
|
540
|
+
// 内核的交付判定必须过进程边界:模型谎称「已验证」时,若不记这一笔,
|
|
541
|
+
// 宿主收到的 JSON 与一切正常逐字节相同,唯一线索只剩 stderr 上一行中文。
|
|
542
|
+
if (event.completionStatus) result.deliveryStatus = event.completionStatus;
|
|
518
543
|
if (!isJson && !verbose) console.log(result.buffer);
|
|
519
544
|
return {};
|
|
520
545
|
|
|
@@ -373,6 +373,30 @@ export class ConfigService {
|
|
|
373
373
|
return null;
|
|
374
374
|
return { ...advisor, provider: resolveProvider(entry), supportsVision: entry.supportsVision !== false };
|
|
375
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* 同步版 advisor 可用性探测——仅用于 resolveAvailability 这类工具列表探测场景。
|
|
378
|
+
* 读当前内存快照(可能略滞后于磁盘最新值),但调用频率高、对实时性要求不高,
|
|
379
|
+
* 下一轮 LLM 请求前 configService.reload() 会刷新。
|
|
380
|
+
*
|
|
381
|
+
* 为什么不用 advisor() 的 fresh reload:
|
|
382
|
+
* resolveAvailability 是在主循环构造工具集时同步调用的,不能 await;
|
|
383
|
+
* 而且每次工具集构造都读盘的话开销太大(主循环每轮都可能构造一次)。
|
|
384
|
+
*/
|
|
385
|
+
advisorAvailabilitySync() {
|
|
386
|
+
const config = this.snapshot.config;
|
|
387
|
+
const advisor = config?.advisor;
|
|
388
|
+
if (!advisor?.modelId)
|
|
389
|
+
return { enabled: false, mode: 'auto', modelId: null, configured: false };
|
|
390
|
+
const entry = config?.models?.find((m) => m.id === advisor.modelId);
|
|
391
|
+
if (!entry?.apiKey)
|
|
392
|
+
return { enabled: false, mode: 'auto', modelId: null, configured: false };
|
|
393
|
+
return {
|
|
394
|
+
enabled: advisor.enabled !== false,
|
|
395
|
+
mode: advisor.mode === 'manual' ? 'manual' : 'auto',
|
|
396
|
+
modelId: advisor.modelId,
|
|
397
|
+
configured: true,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
376
400
|
async subagents() {
|
|
377
401
|
return (await this.reload()).config?.subagents ?? DEFAULT_SUBAGENTS_CONFIG;
|
|
378
402
|
}
|
|
@@ -431,6 +431,29 @@ export class ConfigService {
|
|
|
431
431
|
return { ...advisor, provider: resolveProvider(entry), supportsVision: entry.supportsVision !== false };
|
|
432
432
|
}
|
|
433
433
|
|
|
434
|
+
/**
|
|
435
|
+
* 同步版 advisor 可用性探测——仅用于 resolveAvailability 这类工具列表探测场景。
|
|
436
|
+
* 读当前内存快照(可能略滞后于磁盘最新值),但调用频率高、对实时性要求不高,
|
|
437
|
+
* 下一轮 LLM 请求前 configService.reload() 会刷新。
|
|
438
|
+
*
|
|
439
|
+
* 为什么不用 advisor() 的 fresh reload:
|
|
440
|
+
* resolveAvailability 是在主循环构造工具集时同步调用的,不能 await;
|
|
441
|
+
* 而且每次工具集构造都读盘的话开销太大(主循环每轮都可能构造一次)。
|
|
442
|
+
*/
|
|
443
|
+
advisorAvailabilitySync(): { enabled: boolean; mode: 'auto' | 'manual'; modelId: string | null; configured: boolean } {
|
|
444
|
+
const config = this.snapshot.config;
|
|
445
|
+
const advisor = config?.advisor;
|
|
446
|
+
if (!advisor?.modelId) return { enabled: false, mode: 'auto', modelId: null, configured: false };
|
|
447
|
+
const entry = config?.models?.find((m) => m.id === advisor.modelId);
|
|
448
|
+
if (!entry?.apiKey) return { enabled: false, mode: 'auto', modelId: null, configured: false };
|
|
449
|
+
return {
|
|
450
|
+
enabled: advisor.enabled !== false,
|
|
451
|
+
mode: advisor.mode === 'manual' ? 'manual' : 'auto',
|
|
452
|
+
modelId: advisor.modelId,
|
|
453
|
+
configured: true,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
434
457
|
async subagents(): Promise<SubagentsConfig> {
|
|
435
458
|
return (await this.reload()).config?.subagents ?? DEFAULT_SUBAGENTS_CONFIG;
|
|
436
459
|
}
|
|
@@ -16,7 +16,13 @@
|
|
|
16
16
|
* 设计原则:
|
|
17
17
|
* - 纯工具函数,无全局状态,无副作用(除了 shell 子进程)
|
|
18
18
|
* - 不依赖任何插件代码,可被 src/ 和 plugins/ 同时导入
|
|
19
|
-
* -
|
|
19
|
+
* - 工作目录隔离:相对路径以调用方给的 cwd 为锚,缺省 getWorkingDir()
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ cwd 参数不是可有可无的修饰:**验收必须和写入锚在同一个目录**。
|
|
22
|
+
* 会话进入 session worktree 后(EnterWorktree),写文件的工具跟着 worktree 走
|
|
23
|
+
* (loop.ts 按 sessionState.activeWorktree 重定向 cwd),验收若仍锚原 workspace,
|
|
24
|
+
* 就会在空目录里找产物 —— file_exists 恒 false,工作流被判失败并反复重试,
|
|
25
|
+
* 而产物其实好好躺在 worktree 里。调用方拿得到有效 cwd 时务必传进来。
|
|
20
26
|
* ============================================================
|
|
21
27
|
*/
|
|
22
28
|
import { existsSync, readFileSync } from 'node:fs';
|
|
@@ -92,12 +98,13 @@ function isAbsolutePath(file) {
|
|
|
92
98
|
/^[A-Za-z]:[\\/]/.test(file) // C:\ 或 C:/
|
|
93
99
|
);
|
|
94
100
|
}
|
|
95
|
-
function runShell(command, timeout) {
|
|
101
|
+
function runShell(command, timeout, cwd) {
|
|
96
102
|
return new Promise((resolve) => {
|
|
97
103
|
const isWin = process.platform === 'win32';
|
|
104
|
+
const opts = { timeout, cwd, env: sanitizeCredentialEnv() };
|
|
98
105
|
const child = isWin
|
|
99
|
-
? spawn('cmd', ['/c', command],
|
|
100
|
-
: spawn('bash', ['-c', command],
|
|
106
|
+
? spawn('cmd', ['/c', command], opts)
|
|
107
|
+
: spawn('bash', ['-c', command], opts);
|
|
101
108
|
let stdout = '';
|
|
102
109
|
let stderr = '';
|
|
103
110
|
child.stdout.on('data', (d) => {
|
|
@@ -114,8 +121,8 @@ function runShell(command, timeout) {
|
|
|
114
121
|
});
|
|
115
122
|
});
|
|
116
123
|
}
|
|
117
|
-
export async function verifyShell(command, timeout) {
|
|
118
|
-
const r = await runShell(command, timeout);
|
|
124
|
+
export async function verifyShell(command, timeout, cwd = getWorkingDir()) {
|
|
125
|
+
const r = await runShell(command, timeout, cwd);
|
|
119
126
|
if (r.errored) {
|
|
120
127
|
return {
|
|
121
128
|
check: { type: 'shell', command },
|
|
@@ -129,9 +136,9 @@ export async function verifyShell(command, timeout) {
|
|
|
129
136
|
output: r.stdout.trim() || r.stderr.trim() || `exit code ${r.exitCode}`,
|
|
130
137
|
};
|
|
131
138
|
}
|
|
132
|
-
export function verifyFileExists(file) {
|
|
133
|
-
//
|
|
134
|
-
const resolved = isAbsolutePath(file) ? file : `${
|
|
139
|
+
export function verifyFileExists(file, cwd = getWorkingDir()) {
|
|
140
|
+
// 相对路径以调用方给的 cwd 为锚(绝对路径识别含 UNC)
|
|
141
|
+
const resolved = isAbsolutePath(file) ? file : `${cwd}/${file}`;
|
|
135
142
|
const exists = existsSync(resolved);
|
|
136
143
|
return {
|
|
137
144
|
check: { type: 'file_exists', file },
|
|
@@ -139,8 +146,8 @@ export function verifyFileExists(file) {
|
|
|
139
146
|
output: exists ? '文件存在' : `文件不存在: ${resolved}`,
|
|
140
147
|
};
|
|
141
148
|
}
|
|
142
|
-
export function verifyFileContains(file, pattern) {
|
|
143
|
-
const resolved = isAbsolutePath(file) ? file : `${
|
|
149
|
+
export function verifyFileContains(file, pattern, cwd = getWorkingDir()) {
|
|
150
|
+
const resolved = isAbsolutePath(file) ? file : `${cwd}/${file}`;
|
|
144
151
|
try {
|
|
145
152
|
const content = readFileSync(resolved, 'utf8');
|
|
146
153
|
const regex = new RegExp(pattern);
|
|
@@ -159,11 +166,11 @@ export function verifyFileContains(file, pattern) {
|
|
|
159
166
|
};
|
|
160
167
|
}
|
|
161
168
|
}
|
|
162
|
-
async function verifyTestCount(minCount) {
|
|
169
|
+
async function verifyTestCount(minCount, cwd = getWorkingDir()) {
|
|
163
170
|
// 跨运行时:开发用 bun → bun test;npm 全局安装用 Node → npm test --silent
|
|
164
171
|
// 用 process.versions.bun 探测比 typeof Bun !== 'undefined' 更稳(不依赖全局类型)
|
|
165
172
|
const cmd = process.versions.bun ? 'bun test' : 'npm test --silent';
|
|
166
|
-
const r = await runShell(cmd, 60_000);
|
|
173
|
+
const r = await runShell(cmd, 60_000, cwd);
|
|
167
174
|
const combined = `${r.stdout}\n${r.stderr}`;
|
|
168
175
|
if (r.errored) {
|
|
169
176
|
return {
|
|
@@ -185,7 +192,7 @@ async function verifyTestCount(minCount) {
|
|
|
185
192
|
* 执行一组检查,返回聚合结果。
|
|
186
193
|
* 全部通过 → passed=true;任一失败 → passed=false + summary 列出失败项。
|
|
187
194
|
*/
|
|
188
|
-
export async function runVerification(checks) {
|
|
195
|
+
export async function runVerification(checks, cwd = getWorkingDir()) {
|
|
189
196
|
if (checks.length === 0) {
|
|
190
197
|
return { passed: true, details: [], summary: '无验证项' };
|
|
191
198
|
}
|
|
@@ -194,16 +201,16 @@ export async function runVerification(checks) {
|
|
|
194
201
|
let result;
|
|
195
202
|
switch (check.type) {
|
|
196
203
|
case 'shell':
|
|
197
|
-
result = await verifyShell(check.command ?? '', check.timeout ?? 30_000);
|
|
204
|
+
result = await verifyShell(check.command ?? '', check.timeout ?? 30_000, cwd);
|
|
198
205
|
break;
|
|
199
206
|
case 'file_exists':
|
|
200
|
-
result = verifyFileExists(check.file ?? '');
|
|
207
|
+
result = verifyFileExists(check.file ?? '', cwd);
|
|
201
208
|
break;
|
|
202
209
|
case 'file_contains':
|
|
203
|
-
result = verifyFileContains(check.file ?? '', check.pattern ?? '');
|
|
210
|
+
result = verifyFileContains(check.file ?? '', check.pattern ?? '', cwd);
|
|
204
211
|
break;
|
|
205
212
|
case 'test_count':
|
|
206
|
-
result = await verifyTestCount(check.minCount ?? 0);
|
|
213
|
+
result = await verifyTestCount(check.minCount ?? 0, cwd);
|
|
207
214
|
break;
|
|
208
215
|
default:
|
|
209
216
|
result = {
|
|
@@ -16,7 +16,13 @@
|
|
|
16
16
|
* 设计原则:
|
|
17
17
|
* - 纯工具函数,无全局状态,无副作用(除了 shell 子进程)
|
|
18
18
|
* - 不依赖任何插件代码,可被 src/ 和 plugins/ 同时导入
|
|
19
|
-
* -
|
|
19
|
+
* - 工作目录隔离:相对路径以调用方给的 cwd 为锚,缺省 getWorkingDir()
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ cwd 参数不是可有可无的修饰:**验收必须和写入锚在同一个目录**。
|
|
22
|
+
* 会话进入 session worktree 后(EnterWorktree),写文件的工具跟着 worktree 走
|
|
23
|
+
* (loop.ts 按 sessionState.activeWorktree 重定向 cwd),验收若仍锚原 workspace,
|
|
24
|
+
* 就会在空目录里找产物 —— file_exists 恒 false,工作流被判失败并反复重试,
|
|
25
|
+
* 而产物其实好好躺在 worktree 里。调用方拿得到有效 cwd 时务必传进来。
|
|
20
26
|
* ============================================================
|
|
21
27
|
*/
|
|
22
28
|
|
|
@@ -128,12 +134,17 @@ interface ShellRunResult {
|
|
|
128
134
|
errored: boolean;
|
|
129
135
|
}
|
|
130
136
|
|
|
131
|
-
function runShell(
|
|
137
|
+
function runShell(
|
|
138
|
+
command: string,
|
|
139
|
+
timeout: number,
|
|
140
|
+
cwd: string,
|
|
141
|
+
): Promise<ShellRunResult> {
|
|
132
142
|
return new Promise((resolve) => {
|
|
133
143
|
const isWin = process.platform === 'win32';
|
|
144
|
+
const opts = { timeout, cwd, env: sanitizeCredentialEnv() };
|
|
134
145
|
const child = isWin
|
|
135
|
-
? spawn('cmd', ['/c', command],
|
|
136
|
-
: spawn('bash', ['-c', command],
|
|
146
|
+
? spawn('cmd', ['/c', command], opts)
|
|
147
|
+
: spawn('bash', ['-c', command], opts);
|
|
137
148
|
|
|
138
149
|
let stdout = '';
|
|
139
150
|
let stderr = '';
|
|
@@ -155,8 +166,12 @@ function runShell(command: string, timeout: number): Promise<ShellRunResult> {
|
|
|
155
166
|
});
|
|
156
167
|
}
|
|
157
168
|
|
|
158
|
-
export async function verifyShell(
|
|
159
|
-
|
|
169
|
+
export async function verifyShell(
|
|
170
|
+
command: string,
|
|
171
|
+
timeout: number,
|
|
172
|
+
cwd: string = getWorkingDir(),
|
|
173
|
+
): Promise<CheckResult> {
|
|
174
|
+
const r = await runShell(command, timeout, cwd);
|
|
160
175
|
if (r.errored) {
|
|
161
176
|
return {
|
|
162
177
|
check: { type: 'shell', command },
|
|
@@ -171,9 +186,12 @@ export async function verifyShell(command: string, timeout: number): Promise<Che
|
|
|
171
186
|
};
|
|
172
187
|
}
|
|
173
188
|
|
|
174
|
-
export function verifyFileExists(
|
|
175
|
-
|
|
176
|
-
|
|
189
|
+
export function verifyFileExists(
|
|
190
|
+
file: string,
|
|
191
|
+
cwd: string = getWorkingDir(),
|
|
192
|
+
): CheckResult {
|
|
193
|
+
// 相对路径以调用方给的 cwd 为锚(绝对路径识别含 UNC)
|
|
194
|
+
const resolved = isAbsolutePath(file) ? file : `${cwd}/${file}`;
|
|
177
195
|
const exists = existsSync(resolved);
|
|
178
196
|
return {
|
|
179
197
|
check: { type: 'file_exists', file },
|
|
@@ -182,8 +200,12 @@ export function verifyFileExists(file: string): CheckResult {
|
|
|
182
200
|
};
|
|
183
201
|
}
|
|
184
202
|
|
|
185
|
-
export function verifyFileContains(
|
|
186
|
-
|
|
203
|
+
export function verifyFileContains(
|
|
204
|
+
file: string,
|
|
205
|
+
pattern: string,
|
|
206
|
+
cwd: string = getWorkingDir(),
|
|
207
|
+
): CheckResult {
|
|
208
|
+
const resolved = isAbsolutePath(file) ? file : `${cwd}/${file}`;
|
|
187
209
|
try {
|
|
188
210
|
const content = readFileSync(resolved, 'utf8');
|
|
189
211
|
const regex = new RegExp(pattern);
|
|
@@ -203,11 +225,14 @@ export function verifyFileContains(file: string, pattern: string): CheckResult {
|
|
|
203
225
|
}
|
|
204
226
|
}
|
|
205
227
|
|
|
206
|
-
async function verifyTestCount(
|
|
228
|
+
async function verifyTestCount(
|
|
229
|
+
minCount: number,
|
|
230
|
+
cwd: string = getWorkingDir(),
|
|
231
|
+
): Promise<CheckResult> {
|
|
207
232
|
// 跨运行时:开发用 bun → bun test;npm 全局安装用 Node → npm test --silent
|
|
208
233
|
// 用 process.versions.bun 探测比 typeof Bun !== 'undefined' 更稳(不依赖全局类型)
|
|
209
234
|
const cmd = process.versions.bun ? 'bun test' : 'npm test --silent';
|
|
210
|
-
const r = await runShell(cmd, 60_000);
|
|
235
|
+
const r = await runShell(cmd, 60_000, cwd);
|
|
211
236
|
const combined = `${r.stdout}\n${r.stderr}`;
|
|
212
237
|
if (r.errored) {
|
|
213
238
|
return {
|
|
@@ -232,7 +257,10 @@ async function verifyTestCount(minCount: number): Promise<CheckResult> {
|
|
|
232
257
|
* 执行一组检查,返回聚合结果。
|
|
233
258
|
* 全部通过 → passed=true;任一失败 → passed=false + summary 列出失败项。
|
|
234
259
|
*/
|
|
235
|
-
export async function runVerification(
|
|
260
|
+
export async function runVerification(
|
|
261
|
+
checks: Check[],
|
|
262
|
+
cwd: string = getWorkingDir(),
|
|
263
|
+
): Promise<VerifyResult> {
|
|
236
264
|
if (checks.length === 0) {
|
|
237
265
|
return { passed: true, details: [], summary: '无验证项' };
|
|
238
266
|
}
|
|
@@ -244,16 +272,20 @@ export async function runVerification(checks: Check[]): Promise<VerifyResult> {
|
|
|
244
272
|
|
|
245
273
|
switch (check.type) {
|
|
246
274
|
case 'shell':
|
|
247
|
-
result = await verifyShell(
|
|
275
|
+
result = await verifyShell(
|
|
276
|
+
check.command ?? '',
|
|
277
|
+
check.timeout ?? 30_000,
|
|
278
|
+
cwd,
|
|
279
|
+
);
|
|
248
280
|
break;
|
|
249
281
|
case 'file_exists':
|
|
250
|
-
result = verifyFileExists(check.file ?? '');
|
|
282
|
+
result = verifyFileExists(check.file ?? '', cwd);
|
|
251
283
|
break;
|
|
252
284
|
case 'file_contains':
|
|
253
|
-
result = verifyFileContains(check.file ?? '', check.pattern ?? '');
|
|
285
|
+
result = verifyFileContains(check.file ?? '', check.pattern ?? '', cwd);
|
|
254
286
|
break;
|
|
255
287
|
case 'test_count':
|
|
256
|
-
result = await verifyTestCount(check.minCount ?? 0);
|
|
288
|
+
result = await verifyTestCount(check.minCount ?? 0, cwd);
|
|
257
289
|
break;
|
|
258
290
|
default:
|
|
259
291
|
result = {
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
* 2) / 输入 → discoverPlugins → resolveCommand
|
|
10
10
|
* a) 命中插件且其 plugin.ts 暴露 runCommand → 让 plugin.ts 接管
|
|
11
11
|
* b) 命中插件但只有声明式 commands/*.md → 把 prompt 拼好喂给 runQuery
|
|
12
|
-
* c) 未命中
|
|
13
|
-
*
|
|
12
|
+
* c) 未命中 -> warning 提示未知命令后透传 runQuery(/ 只是激活前缀,
|
|
13
|
+
* 不是严格命令系统;未知的 /xxx 当普通用户输入处理,不报错)
|
|
14
14
|
* ============================================================
|
|
15
15
|
*/
|
|
16
16
|
import { runQuery } from '../loop.js';
|
|
17
|
-
import { buildCommandInput, discoverPlugins, resolveCommand, } from './commandRouter.js';
|
|
17
|
+
import { buildCommandInput, discoverPlugins, listAvailableCommands, resolveCommand, } from './commandRouter.js';
|
|
18
18
|
import { loadPluginApi } from './pluginLoader.js';
|
|
19
19
|
export async function* runWithPlugins(userInput, options) {
|
|
20
20
|
const { provider, history, signal } = options;
|
|
@@ -56,10 +56,21 @@ export async function* runWithPlugins(userInput, options) {
|
|
|
56
56
|
await discoverPlugins();
|
|
57
57
|
const matched = resolveCommand(userInput);
|
|
58
58
|
if (!matched) {
|
|
59
|
-
// 未命中任何插件命令
|
|
60
|
-
// / 只是激活前缀(类似 Claude Code 的 /
|
|
61
|
-
//
|
|
62
|
-
//
|
|
59
|
+
// 未命中任何插件命令 -> 提示后透传 runQuery。
|
|
60
|
+
// / 只是激活前缀(类似 Claude Code 的 / 模式),不是严格命令系统:
|
|
61
|
+
// 不匹配仍当普通对话处理,让 LLM 自行理解意图。但 -p 等无人值守场景里
|
|
62
|
+
// 拼错的 /reseach 会被静默当文本烧一轮 LLM(实战:用户以为在跑工作流,
|
|
63
|
+
// 实际什么都没发生)--先 warning 列出已注册命令,零成本纠正拼写。
|
|
64
|
+
const trimmedUnknown = userInput.trimStart();
|
|
65
|
+
const spaceUnknown = trimmedUnknown.indexOf(' ', 1);
|
|
66
|
+
const unknownCmd = (spaceUnknown >= 0 ? trimmedUnknown.slice(1, spaceUnknown) : trimmedUnknown.slice(1)).trim();
|
|
67
|
+
if (unknownCmd) {
|
|
68
|
+
const available = listAvailableCommands().map((c) => c.command);
|
|
69
|
+
yield {
|
|
70
|
+
type: 'warning',
|
|
71
|
+
message: `未知命令 /${unknownCmd}(已注册:${available.length ? available.join('、') : '无'}),已按普通对话处理。`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
63
74
|
yield* runQuery(userInput, {
|
|
64
75
|
provider,
|
|
65
76
|
history,
|