Skip to content

Commit fdc207b

Browse files
authored
Merge branch 'TQZHR:main' into main
2 parents 33dec6e + f049758 commit fdc207b

32 files changed

Lines changed: 3291 additions & 505 deletions

README.cloudflare.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22

33
这个仓库已经新增 **Cloudflare Workers / Pages** 可部署版本(TypeScript)。
44

5+
> 一键部署前置条件:若使用 GitHub Actions 工作流,请先在仓库 Secrets 配置 `CLOUDFLARE_API_TOKEN``CLOUDFLARE_ACCOUNT_ID`
6+
> Docker 一键启动入口仍是 `docker compose up -d`,请参考 `readme.md`
7+
58
## 功能概览
69

710
- **D1(SQLite)**:持久化 Tokens / API Keys / 管理员会话 / 配置 / 日志
811
- **KV**:缓存 `/images/*` 的图片/视频资源(从 `assets.grok.com` 代理抓取)
912
- **每天 0 点统一清除**:通过 KV `expiration` + Workers Cron 定时清理元数据(`wrangler.toml` 已配置,默认按北京时间 00:00)
13+
- **前端移动端适配一致生效**:Workers 与 FastAPI/Docker 复用同一套 `/static/*` 资源,包含手机端抽屉导航、表格横向滚动、API Key 居中悬浮新增弹窗等交互
1014

1115
> 原 Python/FastAPI 版本仍保留用于本地/Docker;Cloudflare 部署请按本文件走 Worker 版本。
1216
@@ -139,6 +143,11 @@ python scripts/smoke_test.py --base-url https://<你的域名或workers.dev>
139143
3. `wrangler d1 migrations apply DB --remote --config wrangler.ci.toml`
140144
4. `wrangler deploy`
141145

146+
触发策略保持不变:
147+
- `push``main`:自动触发 Cloudflare 部署作业
148+
- `workflow_dispatch`:可手动选择 `cloudflare/docker/both`
149+
- `v*` tag:用于 Docker 构建发布链路
150+
142151
你需要在 GitHub 仓库里配置 Secrets(Settings → Secrets and variables → Actions):
143152

144153
- `CLOUDFLARE_API_TOKEN`
@@ -189,6 +198,19 @@ python scripts/smoke_test.py --base-url https://<你的域名或workers.dev>
189198
- 注意:KV 单条数据有大小限制(建议 ≤ 25MB),且大多数视频播放器会发起 Range 请求;Range 场景会直接代理上游,不一定会命中 KV 缓存。
190199
- 管理后台 API:`/api/*`(用于管理页)
191200

201+
### 8.1) 管理后台 API 兼容语义(与 FastAPI 一致)
202+
203+
- `GET /api/v1/admin/tokens` 返回项新增(增量兼容):
204+
- `token_type`
205+
- `quota_known`
206+
- `heavy_quota`
207+
- `heavy_quota_known`
208+
- `POST /api/v1/admin/keys/update`
209+
- 当 key 不存在时返回 `404`
210+
- 额度语义:
211+
- `remaining_queries = -1` 表示额度未知(unknown quota semantics)
212+
- 前端应结合 `quota_known` / `heavy_quota_known` 判断,不应将未知额度直接判定为“额度用尽”
213+
192214
---
193215

194216
## 9) 部署到 Pages(可选,但不推荐用于“定时清理”)
@@ -224,3 +246,25 @@ region = "aws:us-east-1"
224246

225247
如需调整:把 `region` 改成你想要的区域(例如 `aws:us-west-2`)。
226248
如需关闭:删除 `wrangler.toml` 中的 `[placement]` 段落即可(恢复默认的边缘就近执行)。
249+
250+
---
251+
252+
## 11) 发布后验证(建议)
253+
254+
部署后可执行以下最小检查:
255+
256+
1. 基础健康与登录页:
257+
- `GET /health`
258+
- `GET /login`
259+
2. 管理页可访问性:
260+
- `GET /admin/token`
261+
- `GET /admin/keys`
262+
3. 移动端回归(建议使用 `390x844`):
263+
- `/admin/keys`:点击“新增 Key”后应为居中悬浮弹窗(有遮罩,可点遮罩关闭,可 `Esc` 关闭)
264+
- 顶部导航:手机端应为抽屉菜单(可打开/关闭,点击菜单项后自动收起)
265+
- Token/Keys/Cache 表格:应保持横向滚动,不应压碎列布局
266+
4. 可选 smoke test:
267+
268+
```bash
269+
python scripts/smoke_test.py --base-url https://<你的域名或workers.dev>
270+
```

