chore: refresh fork update patch
This commit is contained in:
1 parent
d3c2164e3c
commit
afbe2bba25
2 files changed
+549
-19
No files matched your search
+10
-8
@@ -7,19 +7,21 @@
|
||||
- Sub2API 基础版本:`0.1.176`
|
||||
- Git 描述:`v0.1.176-5-gfbfdcef81`
|
||||
- 基础提交:`fbfdcef8184ae4b2e224d5cfc47cf1d0e3742710`
|
||||
- 目标源码提交:`8f002e1f74b12cf827153d34c5053043ff4e0549`
|
||||
- 目标 Git tree:`ce07803e4ecc68499cf0d2149b08db4a2c53495b`
|
||||
- SHA-256:`fb0cb5e8aa6fd9c8ab01f318d270082c11668b9030264bfaa35ae462eeb0fe29`
|
||||
- 文件大小:178,340 字节
|
||||
- 目标源码提交:`d3c2164e3c07a65eb47a6f11a55e0f03f70d59bc`
|
||||
- 目标 Git tree(不含 `patches/`):`8b0971756b761512b33dac6b2ffdda15afe90f16`
|
||||
- SHA-256:`598d800e855b65ed2ad8230a92b6c8b23cf20033ba38e637a183251270575982`
|
||||
- 文件大小:200,905 字节
|
||||
|
||||
该 Patch 包含 Codex 5h / 7d 额度透支后端、前端显示、配置、源码构建 Compose、品牌和公开部署文档,并额外包含以下修复:
|
||||
|
||||
- OpenAI OAuth 常规文本“测试账号连接”使用透支请求形态,并接入额度观察及明确额度 429 探测。
|
||||
- `passed/recovered` 后清理并发 429 遗留的账号级限流状态,避免账号实际可用但页面仍显示“限流中”。
|
||||
- API Key、Shadow、图片和 Compact 测试保持原行为。
|
||||
- 更新检查跟踪 `DeanZFC/sub2api-overdraft` 的 `codex-overdraft` 分支和 `FORK_VERSION`,不再把官方 Release 误报为 Fork 更新。
|
||||
- 源码构建只提示 `git pull` 和重新构建,禁用可能覆盖 Fork 功能的二进制在线更新与回退。
|
||||
- 新增对应后端单元测试和故障排查日志。
|
||||
|
||||
Patch 共修改 44 个源码和文档文件。`patches/` 目录本身不包含在 Patch 中,避免 Patch 递归包含自身。
|
||||
Patch 共修改 50 个源码和文档文件。`patches/` 目录本身不包含在 Patch 中,避免 Patch 递归包含自身。
|
||||
|
||||
## 在精确基线上应用
|
||||
|
||||
@@ -55,10 +57,10 @@ git apply --3way /tmp/sub2api-overdraft.patch
|
||||
|
||||
## 完整性验证
|
||||
|
||||
本 Patch 已在临时工作树中从基础提交执行正向应用和反向检查。应用后写入的 Git tree 为:
|
||||
本 Patch 已在临时工作树中从基础提交执行 `git apply --check` 和三方应用。应用后写入的 Git tree 为:
|
||||
|
||||
```text
|
||||
ce07803e4ecc68499cf0d2149b08db4a2c53495b
|
||||
8b0971756b761512b33dac6b2ffdda15afe90f16
|
||||
```
|
||||
|
||||
它与目标源码提交 `8f002e1f74b12cf827153d34c5053043ff4e0549` 的 Git tree 完全一致。
|
||||
它与目标源码提交 `d3c2164e3c07a65eb47a6f11a55e0f03f70d59bc` 排除 `patches/` 后的 Git tree 完全一致。
|
||||
@@ -1,9 +1,9 @@
|
||||
diff --git a/CODEX_OVERDRAFT_DEPLOYMENT_CN.md b/CODEX_OVERDRAFT_DEPLOYMENT_CN.md
|
||||
new file mode 100644
|
||||
index 0000000000000000000000000000000000000000..28b57e0a78a10ac82987a095c9d0f34c74d4a208
|
||||
index 0000000000000000000000000000000000000000..d3b682534b997a725da80fa4738056020669c465
|
||||
--- /dev/null
|
||||
+++ b/CODEX_OVERDRAFT_DEPLOYMENT_CN.md
|
||||
@@ -0,0 +1,515 @@
|
||||
@@ -0,0 +1,526 @@
|
||||
+# sub2api-overdraft 部署与运维指南
|
||||
+
|
||||
+本文档面向希望部署本项目、验证 Codex 5h / 7d 额度透支状态,以及后续安全升级的使用者。
|
||||
@@ -17,6 +17,7 @@ index 0000000000000000000000000000000000000000..28b57e0a78a10ac82987a095c9d0f34c
|
||||
+- 探测请求和透支期请求都可能产生真实上游用量和费用。
|
||||
+- 此行为可能不符合上游服务条款。部署者必须自行确认合规性,并承担账号限制、服务中断等风险。
|
||||
+- 官方 Sub2API 安装脚本和 `weishaw/sub2api:latest` 镜像不包含本 Fork 的透支功能。
|
||||
+- 本 Fork 的后台更新检查读取 `DeanZFC/sub2api-overdraft`,不会把官方 Sub2API Release 当作本项目更新。
|
||||
+
|
||||
+## 功能范围
|
||||
+
|
||||
@@ -364,6 +365,8 @@ index 0000000000000000000000000000000000000000..28b57e0a78a10ac82987a095c9d0f34c
|
||||
+
|
||||
+## 日常升级本 Fork
|
||||
+
|
||||
+源码镜像的当前版本来自仓库根目录的 `FORK_VERSION`。后台检查更新时读取 GitHub 上 `codex-overdraft` 分支的同名文件:远端版本更高时显示更新提示,版本一致时显示最新。源码构建不会执行二进制在线更新或在线回退,以免官方程序覆盖 Fork 功能。
|
||||
+
|
||||
+运行数据均位于被 Git 忽略的目录中,正常 `git pull` 不会覆盖:
|
||||
+
|
||||
+```text
|
||||
@@ -390,6 +393,14 @@ index 0000000000000000000000000000000000000000..28b57e0a78a10ac82987a095c9d0f34c
|
||||
+
|
||||
+如果服务器目录是 `/opt/sub2api`,只需替换第一条路径。升级完成后重新执行“判断功能是否生效”中的镜像、环境变量和日志检查。
|
||||
+
|
||||
+更新后可在管理后台点击刷新,版本应显示为类似 `v0.1.176-overdraft.1`,更新方式应提示源码构建使用 `git pull`。也可以直接检查容器内二进制版本:
|
||||
+
|
||||
+```bash
|
||||
+docker exec sub2api /app/sub2api -version
|
||||
+```
|
||||
+
|
||||
+维护者发布新的源码版本时必须递增 `FORK_VERSION`,例如从 `0.1.176-overdraft.1` 改为 `0.1.176-overdraft.2`。同步到新的上游 Sub2API 版本时,使用 `0.1.177-overdraft.1` 这样的版本号。
|
||||
+
|
||||
+## 合并 Sub2API 官方更新
|
||||
+
|
||||
+本仓库保留两个分支角色:
|
||||
@@ -521,10 +532,10 @@ index 0000000000000000000000000000000000000000..28b57e0a78a10ac82987a095c9d0f34c
|
||||
+- 本项目不是 Sub2API 官方发行版,也不受 OpenAI 或其他上游服务商认可或支持。
|
||||
diff --git a/CODEX_QUOTA_OVERDRAFT_CUSTOMIZATION.md b/CODEX_QUOTA_OVERDRAFT_CUSTOMIZATION.md
|
||||
new file mode 100644
|
||||
index 0000000000000000000000000000000000000000..ec5f417f6fa910f6f08d20a142b36b9fceae4b9e
|
||||
index 0000000000000000000000000000000000000000..ae1cd6c60ba753c6e950c64d86391029e89f7fc8
|
||||
--- /dev/null
|
||||
+++ b/CODEX_QUOTA_OVERDRAFT_CUSTOMIZATION.md
|
||||
@@ -0,0 +1,157 @@
|
||||
@@ -0,0 +1,165 @@
|
||||
+# sub2api-overdraft 5h / 7d 额度透支定制记录
|
||||
+
|
||||
+本文件是维护者实现与升级记录。面向部署者的完整安装、验证、升级和故障排查说明见
|
||||
@@ -536,6 +547,14 @@ index 0000000000000000000000000000000000000000..ec5f417f6fa910f6f08d20a142b36b9f
|
||||
+- 参考实现:<https://github.com/Mxucc/cpa-account-config-manager>
|
||||
+- 功能开关:`gateway.codex_quota_overdraft_enabled`
|
||||
+- 代码默认值:关闭;`deploy/config.example.yaml` 部署示例默认开启
|
||||
+- Fork 版本文件:`FORK_VERSION`
|
||||
+- 更新源:`DeanZFC/sub2api-overdraft` 的 `codex-overdraft` 分支
|
||||
+
|
||||
+## Fork 更新检查
|
||||
+
|
||||
+源码 Docker 构建会读取根目录的 `FORK_VERSION`,并以 `BuildType=source` 写入二进制。后台更新服务通过 GitHub Contents API 读取 Fork 分支上的同名文件,使用语义化版本比较判断是否有新版本。Redis 缓存同时记录仓库和构建类型,因此旧的官方更新缓存不会继续生效。
|
||||
+
|
||||
+源码构建仅显示 `git pull` 更新提示,`PerformUpdate`、指定版本回退和在线回退列表均被禁用,防止官方二进制覆盖透支功能。维护者每次发布 Fork 更新都必须递增 `FORK_VERSION`;普通补丁使用 `0.1.176-overdraft.2`,同步上游版本后使用 `0.1.177-overdraft.1`。
|
||||
+
|
||||
+## 后台测试连接与残留限流修复
|
||||
+
|
||||
@@ -683,10 +702,44 @@ index 0000000000000000000000000000000000000000..ec5f417f6fa910f6f08d20a142b36b9f
|
||||
+WHERE platform = 'openai' AND type = 'oauth';
|
||||
+```
|
||||
diff --git a/Dockerfile b/Dockerfile
|
||||
index b2dfd7034f769abd7e8640090282e6b05a2af4a1..9f50fe85a1ca942a488715afef4628cde5795a1a 100644
|
||||
index b2dfd7034f769abd7e8640090282e6b05a2af4a1..657985091514edeb8358800d4ac49ce7299edd38 100644
|
||||
--- a/Dockerfile
|
||||
+++ b/Dockerfile
|
||||
@@ -108,9 +108,10 @@ FROM ${POSTGRES_IMAGE} AS pg-client
|
||||
@@ -56,6 +56,7 @@ FROM --platform=${BUILDPLATFORM} ${GOLANG_IMAGE} AS backend-builder
|
||||
ARG VERSION=
|
||||
ARG COMMIT=docker
|
||||
ARG DATE
|
||||
+ARG BUILD_TYPE=source
|
||||
ARG GOPROXY
|
||||
ARG GOSUMDB
|
||||
# Populated by buildx from the --platform target (e.g. linux/amd64).
|
||||
@@ -79,20 +80,22 @@ RUN --mount=type=cache,id=sub2api-gomod,target=/go/pkg/mod \
|
||||
|
||||
# Copy backend source first
|
||||
COPY backend/ ./
|
||||
+COPY FORK_VERSION /app/FORK_VERSION
|
||||
|
||||
# Copy frontend dist from previous stage (must be after backend copy to avoid being overwritten)
|
||||
COPY --from=frontend-builder /app/backend/internal/web/dist ./internal/web/dist
|
||||
|
||||
-# Build the binary (BuildType=release for CI builds, embed frontend)
|
||||
-# Version precedence: build arg VERSION > exact git tag > cmd/server/VERSION
|
||||
+# Build the binary and embed the frontend. Fork source builds read FORK_VERSION;
|
||||
+# CI release builds still override VERSION and BUILD_TYPE explicitly.
|
||||
RUN --mount=type=cache,id=sub2api-gomod,target=/go/pkg/mod \
|
||||
--mount=type=cache,id=sub2api-gobuild,target=/root/.cache/go-build \
|
||||
VERSION_VALUE="${VERSION}" && \
|
||||
+ if [ -z "${VERSION_VALUE}" ] && [ -s /app/FORK_VERSION ]; then VERSION_VALUE="$(tr -d '\r\n' < /app/FORK_VERSION)"; fi && \
|
||||
if [ -z "${VERSION_VALUE}" ]; then VERSION_VALUE="$(./scripts/resolve-version.sh)"; fi && \
|
||||
DATE_VALUE="${DATE:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}" && \
|
||||
CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build \
|
||||
-tags embed \
|
||||
- -ldflags="-s -w -X main.Version=${VERSION_VALUE} -X main.Commit=${COMMIT} -X main.Date=${DATE_VALUE} -X main.BuildType=release" \
|
||||
+ -ldflags="-s -w -X main.Version=${VERSION_VALUE} -X main.Commit=${COMMIT} -X main.Date=${DATE_VALUE} -X main.BuildType=${BUILD_TYPE}" \
|
||||
-trimpath \
|
||||
-o /app/sub2api \
|
||||
./cmd/server
|
||||
@@ -108,9 +111,10 @@ FROM ${POSTGRES_IMAGE} AS pg-client
|
||||
FROM ${ALPINE_IMAGE}
|
||||
|
||||
# Labels
|
||||
@@ -700,6 +753,13 @@ index b2dfd7034f769abd7e8640090282e6b05a2af4a1..9f50fe85a1ca942a488715afef4628cd
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apk add --no-cache \
|
||||
diff --git a/FORK_VERSION b/FORK_VERSION
|
||||
new file mode 100644
|
||||
index 0000000000000000000000000000000000000000..7770a2f7520859e155c2a8049db506d7a54063e6
|
||||
--- /dev/null
|
||||
+++ b/FORK_VERSION
|
||||
@@ -0,0 +1 @@
|
||||
+0.1.176-overdraft.1
|
||||
diff --git a/README.md b/README.md
|
||||
index f4ad2fa6b028bdc70ac0b502104a345dd8d940c1..4e0f4be6eea519b4c5c64928924776ebc4721b08 100644
|
||||
--- a/README.md
|
||||
@@ -763,7 +823,7 @@ index f4ad2fa6b028bdc70ac0b502104a345dd8d940c1..4e0f4be6eea519b4c5c64928924776eb
|
||||
|
||||
Please read the following carefully before using this project:
|
||||
diff --git a/README_CN.md b/README_CN.md
|
||||
index 2d2a57a9ff3a363618001dda3ffbfae84bcd0f41..dbfe54d7344edb53293d74d65f15b2fc9f600cd3 100644
|
||||
index 2d2a57a9ff3a363618001dda3ffbfae84bcd0f41..780544e69d8c4bcf48f7ab8c1e1679c99f39c376 100644
|
||||
--- a/README_CN.md
|
||||
+++ b/README_CN.md
|
||||
@@ -1,8 +1,8 @@
|
||||
@@ -777,7 +837,7 @@ index 2d2a57a9ff3a363618001dda3ffbfae84bcd0f41..dbfe54d7344edb53293d74d65f15b2fc
|
||||
|
||||
[](https://golang.org/)
|
||||
[](https://vuejs.org/)
|
||||
@@ -10,14 +10,73 @@
|
||||
@@ -10,14 +10,76 @@
|
||||
[](https://redis.io/)
|
||||
[](https://www.docker.com/)
|
||||
|
||||
@@ -801,6 +861,7 @@ index 2d2a57a9ff3a363618001dda3ffbfae84bcd0f41..dbfe54d7344edb53293d74d65f15b2fc
|
||||
+- 网络、超时、5xx 和普通瞬时 429 不会被误判为额度耗尽;401/403、账号禁用和其他风控仍使用原有策略。
|
||||
+- 多实例通过 PostgreSQL 原子领取(atomic claim)去重;状态保存在现有 `accounts.extra`,无需新增数据表。
|
||||
+- 可通过一个配置开关立即关闭,恢复上游 Sub2API 的调度和请求行为。
|
||||
+- 管理后台从 `DeanZFC/sub2api-overdraft` 的 `codex-overdraft` 分支检查更新,不再使用官方 Sub2API 的版本结果。
|
||||
+
|
||||
+## 快速部署
|
||||
+
|
||||
@@ -832,6 +893,8 @@ index 2d2a57a9ff3a363618001dda3ffbfae84bcd0f41..dbfe54d7344edb53293d74d65f15b2fc
|
||||
+
|
||||
+公开的 `docker-compose.overdraft.yml` 已通过环境变量默认开启该功能。
|
||||
+
|
||||
+源码镜像会把根目录的 `FORK_VERSION` 写入版本信息。管理后台检测到 Fork 新版本后只提示使用 `git pull` 更新源码,不会下载官方二进制覆盖透支功能。完整更新命令见部署指南的“日常升级本 Fork”。
|
||||
+
|
||||
+## 如何确认透支成功
|
||||
+
|
||||
+账号额度达到 100% 后检查日志:
|
||||
@@ -854,6 +917,21 @@ index 2d2a57a9ff3a363618001dda3ffbfae84bcd0f41..dbfe54d7344edb53293d74d65f15b2fc
|
||||
|
||||
## ⚠️ 重要提醒
|
||||
|
||||
@@ -273,12 +335,9 @@ sudo systemctl enable sub2api
|
||||
|
||||
#### 升级
|
||||
|
||||
-可以直接在 **管理后台** 左上角点击 **检测更新** 按钮进行在线升级。
|
||||
+官方发行版可以直接在管理后台进行二进制在线升级。本 Fork 使用源码构建,管理后台会读取 `DeanZFC/sub2api-overdraft` 的 `FORK_VERSION` 检测新版本,并提示使用 `git pull` 后重新构建;不会在线替换二进制。
|
||||
|
||||
-网页升级功能支持:
|
||||
-- 自动检测新版本
|
||||
-- 一键下载并应用更新
|
||||
-- 支持回滚
|
||||
+本 Fork 的升级命令见 [日常升级本 Fork](CODEX_OVERDRAFT_DEPLOYMENT_CN.md#日常升级本-fork)。源码构建不支持页面内一键升级和在线回退,避免误装官方二进制而丢失透支功能。
|
||||
|
||||
#### 常用命令
|
||||
|
||||
diff --git a/README_JA.md b/README_JA.md
|
||||
index 09aa77ee48aec83440ee76054887b2627c1e3792..03c1c30a544c193fd1ffc05a4297344905c2a011 100644
|
||||
--- a/README_JA.md
|
||||
@@ -1214,6 +1292,138 @@ index 76d7e0bdc5fe272bbb4aa1cf24e543ec9acb4a43..328e9f2853005aa9d9287f0de6bca64d
|
||||
+ require.Contains(t, overdraftSQL, `"type" =`)
|
||||
+ require.Contains(t, overdraftSQL, `"parent_account_id" IS NULL`)
|
||||
+}
|
||||
diff --git a/backend/internal/repository/github_release_service.go b/backend/internal/repository/github_release_service.go
|
||||
index 282a215361b33d99f62d6e0fd259d21f5c58e164..f3898d23cc60256a37a4f7ada67f366f4e5f3175 100644
|
||||
--- a/backend/internal/repository/github_release_service.go
|
||||
+++ b/backend/internal/repository/github_release_service.go
|
||||
@@ -2,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
+ "encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -112,6 +113,10 @@ func (c *githubReleaseClientError) FetchRecentReleases(ctx context.Context, repo
|
||||
return nil, c.err
|
||||
}
|
||||
|
||||
+func (c *githubReleaseClientError) FetchRepositoryFile(ctx context.Context, repo, ref, filePath string) ([]byte, error) {
|
||||
+ return nil, c.err
|
||||
+}
|
||||
+
|
||||
func (c *githubReleaseClientError) DownloadFile(ctx context.Context, url, dest string, maxSize int64) error {
|
||||
return c.err
|
||||
}
|
||||
@@ -178,6 +183,56 @@ func (c *githubReleaseClient) FetchRecentReleases(ctx context.Context, repo stri
|
||||
return releases, nil
|
||||
}
|
||||
|
||||
+func (c *githubReleaseClient) FetchRepositoryFile(ctx context.Context, repo, ref, filePath string) ([]byte, error) {
|
||||
+ repo = strings.Trim(strings.TrimSpace(repo), "/")
|
||||
+ ref = strings.TrimSpace(ref)
|
||||
+ filePath = strings.Trim(strings.TrimSpace(filePath), "/")
|
||||
+ if repo == "" || ref == "" || filePath == "" || strings.Contains(filePath, "..") {
|
||||
+ return nil, fmt.Errorf("invalid GitHub repository file reference")
|
||||
+ }
|
||||
+
|
||||
+ endpoint := fmt.Sprintf("https://api.github.com/repos/%s/contents/%s", repo, filePath)
|
||||
+ parsed, err := url.Parse(endpoint)
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ query := parsed.Query()
|
||||
+ query.Set("ref", ref)
|
||||
+ parsed.RawQuery = query.Encode()
|
||||
+ req, err := c.newAPIRequest(ctx, parsed.String())
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+
|
||||
+ resp, err := c.httpClient.Do(req)
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ defer func() { _ = resp.Body.Close() }()
|
||||
+ if resp.StatusCode != http.StatusOK {
|
||||
+ return nil, fmt.Errorf("GitHub API returned %d", resp.StatusCode)
|
||||
+ }
|
||||
+
|
||||
+ var payload struct {
|
||||
+ Encoding string `json:"encoding"`
|
||||
+ Content string `json:"content"`
|
||||
+ }
|
||||
+ if err := json.NewDecoder(io.LimitReader(resp.Body, 256<<10)).Decode(&payload); err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ if payload.Encoding != "base64" {
|
||||
+ return nil, fmt.Errorf("unsupported GitHub file encoding %q", payload.Encoding)
|
||||
+ }
|
||||
+ decoded, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(payload.Content, "\n", ""))
|
||||
+ if err != nil {
|
||||
+ return nil, fmt.Errorf("decode GitHub repository file: %w", err)
|
||||
+ }
|
||||
+ if len(decoded) > 64<<10 {
|
||||
+ return nil, fmt.Errorf("GitHub repository file is too large")
|
||||
+ }
|
||||
+ return decoded, nil
|
||||
+}
|
||||
+
|
||||
func (c *githubReleaseClient) DownloadFile(ctx context.Context, url, dest string, maxSize int64) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
diff --git a/backend/internal/repository/github_release_service_test.go b/backend/internal/repository/github_release_service_test.go
|
||||
index 3f93f0dfb607f32f078434f1ae1f1e75a05696be..fd7c28dd7a7cd18d07e0165ac6e1b8c950ceb947 100644
|
||||
--- a/backend/internal/repository/github_release_service_test.go
|
||||
+++ b/backend/internal/repository/github_release_service_test.go
|
||||
@@ -3,6 +3,7 @@ package repository
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
+ "encoding/base64"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -393,6 +394,39 @@ func (s *GitHubReleaseServiceSuite) TestFetchRecentReleases_Success() {
|
||||
require.Equal(s.T(), "v1.0.0", releases[2].TagName)
|
||||
}
|
||||
|
||||
+func (s *GitHubReleaseServiceSuite) TestFetchRepositoryFile_Success() {
|
||||
+ encoded := base64.StdEncoding.EncodeToString([]byte("0.1.176-overdraft.1\n"))
|
||||
+ s.srv = newLocalTestServer(s.T(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
+ require.Equal(s.T(), "/repos/DeanZFC/sub2api-overdraft/contents/FORK_VERSION", r.URL.Path)
|
||||
+ require.Equal(s.T(), "codex-overdraft", r.URL.Query().Get("ref"))
|
||||
+ require.Equal(s.T(), "application/vnd.github.v3+json", r.Header.Get("Accept"))
|
||||
+ w.Header().Set("Content-Type", "application/json")
|
||||
+ _, _ = w.Write([]byte(`{"encoding":"base64","content":"` + encoded + `"}`))
|
||||
+ }))
|
||||
+ s.client = &githubReleaseClient{
|
||||
+ httpClient: &http.Client{Transport: &testTransport{testServerURL: s.srv.URL}},
|
||||
+ downloadHTTPClient: &http.Client{},
|
||||
+ }
|
||||
+
|
||||
+ content, err := s.client.FetchRepositoryFile(
|
||||
+ context.Background(),
|
||||
+ "DeanZFC/sub2api-overdraft",
|
||||
+ "codex-overdraft",
|
||||
+ "FORK_VERSION",
|
||||
+ )
|
||||
+
|
||||
+ require.NoError(s.T(), err)
|
||||
+ require.Equal(s.T(), "0.1.176-overdraft.1\n", string(content))
|
||||
+}
|
||||
+
|
||||
+func (s *GitHubReleaseServiceSuite) TestFetchRepositoryFile_RejectsTraversal() {
|
||||
+ s.client = newTestGitHubReleaseClient()
|
||||
+
|
||||
+ _, err := s.client.FetchRepositoryFile(context.Background(), "test/repo", "main", "../secret")
|
||||
+
|
||||
+ require.Error(s.T(), err)
|
||||
+}
|
||||
+
|
||||
func (s *GitHubReleaseServiceSuite) TestFetchRecentReleases_Non200() {
|
||||
s.srv = newLocalTestServer(s.T(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
diff --git a/backend/internal/service/account_test_service.go b/backend/internal/service/account_test_service.go
|
||||
index 9e1d29c2f945b42a9ce5d1e5d7990ef72d7d7a95..450af91c8d7d3608c68087da7369c692f3d40436 100644
|
||||
--- a/backend/internal/service/account_test_service.go
|
||||
@@ -3632,6 +3842,310 @@ index ee5b09bbb4b18afcc9f423b74ebea2419a82392b..b56b2b015273d6e463d3e50f8714c6e0
|
||||
|
||||
if err := s.accountRepo.SetTempUnschedulable(ctx, account.ID, *decision.Until, reason); err != nil {
|
||||
slog.Warn("account_scheduling_threshold_set_temp_unsched_failed",
|
||||
diff --git a/backend/internal/service/update_service.go b/backend/internal/service/update_service.go
|
||||
index 82df3bb024605360f36e75daa3891b1483995d27..659ee2c33189c22945278c5d85468cb6b72906ae 100644
|
||||
--- a/backend/internal/service/update_service.go
|
||||
+++ b/backend/internal/service/update_service.go
|
||||
@@ -20,17 +20,21 @@ import (
|
||||
"time"
|
||||
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
+ "golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoUpdateAvailable = infraerrors.Conflict("ALREADY_UP_TO_DATE", "no update available; current version is latest")
|
||||
ErrRollbackVersionNotAllowed = infraerrors.BadRequest("ROLLBACK_VERSION_NOT_ALLOWED", "version is not in the allowed rollback list")
|
||||
+ ErrSourceBuildUpdateRequired = infraerrors.Conflict("SOURCE_BUILD_UPDATE_REQUIRED", "source builds must be updated with git pull and rebuilt")
|
||||
)
|
||||
|
||||
const (
|
||||
- updateCacheKey = "update_check_cache"
|
||||
- updateCacheTTL = 1200 // 20 minutes
|
||||
- githubRepo = "Wei-Shaw/sub2api"
|
||||
+ updateCacheTTL = 1200 // 20 minutes
|
||||
+ githubRepo = "DeanZFC/sub2api-overdraft"
|
||||
+ githubSourceBranch = "codex-overdraft"
|
||||
+ githubForkVersionFile = "FORK_VERSION"
|
||||
+ githubSourceUpdateURL = "https://github.com/DeanZFC/sub2api-overdraft/commits/codex-overdraft"
|
||||
|
||||
// Security: allowed download domains for updates
|
||||
allowedDownloadHost = "github.com"
|
||||
@@ -55,6 +59,7 @@ type UpdateCache interface {
|
||||
type GitHubReleaseClient interface {
|
||||
FetchLatestRelease(ctx context.Context, repo string) (*GitHubRelease, error)
|
||||
FetchRecentReleases(ctx context.Context, repo string, perPage int) ([]*GitHubRelease, error)
|
||||
+ FetchRepositoryFile(ctx context.Context, repo, ref, filePath string) ([]byte, error)
|
||||
DownloadFile(ctx context.Context, url, dest string, maxSize int64) error
|
||||
FetchChecksumFile(ctx context.Context, url string) ([]byte, error)
|
||||
}
|
||||
@@ -138,8 +143,15 @@ func (s *UpdateService) CheckUpdate(ctx context.Context, force bool) (*UpdateInf
|
||||
}
|
||||
}
|
||||
|
||||
- // Fetch from GitHub
|
||||
- info, err := s.fetchLatestRelease(ctx)
|
||||
+ // Source builds track the Fork branch version file. Release builds track the
|
||||
+ // Fork's GitHub Releases and remain eligible for binary replacement.
|
||||
+ var info *UpdateInfo
|
||||
+ var err error
|
||||
+ if s.isSourceBuild() {
|
||||
+ info, err = s.fetchLatestSourceVersion(ctx)
|
||||
+ } else {
|
||||
+ info, err = s.fetchLatestRelease(ctx)
|
||||
+ }
|
||||
if err != nil {
|
||||
// Return cached on error
|
||||
if cached, cacheErr := s.getFromCache(ctx); cacheErr == nil && cached != nil {
|
||||
@@ -163,6 +175,9 @@ func (s *UpdateService) CheckUpdate(ctx context.Context, force bool) (*UpdateInf
|
||||
// PerformUpdate downloads and applies the update
|
||||
// Uses atomic file replacement pattern for safe in-place updates
|
||||
func (s *UpdateService) PerformUpdate(ctx context.Context) error {
|
||||
+ if s.isSourceBuild() {
|
||||
+ return ErrSourceBuildUpdateRequired
|
||||
+ }
|
||||
info, err := s.CheckUpdate(ctx, true)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -307,6 +322,9 @@ func (s *UpdateService) Rollback() error {
|
||||
// strictly older than the current version (the current version itself is excluded),
|
||||
// newest first. Draft and prerelease entries are skipped.
|
||||
func (s *UpdateService) ListRollbackVersions(ctx context.Context) ([]RollbackVersion, error) {
|
||||
+ if s.isSourceBuild() {
|
||||
+ return []RollbackVersion{}, nil
|
||||
+ }
|
||||
releases, err := s.fetchRollbackCandidates(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -327,6 +345,9 @@ func (s *UpdateService) ListRollbackVersions(ctx context.Context) ([]RollbackVer
|
||||
// The target must be one of the versions returned by ListRollbackVersions;
|
||||
// anything else (including the current version) is rejected.
|
||||
func (s *UpdateService) RollbackToVersion(ctx context.Context, version string) error {
|
||||
+ if s.isSourceBuild() {
|
||||
+ return ErrSourceBuildUpdateRequired
|
||||
+ }
|
||||
target := strings.TrimPrefix(strings.TrimSpace(version), "v")
|
||||
if target == "" {
|
||||
return ErrRollbackVersionNotAllowed
|
||||
@@ -432,6 +453,33 @@ func (s *UpdateService) fetchLatestRelease(ctx context.Context) (*UpdateInfo, er
|
||||
}, nil
|
||||
}
|
||||
|
||||
+func (s *UpdateService) fetchLatestSourceVersion(ctx context.Context) (*UpdateInfo, error) {
|
||||
+ raw, err := s.githubClient.FetchRepositoryFile(ctx, githubRepo, githubSourceBranch, githubForkVersionFile)
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ latestVersion := strings.TrimSpace(string(raw))
|
||||
+ if canonicalVersion(latestVersion) == "" {
|
||||
+ return nil, fmt.Errorf("Fork version file contains invalid semantic version %q", latestVersion)
|
||||
+ }
|
||||
+
|
||||
+ return &UpdateInfo{
|
||||
+ CurrentVersion: s.currentVersion,
|
||||
+ LatestVersion: latestVersion,
|
||||
+ HasUpdate: compareVersions(s.currentVersion, latestVersion) < 0,
|
||||
+ ReleaseInfo: &ReleaseInfo{
|
||||
+ Name: "sub2api-overdraft " + latestVersion,
|
||||
+ HTMLURL: githubSourceUpdateURL,
|
||||
+ },
|
||||
+ Cached: false,
|
||||
+ BuildType: s.buildType,
|
||||
+ }, nil
|
||||
+}
|
||||
+
|
||||
+func (s *UpdateService) isSourceBuild() bool {
|
||||
+ return strings.EqualFold(strings.TrimSpace(s.buildType), "source")
|
||||
+}
|
||||
+
|
||||
func (s *UpdateService) downloadFile(ctx context.Context, downloadURL, dest string) error {
|
||||
return s.githubClient.DownloadFile(ctx, downloadURL, dest, maxDownloadSize)
|
||||
}
|
||||
@@ -603,6 +651,8 @@ func (s *UpdateService) getFromCache(ctx context.Context) (*UpdateInfo, error) {
|
||||
Latest string `json:"latest"`
|
||||
ReleaseInfo *ReleaseInfo `json:"release_info"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
+ Repository string `json:"repository"`
|
||||
+ BuildType string `json:"build_type"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(data), &cached); err != nil {
|
||||
return nil, err
|
||||
@@ -611,6 +661,9 @@ func (s *UpdateService) getFromCache(ctx context.Context) (*UpdateInfo, error) {
|
||||
if time.Now().Unix()-cached.Timestamp > updateCacheTTL {
|
||||
return nil, fmt.Errorf("cache expired")
|
||||
}
|
||||
+ if cached.Repository != githubRepo || cached.BuildType != s.buildType {
|
||||
+ return nil, fmt.Errorf("cache belongs to a different update source")
|
||||
+ }
|
||||
|
||||
return &UpdateInfo{
|
||||
CurrentVersion: s.currentVersion,
|
||||
@@ -627,10 +680,14 @@ func (s *UpdateService) saveToCache(ctx context.Context, info *UpdateInfo) {
|
||||
Latest string `json:"latest"`
|
||||
ReleaseInfo *ReleaseInfo `json:"release_info"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
+ Repository string `json:"repository"`
|
||||
+ BuildType string `json:"build_type"`
|
||||
}{
|
||||
Latest: info.LatestVersion,
|
||||
ReleaseInfo: info.ReleaseInfo,
|
||||
Timestamp: time.Now().Unix(),
|
||||
+ Repository: githubRepo,
|
||||
+ BuildType: s.buildType,
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(cacheData)
|
||||
@@ -639,6 +696,12 @@ func (s *UpdateService) saveToCache(ctx context.Context, info *UpdateInfo) {
|
||||
|
||||
// compareVersions compares two semantic versions
|
||||
func compareVersions(current, latest string) int {
|
||||
+ currentSemver := canonicalVersion(current)
|
||||
+ latestSemver := canonicalVersion(latest)
|
||||
+ if currentSemver != "" && latestSemver != "" {
|
||||
+ return semver.Compare(currentSemver, latestSemver)
|
||||
+ }
|
||||
+
|
||||
currentParts := parseVersion(current)
|
||||
latestParts := parseVersion(latest)
|
||||
|
||||
@@ -653,6 +716,20 @@ func compareVersions(current, latest string) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
+func canonicalVersion(value string) string {
|
||||
+ value = strings.TrimSpace(value)
|
||||
+ if value == "" {
|
||||
+ return ""
|
||||
+ }
|
||||
+ if !strings.HasPrefix(value, "v") {
|
||||
+ value = "v" + value
|
||||
+ }
|
||||
+ if !semver.IsValid(value) {
|
||||
+ return ""
|
||||
+ }
|
||||
+ return value
|
||||
+}
|
||||
+
|
||||
func parseVersion(v string) [3]int {
|
||||
v = strings.TrimPrefix(v, "v")
|
||||
parts := strings.Split(v, ".")
|
||||
diff --git a/backend/internal/service/update_service_test.go b/backend/internal/service/update_service_test.go
|
||||
index 243e25d1e47d891434afabb274c709abbdaac935..99ef67944e07fe28af9ddc73161a616ba2d4e6d1 100644
|
||||
--- a/backend/internal/service/update_service_test.go
|
||||
+++ b/backend/internal/service/update_service_test.go
|
||||
@@ -28,12 +28,20 @@ func (s *updateServiceCacheStub) SetUpdateInfo(_ context.Context, data string, _
|
||||
}
|
||||
|
||||
type updateServiceGitHubClientStub struct {
|
||||
- release *GitHubRelease
|
||||
- recentReleases []*GitHubRelease
|
||||
- recentErr error
|
||||
+ release *GitHubRelease
|
||||
+ recentReleases []*GitHubRelease
|
||||
+ recentErr error
|
||||
+ repositoryFile []byte
|
||||
+ repositoryFileErr error
|
||||
+ latestRepo string
|
||||
+ fileRepo string
|
||||
+ fileRef string
|
||||
+ filePath string
|
||||
+ fileCalls int
|
||||
}
|
||||
|
||||
-func (s *updateServiceGitHubClientStub) FetchLatestRelease(context.Context, string) (*GitHubRelease, error) {
|
||||
+func (s *updateServiceGitHubClientStub) FetchLatestRelease(_ context.Context, repo string) (*GitHubRelease, error) {
|
||||
+ s.latestRepo = repo
|
||||
return s.release, nil
|
||||
}
|
||||
|
||||
@@ -41,6 +49,14 @@ func (s *updateServiceGitHubClientStub) FetchRecentReleases(context.Context, str
|
||||
return s.recentReleases, s.recentErr
|
||||
}
|
||||
|
||||
+func (s *updateServiceGitHubClientStub) FetchRepositoryFile(_ context.Context, repo, ref, filePath string) ([]byte, error) {
|
||||
+ s.fileCalls++
|
||||
+ s.fileRepo = repo
|
||||
+ s.fileRef = ref
|
||||
+ s.filePath = filePath
|
||||
+ return s.repositoryFile, s.repositoryFileErr
|
||||
+}
|
||||
+
|
||||
func (s *updateServiceGitHubClientStub) DownloadFile(context.Context, string, string, int64) error {
|
||||
panic("DownloadFile should not be called when no update is available")
|
||||
}
|
||||
@@ -69,6 +85,72 @@ func TestUpdateServicePerformUpdateNoUpdateReturnsSentinel(t *testing.T) {
|
||||
require.ErrorIs(t, err, ErrNoUpdateAvailable)
|
||||
}
|
||||
|
||||
+func TestUpdateServiceSourceBuildTracksForkVersionFile(t *testing.T) {
|
||||
+ client := &updateServiceGitHubClientStub{repositoryFile: []byte("0.1.176-overdraft.2\n")}
|
||||
+ svc := NewUpdateService(
|
||||
+ &updateServiceCacheStub{},
|
||||
+ client,
|
||||
+ "0.1.176-overdraft.1",
|
||||
+ "source",
|
||||
+ )
|
||||
+
|
||||
+ info, err := svc.CheckUpdate(context.Background(), true)
|
||||
+
|
||||
+ require.NoError(t, err)
|
||||
+ require.Equal(t, "0.1.176-overdraft.1", info.CurrentVersion)
|
||||
+ require.Equal(t, "0.1.176-overdraft.2", info.LatestVersion)
|
||||
+ require.True(t, info.HasUpdate)
|
||||
+ require.Equal(t, "source", info.BuildType)
|
||||
+ require.Equal(t, githubSourceUpdateURL, info.ReleaseInfo.HTMLURL)
|
||||
+ require.Equal(t, githubRepo, client.fileRepo)
|
||||
+ require.Equal(t, githubSourceBranch, client.fileRef)
|
||||
+ require.Equal(t, githubForkVersionFile, client.filePath)
|
||||
+ require.Empty(t, client.latestRepo, "源码构建不能查询官方或 Fork 的二进制 Release")
|
||||
+}
|
||||
+
|
||||
+func TestUpdateServiceSourceBuildIgnoresLegacyOfficialCache(t *testing.T) {
|
||||
+ cache := &updateServiceCacheStub{data: `{"latest":"0.1.176","timestamp":4102444800}`}
|
||||
+ client := &updateServiceGitHubClientStub{repositoryFile: []byte("0.1.176-overdraft.1\n")}
|
||||
+ svc := NewUpdateService(cache, client, "0.1.176-overdraft.1", "source")
|
||||
+
|
||||
+ info, err := svc.CheckUpdate(context.Background(), false)
|
||||
+
|
||||
+ require.NoError(t, err)
|
||||
+ require.False(t, info.HasUpdate)
|
||||
+ require.Equal(t, "0.1.176-overdraft.1", info.LatestVersion)
|
||||
+ require.Equal(t, 1, client.fileCalls)
|
||||
+}
|
||||
+
|
||||
+func TestUpdateServiceSourceBuildRejectsBinaryUpdate(t *testing.T) {
|
||||
+ svc := NewUpdateService(
|
||||
+ &updateServiceCacheStub{},
|
||||
+ &updateServiceGitHubClientStub{},
|
||||
+ "0.1.176-overdraft.1",
|
||||
+ "source",
|
||||
+ )
|
||||
+
|
||||
+ err := svc.PerformUpdate(context.Background())
|
||||
+
|
||||
+ require.ErrorIs(t, err, ErrSourceBuildUpdateRequired)
|
||||
+}
|
||||
+
|
||||
+func TestUpdateServiceReleaseBuildUsesForkRepository(t *testing.T) {
|
||||
+ client := &updateServiceGitHubClientStub{release: &GitHubRelease{TagName: "v0.1.177-overdraft.1"}}
|
||||
+ svc := NewUpdateService(&updateServiceCacheStub{}, client, "0.1.176-overdraft.1", "release")
|
||||
+
|
||||
+ info, err := svc.CheckUpdate(context.Background(), true)
|
||||
+
|
||||
+ require.NoError(t, err)
|
||||
+ require.True(t, info.HasUpdate)
|
||||
+ require.Equal(t, githubRepo, client.latestRepo)
|
||||
+}
|
||||
+
|
||||
+func TestCompareVersionsSupportsForkPrereleaseRevisions(t *testing.T) {
|
||||
+ require.Less(t, compareVersions("0.1.176-overdraft.1", "0.1.176-overdraft.2"), 0)
|
||||
+ require.Zero(t, compareVersions("v0.1.176-overdraft.2", "0.1.176-overdraft.2"))
|
||||
+ require.Greater(t, compareVersions("0.1.177-overdraft.1", "0.1.176-overdraft.9"), 0)
|
||||
+}
|
||||
+
|
||||
func newRollbackTestService(current string, releases []*GitHubRelease) *UpdateService {
|
||||
return NewUpdateService(
|
||||
&updateServiceCacheStub{},
|
||||
diff --git a/backend/internal/service/wire.go b/backend/internal/service/wire.go
|
||||
index 2e26c0035c22c55a4b89eb7bd39cc2b05ed2f7f4..1e853b64c4d4133aa2ddf940c63c07f94f8fcd7f 100644
|
||||
--- a/backend/internal/service/wire.go
|
||||
@@ -3746,10 +4260,10 @@ index 095c550993527ea2b53871088f58b02e4082fc63..cd71e4a3d60fd8d9e5db3c8e8b96d28b
|
||||
#
|
||||
diff --git a/deploy/docker-compose.overdraft.yml b/deploy/docker-compose.overdraft.yml
|
||||
new file mode 100644
|
||||
index 0000000000000000000000000000000000000000..3d81a0d37476034f0cc4649107dcfbf7a27c31b9
|
||||
index 0000000000000000000000000000000000000000..eeb8525db74c458ecc7a213f0732d871799b5300
|
||||
--- /dev/null
|
||||
+++ b/deploy/docker-compose.overdraft.yml
|
||||
@@ -0,0 +1,15 @@
|
||||
@@ -0,0 +1,16 @@
|
||||
+# sub2api-overdraft source-build overlay.
|
||||
+# Use together with docker-compose.local.yml from this directory:
|
||||
+# docker compose -f docker-compose.local.yml -f docker-compose.overdraft.yml up -d --build
|
||||
@@ -3761,8 +4275,9 @@ index 0000000000000000000000000000000000000000..3d81a0d37476034f0cc4649107dcfbf7
|
||||
+ context: ..
|
||||
+ dockerfile: Dockerfile
|
||||
+ args:
|
||||
+ VERSION: ${BUILD_VERSION:-codex-overdraft}
|
||||
+ VERSION: ${BUILD_VERSION:-}
|
||||
+ COMMIT: ${BUILD_COMMIT:-local}
|
||||
+ BUILD_TYPE: source
|
||||
+ environment:
|
||||
+ - GATEWAY_CODEX_QUOTA_OVERDRAFT_ENABLED=${GATEWAY_CODEX_QUOTA_OVERDRAFT_ENABLED:-true}
|
||||
diff --git a/frontend/index.html b/frontend/index.html
|
||||
@@ -4045,6 +4560,19 @@ index af5fc5d66d8646f5565a816e4a146adc515edd22..b7abe23af3e7a2309311f00aaeed0eee
|
||||
+ expect(wrapper.text()).toContain('$1.23')
|
||||
+ })
|
||||
})
|
||||
diff --git a/frontend/src/components/common/VersionBadge.vue b/frontend/src/components/common/VersionBadge.vue
|
||||
index 7fd62947f7ea83afa1c68a2e070b1d7733374be6..f3c74b52f0182a96fb649448bc57a3e02b0b85c6 100644
|
||||
--- a/frontend/src/components/common/VersionBadge.vue
|
||||
+++ b/frontend/src/components/common/VersionBadge.vue
|
||||
@@ -651,7 +651,7 @@ import {
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
|
||||
-const GITHUB_REPO = 'Wei-Shaw/sub2api'
|
||||
+const GITHUB_REPO = 'DeanZFC/sub2api-overdraft'
|
||||
// Docker Hub image published by CI (tags carry no "v" prefix, e.g. weishaw/sub2api:0.1.146)
|
||||
const DOCKER_IMAGE = 'weishaw/sub2api'
|
||||
|
||||
diff --git a/frontend/src/i18n/locales/en/dashboard.ts b/frontend/src/i18n/locales/en/dashboard.ts
|
||||
index 4204a9d679c54ad0c38e544c47edcec3074fe184..d51f339950e39d14e2fda2df4fbdd6e43abe56c8 100644
|
||||
--- a/frontend/src/i18n/locales/en/dashboard.ts
|
||||
|
||||
Reference in new issue
Block a user