agency-orchestrator 0.7.5 → 0.8.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.en.md +63 -1
- package/README.md +85 -1
- package/dist/canvas/graph.d.ts +27 -0
- package/dist/canvas/graph.js +71 -0
- package/dist/cli/compare-report.d.ts +9 -0
- package/dist/cli/compare-report.js +25 -0
- package/dist/cli/install.d.ts +33 -0
- package/dist/cli/install.js +64 -0
- package/dist/cli/materialize.d.ts +24 -0
- package/dist/cli/materialize.js +131 -0
- package/dist/cli/parse-inputs.d.ts +11 -0
- package/dist/cli/parse-inputs.js +39 -0
- package/dist/cli/prompt.d.ts +82 -0
- package/dist/cli/prompt.js +219 -0
- package/dist/cli/team.d.ts +45 -0
- package/dist/cli/team.js +146 -0
- package/dist/cli.js +591 -39
- package/dist/connectors/claude.js +7 -1
- package/dist/connectors/factory.js +14 -0
- package/dist/connectors/openai-compatible.d.ts +7 -0
- package/dist/connectors/openai-compatible.js +137 -70
- package/dist/core/compare.d.ts +34 -0
- package/dist/core/compare.js +98 -0
- package/dist/core/executor.d.ts +3 -1
- package/dist/core/executor.js +48 -3
- package/dist/i18n.js +21 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +37 -2
- package/dist/output/reporter.js +20 -7
- package/dist/providers/detect.d.ts +9 -0
- package/dist/providers/detect.js +42 -0
- package/dist/skills/loader.d.ts +27 -0
- package/dist/skills/loader.js +120 -0
- package/dist/types.d.ts +2 -0
- package/dist/utils/paths.d.ts +12 -0
- package/dist/utils/paths.js +39 -0
- package/package.json +7 -5
- package/web/server.js +322 -14
- package/website/dist/_headers +11 -0
- package/website/dist/assets/Changelog-C-2NwXOa.js +8 -0
- package/website/dist/assets/CreativeLibrary-ByU75zJj.js +11 -0
- package/website/dist/assets/{Docs-Dq9F1i8N.js → Docs-upfORkRh.js} +2 -2
- package/website/dist/assets/Experts-DZcMYT0N.js +1 -0
- package/website/dist/assets/Home-C1iExJ44.js +26 -0
- package/website/dist/assets/{Markdown-BGaXFBRR.js → Markdown-Du-_jWYV.js} +1 -1
- package/website/dist/assets/{NotFound-9zocfwEa.js → NotFound-DFOhxeLB.js} +1 -1
- package/website/dist/assets/PromptStudio-CtdWODfK.js +11 -0
- package/website/dist/assets/SiteFooter-CsQnpWsM.js +1 -0
- package/website/dist/assets/Sponsors-Wm7wOV0d.js +26 -0
- package/website/dist/assets/Studio-C5ap-Sga.css +1 -0
- package/website/dist/assets/Studio-Db7R3YwK.js +146 -0
- package/website/dist/assets/{TutorialDetail--sFLZo_H.js → TutorialDetail-VrrsTGTl.js} +1 -1
- package/website/dist/assets/Tutorials-DGzE4QCM.js +6 -0
- package/website/dist/assets/{UsagePanel-CJxj63f8.js → UsagePanel-_z-wE9kf.js} +2 -2
- package/website/dist/assets/{arrow-left-BCbHUH6C.js → arrow-left-LNFkvuFe.js} +1 -1
- package/website/dist/assets/{arrow-up-right-mH56HSwU.js → arrow-up-right-DUa0xHYF.js} +1 -1
- package/website/dist/assets/{badge-DTvx4BWE.js → badge-DnXqkHQO.js} +1 -1
- package/website/dist/assets/{clock-DrQI-wLW.js → clock-DeAEswbe.js} +1 -1
- package/website/dist/assets/{copy-DhfqRzNp.js → copy-CEowRexz.js} +1 -1
- package/website/dist/assets/{copy-button-B3d3vxO8.js → copy-button-eOlzSbi7.js} +1 -1
- package/website/dist/assets/download-CdAgc0a_.js +6 -0
- package/website/dist/assets/external-link-g9VqtAuI.js +6 -0
- package/website/dist/assets/index-CA4ERbPo.css +1 -0
- package/website/dist/assets/index-D4qIfjfN.js +92 -0
- package/website/dist/assets/{mail-ymqqlkHO.js → mail-BQb9aJdA.js} +1 -1
- package/website/dist/assets/{search-VwbFwPwH.js → search-ouXlHBRS.js} +1 -1
- package/website/dist/assets/{sparkles-BjwxgAHu.js → sparkles-FGrgtgsi.js} +2 -7
- package/website/dist/assets/sponsors-DeXMAzHe.js +6 -0
- package/website/dist/assets/track-UypTCFjF.js +1 -0
- package/website/dist/assets/useBackend-DZu6HpVx.js +30 -0
- package/website/dist/assets/useSeo-_9wpTevc.js +1 -0
- package/website/dist/assets/vendor-charts-EdNYpIP7.js +68 -0
- package/website/dist/assets/{vendor-framer-motion-ZdeJPCY2.js → vendor-framer-motion-HzmZRvhs.js} +1 -1
- package/website/dist/assets/{vendor-radix-Bip5Pqfe.js → vendor-radix-5dK8e-Nk.js} +1 -1
- package/website/dist/assets/{vendor-react-DvXH3HTt.js → vendor-react-BYpU7-8G.js} +1 -1
- package/website/dist/assets/{users-BxUUvhSw.js → workflow-BvnziOG8.js} +7 -2
- package/website/dist/assets/workflows-OVJ6BxVu.js +1 -0
- package/website/dist/baidu_verify_codeva-uMqdeLJ0Jl.html +1 -0
- package/website/dist/index.html +27 -5
- package/website/dist/robots.txt +4 -0
- package/website/dist/sitemap.xml +15 -0
- package/website/dist/sponsors/banner-apinebula.jpeg +0 -0
- package/website/dist/sponsors/logo-apinebula-icon.png +0 -0
- package/workflows//350/275/257/344/273/266/345/274/200/345/217/221/346/240/207/345/207/206/346/265/201/347/250/213.yaml +103 -0
- package/workflows//351/234/200/346/261/202/350/275/254/351/241/271/347/233/256/350/204/232/346/211/213/346/236/266.yaml +59 -0
- package/website/dist/assets/Changelog-BGw7GDLg.js +0 -8
- package/website/dist/assets/Experts-IGM7T0ek.js +0 -1
- package/website/dist/assets/Home-hOdH4PIT.js +0 -31
- package/website/dist/assets/SiteFooter-BGeioLFz.js +0 -1
- package/website/dist/assets/Sponsors-Ci2XbWW9.js +0 -31
- package/website/dist/assets/Studio-WnGPl372.js +0 -128
- package/website/dist/assets/Tutorials-DvsHQADD.js +0 -6
- package/website/dist/assets/index-CO3Yi9rl.css +0 -1
- package/website/dist/assets/index-D6kBIE-F.js +0 -87
- package/website/dist/assets/sponsors-DNZC0gwp.js +0 -6
- package/website/dist/assets/vendor-charts-BiMa1Zmo.js +0 -68
package/README.en.md
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
> **Note:** `ao compose --run` auto-detects your language. Both 216 Chinese roles and 184 English roles ([agency-agents](https://github.com/msitarzewski/agency-agents), MIT) are **bundled in the npm package — no extra download needed**. **10 English workflow templates** are ready in `workflows/en/`.
|
|
15
15
|
|
|
16
16
|
> 📖 [Full Tutorial](https://dev.to/jnmetacode/agency-orchestrator-one-sentence-five-ai-agents-a-complete-plan-in-3-minutes-1ij6) — from install to real-world use in 10 minutes
|
|
17
|
+
>
|
|
18
|
+
> 📖 **Free companion learning** → [Learn AI coding from zero](https://aiolaola.com/?utm_source=github&utm_campaign=orchestrator): 180 free hands-on lessons + AI Coding Trilogy online + community · forever free
|
|
17
19
|
|
|
18
20
|
> If you find this useful, please **Star** it — helps others discover the project.
|
|
19
21
|
|
|
@@ -37,7 +39,7 @@ Prefer not to use the command line? Run `ao web` locally and pick experts, run w
|
|
|
37
39
|
<em>Workflows: run built-in templates with one click, or compare several</em>
|
|
38
40
|
</p>
|
|
39
41
|
|
|
40
|
-
> Launch: `ao web` (local — your API key stays on your machine, never uploaded). A [desktop client](
|
|
42
|
+
> Launch: `ao web` (local — your API key stays on your machine, never uploaded). A [desktop client download](https://github.com/jnMetaCode/agency-orchestrator/releases/latest) (Electron · macOS / Windows / Linux) is also available.
|
|
41
43
|
|
|
42
44
|
---
|
|
43
45
|
|
|
@@ -259,6 +261,12 @@ ao init --lang en # (Optional) Copy 184 English roles locally
|
|
|
259
261
|
ao init --workflow # Interactive workflow creator
|
|
260
262
|
ao compose "description" # AI-powered workflow generation
|
|
261
263
|
ao compose "description" --run # Generate AND execute in one command
|
|
264
|
+
ao team save <workflow.yaml> # Save a role line-up as a reusable team (Loadout)
|
|
265
|
+
ao team list / show / rm # Manage saved teams
|
|
266
|
+
ao run --team <name> "new task" # Run a new task with a saved team (locked line-up)
|
|
267
|
+
ao prompt optimize "<prompt>" # AI-optimize a prompt (--save to keep it reusable)
|
|
268
|
+
ao prompt test / list / garden # Test / manage / starter templates (prompt library)
|
|
269
|
+
ao skills [name] # List / view methodology skills to attach to steps
|
|
262
270
|
ao run <workflow.yaml> [options] # Execute workflow
|
|
263
271
|
ao validate <workflow.yaml> # Validate without running
|
|
264
272
|
ao plan <workflow.yaml> # Show execution plan (DAG)
|
|
@@ -293,6 +301,59 @@ The AI will:
|
|
|
293
301
|
|
|
294
302
|
Add `--run` to generate and execute in one command. Supports `--provider` and `--model` flags (default: DeepSeek).
|
|
295
303
|
|
|
304
|
+
### Teams / Loadouts (save a winning line-up and reuse it)
|
|
305
|
+
|
|
306
|
+
Every `compose` builds an ad-hoc team. When a line-up works well, **save it as a team** and apply it to any new task — a team stores only the *roles*, decoupled from any specific task:
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
# Extract the line-up from a workflow that worked well, save it as a team
|
|
310
|
+
ao team save workflows/tech-blog.yaml --name blog-crew
|
|
311
|
+
|
|
312
|
+
# Put the whole crew on a new task (re-designs the steps for those roles and runs)
|
|
313
|
+
ao run --team blog-crew "Write an explainer on the RISC-V architecture"
|
|
314
|
+
|
|
315
|
+
ao team list # List saved teams
|
|
316
|
+
ao team show blog-crew # Inspect the line-up
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
`ao run --team` simply **locks** the compose role catalog down to the team's roles — so nobody is dropped and no role is hallucinated. Teams live in `~/.ao/teams/*.team.yaml` (plain YAML, copy-to-share) and are **shared between the CLI and the web Studio** — pick roles in Studio, hit "Save as team", and `ao run --team` can use it immediately, and vice versa.
|
|
320
|
+
|
|
321
|
+
> Bring your own experts: set `AO_AGENTS_DIR=/your/roles/dir` and `run / compose / roles / web` all switch to your own role library.
|
|
322
|
+
>
|
|
323
|
+
> Fixed global dir: set `AO_HOME=~/.ao` (or any dir) so run outputs (`ao-output`) and generated workflows (`compose` / `--team`) land there instead of scattering across the CWD (#20). Or set `AO_OUTPUT_DIR` / `AO_WORKFLOWS_DIR` individually. Unset = unchanged (writes to current dir).
|
|
324
|
+
|
|
325
|
+
### Prompt Lab
|
|
326
|
+
|
|
327
|
+
Turn gut-feel prompts into assets you can optimize, test, compare, and save:
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
ao prompt optimize "write a tweet selling coffee" --save coffee-copy # AI rewrites it into a sharper prompt
|
|
331
|
+
ao prompt test "You are a translator, output only the translation" --mode system --input "good morning"
|
|
332
|
+
ao prompt list / show coffee-copy # saved prompts + version history
|
|
333
|
+
ao prompt garden # built-in starter templates
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
`--mode system|user` distinguishes role/system prompts from task prompts. Optimize only ever produces a **better prompt** (it never executes the prompt). The Studio "Prompts" tab adds side-by-side original-vs-optimized comparison **with AI scoring**. Stored in `~/.ao/prompts/` (`AO_PROMPTS_DIR` to override), shared between CLI and Studio.
|
|
337
|
+
|
|
338
|
+
### Skills (attach a methodology to a step)
|
|
339
|
+
|
|
340
|
+
Roles decide *who* does it; skills decide *how*. Attach a **skill** (a methodology playbook) to a workflow step and its method is injected into that step — e.g. make the review step follow a structured review method, or the implementation step do TDD:
|
|
341
|
+
|
|
342
|
+
```yaml
|
|
343
|
+
steps:
|
|
344
|
+
- id: review
|
|
345
|
+
role: "engineering/engineering-code-reviewer"
|
|
346
|
+
skill: "chinese-code-review" # single; or skills: ["test-driven-development", ...]
|
|
347
|
+
task: "Review this code {{code}}"
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
```bash
|
|
351
|
+
ao skills # list all available skills
|
|
352
|
+
ao skills test-driven-development # view a skill's methodology
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Skill content comes straight from the open-source [superpowers-zh](https://github.com/jnMetaCode/superpowers-zh) (MIT, 20 skills, bundled as a dependency — zero config). Or set `AO_SKILLS_DIR=/your/skills/dir` for your own.
|
|
356
|
+
|
|
296
357
|
### Resume & Iterate
|
|
297
358
|
|
|
298
359
|
Not happy with a step? No need to start over. `--resume` reloads previous outputs, `--from` specifies where to restart:
|
|
@@ -542,6 +603,7 @@ Your AI subscription ──→ agency-orchestrator ──→ 400+ expert roles c
|
|
|
542
603
|
| [ai-coding-guide](https://github.com/jnMetaCode/ai-coding-guide) | AI coding tools field guide (Chinese) — 66 Claude Code tips + 9 tools best practices |
|
|
543
604
|
| [superpowers-zh](https://github.com/jnMetaCode/superpowers-zh) | AI coding superpowers (Chinese) — 20 skills for Claude Code / Cursor |
|
|
544
605
|
| [shellward](https://github.com/jnMetaCode/shellward) | AI agent security middleware — prompt injection detection, DLP, command safety |
|
|
606
|
+
| [codepet](https://github.com/jnMetaCode/codepet) | Desktop pet that levels up as you code / use Claude Code — Electron, reads metadata only, local-first |
|
|
545
607
|
|
|
546
608
|
## Roadmap
|
|
547
609
|
|
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
**一句话出结果 · 216 个专业 AI 角色 · YAML 零代码 · 10 种大模型 · 支持 key(推荐 DeepSeek),也有 7 种免 key 方式**
|
|
13
13
|
|
|
14
14
|
> 📖 [完整上手教程](https://mp.weixin.qq.com/s/XcGbkMb6TM6NLQiL7ICwbw) — 从安装到实战,10 分钟上手
|
|
15
|
+
>
|
|
16
|
+
> 📖 **免费配套学习** → [从零学会 AI 编程](https://aiolaola.com/?utm_source=github&utm_campaign=orchestrator):180 节免费实操课 + 《AI 编程实战三卷书》在线阅读 + 实战社区 · 永久免费
|
|
15
17
|
|
|
16
18
|
> 觉得有用?请点个 **Star** — 帮助更多人发现这个项目。
|
|
17
19
|
|
|
@@ -25,6 +27,11 @@
|
|
|
25
27
|
|
|
26
28
|
不想敲命令行?本地跑一条 `ao web`,浏览器里勾选专家、运行工作流、查看产物、实时介入——全程图形界面,全中英双语。
|
|
27
29
|
|
|
30
|
+
> 🆕 **AI 自动组队**:不知道选哪些专家?角色页一句话、不选角色,AI 自动从全部专家里挑人组队并运行。
|
|
31
|
+
> 🆕 **可视化画布**:工作流可在画布上拖拽节点 / 连线(自动防环)/ 改任务·角色 / 保存,运行时节点按状态实时点亮。
|
|
32
|
+
> 🆕 **创意库**:内置图像生成提示词库(Nano Banana / Gemini,可搜索 / 分类 / 一键复制)。
|
|
33
|
+
> 🆕 **零配置首跑**:本机已登录 Claude Code / Gemini CLI 等?AO 自动探测并直接用,连 API key 都不用配。
|
|
34
|
+
|
|
28
35
|
<p align="center">
|
|
29
36
|
<img src="./docs/screenshots/studio-roles-zh.png" alt="Studio · 角色组队:从 200+ 专家里勾选,AI 自动合成团队" width="800"><br/>
|
|
30
37
|
<em>角色组队:从 200+ 专家里勾选,AI 自动合成团队并运行</em>
|
|
@@ -35,7 +42,7 @@
|
|
|
35
42
|
<em>工作流:内置模板一键运行,也能对比多个模板</em>
|
|
36
43
|
</p>
|
|
37
44
|
|
|
38
|
-
> 启动:`ao web`(本地,密钥只存你自己机器、绝不外传)。也有 [
|
|
45
|
+
> 启动:`ao web`(本地,密钥只存你自己机器、绝不外传)。也有 [桌面客户端下载](https://github.com/jnMetaCode/agency-orchestrator/releases/latest)(Electron · macOS / Windows / Linux)。
|
|
39
46
|
> 英文界面同样完整 → 见 [English README](./README.en.md)。
|
|
40
47
|
|
|
41
48
|
---
|
|
@@ -106,6 +113,11 @@ ao compose "设计一个 SaaS 产品的定价策略" --run #
|
|
|
106
113
|
npm install -g agency-orchestrator
|
|
107
114
|
```
|
|
108
115
|
|
|
116
|
+
> **装 CLI 还是桌面端?**
|
|
117
|
+
> - **桌面客户端**([下载](https://github.com/jnMetaCode/agency-orchestrator/releases/latest))**自带引擎与 Node,双击即用,无需** `npm i -g agency-orchestrator`。只有想在终端用 `ao` 命令、或接进脚本 / CI 时才需要装 CLI。
|
|
118
|
+
> - 用 `--provider claude-code`(或 `gemini-cli` / `codex-cli` 等)时,需要**本机已安装并登录对应 CLI**;AO 会自动探测已装的,零配置直接用。用 API key 类(deepseek/openai…)则配好 key 即可,无需装任何 CLI。
|
|
119
|
+
> - **自定义目录**:产物 / 数据目录用 `AO_DATA_DIR`(桌面端默认指向 userData),角色库用 `AO_AGENTS_DIR`,统一工作区用 `AO_HOME`。
|
|
120
|
+
|
|
109
121
|
### 第 2 步:一句话跑起来
|
|
110
122
|
|
|
111
123
|
```bash
|
|
@@ -268,6 +280,12 @@ ao init --lang en # (可选)复制 184 个英文角色到
|
|
|
268
280
|
ao init --workflow # 交互式创建工作流
|
|
269
281
|
ao compose "一句话描述" # AI 智能编排工作流
|
|
270
282
|
ao compose "一句话描述" --run # 编排并立即执行
|
|
283
|
+
ao team save <workflow.yaml> # 把角色阵容存成可复用团队 (Loadout)
|
|
284
|
+
ao team list / show / rm # 管理已保存的团队
|
|
285
|
+
ao run --team <名字> "新任务" # 用已保存的团队跑新任务(锁定阵容)
|
|
286
|
+
ao prompt optimize "提示词" # AI 优化提示词(--save 存为可复用资产)
|
|
287
|
+
ao prompt test / list / garden # 测试 / 管理 / 起手模板(提示词沉淀)
|
|
288
|
+
ao skills [名字] # 列出/查看可挂到步骤的方法论 skill
|
|
271
289
|
ao run <workflow.yaml> [选项] # 执行工作流
|
|
272
290
|
ao validate <workflow.yaml> # 校验(不执行)
|
|
273
291
|
ao plan <workflow.yaml> # 查看执行计划(DAG)
|
|
@@ -303,6 +321,59 @@ AI 会自动:
|
|
|
303
321
|
|
|
304
322
|
支持 `--provider` 和 `--model` 参数(默认使用 DeepSeek)。
|
|
305
323
|
|
|
324
|
+
### 团队 / Loadout(把跑得好的阵容存下来复用)
|
|
325
|
+
|
|
326
|
+
`compose` 每次都是临时组队。如果某个角色阵容效果好,把它**存成团队**,套到任意新任务上——团队只保存「角色阵容」,与具体任务解耦:
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
# 从一个跑得好的工作流抽出阵容,存成团队
|
|
330
|
+
ao team save workflows/tech-blog.yaml --name 技术博客组
|
|
331
|
+
|
|
332
|
+
# 让整队人接新活(自动用这几个角色重新设计步骤并运行)
|
|
333
|
+
ao run --team 技术博客组 "写一篇关于 RISC-V 架构的科普"
|
|
334
|
+
|
|
335
|
+
ao team list # 查看已保存的团队
|
|
336
|
+
ao team show 技术博客组 # 查看阵容构成
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
`ao run --team` 的本质 = compose 时把可选角色**锁定**为团队那几个,所以既不会漏人、也不会幻觉出别的角色。团队存在 `~/.ao/teams/*.team.yaml`(纯 YAML,可直接拷贝分享),**命令行和网页 Studio 共用同一份**——Studio 里勾选角色后点「存为团队」,命令行立刻 `ao run --team` 可用,反之亦然。
|
|
340
|
+
|
|
341
|
+
> 自带私有专家:设环境变量 `AO_AGENTS_DIR=/你的角色目录`,`run / compose / roles / web` 全部改用你自己的角色库。
|
|
342
|
+
>
|
|
343
|
+
> 固定全局目录:设 `AO_HOME=~/.ao`(或任意目录),运行产物 `ao-output`、`compose`/`--team` 生成的工作流都落到那里,不再随执行目录散落(#20)。也可用 `AO_OUTPUT_DIR` / `AO_WORKFLOWS_DIR` 单独指定。不设则维持原行为(写到当前目录)。
|
|
344
|
+
|
|
345
|
+
### 提示词优化(Prompt Lab)
|
|
346
|
+
|
|
347
|
+
把「靠感觉」的提示词,变成可优化、可测试、可对比、可沉淀的资产:
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
ao prompt optimize "帮我写个朋友圈文案卖咖啡" --save 咖啡文案 # AI 把它改写成更有效的提示词
|
|
351
|
+
ao prompt test "你是专业翻译,只输出译文" --mode system --input "good morning" # 用样例实跑看输出
|
|
352
|
+
ao prompt list / show 咖啡文案 # 已保存的提示词 + 版本历史
|
|
353
|
+
ao prompt garden # 内置起手模板
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
`--mode system|user` 区分「角色/系统提示词」和「任务提示词」。优化只产出**更好的提示词**(不会直接去执行它)。网页 Studio 的「提示词」页还能原版 vs 优化版**并排对比 + AI 评分**。存在 `~/.ao/prompts/`(`AO_PROMPTS_DIR` 可改),命令行与 Studio 共用。
|
|
357
|
+
|
|
358
|
+
### Skills(给步骤挂方法论)
|
|
359
|
+
|
|
360
|
+
角色决定「谁来做」,skill 决定「怎么做」。给工作流某一步挂一个 **skill**(流程剧本),它的方法论会注入该步——比如让代码审查步骤遵循结构化审查法、让实现步骤走 TDD:
|
|
361
|
+
|
|
362
|
+
```yaml
|
|
363
|
+
steps:
|
|
364
|
+
- id: review
|
|
365
|
+
role: "engineering/engineering-code-reviewer"
|
|
366
|
+
skill: "chinese-code-review" # 单个;或 skills: ["test-driven-development", ...]
|
|
367
|
+
task: "审查这段代码 {{code}}"
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
ao skills # 列出全部可用 skill
|
|
372
|
+
ao skills test-driven-development # 看某个 skill 的方法论
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
skill 内容直接用开源的 [superpowers-zh](https://github.com/jnMetaCode/superpowers-zh)(MIT,20 个,已作为依赖,零配置);也可设 `AO_SKILLS_DIR=/你的skill目录` 挂自己的。
|
|
376
|
+
|
|
306
377
|
### 迭代优化(Resume)
|
|
307
378
|
|
|
308
379
|
跑完一轮觉得某步不满意?不用从头来。`--resume` 加载上一轮输出,`--from` 指定从哪步重跑:
|
|
@@ -523,9 +594,18 @@ ao-output/产品需求评审-2026-03-22/
|
|
|
523
594
|
| [ai-coding-guide](https://github.com/jnMetaCode/ai-coding-guide) | 📖 实战教程 | 66 个 Claude Code 技巧 + 9 款工具最佳实践 + 配置模板 |
|
|
524
595
|
| [shellward](https://github.com/jnMetaCode/shellward) | 🛡️ 安全中间件 | 8 层防御 + DLP 数据流 + 注入检测,**零依赖**(含 MCP Server) |
|
|
525
596
|
| 🆕 [ai-shortfilm-prompts](https://github.com/jnMetaCode/ai-shortfilm-prompts) | 🎬 视频提示词 | Mx-Shell《丧尸清道夫》5 段式方法论 + Skill,Seedance / 小云雀 / Sora / 可灵 / 即梦通用 |
|
|
597
|
+
| 🆕 [codepet](https://github.com/jnMetaCode/codepet)  | 🐾 桌面宠物 | 挂在桌面的桌宠,你写代码 / 用 Claude Code 它就涨经验、升级、换状态、冒话(Electron · 仅读元数据,本地优先) |
|
|
526
598
|
|
|
527
599
|
### 交流
|
|
528
600
|
|
|
601
|
+
<table>
|
|
602
|
+
<tr>
|
|
603
|
+
<td width="170" align="center">
|
|
604
|
+
<img src="assets/qr-wechat.jpg" width="150" alt="微信公众号 AI不止语 二维码"><br>
|
|
605
|
+
<sub>微信扫码关注</sub>
|
|
606
|
+
</td>
|
|
607
|
+
<td>
|
|
608
|
+
|
|
529
609
|
微信公众号 **「AI不止语」**(微信搜索 `AI_BuZhiYu`)— 技术问答 · 项目更新 · 实战文章
|
|
530
610
|
|
|
531
611
|
| 渠道 | 加入方式 |
|
|
@@ -533,6 +613,10 @@ ao-output/产品需求评审-2026-03-22/
|
|
|
533
613
|
| QQ 2群 | [点击加入](https://qm.qq.com/q/EeNQA9xCxy)(群号 1071280067) |
|
|
534
614
|
| 微信群 | 关注公众号后回复「群」获取入群方式 |
|
|
535
615
|
|
|
616
|
+
</td>
|
|
617
|
+
</tr>
|
|
618
|
+
</table>
|
|
619
|
+
|
|
536
620
|
## 路线图
|
|
537
621
|
|
|
538
622
|
- [x] **v0.1** — YAML 工作流、DAG 引擎、4 个 LLM 连接器、CLI、实时输出
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface CanvasPosition {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
}
|
|
5
|
+
export interface CanvasGraphNode {
|
|
6
|
+
id: string;
|
|
7
|
+
position: CanvasPosition;
|
|
8
|
+
/** 该步骤的完整 YAML 定义(保真往返的关键:原样保留所有字段)。 */
|
|
9
|
+
data: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
export interface CanvasGraphEdge {
|
|
12
|
+
id: string;
|
|
13
|
+
source: string;
|
|
14
|
+
target: string;
|
|
15
|
+
}
|
|
16
|
+
export interface CanvasGraph {
|
|
17
|
+
name: string;
|
|
18
|
+
nodes: CanvasGraphNode[];
|
|
19
|
+
edges: CanvasGraphEdge[];
|
|
20
|
+
}
|
|
21
|
+
/** YAML 文本 → 画布 graph。坐标取 meta.layout,没有则给 {0,0}(前端用 dagre 自动布局)。 */
|
|
22
|
+
export declare function workflowToGraph(yamlText: string): CanvasGraph;
|
|
23
|
+
/**
|
|
24
|
+
* 画布 graph → YAML 文本。以 baseYamlText 为底保留顶层字段(name/agents_dir/llm/inputs…);
|
|
25
|
+
* steps 由 graph 节点重建(保留每步全部字段),depends_on 由连线重算,坐标写进 meta.layout。
|
|
26
|
+
*/
|
|
27
|
+
export declare function graphToWorkflow(graph: CanvasGraph, baseYamlText: string): string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// 工作流 YAML ↔ 画布 graph 的双向转换(可编辑画布的核心契约)。
|
|
2
|
+
//
|
|
3
|
+
// 设计原则(防 AI 开发头号风险「往返丢字段」):
|
|
4
|
+
// - graph 节点携带**完整的 step 原始对象**(node.data);
|
|
5
|
+
// - graphToWorkflow 只重算 depends_on(来自连线)和坐标(meta.layout),其余字段原样 dump;
|
|
6
|
+
// - 转换基于原始 YAML doc(不走会丢未知字段的 parseWorkflow),保留 name/agents_dir/llm/inputs 等。
|
|
7
|
+
// - 坐标存进 doc.meta.layout(引擎忽略,不影响可运行性 / 校验)。
|
|
8
|
+
//
|
|
9
|
+
// 这两个函数是纯函数(string in / string out + 朴素对象),便于单测往返。
|
|
10
|
+
import yaml from 'js-yaml';
|
|
11
|
+
/** YAML 文本 → 画布 graph。坐标取 meta.layout,没有则给 {0,0}(前端用 dagre 自动布局)。 */
|
|
12
|
+
export function workflowToGraph(yamlText) {
|
|
13
|
+
const doc = (yaml.load(yamlText) || {});
|
|
14
|
+
const steps = Array.isArray(doc.steps) ? doc.steps : [];
|
|
15
|
+
const meta = (doc.meta || {});
|
|
16
|
+
const layout = (meta.layout || {});
|
|
17
|
+
const nodes = steps.map((step) => {
|
|
18
|
+
const id = String(step.id);
|
|
19
|
+
const pos = layout[id];
|
|
20
|
+
return {
|
|
21
|
+
id,
|
|
22
|
+
position: pos && typeof pos.x === 'number' && typeof pos.y === 'number' ? { x: pos.x, y: pos.y } : { x: 0, y: 0 },
|
|
23
|
+
data: step,
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
const edges = [];
|
|
27
|
+
for (const step of steps) {
|
|
28
|
+
const target = String(step.id);
|
|
29
|
+
const deps = Array.isArray(step.depends_on) ? step.depends_on : [];
|
|
30
|
+
for (const dep of deps) {
|
|
31
|
+
const source = String(dep);
|
|
32
|
+
edges.push({ id: `${source}->${target}`, source, target });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { name: String(doc.name ?? 'workflow'), nodes, edges };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 画布 graph → YAML 文本。以 baseYamlText 为底保留顶层字段(name/agents_dir/llm/inputs…);
|
|
39
|
+
* steps 由 graph 节点重建(保留每步全部字段),depends_on 由连线重算,坐标写进 meta.layout。
|
|
40
|
+
*/
|
|
41
|
+
export function graphToWorkflow(graph, baseYamlText) {
|
|
42
|
+
const doc = (yaml.load(baseYamlText) || {});
|
|
43
|
+
// 入边表:target ← [sources],保持稳定顺序(按节点出现顺序)。
|
|
44
|
+
const incoming = new Map();
|
|
45
|
+
for (const node of graph.nodes)
|
|
46
|
+
incoming.set(node.id, []);
|
|
47
|
+
for (const edge of graph.edges) {
|
|
48
|
+
if (!incoming.has(edge.target))
|
|
49
|
+
incoming.set(edge.target, []);
|
|
50
|
+
incoming.get(edge.target).push(edge.source);
|
|
51
|
+
}
|
|
52
|
+
doc.steps = graph.nodes.map((node) => {
|
|
53
|
+
const step = { ...node.data, id: node.id };
|
|
54
|
+
const deps = incoming.get(node.id) ?? [];
|
|
55
|
+
if (deps.length > 0)
|
|
56
|
+
step.depends_on = deps;
|
|
57
|
+
else
|
|
58
|
+
delete step.depends_on; // 没有入边就不写空数组
|
|
59
|
+
return step;
|
|
60
|
+
});
|
|
61
|
+
// 坐标存进 meta.layout(引擎忽略)。
|
|
62
|
+
const meta = (doc.meta || {});
|
|
63
|
+
const layout = {};
|
|
64
|
+
for (const node of graph.nodes)
|
|
65
|
+
layout[node.id] = { x: Math.round(node.position.x), y: Math.round(node.position.y) };
|
|
66
|
+
meta.layout = layout;
|
|
67
|
+
doc.meta = meta;
|
|
68
|
+
if (graph.name)
|
|
69
|
+
doc.name = graph.name;
|
|
70
|
+
return yaml.dump(doc, { lineWidth: -1, noRefs: true });
|
|
71
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** `ao run --compare` 的终端报告格式化(纯函数,便于单测)。 */
|
|
2
|
+
import type { CompareVerdict } from '../core/compare.js';
|
|
3
|
+
export interface CompareReportInput {
|
|
4
|
+
multiOutput: string;
|
|
5
|
+
baselineOutput: string;
|
|
6
|
+
verdict: CompareVerdict | null;
|
|
7
|
+
}
|
|
8
|
+
/** 把对比结果格式化成终端可读的报告块。 */
|
|
9
|
+
export declare function formatCompareReport(r: CompareReportInput): string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const PREVIEW = 800;
|
|
2
|
+
const preview = (s) => (s.length > PREVIEW ? s.slice(0, PREVIEW) + '\n…[省略,完整产出见 ao-output]' : s);
|
|
3
|
+
/** 把对比结果格式化成终端可读的报告块。 */
|
|
4
|
+
export function formatCompareReport(r) {
|
|
5
|
+
const L = ['', '═'.repeat(50), ' 多智能体 vs 单次基线'];
|
|
6
|
+
if (!r.verdict) {
|
|
7
|
+
L.push(' ⚠️ 评审解析失败,无法给出可信评分(judge 未返回有效 JSON)。');
|
|
8
|
+
L.push(' 两份产出仍可人工对比:多智能体见上方 / ao-output,单次基线见下。');
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
const v = r.verdict;
|
|
12
|
+
const mark = v.winner === 'multi-agent' ? '✅ 多智能体胜' : v.winner === 'baseline' ? '❌ 单次基线胜' : '➖ 打平';
|
|
13
|
+
const conf = v.consistent ? '高可信' : '低可信(疑位置偏置)';
|
|
14
|
+
L.push(` 评审: 多智能体 ${v.multiScore.toFixed(1)} | 单次基线 ${v.baseScore.toFixed(1)} → ${mark}(${conf})`);
|
|
15
|
+
for (const reason of v.reasons)
|
|
16
|
+
if (reason)
|
|
17
|
+
L.push(` · ${reason}`);
|
|
18
|
+
}
|
|
19
|
+
L.push(` 产出长度: 多智能体 ${r.multiOutput.length} 字 / 单次基线 ${r.baselineOutput.length} 字`);
|
|
20
|
+
L.push('─'.repeat(50));
|
|
21
|
+
L.push(' 单次基线产出(多智能体产出见上方 / ao-output):');
|
|
22
|
+
L.push(preview(r.baselineOutput) || ' (空)');
|
|
23
|
+
L.push('═'.repeat(50), '');
|
|
24
|
+
return L.join('\n');
|
|
25
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface ToolTarget {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
/** 安装目标目录:user 级用 home,project 级用 cwd */
|
|
5
|
+
dest: (home: string, cwd: string) => string;
|
|
6
|
+
ext: string;
|
|
7
|
+
scope: 'user' | 'project';
|
|
8
|
+
}
|
|
9
|
+
/** 各编码工具的安装目标(参考 agency-agents-app 的 destination 表)。 */
|
|
10
|
+
export declare const INSTALL_TARGETS: Record<string, ToolTarget>;
|
|
11
|
+
export interface RoleFile {
|
|
12
|
+
rolePath: string;
|
|
13
|
+
category: string;
|
|
14
|
+
id: string;
|
|
15
|
+
absPath: string;
|
|
16
|
+
}
|
|
17
|
+
/** 递归收集角色 .md(只取带 `name:` frontmatter 的真角色,跳过 README/示例等)。 */
|
|
18
|
+
export declare function collectRoleFiles(agentsDir: string): RoleFile[];
|
|
19
|
+
export interface InstallResult {
|
|
20
|
+
installed: number;
|
|
21
|
+
destDir: string;
|
|
22
|
+
files: string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 把角色装进目标工具目录。文件名用 `<category>-<id><ext>` 防跨分类重名。
|
|
26
|
+
* 内容逐字复制(角色 .md 的 frontmatter 即各工具通用格式)。
|
|
27
|
+
*/
|
|
28
|
+
export declare function installRoles(agentsDir: string, target: ToolTarget, opts?: {
|
|
29
|
+
home: string;
|
|
30
|
+
cwd: string;
|
|
31
|
+
category?: string;
|
|
32
|
+
dryRun?: boolean;
|
|
33
|
+
}): InstallResult;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ao install --tool <tool>` —— 把内置 AI 角色装进各编码工具(Claude Code / Cursor 等)。
|
|
3
|
+
*
|
|
4
|
+
* 借鉴 agency-agents-app 的核心能力,但聚焦 AO 生态:中文角色库(agency-agents-zh)是空位,
|
|
5
|
+
* 让"只在编码工具里用角色、不跑工作流"的用户也能一键装入。角色 .md 的 frontmatter
|
|
6
|
+
* (name/description) 本就是 Claude Code agent 格式,多数工具可直接复制。
|
|
7
|
+
*/
|
|
8
|
+
import { readdirSync, statSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
9
|
+
import { join, resolve } from 'node:path';
|
|
10
|
+
/** 各编码工具的安装目标(参考 agency-agents-app 的 destination 表)。 */
|
|
11
|
+
export const INSTALL_TARGETS = {
|
|
12
|
+
'claude-code': { id: 'claude-code', label: 'Claude Code', dest: (h) => join(h, '.claude', 'agents'), ext: '.md', scope: 'user' },
|
|
13
|
+
'copilot': { id: 'copilot', label: 'GitHub Copilot', dest: (h) => join(h, '.github', 'agents'), ext: '.md', scope: 'user' },
|
|
14
|
+
'gemini-cli': { id: 'gemini-cli', label: 'Gemini CLI', dest: (h) => join(h, '.gemini', 'agents'), ext: '.md', scope: 'user' },
|
|
15
|
+
'qwen': { id: 'qwen', label: 'Qwen Code', dest: (h) => join(h, '.qwen', 'agents'), ext: '.md', scope: 'user' },
|
|
16
|
+
'cursor': { id: 'cursor', label: 'Cursor', dest: (_h, cwd) => join(cwd, '.cursor', 'rules'), ext: '.mdc', scope: 'project' },
|
|
17
|
+
'opencode': { id: 'opencode', label: 'opencode', dest: (_h, cwd) => join(cwd, '.opencode', 'agents'), ext: '.md', scope: 'project' },
|
|
18
|
+
};
|
|
19
|
+
const SKIP_DIRS = new Set(['node_modules', 'scripts', 'integrations', 'examples', '.git']);
|
|
20
|
+
/** 递归收集角色 .md(只取带 `name:` frontmatter 的真角色,跳过 README/示例等)。 */
|
|
21
|
+
export function collectRoleFiles(agentsDir) {
|
|
22
|
+
const root = resolve(agentsDir);
|
|
23
|
+
const out = [];
|
|
24
|
+
const walk = (dir, category) => {
|
|
25
|
+
for (const entry of readdirSync(dir)) {
|
|
26
|
+
if (SKIP_DIRS.has(entry))
|
|
27
|
+
continue;
|
|
28
|
+
const full = join(dir, entry);
|
|
29
|
+
const st = statSync(full);
|
|
30
|
+
if (st.isDirectory()) {
|
|
31
|
+
walk(full, category ? `${category}/${entry}` : entry); // 用完整相对目录作 category,避免嵌套分类重名
|
|
32
|
+
}
|
|
33
|
+
else if (entry.endsWith('.md') && !/^(README|CONTRIBUTING|LICENSE|SECURITY)/i.test(entry)) {
|
|
34
|
+
const head = readFileSync(full, 'utf-8').slice(0, 400);
|
|
35
|
+
if (!/^---[\s\S]*?\bname\s*:/.test(head))
|
|
36
|
+
continue; // 必须有 name frontmatter
|
|
37
|
+
const id = entry.replace(/\.md$/, '');
|
|
38
|
+
out.push({ rolePath: `${category}/${id}`, category: category || 'other', id, absPath: full });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
walk(root, '');
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 把角色装进目标工具目录。文件名用 `<category>-<id><ext>` 防跨分类重名。
|
|
47
|
+
* 内容逐字复制(角色 .md 的 frontmatter 即各工具通用格式)。
|
|
48
|
+
*/
|
|
49
|
+
export function installRoles(agentsDir, target, opts = { home: '', cwd: '' }) {
|
|
50
|
+
const roles = collectRoleFiles(agentsDir).filter((r) => !opts.category || r.category === opts.category);
|
|
51
|
+
const destDir = target.dest(opts.home, opts.cwd);
|
|
52
|
+
const files = [];
|
|
53
|
+
if (!opts.dryRun && roles.length)
|
|
54
|
+
mkdirSync(destDir, { recursive: true });
|
|
55
|
+
for (const r of roles) {
|
|
56
|
+
// 文件名用完整分类路径拍平(a/b → a-b),避免嵌套分类下同名角色互相覆盖
|
|
57
|
+
const outName = `${r.category.replace(/\//g, '-')}-${r.id}${target.ext}`;
|
|
58
|
+
const outPath = join(destDir, outName);
|
|
59
|
+
if (!opts.dryRun)
|
|
60
|
+
writeFileSync(outPath, readFileSync(r.absPath, 'utf-8'), 'utf-8');
|
|
61
|
+
files.push(outName);
|
|
62
|
+
}
|
|
63
|
+
return { installed: files.length, destDir, files };
|
|
64
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { WorkflowResult } from '../types.js';
|
|
2
|
+
export interface FileBlock {
|
|
3
|
+
path: string;
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* 解析文本里的"文件块"。返回 [{path, content}],按出现顺序;同路径后者覆盖前者。
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseFileBlocks(text: string): FileBlock[];
|
|
10
|
+
/**
|
|
11
|
+
* 把候选路径规整成相对目标目录的安全路径;非法(绝对路径 / .. 逃逸 / 越界)返回 null。
|
|
12
|
+
*/
|
|
13
|
+
export declare function safeRelPath(candidate: string, destDir: string): string | null;
|
|
14
|
+
export interface MaterializeResult {
|
|
15
|
+
destDir: string;
|
|
16
|
+
stepId: string | null;
|
|
17
|
+
files: string[];
|
|
18
|
+
skipped: string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 从工作流结果落盘:从后往前扫已完成步的产出,取第一个含文件块的步,把其文件写到 destDir。
|
|
22
|
+
* 这样 verify 等收尾步在最后也没事——会自动回退到真正出代码的 build 步。
|
|
23
|
+
*/
|
|
24
|
+
export declare function materializeFromResult(result: WorkflowResult, destDir: string): MaterializeResult;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ao run --materialize <dir>` 核心:把工作流(开发步)产出里的"文件块"落盘成真实项目脚手架。
|
|
3
|
+
*
|
|
4
|
+
* 约定格式(喂给开发角色,解析器主认这个 + 容错):
|
|
5
|
+
* ### path/to/file.ext ← 标题行给出文件路径(# / ** / `` 包裹均可)
|
|
6
|
+
* ```lang
|
|
7
|
+
* <文件内容>
|
|
8
|
+
* ```
|
|
9
|
+
* 也兼容围栏信息串带路径:```ts path=src/x.ts
|
|
10
|
+
*
|
|
11
|
+
* 安全:拒绝绝对路径、`..` 逃逸、越出目标目录的路径(由 safeRelPath 把关)。
|
|
12
|
+
*/
|
|
13
|
+
import { resolve, sep, normalize, join, dirname } from 'node:path';
|
|
14
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
15
|
+
/** 一行去掉 markdown 标记后,是否本身就是一个"看起来像路径"的 token。 */
|
|
16
|
+
function pathFromHeadingLine(line) {
|
|
17
|
+
// 去掉标题 #、加粗 **、行内代码 `、列表符号、首尾空白与结尾冒号
|
|
18
|
+
const stripped = line
|
|
19
|
+
.replace(/^[\s>]*#{1,6}\s*/, '')
|
|
20
|
+
.replace(/\*\*/g, '')
|
|
21
|
+
.replace(/`/g, '')
|
|
22
|
+
.replace(/^[-*]\s+/, '')
|
|
23
|
+
.replace(/[::]\s*$/, '')
|
|
24
|
+
.trim();
|
|
25
|
+
// 必须整行就是一个路径:含 . 扩展名或 /,且只由路径合法字符组成
|
|
26
|
+
if (/^[\w.\-]+(\/[\w.\-]+)*$/.test(stripped) && (stripped.includes('.') || stripped.includes('/'))) {
|
|
27
|
+
return stripped;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
/** 从围栏信息串里取路径:```ts path=src/x.ts 或 ```ts file=src/x.ts 或 ```src/x.ts */
|
|
32
|
+
function pathFromFenceInfo(info) {
|
|
33
|
+
const m = info.match(/(?:path|file)\s*=\s*([\w.\-/]+)/i);
|
|
34
|
+
if (m)
|
|
35
|
+
return m[1];
|
|
36
|
+
const tok = info.trim().split(/\s+/)[0] || '';
|
|
37
|
+
// 信息串第一个 token 本身是路径(含 . 或 /),而非单纯语言名
|
|
38
|
+
if ((tok.includes('/') || /\.[a-zA-Z0-9]+$/.test(tok)) && /^[\w.\-/]+$/.test(tok))
|
|
39
|
+
return tok;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 解析文本里的"文件块"。返回 [{path, content}],按出现顺序;同路径后者覆盖前者。
|
|
44
|
+
*/
|
|
45
|
+
export function parseFileBlocks(text) {
|
|
46
|
+
const lines = text.split(/\r?\n/); // 容忍 CRLF:否则尾随 \r 会让围栏正则匹配不上、整体失效
|
|
47
|
+
const blocks = [];
|
|
48
|
+
let lastNonBlank = ''; // 紧邻围栏的上一非空行(用于取标题路径)
|
|
49
|
+
for (let i = 0; i < lines.length; i++) {
|
|
50
|
+
const fence = lines[i].match(/^(\s*)(`{3,}|~{3,})(.*)$/);
|
|
51
|
+
if (!fence) {
|
|
52
|
+
if (lines[i].trim())
|
|
53
|
+
lastNonBlank = lines[i];
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
// 进入围栏:闭合围栏的长度必须 ≥ 开围栏(这样 ```` 外层可包裹含 ``` 的文件,如 README)
|
|
57
|
+
const marker = fence[2][0];
|
|
58
|
+
const openLen = fence[2].length;
|
|
59
|
+
const closeRe = new RegExp(`^\\s*[${marker}]{${openLen},}\\s*$`);
|
|
60
|
+
const info = fence[3] || '';
|
|
61
|
+
const body = [];
|
|
62
|
+
let closed = false;
|
|
63
|
+
let j = i + 1;
|
|
64
|
+
for (; j < lines.length; j++) {
|
|
65
|
+
if (closeRe.test(lines[j])) {
|
|
66
|
+
closed = true;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
body.push(lines[j]);
|
|
70
|
+
}
|
|
71
|
+
const path = pathFromFenceInfo(info) || pathFromHeadingLine(lastNonBlank);
|
|
72
|
+
if (path)
|
|
73
|
+
blocks.push({ path, content: body.join('\n') });
|
|
74
|
+
// 跳到闭合围栏之后;围栏块不更新 lastNonBlank
|
|
75
|
+
i = closed ? j : lines.length;
|
|
76
|
+
lastNonBlank = '';
|
|
77
|
+
}
|
|
78
|
+
// 同路径去重:保留最后一次
|
|
79
|
+
const byPath = new Map();
|
|
80
|
+
for (const b of blocks)
|
|
81
|
+
byPath.set(b.path, b.content);
|
|
82
|
+
return Array.from(byPath, ([path, content]) => ({ path, content }));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 把候选路径规整成相对目标目录的安全路径;非法(绝对路径 / .. 逃逸 / 越界)返回 null。
|
|
86
|
+
*/
|
|
87
|
+
export function safeRelPath(candidate, destDir) {
|
|
88
|
+
let p = candidate.trim().replace(/^\.\//, '');
|
|
89
|
+
if (!p || p.startsWith('/') || p.startsWith('~') || p.startsWith('\\') || /^[a-zA-Z]:/.test(p))
|
|
90
|
+
return null;
|
|
91
|
+
const rel = normalize(p);
|
|
92
|
+
if (rel.startsWith('..' + sep) || rel === '..' || rel.includes(sep + '..' + sep))
|
|
93
|
+
return null;
|
|
94
|
+
if (rel === '.' || rel === '')
|
|
95
|
+
return null; // 规整后指向目标根本身(如 "foo/..")→ 拒绝,避免写到目录上 EISDIR
|
|
96
|
+
const full = resolve(destDir, rel);
|
|
97
|
+
const base = resolve(destDir);
|
|
98
|
+
if (full !== base && !full.startsWith(base + sep))
|
|
99
|
+
return null;
|
|
100
|
+
return rel;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 从工作流结果落盘:从后往前扫已完成步的产出,取第一个含文件块的步,把其文件写到 destDir。
|
|
104
|
+
* 这样 verify 等收尾步在最后也没事——会自动回退到真正出代码的 build 步。
|
|
105
|
+
*/
|
|
106
|
+
export function materializeFromResult(result, destDir) {
|
|
107
|
+
const out = { destDir, stepId: null, files: [], skipped: [] };
|
|
108
|
+
const steps = result.steps.filter((s) => s.status === 'completed' && s.output);
|
|
109
|
+
// 取"文件块最多"的步:build 出整个项目(多文件),verify 顶多顺带 1 个测试,不会盖过 build。
|
|
110
|
+
// 并列时保留靠前的步(build 通常在 verify 之前)。
|
|
111
|
+
let bestBlocks = [];
|
|
112
|
+
for (const s of steps) {
|
|
113
|
+
const blocks = parseFileBlocks(String(s.output));
|
|
114
|
+
if (blocks.length > bestBlocks.length) {
|
|
115
|
+
bestBlocks = blocks;
|
|
116
|
+
out.stepId = s.id;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const b of bestBlocks) {
|
|
120
|
+
const rel = safeRelPath(b.path, destDir);
|
|
121
|
+
if (!rel) {
|
|
122
|
+
out.skipped.push(b.path);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const full = join(destDir, rel);
|
|
126
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
127
|
+
writeFileSync(full, b.content.endsWith('\n') ? b.content : b.content + '\n', 'utf-8');
|
|
128
|
+
out.files.push(rel);
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 解析 `--input key=value` / `-i key=@file` 参数。
|
|
3
|
+
* argv 形如 process.argv.slice(2)(即包含命令与工作流文件,故从下标 2 起扫描)。
|
|
4
|
+
*
|
|
5
|
+
* 安全:`@file` 会读取本机文件作为输入值。网页 Studio 把请求里的 input 原样拼成 `-i k=v`,
|
|
6
|
+
* 若放行 @file,任意 CSRF/同机来源即可用 `@/path` 读取本机文件(如保存的 API key)经 LLM
|
|
7
|
+
* 输出回传。故网页入口设置 `AO_NO_AT_FILE=1` 关闭展开,此时 `@` 值按字面字符串处理。
|
|
8
|
+
*
|
|
9
|
+
* onError 由调用方提供(CLI 走 console.error + process.exit;测试可抛异常)。
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseInputPairs(argv: string[], onError: (msg: string) => never): Record<string, string>;
|