diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 93777a3..bfcf0ae 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -59,10 +59,15 @@ 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 }} + # 必须显式传小写前缀。该 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 }} @@ -79,7 +84,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,11 +104,12 @@ 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 }} 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 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