Skip to content

Commit 04cdcde

Browse files
committed
Release v1.0.2
1 parent 9ee0737 commit 04cdcde

70 files changed

Lines changed: 2537 additions & 166 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.env.example‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ CHATGPT2API_THREAD_TOKENS=80
1313

1414
# iCloud Privacy Mail 内部服务(启用 Compose 的 local-icloud profile;不会单独暴露端口)
1515
ICLOUD_PRIVACY_MAIL_BASE_URL=http://icloud-privacy-mail:8787
16+
# Apple 登录对共享代理出口较敏感,默认让 Apple/iCloud 官方域名直连。
17+
ICLOUD_PRIVACY_MAIL_NO_PROXY=127.0.0.1,localhost,icloud-privacy-mail,.apple.com,.icloud.com,.apple.com.cn,.icloud.com.cn
1618
# 仅供注册中心的外部自动领取 API 使用;控制台管理不需要单独密钥。
1719
ICLOUD_PRIVACY_MAIL_API_KEY=change-me
1820

@@ -95,6 +97,7 @@ STORAGE_BACKEND=json
9597
# CHATGPT2API_PROXY_RUNTIME_RESOURCE_PROXY_URL=
9698
# 宿主机直接运行 Uvicorn 时,Compose 默认 privoxy 地址会自动映射到此 URL。
9799
# CHATGPT2API_HOST_PRIVOXY_URL=http://127.0.0.1:40080
100+
# CHATGPT2API_HOST_FLARESOLVERR_URL=http://127.0.0.1:8191
98101
# CHATGPT2API_PROXY_RUNTIME_SKIP_SSL_VERIFY=false
99102
# CHATGPT2API_PROXY_RUNTIME_RESET_STATUS_CODES=403
100103

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ git_cache/
3939
*.log
4040
/runtime/
4141
services/checkout_protocol/proxy_state.json
42+
services/checkout_protocol/dumps/
4243
loginwithpassword_*/
4344
logwithemailverifycode_*/
4445
test_batch_output/
@@ -50,4 +51,6 @@ test/
5051
tests/
5152
docs/*.md
5253
scripts/test_*.py
54+
scripts/probe_*.py
55+
scripts/pix_browser_capture.py
5356
/web/

‎CHANGELOG.md‎

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

3+
## 1.0.2 - 2026-07-17
4+
5+
+ [新增] Grok 4.5 OAuth 账号支持按配置独立投递到 Sub2API 和 CPA;凭据始终先保存到本地,远程失败不影响授权结果。
6+
+ [新增] OAuth 账号测试支持“全部账号”和“指定账号”两种模式,逐账号展示成功、失败、耗时和错误原因。
7+
+ [优化] Grok 注册与协议授权日志收敛为关键步骤,并补充 OAuth 投递状态和账号运行状态展示。
8+
+ [文档] README 补充上游项目二开来源声明。
9+
310
## 1.0.1 - 2026-07-15
411

512
+ [新增] Pix 新增方案 3 BR/VN 独立协议,支持 BR Checkout/Provider 与 VN Promotion 双 sticky 出口,并保留方案 1、方案 2 独立选择。

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,3 +320,5 @@ uv run --with pytest --with pytest-asyncio python -m pytest -q
320320
- `GROK2API_LICENSE`
321321

322322
许可证和来源声明必须随分发内容保留。运行时、版本检查、安装地址和容器发布均使用 GPTGrok2API 自有仓库与服务。
323+
324+
本项目基于 [yukkcat/chatgpt2api](https://github.com/yukkcat/chatgpt2api) 二开。

‎VERSION‎

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

‎api/grok_oauth.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ class OAuthAccountDeleteRequest(BaseModel):
4343
ids: list[str] = Field(default_factory=list)
4444

4545

46+
class OAuthAccountTestRequest(BaseModel):
47+
model: str = Field(min_length=1, max_length=128)
48+
prompt: str = Field(min_length=1, max_length=1_200)
49+
50+
4651
async def require_grok_oauth_admin(
4752
authorization: str | None = Header(default=None),
4853
) -> dict[str, object]:
@@ -124,6 +129,14 @@ async def refresh_account(account_id: str) -> dict[str, Any]:
124129
async def sync_models(account_id: str) -> dict[str, Any]:
125130
return await xai_cli_oauth_service.sync_models(account_id)
126131

132+
@router.post("/accounts/{account_id}/test")
133+
async def test_account(account_id: str, body: OAuthAccountTestRequest) -> dict[str, Any]:
134+
return await xai_cli_oauth_service.test_account(
135+
account_id,
136+
model=body.model,
137+
prompt=body.prompt,
138+
)
139+
127140
@router.post("/accounts/status")
128141
async def set_status(body: OAuthAccountStatusRequest) -> dict[str, Any]:
129142
result = xai_cli_oauth_store.set_disabled(body.ids, body.disabled)

‎api/grok_oauth_test.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,29 @@ def test_protocol_job_start_and_status_routes(self) -> None:
6262
self.assertEqual(status.status_code, 200)
6363
self.assertEqual(status.json()["job"]["status"], "running")
6464

65+
def test_account_probe_route_targets_requested_oauth_account(self) -> None:
66+
result = {
67+
"account_id": "oauth-one",
68+
"account": {"id": "oauth-one", "email": "oa***e@example.com"},
69+
"model": "grok-4.5",
70+
"content": "OK",
71+
"elapsed_ms": 120,
72+
}
73+
with patch("api.grok_oauth.require_admin", return_value={"id": "admin", "role": "admin"}), patch.object(
74+
grok_oauth.xai_cli_oauth_service,
75+
"test_account",
76+
new=AsyncMock(return_value=result),
77+
) as test_account:
78+
response = self.client.post(
79+
"/api/grok/oauth/accounts/oauth-one/test",
80+
json={"model": "grok-4.5", "prompt": "只回复 OK"},
81+
headers={"Authorization": "Bearer test-admin-key"},
82+
)
83+
84+
self.assertEqual(response.status_code, 200)
85+
self.assertEqual(response.json()["account_id"], "oauth-one")
86+
test_account.assert_awaited_once_with("oauth-one", model="grok-4.5", prompt="只回复 OK")
87+
6588

6689
if __name__ == "__main__":
6790
unittest.main()

‎api/register_test.py‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,17 @@ def test_grok_account_batch_chat_test_uses_admin_route_and_prompt(self) -> None:
257257
"current": 1,
258258
"current_id": "grok-one",
259259
"cancel_requested": False,
260-
"summary": {"total": 2, "success": 0, "limited": 0, "permission": 0, "failed": 0, "skipped": 1, "pending": 1},
260+
"summary": {
261+
"total": 2,
262+
"success": 0,
263+
"blocked": 0,
264+
"invalid": 0,
265+
"limited": 0,
266+
"permission": 0,
267+
"failed": 0,
268+
"skipped": 1,
269+
"pending": 1,
270+
},
261271
"results": [
262272
{"id": "grok-one", "status": "pending", "error": "", "elapsed_ms": 0},
263273
{"id": "grok-two", "status": "skipped", "error": "账号未保存 SSO 登录态", "elapsed_ms": 0},

‎app/control/account/backends/local.py‎

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,18 @@ def _row_to_token_payload(cls, row: sqlite3.Row) -> dict[str, Any]:
196196
remaining = row[f"{mode}_remaining"]
197197
total = row[f"{mode}_total"]
198198
if always_include or remaining is not None or total is not None:
199-
quota[mode] = {
199+
item = {
200200
"remaining": cls._payload_int(remaining),
201201
"total": cls._payload_int(total),
202202
}
203+
if mode == "console":
204+
reset_at = cls._payload_int(row["console_reset_at"])
205+
source = cls._payload_int(row["console_source"])
206+
if reset_at > 0:
207+
item["reset_at"] = reset_at
208+
if source in (0, 1, 2):
209+
item["source"] = source
210+
quota[mode] = item
203211

204212
return {
205213
"token": row["token"],
@@ -210,6 +218,9 @@ def _row_to_token_payload(cls, row: sqlite3.Row) -> dict[str, Any]:
210218
"fail_count": cls._payload_int(row["usage_fail_count"]),
211219
"last_used_at": row["last_use_at"],
212220
"tags": cls._parse_tags(row["tags"]),
221+
"refresh_status": str(row["refresh_status"] or ""),
222+
"refresh_at": row["refresh_at"],
223+
"refresh_error": str(row["refresh_error"] or "")[:300],
213224
}
214225

215226
def _upsert_sync(
@@ -578,6 +589,20 @@ def _token_payload_select_sql(cls) -> str:
578589
f"THEN json_extract({column}, '$.total') END AS {mode}_total"
579590
for mode, column, _always_include in _TOKEN_PAYLOAD_QUOTAS
580591
)
592+
console_metadata_select = (
593+
"CASE WHEN json_valid(quota_console) "
594+
"THEN json_extract(quota_console, '$.reset_at') END AS console_reset_at, "
595+
"CASE WHEN json_valid(quota_console) "
596+
"THEN json_extract(quota_console, '$.source') END AS console_source"
597+
)
598+
refresh_metadata_select = (
599+
"CASE WHEN json_valid(ext) "
600+
"THEN json_extract(ext, '$.refresh_status') END AS refresh_status, "
601+
"CASE WHEN json_valid(ext) "
602+
"THEN json_extract(ext, '$.refresh_at') END AS refresh_at, "
603+
"CASE WHEN json_valid(ext) "
604+
"THEN json_extract(ext, '$.refresh_error') END AS refresh_error"
605+
)
581606
return f"""
582607
SELECT
583608
token,
@@ -587,7 +612,9 @@ def _token_payload_select_sql(cls) -> str:
587612
usage_use_count,
588613
usage_fail_count,
589614
last_use_at,
590-
{quota_select}
615+
{quota_select},
616+
{console_metadata_select},
617+
{refresh_metadata_select}
591618
FROM {_TBL}
592619
WHERE deleted_at IS NULL
593620
ORDER BY updated_at DESC
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from __future__ import annotations
2+
3+
import tempfile
4+
import unittest
5+
from pathlib import Path
6+
7+
from app.control.account.backends.local import LocalAccountRepository
8+
from app.control.account.commands import AccountPatch, AccountUpsert
9+
from app.control.account.enums import QuotaSource
10+
11+
12+
class LocalAccountRepositoryTokenPayloadTest(unittest.IsolatedAsyncioTestCase):
13+
async def test_console_quota_keeps_source_and_reset_time(self) -> None:
14+
with tempfile.TemporaryDirectory() as temp_dir:
15+
repo = LocalAccountRepository(Path(temp_dir) / "accounts.db")
16+
await repo.initialize()
17+
await repo.upsert_accounts([AccountUpsert(token="test-token", pool="basic")])
18+
await repo.patch_accounts(
19+
[
20+
AccountPatch(
21+
token="test-token",
22+
ext_merge={
23+
"refresh_status": "failed",
24+
"refresh_at": 1_800_000_000_100,
25+
"refresh_error": "上游未返回真实额度数据",
26+
},
27+
quota_console={
28+
"remaining": 0,
29+
"total": 20,
30+
"window_seconds": 3600,
31+
"reset_at": 1_800_000_000_000,
32+
"synced_at": None,
33+
"source": int(QuotaSource.ESTIMATED),
34+
},
35+
)
36+
]
37+
)
38+
39+
payload = (await repo.list_token_payloads())[0]
40+
41+
self.assertEqual(
42+
payload["quota"]["console"],
43+
{
44+
"remaining": 0,
45+
"total": 20,
46+
"reset_at": 1_800_000_000_000,
47+
"source": int(QuotaSource.ESTIMATED),
48+
},
49+
)
50+
self.assertEqual(payload["refresh_status"], "failed")
51+
self.assertEqual(payload["refresh_at"], 1_800_000_000_100)
52+
self.assertEqual(payload["refresh_error"], "上游未返回真实额度数据")
53+
54+
55+
if __name__ == "__main__":
56+
unittest.main()

0 commit comments

Comments
 (0)