Skip to content

Commit 2e7d103

Browse files
committed
Release v1.0.3
1 parent 8fc9534 commit 2e7d103

33 files changed

Lines changed: 1140 additions & 332 deletions

.env.example

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,12 @@ STORAGE_BACKEND=json
7777
# FLARESOLVERR_LOG_LEVEL=info
7878
# TZ=Asia/Shanghai
7979

80-
# Optional local iCloud Privacy Mail sidecar (`--profile local-icloud`)
81-
# sidecar 只在 Compose 内部网络监听 8787,不映射宿主机端口;本系统 provider 不需要 API Key
80+
# Optional iCloud Privacy Mail sidecar (`--profile local-icloud`)
81+
# sidecar 容器内监听 8787;Warp 编排默认映射到宿主机 127.0.0.1:8788
8282
# ICLOUD_PRIVACY_MAIL_API_KEY=replace_with_a_long_random_secret
83-
# ICLOUD_PRIVACY_MAIL_PUBLIC_BASE_URL=http://icloud-privacy-mail:8787
83+
# 返回给浏览器和外部调用方的邮箱 API 地址,必须从容器外可访问。
84+
# 本机 Warp Compose 默认映射到 8788;服务器请改为对外 HTTPS 地址。
85+
# ICLOUD_PRIVACY_MAIL_PUBLIC_BASE_URL=http://127.0.0.1:8788
8486
# ICLOUD_PRIVACY_MAIL_IMAGE=chatgpt2api/icloud-privacy-mail:2026.07.14.1
8587
# WARP 编排默认通过内部 Privoxy 请求 Apple;标准编排可按需填写外部 HTTP 代理。
8688
# ICLOUD_PRIVACY_MAIL_HTTP_PROXY=http://privoxy:8118

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## 1.0.3 - 2026-07-18
4+
5+
+ [修复] 更新中心的最新版本与更新日志统一从 GitHub Releases 和仓库读取,不再依赖独立更新服务器,手动检查会强制刷新 GitHub 缓存。
6+
+ [修复] Outlook 旧版 IMAP OAuth Token 自动兼容 Graph 权限差异,缓存不可用通道并重试瞬时 IMAP 超时,避免重复刷新触发 `AADSTS50196` 或误判 Token 失效。
7+
+ [优化] ChatGPT 注册邮箱等待保留真实 provider 错误;CF 邮箱短窗口重发一次后自动切换其他已启用邮箱来源,避免任务长期卡在无邮件状态。
8+
+ [优化] 注册配置全面改为自动保存,离开页面不再丢失邮箱、Checkout、Sub2API 和 OAuth 投递设置;启动任务前会等待最新修改保存完成。
9+
+ [修复] Grok OAuth 同步到 Sub2API 时使用 Grok 平台字段并补齐 CLI OAuth 元数据与 SSO,远程投递错误继续执行敏感字段脱敏。
10+
+ [修复] Pix 方案 3 固定执行 BR 原价校验、VN 优惠注入、BR 税区与零金额 PIX 校验,并始终在原 BR 出口执行 Approve 与轮询。
11+
+ [文档] 补充 iCloud Privacy Mail 对外 API 地址和本机 `8788` 端口配置说明。
12+
313
## 1.0.2 - 2026-07-17
414

515
+ [新增] Grok 4.5 OAuth 账号支持按配置独立投递到 Sub2API 和 CPA;凭据始终先保存到本地,远程失败不影响授权结果。

README.md

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ GPTGrok2API 是一个自托管的 GPT 与 Grok 统一网关,将已接入的订
2828
| 外部系统接入 | 支持从 Sub2API、远程 CPA、本地 CPA 和 Access Token 导入账号;服务器部署提供 `gptgrok2api` Docker 网络别名。 |
2929
| 管理控制台 | 提供概览、GPT/Grok 账号管理、Grok Runtime、iCloud 邮箱、注册任务、Checkout 任务、代理管理、日志、实时监控、图片管理、调试中心和系统设置。 |
3030
| 存储与备份 | 账号数据支持 JSON、SQLite、PostgreSQL 和 Git 后端;图片支持本地与 WebDAV;备份支持本地归档和 R2。 |
31-
| 运维与发布 | 支持 Docker Compose、WARP 编排、内置 sidecar、Nginx HTTPS、运行日志、指标趋势、健康检查和自有版本更新源|
31+
| 运维与发布 | 支持 Docker Compose、WARP 编排、内置 sidecar、Nginx HTTPS、运行日志、指标趋势、健康检查和 GitHub Releases 更新源|
3232

