agency-orchestrator 0.4.2 → 0.5.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/README.md CHANGED
@@ -515,11 +515,14 @@ ao-output/产品需求评审-2026-03-22/
515
515
  Code/Copilot...) Cursor 直接调用)
516
516
  ```
517
517
 
518
- ## 社区交流
518
+ ## 交流 · Community
519
519
 
520
- | 群名 | 群号 | 加入方式 |
521
- |------|------|---------|
522
- | AI 编程 & Agent 中文实践群 | **1071280067** | [点击加入](https://qm.qq.com/q/EeNQA9xCxy) |
520
+ 微信公众号 **「AI不止语」**(微信搜索 `AI_BuZhiYu`)— 技术问答 · 项目更新 · 实战文章
521
+
522
+ | 渠道 | 加入方式 |
523
+ |------|---------|
524
+ | QQ 群 | [点击加入](https://qm.qq.com/q/EeNQA9xCxy)(群号 1071280067) |
525
+ | 微信群 | 关注公众号后回复「群」获取入群方式 |
523
526
 
524
527
  ## 姊妹项目
525
528
 
@@ -18,7 +18,15 @@ import { join, resolve } from 'node:path';
18
18
  * @param rolePath 角色路径,如 "engineering/engineering-sre"
19
19
  */
20
20
  export function loadAgent(agentsDir, rolePath) {
21
+ // 防止路径穿越攻击(如 ../../etc/passwd)
22
+ if (/\.\.[/\\]/.test(rolePath) || /[^a-zA-Z0-9_\-/]/.test(rolePath)) {
23
+ throw new Error(`非法角色路径: ${rolePath}\n角色路径只能包含字母、数字、下划线、连字符和斜杠`);
24
+ }
21
25
  const fullPath = resolve(agentsDir, `${rolePath}.md`);
26
+ const resolvedDir = resolve(agentsDir);
27
+ if (!fullPath.startsWith(resolvedDir)) {
28
+ throw new Error(`角色路径越界: ${rolePath}`);
29
+ }
22
30
  if (!existsSync(fullPath)) {
23
31
  throw new Error(`角色文件不存在: ${fullPath}\n请确认 agents_dir 和 role 路径正确`);
24
32
  }
@@ -3,6 +3,7 @@ import type { LLMConfig } from '../types.js';
3
3
  export interface RoleSummary {
4
4
  path: string;
5
5
  name: string;
6
+ emoji?: string;
6
7
  description: string;
7
8
  category: string;
8
9
  }
@@ -16,8 +17,14 @@ export declare function buildRoleCatalog(agentsDir: string): RoleSummary[];
16
17
  export declare function formatCatalogForPrompt(roles: RoleSummary[]): string;
17
18
  /**
18
19
  * 构建发给 LLM 的 system prompt
20
+ * @param catalog 角色目录文本
21
+ * @param options.autoRun 如果 true,生成的 YAML 不需要 inputs,用户描述直接嵌入 task
19
22
  */
20
- export declare function buildComposeSystemPrompt(catalog: string): string;
23
+ export declare function buildComposeSystemPrompt(catalog: string, options?: {
24
+ autoRun?: boolean;
25
+ provider?: string;
26
+ model?: string;
27
+ }): string;
21
28
  /**
22
29
  * 构建 user prompt
23
30
  */
@@ -37,6 +44,9 @@ export declare function composeWorkflow(options: {
37
44
  description: string;
38
45
  agentsDir: string;
39
46
  llmConfig: LLMConfig;
47
+ outputName?: string;
48
+ /** 直接运行模式:生成的 YAML 不需要 inputs */
49
+ autoRun?: boolean;
40
50
  }): Promise<{
41
51
  yaml: string;
42
52
  savedPath: string;
@@ -17,6 +17,7 @@ export function buildRoleCatalog(agentsDir) {
17
17
  .map(a => ({
18
18
  path: a.rolePath,
19
19
  name: a.name,
20
+ emoji: a.emoji,
20
21
  description: a.description || '',
21
22
  category: a.rolePath.split('/')[0],
22
23
  }));
@@ -35,7 +36,7 @@ export function formatCatalogForPrompt(roles) {
35
36
  for (const [cat, list] of byCategory) {
36
37
  lines.push(`## ${cat}`);