app/api/v1/admin.py

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,86 @@ def _normalize_limit(v: Any) -> int:
144144
return -1
145145

146146

147+
def _pool_to_token_type(pool_name: str) -> str:
148+
return "ssoSuper" if str(pool_name or "").strip() == "ssoSuper" else "sso"
149+
150+
151+
def _parse_quota_value(v: Any) -> tuple[int, bool]:
152+
if v is None or v == "":
153+
return -1, False
154+
try:
155+
n = int(v)
156+
except Exception:
157+
return -1, False
158+
if n < 0:
159+
return -1, False
160+
return n, True
161+
162+
163+
def _safe_int(v: Any, default: int = 0) -> int:
164+
try:
165+
return int(v)
166+
except Exception:
167+
return default
168+
169+
170+
def _normalize_token_status(raw_status: Any) -> str:
171+
s = str(raw_status or "active").strip().lower()
172+
if s == "expired":
173+
return "invalid"
174+
if s in ("active", "cooling", "invalid", "disabled"):
175+
return s
176+
return "active"
177+
178+
179+
def _normalize_admin_token_item(pool_name: str, item: Any) -> dict | None:
180+
token_type = _pool_to_token_type(pool_name)
181+
182+
if isinstance(item, str):
183+
token = item.strip()
184+
if not token:
185+
return None
186+
if token.startswith("sso="):
187+
token = token[4:]
188+
return {
189+
"token": token,
190+
"status": "active",
191+
"quota": 0,
192+
"quota_known": False,
193+
"heavy_quota": -1,
194+
"heavy_quota_known": False,
195+
"token_type": token_type,
196+
"note": "",
197+
"fail_count": 0,
198+
"use_count": 0,
199+
}
200+
201+
if not isinstance(item, dict):
202+
return None
203+
204+
token = str(item.get("token") or "").strip()
205+
if not token:
206+
return None
207+
if token.startswith("sso="):
208+
token = token[4:]
209+
210+
quota, quota_known = _parse_quota_value(item.get("quota"))
211+
heavy_quota, heavy_quota_known = _parse_quota_value(item.get("heavy_quota"))
212+
213+
return {
214+
"token": token,
215+
"status": _normalize_token_status(item.get("status")),
216+
"quota": quota if quota_known else 0,
217+
"quota_known": quota_known,
218+
"heavy_quota": heavy_quota,
219+
"heavy_quota_known": heavy_quota_known,
220+
"token_type": token_type,
221+
"note": str(item.get("note") or ""),
222+
"fail_count": _safe_int(item.get("fail_count") or 0, 0),
223+
"use_count": _safe_int(item.get("use_count") or 0, 0),
224+
}
225+
226+
147227
@router.get("/api/v1/admin/keys", dependencies=[Depends(verify_api_key)])
148228
async def list_api_keys():
149229
"""List API keys + daily usage/remaining (for admin UI)."""
@@ -227,6 +307,10 @@ async def update_api_key(data: dict):
227307
if not key:
228308
raise HTTPException(status_code=400, detail="Missing key")
229309

310+
existing = api_key_manager.get_key_row(key)
311+
if not existing:
312+
raise HTTPException(status_code=404, detail="Key not found")
313+
230314
if "name" in data and data.get("name") is not None:
231315
name = str(data.get("name") or "").strip()
232316
if name:
@@ -291,7 +375,17 @@ async def get_tokens_api():
291375
"""获取所有 Token"""
292376
storage = get_storage()
293377
tokens = await storage.load_tokens()
294-
return tokens or {}
378+
data = tokens if isinstance(tokens, dict) else {}
379+
out: dict[str, list[dict]] = {}
380+
for pool_name, raw_items in data.items():
381+
arr = raw_items if isinstance(raw_items, list) else []
382+
normalized: list[dict] = []
383+
for item in arr:
384+
obj = _normalize_admin_token_item(pool_name, item)
385+
if obj:
386+
normalized.append(obj)
387+
out[str(pool_name)] = normalized
388+
return out
295389

296390
@router.post("/api/v1/admin/tokens", dependencies=[Depends(verify_api_key)])
297391
async def update_tokens_api(data: dict):

0 commit comments

Comments
 (0)