agency-orchestrator 0.8.1 → 0.9.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 +10 -6
- package/dist/cli/compose.d.ts +25 -0
- package/dist/cli/compose.js +258 -9
- package/dist/cli/provider-guides.d.ts +0 -4
- package/dist/cli/provider-guides.js +12 -0
- package/dist/cli.js +150 -28
- package/dist/connectors/api-providers.d.ts +25 -0
- package/dist/connectors/api-providers.js +19 -0
- package/dist/connectors/claude-code.js +16 -4
- package/dist/connectors/claude.js +1 -0
- package/dist/connectors/cli-base.d.ts +10 -0
- package/dist/connectors/cli-base.js +45 -4
- package/dist/connectors/codex-cli.js +1 -0
- package/dist/connectors/copilot-cli.js +1 -0
- package/dist/connectors/factory.js +15 -34
- package/dist/connectors/gemini-cli.js +1 -0
- package/dist/connectors/ollama.js +1 -0
- package/dist/connectors/openai-compatible.js +1 -0
- package/dist/i18n.js +2 -0
- package/dist/types.d.ts +1 -0
- package/dist/utils/claude-repair.d.ts +46 -0
- package/dist/utils/claude-repair.js +159 -0
- package/dist/utils/codex-relay.d.ts +27 -0
- package/dist/utils/codex-relay.js +108 -0
- package/dist/utils/custom-providers.d.ts +14 -0
- package/dist/utils/custom-providers.js +59 -0
- package/package.json +3 -2
- package/web/server.js +513 -30
- package/website/dist/_headers +10 -4
- package/website/dist/assets/Changelog-Cd1_ta-F.js +8 -0
- package/website/dist/assets/{CreativeLibrary-B_8O1s2c.js → CreativeLibrary-Bafp2-7y.js} +1 -1
- package/website/dist/assets/{Docs-DU2N3IS_.js → Docs-DWjEFgqb.js} +1 -1
- package/website/dist/assets/{Experts-C9z5y2rm.js → Experts-ARxT7R0z.js} +1 -1
- package/website/dist/assets/Home-D1fuy-e9.js +21 -0
- package/website/dist/assets/{Markdown-C0L6FhCW.js → Markdown-DIBk11T6.js} +1 -1
- package/website/dist/assets/{NotFound-xNtHHfkV.js → NotFound-yaONXr2c.js} +1 -1
- package/website/dist/assets/{PromptStudio-17TeFAQ8.js → PromptStudio-DdCuT1Il.js} +1 -1
- package/website/dist/assets/{SiteFooter-DR4eH-53.js → SiteFooter-CuZFXiZH.js} +1 -1
- package/website/dist/assets/Sponsors-D_4M9Xth.js +21 -0
- package/website/dist/assets/Studio-BFYs1jm4.js +182 -0
- package/website/dist/assets/{TutorialDetail-BrfVjMLZ.js → TutorialDetail-C8bjE0Uh.js} +1 -1
- package/website/dist/assets/{Tutorials-DC9c2EvF.js → Tutorials-CscSAnuX.js} +1 -1
- package/website/dist/assets/{UsagePanel-CzoMRrg1.js → UsagePanel-CFfVmSwy.js} +5 -5
- package/website/dist/assets/{arrow-left-RD0wK_Iq.js → arrow-left-CBsGQe3h.js} +1 -1
- package/website/dist/assets/{arrow-up-right-BuwIRRU6.js → arrow-up-right-LSsttJsJ.js} +1 -1
- package/website/dist/assets/{badge-DfAy_y5P.js → badge-xc3rfZGN.js} +1 -1
- package/website/dist/assets/{clock-Cy6q8QUy.js → clock-LLdpHBz2.js} +1 -1
- package/website/dist/assets/{copy-button-DC6rEWiP.js → copy-button-CiQbKrJI.js} +1 -1
- package/website/dist/assets/{copy-DyTNyX56.js → copy-nrJtFYdc.js} +1 -1
- package/website/dist/assets/{download-DIXKHBqo.js → download-DP_pAg1E.js} +1 -1
- package/website/dist/assets/{external-link-CjdD4QgR.js → external-link-CS2XMbHw.js} +1 -1
- package/website/dist/assets/index-DawHdnrM.js +97 -0
- package/website/dist/assets/index-GFHogxlv.css +1 -0
- package/website/dist/assets/{mail-YfVJ15xh.js → mail-Bx74VrG-.js} +1 -1
- package/website/dist/assets/{search-WlZrNuJy.js → search-BH4oWAhq.js} +1 -1
- package/website/dist/assets/{sparkles-C5b30t-L.js → sparkles-jiZMT7tJ.js} +1 -1
- package/website/dist/assets/sponsors-2Ed3J2D2.js +11 -0
- package/website/dist/assets/useBackend-DAjkH7qN.js +30 -0
- package/website/dist/assets/{workflow-C-Cq5FbH.js → workflow-D0-wMIOl.js} +9 -4
- package/website/dist/index.html +2 -2
- package/website/dist/providers-manifest.json +15 -0
- package/website/dist/screenshots/studio-workflows.webp +0 -0
- package/website/dist/sponsors/logo-ccsub-icon.png +0 -0
- package/website/dist/sponsors/logo-cubence-icon.png +0 -0
- package/website/dist/sponsors/logo-rootflowai-icon.png +0 -0
- package/workflows//347/234/201/351/222/261/346/267/267/347/224/250/347/244/272/344/276/213.yaml +58 -0
- package/website/dist/assets/Changelog-C6gJ8sN7.js +0 -8
- package/website/dist/assets/Home-BWGkGVMU.js +0 -26
- package/website/dist/assets/Sponsors-BMRgur4p.js +0 -26
- package/website/dist/assets/Studio-yUpOYHMn.js +0 -151
- package/website/dist/assets/index-CrZmwA66.js +0 -92
- package/website/dist/assets/index-qTaRmBkE.css +0 -1
- package/website/dist/assets/sponsors-DcILXsjq.js +0 -6
- package/website/dist/assets/useBackend-_4T8SBzA.js +0 -30
package/README.md
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
> 觉得有用?请点个 **Star** — 帮助更多人发现这个项目。
|
|
19
19
|
|
|
20
20
|
<p align="center">
|
|
21
|
-
<img src="./demo-zh.gif" alt="
|
|
21
|
+
<img src="./demo-studio-zh.gif" alt="网页 Studio:一句话,AI 自动组队" width="820"><br/>
|
|
22
|
+
<em>网页 Studio:打一句话,AI 自动从 200+ 专家里组队并运行</em>
|
|
22
23
|
</p>
|
|
23
24
|
|
|
24
25
|
---
|
|
@@ -32,11 +33,6 @@
|
|
|
32
33
|
> 🆕 **创意库**:内置图像生成提示词库(Nano Banana / Gemini,可搜索 / 分类 / 一键复制)。
|
|
33
34
|
> 🆕 **零配置首跑**:本机已登录 Claude Code / Gemini CLI 等?AO 自动探测并直接用,连 API key 都不用配。
|
|
34
35
|
|
|
35
|
-
<p align="center">
|
|
36
|
-
<img src="./docs/screenshots/studio-roles-zh.png" alt="Studio · 角色组队:从 200+ 专家里勾选,AI 自动合成团队" width="800"><br/>
|
|
37
|
-
<em>角色组队:从 200+ 专家里勾选,AI 自动合成团队并运行</em>
|
|
38
|
-
</p>
|
|
39
|
-
|
|
40
36
|
<p align="center">
|
|
41
37
|
<img src="./docs/screenshots/studio-workflows-zh.png" alt="Studio · 工作流模板:内置模板一键运行" width="800"><br/>
|
|
42
38
|
<em>工作流:内置模板一键运行,也能对比多个模板</em>
|
|
@@ -49,10 +45,16 @@
|
|
|
49
45
|
|
|
50
46
|
## 一句话出结果
|
|
51
47
|
|
|
48
|
+
也可以纯命令行——一条命令,一句话出结果:
|
|
49
|
+
|
|
52
50
|
```bash
|
|
53
51
|
ao compose "我是一个程序员,想用AI做自媒体副业,目标月入2万,帮我做完整规划" --run
|
|
54
52
|
```
|
|
55
53
|
|
|
54
|
+
<p align="center">
|
|
55
|
+
<img src="./demo-zh.gif" alt="ao compose 命令行演示" width="700">
|
|
56
|
+
</p>
|
|
57
|
+
|
|
56
58
|
5 个 AI 角色自动分工协作:
|
|
57
59
|
|
|
58
60
|
```
|
|
@@ -271,6 +273,8 @@ OPENAI_API_KEY=你的key
|
|
|
271
273
|
|
|
272
274
|
> ⚠️ 注意:这些平台请使用 `provider: "openai"`,不要用 `provider: "ollama"`。Ollama 仅用于本地模型,不发送 API Key。
|
|
273
275
|
|
|
276
|
+
> 💡 **网页 Studio 里更简单**:「供应商」面板支持直接**添加自定义供应商**(任意 OpenAI 兼容 endpoint,带 SiliconFlow / OpenRouter / 火山方舟 / 智谱 / Kimi 等常见预设,点一下自动填 base_url),加完即可在顶部下拉切换使用。Claude Code / Gemini CLI / Codex 卡片还支持配置**第三方中转**(填中转商的 base_url + token,跳过官方账号登录)。
|
|
277
|
+
|
|
274
278
|
## CLI 命令
|
|
275
279
|
|
|
276
280
|
```bash
|
package/dist/cli/compose.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { LLMConfig } from '../types.js';
|
|
2
|
+
/** 对已生成的 workflow YAML 施加预算分档:给「轻活」步骤加 step.llm.model=便宜档。返回新 YAML + 一句说明。 */
|
|
3
|
+
export declare function applyBudgetTiering(yamlText: string, provider: string): {
|
|
4
|
+
yaml: string;
|
|
5
|
+
note?: string;
|
|
6
|
+
};
|
|
2
7
|
/** 精简的角色摘要,供 LLM 选角色用 */
|
|
3
8
|
export interface RoleSummary {
|
|
4
9
|
path: string;
|
|
@@ -62,6 +67,8 @@ export declare function composeWorkflow(options: {
|
|
|
62
67
|
pinnedRoles?: string[];
|
|
63
68
|
/** 保存目录;默认 workflows/。用于把合成结果写到用户目录而非随包模板目录 */
|
|
64
69
|
saveDir?: string;
|
|
70
|
+
/** R3.2 预算模式:把「轻活」步骤自动降到便宜档,省钱不掉关键质量(默认关,零回归) */
|
|
71
|
+
budget?: boolean;
|
|
65
72
|
}): Promise<{
|
|
66
73
|
yaml: string;
|
|
67
74
|
savedPath: string;
|
|
@@ -81,6 +88,24 @@ export declare function repairInvalidRolesInYaml(yamlPath: string, invalidRoles:
|
|
|
81
88
|
}[];
|
|
82
89
|
unresolved: string[];
|
|
83
90
|
};
|
|
91
|
+
/**
|
|
92
|
+
* 修复"变量名本身是对的,只是引用它的 step 忘了把产出该变量的 step 加进 depends_on"
|
|
93
|
+
* 这类错误——parser.ts 的 validateWorkflow 会专门标出这种情况("该变量由非上游 step
|
|
94
|
+
* 产出,需要把对应 step 加进 depends_on"),说明变量确实由某个 step 产出,只是 DAG
|
|
95
|
+
* 边没连上。直接在 YAML 里给该 step 补一条 depends_on,而不是像 autoFixVariableRefs
|
|
96
|
+
* 那样改名字(名字本来就没错)。
|
|
97
|
+
*
|
|
98
|
+
* 只在能安全定位 step 的文本块、且不会成环时才动手;识别不了的 YAML 形状(既不是
|
|
99
|
+
* `depends_on: [a, b]` 单行 flow 风格,也不是多行列表,也找不到 output 字段可插入)
|
|
100
|
+
* 就跳过,留给后面的 autoFixVariableRefs / LLM 修复兜底。
|
|
101
|
+
*/
|
|
102
|
+
export declare function autoFixMissingDependsOn(yamlPath: string): Promise<{
|
|
103
|
+
fixed: number;
|
|
104
|
+
details: {
|
|
105
|
+
step: string;
|
|
106
|
+
addedDep: string;
|
|
107
|
+
}[];
|
|
108
|
+
}>;
|
|
84
109
|
/**
|
|
85
110
|
* 自动修复 compose 生成 YAML 中的变量引用错误。
|
|
86
111
|
*
|
package/dist/cli/compose.js
CHANGED
|
@@ -9,6 +9,69 @@ 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';
|
|
12
|
+
import yaml from 'js-yaml';
|
|
13
|
+
// ── R3.1/R3.2 预算档(--budget):把「轻活」步骤降到便宜档,「重活」步骤保持默认贵档 ──
|
|
14
|
+
// 各 provider 的「便宜档」模型;没有更便宜档的(如 deepseek 默认就是便宜的 deepseek-chat)不列 → --budget 对其为 no-op。
|
|
15
|
+
const BUDGET_LIGHT_MODEL = {
|
|
16
|
+
claude: 'claude-haiku-4-5-20251001',
|
|
17
|
+
openai: 'gpt-5.4-mini',
|
|
18
|
+
apinebula: 'gpt-5.4-mini',
|
|
19
|
+
rootflowai: 'claude-haiku-4-5-20251001',
|
|
20
|
+
cubence: 'claude-haiku-4-5-20251001',
|
|
21
|
+
ccsub: 'claude-haiku-4-5-20251001',
|
|
22
|
+
// deepseek:默认若走贵的 reasoner,轻活降到便宜的 chat;默认已是 chat 时则 no-op(light===topModel)
|
|
23
|
+
deepseek: 'deepseek-chat',
|
|
24
|
+
};
|
|
25
|
+
// 「轻活」词(抽取/汇总/格式化/罗列/翻译/润色…)→ 可降档;「重活」词(分析/设计/评审/创作…)→ 保贵档。
|
|
26
|
+
// 保守策略:重活优先,命中难词或不确定一律保贵档,只对明确的轻活降档,护住关键质量。
|
|
27
|
+
// 明确的「把 X 重塑成 Y」动作(整理成/格式化/翻译成…)——这类步骤只是重组已有内容,
|
|
28
|
+
// 即便 task 里引用了上游的「洞察/分析」等名词,本身也是轻活。优先级高于 HARD,修掉误判。
|
|
29
|
+
const REFORMAT_RE = /整理成|整理为|归纳成|汇总成|格式化|排版成|排好版|输出成|润色|校对|翻译成|翻译为|改写成|精简成|reformat|format into|rewrite as|translate|proofread|polish/i;
|
|
30
|
+
const HARD_TASK_RE = /分析|设计|架构|评审|审查|判断|决策|创作|撰写|写作|方案|策略|规划|论证|洞察|architect|analy|design|review|judge|strateg|\bplan|reason|critique|creat|writ/i;
|
|
31
|
+
const LIGHT_TASK_RE = /抽取|提取|汇总|归纳|整理|罗列|列出|列表|格式化|排版|翻译|校对|润色|清洗|去重|标注|摘要|summar|extract|format|translat|proofread|\blist|organi[sz]e|categor|clean/i;
|
|
32
|
+
function classifyStepTier(step) {
|
|
33
|
+
const text = `${step?.role ?? ''} ${step?.task ?? ''}`;
|
|
34
|
+
if (REFORMAT_RE.test(text))
|
|
35
|
+
return 'light'; // 明确的重塑动作 → 轻活(优先,纠正"引用难词名词"的误判)
|
|
36
|
+
if (HARD_TASK_RE.test(text))
|
|
37
|
+
return 'hard'; // 其次:命中难词一律保贵档
|
|
38
|
+
if (LIGHT_TASK_RE.test(text))
|
|
39
|
+
return 'light';
|
|
40
|
+
return 'hard'; // 不确定 → 保贵档(保守,护质量)
|
|
41
|
+
}
|
|
42
|
+
/** 对已生成的 workflow YAML 施加预算分档:给「轻活」步骤加 step.llm.model=便宜档。返回新 YAML + 一句说明。 */
|
|
43
|
+
export function applyBudgetTiering(yamlText, provider) {
|
|
44
|
+
const light = BUDGET_LIGHT_MODEL[provider];
|
|
45
|
+
if (!light)
|
|
46
|
+
return { yaml: yamlText, note: `--budget:provider "${provider}" 无更便宜档位可降(可能默认已是便宜档),未改动` };
|
|
47
|
+
let doc;
|
|
48
|
+
try {
|
|
49
|
+
doc = yaml.load(yamlText);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return { yaml: yamlText };
|
|
53
|
+
}
|
|
54
|
+
if (!doc || !Array.isArray(doc.steps))
|
|
55
|
+
return { yaml: yamlText };
|
|
56
|
+
const topModel = doc.llm?.model;
|
|
57
|
+
let downgraded = 0;
|
|
58
|
+
for (const step of doc.steps) {
|
|
59
|
+
if (!step || typeof step !== 'object')
|
|
60
|
+
continue;
|
|
61
|
+
if (step.llm?.model)
|
|
62
|
+
continue; // 步骤已显式指定模型 → 尊重,不覆盖
|
|
63
|
+
if (classifyStepTier(step) === 'light' && light !== topModel) {
|
|
64
|
+
step.llm = { ...(step.llm ?? {}), model: light };
|
|
65
|
+
downgraded++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (downgraded === 0)
|
|
69
|
+
return { yaml: yamlText, note: '--budget:没有可降档的轻活步骤(都判为重活或已指定模型),未改动' };
|
|
70
|
+
return {
|
|
71
|
+
yaml: yaml.dump(doc, { lineWidth: -1 }),
|
|
72
|
+
note: `--budget:${downgraded}/${doc.steps.length} 个轻活步骤降到便宜档 ${light}(重活步骤保持默认,省钱不掉关键质量)`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
12
75
|
/**
|
|
13
76
|
* 从 agents 目录构建精简的角色目录
|
|
14
77
|
*/
|
|
@@ -74,9 +137,10 @@ function buildComposeSystemPromptEn(catalog, options) {
|
|
|
74
137
|
## Important: Direct Run Mode
|
|
75
138
|
|
|
76
139
|
This workflow will be executed immediately after generation, so:
|
|
77
|
-
- **Do NOT** generate an inputs section
|
|
140
|
+
- **Do NOT** generate an inputs section (the upfront form filled before running)
|
|
78
141
|
- Embed all specific information from the user's description directly into each step's task
|
|
79
|
-
- Ensure the workflow can
|
|
142
|
+
- Ensure the workflow can start without any "before-run" form inputs
|
|
143
|
+
- This does NOT affect using a human_input step mid-run to ask the user something (that's an in-flight pause, not an upfront form) — if the task genuinely needs the user to clarify something partway through, still use human_input rather than having the model guess`
|
|
80
144
|
: `
|
|
81
145
|
inputs:
|
|
82
146
|
- name: variable_name
|
|
@@ -129,6 +193,12 @@ ${autoRun ? ' Include specific information from the user\'s description' :
|
|
|
129
193
|
Use {{previous_output}} to reference upstream step outputs
|
|
130
194
|
output: output_variable_name
|
|
131
195
|
depends_on: [upstream_step_id] # Only add when there's a dependency
|
|
196
|
+
|
|
197
|
+
# When you need to ask the user something mid-run, use a human_input step (no role, actually pauses for input):
|
|
198
|
+
- id: ask_step_id
|
|
199
|
+
type: human_input
|
|
200
|
+
prompt: "The specific question to ask the user, can reference {{variable_name}} from earlier steps"
|
|
201
|
+
output: user_answer_variable # the user's answer is injected downstream as this variable
|
|
132
202
|
\`\`\`
|
|
133
203
|
|
|
134
204
|
## Design Principles
|
|
@@ -139,8 +209,9 @@ ${autoRun ? ' Include specific information from the user\'s description' :
|
|
|
139
209
|
- **Role naming**: Each step must have a name (approachable job title like "CEO", "Product Manager", "Tech Lead") and emoji, so anyone can instantly see who's speaking
|
|
140
210
|
- **Detailed tasks**: Task descriptions should be specific — tell the role what to do and what format to output
|
|
141
211
|
${inputsDesignPrinciple}
|
|
212
|
+
- **Use human_input when the user needs to clarify something — don't have a role "ask" in its task**: if the task genuinely can't proceed without more info from the user (personal preference, choosing between options, a specific detail not in inputs — classic case: registration/enrollment-type requests where you must ask the user's specifics partway through), insert a \`type: human_input\` step to ask them, and feed the answer downstream as its output variable. Writing "ask the user X" inside a regular role step's task does NOT work — the engine won't actually pause, the model will just make up an answer
|
|
142
213
|
- **Final deliverable**: The last step must output the final deliverable the user wants (e.g., complete article, complete report), not review comments or suggestions. If there's a review step, it should output the revised final version, not a "list of suggestions"
|
|
143
|
-
- **Clean final output (IMPORTANT)**: The LAST step's \`task\` MUST end with an explicit instruction to output ONLY the deliverable itself — no preamble/greeting, no "what I changed"/change-log, no formatting notes, no questions to the user, no suggestions to run \`ao\`/other commands, no "shall I continue?" closers. Append a line like: "⚠️ Output only the final deliverable itself — no preamble, no change-log, no meta-commentary, no questions, no tool/command suggestions."
|
|
214
|
+
- **Clean final output (IMPORTANT)**: The LAST step's \`task\` MUST end with an explicit instruction to output ONLY the deliverable itself — no preamble/greeting, no "what I changed"/change-log, no formatting notes, no questions to the user, no suggestions to run \`ao\`/other commands, no "shall I continue?" closers. Append a line like: "⚠️ Output only the final deliverable itself — no preamble, no change-log, no meta-commentary, no questions, no tool/command suggestions." (If you genuinely need to ask the user something, use the human_input step above instead — not in the final step)
|
|
144
215
|
|
|
145
216
|
## Available Role Catalog
|
|
146
217
|
|
|
@@ -152,6 +223,7 @@ ${catalog}
|
|
|
152
223
|
- **Variable names must use underscores**, no spaces. Correct: "market_analysis", "tech_report". Wrong: "market analysis", "tech report". All id, output, and depends_on values must be snake_case
|
|
153
224
|
- **Variables must have a source**: every \`{{X}}\` referenced in a step's task MUST appear either as an \`inputs\` name OR as the \`output\` field of an earlier step. Do NOT invent variable names that no step produces
|
|
154
225
|
- **Merge / aggregation steps**: if a step references \`{{a}}\`, \`{{b}}\`, \`{{c}}\` from upstream, its \`depends_on\` MUST list every upstream step that produces those outputs. Cross-check before emitting
|
|
226
|
+
- **human_input steps don't need role/task/emoji/name** — only \`type: human_input\`, \`prompt\`, and \`output\`; its output variable can be depended on and \`{{referenced}}\` downstream just like a normal step
|
|
155
227
|
- Only output the YAML code block, nothing else
|
|
156
228
|
- Set concurrency to the maximum number of parallel steps
|
|
157
229
|
- **Important: Split large tasks**. When writing long articles, don't let one step generate more than 800 words. Split by sections into multiple parallel steps (e.g., write_ch1, write_ch2, write_ch3), then use a merge step to rewrite into a coherent complete article
|
|
@@ -169,9 +241,10 @@ function buildComposeSystemPromptZh(catalog, options) {
|
|
|
169
241
|
## 重要:直接运行模式
|
|
170
242
|
|
|
171
243
|
这个工作流生成后会立即执行,所以:
|
|
172
|
-
- **不要**生成 inputs
|
|
244
|
+
- **不要**生成 inputs 段(运行前的表单输入)
|
|
173
245
|
- 把用户描述中的所有具体信息直接写进每个 step 的 task 里
|
|
174
|
-
-
|
|
246
|
+
- 确保工作流无需任何"运行前"输入就能直接启动
|
|
247
|
+
- 这不影响中途用 human_input 步骤向用户提问(那是运行过程中的暂停提问,不是运行前的表单)——如果任务本质上需要用户中途澄清信息,仍然要用 human_input,不要为了凑"自包含"而让模型瞎猜`
|
|
175
248
|
: `
|
|
176
249
|
inputs:
|
|
177
250
|
- name: variable_name
|
|
@@ -224,6 +297,12 @@ ${autoRun ? ' 直接包含用户需求中的具体信息' : ' 使用 {
|
|
|
224
297
|
使用 {{previous_output}} 引用上游步骤的输出
|
|
225
298
|
output: output_variable_name
|
|
226
299
|
depends_on: [upstream_step_id] # 仅在有依赖时添加
|
|
300
|
+
|
|
301
|
+
# 需要向用户询问/确认信息时,用 human_input 类型的步骤(无 role,运行到这一步会真的暂停等用户输入):
|
|
302
|
+
- id: ask_step_id
|
|
303
|
+
type: human_input
|
|
304
|
+
prompt: "向用户提的具体问题,可用 {{variable_name}} 引用之前的变量"
|
|
305
|
+
output: user_answer_variable # 用户的回答会作为这个变量注入下游 task
|
|
227
306
|
\`\`\`
|
|
228
307
|
|
|
229
308
|
## 设计原则
|
|
@@ -234,8 +313,9 @@ ${autoRun ? ' 直接包含用户需求中的具体信息' : ' 使用 {
|
|
|
234
313
|
- **角色命名**:每个步骤必须设置 name(通俗的公司职位名如"老板""产品经理""技术总监")和 emoji,让小白也能一眼看懂谁在说话
|
|
235
314
|
- **任务详细**:task 描述要具体,告诉角色要做什么、输出什么格式
|
|
236
315
|
${inputsDesignPrinciple}
|
|
316
|
+
- **需要用户澄清时用 human_input,不要指望角色在 task 里"提问"**:如果任务本质上需要用户提供额外信息才能继续(如个人偏好、多个方案里选一个、inputs 里没给的具体细节——典型例子是报名/选课/选方案类需求,中途必须问用户具体情况),插入一个 \`type: human_input\` 的步骤向用户提问,把回答作为 output 变量给下游用。普通 role 步骤的 task 里写"请问用户 XXX"是无效的——引擎不会暂停等回答,模型只会自己编一个答案
|
|
237
317
|
- **最终成品**:最后一个步骤必须输出用户想要的最终成品(如完整文章、完整报告),而不是审查意见或修改建议。如果有审校步骤,审校步骤应该直接输出修改后的定稿,而不是"修改建议列表"
|
|
238
|
-
- **干净的最终产出(重要)**:最后一个步骤的 \`task\` 结尾必须显式要求"只输出成品本身"——不要开场白/寒暄、不要"我改了什么/复盘/修改说明"、不要排版备注小节、不要向用户提问或请其拍板、不要建议运行 \`ao\` 或其它命令、不要"要我继续吗"之类收尾。请在该 step 的 task 末尾追加一行类似:「⚠️
|
|
318
|
+
- **干净的最终产出(重要)**:最后一个步骤的 \`task\` 结尾必须显式要求"只输出成品本身"——不要开场白/寒暄、不要"我改了什么/复盘/修改说明"、不要排版备注小节、不要向用户提问或请其拍板、不要建议运行 \`ao\` 或其它命令、不要"要我继续吗"之类收尾。请在该 step 的 task 末尾追加一行类似:「⚠️ 只输出最终成品本身:不要开场白、不要复盘或说明、不要向用户提问、不要建议任何命令或后续动作。」(需要问用户时用上面的 human_input 步骤,不要在最终步骤里问)
|
|
239
319
|
|
|
240
320
|
## 可用角色目录
|
|
241
321
|
|
|
@@ -247,6 +327,7 @@ ${catalog}
|
|
|
247
327
|
- **变量名必须用下划线**,不能有空格。正确:"market_analysis"、"tech_report"。错误:"market analysis"、"tech report"。id、output、depends_on 中的值都必须用 snake_case
|
|
248
328
|
- **变量必须有来源**:每个 task 中的 \`{{X}}\` 引用,X 必须是 \`inputs\` 中的某个 name,或者是前面某个 step 的 \`output\` 字段。不要凭空写一个没有任何 step 产生的变量名
|
|
249
329
|
- **合并/汇总类步骤**:如果一个步骤的 task 里引用了 \`{{a}}\`、\`{{b}}\`、\`{{c}}\` 这些上游变量,它的 \`depends_on\` 必须列出所有产生这些 output 的上游 step。生成完后请逐一核对一遍
|
|
330
|
+
- **human_input 步骤不需要 role/task/emoji/name**,只需要 \`type: human_input\`、\`prompt\`、\`output\`;它产出的 output 变量可以像普通 step 一样被下游 depends_on + {{引用}}
|
|
250
331
|
- 只输出 YAML 代码块,不要输出其他内容
|
|
251
332
|
- concurrency 设为并行步骤的最大数量
|
|
252
333
|
- **重要:拆分大任务**。写长文章时,不要让一个步骤生成超过 800 字的内容。应该按章节拆分成多个并行步骤(如 write_ch1、write_ch2、write_ch3),最后用一个合并步骤重写为连贯的完整文章
|
|
@@ -444,7 +525,7 @@ export async function composeWorkflow(options) {
|
|
|
444
525
|
// 重试后仍有变量引用错误 → 走 fix 链(autoFix → LLM 二次修复)
|
|
445
526
|
const finalErrors = await runVariableFixChain(savedPath, second.errors, validateGenerated, llmConfig, lang);
|
|
446
527
|
warnings.push(...finalErrors);
|
|
447
|
-
const fixedYaml = readFileSync(savedPath, 'utf-8').trim();
|
|
528
|
+
const fixedYaml = finalizeBudget(readFileSync(savedPath, 'utf-8').trim(), options, llmConfig.provider, savedPath, warnings);
|
|
448
529
|
return { yaml: fixedYaml, savedPath, relativePath, warnings };
|
|
449
530
|
}
|
|
450
531
|
}
|
|
@@ -455,9 +536,20 @@ export async function composeWorkflow(options) {
|
|
|
455
536
|
// 首次生成无幻觉角色 → 直接走变量 fix 链
|
|
456
537
|
const finalErrors = await runVariableFixChain(savedPath, first.errors, validateGenerated, llmConfig, lang);
|
|
457
538
|
warnings.push(...finalErrors);
|
|
458
|
-
const finalYaml = readFileSync(savedPath, 'utf-8').trim();
|
|
539
|
+
const finalYaml = finalizeBudget(readFileSync(savedPath, 'utf-8').trim(), options, llmConfig.provider, savedPath, warnings);
|
|
459
540
|
return { yaml: finalYaml, savedPath, relativePath, warnings };
|
|
460
541
|
}
|
|
542
|
+
/** budget 模式收尾:施加分档、写回盘、把说明加入 warnings(供 CLI 回显)。非 budget 原样返回。 */
|
|
543
|
+
function finalizeBudget(yamlText, options, provider, savedPath, warnings) {
|
|
544
|
+
if (!options.budget)
|
|
545
|
+
return yamlText;
|
|
546
|
+
const { yaml: out, note } = applyBudgetTiering(yamlText, provider);
|
|
547
|
+
if (note)
|
|
548
|
+
warnings.push(note);
|
|
549
|
+
if (out !== yamlText)
|
|
550
|
+
writeFileSync(savedPath, out + '\n', 'utf-8');
|
|
551
|
+
return out;
|
|
552
|
+
}
|
|
461
553
|
/**
|
|
462
554
|
* 确定性修复幻觉角色:对每个不存在的 role,从角色库找到"够接近"的真实路径,直接在 YAML 文本里替换。
|
|
463
555
|
* 不依赖再调一次 LLM——只要有可信匹配,就保证替换为库里真实存在、可运行的角色。
|
|
@@ -490,9 +582,24 @@ export function repairInvalidRolesInYaml(yamlPath, invalidRoles, validRolePaths)
|
|
|
490
582
|
* 返回最终仍未解决的 errors。
|
|
491
583
|
*/
|
|
492
584
|
async function runVariableFixChain(savedPath, initialErrors, validateGenerated, llmConfig, lang) {
|
|
493
|
-
|
|
585
|
+
// 除了"未定义的变量","depends_on 指向不存在的 step"(如 image 3 那类:LLM 编了个不存在
|
|
586
|
+
// 的 step id 当依赖)也是同一类"DAG 没搭对"的错误,同样应该进这条修复链,而不是被
|
|
587
|
+
// hasVarError 的窄判断漏掉、悄悄留在最终输出里。
|
|
588
|
+
const hasVarError = (errs) => errs.some(e => e.includes('未定义的变量') || e.includes('依赖不存在的 step') || e.toLowerCase().includes('undefined variable'));
|
|
494
589
|
if (!hasVarError(initialErrors))
|
|
495
590
|
return initialErrors;
|
|
591
|
+
// 阶段 0:确定性修复"变量名没错,只是引用它的 step 忘了把产出该变量的 step
|
|
592
|
+
// 加进 depends_on"——这是 compose 最常见的一类错误(LLM 设计 DAG 时漏连边,
|
|
593
|
+
// 变量名本身对得上),比改名更精确,优先做。
|
|
594
|
+
const depFix = await autoFixMissingDependsOn(savedPath);
|
|
595
|
+
if (depFix.fixed > 0) {
|
|
596
|
+
console.log(` 自动补上了 ${depFix.fixed} 处缺失的 depends_on:`);
|
|
597
|
+
for (const f of depFix.details)
|
|
598
|
+
console.log(` step "${f.step}" → depends_on 加入 "${f.addedDep}"`);
|
|
599
|
+
}
|
|
600
|
+
const afterDepFix = await validateGenerated(savedPath);
|
|
601
|
+
if (!hasVarError(afterDepFix.errors))
|
|
602
|
+
return afterDepFix.errors;
|
|
496
603
|
// 阶段 1: autoFix(启发式,只在 DAG 上游内替换)
|
|
497
604
|
const fixResult = await autoFixVariableRefs(savedPath);
|
|
498
605
|
if (fixResult.fixed > 0) {
|
|
@@ -529,6 +636,148 @@ function extractUndefinedVarNames(errors) {
|
|
|
529
636
|
}
|
|
530
637
|
return [...names];
|
|
531
638
|
}
|
|
639
|
+
/**
|
|
640
|
+
* 修复"变量名本身是对的,只是引用它的 step 忘了把产出该变量的 step 加进 depends_on"
|
|
641
|
+
* 这类错误——parser.ts 的 validateWorkflow 会专门标出这种情况("该变量由非上游 step
|
|
642
|
+
* 产出,需要把对应 step 加进 depends_on"),说明变量确实由某个 step 产出,只是 DAG
|
|
643
|
+
* 边没连上。直接在 YAML 里给该 step 补一条 depends_on,而不是像 autoFixVariableRefs
|
|
644
|
+
* 那样改名字(名字本来就没错)。
|
|
645
|
+
*
|
|
646
|
+
* 只在能安全定位 step 的文本块、且不会成环时才动手;识别不了的 YAML 形状(既不是
|
|
647
|
+
* `depends_on: [a, b]` 单行 flow 风格,也不是多行列表,也找不到 output 字段可插入)
|
|
648
|
+
* 就跳过,留给后面的 autoFixVariableRefs / LLM 修复兜底。
|
|
649
|
+
*/
|
|
650
|
+
export async function autoFixMissingDependsOn(yamlPath) {
|
|
651
|
+
const { parseWorkflow } = await import('../core/parser.js');
|
|
652
|
+
let workflow;
|
|
653
|
+
try {
|
|
654
|
+
workflow = parseWorkflow(yamlPath);
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
return { fixed: 0, details: [] };
|
|
658
|
+
}
|
|
659
|
+
const stepById = new Map();
|
|
660
|
+
for (const step of workflow.steps)
|
|
661
|
+
stepById.set(step.id, step);
|
|
662
|
+
function upstreamStepIds(stepId) {
|
|
663
|
+
const out = new Set();
|
|
664
|
+
const stack = [stepId];
|
|
665
|
+
while (stack.length > 0) {
|
|
666
|
+
const cur = stack.pop();
|
|
667
|
+
const s = stepById.get(cur);
|
|
668
|
+
if (!s)
|
|
669
|
+
continue;
|
|
670
|
+
for (const dep of s.depends_on || []) {
|
|
671
|
+
if (out.has(dep))
|
|
672
|
+
continue;
|
|
673
|
+
out.add(dep);
|
|
674
|
+
stack.push(dep);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return out;
|
|
678
|
+
}
|
|
679
|
+
// 按 "- id: xxx" 行切出每个 step 的文本块(含缩进),后续按顺序在同一份文本上打补丁。
|
|
680
|
+
//
|
|
681
|
+
// 关键坑:task 的自然语言描述里偶尔会出现示例 YAML 片段(如"参考格式:- id: xxx"),
|
|
682
|
+
// 这类假匹配不能只靠"id 是否真实存在"或"第一次出现"来过滤——假匹配完全可能引用一个
|
|
683
|
+
// 真实存在的 id(比如举例时提到了另一个 step 的 id),而且可能出现在真实定义之前。
|
|
684
|
+
// 唯一可靠的判据是 YAML 结构本身:"- id:" 只有在缩进等于 steps 列表项的缩进时才是
|
|
685
|
+
// 真正的 step 边界;task: | 块标量内部的内容缩进必然比这更深。所以先从 "steps:" 之后
|
|
686
|
+
// 第一个列表项算出这份文件真正的 step 缩进,再只认这个缩进层级的匹配。
|
|
687
|
+
const out0 = readFileSync(yamlPath, 'utf-8');
|
|
688
|
+
const stepsKeyMatch = out0.match(/^steps:\s*$/m);
|
|
689
|
+
const stepItemIndentMatch = stepsKeyMatch
|
|
690
|
+
? out0.slice(stepsKeyMatch.index + stepsKeyMatch[0].length).match(/^([ \t]*)-\s*id:/m)
|
|
691
|
+
: null;
|
|
692
|
+
const canonicalIndent = stepItemIndentMatch ? stepItemIndentMatch[1] : null;
|
|
693
|
+
// 找不到 "steps:" 或第一个 step 列表项(YAML 形状异常)——放弃这份确定性修复,交给后续兜底
|
|
694
|
+
if (canonicalIndent === null)
|
|
695
|
+
return { fixed: 0, details: [] };
|
|
696
|
+
const idLineRe = /^([ \t]*)-\s*id:\s*["']?([\w-]+)["']?.*$/gm;
|
|
697
|
+
const matches = [];
|
|
698
|
+
let m;
|
|
699
|
+
let out = out0;
|
|
700
|
+
while ((m = idLineRe.exec(out))) {
|
|
701
|
+
if (m[1] !== canonicalIndent)
|
|
702
|
+
continue; // 缩进不对:块标量内部的假匹配,跳过
|
|
703
|
+
matches.push({ id: m[2], indent: m[1], index: m.index });
|
|
704
|
+
}
|
|
705
|
+
const blocks = matches.map((cur, i) => ({
|
|
706
|
+
id: cur.id,
|
|
707
|
+
indent: cur.indent,
|
|
708
|
+
start: cur.index,
|
|
709
|
+
end: i + 1 < matches.length ? matches[i + 1].index : out.length,
|
|
710
|
+
}));
|
|
711
|
+
const details = [];
|
|
712
|
+
let offsetShift = 0;
|
|
713
|
+
for (const step of workflow.steps) {
|
|
714
|
+
const refs = step.task?.match(/\{\{(\w+)\}\}/g) || [];
|
|
715
|
+
if (refs.length === 0)
|
|
716
|
+
continue;
|
|
717
|
+
for (const ref of refs) {
|
|
718
|
+
const varName = ref.slice(2, -2);
|
|
719
|
+
const upOutputs = new Set();
|
|
720
|
+
for (const id of upstreamStepIds(step.id)) {
|
|
721
|
+
const s = stepById.get(id);
|
|
722
|
+
if (s?.output)
|
|
723
|
+
upOutputs.add(s.output);
|
|
724
|
+
}
|
|
725
|
+
if (upOutputs.has(varName))
|
|
726
|
+
continue;
|
|
727
|
+
const producer = workflow.steps.find((s) => s.output === varName);
|
|
728
|
+
if (!producer || producer.id === step.id)
|
|
729
|
+
continue;
|
|
730
|
+
if ((step.depends_on || []).includes(producer.id))
|
|
731
|
+
continue;
|
|
732
|
+
// 避免成环:producer 不能已经(间接)依赖当前 step
|
|
733
|
+
if (upstreamStepIds(producer.id).has(step.id))
|
|
734
|
+
continue;
|
|
735
|
+
const block = blocks.find(b => b.id === step.id);
|
|
736
|
+
if (!block)
|
|
737
|
+
continue;
|
|
738
|
+
const blockText = out.slice(block.start + offsetShift, block.end + offsetShift);
|
|
739
|
+
const patched = insertDependsOn(blockText, producer.id);
|
|
740
|
+
if (patched && patched !== blockText) {
|
|
741
|
+
out = out.slice(0, block.start + offsetShift) + patched + out.slice(block.end + offsetShift);
|
|
742
|
+
offsetShift += patched.length - blockText.length;
|
|
743
|
+
step.depends_on = [...(step.depends_on || []), producer.id];
|
|
744
|
+
details.push({ step: step.id, addedDep: producer.id });
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
if (details.length > 0)
|
|
749
|
+
writeFileSync(yamlPath, out, 'utf-8');
|
|
750
|
+
return { fixed: details.length, details };
|
|
751
|
+
}
|
|
752
|
+
/** 在单个 step 的文本块里插入一条 depends_on(支持单行 flow 风格 / 多行列表 / 完全没有该字段三种形状)。 */
|
|
753
|
+
function insertDependsOn(blockText, newDep) {
|
|
754
|
+
// Case A: 单行 flow 风格 `depends_on: [a, b]`(compose 生成的 YAML 里最常见的形态)
|
|
755
|
+
const flowRe = /^([ \t]*)depends_on:\s*\[([^\]]*)\](.*)$/m;
|
|
756
|
+
const flowMatch = blockText.match(flowRe);
|
|
757
|
+
if (flowMatch) {
|
|
758
|
+
const items = flowMatch[2].split(',').map(s => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
|
|
759
|
+
if (items.includes(newDep))
|
|
760
|
+
return blockText;
|
|
761
|
+
items.push(newDep);
|
|
762
|
+
return blockText.replace(flowRe, `${flowMatch[1]}depends_on: [${items.join(', ')}]${flowMatch[3]}`);
|
|
763
|
+
}
|
|
764
|
+
// Case B: 多行列表风格 `depends_on:\n - a\n - b`
|
|
765
|
+
const blockListRe = /^([ \t]*)depends_on:\s*\n((?:[ \t]*-\s*.+\n?)+)/m;
|
|
766
|
+
const blockListMatch = blockText.match(blockListRe);
|
|
767
|
+
if (blockListMatch) {
|
|
768
|
+
const itemIndentMatch = blockListMatch[2].match(/^([ \t]*)-/);
|
|
769
|
+
const itemIndent = itemIndentMatch ? itemIndentMatch[1] : blockListMatch[1] + ' ';
|
|
770
|
+
return blockText.replace(blockListRe, `${blockListMatch[0]}${itemIndent}- ${newDep}\n`);
|
|
771
|
+
}
|
|
772
|
+
// Case C: 完全没有 depends_on 字段 —— 插在 output 字段后面(每个 step 通常都有 output)
|
|
773
|
+
const outputRe = /^([ \t]*)output:\s*.+$/m;
|
|
774
|
+
const outputMatch = blockText.match(outputRe);
|
|
775
|
+
if (outputMatch) {
|
|
776
|
+
return blockText.replace(outputRe, `${outputMatch[0]}\n${outputMatch[1]}depends_on: [${newDep}]`);
|
|
777
|
+
}
|
|
778
|
+
// 找不到能安全插入的位置 —— 放弃,交给后续修复兜底
|
|
779
|
+
return null;
|
|
780
|
+
}
|
|
532
781
|
/**
|
|
533
782
|
* 自动修复 compose 生成 YAML 中的变量引用错误。
|
|
534
783
|
*
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* 各 provider 首次使用指南 —— 在 `ao init --provider X` 完成 .env 写入后打印,
|
|
3
3
|
* 告诉用户 provider 自身还需要做什么(OAuth 登录 / 拿 API key / 启本地服务 等)。
|
|
4
4
|
*/
|
|
5
|
+
import { API_PROVIDER_MAP } from '../connectors/api-providers.js';
|
|
5
6
|
export function getProviderGuide(provider, ctx) {
|
|
6
7
|
const p = provider.toLowerCase();
|
|
7
8
|
switch (p) {
|
|
@@ -79,6 +80,17 @@ export function getProviderGuide(provider, ctx) {
|
|
|
79
80
|
` → 再跑: ao init --provider claude --api-key sk-ant-xxx`,
|
|
80
81
|
].join('\n');
|
|
81
82
|
default: {
|
|
83
|
+
// 内置聚合 API(如 compshare/apinebula/agnes/rootflowai)—— base_url 已在
|
|
84
|
+
// api-providers.ts 写死,不需要用户再传 --base-url,只缺 key。
|
|
85
|
+
const spec = API_PROVIDER_MAP[p];
|
|
86
|
+
if (spec) {
|
|
87
|
+
return ctx.hasApiKey
|
|
88
|
+
? `✅ "${provider}" 已配置(base_url 已内置,无需 --base-url)。`
|
|
89
|
+
: [
|
|
90
|
+
`📋 "${provider}" 还缺 API key:`,
|
|
91
|
+
` → 再跑: ao init --provider ${provider} --api-key sk-xxx`,
|
|
92
|
+
].join('\n');
|
|
93
|
+
}
|
|
82
94
|
const lines = [];
|
|
83
95
|
lines.push(`📋 "${provider}" 按自定义 OpenAI 兼容端点处理:`);
|
|
84
96
|
if (!ctx.hasBaseUrl)
|