37
38
  for (const r of list) {
38
- lines.push(`- ${r.path} | ${r.name} | ${r.description}`);
39
+ lines.push(`- ${r.path} | ${r.emoji || ''} ${r.name} | ${r.description}`);
39
40
  }
40
41
  lines.push('');
41
42
  }
@@ -43,16 +44,43 @@ export function formatCatalogForPrompt(roles) {
43
44
  }
44
45
  /**
45
46
  * 构建发给 LLM 的 system prompt
47
+ * @param catalog 角色目录文本
48
+ * @param options.autoRun 如果 true,生成的 YAML 不需要 inputs,用户描述直接嵌入 task
46
49
  */
47
- export function buildComposeSystemPrompt(catalog) {
50
+ export function buildComposeSystemPrompt(catalog, options) {
51
+ const autoRun = options?.autoRun ?? false;
52
+ const provider = options?.provider || 'deepseek';
53
+ const model = options?.model;
54
+ const inputsSection = autoRun
55
+ ? `
56
+ ## 重要:直接运行模式
57
+
58
+ 这个工作流生成后会立即执行,所以:
59
+ - **不要**生成 inputs 段
60
+ - 把用户描述中的所有具体信息直接写进每个 step 的 task 里
61
+ - 确保工作流无需任何外部输入就能直接运行`
62
+ : `
63
+ inputs:
64
+ - name: variable_name
65
+ description: "变量描述"
66
+ required: true`;
67
+ const inputsYamlExample = autoRun ? '' : `
68
+ inputs:
69
+ - name: variable_name
70
+ description: "变量描述"
71
+ required: true
72
+ `;
73
+ const inputsDesignPrinciple = autoRun
74
+ ? '- **自包含**:所有信息直接写在 task 中,不要使用 inputs,工作流必须能直接运行'
75
+ : '- **合理输入**:提取用户需求中的关键变量作为 inputs';
48
76
  return `你是一个 AI 工作流编排专家。用户会用一句话描述他想要的工作流,你需要:
49
77
 
50
78
  1. 从下方角色目录中选择最合适的角色(通常 2-6 个)
51
79
  2. 设计合理的 DAG 依赖关系(哪些步骤可以并行,哪些必须串行)
52
80
  3. 为每个步骤编写详细的 task 描述
53
- 4. 设计合理的输入变量
81
+ ${autoRun ? '4. 把用户描述中的具体信息直接写进 task,不要生成 inputs' : '4. 设计合理的输入变量'}
54
82
  5. 生成完整的 workflow YAML
55
-
83
+ ${autoRun ? inputsSection : ''}
56
84
  ## 输出格式
57
85
 
58
86
  直接输出一个完整的 YAML 代码块,格式如下:
@@ -64,23 +92,20 @@ description: "一句话描述"
64
92
  agents_dir: "agency-agents-zh"
65
93
 
66
94
  llm:
67
- provider: deepseek
68
- model: deepseek-chat
95
+ provider: ${provider}
96
+ ${model ? `model: ${model}` : ''}
69
97
  max_tokens: 4096
70
98
 
71
99
  concurrency: 2
72
-
73
- inputs:
74
- - name: variable_name
75
- description: "变量描述"
76
- required: true
77
-
100
+ ${inputsYamlExample}
78
101
  steps:
79
102
  - id: step_id
80
103
  role: "category/role-name"
104
+ name: "通俗易懂的角色名(如:老板、产品经理、技术总监)"
105
+ emoji: "👔"
81
106
  task: |
82
107
  详细的任务描述...
83
- 使用 {{variable_name}} 引用输入变量
108
+ ${autoRun ? ' 直接包含用户需求中的具体信息' : ' 使用 {{variable_name}} 引用输入变量'}
84
109
  使用 {{previous_output}} 引用上游步骤的输出
85
110
  output: output_variable_name
86
111
  depends_on: [upstream_step_id] # 仅在有依赖时添加
@@ -91,9 +116,10 @@ steps:
91
116
  - **并行优先**:没有数据依赖的步骤应该并行执行(不加 depends_on)
92
117
  - **变量串联**:上游步骤的 output 变量名要和下游步骤 task 中的 {{变量}} 对应
93
118
  - **角色匹配**:选择最专业的角色,不要用一个角色做所有事
119
+ - **角色命名**:每个步骤必须设置 name(通俗的公司职位名如"老板""产品经理""技术总监")和 emoji,让小白也能一眼看懂谁在说话
94
120
  - **任务详细**:task 描述要具体,告诉角色要做什么、输出什么格式
95
- - **合理输入**:提取用户需求中的关键变量作为 inputs
96
- - **最终汇总**:如果有多路并行,最后应该有一个汇总步骤
121
+ ${inputsDesignPrinciple}
122
+ - **最终成品**:最后一个步骤必须输出用户想要的最终成品(如完整文章、完整报告),而不是审查意见或修改建议。如果有审校步骤,审校步骤应该直接输出修改后的定稿,而不是"修改建议列表"
97
123
 
98
124
  ## 可用角色目录
99
125
 
@@ -103,7 +129,9 @@ ${catalog}
103
129
 
104
130
  - role 的值必须严格使用角色目录中的 path(如 "engineering/engineering-code-reviewer"),不要自己编造
105
131
  - 只输出 YAML 代码块,不要输出其他内容
106
- - concurrency 设为并行步骤的最大数量`;
132
+ - concurrency 设为并行步骤的最大数量
133
+ - **重要:拆分大任务**。写长文章时,不要让一个步骤生成超过 800 字的内容。应该按章节拆分成多个并行步骤(如 write_ch1、write_ch2、write_ch3),最后用一个合并步骤重写为连贯的完整文章
134
+ - 每个写作步骤的 task 中要限定输出字数(如"500字以内"),避免单步骤生成时间过长`;
107
135
  }
108
136
  /**
109
137
  * 构建 user prompt
@@ -163,7 +191,11 @@ export async function composeWorkflow(options) {
163
191
  }
164
192
  const catalog = formatCatalogForPrompt(roles);
165
193
  // 2. 构建 prompt
166
- const systemPrompt = buildComposeSystemPrompt(catalog);
194
+ const systemPrompt = buildComposeSystemPrompt(catalog, {
195
+ autoRun: options.autoRun,
196
+ provider: options.llmConfig.provider,
197
+ model: options.llmConfig.model,
198
+ });
167
199
  const userPrompt = buildComposeUserPrompt(description);
168
200
  // 3. 调用 LLM
169
201
  console.log(` 正在用 AI 编排工作流...(${roles.length} 个角色可选)\n`);
@@ -182,7 +214,9 @@ export async function composeWorkflow(options) {
182
214
  if (!existsSync(workflowsDir)) {
183
215
  mkdirSync(workflowsDir, { recursive: true });
184
216
  }
185
- const fileName = generateFileName(description, workflowsDir);
217
+ const fileName = options.outputName
218
+ ? (options.outputName.endsWith('.yaml') ? options.outputName : `${options.outputName}.yaml`)
219
+ : generateFileName(description, workflowsDir);
186
220
  const savedPath = resolve(workflowsDir, fileName);
187
221
  writeFileSync(savedPath, yaml + '\n', 'utf-8');
188
222
  const relativePath = relative(process.cwd(), savedPath);
package/dist/cli.js CHANGED
@@ -81,6 +81,8 @@ async function handleRun() {
81
81
  const watch = args.includes('--watch');
82
82
  let resumeDir = getArgValue('--resume');
83
83
  const fromStep = getArgValue('--from');
84
+ const provider = getArgValue('--provider');
85
+ const model = getArgValue('--model');
84
86
  // --resume last: 自动找最近一次的输出目录
85
87
  if (resumeDir === 'last') {
86
88
  const { findLatestOutput } = await import('./output/reporter.js');
@@ -92,12 +94,21 @@ async function handleRun() {
92
94
  resumeDir = latest;
93
95
  }
94
96
  try {
97
+ // --provider / --model: 命令行覆盖 YAML 中的 LLM 配置
98
+ const cliProviders = ['claude-code', 'gemini-cli', 'copilot-cli', 'codex-cli', 'openclaw-cli'];
99
+ const llmOverride = provider ? {
100
+ provider,
101
+ // CLI provider 不指定 model 时清空(避免 YAML 里的 deepseek-chat 传给 claude CLI)
102
+ model: model || (cliProviders.includes(provider) ? '' : undefined),
103
+ ...(cliProviders.includes(provider) ? { timeout: 600_000 } : {}),
104
+ } : undefined;
95
105
  const result = await run(resolve(filePath), inputs, {
96
106
  outputDir,
97
107
  quiet,
98
108
  watch,
99
109
  resumeDir: resumeDir ? resolve(resumeDir) : undefined,
100
110
  fromStep,
111
+ llmOverride,
101
112
  });
102
113
  process.exit(result.success ? 0 : 1);
103
114
  }
@@ -176,7 +187,21 @@ async function handleExplain() {
176
187
  }
177
188
  }
178
189
  async function handleCompose() {
179
- const description = args[1];
190
+ const autoRun = args.includes('--run');
191
+ // 描述是第一个非 flag 的参数(跳过 compose 本身和 --xxx 的值)
192
+ const flagsWithValue = new Set(['--name', '--provider', '--model', '--agents-dir']);
193
+ let description;
194
+ for (let i = 1; i < args.length; i++) {
195
+ if (args[i] === '--run')
196
+ continue;
197
+ if (args[i].startsWith('--')) {
198
+ if (flagsWithValue.has(args[i]))
199
+ i++; // 跳过 flag 的值
200
+ continue;
201
+ }
202
+ description = args[i];
203
+ break;
204
+ }
180
205
  if (!description) {
181
206
  console.error('用法: ao compose "用一句话描述你想要的工作流"');
182
207
  console.error('');
@@ -186,19 +211,28 @@ async function handleCompose() {
186
211
  console.error(' ao compose "用户反馈分析,分类后分别给产品和技术团队"');
187
212
  console.error('');
188
213
  console.error('选项:');
214
+ console.error(' --run 生成后立即运行(一句话出结果)');
215
+ console.error(' --name <filename> 自定义输出文件名 (不含 .yaml 后缀)');
189
216
  console.error(' --provider <name> LLM 提供商 (默认 deepseek)');
190
217
  console.error(' --model <name> 模型名 (默认 deepseek-chat)');
191
218
  process.exit(1);
192
219
  }
193
220
  const provider = (getArgValue('--provider') || 'deepseek');
194
- const model = getArgValue('--model') || (provider === 'deepseek' ? 'deepseek-chat' : provider === 'claude' ? 'claude-sonnet-4-20250514' : 'gpt-4o');
221
+ const cliProviders = ['claude-code', 'gemini-cli', 'copilot-cli', 'codex-cli', 'openclaw-cli'];
222
+ const model = getArgValue('--model') || (cliProviders.includes(provider) ? '' :
223
+ provider === 'deepseek' ? 'deepseek-chat' :
224
+ provider === 'claude' ? 'claude-sonnet-4-20250514' :
225
+ 'gpt-4o');
195
226
  const agentsDir = getArgValue('--agents-dir') || resolveAgentsDir();
227
+ const outputName = getArgValue('--name');
196
228
  try {
197
229
  const { composeWorkflow } = await import('./cli/compose.js');
198
- const { yaml, relativePath, warnings } = await composeWorkflow({
230
+ const { yaml, savedPath, relativePath, warnings } = await composeWorkflow({
199
231
  description,
200
232
  agentsDir: resolve(agentsDir),
201
233
  llmConfig: { provider, model },
234
+ outputName,
235
+ autoRun,
202
236
  });
203
237
  console.log(`\n ✅ 工作流已生成: ${relativePath}\n`);
204
238
  // 校验警告
@@ -209,6 +243,33 @@ async function handleCompose() {
209
243
  }
210
244
  console.log('');
211
245
  }
246
+ if (autoRun) {
247
+ // --run 模式:校验有严重问题时不执行
248
+ if (warnings.some(w => w.includes('解析失败'))) {
249
+ console.error(' 生成的 YAML 有解析错误,无法自动运行。请手动修复后执行:');
250
+ console.error(` ao run ${relativePath}`);
251
+ process.exit(1);
252
+ }
253
+ console.log('─'.repeat(50));
254
+ console.log(' 开始执行工作流...\n');
255
+ // 保底:如果 LLM 仍然生成了 required inputs,用用户描述填充
256
+ const { parseWorkflow } = await import('./core/parser.js');
257
+ const workflow = parseWorkflow(resolve(savedPath));
258
+ const inputs = {};
259
+ for (const def of workflow.inputs || []) {
260
+ if (def.required && def.default === undefined) {
261
+ inputs[def.name] = description;
262
+ }
263
+ }
264
+ const result = await run(resolve(savedPath), inputs, {
265
+ quiet: false,
266
+ // 用 compose 时同样的 provider 执行,避免 YAML 里写的 provider 和用户实际可用的不一致
267
+ // CLI provider 单步调用可能很慢(1-20 分钟),给足超时
268
+ llmOverride: { provider, model: model || undefined, timeout: cliProviders.includes(provider) ? 600_000 : 300_000 },
269
+ });
270
+ process.exit(result.success ? 0 : 1);
271
+ }
272
+ // 非 --run 模式:显示预览和下一步提示
212
273
  console.log(' 预览:');
213
274
  const previewLines = yaml.split('\n').slice(0, 30);
214
275
  for (const line of previewLines) {
@@ -271,13 +332,41 @@ async function handleInit() {
271
332
  }
272
333
  else {
273
334
  console.log(' 正在下载 agency-agents-zh (186 个 AI 角色定义)...\n');
335
+ let downloaded = false;
336
+ // 优先用 npm(国内镜像快)
274
337
  try {
275
- execSync('git clone --depth 1 https://github.com/jnMetaCode/agency-agents-zh.git', { stdio: 'inherit' });
276
- console.log('\n 下载完成!');
338
+ execSync('npm pack agency-agents-zh --pack-destination .', { stdio: 'pipe' });
339
+ const { readdirSync } = await import('node:fs');
340
+ const tgz = readdirSync('.').find(f => f.startsWith('agency-agents-zh-') && f.endsWith('.tgz'));
341
+ if (tgz) {
342
+ const { mkdirSync } = await import('node:fs');
343
+ mkdirSync('agency-agents-zh', { recursive: true });
344
+ execSync(`tar xzf ${tgz} --strip-components=1 -C agency-agents-zh`, { stdio: 'pipe' });
345
+ const { unlinkSync } = await import('node:fs');
346
+ unlinkSync(tgz);
347
+ downloaded = true;
348
+ console.log(' 通过 npm 下载完成!');
349
+ }
277
350
  }
278
351
  catch {
279
- console.error('\n 下载失败,请手动克隆:');
280
- console.error(' git clone https://github.com/jnMetaCode/agency-agents-zh.git');
352
+ // npm 失败,回退 git clone
353
+ }
354
+ // 回退: git clone
355
+ if (!downloaded) {
356
+ try {
357
+ console.log(' npm 下载失败,尝试 git clone...\n');
358
+ execSync('git clone --depth 1 https://github.com/jnMetaCode/agency-agents-zh.git', { stdio: 'inherit' });
359
+ console.log('\n 下载完成!');
360
+ downloaded = true;
361
+ }
362
+ catch {
363
+ // ignore
364
+ }
365
+ }
366
+ if (!downloaded) {
367
+ console.error('\n 下载失败,请手动安装:');
368
+ console.error(' npm pack agency-agents-zh && tar xzf agency-agents-zh-*.tgz && mv package agency-agents-zh');
369
+ console.error(' 或: git clone https://github.com/jnMetaCode/agency-agents-zh.git');
281
370
  process.exit(1);
282
371
  }
283
372
  }
@@ -294,9 +383,25 @@ function handleRoles() {
294
383
  try {
295
384
  const agents = listAgents(resolve(agentsDir));
296
385
  console.log(`\n 共 ${agents.length} 个角色 (${agentsDir}):\n`);
386
+ // 按分类分组
387
+ const byCategory = new Map();
297
388
  for (const agent of agents) {
298
- const emoji = agent.emoji || ' ';
299
- console.log(` ${emoji} ${agent.name} ${agent.description || '(无描述)'}`);
389
+ const cat = agent.rolePath?.split('/')[0] || 'other';
390
+ const list = byCategory.get(cat) || [];
391
+ list.push(agent);
392
+ byCategory.set(cat, list);
393
+ }
394
+ for (const [category, list] of byCategory) {
395
+ console.log(` ── ${category} (${list.length}) ──`);
396
+ for (const agent of list) {
397
+ const emoji = agent.emoji || ' ';
398
+ const path = agent.rolePath || '';
399
+ console.log(` ${emoji} ${agent.name} ${path}`);
400
+ if (agent.description) {
401
+ console.log(` ${agent.description}`);
402
+ }
403
+ }
404
+ console.log('');
300
405
  }
301
406
  }
302
407
  catch (err) {
@@ -389,6 +494,7 @@ function printHelp() {
389
494
  init 下载/更新 agency-agents-zh
390
495
  init --workflow 交互式创建新工作流
391
496
  compose "描述" AI 智能编排工作流(一句话生成 YAML)
497
+ compose "描述" --run 生成并立即运行(一句话出结果)
392
498
  serve 启动 MCP Server(供 Claude Code / Cursor 调用)
393
499
  run <workflow.yaml> 执行工作流
394
500
  validate <workflow.yaml> 校验工作流定义
@@ -399,6 +505,8 @@ function printHelp() {
399
505
  Options:
400
506
  --input, -i key=value 传入输入变量
401
507
  --input, -i key=@file 从文件读取变量值
508
+ --provider <name> 覆盖 YAML 中的 LLM provider (如 claude-code, deepseek)
509
+ --model <name> 覆盖 YAML 中的模型名
402
510
  --output dir 输出目录 (默认 ao-output/)
403
511
  --resume <dir|last> 从上次运行恢复(加载已完成步骤的输出)
404
512
  --from <step-id> 配合 --resume,从指定步骤重新执行
@@ -1,11 +1,5 @@
1
- /**
2
- * Claude Code CLI Connector
3
- * 通过本地 `claude` CLI 调用,直接��用 Claude Max/Pro 订阅额度,无需 API key
4
- *
5
- * 安装: npm install -g @anthropic-ai/claude-code
6
- * 认证: claude 登录后自动使用订阅额度
7
- */
8
- import { CLIBaseConnector } from './cli-base.js';
9
- export declare class ClaudeCodeConnector extends CLIBaseConnector {
10
- constructor();
1
+ import type { LLMConnector, LLMResult, LLMConfig } from '../types.js';
2
+ export declare class ClaudeCodeConnector implements LLMConnector {
3
+ chat(systemPrompt: string, userMessage: string, config: LLMConfig): Promise<LLMResult>;
4
+ private _exec;
11
5
  }
@@ -1,23 +1,145 @@
1
1
  /**
2
2
  * Claude Code CLI Connector
3
- * 通过本地 `claude` CLI 调用,直接��用 Claude Max/Pro 订阅额度,无需 API key
3
+ * 通过本地 `claude` CLI 调用,直接使用 Claude Max/Pro 订阅额度,无需 API key
4
4
  *
5
5
  * 安装: npm install -g @anthropic-ai/claude-code
6
6
  * 认证: claude 登录后自动使用订阅额度
7
+ *
8
+ * 关键: 使用 --output-format json 而非 text
9
+ * text 格式在管道模式下有缓冲问题,长输出(>1000 字)会导致子进程挂起
10
+ * json 格式一次性输出完整结果,包含 usage 等元数据
7
11
  */
8
- import { CLIBaseConnector } from './cli-base.js';
9
- export class ClaudeCodeConnector extends CLIBaseConnector {
10
- constructor() {
11
- super({
12
- command: 'claude',
13
- displayName: 'Claude Code CLI',
14
- buildArgs: (prompt, config) => {
15
- const args = ['-p', prompt, '--output-format', 'text'];
16
- if (config.model && config.model !== 'claude-code') {
17
- args.push('--model', config.model);
18
- }
19
- return args;
20
- },
12
+ import { spawn } from 'node:child_process';
13
+ import { writeFileSync, unlinkSync } from 'node:fs';
14
+ import { tmpdir } from 'node:os';
15
+ import { join } from 'node:path';
16
+ export class ClaudeCodeConnector {
17
+ async chat(systemPrompt, userMessage, config) {
18
+ const timeout = config.timeout || 600_000; // 默认 10 分钟
19
+ // 用临时文件传系统 prompt(避免命令行过长)
20
+ let systemPromptFile;
21
+ if (systemPrompt) {
22
+ systemPromptFile = join(tmpdir(), `ao-sysprompt-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`);
23
+ writeFileSync(systemPromptFile, systemPrompt, 'utf-8');
24
+ }
25
+ // 使用 json 格式:text 格式在管道中会缓冲挂起
26
+ const args = ['-p', '-', '--output-format', 'json', '--tools', '', '--effort', 'low', '--no-session-persistence'];
27
+ if (systemPromptFile) {
28
+ args.push('--system-prompt-file', systemPromptFile);
29
+ }
30
+ if (config.model && config.model !== 'claude-code') {
31
+ args.push('--model', config.model);
32
+ }
33
+ try {
34
+ return await this._exec(args, userMessage, timeout);
35
+ }
36
+ finally {
37
+ if (systemPromptFile) {
38
+ try {
39
+ unlinkSync(systemPromptFile);
40
+ }
41
+ catch { }
42
+ }
43
+ }
44
+ }
45
+ _exec(args, stdinData, timeout) {
46
+ return new Promise((resolve, reject) => {
47
+ const child = spawn('claude', args, {
48
+ env: { ...process.env },
49
+ stdio: ['pipe', 'pipe', 'pipe'],
50
+ shell: process.platform === 'win32',
51
+ });
52
+ let stdout = '';
53
+ let stderr = '';
54
+ let killed = false;
55
+ let receivedBytes = 0;
56
+ let lastProgressTime = 0;
57
+ const timer = setTimeout(() => {
58
+ killed = true;
59
+ child.kill('SIGTERM');
60
+ setTimeout(() => { try {
61
+ child.kill('SIGKILL');
62
+ }
63
+ catch { } }, 5000);
64
+ }, timeout);
65
+ child.stdout.on('data', (chunk) => {
66
+ stdout += chunk.toString();
67
+ receivedBytes += chunk.length;
68
+ const now = Date.now();
69
+ if (now - lastProgressTime > 10_000) {
70
+ lastProgressTime = now;
71
+ const kb = (receivedBytes / 1024).toFixed(1);
72
+ process.stderr.write(` 📡 已接收 ${kb}KB...\n`);
73
+ }
74
+ });
75
+ child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
76
+ child.stdin.on('error', () => { });
77
+ child.stdin.write(stdinData);
78
+ child.stdin.end();
79
+ child.on('error', (err) => {
80
+ clearTimeout(timer);
81
+ if (err.code === 'ENOENT') {
82
+ reject(new Error('找不到 claude 命令,请先安装 Claude Code CLI\n' +
83
+ '安装: npm install -g @anthropic-ai/claude-code\n' +
84
+ '参考: https://github.com/jnMetaCode/agency-orchestrator#llm-配置'));
85
+ }
86
+ else {
87
+ reject(new Error(`Claude Code CLI 调用失败: ${err.message}`));
88
+ }
89
+ });
90
+ child.on('close', (code) => {
91
+ clearTimeout(timer);
92
+ if (killed) {
93
+ reject(new Error(`Claude Code CLI 超时 (${timeout / 1000}s),可在 YAML 中设置 timeout 增加等待时间`));
94
+ return;
95
+ }
96
+ if (code !== 0 && !stdout.trim()) {
97
+ reject(new Error(`Claude Code CLI 调用失败 (exit ${code}): ${stderr.slice(0, 500)}`));
98
+ return;
99
+ }
100
+ // 解析 JSON 响应
101
+ try {
102
+ const json = JSON.parse(stdout);
103
+ if (json.is_error) {
104
+ reject(new Error(`Claude Code CLI 错误: ${json.result?.slice(0, 300) || 'unknown error'}`));
105
+ return;
106
+ }
107
+ const content = (json.result || '').trim();
108
+ if (!content) {
109
+ reject(new Error(`Claude Code CLI 返回空内容`));
110
+ return;
111
+ }
112
+ // 从 JSON 中提取真实 usage
113
+ const usage = json.usage || json.modelUsage || {};
114
+ resolve({
115
+ content,
116
+ usage: {
117
+ input_tokens: usage.input_tokens || usage.inputTokens || 0,
118
+ output_tokens: usage.output_tokens || usage.outputTokens || 0,
119
+ },
120
+ });
121
+ }
122
+ catch {
123
+ // JSON 解析失败,回退到原始文本
124
+ const content = stdout.trim();
125
+ if (!content) {
126
+ reject(new Error(`Claude Code CLI 返回空内容,stderr: ${stderr.slice(0, 500)}`));
127
+ return;
128
+ }
129
+ // 检测 API 错误
130
+ if (content.length < 500) {
131
+ const apiErrorPattern = /^API Error:|^ECONNRESET|^ETIMEDOUT|^ECONNREFUSED|^Unable to connect|^socket hang up/im;
132
+ if (apiErrorPattern.test(content)) {
133
+ reject(new Error(`Claude Code CLI API 错误: ${content.slice(0, 300)}`));
134
+ return;
135
+ }
136
+ }
137
+ resolve({
138
+ content,
139
+ usage: { input_tokens: 0, output_tokens: 0 },
140
+ });
141
+ }
142
+ });
21
143
  });
22
144
  }
23
145
  }
@@ -4,8 +4,12 @@ export interface CLIConnectorConfig {
4
4
  command: string;
5
5
  /** 显示名称(用于错误消息) */
6
6
  displayName: string;
7
+ /** 安装提示(ENOENT 时显示) */
8
+ installHint?: string;
7
9
  /** 构建命令行参数 */
8
10
  buildArgs: (fullPrompt: string, config: LLMConfig) => string[];
11
+ /** 构建 stdin 模式的参数(prompt 过长时使用,默认用 buildArgs 替换 prompt 为 '-') */
12
+ buildStdinArgs?: (config: LLMConfig) => string[];
9
13
  /** 从 stdout 提取内容(默认 trim) */
10
14
  parseOutput?: (stdout: string) => string;
11
15
  }