waygate-open 1.6.21 → 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 CHANGED
@@ -8,7 +8,7 @@
8
8
  [![NestJS](https://img.shields.io/badge/NestJS-12-E0234E?style=flat-square)](https://nestjs.com)
9
9
  [![SQLite](https://img.shields.io/badge/SQLite-better--sqlite3-003B57?style=flat-square)](https://github.com/WiseLibs/better-sqlite3)
10
10
  [![Node](https://img.shields.io/badge/Node-%E2%89%A522.9-3C873A?style=flat-square)](https://nodejs.org)
11
- [![version](https://img.shields.io/badge/version-1.6.21-4A38C0?style=flat-square)](https://github.com/zhanghaojie513/waygate-open/releases)
11
+ [![version](https://img.shields.io/badge/version-1.6.22-4A38C0?style=flat-square)](https://github.com/zhanghaojie513/waygate-open/releases)
12
12
  [![tests](https://img.shields.io/badge/tests-754%20passed-0A7247?style=flat-square)](./server)
13
13
 
14
14
  ![途关 · Waygate](docs/img/intro.png)
@@ -32,9 +32,13 @@
32
32
  | --- | --- |
33
33
  | ![概览](docs/img/console-overview.png) | ![积分与用量](docs/img/console-usage.png) |
34
34
 
35
- | 请求日志(逐条留痕,SSE 流也在) |
36
- | --- |
37
- | ![请求日志](docs/img/console-requests.png) |
35
+ | 请求日志(逐条留痕,SSE 流也在) | 提供商管理(导入授权 / 健康 / 签到) |
36
+ | --- | --- |
37
+ | ![请求日志](docs/img/console-requests.png) | ![提供商管理](docs/img/console-clients.png) |
38
+
39
+ | API Keys(绑定优先级 / 权限范围 / 配额) | 设置(上游 / 限额 / 日志) |
40
+ | --- | --- |
41
+ | ![API Keys](docs/img/console-keys.png) | ![设置](docs/img/console-settings.png) |
38
42
 
39
43
  ## 功能
40
44
 
@@ -69,7 +73,7 @@ npm install -g waygate-open
69
73
  waygate-open
70
74
  ```
71
75
 
72
- 想看固定版本而不是跟着 `latest` 走:`npx waygate-open@1.6.21`。
76
+ 想看固定版本而不是跟着 `latest` 走:`npx waygate-open@1.6.22`。
73
77
 
74
78
  两种方式行为一致:起本地服务并**自动打开浏览器**到控制台(默认 <http://127.0.0.1:8787/console>)。
75
79
  首次启动的管理口令默认是 **`waygate`** —— 仅当只监听回环(`127.0.0.1`)时套用这个
@@ -423,4 +427,4 @@ tools/ 仓库级工具(lint 门禁、token 合并、截图
423
427
 
424
428
  ---
425
429
 
426
- 版本约定:`1.0.0` 起 API 契约稳定(当前 `1.6.21`,见 `server/src/version.ts`;历史版本见 [Releases](https://github.com/zhanghaojie513/waygate-open/releases))。
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>
@@ -0,0 +1,210 @@
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 ──────────────────────────────────────────────── */
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
+ /* 分类色:仅用于区分五个域,且每处都配文字标签,不单独承载信息 */
20
+ --c1: #2563eb;
21
+ --c2: #0a6a87;
22
+ --c3: #0a7247;
23
+ --c4: #8a590a;
24
+ --c5: #7a3e9d;
25
+ }
26
+ [data-theme="dark"] {
27
+ --bg: #101317;
28
+ --surface: #181c22;
29
+ --fg: #e8ebef;
30
+ --muted: #98a1ad;
31
+ --faint: #5f6a77;
32
+ --line: #2a3038;
33
+ --accent: #60a5fa;
34
+ --accent-soft: rgba(96, 165, 250, .12);
35
+ --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 4px 14px rgba(0, 0, 0, .35);
36
+ --c1: #60a5fa;
37
+ --c2: #3fb6de;
38
+ --c3: #4fc08d;
39
+ --c4: #e0a93e;
40
+ --c5: #c08fe8;
41
+ }
42
+ @media (prefers-color-scheme: dark) {
43
+ :root:not([data-theme="light"]) {
44
+ --bg: #101317; --surface: #181c22; --fg: #e8ebef; --muted: #98a1ad; --faint: #5f6a77;
45
+ --line: #2a3038; --accent: #60a5fa; --accent-soft: rgba(96,165,250,.12);
46
+ --shadow: 0 1px 2px rgba(0,0,0,.4), 0 4px 14px rgba(0,0,0,.35);
47
+ --c1: #60a5fa; --c2: #3fb6de; --c3: #4fc08d; --c4: #e0a93e; --c5: #c08fe8;
48
+ }
49
+ }
50
+
51
+ * { box-sizing: border-box; }
52
+ body {
53
+ margin: 0; background: var(--bg); color: var(--fg);
54
+ font: 400 15px/1.55 system-ui, -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
55
+ -webkit-font-smoothing: antialiased;
56
+ }
57
+ .page { max-width: 1120px; margin: 0 auto; padding: 40px 24px 72px; }
58
+
59
+ header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 8px; }
60
+ h1 { font-size: 26px; font-weight: 650; margin: 0; letter-spacing: -.01em; }
61
+ .chip {
62
+ font: 500 12px/1 ui-monospace, Consolas, monospace;
63
+ color: var(--accent); background: var(--accent-soft);
64
+ padding: 4px 8px; border-radius: 6px;
65
+ }
66
+ .sub { color: var(--muted); margin: 0 0 28px; font-size: 14px; }
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
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; }
75
+ .card {
76
+ background: var(--surface); border: 1px solid var(--line); border-radius: 12px;
77
+ padding: 18px 20px 16px; box-shadow: var(--shadow);
78
+ transition: box-shadow .2s ease, transform .2s ease, border-color .2s ease;
79
+ }
80
+ .card:hover { transform: translateY(-2px); }
81
+ .card h2 {
82
+ font-size: 15px; font-weight: 650; margin: 0 0 3px;
83
+ display: flex; align-items: center; gap: 9px;
84
+ }
85
+ .card h2 i { width: 9px; height: 9px; border-radius: 3px; flex: none; }
86
+ .c1 h2 i { background: var(--c1); } .c2 h2 i { background: var(--c2); }
87
+ .c3 h2 i { background: var(--c3); } .c4 h2 i { background: var(--c4); }
88
+ .c5 h2 i { background: var(--c5); }
89
+ .card .tag { color: var(--faint); font-size: 11.5px; margin: 0 0 13px; }
90
+ .card ul { list-style: none; margin: 0; padding: 0; }
91
+ .card li {
92
+ position: relative; padding: 6px 0 6px 14px; font-size: 13px; color: var(--muted);
93
+ line-height: 1.5; border-top: 1px solid var(--line);
94
+ }
95
+ .card li:first-child { border-top: none; }
96
+ .card li b { color: var(--fg); font-weight: 600; }
97
+ .card li::before {
98
+ content: ""; position: absolute; left: 0; top: 13px; width: 4px; height: 4px;
99
+ border-radius: 50%; background: var(--faint);
100
+ }
101
+ code {
102
+ font: 500 11.5px/1 ui-monospace, Consolas, monospace;
103
+ background: var(--accent-soft); color: var(--accent);
104
+ padding: 2px 5px; border-radius: 4px;
105
+ }
106
+ .card.feat { grid-column: span 2; }
107
+ .foot { color: var(--faint); font-size: 12px; margin-top: 28px; line-height: 1.6; }
108
+
109
+ @media (max-width: 760px) {
110
+ .grid { grid-template-columns: 1fr; }
111
+ .card.feat { grid-column: span 1; }
112
+ }
113
+ @media (prefers-reduced-motion: reduce) {
114
+ .card { transition: none; }
115
+ .card:hover { transform: 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">五域能力地图:控制台 · 接入 · 多提供商 · 积分与领取 · 运行保障。每一条都对应已实现的代码路径。</p>
127
+
128
+ <div class="grid">
129
+
130
+ <section class="card feat c1">
131
+ <h2><i></i>控制台</h2>
132
+ <p class="tag">React 19 单页 · 6 页 · 明暗双主题(跟随系统 / 手动切换)</p>
133
+ <ul>
134
+ <li><b>概览</b> —— 按提供商归属的用量摘要、积分实扣、渠道告警</li>
135
+ <li><b>积分与用量</b> —— 趋势图(Token / 请求数双口径,等距时间轴补空桶)、按模型 / 提供商拆分、缓存命中率与缓存 token 计量</li>
136
+ <li><b>提供商管理</b> —— 提供商行(额度、健康、签到、官方授权导入)+ 密钥绑定与路由优先级</li>
137
+ <li><b>API 密钥</b> —— 权限范围(全部 / 按族 / 按模型 / 按标签)、IP 上限与白名单、token 配额</li>
138
+ <li><b>请求日志</b> —— 逐条留痕(含 SSE)、首字 / 耗时 / 输出速度、游标分页、按密钥筛选、一键取证 30 分钟、留存清理</li>
139
+ <li><b>设置</b> —— 8 组 26 项,含提供商策略、限额、日志、签到与危险操作</li>
140
+ </ul>
141
+ </section>
142
+
143
+ <section class="card c2">
144
+ <h2><i></i>接入</h2>
145
+ <p class="tag">双方言出口 · 一把密钥走所有客户端</p>
146
+ <ul>
147
+ <li><b>OpenAI 兼容</b> —— <code>/v1/chat/completions</code>,流式 / 非流式</li>
148
+ <li><b>Anthropic 兼容</b> —— <code>/v1/messages</code>,Claude Code 等走 <code>x-api-key</code></li>
149
+ <li><b>密钥形态</b> —— <code>sk-waygate-*</code>(控制台创建,明文仅显示一次)</li>
150
+ <li><b>协议翻译</b> —— OpenAI ↔ Anthropic 与三种上游方言互译</li>
151
+ <li><b>流式保活</b> —— 写头后以 SSE 注释 <code>: ping</code> 心跳,长思考不被网关判空闲</li>
152
+ </ul>
153
+ </section>
154
+
155
+ <section class="card c3">
156
+ <h2><i></i>多提供商</h2>
157
+ <p class="tag">三家可同时生效 · 按模型 id 路由 · 凭证统一「官方授权导入」</p>
158
+ <ul>
159
+ <li><b>WorkBuddy</b> —— openai 方言 · 官方 OAuth 授权(v1.6.13 起 <code>ck_</code> 已退役)</li>
160
+ <li><b>MiniMax Code</b> —— anthropic 方言 · OAuth 自动续期</li>
161
+ <li><b>Qoder CN</b> —— COSY 协议 · 客户端登录态身份</li>
162
+ <li><b>凭证导入</b> —— 选客户端 → 跳官方授权页 → 回来自动落盘;「未授权」信号三家各不相同(业务码 / <code>authorization_pending</code> / 404);私有状态只存服务端</li>
163
+ <li><b>路由规则</b> —— 精确匹配(大小写敏感)→ 提供商内模糊回落;声明行优先于 observed</li>
164
+ </ul>
165
+ </section>
166
+
167
+ <section class="card c4">
168
+ <h2><i></i>积分与领取</h2>
169
+ <p class="tag">不做标定 · 上游说多少就是多少</p>
170
+ <ul>
171
+ <li><b>实扣口径</b> —— 积分 = 上游回报 <code>usage.credit</code>;未回报即 <code>null</code>(界面 <code>—</code>),绝不估算</li>
172
+ <li><b>积分账本</b> —— MiniMax <code>credit/details</code> 真实余额(精确到 0.01);Qoder <code>v2/me/usage</code> 真实余额</li>
173
+ <li><b>签到与领取</b> —— 三家代签(MiniMax 08:05 / Qoder 10:05 / WorkBuddy);WorkBuddy 另有成长任务与连登兑换</li>
174
+ <li><b>额度真值</b> —— 三家额度各取其权威来源;取不到就明说</li>
175
+ <li><b>不变式</b> —— ΣbyModel = ΣbyProvider = creditsUsed,服务端断言后才返回</li>
176
+ </ul>
177
+ </section>
178
+
179
+ <section class="card c5">
180
+ <h2><i></i>运行保障</h2>
181
+ <p class="tag">自托管单机 · 反代保真</p>
182
+ <ul>
183
+ <li><b>并发闸</b> —— 槽位占用实时可观测(不泄漏、不单调上涨)</li>
184
+ <li><b>断流重试</b> —— Qoder 零内容断流自动重试一次(客户端无感)</li>
185
+ <li><b>渠道降级</b> —— 上游 11128 渠道校验自动降级重试</li>
186
+ <li><b>渠道熔断</b> —— 连续 3 次「降级也 11128」即开闸:阵发期未打上游、不占槽、不烧计费,毫秒级 502 + <code>retry-after</code>;只计渠道级信号,避免误伤</li>
187
+ <li><b>留存与备份</b> —— 日志按天清理;SQLite <code>VACUUM INTO</code> 备份、冷热双份可回滚</li>
188
+ </ul>
189
+ </section>
190
+
191
+ </div>
192
+
193
+ <p class="foot">
194
+ 图中每一条都对应已实现的代码路径,无规划项。版本 v1.6.28 ·
195
+ 与 <code>server/src/version.ts</code> 同源。
196
+ </p>
197
+ </div>
198
+
199
+ <script>
200
+ (function () {
201
+ var root = document.documentElement;
202
+ var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
203
+ root.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
204
+ document.getElementById('themeToggle').addEventListener('click', function () {
205
+ root.setAttribute('data-theme', root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
206
+ });
207
+ })();
208
+ </script>
209
+ </body>
210
+ </html>
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygate-open",
3
- "version": "1.6.21",
3
+ "version": "1.6.22",
4
4
  "description": "途关 Waygate · 本地启动 —— 把 WorkBuddy / MiniMax Code / Qoder CN 的客户端订阅转成 OpenAI / Anthropic 兼容 base_url(自带控制台,一条命令起服务并自动打开浏览器)",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -12,6 +12,9 @@
12
12
  "server/package.json",
13
13
  "server/migrations",
14
14
  "web/dist",
15
+ "docs/img",
16
+ "docs/architecture.html",
17
+ "docs/features.html",
15
18
  "LICENSE",
16
19
  "README.md"
17
20
  ],
@@ -12,4 +12,4 @@ exports.VERSION = void 0;
12
12
  * `version` 一致,不一致直接 fail build —— 两处版本永远不会悄悄分叉。
13
13
  * 升版本 = 同时改 package.json 与这里(构建会逼你对齐)。
14
14
  */
15
- exports.VERSION = '1.6.21';
15
+ exports.VERSION = '1.6.22';
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygate-server",
3
- "version": "1.6.21",
3
+ "version": "1.6.22",
4
4
  "private": true,
5
5
  "description": "驿站 · Relay — OpenAI 兼容反向代理与控制台(后端)",
6
6
  "engines": {