3333
## iCloud Privacy Mail 模块
3434

@@ -252,23 +252,21 @@ curl https://pro.muyuai.top/v1/models \
252252

253253
## 版本更新
254254

255-
控制台从部署的更新服务器读取更新源
255+
控制台统一从 GitHub 读取最新版本和更新日志
256256

257257
```text
258-
https://pro.muyuai.top/updates/VERSION
259-
https://pro.muyuai.top/updates/CHANGELOG.md
258+
https://api.github.com/repos/AuuCoder/gptGrok2api/releases
259+
https://raw.githubusercontent.com/AuuCoder/gptGrok2api/main/CHANGELOG.md
260260
```
261261

262-
GitHub 仅用于打开项目发布页,不作为控制台版本号和更新日志的读取源。发布新版本时需先更新本地 `VERSION``CHANGELOG.md`,再同步到该更新服务器。
263-
264262
工作方式:
265263

266264
1. 当前运行版本由本机 `/version` 返回。
267-
2. 最新版本从自有更新源 `https://pro.muyuai.top/updates/VERSION` 读取。
268-
3. 更新日志从 `https://pro.muyuai.top/updates/CHANGELOG.md` 读取。
269-
4. 控制台比较版本号并显示更新状态。
265+
2. 最新版本从 GitHub Releases API 读取。
266+
3. 更新日志从 GitHub 仓库 `main` 分支的 `CHANGELOG.md` 读取。
267+
4. 后端缓存 GitHub 查询结果,控制台比较版本号并显示更新状态。
270268

271-
发布新版本时需要同步修改根目录 `VERSION``CHANGELOG.md`然后重新构建并部署更新源服务器
269+
发布新版本时只需更新根目录 `VERSION``CHANGELOG.md`推送版本标签并等待 GitHub Actions 完成,不再维护独立更新服务器
272270

273271
## 本地开发
274272

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.0.2
1+
1.0.3

api/system.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from pydantic import BaseModel, ConfigDict, Field
1313

1414
from api.support import require_admin, require_identity, resolve_image_base_url
15+
from app.platform.update_check import get_latest_release_info
1516
from services.account_service import account_service
1617
from services.backup_service import BackupError, backup_service
1718
from services.config import config
@@ -559,6 +560,10 @@ async def auth_status(authorization: str | None = Header(default=None)):
559560
async def get_version():
560561
return {"version": app_version}
561562

