agency-orchestrator 0.6.6 → 0.6.7
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 +9 -8
- package/dist/cli/compose.d.ts +3 -0
- package/dist/cli/compose.js +149 -3
- package/dist/cli/provider-guides.d.ts +13 -0
- package/dist/cli/provider-guides.js +97 -0
- package/dist/cli.js +97 -19
- package/dist/connectors/cli-base.js +7 -1
- package/dist/connectors/codex-cli.js +12 -4
- package/dist/connectors/copilot-cli.js +6 -2
- package/dist/connectors/factory.js +2 -2
- package/dist/connectors/gemini-cli.js +6 -2
- package/dist/connectors/openai-compatible.js +5 -1
- package/dist/core/executor.js +46 -17
- package/dist/i18n.js +2 -0
- package/dist/utils/duration.d.ts +9 -0
- package/dist/utils/duration.js +24 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -476,13 +476,14 @@ ao-output/产品需求评审-2026-03-22/
|
|
|
476
476
|
Code/Copilot...) Cursor 直接调用)
|
|
477
477
|
```
|
|
478
478
|
|
|
479
|
-
| 项目 |
|
|
480
|
-
|
|
481
|
-
|
|
|
482
|
-
| [agency-agents](https://github.com/
|
|
483
|
-
| [
|
|
484
|
-
| [superpowers-zh](https://github.com/jnMetaCode/superpowers-zh) |
|
|
485
|
-
| [
|
|
479
|
+
| 项目 | 定位 | 一句话 |
|
|
480
|
+
|------|------|-------|
|
|
481
|
+
| **本项目**(agency-orchestrator) | 🚀 编排引擎 | 一句话 → 211 专家协作,**几分钟出方案**(9 家 LLM / 6 免费) |
|
|
482
|
+
| [agency-agents-zh](https://github.com/jnMetaCode/agency-agents-zh)  | 🎭 中文角色库 | 211 个**即插即用** AI 专家,含 46 中国原创(小红书 / 抖音 / 飞书 / 钉钉) |
|
|
483
|
+
| [agency-agents](https://github.com/msitarzewski/agency-agents) | 🎭 英文角色库 | 170+ 个英文 AI 角色 by [@msitarzewski](https://github.com/msitarzewski) |
|
|
484
|
+
| [superpowers-zh](https://github.com/jnMetaCode/superpowers-zh)  | 🧠 工作方法论 | 20 个 skills 教 AI 怎么干活(TDD / 调试 / 代码审查等) |
|
|
485
|
+
| [ai-coding-guide](https://github.com/jnMetaCode/ai-coding-guide) | 📖 实战教程 | 66 个 Claude Code 技巧 + 9 款工具最佳实践 + 配置模板 |
|
|
486
|
+
| [shellward](https://github.com/jnMetaCode/shellward) | 🛡️ 安全中间件 | 8 层防御 + DLP 数据流 + 注入检测,**零依赖**(含 MCP Server) |
|
|
486
487
|
|
|
487
488
|
### 交流
|
|
488
489
|
|
|
@@ -490,7 +491,7 @@ ao-output/产品需求评审-2026-03-22/
|
|
|
490
491
|
|
|
491
492
|
| 渠道 | 加入方式 |
|
|
492
493
|
|------|---------|
|
|
493
|
-
| QQ 群 | [点击加入](https://qm.qq.com/q/EeNQA9xCxy)(群号 1071280067) |
|
|
494
|
+
| QQ 2群 | [点击加入](https://qm.qq.com/q/EeNQA9xCxy)(群号 1071280067) |
|
|
494
495
|
| 微信群 | 关注公众号后回复「群」获取入群方式 |
|
|
495
496
|
|
|
496
497
|
## 路线图
|
package/dist/cli/compose.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export declare function buildComposeSystemPrompt(catalog: string, options?: {
|
|
|
30
30
|
provider?: string;
|
|
31
31
|
model?: string;
|
|
32
32
|
lang?: 'zh' | 'en';
|
|
33
|
+
timeoutMs?: number;
|
|
33
34
|
}): string;
|
|
34
35
|
/**
|
|
35
36
|
* 构建 user prompt
|
|
@@ -55,6 +56,8 @@ export declare function composeWorkflow(options: {
|
|
|
55
56
|
autoRun?: boolean;
|
|
56
57
|
/** 语言:自动检测或指定 */
|
|
57
58
|
lang?: 'zh' | 'en';
|
|
59
|
+
/** 生成的 YAML 中写入的单步超时(ms);未指定时 API=300s / ollama=600s */
|
|
60
|
+
timeoutMs?: number;
|
|
58
61
|
}): Promise<{
|
|
59
62
|
yaml: string;
|
|
60
63
|
savedPath: string;
|
package/dist/cli/compose.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 支持中文(agency-agents-zh)和英文(agency-agents)角色库。
|
|
6
6
|
*/
|
|
7
7
|
import { listAgents } from '../agents/loader.js';
|
|
8
|
-
import { existsSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
9
9
|
import { resolve, relative } from 'node:path';
|
|
10
10
|
import { createConnector } from '../connectors/factory.js';
|
|
11
11
|
import { t } from '../i18n.js';
|
|
@@ -68,7 +68,7 @@ function buildComposeSystemPromptEn(catalog, options) {
|
|
|
68
68
|
const model = options?.model;
|
|
69
69
|
const isLocal = provider === 'ollama';
|
|
70
70
|
const maxTokens = isLocal ? 8192 : 4096;
|
|
71
|
-
const timeoutMs = isLocal ? 600000 :
|
|
71
|
+
const timeoutMs = options?.timeoutMs ?? (isLocal ? 600000 : 300000);
|
|
72
72
|
const inputsSection = autoRun
|
|
73
73
|
? `
|
|
74
74
|
## Important: Direct Run Mode
|
|
@@ -160,7 +160,7 @@ function buildComposeSystemPromptZh(catalog, options) {
|
|
|
160
160
|
const model = options?.model;
|
|
161
161
|
const isLocal = provider === 'ollama';
|
|
162
162
|
const maxTokens = isLocal ? 8192 : 4096;
|
|
163
|
-
const timeoutMs = isLocal ? 600000 :
|
|
163
|
+
const timeoutMs = options?.timeoutMs ?? (isLocal ? 600000 : 300000);
|
|
164
164
|
const inputsSection = autoRun
|
|
165
165
|
? `
|
|
166
166
|
## 重要:直接运行模式
|
|
@@ -319,6 +319,7 @@ export async function composeWorkflow(options) {
|
|
|
319
319
|
provider: options.llmConfig.provider,
|
|
320
320
|
model: options.llmConfig.model,
|
|
321
321
|
lang,
|
|
322
|
+
timeoutMs: options.timeoutMs,
|
|
322
323
|
});
|
|
323
324
|
const userPrompt = buildComposeUserPrompt(description, lang);
|
|
324
325
|
// 3. 调用 LLM
|
|
@@ -385,6 +386,20 @@ export async function composeWorkflow(options) {
|
|
|
385
386
|
if (retryYaml && retryYaml.includes('steps:')) {
|
|
386
387
|
writeFileSync(savedPath, retryYaml + '\n', 'utf-8');
|
|
387
388
|
const second = await validateGenerated(savedPath);
|
|
389
|
+
// 重试后仍有变量引用错误 → 自动修复
|
|
390
|
+
const retryVarErrors = second.errors.filter(e => e.includes('未定义的变量'));
|
|
391
|
+
if (retryVarErrors.length > 0) {
|
|
392
|
+
const fixResult = await autoFixVariableRefs(savedPath);
|
|
393
|
+
if (fixResult.fixed > 0) {
|
|
394
|
+
console.log(` 自动修复了 ${fixResult.fixed} 个变量引用:`);
|
|
395
|
+
for (const f of fixResult.details)
|
|
396
|
+
console.log(` {{${f.from}}} → {{${f.to}}}`);
|
|
397
|
+
const afterFix = await validateGenerated(savedPath);
|
|
398
|
+
warnings.push(...afterFix.errors);
|
|
399
|
+
const fixedYaml = readFileSync(savedPath, 'utf-8').trim();
|
|
400
|
+
return { yaml: fixedYaml, savedPath, relativePath, warnings };
|
|
401
|
+
}
|
|
402
|
+
}
|
|
388
403
|
warnings.push(...second.errors);
|
|
389
404
|
return { yaml: retryYaml, savedPath, relativePath, warnings };
|
|
390
405
|
}
|
|
@@ -393,6 +408,137 @@ export async function composeWorkflow(options) {
|
|
|
393
408
|
warnings.push(`自动修正失败(保留原始输出): ${err instanceof Error ? err.message : err}`);
|
|
394
409
|
}
|
|
395
410
|
}
|
|
411
|
+
// 自动修复未定义的变量引用(LLM 常见错误:变量名与 output 名不一致)
|
|
412
|
+
const varErrors = first.errors.filter(e => e.includes('未定义的变量'));
|
|
413
|
+
if (varErrors.length > 0) {
|
|
414
|
+
const fixResult = await autoFixVariableRefs(savedPath);
|
|
415
|
+
if (fixResult.fixed > 0) {
|
|
416
|
+
console.log(` 自动修复了 ${fixResult.fixed} 个变量引用:`);
|
|
417
|
+
for (const f of fixResult.details) {
|
|
418
|
+
console.log(` {{${f.from}}} → {{${f.to}}}`);
|
|
419
|
+
}
|
|
420
|
+
// 重新校验
|
|
421
|
+
const afterFix = await validateGenerated(savedPath);
|
|
422
|
+
warnings.push(...afterFix.errors);
|
|
423
|
+
const fixedYaml = readFileSync(savedPath, 'utf-8').trim();
|
|
424
|
+
return { yaml: fixedYaml, savedPath, relativePath, warnings };
|
|
425
|
+
}
|
|
426
|
+
}
|
|
396
427
|
warnings.push(...first.errors);
|
|
397
428
|
return { yaml, savedPath, relativePath, warnings };
|
|
398
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* 自动修复 compose 生成 YAML 中的变量引用错误。
|
|
432
|
+
* 常见情况:LLM 用 step id 或 role 名代替 output 变量名。
|
|
433
|
+
*/
|
|
434
|
+
async function autoFixVariableRefs(yamlPath) {
|
|
435
|
+
const { parseWorkflow } = await import('../core/parser.js');
|
|
436
|
+
const content = readFileSync(yamlPath, 'utf-8');
|
|
437
|
+
let workflow;
|
|
438
|
+
try {
|
|
439
|
+
workflow = parseWorkflow(yamlPath);
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
return { fixed: 0, details: [] };
|
|
443
|
+
}
|
|
444
|
+
const inputNames = new Set((workflow.inputs || []).map((i) => i.name));
|
|
445
|
+
const outputNames = new Set();
|
|
446
|
+
const stepIdToOutput = new Map();
|
|
447
|
+
for (const step of workflow.steps) {
|
|
448
|
+
if (step.output) {
|
|
449
|
+
outputNames.add(step.output);
|
|
450
|
+
stepIdToOutput.set(step.id, step.output);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const allDefined = new Set([...inputNames, ...outputNames, '_loop_iteration']);
|
|
454
|
+
const replacements = [];
|
|
455
|
+
let fixedContent = content;
|
|
456
|
+
// 找出所有未定义的变量引用
|
|
457
|
+
const undefinedVars = new Set();
|
|
458
|
+
for (const step of workflow.steps) {
|
|
459
|
+
const refs = step.task?.match(/\{\{(\w+)\}\}/g) || [];
|
|
460
|
+
for (const ref of refs) {
|
|
461
|
+
const varName = ref.slice(2, -2);
|
|
462
|
+
if (!allDefined.has(varName)) {
|
|
463
|
+
undefinedVars.add(varName);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
for (const badVar of undefinedVars) {
|
|
468
|
+
// 策略1:badVar 是某个 step id,该 step 有 output → 替换为 output
|
|
469
|
+
if (stepIdToOutput.has(badVar)) {
|
|
470
|
+
const goodVar = stepIdToOutput.get(badVar);
|
|
471
|
+
fixedContent = fixedContent.replace(new RegExp(`\\{\\{${badVar}\\}\\}`, 'g'), `{{${goodVar}}}`);
|
|
472
|
+
replacements.push({ from: badVar, to: goodVar });
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
// 策略2:模糊匹配 — 找子串包含关系最强的 output 变量
|
|
476
|
+
const best = findBestMatch(badVar, [...outputNames]);
|
|
477
|
+
if (best) {
|
|
478
|
+
fixedContent = fixedContent.replace(new RegExp(`\\{\\{${badVar}\\}\\}`, 'g'), `{{${best}}}`);
|
|
479
|
+
replacements.push({ from: badVar, to: best });
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
// 策略3:按 depends_on 找上游有 output 且尚未被引用的步骤
|
|
483
|
+
const alreadyUsed = new Set(replacements.map(r => r.to));
|
|
484
|
+
for (const step of workflow.steps) {
|
|
485
|
+
const refs = step.task?.match(/\{\{(\w+)\}\}/g) || [];
|
|
486
|
+
const hasBadRef = refs.some((r) => r.slice(2, -2) === badVar);
|
|
487
|
+
if (hasBadRef && step.depends_on?.length) {
|
|
488
|
+
// 优先选还没被占用的上游 output
|
|
489
|
+
const deps = step.depends_on.filter(d => stepIdToOutput.has(d));
|
|
490
|
+
const unusedDep = deps.find(d => !alreadyUsed.has(stepIdToOutput.get(d)));
|
|
491
|
+
const depId = unusedDep || deps[0];
|
|
492
|
+
if (depId) {
|
|
493
|
+
const goodVar = stepIdToOutput.get(depId);
|
|
494
|
+
fixedContent = fixedContent.replace(new RegExp(`\\{\\{${badVar}\\}\\}`, 'g'), `{{${goodVar}}}`);
|
|
495
|
+
replacements.push({ from: badVar, to: goodVar });
|
|
496
|
+
}
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (replacements.length > 0) {
|
|
502
|
+
writeFileSync(yamlPath, fixedContent, 'utf-8');
|
|
503
|
+
}
|
|
504
|
+
return { fixed: replacements.length, details: replacements };
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* 模糊匹配:找子串包含、前缀/后缀重叠最多的候选
|
|
508
|
+
*/
|
|
509
|
+
function findBestMatch(target, candidates) {
|
|
510
|
+
if (candidates.length === 0)
|
|
511
|
+
return null;
|
|
512
|
+
const t = target.toLowerCase();
|
|
513
|
+
// 完全包含关系
|
|
514
|
+
for (const c of candidates) {
|
|
515
|
+
const cl = c.toLowerCase();
|
|
516
|
+
if (t.includes(cl) || cl.includes(t))
|
|
517
|
+
return c;
|
|
518
|
+
}
|
|
519
|
+
// 按公共子串长度打分
|
|
520
|
+
let best = '';
|
|
521
|
+
let bestScore = 0;
|
|
522
|
+
for (const c of candidates) {
|
|
523
|
+
const cl = c.toLowerCase();
|
|
524
|
+
const score = longestCommonSubstring(t, cl);
|
|
525
|
+
if (score > bestScore && score >= 3) {
|
|
526
|
+
bestScore = score;
|
|
527
|
+
best = c;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return best || null;
|
|
531
|
+
}
|
|
532
|
+
function longestCommonSubstring(a, b) {
|
|
533
|
+
let max = 0;
|
|
534
|
+
for (let i = 0; i < a.length; i++) {
|
|
535
|
+
for (let j = 0; j < b.length; j++) {
|
|
536
|
+
let k = 0;
|
|
537
|
+
while (i + k < a.length && j + k < b.length && a[i + k] === b[j + k])
|
|
538
|
+
k++;
|
|
539
|
+
if (k > max)
|
|
540
|
+
max = k;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return max;
|
|
544
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 各 provider 首次使用指南 —— 在 `ao init --provider X` 完成 .env 写入后打印,
|
|
3
|
+
* 告诉用户 provider 自身还需要做什么(OAuth 登录 / 拿 API key / 启本地服务 等)。
|
|
4
|
+
*/
|
|
5
|
+
export interface GuideContext {
|
|
6
|
+
/** 本次 init 是否提供了 --api-key */
|
|
7
|
+
hasApiKey: boolean;
|
|
8
|
+
/** 本次 init 是否提供了 --base-url,或环境已存在 OPENAI_BASE_URL */
|
|
9
|
+
hasBaseUrl: boolean;
|
|
10
|
+
/** 本次 init 指定的 model(用于 ollama pull 提示) */
|
|
11
|
+
model?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function getProviderGuide(provider: string, ctx: GuideContext): string;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 各 provider 首次使用指南 —— 在 `ao init --provider X` 完成 .env 写入后打印,
|
|
3
|
+
* 告诉用户 provider 自身还需要做什么(OAuth 登录 / 拿 API key / 启本地服务 等)。
|
|
4
|
+
*/
|
|
5
|
+
export function getProviderGuide(provider, ctx) {
|
|
6
|
+
const p = provider.toLowerCase();
|
|
7
|
+
switch (p) {
|
|
8
|
+
case 'gemini-cli':
|
|
9
|
+
return [
|
|
10
|
+
`📋 首次使用 gemini-cli,二选一:`,
|
|
11
|
+
` A. Google 账号登录(免费 1000 次/天,推荐)`,
|
|
12
|
+
` → 终端跑一次: gemini -p "hi",首次会弹出浏览器登录`,
|
|
13
|
+
` B. API Key`,
|
|
14
|
+
` → 从 https://aistudio.google.com/app/apikey 申请(不是 Cloud Console)`,
|
|
15
|
+
` → export GEMINI_API_KEY=xxx 或加到 .env`,
|
|
16
|
+
].join('\n');
|
|
17
|
+
case 'claude-code':
|
|
18
|
+
return [
|
|
19
|
+
`📋 首次使用 Claude Code:`,
|
|
20
|
+
` → 终端跑一次: claude,首次会弹出浏览器完成 Anthropic 账号登录`,
|
|
21
|
+
` → 或设 API Key: export ANTHROPIC_API_KEY=sk-ant-xxx`,
|
|
22
|
+
].join('\n');
|
|
23
|
+
case 'copilot-cli':
|
|
24
|
+
return [
|
|
25
|
+
`📋 首次使用 GitHub Copilot CLI:`,
|
|
26
|
+
` → 需要 GitHub Copilot 付费订阅`,
|
|
27
|
+
` → 终端跑 copilot 命令,按提示完成 GitHub 账号登录`,
|
|
28
|
+
].join('\n');
|
|
29
|
+
case 'codex-cli':
|
|
30
|
+
return [
|
|
31
|
+
`📋 首次使用 OpenAI Codex CLI:`,
|
|
32
|
+
` → 需要 ChatGPT Plus/Pro 订阅`,
|
|
33
|
+
` → 终端跑 codex 命令,按提示完成 OpenAI 账号登录`,
|
|
34
|
+
].join('\n');
|
|
35
|
+
case 'openclaw-cli':
|
|
36
|
+
return [
|
|
37
|
+
`📋 首次使用 OpenClaw CLI:`,
|
|
38
|
+
` → 终端跑 openclaw 命令,按提示完成登录或配置 token`,
|
|
39
|
+
` → 也可用 OPENCLAW_AGENT 环境变量指定 agent`,
|
|
40
|
+
].join('\n');
|
|
41
|
+
case 'hermes-cli':
|
|
42
|
+
return [
|
|
43
|
+
`📋 首次使用 Hermes CLI:`,
|
|
44
|
+
` → 参考 Hermes 官方文档完成 token 配置`,
|
|
45
|
+
` → 支持多模型,在 YAML 里用 model 字段指定 (如 anthropic/claude-sonnet-4、openai/gpt-4o)`,
|
|
46
|
+
].join('\n');
|
|
47
|
+
case 'ollama': {
|
|
48
|
+
const model = ctx.model || '<model>';
|
|
49
|
+
return [
|
|
50
|
+
`📋 Ollama 本地推理:`,
|
|
51
|
+
` → 启动服务: ollama serve`,
|
|
52
|
+
` → 拉取模型: ollama pull ${model}`,
|
|
53
|
+
` → 默认地址 http://localhost:11434,可用 OLLAMA_BASE_URL 覆盖`,
|
|
54
|
+
].join('\n');
|
|
55
|
+
}
|
|
56
|
+
case 'deepseek':
|
|
57
|
+
return ctx.hasApiKey
|
|
58
|
+
? `✅ DeepSeek 已配置,可以跑: ao run workflows/story-creation.yaml -i premise="测试"`
|
|
59
|
+
: [
|
|
60
|
+
`📋 DeepSeek 还缺 API key:`,
|
|
61
|
+
` → 从 https://platform.deepseek.com/ 申请`,
|
|
62
|
+
` → 再跑: ao init --provider deepseek --api-key sk-xxx`,
|
|
63
|
+
].join('\n');
|
|
64
|
+
case 'openai':
|
|
65
|
+
return ctx.hasApiKey
|
|
66
|
+
? `✅ OpenAI 已配置。`
|
|
67
|
+
: [
|
|
68
|
+
`📋 OpenAI 还缺 API key:`,
|
|
69
|
+
` → 从 https://platform.openai.com/api-keys 申请`,
|
|
70
|
+
` → 再跑: ao init --provider openai --api-key sk-xxx`,
|
|
71
|
+
].join('\n');
|
|
72
|
+
case 'anthropic':
|
|
73
|
+
case 'claude':
|
|
74
|
+
return ctx.hasApiKey
|
|
75
|
+
? `✅ Anthropic Claude 已配置。`
|
|
76
|
+
: [
|
|
77
|
+
`📋 Anthropic Claude 还缺 API key:`,
|
|
78
|
+
` → 从 https://console.anthropic.com/settings/keys 申请`,
|
|
79
|
+
` → 再跑: ao init --provider claude --api-key sk-ant-xxx`,
|
|
80
|
+
].join('\n');
|
|
81
|
+
default: {
|
|
82
|
+
const lines = [];
|
|
83
|
+
lines.push(`📋 "${provider}" 按自定义 OpenAI 兼容端点处理:`);
|
|
84
|
+
if (!ctx.hasBaseUrl)
|
|
85
|
+
lines.push(` ⚠️ 还缺 --base-url(/v1 兼容接口地址)`);
|
|
86
|
+
if (!ctx.hasApiKey)
|
|
87
|
+
lines.push(` ⚠️ 还缺 --api-key`);
|
|
88
|
+
lines.push(``);
|
|
89
|
+
lines.push(` 常见国内端点可参考:`);
|
|
90
|
+
lines.push(` 智谱 GLM: https://open.bigmodel.cn/api/paas/v4`);
|
|
91
|
+
lines.push(` Moonshot: https://api.moonshot.cn/v1`);
|
|
92
|
+
lines.push(` 通义: https://dashscope.aliyuncs.com/compatible-mode/v1`);
|
|
93
|
+
lines.push(` 硅基流动: https://api.siliconflow.cn/v1`);
|
|
94
|
+
return lines.join('\n');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ import { run } from './index.js';
|
|
|
18
18
|
import { scheduleUpdateCheck } from './utils/version-check.js';
|
|
19
19
|
import { t, detectLang } from './i18n.js';
|
|
20
20
|
import { loadEnvFile, writeEnvFile, ensureEnvGitignored } from './utils/env-loader.js';
|
|
21
|
+
import { parseDuration } from './utils/duration.js';
|
|
21
22
|
// Auto-load ./.env (shell env wins; no overwrite)
|
|
22
23
|
loadEnvFile();
|
|
23
24
|
// Suppress Node's DEP0190 warning from legitimate shell:true on Windows (.cmd shims).
|
|
@@ -99,6 +100,18 @@ async function handleRun() {
|
|
|
99
100
|
// Precedence: CLI flag > .env (AO_PROVIDER/AO_MODEL) > YAML
|
|
100
101
|
const provider = (getArgValue('--provider') || process.env.AO_PROVIDER);
|
|
101
102
|
const model = getArgValue('--model') || process.env.AO_MODEL;
|
|
103
|
+
const baseUrl = getArgValue('--base-url') || getArgValue('--baseurl');
|
|
104
|
+
const apiKey = getArgValue('--api-key') || getArgValue('--apikey');
|
|
105
|
+
const timeoutRaw = getArgValue('--timeout');
|
|
106
|
+
let timeoutMs;
|
|
107
|
+
if (timeoutRaw !== undefined) {
|
|
108
|
+
const parsed = parseDuration(timeoutRaw);
|
|
109
|
+
if (parsed === null) {
|
|
110
|
+
console.error(`--timeout 值无效: "${timeoutRaw}"(支持 300000 / 300s / 5m / 0)`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
timeoutMs = parsed;
|
|
114
|
+
}
|
|
102
115
|
// --resume last: 自动找最近一次的输出目录
|
|
103
116
|
if (resumeDir === 'last') {
|
|
104
117
|
const { findLatestOutput } = await import('./output/reporter.js');
|
|
@@ -110,14 +123,29 @@ async function handleRun() {
|
|
|
110
123
|
resumeDir = latest;
|
|
111
124
|
}
|
|
112
125
|
try {
|
|
113
|
-
// --provider / --model: 命令行覆盖 YAML 中的 LLM 配置
|
|
126
|
+
// --provider / --model / --base-url / --api-key / --timeout: 命令行覆盖 YAML 中的 LLM 配置
|
|
114
127
|
const cliProviders = ['claude-code', 'gemini-cli', 'copilot-cli', 'codex-cli', 'openclaw-cli', 'hermes-cli'];
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
128
|
+
let llmOverride;
|
|
129
|
+
if (provider || model || baseUrl || apiKey || timeoutMs !== undefined) {
|
|
130
|
+
llmOverride = {};
|
|
131
|
+
if (provider) {
|
|
132
|
+
llmOverride.provider = provider;
|
|
133
|
+
// CLI provider 不指定 model 时清空(避免 YAML 里的 deepseek-chat 传给 claude CLI)
|
|
134
|
+
llmOverride.model = model || (cliProviders.includes(provider) ? '' : undefined);
|
|
135
|
+
if (cliProviders.includes(provider))
|
|
136
|
+
llmOverride.timeout = 600_000;
|
|
137
|
+
}
|
|
138
|
+
else if (model) {
|
|
139
|
+
llmOverride.model = model;
|
|
140
|
+
}
|
|
141
|
+
if (baseUrl)
|
|
142
|
+
llmOverride.base_url = baseUrl;
|
|
143
|
+
if (apiKey)
|
|
144
|
+
llmOverride.api_key = apiKey;
|
|
145
|
+
// --timeout 最后赋值,优先级高于 CLI provider 自动 600s
|
|
146
|
+
if (timeoutMs !== undefined)
|
|
147
|
+
llmOverride.timeout = timeoutMs;
|
|
148
|
+
}
|
|
121
149
|
const result = await run(resolve(filePath), inputs, {
|
|
122
150
|
outputDir,
|
|
123
151
|
quiet,
|
|
@@ -205,7 +233,7 @@ async function handleExplain() {
|
|
|
205
233
|
async function handleCompose() {
|
|
206
234
|
const autoRun = args.includes('--run');
|
|
207
235
|
// 描述是第一个非 flag 的参数(跳过 compose 本身和 --xxx 的值)
|
|
208
|
-
const flagsWithValue = new Set(['--name', '--provider', '--model', '--agents-dir', '--lang']);
|
|
236
|
+
const flagsWithValue = new Set(['--name', '--provider', '--model', '--agents-dir', '--lang', '--base-url', '--baseurl', '--api-key', '--apikey', '--timeout']);
|
|
209
237
|
let description;
|
|
210
238
|
for (let i = 1; i < args.length; i++) {
|
|
211
239
|
if (args[i] === '--run')
|
|
@@ -232,14 +260,36 @@ async function handleCompose() {
|
|
|
232
260
|
console.error(' --provider <name> LLM 提供商 (默认 deepseek)');
|
|
233
261
|
console.error(' --model <name> 模型名 (默认 deepseek-chat)');
|
|
234
262
|
console.error(' --lang <zh|en> 语言 (默认自动检测 / auto-detect)');
|
|
263
|
+
console.error(' --timeout <值> 单步超时,支持 300000/300s/5m (默认 API 300s, 本地/CLI 600s)');
|
|
235
264
|
process.exit(1);
|
|
236
265
|
}
|
|
237
266
|
const provider = (getArgValue('--provider') || process.env.AO_PROVIDER || 'deepseek');
|
|
238
267
|
const cliProviders = ['claude-code', 'gemini-cli', 'copilot-cli', 'codex-cli', 'openclaw-cli', 'hermes-cli'];
|
|
239
|
-
const
|
|
268
|
+
const knownApiProviders = ['deepseek', 'claude', 'openai', 'ollama'];
|
|
269
|
+
const isUnknownProvider = !cliProviders.includes(provider) && !knownApiProviders.includes(provider);
|
|
270
|
+
const cliModel = getArgValue('--model') || process.env.AO_MODEL;
|
|
271
|
+
// 未知 provider(如 zhipu/qwen/moonshot 走 openai-compatible)必须显式指定 model,
|
|
272
|
+
// 否则会用下面的 'gpt-4o' 默认值去调自定义端点,必然 404。
|
|
273
|
+
if (isUnknownProvider && !cliModel) {
|
|
274
|
+
console.error(`\n错误: provider "${provider}" 是自定义 OpenAI 兼容端点,必须用 --model 显式指定模型名(例如 --model glm-4-plus)。`);
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
const model = cliModel || (cliProviders.includes(provider) ? '' :
|
|
240
278
|
provider === 'deepseek' ? 'deepseek-chat' :
|
|
241
279
|
provider === 'claude' ? 'claude-sonnet-4-20250514' :
|
|
242
280
|
'gpt-4o');
|
|
281
|
+
const baseUrl = getArgValue('--base-url') || getArgValue('--baseurl');
|
|
282
|
+
const apiKey = getArgValue('--api-key') || getArgValue('--apikey');
|
|
283
|
+
const composeTimeoutRaw = getArgValue('--timeout');
|
|
284
|
+
let composeTimeoutMs;
|
|
285
|
+
if (composeTimeoutRaw !== undefined) {
|
|
286
|
+
const parsed = parseDuration(composeTimeoutRaw);
|
|
287
|
+
if (parsed === null) {
|
|
288
|
+
console.error(`--timeout 值无效: "${composeTimeoutRaw}"(支持 300000 / 300s / 5m / 0)`);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
composeTimeoutMs = parsed;
|
|
292
|
+
}
|
|
243
293
|
// 自动检测语言:英文输入 → 优先英文角色库
|
|
244
294
|
const { detectLang } = await import('./cli/compose.js');
|
|
245
295
|
const composeLang = getArgValue('--lang') ?? detectLang(description);
|
|
@@ -250,10 +300,16 @@ async function handleCompose() {
|
|
|
250
300
|
const { yaml, savedPath, relativePath, warnings } = await composeWorkflow({
|
|
251
301
|
description,
|
|
252
302
|
agentsDir: resolve(agentsDir),
|
|
253
|
-
llmConfig: {
|
|
303
|
+
llmConfig: {
|
|
304
|
+
provider,
|
|
305
|
+
model,
|
|
306
|
+
...(baseUrl ? { base_url: baseUrl } : {}),
|
|
307
|
+
...(apiKey ? { api_key: apiKey } : {}),
|
|
308
|
+
},
|
|
254
309
|
outputName,
|
|
255
310
|
autoRun,
|
|
256
311
|
lang: composeLang,
|
|
312
|
+
timeoutMs: composeTimeoutMs,
|
|
257
313
|
});
|
|
258
314
|
console.log(`\n ${t('compose.generated', { path: relativePath })}\n`);
|
|
259
315
|
// 校验警告
|
|
@@ -285,8 +341,16 @@ async function handleCompose() {
|
|
|
285
341
|
const result = await run(resolve(savedPath), inputs, {
|
|
286
342
|
quiet: false,
|
|
287
343
|
// 用 compose 时同样的 provider 执行,避免 YAML 里写的 provider 和用户实际可用的不一致
|
|
288
|
-
// CLI provider 单步调用可能很慢(1-20
|
|
289
|
-
llmOverride: {
|
|
344
|
+
// CLI provider 单步调用可能很慢(1-20 分钟),给足超时;用户显式 --timeout 优先
|
|
345
|
+
llmOverride: {
|
|
346
|
+
provider,
|
|
347
|
+
model: model || undefined,
|
|
348
|
+
timeout: composeTimeoutMs !== undefined
|
|
349
|
+
? composeTimeoutMs
|
|
350
|
+
: (cliProviders.includes(provider) ? 600_000 : 300_000),
|
|
351
|
+
...(baseUrl ? { base_url: baseUrl } : {}),
|
|
352
|
+
...(apiKey ? { api_key: apiKey } : {}),
|
|
353
|
+
},
|
|
290
354
|
});
|
|
291
355
|
process.exit(result.success ? 0 : 1);
|
|
292
356
|
}
|
|
@@ -358,17 +422,17 @@ async function handleInit() {
|
|
|
358
422
|
updates[urlVar] = cfgBaseUrl;
|
|
359
423
|
}
|
|
360
424
|
if (cfgApiKey) {
|
|
361
|
-
//
|
|
425
|
+
// 路由到 factory 里 connector 实际会读的 env 变量。
|
|
426
|
+
// factory.ts 只对 deepseek / claude / openai 有专属分支;其他 provider
|
|
427
|
+
// (zhipu/glm/qwen/moonshot 等)都走 default → openai-compatible,读 OPENAI_API_KEY。
|
|
428
|
+
// 所以未知 provider 的 key 也写到 OPENAI_API_KEY,不再写死代码的 AO_API_KEY / ZHIPU_API_KEY。
|
|
362
429
|
const p = (cfgProvider || process.env.AO_PROVIDER || '').toLowerCase();
|
|
363
430
|
const keyVar = p === 'deepseek' ? 'DEEPSEEK_API_KEY' :
|
|
364
|
-
p === '
|
|
365
|
-
|
|
366
|
-
p === 'zhipu' || p === 'glm' ? 'ZHIPU_API_KEY' :
|
|
367
|
-
p === 'qwen' || p === 'dashscope' ? 'DASHSCOPE_API_KEY' :
|
|
368
|
-
p === 'moonshot' || p === 'kimi' ? 'MOONSHOT_API_KEY' :
|
|
369
|
-
'AO_API_KEY';
|
|
431
|
+
p === 'anthropic' || p === 'claude' ? 'ANTHROPIC_API_KEY' :
|
|
432
|
+
'OPENAI_API_KEY';
|
|
370
433
|
updates[keyVar] = cfgApiKey;
|
|
371
434
|
}
|
|
435
|
+
const hasBaseUrl = !!cfgBaseUrl || !!process.env.OPENAI_BASE_URL;
|
|
372
436
|
const envPath = resolve(process.cwd(), '.env');
|
|
373
437
|
writeEnvFile(updates);
|
|
374
438
|
const gitignoreUpdated = ensureEnvGitignored();
|
|
@@ -379,6 +443,20 @@ async function handleInit() {
|
|
|
379
443
|
}
|
|
380
444
|
if (gitignoreUpdated)
|
|
381
445
|
console.log(` 🔒 已将 .env 加入 .gitignore`);
|
|
446
|
+
// 打印 provider 专属的首次使用指南
|
|
447
|
+
if (cfgProvider) {
|
|
448
|
+
const { getProviderGuide } = await import('./cli/provider-guides.js');
|
|
449
|
+
const guide = getProviderGuide(cfgProvider, {
|
|
450
|
+
hasApiKey: !!cfgApiKey,
|
|
451
|
+
hasBaseUrl,
|
|
452
|
+
model: cfgModel,
|
|
453
|
+
});
|
|
454
|
+
if (guide) {
|
|
455
|
+
console.log('');
|
|
456
|
+
for (const line of guide.split('\n'))
|
|
457
|
+
console.log(` ${line}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
382
460
|
console.log(`\n 下次运行 ao 时会自动加载这些配置。`);
|
|
383
461
|
console.log(` 也可手动编辑 .env 或复制到其他项目复用。`);
|
|
384
462
|
return;
|
|
@@ -90,7 +90,13 @@ export class CLIBaseConnector {
|
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
92
|
if (code !== 0 && !stdout.trim()) {
|
|
93
|
-
|
|
93
|
+
// 启发式识别"首次未认证"类错误(各 CLI 工具首次运行时都要求登录),给中文引导
|
|
94
|
+
const authPattern = /auth method|not authenticated|not logged in|please (login|sign[\s-]*in)|unauthorized|credentials|_API_KEY/i;
|
|
95
|
+
const looksLikeAuth = authPattern.test(stderr);
|
|
96
|
+
const hint = looksLikeAuth
|
|
97
|
+
? `\n 提示: 首次使用 ${this.cfg.displayName} 需要先在终端跑一次 \`${this.cfg.command}\` 完成账号登录,或设置对应的 API KEY 环境变量`
|
|
98
|
+
: '';
|
|
99
|
+
reject(new Error(`${this.cfg.displayName} 调用失败 (exit ${code}): ${stderr.slice(0, 500)}${hint}`));
|
|
94
100
|
return;
|
|
95
101
|
}
|
|
96
102
|
const content = this.cfg.parseOutput
|
|
@@ -11,11 +11,19 @@ export class CodexCLIConnector extends CLIBaseConnector {
|
|
|
11
11
|
super({
|
|
12
12
|
command: 'codex',
|
|
13
13
|
displayName: 'OpenAI Codex CLI',
|
|
14
|
-
buildArgs: (prompt,
|
|
15
|
-
|
|
14
|
+
buildArgs: (prompt, config) => {
|
|
15
|
+
const args = ['exec', '--skip-git-repo-check', '--sandbox', 'read-only'];
|
|
16
|
+
if (config.model)
|
|
17
|
+
args.push('--model', config.model);
|
|
18
|
+
args.push(prompt);
|
|
19
|
+
return args;
|
|
16
20
|
},
|
|
17
|
-
buildStdinArgs: (
|
|
18
|
-
|
|
21
|
+
buildStdinArgs: (config) => {
|
|
22
|
+
const args = ['exec', '--skip-git-repo-check', '--sandbox', 'read-only'];
|
|
23
|
+
if (config.model)
|
|
24
|
+
args.push('--model', config.model);
|
|
25
|
+
args.push('-');
|
|
26
|
+
return args;
|
|
19
27
|
},
|
|
20
28
|
});
|
|
21
29
|
}
|
|
@@ -11,8 +11,12 @@ export class CopilotCLIConnector extends CLIBaseConnector {
|
|
|
11
11
|
super({
|
|
12
12
|
command: 'copilot',
|
|
13
13
|
displayName: 'GitHub Copilot CLI',
|
|
14
|
-
buildArgs: (prompt,
|
|
15
|
-
|
|
14
|
+
buildArgs: (prompt, config) => {
|
|
15
|
+
const args = [];
|
|
16
|
+
if (config.model)
|
|
17
|
+
args.push('--model', config.model);
|
|
18
|
+
args.push('-p', prompt);
|
|
19
|
+
return args;
|
|
16
20
|
},
|
|
17
21
|
});
|
|
18
22
|
}
|
|
@@ -30,12 +30,12 @@ export function createConnector(config) {
|
|
|
30
30
|
case 'deepseek':
|
|
31
31
|
return new OpenAICompatibleConnector({
|
|
32
32
|
apiKey: config.api_key || process.env.DEEPSEEK_API_KEY,
|
|
33
|
-
baseUrl: config.base_url || 'https://api.deepseek.com/v1',
|
|
33
|
+
baseUrl: config.base_url || process.env.OPENAI_BASE_URL || 'https://api.deepseek.com/v1',
|
|
34
34
|
});
|
|
35
35
|
case 'openai':
|
|
36
36
|
return new OpenAICompatibleConnector({
|
|
37
37
|
apiKey: config.api_key || process.env.OPENAI_API_KEY,
|
|
38
|
-
baseUrl: config.base_url || 'https://api.openai.com/v1',
|
|
38
|
+
baseUrl: config.base_url || process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
|
|
39
39
|
});
|
|
40
40
|
default:
|
|
41
41
|
// 未知 provider:如果提供了 base_url,当作 OpenAI 兼容 API 处理
|
|
@@ -11,8 +11,12 @@ export class GeminiCLIConnector extends CLIBaseConnector {
|
|
|
11
11
|
super({
|
|
12
12
|
command: 'gemini',
|
|
13
13
|
displayName: 'Gemini CLI',
|
|
14
|
-
buildArgs: (prompt,
|
|
15
|
-
|
|
14
|
+
buildArgs: (prompt, config) => {
|
|
15
|
+
const args = [];
|
|
16
|
+
if (config.model)
|
|
17
|
+
args.push('-m', config.model);
|
|
18
|
+
args.push('-p', prompt);
|
|
19
|
+
return args;
|
|
16
20
|
},
|
|
17
21
|
});
|
|
18
22
|
}
|
|
@@ -55,7 +55,11 @@ export class OpenAICompatibleConnector {
|
|
|
55
55
|
}
|
|
56
56
|
catch (err) {
|
|
57
57
|
clearTimeout(timer);
|
|
58
|
-
|
|
58
|
+
const url = `${this.baseUrl}/chat/completions`;
|
|
59
|
+
const hint = !this.apiKey
|
|
60
|
+
? '\n 可能原因: 未设置 API Key,请检查环境变量(DEEPSEEK_API_KEY 或 OPENAI_API_KEY)或 .env 配置'
|
|
61
|
+
: `\n 可能原因: 无法连接 ${this.baseUrl},请检查 base_url 是否正确、网络是否可达`;
|
|
62
|
+
throw new Error(`请求失败: ${url}\n ${err instanceof Error ? err.message : err}${hint}`);
|
|
59
63
|
}
|
|
60
64
|
if (!response.ok) {
|
|
61
65
|
clearTimeout(timer);
|
package/dist/core/executor.js
CHANGED
|
@@ -221,30 +221,58 @@ async function executeStep(node, opts) {
|
|
|
221
221
|
const effectiveConfig = stepLlm
|
|
222
222
|
? { ...opts.llmConfig, ...stepLlm }
|
|
223
223
|
: opts.llmConfig;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
224
|
+
// connector 把 api_key / base_url 存在构造时的私有字段,chat(config) 不会再读
|
|
225
|
+
// 所以 step 级覆盖任一凭证字段时必须重建 connector
|
|
226
|
+
const needsNewConnector = !!(stepLlm && ((stepLlm.provider && stepLlm.provider !== opts.llmConfig.provider) ||
|
|
227
|
+
stepLlm.base_url !== undefined ||
|
|
228
|
+
stepLlm.api_key !== undefined));
|
|
229
|
+
const effectiveConnector = needsNewConnector ? createConnector(effectiveConfig) : opts.connector;
|
|
230
|
+
// timeout / retry / CLI 判定必须基于 effectiveConfig,否则 step 级覆盖这几个字段时会被全局值吃掉
|
|
231
|
+
const effectiveIsCLI = effectiveConfig.provider.endsWith('-cli') || effectiveConfig.provider === 'claude-code';
|
|
232
|
+
const effectiveIsLocal = effectiveConfig.provider === 'ollama';
|
|
233
|
+
// timeout 策略:
|
|
234
|
+
// - 用户显式设置(含 timeout: 0 表示不限时)→ 第一次按此值
|
|
235
|
+
// - 未设置 → provider 默认(API 120s / CLI/ollama 600s)
|
|
236
|
+
// - 因超时触发 retry 时,下一轮 timeout x1.5(上限 900s)
|
|
237
|
+
// 非超时类错误(429/500/ECONNRESET 等)保持原 timeout,避免无谓放大
|
|
238
|
+
const defaultTimeout = effectiveIsCLI ? 600_000 : effectiveIsLocal ? 600_000 : 120_000;
|
|
239
|
+
const baseTimeout = effectiveConfig.timeout !== undefined ? effectiveConfig.timeout : defaultTimeout;
|
|
240
|
+
const effectiveMaxRetry = effectiveConfig.retry ?? opts.maxRetry;
|
|
241
|
+
const TIMEOUT_CAP = 900_000;
|
|
242
|
+
// 带重试的 LLM 调用(timeout 在网络超时类错误重试时自动延长)
|
|
228
243
|
let lastError = null;
|
|
229
|
-
|
|
244
|
+
let attemptTimeout = baseTimeout;
|
|
245
|
+
for (let attempt = 0; attempt <= effectiveMaxRetry; attempt++) {
|
|
230
246
|
try {
|
|
231
|
-
|
|
247
|
+
// attemptTimeout 同时传给 connector(控制内层 fetch/CLI timeout)和 withTimeout(外层兜底),
|
|
248
|
+
// 否则 connector 内部还按旧 timeout 硬断,递增就白加了
|
|
249
|
+
const attemptConfig = { ...effectiveConfig, timeout: attemptTimeout };
|
|
250
|
+
const result = await withTimeout(effectiveConnector.chat(systemPrompt, userMessage, attemptConfig), attemptTimeout);
|
|
232
251
|
node.tokenUsage = { input: result.usage.input_tokens, output: result.usage.output_tokens };
|
|
233
252
|
return result.content;
|
|
234
253
|
}
|
|
235
254
|
catch (err) {
|
|
236
255
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
237
|
-
if (attempt <
|
|
238
|
-
const isCLI = opts.llmConfig.provider.endsWith('-cli') || opts.llmConfig.provider === 'claude-code';
|
|
256
|
+
if (attempt < effectiveMaxRetry && isRetryable(lastError)) {
|
|
239
257
|
const errorClass = classifyError(lastError);
|
|
258
|
+
// connection 类错误(超时/ECONNRESET/aborted/socket hang up 等)→ 下一次 timeout x1.5
|
|
259
|
+
// 上限 900s,0=不限时保持不变,rate_limit / server_error 保持原值避免无谓放大
|
|
260
|
+
let nextTimeout = attemptTimeout;
|
|
261
|
+
if (errorClass === 'connection' && attemptTimeout > 0 && attemptTimeout < TIMEOUT_CAP) {
|
|
262
|
+
nextTimeout = Math.min(Math.round(attemptTimeout * 1.5), TIMEOUT_CAP);
|
|
263
|
+
}
|
|
240
264
|
// 分级退避:rate_limit 最长,connection 中等,server_error 最短
|
|
241
|
-
const baseByClass =
|
|
265
|
+
const baseByClass = effectiveIsCLI
|
|
242
266
|
? { rate_limit: 15_000, connection: 10_000, server_error: 5_000 }
|
|
243
267
|
: { rate_limit: 5_000, connection: 2_000, server_error: 1_000 };
|
|
244
268
|
const base = baseByClass[errorClass] || 1_000;
|
|
245
269
|
const jitter = Math.random() * 0.3; // 0-30% 抖动,防止并发步骤同时重试
|
|
246
270
|
const delay = Math.round(base * Math.pow(2, attempt) * (1 + jitter));
|
|
247
|
-
|
|
271
|
+
const extendHint = nextTimeout !== attemptTimeout
|
|
272
|
+
? `(timeout 延长至 ${Math.round(nextTimeout / 1000)}s)`
|
|
273
|
+
: '';
|
|
274
|
+
process.stderr.write(`\n ⚠️ ${node.step.id} 失败 (${lastError.message.slice(0, 80)}),${Math.round(delay / 1000)}s 后重试${extendHint} (${attempt + 1}/${effectiveMaxRetry})...\n`);
|
|
275
|
+
attemptTimeout = nextTimeout;
|
|
248
276
|
await sleep(delay);
|
|
249
277
|
continue;
|
|
250
278
|
}
|
|
@@ -304,7 +332,7 @@ function withTimeout(promise, ms) {
|
|
|
304
332
|
if (!ms)
|
|
305
333
|
return promise; // 0 = 不限时(CLI provider 写完自动停)
|
|
306
334
|
return new Promise((resolve, reject) => {
|
|
307
|
-
const timer = setTimeout(() => reject(new Error(`超时 (${ms}ms)
|
|
335
|
+
const timer = setTimeout(() => reject(new Error(`超时 (${ms}ms),可用 --timeout 或 YAML llm.timeout 延长`)), ms);
|
|
308
336
|
promise
|
|
309
337
|
.then(val => { clearTimeout(timer); resolve(val); })
|
|
310
338
|
.catch(err => { clearTimeout(timer); reject(err); });
|
|
@@ -313,16 +341,17 @@ function withTimeout(promise, ms) {
|
|
|
313
341
|
/** 错误分级:不同错误类型使用不同退避策略(借鉴 Claude Code 架构) */
|
|
314
342
|
function classifyError(error) {
|
|
315
343
|
const msg = error.message.toLowerCase();
|
|
316
|
-
//
|
|
317
|
-
if (msg
|
|
344
|
+
// 限速:需要更长退避。用 \b 边界匹配,避免 "1429ms" / "429 ids" 等子串误判
|
|
345
|
+
if (/\b429\b/.test(msg) || msg.includes('rate'))
|
|
318
346
|
return 'rate_limit';
|
|
319
|
-
//
|
|
320
|
-
if (
|
|
347
|
+
// 服务端错误:短退避即可。5xx 状态码用 \b 边界匹配,避免 "500ms" / "450000ms" 等误判
|
|
348
|
+
if (/\b5\d\d\b/.test(msg) || msg.includes('api 错误'))
|
|
321
349
|
return 'server_error';
|
|
322
|
-
//
|
|
350
|
+
// 连接断开/超时:中等退避("超时"识别中文 withTimeout 抛出的消息)
|
|
323
351
|
if (msg.includes('econnreset') || msg.includes('econnrefused') ||
|
|
324
352
|
msg.includes('etimedout') || msg.includes('socket hang up') ||
|
|
325
|
-
msg.includes('terminated') || msg.includes('aborted') ||
|
|
353
|
+
msg.includes('terminated') || msg.includes('aborted') ||
|
|
354
|
+
msg.includes('timeout') || msg.includes('超时'))
|
|
326
355
|
return 'connection';
|
|
327
356
|
return 'non_retryable';
|
|
328
357
|
}
|
package/dist/i18n.js
CHANGED
|
@@ -167,6 +167,7 @@ const dict = {
|
|
|
167
167
|
--input, -i key=@file 从文件读取变量值
|
|
168
168
|
--provider <name> 覆盖 YAML 中的 LLM provider (如 claude-code, deepseek)
|
|
169
169
|
--model <name> 覆盖 YAML 中的模型名
|
|
170
|
+
--timeout <值> 单步超时,支持 300000/300s/5m/0 (0=不限时)
|
|
170
171
|
--output dir 输出目录 (默认 ao-output/)
|
|
171
172
|
--resume <dir|last> 从上次运行恢复(加载已完成步骤的输出)
|
|
172
173
|
--from <step-id> 配合 --resume,从指定步骤重新执行
|
|
@@ -220,6 +221,7 @@ const dict = {
|
|
|
220
221
|
--input, -i key=@file Read variable value from a file
|
|
221
222
|
--provider <name> Override LLM provider (e.g. claude-code, deepseek)
|
|
222
223
|
--model <name> Override model name from YAML
|
|
224
|
+
--timeout <value> Per-step timeout, accepts 300000/300s/5m/0 (0=no limit)
|
|
223
225
|
--output dir Output directory (default: ao-output/)
|
|
224
226
|
--resume <dir|last> Resume from previous run (reuse completed step outputs)
|
|
225
227
|
--from <step-id> With --resume, re-run starting from this step
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 解析时长字符串 → 毫秒。
|
|
3
|
+
* "300000" / "300000ms" → 300000
|
|
4
|
+
* "300s" → 300000
|
|
5
|
+
* "5m" → 300000
|
|
6
|
+
* "0" → 0 (调用方可用来表示"不限时")
|
|
7
|
+
* 非法输入返回 null,由调用方报错。
|
|
8
|
+
*/
|
|
9
|
+
export function parseDuration(input) {
|
|
10
|
+
const m = /^(\d+)\s*(ms|s|m)?$/i.exec(input.trim());
|
|
11
|
+
if (!m)
|
|
12
|
+
return null;
|
|
13
|
+
const n = parseInt(m[1], 10);
|
|
14
|
+
if (!Number.isFinite(n) || n < 0)
|
|
15
|
+
return null;
|
|
16
|
+
const unit = (m[2] || 'ms').toLowerCase();
|
|
17
|
+
if (unit === 'ms')
|
|
18
|
+
return n;
|
|
19
|
+
if (unit === 's')
|
|
20
|
+
return n * 1000;
|
|
21
|
+
if (unit === 'm')
|
|
22
|
+
return n * 60_000;
|
|
23
|
+
return null;
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agency-orchestrator",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"description": "Multi-agent YAML workflow engine — 211 AI roles, auto DAG parallelism, zero code. One sentence → multiple AI roles collaborate → complete plan in minutes. 10 LLM providers, 7 need no API key.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"multi-agent",
|