waygate-open 1.6.20 → 1.6.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -16
- package/docs/architecture.html +358 -0
- package/docs/features.html +210 -0
- package/docs/img/console-clients.png +0 -0
- package/docs/img/console-keys.png +0 -0
- package/docs/img/console-overview.png +0 -0
- package/docs/img/console-requests.png +0 -0
- package/docs/img/console-settings.png +0 -0
- package/docs/img/console-usage.png +0 -0
- package/docs/img/intro.png +0 -0
- package/package.json +4 -1
- package/server/dist/version.js +1 -1
- package/server/package.json +1 -1
- package/web/dist/assets/{ApiKeysPage-Ds9rZYlg.js → ApiKeysPage-Cn2_8OWb.js} +1 -1
- package/web/dist/assets/{ClientMetricTip-Chd6mrrX.js → ClientMetricTip-CV-tIEGI.js} +1 -1
- package/web/dist/assets/{ClientsPage-Dp2-qFLr.js → ClientsPage-C0puU88g.js} +1 -1
- package/web/dist/assets/{LandingPage-jtP5HQR2.js → LandingPage-CalW5J1W.js} +1 -1
- package/web/dist/assets/{OverviewPage-CYyAbUht.js → OverviewPage-zbCRd73f.js} +1 -1
- package/web/dist/assets/{RequestsPage-OZHb0DDI.js → RequestsPage-BuxiQaR8.js} +1 -1
- package/web/dist/assets/{Select-CU6tc1OR.js → Select-9M93knZG.js} +1 -1
- package/web/dist/assets/{SettingsPage-CJM9qZ6e.js → SettingsPage-Q-ng0fBB.js} +1 -1
- package/web/dist/assets/{UsagePage-D9cm7cc6.js → UsagePage-BNZH_R06.js} +1 -1
- package/web/dist/assets/{index-DO57ipCG.js → index-Dbz5lhfj.js} +2 -2
- package/web/dist/index.html +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
[](https://nestjs.com)
|
|
9
9
|
[](https://github.com/WiseLibs/better-sqlite3)
|
|
10
10
|
[](https://nodejs.org)
|
|
11
|
-
[](https://github.com/zhanghaojie513/waygate-open/releases)
|
|
12
12
|
[](./server)
|
|
13
13
|
|
|
14
14
|

|
|
@@ -32,9 +32,13 @@
|
|
|
32
32
|
| --- | --- |
|
|
33
33
|
|  |  |
|
|
34
34
|
|
|
35
|
-
| 请求日志(逐条留痕,SSE 流也在) |
|
|
36
|
-
| --- |
|
|
37
|
-
|  |
|
|
35
|
+
| 请求日志(逐条留痕,SSE 流也在) | 提供商管理(导入授权 / 健康 / 签到) |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
|  |  |
|
|
38
|
+
|
|
39
|
+
| API Keys(绑定优先级 / 权限范围 / 配额) | 设置(上游 / 限额 / 日志) |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
|  |  |
|
|
38
42
|
|
|
39
43
|
## 功能
|
|
40
44
|
|
|
@@ -45,27 +49,40 @@
|
|
|
45
49
|
- **请求日志**:提供商列按「模型 id → 目录归属」解析(`requests` 无 provider_id,绑定名只代表路由偏好);SSE 流也留痕;**游标(keyset)分页**——翻页期间的新写入不产生重复行;按 `logs.retainDays` 每日自动清理过期日志(启动即补跑停机期间欠的账)
|
|
46
50
|
- **积分与用量**:趋势图(Token / 请求数双口径,**等距时间轴**,空桶补零)、按模型 / 按提供商拆分、缓存命中率、缓存 token 计量(Anthropic 口径:prompt = input + cache_creation + cache_read)
|
|
47
51
|
- **积分账本**:MiniMax `credit/details` 真实余额(精确到 0.01);凭证 refresh token 自动续期 + 每日签到(专用账号后即可代签);WorkBuddy 资源包聚合余额(`get-user-resource` POST 实测口径)
|
|
48
|
-
- **Qoder 签到与积分**(v1.6.0,积分口径 v1.6.1 更正):`campaigns` 签到代领(每日 100 Credits
|
|
52
|
+
- **Qoder 签到与积分**(v1.6.0,积分口径 v1.6.1 更正):`campaigns` 签到代领(每日 100 Credits,进**资源包**)。走 `openapi.qoder.com.cn`,Bearer 令牌鉴权(**无需 COSY 签名**——那套只用于聊天路径),不顶掉桌面登录。**余额口径**:`v2/me/usage` 给真实余额(`userQuota` 套餐内 + `addOnQuota` 资源包),展示为「发放/剩余/已消耗」;该端点**不提供到期时刻**,故「最近过期」显示 `—`。另注意 `credits-summary.totalCredits` 是**历史累计消耗**(等于逐日用量之和),与余额不是一回事,单列展示不混算
|
|
49
53
|
- **WorkBuddy 领取域**(v1.4.0;v1.6.11 起与转发统一):签到代领 + 成长任务 + 连登兑换走**用户会话域**(OAuth,`providers/<id>.oauth`,多会话共存不互踢)——正是这条链路让 `ck_` 得以整体退役。诚实边界:任务多为「体验 XX」型,前置条件不满足就如实记 `skipped`,**绝不伪造行为去凑任务**;补登卡/抽奖等资源消耗型操作不自动化
|
|
50
|
-
- **容错**:Qoder 零内容断流**自动重试一次**(对客户端无感);上游 11128
|
|
54
|
+
- **容错**:Qoder 零内容断流**自动重试一次**(对客户端无感);上游 11128 渠道校验自动降级重试;**11128 渠道熔断**(连续 3 次「降级重试也 11128」即开闸,冷却期内未打上游、未占并发槽、未计费,毫秒级 502 + `retry-after`,阵发过去自动半开放行探测)——上游阵发期每个失败要烧 ~65s,并发槽是全局单计数,不加熔断会烧满槽饿死其他提供商;**只计渠道级信号**(内容级 11128 降级能过的不计入),避免一个客户端的请求特征变成全提供商的闸门
|
|
55
|
+
- **审计不丢行**:流式请求在途期间密钥被删时,落库撞 `key_id` 外键会丢整行 —— 现自动降级 `key_id = NULL` 重试保住整行(tokens / 积分 / 状态 / 耗时照常),`client_id` / `key_hint` 快照仍在,note 标记 `key_deleted_inflight`
|
|
51
56
|
- **控制台**:React 19 单页,明暗双主题(跟随系统/手动,侧栏底部切换;官网与控制台同套主题——棱镜场景浅色下以 `multiply` 显形、深色下以 `screen`),CSS 伪元素 tooltip 体系,同形状骨架屏;页间数据先旧后新(SWR:重进页面即刻渲染上次数据、后台刷新无缝替换,自己的写操作写穿缓存不闪旧态);「提供商健康」只读总览
|
|
52
57
|
- **性能**:路由级代码分割:官网与控制台互不加载对方代码,首屏 gzip <100K;vendor 独立 chunk 享 30d `immutable` 长缓存(发版只重下业务 chunk);概览 5s TTL 缓存(写操作即清)、SQL 语句按文本缓存复用、凭证文件 stat 失效缓存、last_seen 节流落库——同进程转发 SSE,省下的每次查询/写库都是还给流的事件循环时间;Nginx gzip(level 5)压缩传输
|
|
53
58
|
|
|
54
59
|
## 快速开始
|
|
55
60
|
|
|
56
|
-
### 方式一:npm
|
|
61
|
+
### 方式一:npm 包(推荐)
|
|
62
|
+
|
|
63
|
+
**试用 / 一次性运行** —— `npx` 首次会自动下载到缓存并执行,不必先全局安装:
|
|
57
64
|
|
|
58
65
|
```bash
|
|
59
66
|
npx waygate-open
|
|
60
67
|
```
|
|
61
68
|
|
|
62
|
-
|
|
69
|
+
**长期使用** —— 装成全局命令,之后直接敲 `waygate-open`(不再经 npx,启动更快、版本可控):
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm install -g waygate-open
|
|
73
|
+
waygate-open
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
想看固定版本而不是跟着 `latest` 走:`npx waygate-open@1.6.22`。
|
|
77
|
+
|
|
78
|
+
两种方式行为一致:起本地服务并**自动打开浏览器**到控制台(默认 <http://127.0.0.1:8787/console>)。
|
|
63
79
|
首次启动的管理口令默认是 **`waygate`** —— 仅当只监听回环(`127.0.0.1`)时套用这个
|
|
64
80
|
默认值;对外监听时改为随机生成并打印在启动日志里。登录后建议去「设置 → 管理口令」改掉。
|
|
65
81
|
|
|
66
|
-
数据默认落在 `~/.waygate/open
|
|
82
|
+
数据默认落在 `~/.waygate/open/`(库与密钥都在那里)—— 与工作目录无关,
|
|
83
|
+
所以全局安装后在任意目录启动都读同一份数据。
|
|
67
84
|
|
|
68
|
-
|
|
85
|
+
常用参数(都有等价环境变量;全局安装后把 `npx waygate-open` 换成 `waygate-open` 即可):
|
|
69
86
|
|
|
70
87
|
```bash
|
|
71
88
|
npx waygate-open --port 9000 # 换端口(RELAY_PORT)
|
|
@@ -104,9 +121,27 @@ npm run dev:mock # 或:纯前端开发,内置 Mock 数据(口令
|
|
|
104
121
|
| `RELAY_UPSTREAM_KEY` | 可选:无 OAuth 时代的全局出站凭证(**v1.6.11 起 WorkBuddy 走 OAuth,不再需要**) |
|
|
105
122
|
| `RELAY_SECRET` | HMAC:密钥哈希 + 会话令牌签名,丢失等于全部密钥作废 |
|
|
106
123
|
| `RELAY_ADMIN_PASSWORD_HASH` | 管理口令 scrypt 哈希(不设则四级回落,首启打印一次性口令) |
|
|
107
|
-
| `RELAY_DATA_DIR` / `RELAY_PORT` |
|
|
124
|
+
| `RELAY_DATA_DIR` / `RELAY_PORT` | 数据目录与端口(默认 8787)。**数据目录默认值随形态不同**:源码模式 = `./data`(相对启动目录);npm 包(`npx` / 全局)由 bin 传 `~/.waygate/open`,与工作目录无关 |
|
|
125
|
+
|
|
126
|
+
### 第一步:导入客户端凭证
|
|
127
|
+
|
|
128
|
+
三家凭证都来自各客户端的**官方授权**(用户拿不到、也无法手填),入口只有一个:
|
|
129
|
+
控制台「提供商管理 → 导入客户端」(或某一行上的「重新导入授权」用于换账号 / 刷新令牌)。
|
|
130
|
+
|
|
131
|
+
| 客户端 | 授权方式 | 你需要做什么 |
|
|
132
|
+
| --- | --- | --- |
|
|
133
|
+
| **WorkBuddy** | 官方 OAuth 设备授权 | 弹窗里点「打开授权页」→ 在浏览器确认 → 回来自动落盘 |
|
|
134
|
+
| **MiniMax Code** | PKCE 设备码 | 同上(授权页显示设备码时确认即可) |
|
|
135
|
+
| **Qoder CN** | PKCE 设备授权 | 同上(未授权时上游回 404,属正常等待态) |
|
|
136
|
+
|
|
137
|
+
> [!NOTE]
|
|
138
|
+
> 私有状态(`device_code` / `code_verifier`)**只存服务端**,绝不回传浏览器。
|
|
139
|
+
> 授权窗口最多等 2 分钟;超时后可用命令续上(`--help` 看对应命令名)。
|
|
140
|
+
|
|
141
|
+
**同一个客户端出现在两台机器**(例如 relay 跑在服务器、客户端在你本机)时,
|
|
142
|
+
服务端读不到本机登录态 —— 这是正常的,按弹窗提示用「本机推送命令」从你的机器推上去即可。
|
|
108
143
|
|
|
109
|
-
**Qoder CN
|
|
144
|
+
**Qoder CN 凭证**(若要用第三家):也可从本机 Qoder 客户端登录态一键导入,无需手抄令牌:
|
|
110
145
|
|
|
111
146
|
```bash
|
|
112
147
|
cd server && node tools/qoder-import-credential.mjs <provider_id>
|
|
@@ -170,7 +205,7 @@ curl http://127.0.0.1:8787/v1/chat/completions \
|
|
|
170
205
|
| 提供商 | 协议 | 凭证形态 | 目录来源 | 积分口径 |
|
|
171
206
|
| --- | --- | --- | --- | --- |
|
|
172
207
|
| **WorkBuddy** | `openai`(`/v2/chat/completions`) | **官方 OAuth 设备授权**(转发与领取域同一凭证,v1.6.11 起;原 `ck_` 密钥已退役) | 上游拉取 + 逐项实测 | 按次回报 `usage.credit`(实扣)+ 资源包聚合余额 |
|
|
173
|
-
| **MiniMax Code** | `anthropic`(`/mavis/api/v1/llm/v1/messages`) | OAuth refresh token(`mmort_`,自动换新) |
|
|
208
|
+
| **MiniMax Code** | `anthropic`(`/mavis/api/v1/llm/v1/messages`) | OAuth refresh token(`mmort_`,自动换新) | **上游真拉取** `/mavis/api/v1/models`(失败回落内置种子) | **独立账本** `credit/details`(精确到 0.01);单次不回报 → 记 `null` 不编造 |
|
|
174
209
|
| **Qoder CN** | `qoder`(COSY 协议,`/algo/…/agent_chat_generation`) | 客户端登录态身份 JSON(`dt-`/`drt-`) | 网关 `/model/list` 真拉取(13 模型) | SSE `usage.credits` × `billable`(免费档实扣 0);余额走 `openapi` 的 `v2/me/usage`(套餐内 + 资源包,Bearer 无 COSY 签名);到期时刻上游不提供 |
|
|
175
210
|
|
|
176
211
|
**路由规则**:模型 id 先跨全部生效提供商**精确匹配(大小写敏感)**,再按提供商内模糊回落。声明行(seed/upstream/manual)优先于 observed 行——observed 泄漏曾导致幽灵目录行抢走别家请求(已修)。
|
|
@@ -297,7 +332,7 @@ mindmap
|
|
|
297
332
|
|
|
298
333
|
```bash
|
|
299
334
|
cd server
|
|
300
|
-
npm test #
|
|
335
|
+
npm test # 754 项单测(vitest):路由优先级、口径不变式、三种方言翻译、失败落库、趋势桶补齐…
|
|
301
336
|
npm run typecheck # tsc --noEmit
|
|
302
337
|
npm run smoke:boot # 真起 dist:healthz / 登录 / 目录 / 设置四关
|
|
303
338
|
```
|
|
@@ -353,10 +388,11 @@ npm run dev # 联调:代理到 127.0.0.1:8787(推荐)
|
|
|
353
388
|
```
|
|
354
389
|
server/ NestJS + Fastify + better-sqlite3(单文件 SQLite)
|
|
355
390
|
src/proxy/ 转发核心:路由解析、三种方言出口(openai/anthropic/qoder)、失败落库
|
|
391
|
+
channel-breaker.ts 11128 渠道熔断状态机(按提供商独立、内存态、时钟可注入)
|
|
356
392
|
anthropic-upstream/ MiniMax Code 出站(oauth 令牌、账本、SSE 解码、签到)
|
|
357
393
|
qoder-upstream/ Qoder CN 出站(COSY 会话/签名/自定义 base64、SSE 解码、签到与积分账本)
|
|
358
394
|
src/api/ 控制台 API:概览聚合、密钥、目录、提供商、设置(含设置元数据目录)
|
|
359
|
-
src/catalog/ 目录同步(上游拉取 / observed 并入 / 幽灵行防护 / Qoder 拉取)
|
|
395
|
+
src/catalog/ 目录同步(上游拉取 / observed 并入 / 幽灵行防护 / Qoder 与 MiniMax 拉取)
|
|
360
396
|
src/store/ 迁移、仓储与日志留存清理(schema.sql 即迁移事实源)
|
|
361
397
|
src/common/ 跨模块小件(凭证文件的 stat 失效进程内缓存等)
|
|
362
398
|
tools/ 运维与验收脚本(smoke:boot、凭证导入、迁移…)
|
|
@@ -391,4 +427,4 @@ tools/ 仓库级工具(lint 门禁、token 合并、截图
|
|
|
391
427
|
|
|
392
428
|
---
|
|
393
429
|
|
|
394
|
-
版本约定:`1.0.0` 起 API 契约稳定(当前 `1.6.
|
|
430
|
+
版本约定:`1.0.0` 起 API 契约稳定(当前 `1.6.22`,见 `server/src/version.ts`;历史版本见 [Releases](https://github.com/zhanghaojie513/waygate-open/releases))。
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>途关 Waygate · 技术架构</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* ── Theme tokens: every color goes through these ─────────────── */
|
|
9
|
+
:root {
|
|
10
|
+
--bg: #fafafa;
|
|
11
|
+
--surface: #ffffff;
|
|
12
|
+
--fg: #1a1d21;
|
|
13
|
+
--muted: #6b7280;
|
|
14
|
+
--faint: #9aa3af;
|
|
15
|
+
--line: #e2e5ea;
|
|
16
|
+
--accent: #2563eb;
|
|
17
|
+
--accent-soft: rgba(37, 99, 235, .10);
|
|
18
|
+
--shadow: 0 1px 2px rgba(16, 24, 40, .06), 0 4px 12px rgba(16, 24, 40, .05);
|
|
19
|
+
--edge: #9aa3af;
|
|
20
|
+
--edge-hl: #2563eb;
|
|
21
|
+
--band-label: #98a1ad;
|
|
22
|
+
--store: #0a6a87;
|
|
23
|
+
}
|
|
24
|
+
[data-theme="dark"] {
|
|
25
|
+
--bg: #101317;
|
|
26
|
+
--surface: #181c22;
|
|
27
|
+
--fg: #e8ebef;
|
|
28
|
+
--muted: #98a1ad;
|
|
29
|
+
--faint: #5f6a77;
|
|
30
|
+
--line: #2a3038;
|
|
31
|
+
--accent: #60a5fa;
|
|
32
|
+
--accent-soft: rgba(96, 165, 250, .12);
|
|
33
|
+
--shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 4px 14px rgba(0, 0, 0, .35);
|
|
34
|
+
--edge: #4b5563;
|
|
35
|
+
--edge-hl: #60a5fa;
|
|
36
|
+
--band-label: #5f6a77;
|
|
37
|
+
--store: #3fb6de;
|
|
38
|
+
}
|
|
39
|
+
@media (prefers-color-scheme: dark) {
|
|
40
|
+
:root:not([data-theme="light"]) {
|
|
41
|
+
--bg: #101317; --surface: #181c22; --fg: #e8ebef; --muted: #98a1ad; --faint: #5f6a77;
|
|
42
|
+
--line: #2a3038; --accent: #60a5fa; --accent-soft: rgba(96,165,250,.12);
|
|
43
|
+
--shadow: 0 1px 2px rgba(0,0,0,.4), 0 4px 14px rgba(0,0,0,.35);
|
|
44
|
+
--edge: #4b5563; --edge-hl: #60a5fa; --band-label: #5f6a77; --store: #3fb6de;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
* { box-sizing: border-box; }
|
|
49
|
+
body {
|
|
50
|
+
margin: 0; background: var(--bg); color: var(--fg);
|
|
51
|
+
font: 400 15px/1.55 system-ui, -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
52
|
+
-webkit-font-smoothing: antialiased;
|
|
53
|
+
}
|
|
54
|
+
.page { max-width: 1080px; margin: 0 auto; padding: 40px 24px 72px; }
|
|
55
|
+
|
|
56
|
+
header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 8px; }
|
|
57
|
+
h1 { font-size: 26px; font-weight: 650; margin: 0; letter-spacing: -.01em; }
|
|
58
|
+
.chip {
|
|
59
|
+
font: 500 12px/1 ui-monospace, Consolas, monospace;
|
|
60
|
+
color: var(--accent); background: var(--accent-soft);
|
|
61
|
+
padding: 4px 8px; border-radius: 6px;
|
|
62
|
+
}
|
|
63
|
+
.sub { color: var(--muted); margin: 0 0 8px; font-size: 14px; }
|
|
64
|
+
.legend { display: flex; gap: 16px; flex-wrap: wrap; margin: 0 0 28px; color: var(--muted); font-size: 12px; }
|
|
65
|
+
.legend i { display: inline-block; width: 24px; height: 0; border-top: 2px solid var(--edge-hl); vertical-align: middle; margin-right: 6px; }
|
|
66
|
+
.legend i.dash { border-top: 2px dashed var(--edge); }
|
|
67
|
+
.theme-toggle {
|
|
68
|
+
margin-left: auto; border: 1px solid var(--line); background: var(--surface);
|
|
69
|
+
color: var(--muted); border-radius: 8px; padding: 5px 10px; font-size: 12px; cursor: pointer;
|
|
70
|
+
}
|
|
71
|
+
.theme-toggle:hover { color: var(--fg); }
|
|
72
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
73
|
+
|
|
74
|
+
h2.sec {
|
|
75
|
+
font-size: 12px; font-weight: 700; letter-spacing: .1em; color: var(--accent);
|
|
76
|
+
text-transform: uppercase; margin: 0 0 3px;
|
|
77
|
+
}
|
|
78
|
+
p.sec-note { color: var(--faint); font-size: 12px; margin: 0 0 24px; }
|
|
79
|
+
.divider { height: 1px; background: var(--line); margin: 48px 0 40px; }
|
|
80
|
+
|
|
81
|
+
/* ── Bands & nodes(标签在左,与模板一致)────────────────────── */
|
|
82
|
+
#diagramA, #diagramB { position: relative; }
|
|
83
|
+
.band { display: grid; grid-template-columns: 84px 1fr; gap: 16px; align-items: start; margin-bottom: 40px; }
|
|
84
|
+
.band:last-child { margin-bottom: 0; }
|
|
85
|
+
.band-label {
|
|
86
|
+
color: var(--band-label); font-size: 11px; font-weight: 650; letter-spacing: .06em;
|
|
87
|
+
padding-top: 14px; text-align: right; line-height: 1.35;
|
|
88
|
+
}
|
|
89
|
+
.nodes { display: flex; flex-wrap: wrap; gap: 12px; }
|
|
90
|
+
.node {
|
|
91
|
+
flex: 1 1 200px; min-width: 178px;
|
|
92
|
+
background: var(--surface); border: 1px solid var(--line); border-radius: 10px;
|
|
93
|
+
padding: 12px 14px; box-shadow: var(--shadow); cursor: pointer;
|
|
94
|
+
transition: opacity .18s ease, border-color .18s ease;
|
|
95
|
+
}
|
|
96
|
+
.node b { display: block; font-size: 14px; font-weight: 600; }
|
|
97
|
+
.node span { display: block; color: var(--muted); font-size: 12px; margin-top: 2px; line-height: 1.45; }
|
|
98
|
+
.node.hl { border-color: var(--accent); }
|
|
99
|
+
.node.dim { opacity: .35; }
|
|
100
|
+
.node.store b { color: var(--store); }
|
|
101
|
+
|
|
102
|
+
/* ── Connector overlay ─────────────────────────────────────────── */
|
|
103
|
+
#edgesA, #edgesB { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
|
|
104
|
+
#edgesA path, #edgesB path { fill: none; stroke: var(--edge); stroke-width: 1.4; transition: opacity .18s ease, stroke .18s ease; }
|
|
105
|
+
#edgesA path.hl, #edgesB path.hl { stroke: var(--edge-hl); stroke-width: 2; }
|
|
106
|
+
#edgesA path.dim, #edgesB path.dim { opacity: .25; }
|
|
107
|
+
.detail { color: var(--muted); font-size: 12px; margin-top: 6px; min-height: 18px; }
|
|
108
|
+
|
|
109
|
+
@media (max-width: 480px) {
|
|
110
|
+
.band { grid-template-columns: 1fr; }
|
|
111
|
+
.band-label { text-align: left; padding-top: 0; }
|
|
112
|
+
#edgesA, #edgesB { display: none; }
|
|
113
|
+
}
|
|
114
|
+
@media (prefers-reduced-motion: reduce) {
|
|
115
|
+
.node, #edgesA path, #edgesB path { transition: none; }
|
|
116
|
+
}
|
|
117
|
+
</style>
|
|
118
|
+
</head>
|
|
119
|
+
<body>
|
|
120
|
+
<div class="page">
|
|
121
|
+
<header>
|
|
122
|
+
<h1>途关 Waygate · 技术架构</h1>
|
|
123
|
+
<span class="chip">v1.6.28</span>
|
|
124
|
+
<button class="theme-toggle" id="themeToggle" type="button">切换主题</button>
|
|
125
|
+
</header>
|
|
126
|
+
<p class="sub">自托管订阅反代:把按客户端订阅计费的服务,转成 OpenAI / Anthropic 兼容接口。</p>
|
|
127
|
+
<div class="legend">
|
|
128
|
+
<span><i></i>请求主链路</span>
|
|
129
|
+
<span><i class="dash"></i>支撑与数据流</span>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<h2 class="sec">A · 请求链路</h2>
|
|
133
|
+
<p class="sec-note">调用端 → 鉴权 → 翻译 → 反代管线 → 上游三家(按模型 id 路由,可同时生效)。三家凭证均由控制台「官方授权」导入后落盘服务端。</p>
|
|
134
|
+
<div id="diagramA">
|
|
135
|
+
<svg id="edgesA" aria-hidden="true"></svg>
|
|
136
|
+
|
|
137
|
+
<div class="band">
|
|
138
|
+
<div class="band-label">调用端</div>
|
|
139
|
+
<div class="nodes">
|
|
140
|
+
<div class="node" id="a-agent" tabindex="0" role="button" aria-pressed="false"><b>编码 Agent / 工具</b><span>OpenAI 兼容 · sk-waygate-*</span></div>
|
|
141
|
+
<div class="node" id="a-claude" tabindex="0" role="button" aria-pressed="false"><b>Claude 系客户端</b><span>Anthropic /v1/messages</span></div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div class="band">
|
|
146
|
+
<div class="band-label">网关 · 8787</div>
|
|
147
|
+
<div class="nodes">
|
|
148
|
+
<div class="node" id="a-guard" tabindex="0" role="button" aria-pressed="false"><b>KeyGuard 鉴权</b><span>密钥 · IP 策略 · 配额 · 模型范围</span></div>
|
|
149
|
+
<div class="node" id="a-trans" tabindex="0" role="button" aria-pressed="false"><b>协议翻译</b><span>OpenAI ↔ Anthropic ↔ 三种上游方言</span></div>
|
|
150
|
+
<div class="node" id="a-pipe" tabindex="0" role="button" aria-pressed="false"><b>反代管线</b><span>模型路由 · 并发闸 · SSE 透传 · usage 累计</span></div>
|
|
151
|
+
<div class="node" id="a-retry" tabindex="0" role="button" aria-pressed="false"><b>容错</b><span>断流重试 · 11128 渠道降级 · 渠道熔断(快速失败不占槽)</span></div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div class="band">
|
|
156
|
+
<div class="band-label">上游</div>
|
|
157
|
+
<div class="nodes">
|
|
158
|
+
<div class="node" id="a-wb" tabindex="0" role="button" aria-pressed="false"><b>WorkBuddy</b><span>openai 方言 · 官方 OAuth 授权(ck_ 已退役)</span></div>
|
|
159
|
+
<div class="node" id="a-mm" tabindex="0" role="button" aria-pressed="false"><b>MiniMax Code</b><span>anthropic 方言 · OAuth 自动换 token</span></div>
|
|
160
|
+
<div class="node" id="a-qd" tabindex="0" role="button" aria-pressed="false"><b>Qoder CN</b><span>COSY 协议 · 客户端登录态身份</span></div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
<p class="detail" id="detailA" aria-live="polite">悬停或点击任意模块,查看它的直接链路。</p>
|
|
165
|
+
|
|
166
|
+
<div class="divider"></div>
|
|
167
|
+
|
|
168
|
+
<h2 class="sec">B · 控制与数据面</h2>
|
|
169
|
+
<p class="sec-note">控制台经 Admin API 读写;模型目录喂给路由,聚合与账本落在同一个 SQLite 文件。</p>
|
|
170
|
+
<div id="diagramB">
|
|
171
|
+
<svg id="edgesB" aria-hidden="true"></svg>
|
|
172
|
+
|
|
173
|
+
<div class="band">
|
|
174
|
+
<div class="band-label">管理面</div>
|
|
175
|
+
<div class="nodes">
|
|
176
|
+
<div class="node" id="b-web" tabindex="0" role="button" aria-pressed="false"><b>Web 管理控制台</b><span>React 19 + Vite · 明暗双主题 · 6 页</span></div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div class="band">
|
|
181
|
+
<div class="band-label">Admin API</div>
|
|
182
|
+
<div class="nodes">
|
|
183
|
+
<div class="node" id="b-api" tabindex="0" role="button" aria-pressed="false"><b>控制台 API</b><span>概览 · 用量 · 提供商 · 密钥 · 设置</span></div>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<div class="band">
|
|
188
|
+
<div class="band-label">数据服务</div>
|
|
189
|
+
<div class="nodes">
|
|
190
|
+
<div class="node" id="b-catalog" tabindex="0" role="button" aria-pressed="false"><b>模型目录</b><span>三家真拉取(WB / Qoder / MiniMax)· seed 回落 · observed 并入</span></div>
|
|
191
|
+
<div class="node" id="b-agg" tabindex="0" role="button" aria-pressed="false"><b>用量聚合</b><span>requests + aggregates 双写 · 留存清理</span></div>
|
|
192
|
+
<div class="node" id="b-quota" tabindex="0" role="button" aria-pressed="false"><b>额度采集</b><span>MiniMax 账本 · Qoder 余额 · WB 额度真值</span></div>
|
|
193
|
+
<div class="node" id="b-claim" tabindex="0" role="button" aria-pressed="false"><b>签到与领取</b><span>三家代签 · WB 成长任务 / 连登兑换</span></div>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div class="band">
|
|
198
|
+
<div class="band-label">凭证来源<br />v1.6.12+</div>
|
|
199
|
+
<div class="nodes">
|
|
200
|
+
<div class="node" id="b-import" tabindex="0" role="button" aria-pressed="false"><b>官方授权导入</b><span>三家设备授权(WB 业务码 / MiniMax PKCE / Qoder PKCE+404)· 私有状态只存服务端</span></div>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div class="band">
|
|
205
|
+
<div class="band-label">存储</div>
|
|
206
|
+
<div class="nodes">
|
|
207
|
+
<div class="node store" id="b-store" tabindex="0" role="button" aria-pressed="false"><b>SQLite · 单文件</b><span>requests · aggregates · client_keys · model_catalog · providers</span></div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
<p class="detail" id="detailB" aria-live="polite">悬停或点击任意模块,查看它的直接链路。</p>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<script>
|
|
215
|
+
(function () {
|
|
216
|
+
var IDLE = '悬停或点击任意模块,查看它的直接链路。';
|
|
217
|
+
|
|
218
|
+
function makeDiagram(diagramId, svgId, detailId, EDGES) {
|
|
219
|
+
var diagram = document.getElementById(diagramId);
|
|
220
|
+
var svg = document.getElementById(svgId);
|
|
221
|
+
var detail = document.getElementById(detailId);
|
|
222
|
+
var paths = [];
|
|
223
|
+
|
|
224
|
+
function rel(el) {
|
|
225
|
+
var r = el.getBoundingClientRect(), d = diagram.getBoundingClientRect();
|
|
226
|
+
return { x: r.left - d.left, y: r.top - d.top, w: r.width, h: r.height };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* 正交折线:上者底边中点 → 下者顶边中点,中途横移。
|
|
230
|
+
同层(y 区间重叠)走水平直线,避免绕一圈。 */
|
|
231
|
+
function elbow(a, b) {
|
|
232
|
+
var overlapY = Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y);
|
|
233
|
+
if (overlapY > Math.min(a.h, b.h) * 0.6) {
|
|
234
|
+
var acx = a.x + a.w / 2, bcx = b.x + b.w / 2;
|
|
235
|
+
var acy = a.y + a.h / 2;
|
|
236
|
+
var sx = bcx > acx ? a.x + a.w : a.x;
|
|
237
|
+
var ex = bcx > acx ? b.x : b.x + b.w;
|
|
238
|
+
return 'M' + sx + ',' + acy + ' H' + ex;
|
|
239
|
+
}
|
|
240
|
+
if (b.y < a.y) { var t = a; a = b; b = t; }
|
|
241
|
+
var x1 = a.x + a.w / 2, y1 = a.y + a.h, x2 = b.x + b.w / 2, y2 = b.y;
|
|
242
|
+
var my = (y1 + y2) / 2;
|
|
243
|
+
if (Math.abs(x1 - x2) < 2) return 'M' + x1 + ',' + y1 + ' V' + y2;
|
|
244
|
+
return 'M' + x1 + ',' + y1 + ' V' + my + ' H' + x2 + ' V' + y2;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function draw() {
|
|
248
|
+
svg.innerHTML = '';
|
|
249
|
+
paths = [];
|
|
250
|
+
var d = diagram.getBoundingClientRect();
|
|
251
|
+
svg.setAttribute('viewBox', '0 0 ' + d.width + ' ' + d.height);
|
|
252
|
+
EDGES.forEach(function (e) {
|
|
253
|
+
var ea = document.getElementById(e.from), eb = document.getElementById(e.to);
|
|
254
|
+
if (!ea || !eb) return;
|
|
255
|
+
var p = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
256
|
+
p.setAttribute('d', elbow(rel(ea), rel(eb)));
|
|
257
|
+
if (e.dash) p.setAttribute('stroke-dasharray', e.dash);
|
|
258
|
+
p.dataset.from = e.from; p.dataset.to = e.to;
|
|
259
|
+
svg.appendChild(p);
|
|
260
|
+
paths.push(p);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
var pinned = null;
|
|
265
|
+
function label(id) {
|
|
266
|
+
var el = document.getElementById(id);
|
|
267
|
+
return el ? el.querySelector('b').textContent : id;
|
|
268
|
+
}
|
|
269
|
+
function highlight(id) {
|
|
270
|
+
var nodes = diagram.querySelectorAll('.node');
|
|
271
|
+
nodes.forEach(function (n) { n.classList.remove('hl', 'dim'); });
|
|
272
|
+
paths.forEach(function (p) { p.classList.remove('hl', 'dim'); });
|
|
273
|
+
if (!id) return;
|
|
274
|
+
var linked = {};
|
|
275
|
+
paths.forEach(function (p) {
|
|
276
|
+
if (p.dataset.from === id || p.dataset.to === id) {
|
|
277
|
+
p.classList.add('hl'); linked[p.dataset.from] = 1; linked[p.dataset.to] = 1;
|
|
278
|
+
} else { p.classList.add('dim'); }
|
|
279
|
+
});
|
|
280
|
+
nodes.forEach(function (n) {
|
|
281
|
+
if (n.id === id) n.classList.add('hl');
|
|
282
|
+
else if (!linked[n.id]) n.classList.add('dim');
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
function describe(id) {
|
|
286
|
+
var el = document.getElementById(id);
|
|
287
|
+
var inc = EDGES.filter(function (e) { return e.from === id || e.to === id; });
|
|
288
|
+
var dirs = inc.map(function (e) { return e.from === id ? '→ ' + label(e.to) : '← ' + label(e.from); });
|
|
289
|
+
detail.textContent = label(id) + '(' + inc.length + ' 条链路):' + (dirs.join(';') || '无');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
diagram.querySelectorAll('.node').forEach(function (n) {
|
|
293
|
+
n.addEventListener('mouseenter', function () { if (!pinned) highlight(n.id); });
|
|
294
|
+
n.addEventListener('mouseleave', function () {
|
|
295
|
+
if (!pinned) { highlight(null); detail.textContent = IDLE; }
|
|
296
|
+
});
|
|
297
|
+
function toggle() {
|
|
298
|
+
if (pinned === n.id) {
|
|
299
|
+
pinned = null; n.setAttribute('aria-pressed', 'false');
|
|
300
|
+
highlight(null); detail.textContent = IDLE;
|
|
301
|
+
} else {
|
|
302
|
+
if (pinned) {
|
|
303
|
+
var prev = document.getElementById(pinned);
|
|
304
|
+
if (prev) prev.setAttribute('aria-pressed', 'false');
|
|
305
|
+
}
|
|
306
|
+
pinned = n.id; n.setAttribute('aria-pressed', 'true');
|
|
307
|
+
highlight(n.id); describe(n.id);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
n.addEventListener('click', toggle);
|
|
311
|
+
n.addEventListener('keydown', function (ev) {
|
|
312
|
+
if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); toggle(); }
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
draw();
|
|
317
|
+
requestAnimationFrame(draw);
|
|
318
|
+
if (document.fonts && document.fonts.ready) document.fonts.ready.then(draw);
|
|
319
|
+
var rt;
|
|
320
|
+
window.addEventListener('resize', function () { clearTimeout(rt); rt = setTimeout(draw, 120); });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
makeDiagram('diagramA', 'edgesA', 'detailA', [
|
|
324
|
+
{ from: 'a-agent', to: 'a-guard' },
|
|
325
|
+
{ from: 'a-claude', to: 'a-guard' },
|
|
326
|
+
{ from: 'a-guard', to: 'a-trans' },
|
|
327
|
+
{ from: 'a-trans', to: 'a-pipe' },
|
|
328
|
+
{ from: 'a-pipe', to: 'a-retry', dash: '3 4' },
|
|
329
|
+
{ from: 'a-pipe', to: 'a-wb' },
|
|
330
|
+
{ from: 'a-pipe', to: 'a-mm' },
|
|
331
|
+
{ from: 'a-pipe', to: 'a-qd' }
|
|
332
|
+
]);
|
|
333
|
+
|
|
334
|
+
makeDiagram('diagramB', 'edgesB', 'detailB', [
|
|
335
|
+
{ from: 'b-web', to: 'b-api' },
|
|
336
|
+
{ from: 'b-api', to: 'b-catalog' },
|
|
337
|
+
{ from: 'b-api', to: 'b-agg' },
|
|
338
|
+
{ from: 'b-api', to: 'b-quota' },
|
|
339
|
+
{ from: 'b-api', to: 'b-import', dash: '4 4' },
|
|
340
|
+
{ from: 'b-catalog', to: 'b-store', dash: '4 4' },
|
|
341
|
+
{ from: 'b-agg', to: 'b-store', dash: '4 4' },
|
|
342
|
+
{ from: 'b-quota', to: 'b-store', dash: '4 4' },
|
|
343
|
+
{ from: 'b-claim', to: 'b-store', dash: '4 4' },
|
|
344
|
+
{ from: 'b-import', to: 'b-store', dash: '4 4' }
|
|
345
|
+
]);
|
|
346
|
+
|
|
347
|
+
var toggle = document.getElementById('themeToggle');
|
|
348
|
+
toggle.addEventListener('click', function () {
|
|
349
|
+
var root = document.documentElement;
|
|
350
|
+
var dark = root.getAttribute('data-theme') === 'dark';
|
|
351
|
+
root.setAttribute('data-theme', dark ? 'light' : 'dark');
|
|
352
|
+
});
|
|
353
|
+
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
354
|
+
document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
|
|
355
|
+
})();
|
|
356
|
+
</script>
|
|
357
|
+
</body>
|
|
358
|
+
</html>
|