563+
@router.get("/meta/update", include_in_schema=False)
564+
async def get_update_meta(force: bool = Query(False)):
565+
return await get_latest_release_info(force=force)
566+
562567
@router.get("/updates/VERSION", response_class=PlainTextResponse)
563568
async def get_update_version():
564569
return PlainTextResponse(

api/system_test.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import unittest
2+
from unittest.mock import AsyncMock, patch
3+
4+
from fastapi import FastAPI
5+
from fastapi.testclient import TestClient
6+
7+
from api import system
8+
9+
10+
class SystemVersionRoutesTest(unittest.TestCase):
11+
def setUp(self):
12+
app = FastAPI()
13+
app.include_router(system.create_router("1.0.3"))
14+
self.client = TestClient(app)
15+
16+
def test_update_meta_uses_github_update_service_and_forwards_force(self):
17+
payload = {
18+
"current_version": "1.0.3",
19+
"latest_version": "1.0.3",
20+
"status": "ok",
21+
"changelog": "# Changelog",
22+
}
23+
update_info = AsyncMock(return_value=payload)
24+
25+
with patch.object(system, "get_latest_release_info", new=update_info):
26+
response = self.client.get("/meta/update?force=true")
27+
28+
self.assertEqual(response.status_code, 200)
29+
self.assertEqual(response.json(), payload)
30+
update_info.assert_awaited_once_with(force=True)
31+
32+
33+
if __name__ == "__main__":
34+
unittest.main()

app/platform/update_check.py

Lines changed: 49 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import asyncio
66
from datetime import datetime, timezone
7+
import json
78
import re
89
import time
910
from typing import Any
@@ -12,7 +13,8 @@
1213

1314
from app.platform.meta import get_project_version
1415

15-
_UPDATE_BASE_URL = "https://pro.muyuai.top/updates"
16+
_GITHUB_RELEASES_API_URL = "https://api.github.com/repos/AuuCoder/gptGrok2api/releases?per_page=20"
17+
_GITHUB_CHANGELOG_URL = "https://raw.githubusercontent.com/AuuCoder/gptGrok2api/main/CHANGELOG.md"
1618
_RELEASE_PAGE_URL = "https://github.com/AuuCoder/gptGrok2api/releases"
1719
_CACHE_TTL_SECONDS = 86400.0
1820
_ERROR_TTL_SECONDS = 300.0
@@ -72,12 +74,12 @@ def _select_latest_release(releases: list[dict[str, Any]]) -> dict[str, Any] | N
7274

7375
def _normalize_error_message(value: str) -> str:
7476
text = str(value or "").strip()
75-
if text.startswith("Update source query failed:"):
76-
status_match = re.search(r"Update source query failed:\s*(\d{3})", text)
77+
if text.startswith("GitHub update query failed:"):
78+
status_match = re.search(r"GitHub update query failed:\s*(\d{3})", text)
7779
if status_match:
78-
return f"Update source query failed ({status_match.group(1)})."
79-
return "Update source query failed."
80-
if text == "Update source returned an empty version":
80+
return f"GitHub update query failed ({status_match.group(1)})."
81+
return "GitHub update query failed."
82+
if text == "GitHub Releases returned no published version":
8183
return text
8284
return text or "Update check failed."
8385

@@ -87,9 +89,10 @@ def _build_payload(release: dict[str, Any] | None = None, error: str = "") -> di
8789
release = release or {}
8890
latest_version = _normalize_version(str(release.get("tag_name") or release.get("name") or ""))
8991
release_name = str(release.get("name") or "").strip()
90-
release_url = str(release.get("html_url") or "").strip()
92+
release_url = str(release.get("html_url") or _RELEASE_PAGE_URL).strip()
9193
published_at = str(release.get("published_at") or "").strip()
9294
release_notes = str(release.get("body") or "").strip()
95+
changelog = str(release.get("changelog") or "").strip()
9396
has_remote = bool(release)
9497
return {
9598
"current_version": current_version,
@@ -98,41 +101,57 @@ def _build_payload(release: dict[str, Any] | None = None, error: str = "") -> di
98101
"release_url": release_url,
99102
"published_at": published_at,
100103
"release_notes": release_notes,
104+
"changelog": changelog,
101105
"update_available": has_remote and bool(latest_version) and _is_newer(latest_version, current_version),
102106
"checked_at": _utc_now_iso(),
103107
"status": "error" if error else "ok",
104108
"error": _normalize_error_message(error),
105109
}
106110

107111

108-
async def _fetch_latest_release() -> dict[str, Any]:
109-
timeout = aiohttp.ClientTimeout(total=10)
112+
async def _fetch_github_text(session: aiohttp.ClientSession, url: str, accept: str) -> str:
110113
headers = {
111-
"Accept": "text/plain",
114+
"Accept": accept,
115+
"X-GitHub-Api-Version": "2022-11-28",
112116
"User-Agent": "gptgrok2api-update-check",
113117
}
118+
async with session.get(url, headers=headers) as response:
119+
content = (await response.text()).strip()
120+
if response.status != 200:
121+
raise RuntimeError(f"GitHub update query failed: {response.status} {content}".strip())
122+
return content
123+
124+
125+
async def _fetch_github_releases(session: aiohttp.ClientSession) -> list[dict[str, Any]]:
126+
content = await _fetch_github_text(
127+
session,
128+
_GITHUB_RELEASES_API_URL,
129+
"application/vnd.github+json",
130+
)
131+
try:
132+
payload = json.loads(content)
133+
except ValueError as exc:
134+
raise RuntimeError("GitHub Releases returned invalid JSON") from exc
135+
if not isinstance(payload, list):
136+
raise RuntimeError("GitHub Releases returned an invalid payload")
137+
return [item for item in payload if isinstance(item, dict)]
138+
139+
140+
async def _fetch_github_changelog(session: aiohttp.ClientSession) -> str:
141+
return await _fetch_github_text(session, _GITHUB_CHANGELOG_URL, "text/plain")
142+
143+
144+
async def _fetch_latest_release() -> dict[str, Any]:
145+
timeout = aiohttp.ClientTimeout(total=10)
114146
async with aiohttp.ClientSession(timeout=timeout) as session:
115-
async def fetch_text(filename: str) -> str:
116-
url = f"{_UPDATE_BASE_URL}/{filename}"
117-
async with session.get(url, headers=headers) as response:
118-
content = (await response.text()).strip()
119-
if response.status != 200:
120-
raise RuntimeError(f"Update source query failed: {response.status} {content}".strip())
121-
return content
122-
123-
version, changelog = await asyncio.gather(
124-
fetch_text("VERSION"),
125-
fetch_text("CHANGELOG.md"),
147+
releases, changelog = await asyncio.gather(
148+
_fetch_github_releases(session),
149+
_fetch_github_changelog(session),
126150
)
127-
if not version:
128-
raise RuntimeError("Update source returned an empty version")
129-
return {
130-
"tag_name": version,
131-
"name": f"v{_normalize_version(version)}",
132-
"html_url": _RELEASE_PAGE_URL,
133-
"published_at": "",
134-
"body": changelog,
135-
}
151+
latest = _select_latest_release(releases)
152+
if latest is None:
153+
raise RuntimeError("GitHub Releases returned no published version")
154+
return {**latest, "changelog": changelog}
136155

137156

138157
async def get_latest_release_info(force: bool = False) -> dict[str, Any]:

app/platform/update_check_test.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import unittest
2+
from unittest.mock import AsyncMock, patch
3+
4+
from app.platform import update_check
5+
6+
7+
class UpdateCheckTest(unittest.IsolatedAsyncioTestCase):
8+
def test_select_latest_release_uses_version_order_and_skips_drafts(self):
9+
releases = [
10+
{"tag_name": "v1.9.9", "draft": False},
11+
{"tag_name": "v2.0.0", "draft": True},
12+
{"tag_name": "v1.10.0", "draft": False},
13+
{"tag_name": "nightly", "draft": False},
14+
]
15+
16+
selected = update_check._select_latest_release(releases)
17+
18+
self.assertIsNotNone(selected)
19+
self.assertEqual(selected["tag_name"], "v1.10.0")
20+
21+
def test_build_payload_exposes_github_changelog(self):
22+
release = {
23+
"tag_name": "v1.0.3",
24+
"name": "v1.0.3",
25+
"html_url": "https://github.com/AuuCoder/gptGrok2api/releases/tag/v1.0.3",
26+
"published_at": "2026-07-18T11:31:23Z",
27+
"body": "release notes",
28+
"changelog": "# Changelog\n\n## 1.0.3",
29+
}
30+
31+
with patch.object(update_check, "get_project_version", return_value="1.0.2"):
32+
payload = update_check._build_payload(release=release)
33+
34+
self.assertEqual(payload["current_version"], "1.0.2")
35+
self.assertEqual(payload["latest_version"], "1.0.3")
36+
self.assertEqual(payload["changelog"], release["changelog"])
37+
self.assertTrue(payload["update_available"])
38+
39+
async def test_fetch_latest_release_combines_github_release_and_changelog(self):
40+
releases = [
41+
{"tag_name": "v1.0.2", "draft": False},
42+
{"tag_name": "v1.0.3", "draft": False, "body": "notes"},
43+
]
44+
with patch.object(
45+
update_check,
46+
"_fetch_github_releases",
47+
new=AsyncMock(return_value=releases),
48+
), patch.object(
49+
update_check,
50+
"_fetch_github_changelog",
51+
new=AsyncMock(return_value="# Changelog"),
52+
):
53+
release = await update_check._fetch_latest_release()
54+
55+
self.assertEqual(release["tag_name"], "v1.0.3")
56+
self.assertEqual(release["changelog"], "# Changelog")
57+
58+
59+
if __name__ == "__main__":
60+
unittest.main()

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "chatgpt2api"
3-
version = "1.0.2"
3+
version = "1.0.3"
44
description = "Add your description here"
55
readme = "README.md"
66
requires-python = ">=3.13"

0 commit comments

Comments
 (0)