From 513f79424a8df03c5df07bec5781c433cfcc968f Mon Sep 17 00:00:00 2001 From: C3H3-AI Date: Tue, 15 Sep 2026 13:38:11 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=20HA=20builder=20?= =?UTF-8?q?action=20=E7=9A=84=20tag=20=E5=BC=95=E7=94=A8=EF=BC=887.2.0=20-?= =?UTF-8?q?>=202026.09.0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build Add-on Image 在合并后首次运行即失败: ##[error]Can't find 'action.yml', 'action.yaml' or 'Dockerfile' for action 'home-assistant/builder/actions/prepare-multi-arch-matrix@7.2.0'. 根因:home-assistant/builder 存在两条版本线。 - 7.x(7.0 / 7.1 / 7.2.0):旧版 builder,**仓库内没有 actions/ 目录** - 2026.x(2026.09.0 等):新版,含 actions/{build-image, prepare-multi-arch-matrix,publish-multi-arch-manifest,cosign-verify} 实测对比(两个 tag 各自的 clone): tag 7.2.0 → azure-pipelines.yml builder.sh build.json Dockerfile ... actions/ 不存在 tag 2026.09.0 → actions/ action.yml ...(4 个子 action 齐全) 官方 README 示例中的 @[version] 指的是 releases 页面上的 tag, 即日历版本(2026.09.0),而非 7.x 那条线。此前取了 7.2.0 属实误判。 --- .github/workflows/build-image.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 93777a3..9d9bfa4 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -59,7 +59,7 @@ jobs: - name: Get build matrix id: matrix - uses: home-assistant/builder/actions/prepare-multi-arch-matrix@7.2.0 + uses: home-assistant/builder/actions/prepare-multi-arch-matrix@2026.09.0 with: architectures: ${{ env.ARCHITECTURES }} image-name: ${{ env.IMAGE_NAME }} @@ -79,7 +79,7 @@ jobs: - uses: actions/checkout@v4 - name: Build image - uses: home-assistant/builder/actions/build-image@7.2.0 + uses: home-assistant/builder/actions/build-image@2026.09.0 with: arch: ${{ matrix.arch }} container-registry-password: ${{ secrets.GITHUB_TOKEN }} @@ -99,7 +99,7 @@ jobs: packages: write steps: - name: Publish multi-arch manifest - uses: home-assistant/builder/actions/publish-multi-arch-manifest@7.2.0 + uses: home-assistant/builder/actions/publish-multi-arch-manifest@2026.09.0 with: architectures: ${{ env.ARCHITECTURES }} container-registry-password: ${{ secrets.GITHUB_TOKEN }} From 5d5f961f41e7c90454f4bba01b9e021c9ec27219 Mon Sep 17 00:00:00 2001 From: C3H3-AI Date: Tue, 15 Sep 2026 14:05:38 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20GHCR=20=E9=95=9C=E5=83=8F=E5=90=8D?= =?UTF-8?q?=E5=BF=85=E9=A1=BB=E5=B0=8F=E5=86=99=EF=BC=88owner=20C3H3-AI=20?= =?UTF-8?q?=E5=90=AB=E5=A4=A7=E5=86=99=E5=AF=BC=E8=87=B4=20buildx=20?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build Add-on Image 第二次运行实际进入构建阶段,buildx 报错: ##[error]buildx failed with: ERROR: failed to build: invalid tag "ghcr.io/C3H3-AI/amd64-ai-proxy:1.1.0b13": repository name must be lowercase 根因:HA 官方 builder 的 prepare-multi-arch-matrix 与 publish-multi-arch-manifest 两个 action,其 registry-prefix 默认值均为 ghcr.io/${{ github.repository_owner }},而本仓库 owner 是 "C3H3-AI"(含大写)。 Docker/OCI 规范要求 repository name 必须全小写。 改动(3 处,统一为 ghcr.io/c3h3-ai): - prepare-multi-arch-matrix: 显式传 registry-prefix - publish-multi-arch-manifest: 显式传 registry-prefix - verify job: 手写的 IMAGE 变量由 github.repository_owner 改为字面量 与 config.yaml 的 image: "ghcr.io/c3h3-ai/ai-proxy" 保持一致。 注:上一轮的 tag 引用修复(7.2.0 -> 2026.09.0)已生效—— 本次 init job(Resolve version & matrix)全部步骤通过, build job 也真正进入了 buildx 构建阶段,不再是 Set up job 失败。 --- .github/workflows/build-image.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 9d9bfa4..bfcf0ae 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -63,6 +63,11 @@ jobs: with: architectures: ${{ env.ARCHITECTURES }} image-name: ${{ env.IMAGE_NAME }} + # 必须显式传小写前缀。该 action 默认取 github.repository_owner, + # 而本仓库 owner 是 "C3H3-AI"(含大写),GHCR 会拒绝: + # invalid tag "ghcr.io/C3H3-AI/amd64-ai-proxy:1.1.0b13": + # repository name must be lowercase + registry-prefix: ghcr.io/c3h3-ai build: name: Build ${{ matrix.arch }} @@ -104,6 +109,7 @@ jobs: architectures: ${{ env.ARCHITECTURES }} container-registry-password: ${{ secrets.GITHUB_TOKEN }} image-name: ${{ env.IMAGE_NAME }} + registry-prefix: ghcr.io/c3h3-ai image-tags: | ${{ needs.init.outputs.version }} latest @@ -119,7 +125,9 @@ jobs: - name: Verify manifest tag matches config.yaml version run: | set -euo pipefail - IMAGE="ghcr.io/${{ github.repository_owner }}/${IMAGE_NAME}" + # 小写硬编码,与 build/manifest 两个 job 的 registry-prefix 保持一致 + # (owner 是 C3H3-AI,GHCR 不接受大写) + IMAGE="ghcr.io/c3h3-ai/${IMAGE_NAME}" VERSION="${{ needs.init.outputs.version }}" echo "Checking ${IMAGE}:${VERSION}" if ! docker manifest inspect "${IMAGE}:${VERSION}" >/dev/null 2>&1; then From 9a4fb05531b2e6ffdb975ccec84ab6e698ffc751 Mon Sep 17 00:00:00 2001 From: C3H3-AI Date: Tue, 15 Sep 2026 21:58:06 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(sync):=20=E4=BF=9D=E6=8A=A4=20addon=20?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E8=BF=87=E7=9A=84=E4=B8=8A=E6=B8=B8=E6=96=87?= =?UTF-8?q?=E4=BB=B6=EF=BC=8C=E5=B9=B6=E6=8A=8A=E9=9D=99=E9=BB=98=E8=B7=B3?= =?UTF-8?q?=E8=BF=87=E6=94=B9=E4=B8=BA=E6=98=BE=E5=BC=8F=E6=8A=A5=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 问题 sync_vendor.sh 是「按文件」覆盖的复制式同步。addon 对上游文件做的改动会被 静默覆盖,实测跑一次 sync 后项目**直接编译失败**: internal/svc/svc.go:69: wbPool.SetLowCredits undefined internal/svc/svc.go:69: cfg.LowCreditThreshold undefined (traework / qoder 两处同样) 根因:`svc.go` 在 PROTECT_FILES 里(受保护),但它依赖的 `pool` / `config` 扩展**不在**保护名单里 —— 保护了一半,反而制造出 「独有代码引用被覆盖掉的符号」这种最坏组合。 同时会丢失本次新增的修复: - server/handler.go TraeWork 预刷新窗口按平台区分 - upstream/client.go 流式请求改用无总时长上限的 client ## 为何这四处无法移到 addon 独有包(已逐项验证) - pool.go : 改了 Pick()/pickExcluding() 的**内部实现**。 Go 不支持方法覆写 —— 同名函数放同包新文件会直接编译冲突; 而所需状态 lowCredit/lowCredits 是**私有字段**, 外部包(Go 语言规则)无法访问。 - config.go : LowCreditThreshold 是 config.Config 的字段,被 svc.go 引用。 - server/handler.go: 改 chatCompletions 内部逻辑。 - upstream/client.go: 改 Client 结构体与方法。 结论:这四个文件**注定要被 addon 修改**,只能 PROTECT。 ## 改动 1. PROTECT_FILES 分为 A 类(addon 独有文件)/ B 类(addon 修改过的上游文件), B 类新增 pool/config/server/upstream 四个文件,并注明各自不可外移的原因。 2. 新增 B 类受保护文件的**上游差异检查**:sync 结束时逐个比对, 若上游自基线(BASELINE_COMMIT)后改动过,打印 `[WARN] ... 需人工 merge` 并汇总。目的是让 PROTECT **不成为黑洞** —— 上游的改动不会被永久静默忽略。 3. 比较时对上游做 `tr -d '\r'`:上游文件是 CRLF、addon 是 LF, 不去掉会让全文件都显示为有差异而失去意义(这个坑已实测踩到)。 ## 代价(已在 SYNC.md 载明) 这四个文件**不再跟随上游自动更新**,上游改了需人工 merge。 该代价**目前为零**:经核对,上游自基线 c62d0bc 起对 pool/config/server/upstream 的改动提交数为 0。 ## 验证 完整链路实测(干净副本,未污染工作区): sync_vendor.sh exit=0 go build ./... exit=0 ← 改造前此步失败 go test ./... 7 包通过 / 0 失败 六处改动在 sync 后全部幸存:TZ、TraeWork 预热、StreamHTTP、 is_custom_model、SetLowCredits、LowCreditThreshold。 WARN 路径亦实测:把基线改为 13e7b64 后正确输出 「pool.go 上游改动了 1 次 / handler.go 改动了 2 次」。 --- SYNC.md | 37 +++++++++++++++-- src/sync_vendor.sh | 100 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 131 insertions(+), 6 deletions(-) diff --git a/SYNC.md b/SYNC.md index 314702f..3183690 100644 --- a/SYNC.md +++ b/SYNC.md @@ -27,9 +27,31 @@ addon 采用「复制 + 生成式 sync + 适配层隔离」策略,而非 Go mo | 包 | 来源 | 更新策略 | |---|---|---| -| `auth` `config` `login` `login_qoder` `login_trae` `pool` `provider` `qoder` `scheduler` `server` `traework` `upstream` | 上游 | **sync 生成**,不带手写改动(见 GENERATED 头) | -| `svc` | addon 独有 | 自行维护(HA 加载 `trae-*.json`) | -| `trae_adaptor` | addon 独有 | 自行维护(公网回调 + refreshToken 兜底,桌面版没有) | +| `auth` `login` `login_qoder` `login_trae` `provider` `qoder` `scheduler` `traework` | 上游 | **sync 生成**,不带手写改动(见 GENERATED 头) | +| `svc` `trae_adaptor` | addon 独有 | 自行维护 | +| **`pool` `config` `server` `upstream`** | 上游 + **addon 改动** | ⚠️ **受保护,不被 sync 覆盖**,见下节 | + +## ⚠️ 受保护文件(B 类)——为什么它们不再自动同步 + +addon 对 `pool` / `config` / `server` / `upstream` 四个包做了改动,这些改动 +**无法搬到 addon 独有包**,已逐项验证原因: + +| 文件 | addon 改动 | 为何无法外移 | +|---|---|---| +| `pool/pool.go` | `SetLowCredits`、`Pick()` 语义改为排除低积分账号 | 改了 `Pick()`/`pickExcluding()` 的**内部实现**。Go **不支持方法覆写**(同名函数放同包新文件会编译冲突);所需状态 `lowCredit`/`lowCredits` 是**私有字段**,外部包访问不了 | +| `config/config.go` | 新增 `LowCreditThreshold` | 被 `svc.go` 引用;字段是 `config.Config` 的一部分 | +| `server/handler.go` | TraeWork 预刷新窗口按平台区分 | 改 `chatCompletions` 内部逻辑 | +| `upstream/client.go` | 流式请求用无总时长上限的 client | 改 `Client` 结构体与方法 | + +**代价(须知情)**:这些文件**不再跟随上游自动更新**。上游若改动了它们, +需要**人工 merge**。 + +**这个代价目前为零**:经核对,上游自基线 `c62d0bc` 起对 +`pool` / `config` / `server` / `upstream` 的改动提交数为 **0**。 + +**不会成为黑洞**:`sync_vendor.sh` 结束时会对这些文件做**上游差异检查**, +若上游在基线之后改动过,会显式打印 `[WARN]` 并汇总提示需人工 merge +(而不是静默跳过)。 ## 上游更新时怎么做(一次性核对) @@ -56,8 +78,15 @@ cd D:/ai-hub/integrations/ha-ai-proxy/src 生成脚本会: 1. 复制上游 `internal/{auth,config,login,login_qoder,login_trae,pool,provider,qoder,scheduler,server,traework,upstream}` 到 addon `src/internal/` 2. 在每个生成文件顶部打 `// CODE GENERATED FROM wild-work@ — DO NOT EDIT, run sync_vendor.sh` -3. **不动** `svc` 和 `trae_adaptor`(addon 独有) +3. **不动** `svc` / `trae_adaptor`(addon 独有)与 **B 类受保护文件**(见上节) 4. 输出变更文件列表供 review +5. **对 B 类受保护文件做上游差异检查**,上游改过的打印 `[WARN]` 提示人工 merge + +> ⚠️ **sync 前务必确认工作区干净**(`git status` 无未提交改动)。 +> sync 会直接覆盖 20+ 个文件,未提交的改动无法恢复。 +> +> ⚠️ **如果上游尚未更新**(`git log OLD..HEAD` 无输出),**不要跑 sync**。 +> 跑了只会把 addon 落后的无关文件一起刷新,制造噪音 commit。 ## 本地验证编译(实测可用) diff --git a/src/sync_vendor.sh b/src/sync_vendor.sh index f977e7f..ef18e05 100644 --- a/src/sync_vendor.sh +++ b/src/sync_vendor.sh @@ -28,11 +28,48 @@ COMMIT="$(cd "${UPSTREAM_DIR}" && git rev-parse --short HEAD)" UP_INTERNAL="${UPSTREAM_DIR}/internal" SYNC_PKGS="auth config login login_qoder login_trae pool provider qoder scheduler server traework upstream" -# 以下文件是 addon 独有、手维护,sync 时绝不覆盖(保留本地版本) -# 用数组 + 逐个比对,避免把换行塞进 case 模式导致匹配失效(旧写法静默失效过) +# 上次实际同步到的上游 commit。用于判断「上游自基线后是否改动过受保护文件」。 +# 同步成功且人工确认无遗漏后,把这里更新为新的 COMMIT。 +# 见 SYNC.md「对齐基线」。 +BASELINE_COMMIT="c62d0bc" + +# ── 受保护文件(sync 时不覆盖,保留 addon 版本)───────────────────────────── +# +# 分两类,原因不同: +# +# 【A 类】addon 独有文件 —— 上游根本没有这些文件,PROTECT 只是显式声明。 +# +# 【B 类】addon 修改过的上游文件 —— 这些文件上游有,但 addon 做了**无法外移**的改动。 +# 为何无法外移(已逐项验证): +# - pool.go : 改了 Pick()/pickExcluding() 的**内部实现**。Go 不支持方法覆写, +# 同名函数放同包新文件会直接编译冲突;而所需状态 +# (lowCredit/lowCredits) 是**私有字段**,外部包访问不了。 +# - config.go: 新增 LowCreditThreshold 字段,被 svc.go 引用。 +# - server/handler.go: TraeWork 预刷新窗口按平台区分(defaultRefreshSkew)。 +# - upstream/client.go: 流式请求改用无总时长上限的 client(StreamHTTP)。 +# +# ⚠️ B 类的代价:这些文件**不再跟随上游自动更新**。上游若改动了它们, +# 本脚本会在结尾**显式报告**(不会静默跳过),届时需人工 merge。 PROTECT_FILES=( + # A 类:addon 独有 "internal/login_trae/addon_extras.go" "internal/svc/svc.go" + # B 类:addon 修改过的上游文件 + "internal/pool/pool.go" + "internal/config/config.go" + "internal/server/handler.go" + "internal/upstream/client.go" + "internal/upstream/headers.go" + "internal/traework/constants.go" +) + +# B 类保护文件清单:用于结尾的上游差异检查。 +# 只对这些文件检查「上游相对当前 addon 副本是否已变化」。 +CHECK_UPSTREAM_FILES=( + "internal/pool/pool.go" + "internal/config/config.go" + "internal/server/handler.go" + "internal/upstream/client.go" "internal/upstream/headers.go" "internal/traework/constants.go" ) @@ -69,3 +106,62 @@ done echo "==> done: ${CHANGED} files changed (from wild-work@${COMMIT})" if [ "${CHANGED}" -gt 0 ]; then echo " run: go build ./... && go vet ./internal/..."; fi + +# ── B 类保护文件的上游差异检查 ──────────────────────────────────────────── +# +# 受保护文件不会被覆盖,但**必须让人知道上游是否动过它们** —— +# 否则 PROTECT 就成了黑洞:上游的修复/改动被永久静默忽略。 +# +# 判断方式:把上游当前版本按 sync 的同样方式规范化(去 CRLF、改 module 前缀、 +# 去掉 GENERATED 头),与 addon 本地版本比较。若**上游自身**相对上次同步基线 +# 发生了变化,本地副本与上游的差异就会超出 addon 自有改动,此处给出提示。 +# +# 注意:上游文件是 CRLF、addon 是 LF,比较前必须 tr -d '\r', +# 否则全文件都会显示为「有差异」而失去意义(这个坑踩过)。 +echo +echo "==> 受保护文件(B 类)上游差异检查" +NEED_MERGE=0 +for pf in "${CHECK_UPSTREAM_FILES[@]}"; do + up_file="${UP_INTERNAL}/${pf#internal/}" + ad_file="${ADDON_INTERNAL}/${pf#internal/}" + if [ ! -f "${up_file}" ]; then + echo " [n/a ] ${pf} — 上游已无此文件,请确认是否应移除" + continue + fi + if [ ! -f "${ad_file}" ]; then + echo " [n/a ] ${pf} — addon 本地不存在" + continue + fi + # 规范化上游版本:去 CRLF、改 module 前缀 + norm_up="$(mktemp)" + sed "s#wild-work/internal#github.com/rockswang/workbuddy-wild/internal#g" "${up_file}" | tr -d '\r' > "${norm_up}" + # addon 版本:去掉 GENERATED 头两行 + norm_ad="$(mktemp)" + tail -n +3 "${ad_file}" > "${norm_ad}" + + diff_lines=$(diff "${norm_up}" "${norm_ad}" 2>/dev/null | grep -c '^[<>]' || true) + rm -f "${norm_up}" "${norm_ad}" + + if [ "${diff_lines}" -eq 0 ]; then + echo " [ok ] ${pf} — 与上游一致(addon 未改动或已同步)" + else + # 有差异是正常的(addon 自有改动)。真正要提示的是「上游在基线之后动过它」。 + if [ -n "${BASELINE_COMMIT:-}" ]; then + up_changed=$(cd "${UPSTREAM_DIR}" && git log --oneline "${BASELINE_COMMIT}..${COMMIT}" -- "internal/${pf#internal/}" 2>/dev/null | wc -l || echo 0) + if [ "${up_changed}" -gt 0 ]; then + echo " [WARN] ${pf} — 上游自基线 ${BASELINE_COMMIT} 起改动了 ${up_changed} 次,需人工 merge" + NEED_MERGE=$((NEED_MERGE+1)) + else + echo " [keep] ${pf} — 差异 ${diff_lines} 行(addon 自有改动;上游未动)" + fi + else + echo " [keep] ${pf} — 差异 ${diff_lines} 行(addon 自有改动)" + fi + fi +done + +if [ "${NEED_MERGE}" -gt 0 ]; then + echo + echo "⚠️ 有 ${NEED_MERGE} 个受保护文件上游已改动,请人工 merge 后再提交。" + echo " (这些文件不会自动同步,是设计使然:见 PROTECT_FILES 注释)" +fi