diff --git a/articles/00-overview.md b/articles/00-overview.md index d1641f6..a64c1b2 100644 --- a/articles/00-overview.md +++ b/articles/00-overview.md @@ -183,12 +183,12 @@ Go 守护进程),但引擎接入深度严重向 Codex 倾斜、开源版功 | 项目 | 结论 | 适合 | 不适合 | | --- | --- | --- | --- | | **iPolloWork** | 把多个 Agent 引擎收进同一个可编辑工作台的野心之作——方向踩在点上,但三种引擎的接入深度并不齐整,且许可证决定了它暂时只适合个人或极小团队试用 | 想在同一个界面管理多引擎项目、并希望 agent 产出的 PPT / 网页 / 设计 / 视频之后还能继续手工修改的个人开发者,或少于 3 人的极小团队 | 3 人以上需要正式落地的团队(许可证要求书面授权)、期待 Codex 原生级集成的用户(目前只有 MCP 控制面)、以及追求稳定的生产环境(v0.50.x、一天多版、DSH 集成未进稳定版) | -| **maka** | 四个里唯一把「执行记录」当地基的项目——治理与可复现性最强,产品成熟度最弱。 | 对审计、数据主权、可复现评测有硬要求的团队; 做 agent 横向对比研究的工程团队。 | 要开箱即用、要 Windows / Linux、要丰富第三方集成的人; 以为 ASF 品牌等于生产可用的决策者。 | +| **maka** | 唯一把「执行记录」当地基的项目——治理与可复现性最强,产品成熟度最弱。 | 对审计、数据主权、可复现评测有硬要求的团队; 做 agent 横向对比研究的工程团队。 | 要开箱即用、要 Windows / Linux、要丰富第三方集成的人; 以为 ASF 品牌等于生产可用的决策者。 | | **OpenClaw** | 388k star 的个人 AI 助理,常驻你自己的机器、从聊天软件驱动—— 但沙箱默认关闭、版本号已经乱到读不懂发布节奏、关键提交仍系于一人之手。 | 想让 AI 真正接管日常事务且愿意自己加固部署环境的重度个人用户; 想研究"非开发者向 agent 产品长什么样"的人(本体系里唯一的样本)。 | 把 star 数当成生产就绪度的人;指望沙箱开箱即保护的人; 需要可预测升级路径的团队;希望助理"中立接入多个引擎"的人(它自己就是引擎,不接别人)。 | | **openwork** | 把「能力」而不是「桌面」做成产品——工程治理最好的一个,但它和自称要替代的东西不是一个物种。 | 已固定在某款 agent 上、只想让团队能力跨工具复用的开发者; 需要统一管控 agent 能力与模型配额的组织 IT。 | 想要一个开箱即用的桌面 agent 工作台的人; 以为拿到的是完整 Cowork 平替的人。 | | **orca** | 兼容面最广的并行 agent 编排外壳——广度做到极致,深度与治理都还是草稿。 | 已在重度使用 CLI agent、想同时跑多个方案横向比对的个人开发者与小团队。 | 需要稳定 SLA、指望社区 triage 与多人维护保障的团队; 指望它统一各引擎能力(结构化调用、统一权限、一致口径)的架构选型者。 | -| **QwenPaw** | 七个样本里安全默认值最完整、巴士系数最健康的个人助理—— 但至今没有发过一个正式版,且 `--defaults` 安装路径会默认打开遥测。 | 把数据与隐私放第一位、希望助理完全跑在本地的个人与小团队; 需要在钉钉 / 飞书 / 微信 / QQ 渠道里用助理的国内团队; 想不买 API key 先把流程跑通的尝鲜者。 | 需要正式版与稳定升级路径的团队(它全是 beta); 指望 2B / 4B 本地模型扛复杂 agent 任务的人; 把"五层安全"当成不可绕过硬边界的人(Tool Guard 有 OFF 档)。 | -| **Tutti** | 把「多 agent 共享上下文、接力干活」做成了产品,且是七个里发布纪律最好的—— 但接入深度严重偏袒 Codex(17 倍差距),而宣传的「多人协作」全在闭源 VM 版。 | 手里已有多个 agent 订阅、想让它们在同一工作区里接力与共享上下文的开发者; 想让 agent 产出设计稿 / 文档 / PPT 而不只是代码的人; 看重发布纪律与工程规范(NOTICE 精度、正式版节奏)的人。 | 指望开源版做团队多人协作的人(那七项功能全在闭源 VM 侧,且需邀请码); 主要用 Kimi / ComputerUse / BrowserUse 的人(这三个接入最薄、测试为零); Windows 用户且主力引擎不是 Codex 的人(只有 Codex 做了 Windows 适配)。 | +| **QwenPaw** | 安全默认值最完整、巴士系数最健康的个人助理—— 但至今没有发过一个正式版,且 `--defaults` 安装路径会默认打开遥测。 | 把数据与隐私放第一位、希望助理完全跑在本地的个人与小团队; 需要在钉钉 / 飞书 / 微信 / QQ 渠道里用助理的国内团队; 想不买 API key 先把流程跑通的尝鲜者。 | 需要正式版与稳定升级路径的团队(它全是 beta); 指望 2B / 4B 本地模型扛复杂 agent 任务的人; 把「五层安全」当成不可绕过硬边界的人(Tool Guard 有 OFF 档)。 | +| **Tutti** | 把「多 agent 共享上下文、接力干活」做成了产品,且是发布纪律最好的—— 但接入深度严重偏袒 Codex(17 倍差距),而宣传的「多人协作」全在闭源 VM 版。 | 手里已有多个 agent 订阅、想让它们在同一工作区里接力与共享上下文的开发者; 想让 agent 产出设计稿 / 文档 / PPT 而不只是代码的人; 看重发布纪律与工程规范(NOTICE 精度、正式版节奏)的人。 | 指望开源版做团队多人协作的人(那七项功能全在闭源 VM 侧,且需邀请码); 主要用 Kimi / ComputerUse / BrowserUse 的人(这三个接入最薄、测试为零); Windows 用户且主力引擎不是 Codex 的人(只有 Codex 做了 Windows 适配)。 | 视图 A · 形态纵深 × 引擎立场 @@ -261,12 +261,12 @@ Go 守护进程),但引擎接入深度严重向 Codex 倾斜、开源版功 | 项目 | 一句话结论 | 深度文 | | --- | --- | --- | | **iPolloWork** | 把多个 Agent 引擎收进同一个可编辑工作台的野心之作 | [已发布](https://blog.apescale.com/?post=97) | -| **maka** | 四个里唯一把「执行记录」当地基的项目 | 待发布(见下方横评) | +| **maka** | 唯一把「执行记录」当地基的项目 | 待发布(见下方横评) | | **OpenClaw** | 388k star 的个人 AI 助理,常驻你自己的机器、从聊天软件驱动 | 待发布(见下方横评) | | **openwork** | 把「能力」而不是「桌面」做成产品 | 待发布(见下方横评) | | **orca** | 兼容面最广的并行 agent 编排外壳 | 待发布(见下方横评) | -| **QwenPaw** | 七个样本里安全默认值最完整、巴士系数最健康的个人助理 | 待发布(见下方横评) | -| **Tutti** | 把「多 agent 共享上下文、接力干活」做成了产品,且是七个里发布纪律最好的 | 待发布(见下方横评) | +| **QwenPaw** | 安全默认值最完整、巴士系数最健康的个人助理 | 待发布(见下方横评) | +| **Tutti** | 把「多 agent 共享上下文、接力干活」做成了产品,且是发布纪律最好的 | 待发布(见下方横评) | **跨项目文章** diff --git a/data/projects/maka.yaml b/data/projects/maka.yaml index 2902ff9..424160f 100644 --- a/data/projects/maka.yaml +++ b/data/projects/maka.yaml @@ -207,7 +207,7 @@ chart: verdict: - one_liner: 四个里唯一把「执行记录」当地基的项目——治理与可复现性最强,产品成熟度最弱。 + one_liner: 唯一把「执行记录」当地基的项目——治理与可复现性最强,产品成熟度最弱。 who_should_use: |- 对审计、数据主权、可复现评测有硬要求的团队; 做 agent 横向对比研究的工程团队。 diff --git a/data/projects/openwork.yaml b/data/projects/openwork.yaml index 3d147b2..a269ca4 100644 --- a/data/projects/openwork.yaml +++ b/data/projects/openwork.yaml @@ -32,6 +32,15 @@ business: name: "目录切分许可(GitLab 式):ee/ 之外为 MIT;ee/ 为 OpenWork EE License" openness: L3 redistributable: false + # ↓ 排除 mirror.excludes 之后的状态。mirror_sync.sh 判可见性时用的是**这个**, + # 不是上面的 redistributable——目录切分许可下两者结论相反。 + # 依据:根 LICENSE 原文(已人工阅读远端全文) + # "All content that resides under the /ee directory ... is licensed under + # the OpenWork Enterprise Edition (EE) License" + # "Content outside of the above mentioned directories or restrictions + # above is available under the 'MIT' license as defined below." + # 排除 ee/ 后 → 纯 MIT → 可公开。 + redistributable_after_excludes: true key_restrictions: - ee/ 目录(Den 控制面、MCP 网关、推理)生产使用需有效订阅;5 人以下组织在排除 Enterprise 功能后免费 - 每个 ee/ 版本公开发布满两年后,自动追加 MIT 授权(Grant of Future License) @@ -214,12 +223,27 @@ verdict: 以为拿到的是完整 Cowork 平替的人。 mirror: - gitea_repo: "gitinbox/openwork" - visibility: public + gitea_repo: gitinbox/openwork + visibility: private 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,可公开;完整镜像则必须私有。" + last_sync: 2026-09-01 + mode: source-only + # 目录切分许可:ee/ 之外是 MIT,ee/ 是 EE License。 + # 本档案的 visibility: public **完全建立在「这个排除真的生效」这个前提上**—— + # ee/ 一旦进了镜像,公开分发就是越权。 + # 声明必须写在档案里:mirror_sync.sh 只读档案、不读 registry.yaml。 + # registry.yaml 里的 mirror_excludes 只是批量抓取的提示,不参与镜像决策。 + # + # 第二个排除项是 2026-09-02 补的:只排除 ee/ 时,根目录的 + # LICENSES/LicenseRef-OpenWork-EE.txt 仍会被推进去。它是 REUSE 规范的引用文件, + # 存在的唯一意义是给 ee/ 下的文件做 SPDX 引用(ee/ 里写着 + # "SPDX-License-Identifier: LicenseRef-OpenWork-EE")。ee/ 一删,它就成了孤儿。 + # 许可证文本本身公开不侵权(上游自己就公开),但留着会让镜像名不副实—— + # 一个不含 EE 代码的仓库里躺着 EE 许可证,读者会以为漏排了。 + excludes: + - "ee/" + - "LICENSES/LicenseRef-OpenWork-EE.txt" + sync_note: "已镜像(source-only 快照,2026-09-01)。上游 291 MB,本机到 GitHub 实测 50 KB/s,拉取耗时较长且 git clone 无断点续传。许可证为目录切分(根 LICENSE 原文:/ee 之外为 MIT,/ee 为 OpenWork EE License)——镜像时排除 ee/ 与 LICENSES/LicenseRef-OpenWork-EE.txt,排除后为纯 MIT 故判 public。未排除的完整镜像会被脚本在推送前拒绝(public 目标 + 有 excludes 声明 + full 模式)。" article: emlog_id: 0 # emlog 文章 ID,发布后回写 published_at: "" diff --git a/data/projects/qwenpaw.yaml b/data/projects/qwenpaw.yaml index d938ad1..5ec5754 100644 --- a/data/projects/qwenpaw.yaml +++ b/data/projects/qwenpaw.yaml @@ -230,7 +230,7 @@ chart: chart_note: "与 OpenClaw 同赛道对照看:形态略浅、本地优先更强、安全默认值显著更高" verdict: one_liner: |- - 七个样本里安全默认值最完整、巴士系数最健康的个人助理—— + 安全默认值最完整、巴士系数最健康的个人助理—— 但至今没有发过一个正式版,且 `--defaults` 安装路径会默认打开遥测。 who_should_use: |- 把数据与隐私放第一位、希望助理完全跑在本地的个人与小团队; @@ -239,7 +239,7 @@ verdict: who_should_not: |- 需要正式版与稳定升级路径的团队(它全是 beta); 指望 2B / 4B 本地模型扛复杂 agent 任务的人; - 把"五层安全"当成不可绕过硬边界的人(Tool Guard 有 OFF 档)。 + 把「五层安全」当成不可绕过硬边界的人(Tool Guard 有 OFF 档)。 mirror: gitea_repo: gitinbox/qwenpaw visibility: public @@ -247,8 +247,9 @@ mirror: last_sync: 2026-09-01 mode: source-only sync_note: |- - 109 MB,理论可行(约 36 分钟拉完),但超过 Gitea 50 MB 推送上限, - 需走 mirror_sync.sh 的 source-only 快照降级。尚未执行,待带宽窗口。 + 已于 2026-09-01 完成 source-only 镜像(gitinbox/qwenpaw,public)。 + 109 MB 裸仓超 Gitea 50 MB 推送上限,故剔除图片/音视频/字体等素材后按快照提交: + 推送 18 MB;完整保真裸仓(281 MB)保留在 .mirrors/qwenpaw.git,增量更新重跑脚本即可。 article: emlog_id: 0 # emlog 文章 ID,发布后回写 published_at: "" diff --git a/data/projects/tutti.yaml b/data/projects/tutti.yaml index 1a9bdbe..efe4b2a 100644 --- a/data/projects/tutti.yaml +++ b/data/projects/tutti.yaml @@ -276,7 +276,7 @@ chart: chart_note: "引擎中立项给的是协议层统一;若按投入对等度重评,该项应下调至 4 左右" verdict: one_liner: |- - 把「多 agent 共享上下文、接力干活」做成了产品,且是七个里发布纪律最好的—— + 把「多 agent 共享上下文、接力干活」做成了产品,且是发布纪律最好的—— 但接入深度严重偏袒 Codex(17 倍差距),而宣传的「多人协作」全在闭源 VM 版。 who_should_use: |- 手里已有多个 agent 订阅、想让它们在同一工作区里接力与共享上下文的开发者; @@ -287,15 +287,17 @@ verdict: 主要用 Kimi / ComputerUse / BrowserUse 的人(这三个接入最薄、测试为零); Windows 用户且主力引擎不是 Codex 的人(只有 Codex 做了 Windows 适配)。 mirror: - gitea_repo: "gitinbox/tutti" + gitea_repo: gitinbox/tutti visibility: public upstream: "https://github.com/tutti-os/tutti" - last_sync: "" - mode: "pending" + last_sync: 2026-09-01 + mode: source-only sync_note: |- - 243 MB。本地已有浅克隆(/Users/figmarlite/.../tutti_repo,8291 文件), - 技术维度分析即基于此,可直接复用做快照,无需重新下载。 - 仍需 unshallow 补全历史后走 source-only 降级(超 Gitea 50 MB 上限)。尚未执行。 + 已于 2026-09-01 完成 source-only 镜像(gitinbox/tutti,public)。 + 243 MB 裸仓超 Gitea 50 MB 推送上限,unshallow 补全历史后按快照提交: + 推送 14 MB;完整保真裸仓(254 MB)保留在 .mirrors/tutti.git。 + 技术维度分析基于本地浅克隆 /Users/figmarlite/.../tutti_repo(8291 文件), + 与本镜像是两份独立的检出,改动不同步。 article: emlog_id: 0 # emlog 文章 ID,发布后回写 published_at: "" diff --git a/data/registry.yaml b/data/registry.yaml index 6f34e9a..6e40b88 100644 --- a/data/registry.yaml +++ b/data/registry.yaml @@ -14,6 +14,10 @@ projects: added_at: 2026-09-01 license_level: L3 # 目录切分许可:ee/ 之外 MIT,ee/ 为 EE License mirror_visibility: public # 人工复核:镜像时排除 ee/ 后为纯 MIT,故可公开 + # ⚠️ 这个字段**不参与镜像决策**,只是批量抓取时的提示。 + # mirror_sync.sh 只读 data/projects/openwork.yaml 的 mirror.excludes。 + # 曾因只写在这里、脚本又不读它,导致 public 可见性建立在一条从未生效的规则上。 + # 2026-09-01 已修:脚本支持并强制校验档案里的 excludes。 mirror_excludes: ["ee/"] - id: orca repo: stablyai/orca diff --git a/scripts/build_matrix.py b/scripts/build_matrix.py index 2358101..09da06c 100755 --- a/scripts/build_matrix.py +++ b/scripts/build_matrix.py @@ -361,6 +361,31 @@ def quadrant_svg(items: list[dict]) -> str: # ---------------------------------------------------------------- 主流程 +# sync_note 是手写备注,mode / last_sync 是脚本回写的事实。 +# 两者会漂移:镜像跑完之后 mode 变成了 source-only,note 里还写着「尚未执行,待带宽窗口」。 +# 档案里出现和事实相反的话,比没有备注更糟——读者会照着错的那个信。 +STALE_PHRASES = ("尚未执行", "暂未镜像", "待带宽", "尚未镜像", "未镜像") + + +def check_mirror_notes(items: list[dict]) -> None: + """mirror 块已跑完(mode 非 pending/空)却还写着「尚未执行」→ 提示改。""" + bad = [] + for d in items: + m = d.get("mirror") or {} + mode = str(m.get("mode") or "").strip() + if mode in ("", "pending", "link-only"): + continue # 没跑过 / 明确不镜像,note 说「待执行」是对的 + note = str(m.get("sync_note") or "") + hit = [p for p in STALE_PHRASES if p in note] + if hit: + bad.append((d.get("id") or d.get("name"), mode, "/".join(hit))) + if not bad: + return + print(f" [warn] {len(bad)} 个档案的 mirror.sync_note 与实际状态矛盾:") + for aid, mode, hit in bad: + print(f" - {aid}:mode={mode},但 note 里仍有「{hit}」") + + def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--stdout", action="store_true", help="只打印不写文件") @@ -373,6 +398,8 @@ def main() -> int: print(f"==> 载入 {len(items)} 个项目:{', '.join(d.get('name','') for d in items)}") + check_mirror_notes(items) + matrix = build_tables(items) chart = quadrant_svg(items) if len(items) >= QUAD_THRESHOLD else stack_diagram(items) block = f"{BEGIN}\n{matrix}\n\n{chart}\n{END}" diff --git a/scripts/mirror_sync.sh b/scripts/mirror_sync.sh index a48ac02..a0502e8 100755 --- a/scripts/mirror_sync.sh +++ b/scripts/mirror_sync.sh @@ -21,6 +21,9 @@ DEPTH="" # 而这个分支只在特定体积下才走到——不初始化就成了「偶尔才炸」的雷。 SOURCE_ONLY="auto" SOURCE_ONLY_FORCE="no" +# private → public 的显式授权开关。默认 0:公开不可逆,绝不自动放宽。 +SET_PUBLIC="0" +EXCLUDE_COUNT="0" while [[ $# -gt 0 ]]; do case "$1" in @@ -29,6 +32,7 @@ while [[ $# -gt 0 ]]; do --url) GITEA_URL="$2"; shift 2;; --depth) DEPTH="$2"; shift 2;; --no-source-only) SOURCE_ONLY="no"; shift;; + --set-public) SET_PUBLIC="1"; shift;; *) echo "未知参数: $1" >&2; exit 1;; esac done @@ -55,15 +59,45 @@ lic = (d.get("business") or {}).get("license") or {} print(f"UPSTREAM={shlex.quote(str(m.get('upstream') or ''))}") print(f"REPO_NAME={shlex.quote(str(m.get('gitea_repo') or '').split('/')[-1] or d.get('id',''))}") # 可见性以许可证判定为准,不信任档案里手写的 visibility -vis = "public" if lic.get("redistributable") else "private" +exc = m.get("excludes") or [] +if exc: + # 目录切分许可:被排除的目录不进镜像,所以决定可见性的是**排除后**的内容。 + # 这里必须显式读 redistributable_after_excludes,缺了就报错—— + # 不能拿整体许可证去猜:openwork 整体是 L3(含 ee/),排除后是纯 MIT, + # 两种情况结论相反,猜错就是把受限内容公开出去,或者把能公开的白白锁起来。 + ra = lic.get("redistributable_after_excludes") + if ra is None: + sys.stderr.write( + "档案声明了 mirror.excludes,但 business.license 里没有 " + "redistributable_after_excludes,无法判定排除后的可见性。\n" + "请在许可证块补上该字段(true=排除后可公开,false=仍须私有)。\n") + sys.exit(2) + vis = "public" if ra else "private" +else: + vis = "public" if lic.get("redistributable") else "private" print(f"VISIBILITY={shlex.quote(vis)}") print(f"LIC_LEVEL={shlex.quote(str(lic.get('openness') or '?'))}") print(f"LIC_NAME={shlex.quote(str(lic.get('name') or ''))}") +# 目录切分许可的排除项(如 OpenWork 的 ee/)。 +# 必须读档案 mirror.excludes,不能读 registry.yaml 的 mirror_excludes: +# 后者只是批量抓取的提示,脚本从来没读过它,写在那里等于没写—— +# openwork 的 visibility: public 曾因此建立在一条从未生效的规则上。 +print("EXCLUDES=" + shlex.quote("\n".join(str(x) for x in (m.get("excludes") or [])))) PY # shellcheck disable=SC1090 source "${CONF_FILE}" +# 换行分隔 → 数组。空输入要能正确得到零长数组,否则下面 ${#EXCLUDE_PATHS[@]} +# 在 set -u 下会因未绑定而炸(这类"偶尔才走到"的分支最容易埋雷)。 +EXCLUDE_PATHS=() +if [[ -n "${EXCLUDES}" ]]; then + while IFS= read -r _line; do + [[ -n "${_line}" ]] && EXCLUDE_PATHS+=("${_line}") + done <<< "${EXCLUDES}" +fi +EXCLUDE_COUNT=${#EXCLUDE_PATHS[@]} + [[ -z "${UPSTREAM}" ]] && { echo "档案缺少 mirror.upstream" >&2; exit 1; } echo "==> 镜像 ${ID}" @@ -74,6 +108,10 @@ echo " 可见性 : ${VISIBILITY}" if [[ "${VISIBILITY}" == "private" ]]; then echo " ⚠ L3 级 source-available,已强制私有。公开镜像可能构成未经授权的分发。" fi +if [[ ${#EXCLUDE_PATHS[@]} -gt 0 ]]; then + echo " 排除路径 : ${EXCLUDE_PATHS[*]}" + echo " ⚠ 本项目的 public 可见性建立在「上述路径不进镜像」之上,推送前会二次校验。" +fi # ---- 凭据:优先环境变量,回退到 macOS 钥匙串 ---- if [[ -z "${GITEA_TOKEN:-}" ]]; then @@ -102,11 +140,35 @@ else 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 fi +# ---- 放宽方向(private → public)必须显式授权 ---- +# 公开是不可逆动作:一旦有人 fork/克隆,就再也收不回来。 +# 所以这里**不自动翻**——只报告,要求显式加 --set-public。 +# 2026-09-02 补:原先脚本只有"收紧"方向的修正,openwork 判定为 public 但仓库 +# 停在 private,重跑多少次都不会翻过来,档案与事实不一致却看不出原因。 +if [[ "${VISIBILITY}" == "public" ]]; then + CUR_PRIVATE="$(curl -sS -u "${GITEA_USER}:${GITEA_TOKEN}" "${API}/repos/${ORG}/${REPO_NAME}" \ + | /Users/figmarlite/.workbuddy/binaries/python/envs/default/bin/python -c 'import sys,json;print("true" if json.load(sys.stdin).get("private") else "false")' 2>/dev/null || echo unknown)" + if [[ "${CUR_PRIVATE}" == "true" ]]; then + if [[ "${SET_PUBLIC}" == "1" ]]; then + echo " ⚠ 按 --set-public 将 ${ORG}/${REPO_NAME} 从 private 翻为 public(不可逆)" + curl -sS -o /dev/null -u "${GITEA_USER}:${GITEA_TOKEN}" -X PATCH \ + -H 'Content-Type: application/json' -d '{"private":false}' "${API}/repos/${ORG}/${REPO_NAME}" \ + && echo " 已翻为 public" + else + echo "" + echo " ⚠ ${ORG}/${REPO_NAME} 判定应为 public,但远端当前是 private。" + echo " 公开不可逆,脚本默认不自动翻。确认后重跑并加 --set-public。" + echo " 判定依据:许可证 ${LIC_LEVEL},排除 ${EXCLUDE_COUNT} 项后 redistributable=${VISIBILITY}" + fi + fi +fi + # ---- 同步 ---- MIRROR_DIR="${ROOT}/.mirrors/${REPO_NAME}.git" mkdir -p "${ROOT}/.mirrors" @@ -241,6 +303,23 @@ if [[ ( "${PACK_MB}" -gt "${MAX_PUSH_MB}" || "${SOURCE_ONLY_FORCE}" == "yes" ) \ git init -q "${SNAP}/repo" git --git-dir="${MIRROR_DIR}" --work-tree="${SNAP}/repo" checkout HEAD -- . 2>/dev/null + + # 目录切分许可的排除在这里落地:工作树 checkout 完之后整棵删掉。 + # 注意必须在资产剔除**之前**做——ee/ 这类目录里也可能有图片/字体, + # 顺序反了会留下空目录和残留文件。 + if [[ ${#EXCLUDE_PATHS[@]} -gt 0 ]]; then + for _exc in "${EXCLUDE_PATHS[@]}"; do + if [[ -e "${SNAP}/repo/${_exc}" ]]; then + rm -rf "${SNAP}/repo/${_exc}" + echo " 已排除 ${_exc}" + else + # 排除了个不存在的路径,多半是上游改了目录结构。 + # 不静默放过——否则哪天 ee/ 挪了窝,我们还在按 public 推。 + echo " ⚠ 声明要排除的 ${_exc} 在上游不存在,请核对档案的 excludes 是否过期" + fi + done + fi + 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' \ @@ -258,6 +337,10 @@ if [[ ( "${PACK_MB}" -gt "${MAX_PUSH_MB}" || "${SOURCE_ONLY_FORCE}" == "yes" ) \ ## 这不是完整保真镜像 本仓库只含**源码与文档**,已剔除图片、音视频、字体等非源码素材。 +$(if [[ ${#EXCLUDE_PATHS[@]} -gt 0 ]]; then + printf '此外按许可证要求排除了以下目录:%s。\n' "${EXCLUDE_PATHS[*]}" + printf '本仓库的公开可见性以该排除为前提;完整上游含受限许可内容。\n' + fi) 原因:Gitea 前置 nginx 的 \`client_max_body_size\` 上限约 ${MAX_PUSH_MB} MB, 完整裸仓打包 ${PACK_MB} MB 会被 HTTP 413 拒绝。 @@ -279,6 +362,42 @@ else PUSH_DIR="${MIRROR_DIR}" fi +# ---- 推送前硬校验:排除项绝不能进公开镜像 ---- +# 这是整套许可证分级的最后一道闸。前面所有环节(声明、checkout 后删除)都可能有 +# 遗漏或顺序问题,而"public"这个动作的代价是不可逆的——东西一旦公开就收不回来。 +# 所以这里不看过程只看结果:直接检查待推送目录里还有没有这些路径。 +if [[ "${VISIBILITY}" == "public" && ${#EXCLUDE_PATHS[@]} -gt 0 ]]; then + if [[ "${MIRROR_MODE}" == "full" ]]; then + # full 模式直接拒:push --mirror 推的是完整裸仓,剔除目录需要重写历史 + # (git filter-repo),本脚本不做。更关键的是——裸仓没有工作树, + # 文件系统层面的检查会**假通过**,而 ee/ 的内容在历史里照样推得出去。 + # 这种"检查了但没真检查到"比不检查更危险,所以这里不看结果直接拦。 + echo "" >&2 + echo "✗ 拒绝以 full 模式推送到公开仓库:${EXCLUDE_PATHS[*]}" >&2 + echo " full 模式用 push --mirror 推完整裸仓,无法在此层面剔除受限目录," >&2 + echo " 且历史提交里仍含该内容。请二选一:" >&2 + echo " a) 把档案 mirror.visibility 改为 private;" >&2 + echo " b) 让体积超过闸门(MAX_PUSH_MB=${MAX_PUSH_MB})走 source-only 路径,排除在该路径生效。" >&2 + exit 1 + fi + + # source-only:有真实工作树,可以做结果校验。 + # 不看"我们有没有执行删除",只看"待推送内容里到底还有没有"—— + # 过程可能被改坏,结果不会说谎。 + LEAK=() + for _exc in "${EXCLUDE_PATHS[@]}"; do + _base="${_exc%/}" + [[ -e "${PUSH_DIR}/${_base}" ]] && LEAK+=("${_base}") + done + if [[ ${#LEAK[@]} -gt 0 ]]; then + echo "" >&2 + echo "✗ 拒绝推送:以下受限目录仍在待推送内容中:${LEAK[*]}" >&2 + echo " 目标仓库是 **public**,而其中含受限许可内容。" >&2 + exit 1 + fi + echo " 排除校验通过:${EXCLUDE_PATHS[*]} 均不存在于待推送内容" +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"