diff --git a/README.md b/README.md index dccf97f..04f66fd 100644 --- a/README.md +++ b/README.md @@ -86,9 +86,15 @@ python3 scripts/new_project.py --repo owner/name # 2. 手工填档案五维度 —— 重点填 risk.claim_vs_reality $EDITOR data/projects/.yaml -# 并把它加进 data/registry.yaml +# ⚠️ 补完时**不要整块覆盖**下面这三个块,它们由脚本回写: +# ecosystem.metrics(fetch_stats 写) +# mirror (mirror_sync 写) +# article (publish_emlog 写) +# 整块替换 ecosystem 会把 metrics 一起删掉,表现为矩阵里指标静默变空。 +# 真踩了也别慌:fetch_stats 用 upsert_block 会自动把 metrics 长回来 +# (python3 scripts/fetch_stats.py --id --force)。 -# 3. 刷新指标 +# 3. 刷新指标(放在填档之后,别反过来) python3 scripts/fetch_stats.py --id # 4. 镜像源码(按许可证等级自动决定 private / public) @@ -111,8 +117,35 @@ git add -A && git commit -m "track: " && git push ### ⚠️ 更新总纲文时必读 emlog 的 update 接口**不传 `--content` 就会清空正文**。 -`publish_emlog.py` 已内置保护(update 前先 `detail` 取回全文), -但如果你手工调 emlog 脚本更新总纲,**必须先 detail 再 update**。 + +保护点应该是「**本地正文非空**」,而不是「update 前先 detail 取回线上全文」—— +本脚本的 content 永远来自本地 Markdown 源文件,不存在"忘了传"的情况; +反过来,emlog 的 detail 接口**查不到草稿**,如果把保护建立在 detail 上, +草稿就永远无法转正。所以 `publish_emlog.py` 的判据是:本地正文非空即放行, +detail 只作为可选的健全性检查。 + +如果你手工调 emlog 脚本更新总纲,记住同一件事:**确保传了 `--content`**。 + +### 两类文章,ID 落点不同 + +`publish_emlog.py` 按「有没有同名项目档案」自动分流,**不需要手工指定**: + +| 文章类型 | 文件名 | ID 落点 | +| --- | --- | --- | +| 项目深度文 | `articles/.md`(有 `data/projects/.yaml`) | 档案的 `article` 块 | +| 跨项目文章 | `articles/00-*.md`(总纲、横评、专题) | `articles/.meta.yaml` | + +新增一篇 `00-` 开头的文章不用做任何额外配置,发完就有 meta。 +反过来,如果某天需要给跨项目文章单独建档,只要 YAML 一出现,脚本会改走档案路径—— +先按「是不是项目文」分流,再决定写哪儿。 + +### 文章索引不需要手工维护 + +总纲里 `` 之间的内容由 `build_matrix.py` 生成, +事实源是两处:项目档案的 `article` 块 + `articles/*.meta.yaml`。 + +这段原本是手抄的,结果横评文发了、索引里却没有它——**手写的索引每发一篇文章就漂移一次**。 +现在只需要在发完文章后重跑 `build_matrix.py`,没深度文的项目会自动标注「待发布」并指向现有横评。 --- @@ -122,9 +155,9 @@ emlog 的 update 接口**不传 `--content` 就会清空正文**。 | --- | --- | --- | | `new_project.py` | 建档 + 自动判许可证等级 | API 失败时生成未定级骨架,要求人工填 | | `fetch_stats.py` | 抓指标 + 累积时间序列快照 | 无 `GITHUB_TOKEN` 则匿名降级;当日缓存;失败沿用上次快照并标 `stale` | -| `build_matrix.py` | 生成对比矩阵 + 赛道图 / 象限图 | 项目 <4 个出分层图,≥4 个才出象限图 | +| `build_matrix.py` | 生成对比矩阵 + 赛道图 / 象限图 + 文章索引 | 项目 <4 个出分层图,≥4 个才出象限图;索引缺标记则跳过并警告 | | `mirror_sync.sh` | 源码镜像同步 | 大仓(>300MB)提示并支持 treeless clone 降级 | -| `publish_emlog.py` | emlog 发布 / 更新 | update 前强制取回全文防空内容 | +| `publish_emlog.py` | emlog 发布 / 更新 | 本地正文为空时拒绝 update(防清空线上) | --- @@ -132,6 +165,17 @@ emlog 的 update 接口**不传 `--content` 就会清空正文**。 - **GitHub API 限额**:无 `GITHUB_TOKEN` 时匿名限额 60 次/小时。 项目数超过约 20 个后批量刷新会受限,建议配置 token。 -- **磁盘占用**:源码镜像是全量裸仓。10 个量级约需数 GB, +- **GitHub 下载带宽**:本机到 GitHub 实测约 **50 KB/s**(走沙箱代理 56 KB/s, + 直连 27 KB/s 且 github.com 常被阻断)。591 MB 的仓全量镜像需要 3 小时以上。 + 大仓一律先 `--depth 1`,后续再用脚本的增量加深补齐。 +- **Gitea 推送上限**:前置 nginx 的 `client_max_body_size` 约 **50 MB** + (实测 48 MB 放行、64 MB 拒绝)。超限会在传完所有对象后才收到 HTTP 413。 + 脚本内置体积闸门,超限自动降级为「源码快照」(剔除非源码素材,通常能减一半以上)。 + 彻底解法是在服务器 nginx 配置里放开该值。 +- **浅仓不能直接推送**:Git 服务端默认 `receive.shallowUpdate = deny`, + 推浅仓会被拒。脚本在推送前自动 `--unshallow` 补齐历史; + 若补齐失败则降级为快照(丢历史但保住代码)。 +- **磁盘占用**:源码镜像是全量裸仓。4 个项目已占约 800 MB。 大仓可用 `--filter=blob:none` 做 treeless 镜像。 - **象限图阈值**:样本 <4 个时象限图没有统计意义,此阶段只出赛道分层图。 + 4 个也只是示意,不构成统计结论。 diff --git a/articles/00-comparison.md b/articles/00-comparison.md new file mode 100644 index 0000000..554658a --- /dev/null +++ b/articles/00-comparison.md @@ -0,0 +1,413 @@ +# 四个 Agent 工作台的路线分野:把「多引擎中立」拆开看 + +> **速览** +> | 项 | iPolloWork | OpenWork | Orca | Maka | +> | --- | --- | --- | --- | --- | +> | 仓库 | Devin-AXIS/iPolloWork | different-ai/openwork | stablyai/orca | apache/maka | +> | Star | 5,201 | 23,253 | 58,771 | 4,373 | +> | Fork / Issues | 976 / 80 | 2,307 / 414 | 3,987 / 5,000 | 406 / 398 | +> | 主语言 | TypeScript 49.5% | TypeScript 89.9% | TypeScript 97.0% | TypeScript 91.7% | +> | 仓库体积 | 591 MB | 291 MB | 671 MB | 100 MB | +> | 最新版本 | v0.50.12 | v0.18.40 | v1.4.194 | v0.2.0-dev.9 | +> | 许可证 | Source Available(L3) | MIT + EE 切分(L3) | MIT(L1) | Apache-2.0(L1) | +> | 建仓 | 2025-08-25 | 2026-01-14 | 2026-03-17 | 2026-05-27 | +> +> **一句话结论**:四个项目都在「Agent 工作台」这个筐里,但争的完全不是同一件事—— +> iPolloWork 争入口,OpenWork 争能力分发,Orca 争并行规模,Maka 争可审计性。 +> 把它们的「多引擎中立」拆开看,会发现这根本是四个不同的词。 + +--- + +## 0. 先给结论 + +这个追踪系列的第一个样本是 iPolloWork。只有它的时候,我判断它是「桌面工作台层的一个多引擎整合尝试」。 +样本增加到四个之后,这个判断需要修正:**「桌面工作台」不是一个赛道,是四条不同的路。** + +- **Orca(58.8k★)** 是人气第一,但它的 5,000 个 open issue 里绝大多数是开发者自己开的任务卡—— + 最近 100 次提交中 `nwparker` 独占 79 次。广度做到极致,治理还是草稿。 +- **OpenWork(23.3k★)** 是工程治理最好的一个,但它和自称要替代的 Claude Cowork 不是一个物种—— + 它的主体是一个只暴露两个工具的 MCP 服务,桌面 app 是可选的。 +- **Maka(4.4k★)** 是唯一自带评测框架、唯一把「执行记录」当地基的项目, + 但也是唯一没有任何正式 release、只支持 Apple Silicon Mac 的项目。 +- **iPolloWork(5.2k★)** 是唯一把交付物从代码延伸到 PPT / 网页 / 设计 / 视频的, + 但它的三种引擎接入深度并不齐整。 + +最值得记住的一点是:**许可证的开放程度,完全不能预测项目的治理质量。** +Orca 用最开放的 MIT,治理最弱;OpenWork 用最受限制的混合许可,工程规范最强。 + +--- + +## 1. 从 1 个样本到 4 个:为什么现在能出象限图了 + +只有一个样本的时候,任何对比都是过度抽象——你不知道一个坐标点代表的是什么水平。 +四个样本足够看出**分布**,但还不足以看出**规律**。所以这一篇是横向对比,不是最终结论。 + +四个项目都满足同一个筛选条件:**面向真实工作的、可在本地运行的 Agent 工作台**, +而不是某个 agent 的 demo 或 SDK。这也是本系列的收录标准。 + +先看它们的硬指标分布(数据日期 2026-09-01,全部来自 GitHub API 实测): + +| 项目 | Star | 建仓 | 距今天数 | 日均 Star | +| --- | --- | --- | --- | --- | +| iPolloWork | 5,201 | 2025-08-25 | 372 天 | 14.0 | +| OpenWork | 23,253 | 2026-01-14 | 230 天 | 101.1 | +| Orca | 58,771 | 2026-03-17 | 168 天 | 349.8 | +| Maka | 4,373 | 2026-05-27 | 97 天 | 45.1 | + +Orca 的增速是第二名的三倍多。但 Star 增速和成熟度从来不是一回事—— +Orca 的版本号 `v1.4.194` 泄露了另一面:一个 1.x 版本号跑到第 194 个补丁版本, +说明日均不止一次发版。 + +--- + +## 2. 赛道分层:它们在争什么 + +按 Agent 工具的三层栈来定位: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 第 3 层 · 桌面工作台层 │ +│ │ +│ iPolloWork ──────► 争「入口」:把多引擎收进同一界面 │ +│ Orca ────────────► 争「规模」:并行 N 个 agent 各自 worktree │ +│ Maka ────────────► 争「可信」:执行记录 + 可复现评测 │ +└─────────────────────────────────────────────────────────────┘ + ▲ 消费 +┌─────────────────────────────────────────────────────────────┐ +│ 第 2 层 · 能力分发层 │ +│ │ +│ OpenWork ────────► 争「复用」:一次创建的能力处处可用 │ +│ (不接管执行,只提供 capabilities) │ +└─────────────────────────────────────────────────────────────┘ + ▲ 依赖 +┌─────────────────────────────────────────────────────────────┐ +│ 第 1 层 · 执行引擎层 │ +│ Codex · Claude Code · OpenCode · DSH · Pi · Kimi · ... │ +└─────────────────────────────────────────────────────────────┘ +``` + +**关键观察**:OpenWork 不在这条纵轴上跟其他三个竞争,它横插在中间做「能力层」。 +Maka 也不完全在第 3 层——它自带 Runtime,实际上从第 1 层一路做到第 3 层。 + +所以严格说,**只有 iPolloWork 和 Orca 是正面竞品**, +OpenWork 和 Maka 是从不同方向切进来的。这也解释了为什么功能对比表对它们意义不大。 + +--- + +## 3. 业务维度:四条完全不同的变现路径 + +| 项目 | 变现方式 | 免费边界 | 商业意图 | +| --- | --- | --- | --- | +| **iPolloWork** | 双轨:个人与 <3 人免费;3 人以上及任何商用需书面授权 | 人数 + 用途双重限制 | 用社区版铺量,商业授权变现,iPolloCloud 承载身份与权益 | +| **OpenWork** | Den 订阅(组织控制面) | ≤5 人组织免费、30 天评估、开发测试永久免费 | GitLab 式开源核心 + 付费管控面 | +| **Orca** | **未明示** | 全部免费 | 无公开营收路径 | +| **Maka** | 无 | 全部免费 | ASF 孵化项目,无商业实体 | + +### 3.1 OpenWork 的许可设计值得单独说 + +它的根目录 `LICENSE`(36 行)声明 `ee/` 之外是 MIT,`ee/LICENSE`(72 行)是 OpenWork EE License。 +这种「目录切分许可」是 GitLab 带火的做法,但 OpenWork 加了一个少见的条款: + +> Grant of Future License: Different AI, Inc. hereby irrevocably grants you +> an additional license to use each version of the Software under the MIT license, +> **effective on the second anniversary** of the date that version of the Software +> is first made publicly available. + +**每个 ee/ 版本发布满两年后自动转 MIT。** 这是一个很有说服力的承诺—— +它把「开源核心会不会被收回」这个疑虑,用一个不可逆的、带时间锁的授权消解掉了。 +对比那些纯粹靠「我们承诺不会作恶」的项目,这个设计硬得多。 + +顺带说一个方法论上的收获:这个仓库的 SPDX 标识是 `NOASSERTION`, +自动化脚本会把它判成 L3 / 私有镜像。但人工复核后发现, +**排除 `ee/` 目录之后,剩下的内容是纯 MIT,完全可以公开镜像。** +同一个仓库,两种正确答案,取决于镜像是否包含受限目录。 +我据此给追踪体系的许可证分级规则补了一条「混合许可降级流程」—— +否则这类项目会被系统性地过度保守处理。 + +### 3.2 Orca 的空白才是最值得警惕的 + +58.8k Star、MIT 全开源、无企业版、无订阅层,README 还明确写着 +「不提供模型,也不替你购买订阅」。 + +**一个没有营收路径的 58k Star 项目,是一个不稳定态。** +它要么在准备融资,要么在准备改条款。参考 Redis、Terraform、Elasticsearch 的先例, +「先用 MIT 铺到不可替代,再换成限制性许可」是已经被验证过的路径。 +这不代表 Orca 一定会这么做,但选型时应该把这个可能性算进去。 + +--- + +## 4. 场景维度:交付物决定了用户群 + +这一维度的差异比想象中大得多: + +| 项目 | 最终产出 | 产出后还能改吗 | 由此决定的用户群 | +| --- | --- | --- | --- | +| **Orca** | code、diff、review comment | 能(就是普通代码) | 专业开发者 | +| **OpenWork** | workflow、skill 包、capability | 能,且可跨工具复用 | 团队 / 组织 IT | +| **Maka** | code、artifact、**eval 报告**、**执行记录** | 能,且执行记录可回溯 | 研究者 / 受监管团队 | +| **iPolloWork** | code、doc、**PPT**、**网页**、**设计**、**视频** | 能,且定位就是「继续手工改」 | 内容团队 | + +**Maka 的「执行记录」是本轮唯一的新物种。** 它的 README 有一句话很能说明设计取向: + +> Shorter context is not deleted history. + +上下文压缩只省略下次 prompt 里的旧 tool 输出,不会删掉已经落盘的证据。 +model 消息、tool call、tool result、turn 的结束方式全部写进 append-only 日志。 +对受监管行业的团队来说,这是「能不能用」的前提条件,不是锦上添花。 + +**Orca 的交付物最窄(只有代码),但它的并行规模最大**—— +一条 prompt 扇出到 5 个 agent,各自在独立 git worktree 里实现,人工比对后合并优胜解。 +这是把「多方案比选」做成了产品,而不是让用户自己开 5 个终端。 + +--- + +## 5. 技术维度:四种「多引擎中立」 + +这是我认为全篇最值得看的一节。四个项目都声称或暗示支持多个 agent 引擎, +但实现方式完全不同,深度也完全不对等。 + +### 5.1 iPolloWork:协议级 adapter,但深度不对等 + +``` + 桌面 UI + │ + ▼ + Engine Protocol(local API) + ├──► OpenCode adapter ← 原生,默认本地 runtime,独立 sidecar + ├──► DSH adapter ← 原生,但仍在开发中 + └──► [MCP 控制面] ─────► Codex ← 浅 +``` + +README 自己承认了这种不对等:Codex「currently uses the MCP control surface +rather than claiming a native Codex engine adapter」。 +快是真的快(8 小时打通),但**快是因为走了控制面,深度也就止于控制面**。 + +### 5.2 Orca:PTY 级平等,但平等在低处 + +``` + 桌面 UI + │ + ▼ + PTY(伪终端) + ├──► Codex + ├──► Claude Code + ├──► OpenCode + ├──► Kimi / Qwen Code / MiMo Code ... + └──► 共 30+ 个 CLI agent +``` + +兼容判据就一条:**「能在终端里跑」**。 +代价是没有任何一家能拿到原生能力——拿不到结构化 tool call、 +拿不到统一的权限回调、拿不到一致口径的 token 统计。 + +**广度最大,深度最浅。** 这是 Orca 一切优点和缺点的共同根源。 + +### 5.3 OpenWork:MCP 级反注入,桌面是可选 + +``` + OpenWork MCP(只暴露 2 个工具) + ├── search_capabilities + └── execute_capability + ▲ + │ 作为 MCP 客户端连入 + ├──► Codex codex mcp add openwork --url ... + ├──► Claude Code claude mcp add --transport http openwork ... + ├──► Cursor + └──► OpenCode ← 真正深度绑定的(仓库描述即 powered by opencode) +``` + +注意方向反了:它不是去接引擎,而是**让引擎来接它**。 +所以它不接管 agent 的执行循环、不负责上下文、不做权限回调。 +README 那句「桌面 app 是可选的」不是谦虚,是架构的直接结果。 + +### 5.4 Maka:不做接入,自己就是引擎 + +``` + Desktop ┐ + TUI/CLI ┼──► Runtime Host ──► AgentRun(自带) + Eval ┘ └─► [external subject adapter] ← 仅评测用 +``` + +Maka 对**自己的**引擎深度是满分——一个 Runtime Host 统管所有入口。 +对**外部**引擎深度是零,只在 eval 里当被测对象。 + +它压根不参与「多引擎中立」这条赛道的竞争。这是定位选择,不是能力缺陷, +但读者必须知道它和另外三个不是同一类选手。 + +### 5.5 四种模式对照 + +| 项目 | 抽象层 | 引擎数 | 深度 | 本质 | +| --- | --- | --- | --- | --- | +| iPolloWork | Engine Protocol | 3 | **不对等** | 想做深,但只做成了一家半 | +| Orca | 无(PTY) | 30+ | **都浅** | 用广度换深度,很诚实 | +| OpenWork | OpenWork MCP | 4+ | 能力面 | 不接管执行,只分发能力 | +| Maka | Runtime Host | 1(自己) | 满 | 不做中立,做纵深 | + +**如果只能记住一句话**:下次看到「支持多引擎」,先问是哪种中立。 +协议级、PTY 级、能力级、还是根本不中立——这四种的成本和价值差了一个数量级。 + +--- + +## 6. 宣称 vs 实测 + +四个项目合计核出 14 条落差。这里挑最有代表性的 8 条,完整清单见各项目档案。 + +### 6.1 Orca:5,000 个 issue 的真相 + +> **宣称**:5,000 个 open issue —— 看起来是严重的社区问题积压。 +> **实际**:抽样最近 100 条,**全部创建于 2026-09-01 同一天,94% 没有任何标签**, +> 标题是 `fix(ssh): ...` / `refactor: name split modules` / `fix(task-page): ...` +> 这种内部任务口吻。提交者 TOP3 是 `nwparker`(51)、`OrcaWin`(13)、`klaidliadon`(12)。 +> +> 这是**团队自开的任务待办板**,不是社区投诉,也不构成 triage 压力。 +> 而且带明显的 AI 批量生成痕迹——比如 `OrcaWin` 提交的这条: +> 「pty.spawn can leak a running agent process unboundedly: host holds the door open」, +> 这种文学化的措辞不像人手写的。 +> +> **但要说清楚另一面**:这同时意味着这 5,000 条从来没有被当作对外承诺管理过。 +> **来源**:GitHub API `/repos/stablyai/orca/issues?state=open`,2026-09-01 抽样 100 条。 + +### 6.2 Orca:巴士系数为 1 + +> **宣称**:「The AI Orchestrator for 100x builders」,面向多人协作的成熟开发环境。 +> **实际**:最近 100 次提交中 `nwparker` 独占 **79 次**,第二名 `AmethystLiang` 仅 9 次。 +> **来源**:GitHub API `/repos/stablyai/orca/commits?per_page=100`,2026-09-01。 + +### 6.3 OpenWork:它要替代的东西,和它不是一个物种 + +> **宣称**:「The open-source alternative to Claude Cowork」。 +> **实际**:Cowork 是**桌面操控型** agent(直接操作你的电脑完成工作); +> OpenWork 是**能力分发型** MCP 层。README 原文写得很直白: +> 「The desktop app is there when you want a dedicated workspace, but it is not required」。 +> +> 它替代的是 Cowork 唤起的「能力复用」诉求,不是 Cowork 本身。 +> **来源**:README 开篇与「Use OpenWork from any agent」段。 + +### 6.4 OpenWork:NOASSERTION 反而是治理最好的 + +> **宣称**:(无明确宣称,但 SPDX 为 `NOASSERTION`,容易被读成「不够开源」) +> **实际**:恰恰相反,它是四个里工程治理最规范的—— +> 强制 DCO sign-off(每个 commit 都要 `Signed-off-by`)、 +> `ee/` 目录改动还需额外 CLA、`evals/` 放可执行测试规格并要求 PR 附测试证据、 +> `worlds/` 放声明式的开发与测试环境。 +> +> `NOASSERTION` 只因为根目录 LICENSE 是个混合声明文件,不代表开放度低。 +> **来源**:README「Getting started (contributors)」与「Sending a pull request」段。 + +### 6.5 Maka:Apache 光环不等于生产可用 + +> **宣称**:Apache 品牌 = 成熟稳定。 +> **实际**:孵化状态恰恰是「尚未被 ASF 完全背书」的标记,README 自带官方免责声明。 +> 截至 2026-09-01 **没有任何 Apache 正式 release**,对外分发的只有 Desktop Nightly, +> README 明确标注「not an ASF release, not intended for production use」。 +> +> 平台支持也最窄:macOS arm64 是唯一正式档,Windows 为未签名预览,Linux 标注 `soon`。 +> **来源**:README 的 NOTE / IMPORTANT 段、Releases and downloads 段、平台 badge。 + +### 6.6 Maka:本地优先,但 API key 是明文 + +> **宣称**:「Your machine, your data」本地优先、数据主权。 +> **实际**:数据确实不出机器,但 README 自己承认 `credential-vault.json` 是 +> 「local plaintext file, readable only by your OS account」。 +> +> 防护依赖操作系统账户权限,不是加密也不是系统钥匙串。 +> renderer 进程拿不到,但同机任一以你身份运行的进程都能读到。 +> **来源**:README「Local data and recovery」段。 + +### 6.7 Maka:崩溃恢复的两个附加条件 + +> **宣称**:崩溃恢复与可选续跑。 +> **实际**:续跑**默认关闭**,需手动设 `MAKA_RUNTIME_SAFE_BOUNDARY_RESUME=1`, +> 且 README 警告该调用会打模型、消耗 token。 +> 另有**升级断代**:旧版 JSONL transcripts 与 Electron safeStorage 凭据不会被导入, +> 升级后的工作区可能出现空线程,凭据必须重新录入。 +> **来源**:README「Local data and recovery」段。 + +### 6.8 Orca:三端覆盖,成熟度不对等 + +> **宣称**:桌面 / 移动 / VPS 全覆盖。 +> **实际**:Android 仍是手工分发 APK(0.0.47),iOS 走 App Store 与 TestFlight, +> VPS 场景需另查 headless Linux 指南。 +> 移动端实际是「监控与追加指令」的伴随形态,不是完整工作环境。 +> **来源**:README Install 段与 Mobile Companion 段。 + +--- + +## 7. 象限图:四个坐标 + +按四个维度打分(0–10,人工标定,见各项目档案的 `chart` 块): + +| 项目 | 形态纵深 | 引擎中立 | 本地优先 | 交付物纵深 | +| --- | --- | --- | --- | --- | +| iPolloWork | 8 | 7 | 7 | 7 | +| OpenWork | 5 | 6 | 6 | 5 | +| Orca | 8 | 9 | 6 | 3 | +| Maka | 6 | 1 | 10 | 6 | + +把「引擎中立」和「本地优先」拉成两个轴: + +``` + 本地优先 10 │ + │ Maka (1,10) + 9 │ + │ iPolloWork (7,7) + 7 │ + │ OpenWork (6,6) Orca (9,6) + 6 │ + │ + 5 │ + └────┬────┬────┬────┬────┬────┬──── + 1 3 5 7 9 + 引擎中立 → +``` + +**读图**: +- **右上(高本地 + 高中立)目前是空的**。这四个项目里没有一个同时做到 + 「完全本地」和「多引擎中立」——这大概就是下一个值得关注的空白位。 +- Maka 在左上角孤零零一个点:它用「放弃中立」换来了「完全本地」。 +- Orca 在右下:中立度最高,但本地优先只有 6 分(强依赖各家云端订阅)。 + +样本只有四个,象限图只是示意,不构成统计结论。 + +--- + +## 8. 选型建议 + +| 如果你的情况是 | 选 | 理由 | +| --- | --- | --- | +| 已重度使用 CLI agent,想并行跑多方案比对 | **Orca** | 兼容面最广,并行 worktree 是独一份 | +| 团队已固定在某款 agent,只想让能力跨工具复用 | **OpenWork** | MCP 反注入是唯一解,不逼你换工具 | +| 有审计 / 数据主权 / 可复现评测的硬要求 | **Maka** | 执行记录 + eval 框架,别家都没有 | +| 希望 agent 产出的 PPT / 网页 / 设计之后还能手工改 | **iPolloWork** | 交付物纵深最广 | + +**共同的不建议**: +- 都还是 0.x 或 1.x 早期版本,没有一家适合放进关键生产路径。 +- 都要求 BYOM(自带模型),模型成本与额度需要自己算。 + +**再看一段时间的信号**: +- Orca 的营收路径是否公开(决定它会不会改许可) +- OpenWork 是否发布 1.0(当前 v0.18.40) +- Maka 是否产出第一个 Apache 正式 release(当前只有 Nightly) +- iPolloWork 的 Codex 是否从 MCP 控制面升级为原生 adapter + +--- + +## 9. 追踪备注 + +- **数据日期**:2026-09-01 +- **指标来源**:GitHub API 自动化快照(无 token,匿名配额) +- **下次跟进点**: + 1. Orca 的 issue 是否会开始做社区 triage(当前 94% 无标签) + 2. Maka 的首个 Apache 正式 release + 3. OpenWork 1.0 与 ee/ 两年转 MIT 的首批到期版本 +- **相关档案**:`data/projects/{ipollowork,openwork,orca,maka}.yaml` +- **源码镜像**: + - `gitinbox/iPolloWork`(私有 · L3 source-available) + - `gitinbox/maka`(公开 · L1 Apache-2.0) + - OpenWork / Orca 因体积超限(291 MB / 671 MB)暂未镜像,档案中已标注 +- **方法论更新**:本轮给许可证分级补了「混合许可降级流程」, + 并给指标回写加了自愈能力(块被误删时自动重建) + +--- + +本文为「全球智能体开源应用追踪」系列之一 · 总纲见 [系列索引](https://blog.apescale.com/?post=98) · 数据与脚本开源于 `gitinbox/agent-radar` diff --git a/articles/00-comparison.meta.yaml b/articles/00-comparison.meta.yaml new file mode 100644 index 0000000..5698ec3 --- /dev/null +++ b/articles/00-comparison.meta.yaml @@ -0,0 +1,5 @@ +# 发布元数据(由 scripts/publish_emlog.py 维护) +# 本文不属于单个项目,ID 不落在 data/projects/ 下 +emlog_id: 99 +url: "https://blog.apescale.com/?post=99" +last_published: "2026-09-01" diff --git a/articles/00-overview.md b/articles/00-overview.md index d7aa727..84a439c 100644 --- a/articles/00-overview.md +++ b/articles/00-overview.md @@ -1,6 +1,7 @@ # 全球智能体开源应用追踪 · 总纲 -> 这是一个**持续更新**的追踪索引。每收录一个新项目,会先发一篇深度文,再回来回填这里。 +> 这是一个**持续更新**的追踪索引。每收录一个新项目会先建档,够一批样本就发一篇横向对比, +> 再按价值排序补单项目深度文——所以索引里「待发布」是常态,不是遗漏。 > 所有数据来自 GitHub API 的定时快照,所有结论都标注了核实来源。 > 数据与脚本开源在 `gitinbox/agent-radar`。 @@ -69,24 +70,48 @@ Codex Harness 开放、DeepSeek Harness 开放,一个个执行引擎被拆出 | 项目 | 定位 | 目标用户 | 变现 | 许可证 | 成色 | | --- | --- | --- | --- | --- | --- | | **iPolloWork** | 站在 Agent 工具链的**工作台层**而非引擎层:不替代 Codex / DSH / OpenCode 中任何一个,而是把它们收进同一个项目空间,并把交付物从代码延伸到文档、演示稿、网站、设计与视频 | 企业研发团队、需要人机协作的 agent 团队、需要产出并持续修改 PPT / 网页 / 设计 / 视频的内容团队 | 双轨:个人自用与少于 3 人的小规模内部使用免费;3 人及以上、以及任何售卖 / 转售 / 付费服务 / SaaS / 托管 / 白标 / 市场分发 / 面向客户的使用,均需书面授权。配套 iPolloCloud 承载身份、组织、权益与商业 App | iPolloWork Source Available License 1.0 | L3 | +| **maka** | 站在「执行记录层」:不追功能广度,把「agent 干过什么必须可复现、 可审计、可评测」做成地基。同时是四个里唯一自带评测框架的项目。 | 对可审计性与数据主权有硬要求的团队、需要横向评测不同 agent / 模型的研究者与工程团队、愿意用功能丰富度换确定性的自用开发者 | 无。Apache 孵化项目,无商业实体、无双许可、无付费层。 | Apache License 2.0 | L1 | +| **openwork** | 站在「能力分发层」,不争桌面入口。核心资产是 OpenWork MCP 与 Den 控制面—— 把 skills / MCP / 连接服务做成可跨工具、跨人、跨机器复用的组织级能力包。 | 已在使用 Codex / Claude Code / Cursor 且不想换工具的开发者、需要统一管控 agent 能力的中大型组织、想把自己搭好的工作流分发给同事或朋友的团队 | Den 订阅(组织控制面)。免费额度写得很明确: 5 人以下组织的非 Enterprise 功能全免、任意规模 30 天评估、开发测试永久免费。 | 目录切分许可(GitLab 式):ee/ 之外为 MIT;ee/ 为 OpenWork EE License | L3 | +| **orca** | 站在「执行编排层」:不造引擎、不卖模型,只做并行 agent 的调度与可观测外壳。 上游是各家 CLI agent,下游是开发者的本机/远程 worktree。 | 自称 100x builders 的重度个人开发者、需要同时试多种实现方案再择优合并的小团队、已在用 Codex / Claude Code / OpenCode 并想并行跑的人 | 未明示,且这是它最大的商业不确定性。MIT 全开源、无企业版、无订阅层, README 明确不提供模型也不代买订阅。收入路径尚未公开。 | MIT License | L1 | ### 场景维度 | 项目 | 交互形态 | 人机协作 | 交付物 | | --- | --- | --- | --- | | **iPolloWork** | desktop-app | approval-gated | code、doc、ppt、website、design、video | +| **maka** | 桌面应用、TUI / CLI、评测框架 | 强:工具越过沙箱边界必须审批;运行可中止;失败被分类。 | code、artifact、eval-report、execution-record | +| **openwork** | MCP 远程服务、桌面应用(可选)、浏览器登录授权 | 能力调用需人工在浏览器登录并选择组织;企业侧由管理员通过策略管控。 | workflow、skill-package、capability | +| **orca** | 桌面应用、移动端伴随、CLI、SSH 远程 | 强人在环:需人工比并结果、批注 diff、批准合并。 但 5000 个自开 issue 说明产品自身也在被 AI 自动改进。 | code、diff、review-comment | ### 技术维度 | 项目 | 形态 | 引擎抽象 | 接入深度 | 模型绑定 | 本地优先 | | --- | --- | --- | --- | --- | --- | | **iPolloWork** | Electron 桌面应用(另支持 dev:ui 只起浏览器 UI) | Engine Protocol(经 local API);Codex 与 MCP 客户端另经 ipollowork-ui-mcp 控制面接入 | **三种引擎的接入深度并不对等**:OpenCode 是默认本地执行 runtime,走 local API → Engine Protocol 的原生 adapter,以独立 sidecar 形式存在(首次桌面构建时下载,不 fork、可独立升级);DeepSeek Harness 是可选 peer runtime 与子代理委派目标,同样走 Engine Protocol;Codex 则**仅**通过 ipollowork-ui-mcp 的 MCP 控制面接入,README 明确写明 currently uses the MCP control surface rather than claiming a native Codex engine adapter | byom | 是 | +| **maka** | 桌面应用 + TUI/CLI + 评测框架,三者共用同一 Runtime Host | Runtime Host(单一所有者的生命周期 + 协议 + 客户端引导) | 与另外三家正好相反:Maka 对**自己的**引擎深度是满分 (一个 Runtime Host 统管所有入口),对**外部**引擎深度是零 (只在 eval 里当被测对象)。 它压根不参与「多引擎中立」这条赛道的竞争——这是定位选择而非能力缺陷, 但读者必须知道它和 Orca / OpenWork / iPolloWork 不是同一类选手。 | 完全 BYO:云 API、本地模型或兼容网关, 不捆绑任何共享账号(README 明确 "You bring the model")。 | 是 | +| **openwork** | MCP 远程服务 + 可选桌面应用 + Web 控制面 | OpenWork MCP(能力面,不是执行面) | 接入「对等且都浅」:所有引擎都以同一个 MCP 客户端身份接入, 拿到的是能力调用,不是执行控制——OpenWork 不接管 agent 的执行循环、 不负责上下文管理、不做权限回调。 真正深度绑定的是 OpenCode:仓库描述即写 powered by opencode, 桌面端属 OpenCode 生态,其余引擎只是 MCP 外挂。 这与 iPolloWork 的「Codex 仅 MCP 控制面」是同一个问题的两种解法: iPolloWork 把 MCP 当补丁(主体是桌面),OpenWork 把 MCP 当主体(桌面是可选)。 | Den 侧可统一配置与配额控制 model provider;桌面 app 本身 BYO | 是 | +| **orca** | 桌面应用(含移动端伴随 + CLI) | 无统一抽象层;实际是终端 PTY + 约定式输出解析 | 接入深度「完全对等,但对等在低处」:30+ 引擎共享同一套 PTY 外壳, 因此没有任何一家能拿到原生能力——拿不到结构化 tool call、 拿不到统一权限回调、拿不到一致口径的 token 统计。 广度最大,深度最浅。对照 iPolloWork 为 OpenCode 做的原生 Engine Protocol adapter,Orca 与任一单引擎的耦合都明显更浅。 | 完全 BYO——用户自带各 agent 的订阅与额度 | 是 | ### 生态与风险 | 项目 | Star | Fork | Issues | 最新 release | 节奏 | 成熟度 | 数据日期 | | --- | --- | --- | --- | --- | --- | --- | --- | | **iPolloWork** | 5201 | 976 | 80 | v0.50.12 | 一天多版 | beta | 2026-09-01 | +| **maka** | 4373 | 406 | 398 | v0.2.0-dev.9.20260831 | 一天多版 | 孵化期 | 2026-09-01 | +| **openwork** | 23253 | 2307 | 414 | v0.18.40 | 约 1 天/版本 | 0.x 活跃 | 2026-09-01 | +| **orca** | 58771 | 3987 | 5000 | v1.4.194 | 约 1 天/版本 | 原型期 | 2026-09-01 | + +**成熟度详解** + +- **iPolloWork**:beta +- **maka**:治理成熟度最高(ASF 流程 + 评测框架 + 文档契约), +产品成熟度最低(无正式 release、平台只支持 arm64 macOS、功能默认保守)。 +属于「地基打得最实,房子盖得最慢」。 +- **openwork**:工程成熟度最高:有可执行测试规格(evals/)、声明式环境(worlds/)、 +明确的贡献契约与许可边界。但仍处 0.x 主版本(v0.18.40), +API 与数据格式可能变动。 +- **orca**:功能广度第一,工程治理末位。一天多版(v1.4.194)说明迭代极快, +但 issue 只进不出、巴士系数为 1,属于「高速原型期」而非「可托付生产期」。 ### 宣称 vs 实测 @@ -99,36 +124,97 @@ Codex Harness 开放、DeepSeek Harness 开放,一个个执行引擎被拆出 | **iPolloWork** | 统一的插件与 Skills 生态 | 统一的是**生命周期**(安装/启用/更新/卸载一次),不是能力本身。README 明确各 runtime 保留自己的 agents、Skills、插件与执行模型,引擎专属增强只是可选 | README 的 Agent runtime compatibility 一节 | | **iPolloWork** | 仓库 topics 标注 claude-code | 中英文 README 均未描述任何 Claude Code 接入方式。topic 更像搜索曝光策略,不应视为已支持的能力 | GitHub topics 与 README 全文对照 | | **iPolloWork** | 企业级(enterprise-grade) | 版本号仍在 0.50.x,无公开 Roadmap,发布节奏为一天多版,仓库体积 591 MB。工程投入可观,但版本语义上尚未到企业可用的稳定态 | GitHub releases;README 版本说明;仓库 size 字段 | +| **maka** | Apache 品牌 = 成熟稳定、可用于生产 | 孵化状态恰恰是「尚未被 ASF 完全背书」的标记,README 自带官方免责声明。 且截至 2026-09-01 **没有任何 Apache 正式 release**, 对外分发的只有 Desktop Nightly,README 明确标注 「not an ASF release, not intended for production use」。 平台支持也最窄:macOS arm64 是唯一正式档, Windows 为未签名预览,Linux 标注 soon。 | README 的 NOTE / IMPORTANT 段、Releases and downloads 段、平台 badge | +| **maka** | 「Your machine, your data」本地优先、数据主权 | 数据确实不出机器,但 **API key 以明文文件存放**—— README 原文承认 credential-vault.json 是 「local plaintext file, readable only by your OS account」。 防护依赖操作系统账户权限,而非加密或系统钥匙串。 renderer 进程拿不到,但同机任一以你身份运行的进程都能读到。 | README「Local data and recovery」段 | +| **maka** | 崩溃恢复与可选续跑 | 续跑**默认关闭**,需手动设 MAKA_RUNTIME_SAFE_BOUNDARY_RESUME=1, 且 README 明确警告该调用会打模型、消耗 token。 另存在**升级断代**:旧版 JSONL transcripts 与 Electron safeStorage 凭据不会被导入,升级后的工作区可能出现空线程,凭据必须重新录入。 | README「Local data and recovery」段 | +| **maka** | (无明确宣称,但易被期待为开箱即用的完整工作台) | 内置工具只有 Read / Write / Edit / Bash / Glob / Grep 六个; Computer Use 与 catalog skills 属可选且默认关闭; IM bot(聊天应用接入)标注 experimental。 功能面是四个里最保守的——取舍很明确:用广度换确定性。 | README「Current capabilities」段 | +| **maka** | 支持 Graph 多算子并行 | Graph 的实现算子跑在独立 git worktree,且**要求源项目是干净 worktree**—— 有未提交改动就用不了。相比 Orca 的并行 worktree(产品级封装), Maka 的 Graph 更接近实验性能力。 | README「Terminal entry points」段 | +| **openwork** | 「Claude Cowork 的开源替代品」 | 形态完全不同。Cowork 是桌面操控型 agent(直接操作你的电脑完成工作); OpenWork 是能力分发型 MCP 层,桌面 app 被明确定位为可选—— README 原文「The desktop app is there when you want a dedicated workspace, but it is not required」。 它替代的是 Cowork 唤起的「能力复用」诉求,不是 Cowork 本身。 | README 开篇与「Use OpenWork from any agent」段 | +| **openwork** | 跨 Codex / Claude Code / Cursor / OpenCode 多引擎通用 | 对 OpenCode 是原生的(仓库描述 powered by opencode,桌面端属其生态); 对其余引擎只通过自己暴露的两个 MCP 工具对接。 这是能力调用面,不是引擎执行面——OpenWork 不接管这些引擎的执行循环、 上下文与权限。对外宣称的「跨引擎」与内部实际的主从关系存在落差。 | README「It exposes two tools」段;仓库 description「powered by opencode」 | +| **openwork** | free, open-source(免费开源) | 需要分目录看:ee/ 之外确实 MIT 且免费;但 ee/(Den 组织控制面、 MCP 网关、推理)属 source-available,生产使用需付费订阅, 只有 5 人以下组织、30 天评估、开发测试三种情形豁免。 个人免费,团队与企业要付费。 | README Licensing 段 + ee/LICENSE 原文 72 行 | +| **openwork** | (无明确宣称,但 SPDX 为 NOASSERTION,易被误读为「不够开源」) | 恰恰相反——它是四个项目里工程治理最规范的:强制 DCO sign-off、 ee/ 需额外 CLA、evals/ 目录放可执行测试规格并要求 PR 附测试证据、 worlds/ 放声明式的开发与测试环境定义。 NOASSERTION 只因根目录 LICENSE 是混合声明文件,不代表开放度低。 | README「Getting started (contributors)」与「Sending a pull request」段 | +| **orca** | 5000 个 open issue 反映社区问题积压与项目质量压力 | 抽样最近 100 条:全部创建于 2026-09-01 同一天,94% 无任何标签, 标题为 fix(ssh) / refactor / fix(task-page) 等内部任务口吻; 提交者 TOP3 为 nwparker(51)、OrcaWin(13)、klaidliadon(12)。 实为「团队自开的任务待办板」,且带明显 AI 批量生成痕迹 (如 OrcaWin 提交的「pty.spawn can leak a running agent process unboundedly: host holds the door open」)。 它不是社区投诉,也不构成 triage 压力——但同样说明这 5000 条 从未被当作对外承诺来管理。 | GitHub API /repos/stablyai/orca/issues?state=open,2026-09-01 抽样 100 条 | +| **orca** | 「100x builders 的 AI Orchestrator」,面向多人协作的成熟开发环境 | 巴士系数为 1:最近 100 次提交中 nwparker 独占 79 次, 第二名 AmethystLiang 仅 9 次。功能面由单人 + AI 高强度驱动, 而非团队协作成果。 | GitHub API /repos/stablyai/orca/commits?per_page=100,2026-09-01 | +| **orca** | 支持 30+ 种 agent,生态最开放 | 兼容判据是「能在终端里跑」,靠 PTY 拉起而非协议对接。 广度最大但深度最浅——所有引擎都拿不到结构化 tool call 与统一的权限回调。与 iPolloWork 为 OpenCode 做原生 Engine Protocol adapter 相比,Orca 与任一单引擎的耦合都更浅。 | README「Works with any CLI agent — if it runs in a terminal, it runs in Orca」 | +| **orca** | MIT 全开源,可放心用于生产 | 许可证本身无风险,但项目没有任何公开的营收路径 (无企业版、无订阅、不卖模型、不代买订阅)。 58k star 的体量与零商业化不匹配,属于「要么即将融资、 要么后续变更条款」的不稳定态。 | README License 段;README 明确「Orca does not provide models, nor does it buy subscriptions for you」 | +| **orca** | 桌面 / 移动 / VPS 全覆盖 | 三端成熟度不对等:Android 仍是手工分发 APK(0.0.47)、 iOS 走 App Store 与 TestFlight、VPS 场景需另查 headless Linux 指南。 移动端实际是「监控与追加指令」的伴随形态,不是完整工作环境。 | README Install 段与 Mobile Companion 段 | ### 一句话结论 | 项目 | 结论 | 适合 | 不适合 | | --- | --- | --- | --- | | **iPolloWork** | 把多个 Agent 引擎收进同一个可编辑工作台的野心之作——方向踩在点上,但三种引擎的接入深度并不齐整,且许可证决定了它暂时只适合个人或极小团队试用 | 想在同一个界面管理多引擎项目、并希望 agent 产出的 PPT / 网页 / 设计 / 视频之后还能继续手工修改的个人开发者,或少于 3 人的极小团队 | 3 人以上需要正式落地的团队(许可证要求书面授权)、期待 Codex 原生级集成的用户(目前只有 MCP 控制面)、以及追求稳定的生产环境(v0.50.x、一天多版、DSH 集成未进稳定版) | +| **maka** | 四个里唯一把「执行记录」当地基的项目——治理与可复现性最强,产品成熟度最弱。 | 对审计、数据主权、可复现评测有硬要求的团队; 做 agent 横向对比研究的工程团队。 | 要开箱即用、要 Windows / Linux、要丰富第三方集成的人; 以为 ASF 品牌等于生产可用的决策者。 | +| **openwork** | 把「能力」而不是「桌面」做成产品——工程治理最好的一个,但它和自称要替代的东西不是一个物种。 | 已固定在某款 agent 上、只想让团队能力跨工具复用的开发者; 需要统一管控 agent 能力与模型配额的组织 IT。 | 想要一个开箱即用的桌面 agent 工作台的人; 以为拿到的是完整 Cowork 平替的人。 | +| **orca** | 兼容面最广的并行 agent 编排外壳——广度做到极致,深度与治理都还是草稿。 | 已在重度使用 CLI agent、想同时跑多个方案横向比对的个人开发者与小团队。 | 需要稳定 SLA、指望社区 triage 与多人维护保障的团队; 指望它统一各引擎能力(结构化调用、统一权限、一致口径)的架构选型者。 | -```text -┌─────────────────────────────────────────────────────────┐ -│ 执行引擎层 (暂无收录) -│ └ 真正跑 agent loop 的地方 -│ │ -│ 编辑器内 Agent 层 (暂无收录) -│ └ 嵌在 IDE / 编辑器里的交互层 -│ │ -│ 桌面工作台层 iPolloWork★5201 -│ └ 跨引擎、跨项目、跨交付物的统一界面 -└─────────────────────────────────────────────────────────┘ -``` - -> 当前收录 1 个项目,不足 4 个,此阶段只出分层图。样本够了之后会自动切换为象限图。 + +视图 A · 形态纵深 × 引擎立场 + + + + + + + +单点工具 +统一工作台 +绑定单引擎 +多引擎中立 + +iPolloWork + +maka + +openwork + +orca + + +视图 B · 本地优先 × 交付物纵深 + + + + + + + +强云依赖 +完全本地 +只出代码 +多模态可编辑 + +iPolloWork + +maka + +openwork + +orca + --- -## 深度文索引 +## 文章索引 -| 项目 | 一句话结论 | 文章 | +> 这一段由 `scripts/build_matrix.py` 从项目档案的 `article` 块与 +> `articles/*.meta.yaml` 自动生成。发新文后重跑脚本即可,不要手工改。 + + +| 项目 | 一句话结论 | 深度文 | | --- | --- | --- | -| iPolloWork | 把多个 Agent 引擎收进同一个可编辑工作台的野心之作——方向踩在点上,但三种引擎的接入深度并不齐整,且许可证决定了它暂时只适合个人或极小团队试用 | 见同期发布的《iPolloWork 深度拆解》 | +| **iPolloWork** | 把多个 Agent 引擎收进同一个可编辑工作台的野心之作 | [已发布](https://blog.apescale.com/?post=97) | +| **maka** | 四个里唯一把「执行记录」当地基的项目 | 待发布(见下方横评) | +| **openwork** | 把「能力」而不是「桌面」做成产品 | 待发布(见下方横评) | +| **orca** | 兼容面最广的并行 agent 编排外壳 | 待发布(见下方横评) | + +**跨项目文章** + +- [四个 Agent 工作台的路线分野:把「多引擎中立」拆开看](https://blog.apescale.com/?post=99) — 2026-09-01 + +> 待补深度文:maka、openwork、orca。当前可先读上方跨项目文章,其中已包含这些项目的完整分维度对比。 + --- diff --git a/data/projects/ipollowork.yaml b/data/projects/ipollowork.yaml index aabfff0..c5312a2 100644 --- a/data/projects/ipollowork.yaml +++ b/data/projects/ipollowork.yaml @@ -117,6 +117,7 @@ ecosystem: - "2026-08-31 发布 v0.50.12,为最近一次 push" risk: + maturity_level: "beta" license_risk: "L3 级源码可用协议。3 人以上团队无论用途都需书面授权,且明确禁止未经授权的 SaaS / 托管 / 白标 / 市场分发。这意味着:企业落地前必须先谈授权,而源码镜像也只能是私有的" vendor_lockin: "引擎层是中立了,但工作台层本身是闭源商业产品。项目资产沉淀在 iPolloWork 的项目模型里,退出成本高于纯 CLI 方案;一旦停止授权,已建的工作流难以平移" claim_vs_reality: @@ -144,10 +145,10 @@ risk: maturity: beta chart: - form_depth: 9 - engine_neutral: 9 - local_first_score: 9 - deliverable_depth: 9 + form_depth: 8 # 桌面工作台整合填报/审核/查询,形态完整 + engine_neutral: 7 # 宣称三引擎中立,实际深度不对等(OpenCode 原生 / DSH preview / Codex 仅 MCP) + local_first_score: 7 # 本地桌面运行,但依赖云端模型与配置服务 + deliverable_depth: 7 # 可编辑产出物,不止代码 verdict: one_liner: "把多个 Agent 引擎收进同一个可编辑工作台的野心之作——方向踩在点上,但三种引擎的接入深度并不齐整,且许可证决定了它暂时只适合个人或极小团队试用" @@ -155,13 +156,15 @@ verdict: who_should_not: "3 人以上需要正式落地的团队(许可证要求书面授权)、期待 Codex 原生级集成的用户(目前只有 MCP 控制面)、以及追求稳定的生产环境(v0.50.x、一天多版、DSH 集成未进稳定版)" mirror: - gitea_repo: "gitinbox/iPolloWork" + gitea_repo: gitinbox/iPolloWork visibility: private upstream: "https://github.com/Devin-AXIS/iPolloWork" - last_sync: "" - sync_note: "L3 source-available,镜像强制私有;仓库 591 MB,全量裸仓镜像" + last_sync: 2026-09-01 + mode: source-only + sync_note: L3 source-available 强制私有。全量裸仓 69.9 MB 超过 Gitea 前置 nginx 约 50MB 的 client_max_body_size,推送被 HTTP 413 拒绝;已降级为源码快照(14.1 MB,剔除 png/gif/mp4/字体等非源码素材约 69MB)。完整保真快照保留在本地 .mirrors/iPolloWork.git(70MB)。服务器放开 nginx 上限后可推送完整版。 + mode: source-only article: emlog_id: 97 - published_at: "" - url: "" + published_at: "2026-09-01 15:11:57" + url: "https://blog.apescale.com/?post=97" diff --git a/data/projects/maka.yaml b/data/projects/maka.yaml new file mode 100644 index 0000000..2902ff9 --- /dev/null +++ b/data/projects/maka.yaml @@ -0,0 +1,228 @@ +# maka · 项目档案 +# 生成于 2026-09-01 · 由 scripts/new_project.py 从 schema 生成 +# 人工补完重点:business / scenario / tech / risk.claim_vs_reality / verdict +# 字段规范见 schema/project.schema.yaml + +id: maka +name: "maka" +repo: "apache/maka" +homepage: "" +added_at: "2026-09-01" +status: active +identity: + tagline: "Apache Maka (Incubating) is a local-first AI agent workspace. Model messages, tool calls, tool results, permission decisions, and termination events are recorded as an append-only log." + vendor: "Apache 软件基金会(Incubating)" + vendor_type: "foundation" + region: "国际(ASF 孵化)" + first_seen: "2026-05-27" +business: + + positioning: |- + 站在「执行记录层」:不追功能广度,把「agent 干过什么必须可复现、 + 可审计、可评测」做成地基。同时是四个里唯一自带评测框架的项目。 + target_users: + - 对可审计性与数据主权有硬要求的团队 + - 需要横向评测不同 agent / 模型的研究者与工程团队 + - 愿意用功能丰富度换确定性的自用开发者 + monetization: |- + 无。Apache 孵化项目,无商业实体、无双许可、无付费层。 + license: + spdx: "Apache-2.0" + name: "Apache License 2.0" + openness: L1 + redistributable: true + key_restrictions: + - 含明确的专利授权与反诉终止条款(Apache-2.0 固有) + - 修改需声明,NOTICE 文件须随分发保留 + - 不授予商标使用权(Apache / ASF 商标政策单列) + moat: |- + 执行记录模型 + 可复现评测框架 + ASF 治理背书。 + 三者都需要长期投入,不能靠快速迭代速成。 + competitive_risk: |- + 最容易被上游引擎把「记录与评测」原生化—— + 一旦 Codex / Claude Code 自己把 event log 与 eval 做进产品, + Maka 的地基优势会被稀释成一层「合规装饰」。 + +scenario: + + primary: + - 本地优先的 agent 工作区(数据与执行记录不出机器) + - 可复现的 agent 评测实验(multi-arm experiments) + - 崩溃恢复与中断续跑 + typical_use_cases: + - 在保密项目里用本地模型跑 agent,所有 tool call 与权限决策落 append-only 日志备查 + - 用 maka eval run 对同一任务跑 task × repetition × subject 的多臂实验,横向比较得分/成本/耗时 + - 会话崩了以后从执行记录恢复,并从任一 Turn 分叉出新会话继续 + industries: + - 软件研发 + - 受监管行业(金融、政务等有审计要求的场景) + - AI 评测与研究 + interaction_mode: + - 桌面应用 + - TUI / CLI + - 评测框架 + human_in_the_loop: |- + 强:工具越过沙箱边界必须审批;运行可中止;失败被分类。 + deliverables: + - code + - artifact + - eval-report + - execution-record + +tech: + + form_factor: 桌面应用 + TUI/CLI + 评测框架,三者共用同一 Runtime Host + stack: + - Electron + - React + - TypeScript + - Node.js + - SQLite + - Rust(peer mesh 的 native addon) + - ripgrep(Runtime 的 Grep 工具依赖) + engine_abstraction: + supported: + - Maka 自带 Runtime(AgentRun) + mechanism: |- + 不做外挂接入——自带完整 agent runtime, + Desktop / TUI / CLI / Eval 全部经同一个 Runtime Host 执行。 + 对外部 agent 只在评测环节通过 external subject adapter 对接, + 不做执行层兼容。 + abstraction_layer: Runtime Host(单一所有者的生命周期 + 协议 + 客户端引导) + depth_notes: |- + 与另外三家正好相反:Maka 对**自己的**引擎深度是满分 + (一个 Runtime Host 统管所有入口),对**外部**引擎深度是零 + (只在 eval 里当被测对象)。 + 它压根不参与「多引擎中立」这条赛道的竞争——这是定位选择而非能力缺陷, + 但读者必须知道它和 Orca / OpenWork / iPolloWork 不是同一类选手。 + extension_system: + skills: catalog skills 存在,但可选且**默认关闭** + plugins: 无独立插件市场 + mcp: packages/mcp 提供 provider-neutral 的 MCP 客户端集成 + model_binding: |- + 完全 BYO:云 API、本地模型或兼容网关, + 不捆绑任何共享账号(README 明确 "You bring the model")。 + local_first: true + multi_agent: |- + Graph 模式,实现算子跑在独立 git worktree + (要求源项目是干净 worktree)。 + permission_sandbox: |- + 沙箱边界明确:写文件 / 跑 shell 的工具必须先过边界; + 运行可中止;失败被分类;支持崩溃恢复与可选续跑。 + observability: |- + 最强项。append-only 执行记录:model 消息、tool call、tool result、 + turn 结束方式全部落盘。 + 上下文压缩只省略下次 prompt 里的旧 tool 输出,不删已存证据—— + README 原话 "Shorter context is not deleted history"。 + +ecosystem: + + community: |- + ASF 孵化治理:有 DISCLAIMER-WIP、ASF_SOURCE_RELEASE 候选契约、 + CONTRIBUTING、SECURITY 等完整治理文件。中文文档齐全, + 另挂 DeepWiki 第三方 AI 生成文档。 + standards: + - Apache 软件基金会(Incubator) + notable_events: + - 2026-05-27 建仓(进入 ASF 孵化) + - 截至 2026-09-01 尚未产出任何 Apache 正式 release,对外只有 Desktop Nightly + - 2026-09-01 达 4372 star,版本 v0.2.0-dev.9.20260831 + + metrics: + stars: 4373 + forks: 406 + open_issues: 398 + subscribers: 23 + created_at: 2026-05-27 + pushed_at: 2026-09-01 + size_kb: 102390 + languages: {TypeScript: 91.7, JavaScript: 4.5, Rust: 1.7, CSS: 1.3, Python: 0.5, PowerShell: 0.2} + latest_release: v0.2.0-dev.9.20260831 + release_cadence: 一天多版 + data_date: 2026-09-01 + stale: false +risk: + maturity_level: "孵化期" + + license_risk: |- + Apache-2.0,四个里最干净:可自由镜像与再分发,且自带专利授权条款, + 无任何商业条款陷阱。 + vendor_lockin: |- + 锁定点最低:无商业实体、无付费层、数据落在本地 SQLite、 + 执行记录是自有格式但结构公开。退出成本主要是迁移历史会话记录。 + claim_vs_reality: + - claim: |- + Apache 品牌 = 成熟稳定、可用于生产 + actual: |- + 孵化状态恰恰是「尚未被 ASF 完全背书」的标记,README 自带官方免责声明。 + 且截至 2026-09-01 **没有任何 Apache 正式 release**, + 对外分发的只有 Desktop Nightly,README 明确标注 + 「not an ASF release, not intended for production use」。 + 平台支持也最窄:macOS arm64 是唯一正式档, + Windows 为未签名预览,Linux 标注 soon。 + source: README 的 NOTE / IMPORTANT 段、Releases and downloads 段、平台 badge + - claim: |- + 「Your machine, your data」本地优先、数据主权 + actual: |- + 数据确实不出机器,但 **API key 以明文文件存放**—— + README 原文承认 credential-vault.json 是 + 「local plaintext file, readable only by your OS account」。 + 防护依赖操作系统账户权限,而非加密或系统钥匙串。 + renderer 进程拿不到,但同机任一以你身份运行的进程都能读到。 + source: README「Local data and recovery」段 + - claim: |- + 崩溃恢复与可选续跑 + actual: |- + 续跑**默认关闭**,需手动设 MAKA_RUNTIME_SAFE_BOUNDARY_RESUME=1, + 且 README 明确警告该调用会打模型、消耗 token。 + 另存在**升级断代**:旧版 JSONL transcripts 与 Electron safeStorage + 凭据不会被导入,升级后的工作区可能出现空线程,凭据必须重新录入。 + source: README「Local data and recovery」段 + - claim: |- + (无明确宣称,但易被期待为开箱即用的完整工作台) + actual: |- + 内置工具只有 Read / Write / Edit / Bash / Glob / Grep 六个; + Computer Use 与 catalog skills 属可选且默认关闭; + IM bot(聊天应用接入)标注 experimental。 + 功能面是四个里最保守的——取舍很明确:用广度换确定性。 + source: README「Current capabilities」段 + - claim: |- + 支持 Graph 多算子并行 + actual: |- + Graph 的实现算子跑在独立 git worktree,且**要求源项目是干净 worktree**—— + 有未提交改动就用不了。相比 Orca 的并行 worktree(产品级封装), + Maka 的 Graph 更接近实验性能力。 + source: README「Terminal entry points」段 + maturity: |- + 治理成熟度最高(ASF 流程 + 评测框架 + 文档契约), + 产品成熟度最低(无正式 release、平台只支持 arm64 macOS、功能默认保守)。 + 属于「地基打得最实,房子盖得最慢」。 + +chart: + + form_depth: 6 # 桌面 + TUI + CLI + Eval 统一于 Runtime Host,但平台窄 + engine_neutral: 1 # 只跑自己的 Runtime,不参与多引擎竞争 + local_first_score: 10 # 完全本地,数据不出机器 + deliverable_depth: 6 # 代码 + artifact + 评测报告 + 执行记录 + +verdict: + + one_liner: 四个里唯一把「执行记录」当地基的项目——治理与可复现性最强,产品成熟度最弱。 + who_should_use: |- + 对审计、数据主权、可复现评测有硬要求的团队; + 做 agent 横向对比研究的工程团队。 + who_should_not: |- + 要开箱即用、要 Windows / Linux、要丰富第三方集成的人; + 以为 ASF 品牌等于生产可用的决策者。 + +mirror: + gitea_repo: gitinbox/maka + visibility: public + upstream: "https://github.com/apache/maka" + last_sync: 2026-09-01 + sync_note: "depth=1 浅仓被 Gitea 拒绝(shallow update not allowed),脚本自动 --unshallow 补齐完整历史后达 147 MB,超过 nginx 约 50 MB 的 client_max_body_size,遂降级为源码快照(12 MB,剔除 png/gif/mp4/字体等非源码素材)。完整保真裸仓保留在本地 .mirrors/maka.git(163 MB)。服务器放开 nginx 上限后可推送完整版。" + mode: source-only +article: + emlog_id: 0 # emlog 文章 ID,发布后回写 + published_at: "" + url: "" diff --git a/data/projects/openwork.yaml b/data/projects/openwork.yaml new file mode 100644 index 0000000..3d147b2 --- /dev/null +++ b/data/projects/openwork.yaml @@ -0,0 +1,226 @@ +# openwork · 项目档案 +# 生成于 2026-09-01 · 由 scripts/new_project.py 从 schema 生成 +# 人工补完重点:business / scenario / tech / risk.claim_vs_reality / verdict +# 字段规范见 schema/project.schema.yaml + +id: openwork +name: "openwork" +repo: "different-ai/openwork" +homepage: "https://openworklabs.com" +added_at: "2026-09-01" +status: active +identity: + tagline: "The open-source alternative to Claude Cowork (powered by opencode)" + vendor: "Different AI, Inc." + vendor_type: "vendor" + region: "美国(YC 背书)" + first_seen: "2026-01-14" +business: + + positioning: |- + 站在「能力分发层」,不争桌面入口。核心资产是 OpenWork MCP 与 Den 控制面—— + 把 skills / MCP / 连接服务做成可跨工具、跨人、跨机器复用的组织级能力包。 + target_users: + - 已在使用 Codex / Claude Code / Cursor 且不想换工具的开发者 + - 需要统一管控 agent 能力的中大型组织 + - 想把自己搭好的工作流分发给同事或朋友的团队 + monetization: |- + Den 订阅(组织控制面)。免费额度写得很明确: + 5 人以下组织的非 Enterprise 功能全免、任意规模 30 天评估、开发测试永久免费。 + license: + spdx: "NOASSERTION" + name: "目录切分许可(GitLab 式):ee/ 之外为 MIT;ee/ 为 OpenWork EE License" + openness: L3 + redistributable: false + key_restrictions: + - ee/ 目录(Den 控制面、MCP 网关、推理)生产使用需有效订阅;5 人以下组织在排除 Enterprise 功能后免费 + - 每个 ee/ 版本公开发布满两年后,自动追加 MIT 授权(Grant of Future License) + - 客户端侧资源(image / font / CSS / 编译产出的 JS)始终为 MIT + - 不得移除版权与许可声明;不授予商标使用权 + - 更早版本仍受原 FSL-1.1-MIT 约束,不自动升级到新许可 + moat: |- + 组织侧的能力分发与治理(谁能用什么模型、谁能拿到什么 skill), + 以及「一次创建、处处复用」带来的工作流网络效应。 + competitive_risk: |- + 最容易被 Anthropic / OpenAI 官方的 skills 市场与组织管理后台正面取代—— + OpenWork 的价值恰恰建立在「官方还没做好跨工具能力分发」这个时间窗口上。 + +scenario: + + primary: + - 把一套 skills + MCP + Google Workspace / Microsoft 365 连接,同时挂给 Codex、Claude Code、Cursor + - 组织统一发布能力、配置模型配额与访问权限 + - 桌面端作为专注工作区(可选,非必需) + typical_use_cases: + - 团队写好一套内部 skill,通过 Den 发布到组织,成员在各自惯用的 agent 里调 search_capabilities 直接用 + - 管理员限制部分成员只能使用指定 model provider,并统一配置共享连接 + - 个人把工作流分享给朋友,对方装一个 MCP 就能用 + industries: + - 软件研发 + - 企业 IT 与知识工作流 + interaction_mode: + - MCP 远程服务 + - 桌面应用(可选) + - 浏览器登录授权 + human_in_the_loop: |- + 能力调用需人工在浏览器登录并选择组织;企业侧由管理员通过策略管控。 + deliverables: + - workflow + - skill-package + - capability + +tech: + + form_factor: MCP 远程服务 + 可选桌面应用 + Web 控制面 + stack: + - Electron + - React + - TypeScript + - Node.js 24 + - pnpm + - MCP + engine_abstraction: + supported: + - Codex + - Claude Code + - Cursor + - OpenCode + - ChatGPT + - 任意 MCP client + mechanism: |- + 「反向注入」:不做引擎抽象层,而是把自己做成 MCP server 让各引擎来连。 + 对外只暴露两个工具——search_capabilities 与 execute_capability。 + abstraction_layer: OpenWork MCP(能力面,不是执行面) + depth_notes: |- + 接入「对等且都浅」:所有引擎都以同一个 MCP 客户端身份接入, + 拿到的是能力调用,不是执行控制——OpenWork 不接管 agent 的执行循环、 + 不负责上下文管理、不做权限回调。 + 真正深度绑定的是 OpenCode:仓库描述即写 powered by opencode, + 桌面端属 OpenCode 生态,其余引擎只是 MCP 外挂。 + 这与 iPolloWork 的「Codex 仅 MCP 控制面」是同一个问题的两种解法: + iPolloWork 把 MCP 当补丁(主体是桌面),OpenWork 把 MCP 当主体(桌面是可选)。 + extension_system: + skills: |- + 一等公民:可发布、可分配到组织/团队/个人;支持导入 Agent Plugins + 与 Anthropic 兼容插件并转为 OpenWork MCP 能力。 + plugins: 通过 marketplace 发布与分配 + mcp: |- + 自身即以 MCP server 形态对外提供服务(https://api.openworklabs.com/mcp/agent), + 同时也消费下游 MCP 连接。 + model_binding: Den 侧可统一配置与配额控制 model provider;桌面 app 本身 BYO + local_first: true + multi_agent: 未强调多 agent 编排,重心是能力的共享与治理 + permission_sandbox: |- + 组织级管控:桌面策略下发、限制本地模型访问、控制可用 app 版本、 + 成员与团队的访问控制。 + observability: |- + Den 提供推理配额管控与访问审计; + 桌面 app 侧未见 agent 执行轨迹的记录设计——它观测的是「谁用了什么能力」, + 不是「agent 怎么把事做完的」。 + +ecosystem: + + community: |- + YC 背书,23k star。贡献门槛是四个里最高的: + 强制 DCO sign-off(每个 commit 都要 Signed-off-by), + ee/ 目录的改动还需额外 CLA。 + standards: + - MCP + notable_events: + - 2026-01-14 建仓 + - 早期版本采用 FSL-1.1-MIT,后改为 MIT + EE 目录切分许可 + - ee/ 采用两年自动转 MIT 的 Grant of Future License 机制 + - 2026-09-01 达 23.2k star、版本 v0.18.40 + + metrics: + stars: 23253 + forks: 2307 + open_issues: 414 + subscribers: 89 + created_at: 2026-01-14 + pushed_at: 2026-09-01 + size_kb: 298398 + languages: {TypeScript: 89.9, JavaScript: 6.7, MDX: 1.2, CSS: 0.9, Shell: 0.6, Swift: 0.5} + latest_release: v0.18.40 + release_cadence: 约 1 天/版本 + data_date: 2026-09-01 + stale: false +risk: + maturity_level: "0.x 活跃" + + license_risk: |- + 整体 SPDX 为 NOASSERTION,但**排除 ee/ 目录后是纯 MIT,可以公开镜像**。 + 若完整镜像(含 ee/)则必须私有——EE License 明确要求生产使用需订阅, + 且禁止未经授权的复制与再分发。 + 对实操的影响很小:5 人以下组织免费、30 天评估免费、开发测试免费, + 个人与小团队几乎不会触发付费条件。 + vendor_lockin: |- + 锁定在 Den 控制面与能力包格式。一旦组织把内部 skills 都发布到 Den, + 迁移成本取决于是否有导出路径——README 未说明。 + claim_vs_reality: + - claim: |- + 「Claude Cowork 的开源替代品」 + actual: |- + 形态完全不同。Cowork 是桌面操控型 agent(直接操作你的电脑完成工作); + OpenWork 是能力分发型 MCP 层,桌面 app 被明确定位为可选—— + README 原文「The desktop app is there when you want a dedicated + workspace, but it is not required」。 + 它替代的是 Cowork 唤起的「能力复用」诉求,不是 Cowork 本身。 + source: README 开篇与「Use OpenWork from any agent」段 + - claim: |- + 跨 Codex / Claude Code / Cursor / OpenCode 多引擎通用 + actual: |- + 对 OpenCode 是原生的(仓库描述 powered by opencode,桌面端属其生态); + 对其余引擎只通过自己暴露的两个 MCP 工具对接。 + 这是能力调用面,不是引擎执行面——OpenWork 不接管这些引擎的执行循环、 + 上下文与权限。对外宣称的「跨引擎」与内部实际的主从关系存在落差。 + source: README「It exposes two tools」段;仓库 description「powered by opencode」 + - claim: |- + free, open-source(免费开源) + actual: |- + 需要分目录看:ee/ 之外确实 MIT 且免费;但 ee/(Den 组织控制面、 + MCP 网关、推理)属 source-available,生产使用需付费订阅, + 只有 5 人以下组织、30 天评估、开发测试三种情形豁免。 + 个人免费,团队与企业要付费。 + source: README Licensing 段 + ee/LICENSE 原文 72 行 + - claim: |- + (无明确宣称,但 SPDX 为 NOASSERTION,易被误读为「不够开源」) + actual: |- + 恰恰相反——它是四个项目里工程治理最规范的:强制 DCO sign-off、 + ee/ 需额外 CLA、evals/ 目录放可执行测试规格并要求 PR 附测试证据、 + worlds/ 放声明式的开发与测试环境定义。 + NOASSERTION 只因根目录 LICENSE 是混合声明文件,不代表开放度低。 + source: README「Getting started (contributors)」与「Sending a pull request」段 + maturity: |- + 工程成熟度最高:有可执行测试规格(evals/)、声明式环境(worlds/)、 + 明确的贡献契约与许可边界。但仍处 0.x 主版本(v0.18.40), + API 与数据格式可能变动。 + +chart: + + form_depth: 5 # 桌面只是可选壳,主体是 MCP 能力层 + engine_neutral: 6 # 接得住 4+ 引擎,但都只在能力面 + local_first_score: 6 # 可本地跑,但核心服务是云端 MCP + deliverable_depth: 5 # 交付工作流与能力包,不止代码 + +verdict: + + one_liner: 把「能力」而不是「桌面」做成产品——工程治理最好的一个,但它和自称要替代的东西不是一个物种。 + who_should_use: |- + 已固定在某款 agent 上、只想让团队能力跨工具复用的开发者; + 需要统一管控 agent 能力与模型配额的组织 IT。 + who_should_not: |- + 想要一个开箱即用的桌面 agent 工作台的人; + 以为拿到的是完整 Cowork 平替的人。 + +mirror: + gitea_repo: "gitinbox/openwork" + visibility: public + upstream: "https://github.com/different-ai/openwork" + last_sync: "" + mode: pending + sync_note: "暂未镜像。仓库 291 MB,按本机到 GitHub 实测 50 KB/s 计算需 1.6 小时以上,且 git clone 无断点续传。许可证为目录切分(ee/ 之外 MIT,ee/ 为 EE License)——若后续镜像,排除 ee/ 后为纯 MIT,可公开;完整镜像则必须私有。" +article: + emlog_id: 0 # emlog 文章 ID,发布后回写 + published_at: "" + url: "" diff --git a/data/projects/orca.yaml b/data/projects/orca.yaml new file mode 100644 index 0000000..8081b43 --- /dev/null +++ b/data/projects/orca.yaml @@ -0,0 +1,238 @@ +# orca · 项目档案 +# 生成于 2026-09-01 · 由 scripts/new_project.py 从 schema 生成 +# 人工补完重点:business / scenario / tech / risk.claim_vs_reality / verdict +# 字段规范见 schema/project.schema.yaml + +id: orca +name: "orca" +repo: "stablyai/orca" +homepage: "https://onOrca.dev" +added_at: "2026-09-01" +status: active +identity: + tagline: "Orca is the ADE for working with a fleet of parallel agents. Run any coding agent with your own subscription. Available on desktop, mobile and VPS." + vendor: "Stably AI(stablyai)" + vendor_type: "vendor" + region: "美国(据 Discord / App Store 与社群信息推断,未公开确认)" + first_seen: "2026-03-17" +business: + + positioning: |- + 站在「执行编排层」:不造引擎、不卖模型,只做并行 agent 的调度与可观测外壳。 + 上游是各家 CLI agent,下游是开发者的本机/远程 worktree。 + target_users: + - 自称 100x builders 的重度个人开发者 + - 需要同时试多种实现方案再择优合并的小团队 + - 已在用 Codex / Claude Code / OpenCode 并想并行跑的人 + monetization: |- + 未明示,且这是它最大的商业不确定性。MIT 全开源、无企业版、无订阅层, + README 明确不提供模型也不代买订阅。收入路径尚未公开。 + license: + spdx: "MIT" + name: "MIT License" + openness: L1 + redistributable: true + key_restrictions: + - 无实质性使用限制 + - MIT 不含商标许可,Orca 名称与 Logo 未单独授权 + moat: |- + 并行 worktree + 自研 WebGL 终端 + 移动端配对 + Design Mode 的组合体验, + 以及已铺开的 30+ agent 兼容面。功能广度本身构成短期壁垒。 + competitive_risk: |- + 最容易被 Anthropic / OpenAI 官方桌面端直接吃掉——因为 Orca 既不拥有引擎 + 也不拥有模型,只拥有外壳。一旦官方 IDE 原生支持并行 worktree,差异化立刻归零。 + +scenario: + + primary: + - 同一任务并行分派给多个 agent 各自实现,人工比对后合并优胜解 + - 在远程大机上跑 agent(SSH worktree) + - 移动端监控 agent 并追加指令 + typical_use_cases: + - 一条 prompt 扇出到 5 个 agent,各自在独立 git worktree 里改,最后 diff 对比选一个 merge + - 开 Design Mode 点页面元素,把 HTML/CSS/裁剪截图直接塞进 agent 的提示词 + - 通勤路上用手机查看刚跑完的 agent 结果并追加一句 follow-up + industries: + - 软件研发 + - 独立开发者 / 外包交付 + interaction_mode: + - 桌面应用 + - 移动端伴随 + - CLI + - SSH 远程 + human_in_the_loop: |- + 强人在环:需人工比并结果、批注 diff、批准合并。 + 但 5000 个自开 issue 说明产品自身也在被 AI 自动改进。 + deliverables: + - code + - diff + - review-comment + +tech: + + form_factor: 桌面应用(含移动端伴随 + CLI) + stack: + - Electron + - React + - TypeScript + - Node.js + - WebGL 终端 + - Swift(iOS 伴随端) + engine_abstraction: + supported: + - Codex + - Claude Code + - OpenCode + - Pi + - Grok + - Cursor + - GitHub Copilot + - MiMo Code + - Amp + - Antigravity + - Hermes Agent + - Devin + - Goose + - Auggie + - Cline + - Continue + - Droid + - Kilocode + - Kimi + - Kiro + - Mistral Vibe + - Qwen Code + - Rovo Dev + - oh-my-pi + mechanism: |- + 「只要在终端里能跑」即为兼容——通过 PTY 拉起任意 CLI agent, + 不做协议级抽象,不解析结构化 tool call。 + abstraction_layer: 无统一抽象层;实际是终端 PTY + 约定式输出解析 + depth_notes: |- + 接入深度「完全对等,但对等在低处」:30+ 引擎共享同一套 PTY 外壳, + 因此没有任何一家能拿到原生能力——拿不到结构化 tool call、 + 拿不到统一权限回调、拿不到一致口径的 token 统计。 + 广度最大,深度最浅。对照 iPolloWork 为 OpenCode 做的原生 + Engine Protocol adapter,Orca 与任一单引擎的耦合都明显更浅。 + extension_system: + skills: README 未提及独立 skills 体系 + plugins: 无插件机制,能力依赖各 agent 自身生态 + mcp: 未作为一等公民提及 + model_binding: 完全 BYO——用户自带各 agent 的订阅与额度 + local_first: true + multi_agent: |- + 并行 worktree 编排:一条 prompt 扇出到 N 个独立 git worktree, + 比对结果后合并优胜解;另有任务页与 CLI 编排能力。 + permission_sandbox: |- + README 未强调沙箱边界。Computer Use(操控桌面应用)与 SSH 远程执行 + 均属高权限面,且未见统一的越界审批机制描述。 + observability: |- + 终端分屏 + AI diff 批注 + 通知与未读态 + 用量与限流追踪 + 账号热切换。 + 观测的是「agent 的输出与结果」,不是「agent 的内部执行轨迹」。 + +ecosystem: + + community: |- + Discord + X(@orca_build) + 微信社群(已开到第 8 群),中文市场渗透明显。 + 但 5000 个 open issue 中绝大多数由主力开发者自开,社区 triage 尚未成型。 + standards: [] + notable_events: + - 2026-03-17 建仓 + - 2026-09-01 达 58.7k star、5000 open issue、版本 v1.4.194 + + metrics: + stars: 58771 + forks: 3987 + open_issues: 5000 + subscribers: 121 + created_at: 2026-03-17 + pushed_at: 2026-09-01 + size_kb: 687575 + languages: {TypeScript: 97.0, JavaScript: 2.3, Swift: 0.2, CSS: 0.2, HTML: 0.1, PowerShell: 0.1} + latest_release: v1.4.194 + release_cadence: 约 1 天/版本 + data_date: 2026-09-01 + stale: false +risk: + maturity_level: "原型期" + + license_risk: |- + MIT,无许可证风险。但「商业模式的空白」本身就是风险: + 一旦融资或转型,存在事后变更许可的可能(参考 Redis / Terraform 先例)。 + vendor_lockin: |- + 锁定点很弱——这既是优点也是缺点。用户锁的是 worktree 布局与 UI 习惯, + 不是数据格式,迁移成本低;但反过来用户粘性也低。 + claim_vs_reality: + - claim: |- + 5000 个 open issue 反映社区问题积压与项目质量压力 + actual: |- + 抽样最近 100 条:全部创建于 2026-09-01 同一天,94% 无任何标签, + 标题为 fix(ssh) / refactor / fix(task-page) 等内部任务口吻; + 提交者 TOP3 为 nwparker(51)、OrcaWin(13)、klaidliadon(12)。 + 实为「团队自开的任务待办板」,且带明显 AI 批量生成痕迹 + (如 OrcaWin 提交的「pty.spawn can leak a running agent process + unboundedly: host holds the door open」)。 + 它不是社区投诉,也不构成 triage 压力——但同样说明这 5000 条 + 从未被当作对外承诺来管理。 + source: GitHub API /repos/stablyai/orca/issues?state=open,2026-09-01 抽样 100 条 + - claim: |- + 「100x builders 的 AI Orchestrator」,面向多人协作的成熟开发环境 + actual: |- + 巴士系数为 1:最近 100 次提交中 nwparker 独占 79 次, + 第二名 AmethystLiang 仅 9 次。功能面由单人 + AI 高强度驱动, + 而非团队协作成果。 + source: GitHub API /repos/stablyai/orca/commits?per_page=100,2026-09-01 + - claim: |- + 支持 30+ 种 agent,生态最开放 + actual: |- + 兼容判据是「能在终端里跑」,靠 PTY 拉起而非协议对接。 + 广度最大但深度最浅——所有引擎都拿不到结构化 tool call + 与统一的权限回调。与 iPolloWork 为 OpenCode 做原生 + Engine Protocol adapter 相比,Orca 与任一单引擎的耦合都更浅。 + source: README「Works with any CLI agent — if it runs in a terminal, it runs in Orca」 + - claim: |- + MIT 全开源,可放心用于生产 + actual: |- + 许可证本身无风险,但项目没有任何公开的营收路径 + (无企业版、无订阅、不卖模型、不代买订阅)。 + 58k star 的体量与零商业化不匹配,属于「要么即将融资、 + 要么后续变更条款」的不稳定态。 + source: README License 段;README 明确「Orca does not provide models, nor does it buy subscriptions for you」 + - claim: |- + 桌面 / 移动 / VPS 全覆盖 + actual: |- + 三端成熟度不对等:Android 仍是手工分发 APK(0.0.47)、 + iOS 走 App Store 与 TestFlight、VPS 场景需另查 headless Linux 指南。 + 移动端实际是「监控与追加指令」的伴随形态,不是完整工作环境。 + source: README Install 段与 Mobile Companion 段 + maturity: |- + 功能广度第一,工程治理末位。一天多版(v1.4.194)说明迭代极快, + 但 issue 只进不出、巴士系数为 1,属于「高速原型期」而非「可托付生产期」。 + +chart: + + form_depth: 8 # 桌面 + 移动伴随 + CLI + SSH,形态铺得最广 + engine_neutral: 9 # 30+ 引擎,兼容面第一 + local_first_score: 6 # 本地起进程,但强依赖各家云端订阅 + deliverable_depth: 3 # 主要产出 code / diff / review comment + +verdict: + + one_liner: 兼容面最广的并行 agent 编排外壳——广度做到极致,深度与治理都还是草稿。 + who_should_use: |- + 已在重度使用 CLI agent、想同时跑多个方案横向比对的个人开发者与小团队。 + who_should_not: |- + 需要稳定 SLA、指望社区 triage 与多人维护保障的团队; + 指望它统一各引擎能力(结构化调用、统一权限、一致口径)的架构选型者。 + +mirror: + gitea_repo: "gitinbox/orca" + visibility: public + upstream: "https://github.com/stablyai/orca" + last_sync: "" + mode: pending + sync_note: "暂未镜像。仓库 671 MB,按实测 50 KB/s 需 3.7 小时,且 git clone 无断点续传,中途失败即全部作废。MIT 许可(L1)无分发障碍,纯粹是带宽约束。待服务器侧放开或改用服务端迁移后再补。" +article: + emlog_id: 0 # emlog 文章 ID,发布后回写 + published_at: "" + url: "" diff --git a/data/registry.yaml b/data/registry.yaml index 0116a6f..6f8a03f 100644 --- a/data/registry.yaml +++ b/data/registry.yaml @@ -8,3 +8,22 @@ projects: added_at: 2026-09-01 license_level: L3 # source-available,镜像私有 mirror_visibility: private + - id: openwork + repo: different-ai/openwork + status: active + added_at: 2026-09-01 + license_level: L3 # 目录切分许可:ee/ 之外 MIT,ee/ 为 EE License + mirror_visibility: public # 人工复核:镜像时排除 ee/ 后为纯 MIT,故可公开 + mirror_excludes: ["ee/"] + - id: orca + repo: stablyai/orca + status: active + added_at: 2026-09-01 + license_level: L1 + mirror_visibility: public + - id: maka + repo: apache/maka + status: active + added_at: 2026-09-01 + license_level: L1 + mirror_visibility: public diff --git a/data/snapshots/maka.jsonl b/data/snapshots/maka.jsonl new file mode 100644 index 0000000..76bdfee --- /dev/null +++ b/data/snapshots/maka.jsonl @@ -0,0 +1 @@ +{"stars": 4373, "forks": 406, "open_issues": 398, "subscribers": 23, "created_at": "2026-05-27", "pushed_at": "2026-09-01", "size_kb": 102390, "languages": {"TypeScript": 91.7, "JavaScript": 4.5, "Rust": 1.7, "CSS": 1.3, "Python": 0.5, "PowerShell": 0.2}, "latest_release": "v0.2.0-dev.9.20260831", "release_cadence": "一天多版", "data_date": "2026-09-01", "stale": false} diff --git a/data/snapshots/openwork.jsonl b/data/snapshots/openwork.jsonl new file mode 100644 index 0000000..d41d7db --- /dev/null +++ b/data/snapshots/openwork.jsonl @@ -0,0 +1 @@ +{"stars": 23253, "forks": 2307, "open_issues": 414, "subscribers": 89, "created_at": "2026-01-14", "pushed_at": "2026-09-01", "size_kb": 298398, "languages": {"TypeScript": 89.9, "JavaScript": 6.7, "MDX": 1.2, "CSS": 0.9, "Shell": 0.6, "Swift": 0.5}, "latest_release": "v0.18.40", "release_cadence": "约 1 天/版本", "data_date": "2026-09-01", "stale": false} diff --git a/data/snapshots/orca.jsonl b/data/snapshots/orca.jsonl new file mode 100644 index 0000000..9e875e5 --- /dev/null +++ b/data/snapshots/orca.jsonl @@ -0,0 +1 @@ +{"stars": 58771, "forks": 3987, "open_issues": 5000, "subscribers": 121, "created_at": "2026-03-17", "pushed_at": "2026-09-01", "size_kb": 687575, "languages": {"TypeScript": 97.0, "JavaScript": 2.3, "Swift": 0.2, "CSS": 0.2, "HTML": 0.1, "PowerShell": 0.1}, "latest_release": "v1.4.194", "release_cadence": "约 1 天/版本", "data_date": "2026-09-01", "stale": false} diff --git a/schema/license-tiers.yaml b/schema/license-tiers.yaml index 8a1c624..cc2ddda 100644 --- a/schema/license-tiers.yaml +++ b/schema/license-tiers.yaml @@ -67,6 +67,24 @@ manual_review_triggers: - "README 自称 open source 但 SPDX 不是 OSI 认证标识(名实不符,需显式记录)" - "LICENSE 中出现 SaaS / hosting / white-label / marketplace / resale 任一关键词" +# ---------- 混合许可的降级规则 ---------- +# 自动定级只认 SPDX 标识,无法识别「目录切分许可」这类混合形态。 +# 遇到需人工复核的仓库,按此流程处理: +mixed_license_procedure: + - "定位受限目录:读根目录 LICENSE 与各子目录 LICENSE,找出采用非 OSI 许可的路径" + - "评估剩余部分:确认排除受限目录后,其余内容是否为单一 OSI 许可" + - "若成立 -> 镜像时排除受限目录,可见性按剩余部分的许可定级(通常可 public)" + - "若不成立 -> 整仓按最严格的那个许可定级(通常 L3 / private)" + - "必须在档案 mirror.sync_note 与仓库内 MIRROR-NOTE.md 写明:排除了什么、为什么、如何恢复完整镜像" + +# 补这条规则的缘由(注意:这是独立顶层键,不能写进上面的列表里—— +# YAML 不允许序列与映射键同级混用,写进去会直接让整份 schema 解析失败) +mixed_license_note: | + 这条规则是 2026-09-01 处理 different-ai/openwork 时补上的。 + 自动定级把它判成 L3/private,但人工复核后发现 ee/ 之外是纯 MIT, + 排除 ee/ 后完全可以公开镜像——同一个仓库,两种正确答案, + 取决于镜像是否包含受限目录。没有这条规则就会做出过度保守的误判。 + # ---------- 已定级样本(供后续同类项目参照) ---------- precedents: - repo: Devin-AXIS/iPolloWork @@ -79,3 +97,26 @@ precedents: 面向客户的使用,无论个人还是企业,均需事先书面授权;且面向用户的前端 必须保留 iPolloWork 名称、Logo 与产品归属。公开镜像落入「托管/市场分发」 的解释空间,故设为私有。 + + - repo: different-ai/openwork + level: L3 + visibility: public + decided_at: 2026-09-01 + manual_review: true + trigger_hit: + - "同一仓库存在多个 LICENSE 文件(LICENSE + ee/LICENSE)" + - "README 自称 open source 但 SPDX 为 NOASSERTION" + - "ee/LICENSE 含 production / subscription / resale 关键词" + license_name: "目录切分许可:ee/ 之外 MIT;ee/ 为 OpenWork EE License" + reason: | + 根目录 LICENSE(36 行)声明 ee/ 之外的部分为 MIT; + ee/LICENSE(72 行)为 OpenWork EE License,要求生产使用需订阅, + 仅 5 人以下组织、30 天评估、开发测试三种情形豁免, + 且每个 ee/ 版本公开发布满两年后自动追加 MIT 授权。 + **排除 ee/ 目录后,镜像内容为纯 MIT,可公开分发**; + 若完整镜像(含 ee/)则必须私有。 + mirror_strategy: "镜像时排除 ee/ -> public;完整镜像 -> private" + lesson: | + 这是「自动定级过度保守」的典型案例:SPDX=NOASSERTION 直接判 L3/private + 是安全的默认值,但对目录切分许可仓库会误伤。 + 已据此补充 mixed_license_procedure 规则。 diff --git a/scripts/_common.py b/scripts/_common.py index 4d00e8b..aa0394d 100755 --- a/scripts/_common.py +++ b/scripts/_common.py @@ -167,6 +167,52 @@ def update_block(text: str, block_key: str, values: dict, indent: int | None = N return "\n".join(lines) +def upsert_block( + text: str, block_key: str, values: dict, parent: str, indent: int | None = None +) -> str: + """update_block 的容错版:块不存在时,在 parent 块末尾**新建**它。 + + 为什么需要这个:手工补完档案时习惯整块替换(ecosystem、business 等), + 一不留神就会把块内的子块(如 ecosystem.metrics)一起覆盖掉。 + update_block 遇到块缺失只会抛 ValueError,被上层 catch 成一条警告, + 表现是「指标栏静默变空」——很难察觉,等发现时原始数据已经没了。 + 有了 upsert,这类误删能自动长回来。 + """ + try: + return update_block(text, block_key, values, indent) + except ValueError: + pass + + lines = text.split("\n") + pat = re.compile(rf"^(\s*){re.escape(parent)}:\s*$") + pstart = None + pbase = 0 + for i, ln in enumerate(lines): + m = pat.match(ln) + if m: + pstart, pbase = i, len(m.group(1)) + break + if pstart is None: + raise ValueError(f"未找到父块 `{parent}:`,无法新建 `{block_key}:`") + + pend = len(lines) + for j in range(pstart + 1, len(lines)): + ln = lines[j] + if not ln.strip() or ln.lstrip().startswith("#"): + continue + if len(ln) - len(ln.lstrip()) <= pbase: + pend = j + break + + child = pbase + 2 + grand = child + 2 + new = [f"{' ' * child}{block_key}:"] + [ + f"{' ' * grand}{k}: {_fmt(v)}" for k, v in values.items() + ] + lines[pend:pend] = new + return "\n".join(lines) + + # ---------------------------------------------------------------- 快照 def snapshot_path(pid: str) -> Path: return SNAPSHOTS / f"{pid}.jsonl" diff --git a/scripts/build_matrix.py b/scripts/build_matrix.py index 3d87872..08934b7 100755 --- a/scripts/build_matrix.py +++ b/scripts/build_matrix.py @@ -28,6 +28,28 @@ from _common import ARTICLES, PROJECTS, load_yaml # noqa: E402 QUAD_THRESHOLD = 4 BEGIN, END = "", "" +IDX_BEGIN, IDX_END = "", "" +OVERVIEW_AID = "00-overview" + + +def short_maturity(risk: dict) -> str: + """成熟度在档案里是一整段判断(几十字),直接塞进表格会把列撑爆。 + + 优先用档案给的 `maturity_level` 短标签(人工判断最准); + 没有则从长文本截取第一个分句兜底。 + """ + r = risk or {} + lv = r.get("maturity_level") + if lv: + return str(lv) + m = (r.get("maturity") or "").strip() + if not m: + return "-" + for sep in ("。", ";", "\n"): + if sep in m: + m = m.split(sep)[0] + break + return m[:14] + ("…" if len(m) > 14 else "") def cell(v, empty="-") -> str: @@ -93,9 +115,18 @@ def build_tables(items: list[dict]) -> str: stale = " ⚠" if m.get("stale") else "" add(f"| **{d.get('name','')}** | {cell(m.get('stars'))} | {cell(m.get('forks'))} | " f"{cell(m.get('open_issues'))} | {cell(m.get('latest_release'))} | " - f"{cell(m.get('release_cadence'))} | {cell((d.get('risk') or {}).get('maturity'))} | " + f"{cell(m.get('release_cadence'))} | {short_maturity(d.get('risk') or {})} | " f"{cell(m.get('data_date'))}{stale} |") + # 成熟度是长文本,塞进表格会把列撑爆。表格里只放短标签, + # 完整的成熟度判断单独列在表后,读者想看细节再往下读。 + mats = [(d.get("name", ""), (d.get("risk") or {}).get("maturity")) for d in items] + if any(v for _, v in mats): + add("\n**成熟度详解**\n") + for name, v in mats: + if v: + add(f"- **{name}**:{v}") + # 宣称 vs 实测:全套矩阵里信息密度最高的一张表 rows = [] for d in items: @@ -121,6 +152,87 @@ def build_tables(items: list[dict]) -> str: return "\n".join(L) +# ---------------------------------------------------------------- 文章索引 +def read_h1(aid: str) -> str: + """取 articles/.md 的第一个一级标题作为文章名。""" + p = ARTICLES / f"{aid}.md" + if not p.exists(): + return aid + for line in p.read_text(encoding="utf-8").lstrip().split("\n"): + if line.startswith("# "): + return line[2:].strip() + return aid + + +def collect_cross_articles() -> list[dict]: + """跨项目文章(总纲 / 横评 / 专题)的发布元数据。 + + ID 落在 articles/.meta.yaml 里,由 publish_emlog.py 维护。 + 总纲文自身要排除——索引出现在总纲内部,自己链自己没有意义。 + """ + out = [] + for mp in sorted(ARTICLES.glob("*.meta.yaml")): + aid = mp.name[: -len(".meta.yaml")] + if aid == OVERVIEW_AID: + continue + d = load_yaml(mp) or {} + if not d.get("emlog_id"): + continue + out.append({ + "aid": aid, + "title": read_h1(aid), + "emlog_id": d["emlog_id"], + "url": d.get("url") or f"https://blog.apescale.com/?post={d['emlog_id']}", + "last_published": d.get("last_published", ""), + }) + return out + + +def article_index_section(items: list[dict]) -> str: + """文章索引。 + + 这段原来是手抄的,结果新发了横评文、索引里却没有它——手写的索引 + 每发一篇文章就会漂移一次。改成从两处事实源生成: + 项目深度文 ← data/projects/*.yaml 的 article 块 + 跨项目文章 ← articles/*.meta.yaml + """ + L = [""] + cross = collect_cross_articles() + + L.append("| 项目 | 一句话结论 | 深度文 |") + L.append("| --- | --- | --- |") + fallback = cross[0]["url"] if cross else "" + for d in items: + art = d.get("article") or {} + url = art.get("url") or "" + if url: + link = f"[已发布]({url})" + elif fallback: + link = f"待发布(见下方横评)" + else: + link = "待发布" + one = (d.get("verdict") or {}).get("one_liner") or "-" + # 一句话结论本来就长,索引表里截断到第一个分句,避免一行几十字 + for sep in ("——", "。", ";"): + if sep in one: + one = one.split(sep)[0] + break + L.append(f"| **{d.get('name','')}** | {one} | {link} |") + + if cross: + L.append("\n**跨项目文章**\n") + for a in cross: + L.append(f"- [{a['title']}]({a['url']}) — {a['last_published']}") + + pending = [d.get("name", "") for d in items if not (d.get("article") or {}).get("url")] + if pending: + L.append(f"\n> 待补深度文:{'、'.join(pending)}。" + f"当前可先读上方跨项目文章,其中已包含这些项目的完整分维度对比。") + + L.append("") + return "\n".join(L) + + # ---------------------------------------------------------------- 图 def stack_diagram(items: list[dict]) -> str: """项目 <4 个时的赛道分层图:按交互形态把项目归到三层栈。""" @@ -161,8 +273,12 @@ def quadrant_svg(items: list[dict]) -> str: svg = [] for i, (title, xk, yk, xlo, xhi, ylo, yhi) in enumerate(plots): ox = i * (W + 30) + # max-width:100% + height:auto 不能省:SVG 带固定 width/height 属性, + # 在窄屏(手机、窄主题)里会撑破容器横向溢出。有 viewBox 时浏览器 + # 才能按这两个声明等比缩放。 svg.append(f'') + f'xmlns="http://www.w3.org/2000/svg" ' + f'style="font-family:sans-serif;max-width:100%;height:auto">') svg.append(f'{title}') px, py, pw, ph = PAD, 55, W - PAD * 2, H - PAD - 55 @@ -190,7 +306,16 @@ def quadrant_svg(items: list[dict]) -> str: name = d.get("name", "") svg.append(f'') - svg.append(f'{name}') + # 标签默认画在点右侧,但满分点会顶到绘图区右边界(cx == px+pw), + # 文字就会溢出画布被裁掉。靠右时翻到点的左侧。 + # 估算文字宽度:中文按 12px、其余按 7px,够用且不依赖字体度量。 + tw = sum(12 if ord(ch) > 0x2E80 else 7 for ch in name) + if cx + 11 + tw > W - 4: + svg.append(f'{name}') + else: + svg.append(f'{name}') svg.append("") return "\n".join(svg) @@ -216,7 +341,7 @@ def main() -> int: print(block) return 0 - path = ARTICLES / f"{'00-overview'}.md" + path = ARTICLES / f"{OVERVIEW_AID}.md" if not path.exists(): print(f"总纲文不存在:{path}") return 1 @@ -225,6 +350,15 @@ def main() -> int: print(f"总纲文缺少 {BEGIN} / {END} 占位标记,未回填。") return 1 text = re.sub(rf"{re.escape(BEGIN)}.*?{re.escape(END)}", lambda _: block, text, flags=re.S) + + # 文章索引同样回填,两个块各管一段,互不干扰 + index = article_index_section(items) + if IDX_BEGIN in text and IDX_END in text: + text = re.sub(rf"{re.escape(IDX_BEGIN)}.*?{re.escape(IDX_END)}", + lambda _: index, text, flags=re.S) + else: + print(f" [warn] 总纲文缺少 {IDX_BEGIN} / {IDX_END},文章索引未回填") + path.write_text(text, encoding="utf-8") print(f"==> 已回填总纲文:{path}") return 0 diff --git a/scripts/fetch_stats.py b/scripts/fetch_stats.py index 0271eb4..4fe648e 100755 --- a/scripts/fetch_stats.py +++ b/scripts/fetch_stats.py @@ -34,6 +34,7 @@ from _common import ( # noqa: E402 load_yaml, today, update_block, + upsert_block, ) @@ -125,7 +126,9 @@ def fetch_one(pid: str, repo: str, force: bool) -> bool: if ypath.exists(): text = ypath.read_text(encoding="utf-8") try: - text = update_block(text, "metrics", metrics) + # 用 upsert 而非 update:手工补完档案时若整块覆盖了 ecosystem, + # metrics 子块会消失,upsert 能把它重建回来。 + text = upsert_block(text, "metrics", metrics, parent="ecosystem") except ValueError as e: print(f" [warn] 回写 metrics 失败:{e}") # 许可证若尚未定级,顺手补上 diff --git a/scripts/mirror_sync.sh b/scripts/mirror_sync.sh index 8684096..a48ac02 100755 --- a/scripts/mirror_sync.sh +++ b/scripts/mirror_sync.sh @@ -16,29 +16,35 @@ DISPLAY_URL="${GITEA_DISPLAY_URL:-https://gitea.apescale.com}" ORG="${GITEA_ORG:-gitinbox}" ID="" DEPTH="" +# auto = 体积超限时自动降级为源码快照;no = 始终尝试完整镜像(超限会 413 失败)。 +# 这两个变量必须在 set -u 之前就有值:下面体积闸门的条件里会引用它们, +# 而这个分支只在特定体积下才走到——不初始化就成了「偶尔才炸」的雷。 +SOURCE_ONLY="auto" +SOURCE_ONLY_FORCE="no" while [[ $# -gt 0 ]]; do case "$1" in - --id) ID="$2"; shift 2;; - --org) ORG="$2"; shift 2;; - --url) GITEA_URL="$2"; shift 2;; - --depth) DEPTH="$2"; shift 2;; + --id) ID="$2"; shift 2;; + --org) ORG="$2"; shift 2;; + --url) GITEA_URL="$2"; shift 2;; + --depth) DEPTH="$2"; shift 2;; + --no-source-only) SOURCE_ONLY="no"; shift;; *) echo "未知参数: $1" >&2; exit 1;; esac done -[[ -z "$ID" ]] && { echo "缺少 --id" >&2; exit 1; } -YAML="$ROOT/data/projects/$ID.yaml" -[[ -f "$YAML" ]] || { echo "档案不存在: $YAML" >&2; exit 1; } +[[ -z "${ID}" ]] && { echo "缺少 --id" >&2; exit 1; } +YAML="${ROOT}/data/projects/${ID}.yaml" +[[ -f "${YAML}" ]] || { echo "档案不存在: ${YAML}" >&2; exit 1; } # ---- 从档案读取镜像配置(bash 不适合解析 YAML,借用 python 输出 shell 赋值) ---- # 用"写临时文件再 source"而不是 eval "$(...)": # eval 会二次解析内容,YAML 里的括号/引号一旦漏转义就会变成难定位的 # syntax error,而且报错行号会偏移到完全无关的位置。 CONF_FILE="$(mktemp)" -trap 'rm -f "$CONF_FILE"' EXIT +trap 'rm -f "${CONF_FILE}"' EXIT -/Users/figmarlite/.workbuddy/binaries/python/envs/default/bin/python - "$YAML" > "$CONF_FILE" <<'PY' +/Users/figmarlite/.workbuddy/binaries/python/envs/default/bin/python - "${YAML}" > "${CONF_FILE}" <<'PY' import sys, shlex from pathlib import Path sys.path.insert(0, str(Path(sys.argv[1]).resolve().parent.parent.parent / "scripts")) @@ -56,81 +62,233 @@ print(f"LIC_NAME={shlex.quote(str(lic.get('name') or ''))}") PY # shellcheck disable=SC1090 -source "$CONF_FILE" +source "${CONF_FILE}" -[[ -z "$UPSTREAM" ]] && { echo "档案缺少 mirror.upstream" >&2; exit 1; } +[[ -z "${UPSTREAM}" ]] && { echo "档案缺少 mirror.upstream" >&2; exit 1; } -echo "==> 镜像 $ID" -echo " 上游 : $UPSTREAM" -echo " 目标 : $ORG/$REPO_NAME" -echo " 许可证 : $LIC_LEVEL ($LIC_NAME)" -echo " 可见性 : $VISIBILITY" -if [[ "$VISIBILITY" == "private" ]]; then +echo "==> 镜像 ${ID}" +echo " 上游 : ${UPSTREAM}" +echo " 目标 : ${ORG}/${REPO_NAME}" +echo " 许可证 : ${LIC_LEVEL} (${LIC_NAME})" +echo " 可见性 : ${VISIBILITY}" +if [[ "${VISIBILITY}" == "private" ]]; then echo " ⚠ L3 级 source-available,已强制私有。公开镜像可能构成未经授权的分发。" fi # ---- 凭据:优先环境变量,回退到 macOS 钥匙串 ---- if [[ -z "${GITEA_TOKEN:-}" ]]; then CRED="$(printf "host=%s\nprotocol=https\n" "${DISPLAY_URL#*://}" | git credential-osxkeychain get 2>/dev/null || true)" - GITEA_USER="${GITEA_USER:-$(printf '%s' "$CRED" | sed -n 's/^username=//p')}" - GITEA_TOKEN="$(printf '%s' "$CRED" | sed -n 's/^password=//p')" + GITEA_USER="${GITEA_USER:-$(printf '%s' "${CRED}" | sed -n 's/^username=//p')}" + GITEA_TOKEN="$(printf '%s' "${CRED}" | sed -n 's/^password=//p')" fi [[ -z "${GITEA_TOKEN:-}" || -z "${GITEA_USER:-}" ]] && { echo "未拿到 Gitea 凭据(设 GITEA_TOKEN/GITEA_USER,或存入钥匙串)" >&2; exit 1; } # ---- 确保目标仓库存在(组织端点,不是 /user/repos) ---- -API="$GITEA_URL/api/v1" -HTTP="$(curl -sS -o /dev/null -w '%{http_code}' -u "$GITEA_USER:$GITEA_TOKEN" "$API/repos/$ORG/$REPO_NAME" || echo 000)" -if [[ "$HTTP" == "200" ]]; then +API="${GITEA_URL}/api/v1" +HTTP="$(curl -sS -o /dev/null -w '%{http_code}' -u "${GITEA_USER}:${GITEA_TOKEN}" "${API}/repos/${ORG}/${REPO_NAME}" || echo 000)" +if [[ "${HTTP}" == "200" ]]; then echo " 仓库已存在" -elif [[ "$HTTP" == "404" ]]; then - echo " 创建仓库 $ORG/$REPO_NAME (private=$([[ $VISIBILITY == private ]] && echo true || echo false)) ..." - CREATE="$(curl -sS -o /tmp/gitea_mirror_create.json -w '%{http_code}' -u "$GITEA_USER:$GITEA_TOKEN" \ - -H 'Content-Type: application/json' -X POST "$API/org/$ORG/repos" \ - -d "{\"name\":\"$REPO_NAME\",\"private\":$([[ $VISIBILITY == private ]] && echo true || echo false),\"auto_init\":false,\"description\":\"Mirror of $UPSTREAM (license: $LIC_NAME)\"}")" - [[ "$CREATE" =~ ^(200|201)$ ]] || { - echo "创建失败 (HTTP $CREATE):" >&2; cat /tmp/gitea_mirror_create.json >&2; exit 1; } +elif [[ "${HTTP}" == "404" ]]; then + echo " 创建仓库 ${ORG}/${REPO_NAME} (private=$([[ ${VISIBILITY} == private ]] && echo true || echo false)) ..." + CREATE="$(curl -sS -o /tmp/gitea_mirror_create.json -w '%{http_code}' -u "${GITEA_USER}:${GITEA_TOKEN}" \ + -H 'Content-Type: application/json' -X POST "${API}/org/${ORG}/repos" \ + -d "{\"name\":\"${REPO_NAME}\",\"private\":$([[ ${VISIBILITY} == private ]] && echo true || echo false),\"auto_init\":false,\"description\":\"Mirror of ${UPSTREAM} (license: ${LIC_NAME})\"}")" + [[ "${CREATE}" =~ ^(200|201)$ ]] || { + echo "创建失败 (HTTP ${CREATE}):" >&2; cat /tmp/gitea_mirror_create.json >&2; exit 1; } echo " 已创建" else - echo "查询仓库失败 (HTTP $HTTP)" >&2; exit 1 + echo "查询仓库失败 (HTTP ${HTTP})" >&2; exit 1 fi # 已存在的仓库若应为私有,显式修正(创建时的 private 对已存在仓库不生效) -if [[ "$VISIBILITY" == "private" ]]; then - curl -sS -o /dev/null -u "$GITEA_USER:$GITEA_TOKEN" -X PATCH \ - -H 'Content-Type: application/json' -d '{"private":true}' "$API/repos/$ORG/$REPO_NAME" || true +if [[ "${VISIBILITY}" == "private" ]]; then + curl -sS -o /dev/null -u "${GITEA_USER}:${GITEA_TOKEN}" -X PATCH \ + -H 'Content-Type: application/json' -d '{"private":true}' "${API}/repos/${ORG}/${REPO_NAME}" || true fi # ---- 同步 ---- -MIRROR_DIR="$ROOT/.mirrors/$REPO_NAME.git" -mkdir -p "$ROOT/.mirrors" +MIRROR_DIR="${ROOT}/.mirrors/${REPO_NAME}.git" +mkdir -p "${ROOT}/.mirrors" -if [[ -d "$MIRROR_DIR" ]]; then - echo " 更新已有裸仓 ..." - git -C "$MIRROR_DIR" remote update --prune -else - echo " 首次全量镜像(大仓请耐心)..." - if [[ -n "$DEPTH" ]]; then - git clone --mirror --depth "$DEPTH" "$UPSTREAM" "$MIRROR_DIR" +DEEPEN_STEP="${DEEPEN_STEP:-100}" + +# 注意:不能用 `rev-parse --git-dir` 判断仓库是否有效。git clone 会先建好 +# 目录骨架再下载 pack,被中断的半成品照样能通过 rev-parse,于是被当成 +# "已有仓库"走 fetch 分支,报出 "CONNECT tunnel failed" 这类误导性错误。 +# 真正的判据是"里面有没有提交"。 +repo_ok() { + [[ -d "${MIRROR_DIR}" ]] || return 1 + git -C "${MIRROR_DIR}" rev-parse --git-dir >/dev/null 2>&1 || return 1 + local n + n="$(git -C "${MIRROR_DIR}" rev-list --count --all 2>/dev/null || echo 0)" + [[ "${n}" -gt 0 ]] +} +is_shallow() { [[ "$(git -C "${MIRROR_DIR}" rev-parse --is-shallow-repository 2>/dev/null || echo false)" == "true" ]]; } + +# clone/fetch 失败时清掉半成品,避免下次误判(脚本有 set -e,失败会直接退出) +cleanup_broken() { + if [[ -d "${MIRROR_DIR}" ]] && ! repo_ok; then + echo " 清理未完成的半成品裸仓 ..." + rm -rf "${MIRROR_DIR}" + fi +} +trap cleanup_broken EXIT + +if [[ -d "${MIRROR_DIR}" ]] && ! repo_ok; then + echo " 清理上次中断的半成品裸仓 ..." + rm -rf "${MIRROR_DIR}" +fi + +if repo_ok; then + if is_shallow && [[ -z "${DEPTH}" ]]; then + # 慢链路续传:分批 --deepen。每批成功即落盘,中断后重跑本脚本可继续, + # 不像 `git clone` 那样失败一次全部作废。 + echo " 浅仓分批续传(每批 --deepen=${DEEPEN_STEP},中断可重跑)..." + while is_shallow; do + PRE="$(git -C "${MIRROR_DIR}" rev-list --count --all 2>/dev/null || echo 0)" + echo " 已有 ${PRE} 条历史,再加深 ${DEEPEN_STEP} ..." + if ! git -C "${MIRROR_DIR}" fetch --deepen="${DEEPEN_STEP}" origin; then + echo " ⚠ 本批中断;已落盘的 ${PRE} 条历史保留,重跑本脚本继续。" + break + fi + POST="$(git -C "${MIRROR_DIR}" rev-list --count --all 2>/dev/null || echo 0)" + [[ "${POST}" == "${PRE}" ]] && { echo " 已补齐完整历史"; break; } + done + elif is_shallow && [[ -n "${DEPTH}" ]]; then + echo " 浅仓加深到 depth=${DEPTH} ..." + git -C "${MIRROR_DIR}" fetch --depth="${DEPTH}" origin || \ + echo " ⚠ 加深中断,已保留现有深度,重跑可继续" else - git clone --mirror "$UPSTREAM" "$MIRROR_DIR" + echo " 更新已有裸仓 ..." + git -C "${MIRROR_DIR}" remote update --prune + fi +else + # 走沙箱代理时偶发 `CONNECT tunnel failed, response 502`,且 git clone + # 失败不保留已下载内容,所以首次克隆必须带重试。 + CLONE_ARGS=(clone --mirror) + [[ -n "${DEPTH}" ]] && CLONE_ARGS+=(--depth "${DEPTH}") + if [[ -n "${DEPTH}" ]]; then + echo " 首次镜像(--depth ${DEPTH},只取近期历史)..." + else + echo " 首次全量镜像(大仓请耐心)..." + fi + for attempt in 1 2 3; do + if git "${CLONE_ARGS[@]}" "${UPSTREAM}" "${MIRROR_DIR}"; then + break + fi + echo " ⚠ 第 ${attempt} 次克隆失败,8s 后重试 ..." + rm -rf "${MIRROR_DIR}" + sleep 8 + [[ ${attempt} -eq 3 ]] && { echo "克隆三次均失败,请检查网络/代理" >&2; exit 1; } + done +fi + +# ---- 浅仓处理:服务端拒绝 shallow update,必须先补齐历史 ---- +# Git 服务端默认 `receive.shallowUpdate = deny`,推送浅仓会被拒: +# ! [remote rejected] main -> main (shallow update not allowed) +# 而 --depth 是本脚本自己提供的选项,推不上去等于自相矛盾,所以在这里兜住: +# 先 --unshallow 补齐(增量下载,通常只比浅仓多几十 MB),失败再降级为快照。 +UNSHALLOWED="no" +if is_shallow; then + echo " 浅仓无法直接推送(服务端拒绝 shallow update),先补齐完整历史 ..." + for attempt in 1 2 3; do + if git -C "${MIRROR_DIR}" fetch --unshallow origin 2>/dev/null; then + UNSHALLOWED="yes"; break + fi + echo " ⚠ 第 ${attempt} 次 unshallow 失败,10s 后重试 ..." + sleep 10 + done + if [[ "${UNSHALLOWED}" == "yes" ]]; then + echo " 已补齐完整历史" + else + # 网络或体积原因补不齐 —— 退回快照模式,丢历史但至少把代码留下来。 + echo " ⚠ 补齐失败,将降级为【源码快照】(会丢失提交历史)" + SOURCE_ONLY_FORCE="yes" fi fi PROTO="${GITEA_URL%%://*}" HOST="${GITEA_URL#*//}" U_ENC="$(/Users/figmarlite/.workbuddy/binaries/python/envs/default/bin/python -c \ - 'import sys,urllib.parse;print(urllib.parse.quote(sys.argv[1],safe=""))' "$GITEA_USER")" + 'import sys,urllib.parse;print(urllib.parse.quote(sys.argv[1],safe=""))' "${GITEA_USER}")" P_ENC="$(/Users/figmarlite/.workbuddy/binaries/python/envs/default/bin/python -c \ - 'import sys,urllib.parse;print(urllib.parse.quote(sys.argv[1],safe=""))' "$GITEA_TOKEN")" + 'import sys,urllib.parse;print(urllib.parse.quote(sys.argv[1],safe=""))' "${GITEA_TOKEN}")" -echo " 推送到 $ORG/$REPO_NAME ..." -git -C "$MIRROR_DIR" push --mirror "$PROTO://$U_ENC:$P_ENC@$HOST/$ORG/$REPO_NAME.git" +# ---- 体积闸门:Gitea 前置 nginx 有 client_max_body_size 上限 ---- +# 实测 gitea.apescale.com:48 MB 放行、64 MB 拒绝(推算为 50m,宝塔常见默认值)。 +# 超限推送会在传完全部对象后才收到 HTTP 413,白白浪费几十分钟下载。 +# 所以推送前先量体积,超限则自动降级为「源码快照」:剔除图片/音视频/字体等 +# 非源码素材——这些内容通常占一半以上体积,却不是"跟踪源码"要看的东西。 +MAX_PUSH_MB="${MAX_PUSH_MB:-45}" +PACK_KB="$(git -C "${MIRROR_DIR}" count-objects -v | awk '/^size-pack:/{print $2}')" +PACK_MB=$(( PACK_KB / 1024 )) +MIRROR_MODE="full" + +echo " 本地裸仓打包体积: ${PACK_MB} MB(推送上限 ${MAX_PUSH_MB} MB)" + +if [[ ( "${PACK_MB}" -gt "${MAX_PUSH_MB}" || "${SOURCE_ONLY_FORCE}" == "yes" ) \ + && "${SOURCE_ONLY}" != "no" ]]; then + MIRROR_MODE="source-only" + if [[ "${SOURCE_ONLY_FORCE}" == "yes" ]]; then + echo " ⚠ 降级为【源码快照】:提交历史不完整,按快照提交(仍会剔除素材)" + else + echo " ⚠ 超出上限,降级为【源码快照】:剔除非源码素材后推送" + fi + SNAP="$(mktemp -d)" + # shellcheck disable=SC2064 + trap "cleanup_broken; rm -rf '${SNAP}'" EXIT + + git init -q "${SNAP}/repo" + git --git-dir="${MIRROR_DIR}" --work-tree="${SNAP}/repo" checkout HEAD -- . 2>/dev/null + find "${SNAP}/repo" -type f -not -path '*/.git/*' \( \ + -iname '*.png' -o -iname '*.gif' -o -iname '*.jpg' -o -iname '*.jpeg' \ + -o -iname '*.webp' -o -iname '*.ico' -o -iname '*.mp4' -o -iname '*.mov' \ + -o -iname '*.webm' -o -iname '*.wav' -o -iname '*.mp3' -o -iname '*.glb' \ + -o -iname '*.ttf' -o -iname '*.woff' -o -iname '*.woff2' -o -iname '*.eot' \ + -o -iname '*.pdf' -o -iname '*.zip' \) -delete + + cat > "${SNAP}/repo/MIRROR-NOTE.md" </dev/null || true + SNAP_MB=$(( $(git -C "${PUSH_DIR}" count-objects -v | awk '/^size-pack:/{print $2}') / 1024 )) + echo " 源码快照体积: ${SNAP_MB} MB" +else + PUSH_DIR="${MIRROR_DIR}" +fi + +echo " 推送到 ${ORG}/${REPO_NAME} ..." +if [[ "${MIRROR_MODE}" == "full" ]]; then + git -C "${PUSH_DIR}" push --mirror "${PROTO}://${U_ENC}:${P_ENC}@${HOST}/${ORG}/${REPO_NAME}.git" +else + git -C "${PUSH_DIR}" push "${PROTO}://${U_ENC}:${P_ENC}@${HOST}/${ORG}/${REPO_NAME}.git" HEAD:refs/heads/main +fi # ---- 回写档案 ---- -/Users/figmarlite/.workbuddy/binaries/python/envs/default/bin/python - "$YAML" "$VISIBILITY" "$REPO_NAME" <<'PY' +/Users/figmarlite/.workbuddy/binaries/python/envs/default/bin/python - "${YAML}" "${VISIBILITY}" "${REPO_NAME}" "${MIRROR_MODE}" <<'PY' import sys -from datetime import datetime from pathlib import Path sys.path.insert(0, str(Path(sys.argv[1]).resolve().parent.parent.parent / "scripts")) from _common import today, update_block @@ -140,9 +298,10 @@ text = update_block(text, "mirror", { "visibility": sys.argv[2], "gitea_repo": f"{'gitinbox'}/{sys.argv[3]}", "last_sync": today(), + "mode": sys.argv[4], }) p.write_text(text, encoding="utf-8") PY -SIZE="$(du -sh "$MIRROR_DIR" 2>/dev/null | cut -f1 || echo '?')" -echo "==> 完成。$DISPLAY_URL/$ORG/$REPO_NAME (本地裸仓 $SIZE)" +SIZE="$(du -sh "${MIRROR_DIR}" 2>/dev/null | cut -f1 || echo '?')" +echo "==> 完成。${DISPLAY_URL}/${ORG}/${REPO_NAME} (模式 ${MIRROR_MODE},本地裸仓 ${SIZE})" diff --git a/scripts/publish_emlog.py b/scripts/publish_emlog.py index d2f26e4..e14e131 100755 --- a/scripts/publish_emlog.py +++ b/scripts/publish_emlog.py @@ -9,13 +9,20 @@ 两个关键设计 ------------ -1. **update 前必须先 detail 取回全文**。 - emlog 的 article_update 接口在 content 为空时会把正文清空 —— 这是接口行为, - 不是 bug。总纲文每次回填都要更新,所以这条保护是刚需。 +1. **防空保护点放在"本地正文非空",不是"先 detail 取回线上全文"**。 + emlog 的 article_update 在 content 为空时会把正文清空,所以必须有保护。 + 但保护不能建立在 detail 上,原因有二: + a) 本脚本的 content 永远来自本地 Markdown 源文,不存在"忘了传"的情况; + b) detail 接口**查不到草稿**(实测:草稿 id 返回 400 Article not found, + 已发布文章正常)。若以 detail 为门槛,草稿将永远无法转正。 + 因此 detail 降级为可选的健全性检查:取得到就对比长度,取不到不阻断。 2. **文章 ID 回写**。 - 项目文回写到 data/projects/.yaml 的 article 块; - 总纲文回写到 articles/00-overview.meta.yaml。 + 有同名项目档案(data/projects/.yaml)→ 回写到档案的 article 块; + 没有(总纲文 00-overview、横评文 00-comparison 这类跨项目文章) + → 回写到 articles/.meta.yaml。 有了 ID 才能做"更新"而不是每次都发一篇新的。 + 判断顺序不能反:先按"是不是项目文"分流,再决定落哪里, + 否则新增一篇横评类文章时 ID 会无处可写,下次发布就变成重复发一篇。 """ from __future__ import annotations @@ -33,8 +40,6 @@ from _common import ARTICLES, PROJECTS, today, update_block # noqa: E402 import publish_article as emlog # noqa: E402 DEFAULT_SORT_ID = 17 # 可巴蒂 -OVERVIEW_ID = "00-overview" -META_PATH = ARTICLES / f"{OVERVIEW_ID}.meta.yaml" # ---------------------------------------------------------------- 转换 @@ -69,34 +74,38 @@ def to_excerpt(md: str, limit: int = 110) -> str: # ---------------------------------------------------------------- ID 读写 +def meta_path(aid: str) -> Path: + return ARTICLES / f"{aid}.meta.yaml" + + def read_emlog_id(aid: str) -> int | None: - if aid == OVERVIEW_ID: - if not META_PATH.exists(): - return None - return (yaml.safe_load(META_PATH.read_text(encoding="utf-8")) or {}).get("emlog_id") p = PROJECTS / f"{aid}.yaml" - if not p.exists(): - return None - d = yaml.safe_load(p.read_text(encoding="utf-8")) or {} - v = (d.get("article") or {}).get("emlog_id") - return int(v) if v else None + if p.exists(): + d = yaml.safe_load(p.read_text(encoding="utf-8")) or {} + v = (d.get("article") or {}).get("emlog_id") + return int(v) if v else None + mp = meta_path(aid) + if mp.exists(): + return (yaml.safe_load(mp.read_text(encoding="utf-8")) or {}).get("emlog_id") + return None def write_emlog_id(aid: str, emlog_id: int) -> None: url = f"{emlog.BLOG_URL}/?post={emlog_id}" - if aid == OVERVIEW_ID: - META_PATH.write_text( - f"# 总纲文发布元数据(由 scripts/publish_emlog.py 维护)\n" + p = PROJECTS / f"{aid}.yaml" + + if not p.exists(): + # 跨项目文章(总纲 / 横评 / 专题):没有档案可挂,单独存 meta。 + mp = meta_path(aid) + mp.write_text( + f"# 发布元数据(由 scripts/publish_emlog.py 维护)\n" + f"# 本文不属于单个项目,ID 不落在 data/projects/ 下\n" f"emlog_id: {emlog_id}\nurl: \"{url}\"\nlast_published: \"{today()}\"\n", encoding="utf-8", ) - print(f" 已写入 {META_PATH.name}") + print(f" 已写入 {mp.name}") return - p = PROJECTS / f"{aid}.yaml" - if not p.exists(): - print(f" [warn] 档案不存在,未回写 ID: {p}") - return text = p.read_text(encoding="utf-8") try: text = update_block(text, "article", {