waygate-open 1.6.18 → 1.6.20

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,8 +8,8 @@
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.18-4A38C0?style=flat-square)](https://github.com/zhanghaojie513/waygate-open/releases)
12
- [![tests](https://img.shields.io/badge/tests-741%20passed-0A7247?style=flat-square)](./server)
11
+ [![version](https://img.shields.io/badge/version-1.6.20-4A38C0?style=flat-square)](https://github.com/zhanghaojie513/waygate-open/releases)
12
+ [![tests](https://img.shields.io/badge/tests-754%20passed-0A7247?style=flat-square)](./server)
13
13
 
14
14
  ![途关 · Waygate](docs/img/intro.png)
15
15
 
@@ -391,4 +391,4 @@ tools/ 仓库级工具(lint 门禁、token 合并、截图
391
391
 
392
392
  ---
393
393
 
394
- 版本约定:`1.0.0` 起 API 契约稳定(当前 `1.6.18`,见 `server/src/version.ts`;历史版本见 [Releases](https://github.com/zhanghaojie513/waygate-open/releases))。
394
+ 版本约定:`1.0.0` 起 API 契约稳定(当前 `1.6.20`,见 `server/src/version.ts`;历史版本见 [Releases](https://github.com/zhanghaojie513/waygate-open/releases))。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygate-open",
3
- "version": "1.6.18",
3
+ "version": "1.6.20",
4
4
  "description": "途关 Waygate · 本地启动 —— 把 WorkBuddy / MiniMax Code / Qoder CN 的客户端订阅转成 OpenAI / Anthropic 兼容 base_url(自带控制台,一条命令起服务并自动打开浏览器)",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ /**
3
+ * 11128 渠道熔断(v1.6.27,2026-10-17 事故复盘产物)。
4
+ *
5
+ * ## 为什么需要
6
+ * WorkBuddy 上游的渠道校验拒绝(11128)呈**阵发**(10-06 59 次/12h、10-17 8 次/11min),
7
+ * 阵发期每个失败要烧 ~65s(模板尝试 + 降级重试各 ~32s 慢拒绝)。并发槽是**全局单计数**,
8
+ * 不按提供商分 —— 客户端重试会把 4 个槽全部占满 ~1 分钟,MiniMax/Qoder 的正常流量
9
+ * 一起被饿死。熔断后改为立即失败(毫秒级、不占槽),阵发过去自动恢复。
10
+ *
11
+ * ## 只计「渠道级」信号(防误伤的关键)
12
+ * 11128 有两种成因:
13
+ * · 内容级:system 模板指纹被拦(L1 改写漏了新变体)—— **降级重试(中性提示)能过**;
14
+ * · 渠道级:上游阵发全拒 —— **降级重试也是 11128**。
15
+ * 只有第二种计入熔断(调用方以「请求带 sysprompt_degraded_retry note 且最终仍 11128」
16
+ * 判定)。内容级失败计数会把某一个客户端的请求特征变成全提供商的闸门,误伤他人。
17
+ *
18
+ * ## 状态机(每提供商独立,内存态,重启即重学)
19
+ * CLOSED --连续 N 次(默认3)渠道级拒绝--> OPEN(冷却 30s,指数退避 ×2,封顶 300s)
20
+ * OPEN --冷却到期--> 放行下一请求作探测(半开,不单独建态:到期后请求自然放行)
21
+ * 探测成功 --> 全复位;探测仍 11128 --> 再开闸,退避升一档
22
+ * 任何成功(含降级重试成功)或非 11128 的业务失败(如 11115,说明校验链路活着)都复位。
23
+ *
24
+ * 纯逻辑、无 IO,时钟可注入 —— 单测不睡真时间(channel-breaker.spec.ts)。
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.ChannelBreaker = exports.BREAKER_MAX_COOLDOWN_MS = exports.BREAKER_BASE_COOLDOWN_MS = exports.BREAKER_THRESHOLD = void 0;
28
+ /** 连续渠道级拒绝多少次后开闸。 */
29
+ exports.BREAKER_THRESHOLD = 3;
30
+ /** 首次开闸冷却;此后每次再开 ×2。 */
31
+ exports.BREAKER_BASE_COOLDOWN_MS = 30_000;
32
+ /** 冷却封顶(与 quota.backoffMaxSeconds 同量级:长阵发期探测成本收敛到每 5 分钟一次)。 */
33
+ exports.BREAKER_MAX_COOLDOWN_MS = 300_000;
34
+ class ChannelBreaker {
35
+ now;
36
+ states = new Map();
37
+ constructor(now = Date.now) {
38
+ this.now = now;
39
+ }
40
+ /**
41
+ * 该提供商当前是否被熔断。
42
+ * @returns 仍处开闸期 → 剩余毫秒数;否则 null(含「冷却已过、可放行探测」)。
43
+ */
44
+ blocked(providerId) {
45
+ const s = this.states.get(providerId);
46
+ if (!s)
47
+ return null;
48
+ const remain = s.openUntil - this.now();
49
+ return remain > 0 ? remain : null;
50
+ }
51
+ /** 记一次渠道级拒绝(仅「降级重试也 11128」的最终失败才该调这里)。 */
52
+ recordChannelRejection(providerId) {
53
+ const s = this.states.get(providerId) ?? { consec: 0, opens: 0, openUntil: 0 };
54
+ s.consec += 1;
55
+ let opened = false;
56
+ let cooldownMs = 0;
57
+ if (s.consec >= exports.BREAKER_THRESHOLD) {
58
+ s.opens += 1;
59
+ opened = true;
60
+ cooldownMs = Math.min(exports.BREAKER_BASE_COOLDOWN_MS * 2 ** (s.opens - 1), exports.BREAKER_MAX_COOLDOWN_MS);
61
+ s.openUntil = this.now() + cooldownMs;
62
+ }
63
+ this.states.set(providerId, s);
64
+ return { opened, opens: s.opens, cooldownMs };
65
+ }
66
+ /** 记一次「渠道健康」信号:成功返回,或非 11128 的业务失败(校验链路已工作到深层)。全复位。 */
67
+ recordHealthy(providerId) {
68
+ this.states.delete(providerId);
69
+ }
70
+ /** 测试/排障观察用。 */
71
+ snapshot(providerId) {
72
+ const s = this.states.get(providerId);
73
+ return s ? { ...s } : null;
74
+ }
75
+ }
76
+ exports.ChannelBreaker = ChannelBreaker;
@@ -38,6 +38,7 @@ const credential_1 = require("./qoder-upstream/credential");
38
38
  const cosy_1 = require("./qoder-upstream/cosy");
39
39
  const providers_repo_1 = require("../store/repositories/providers.repo");
40
40
  const wb_oauth_service_1 = require("./openai-upstream/wb-oauth.service");
41
+ const channel_breaker_1 = require("./channel-breaker");
41
42
  /** 上游「渠道校验」拒绝业务码(system 提示命中模板指纹,见 probe/05-11128-fix-plan.md)。 */
42
43
  const CHANNEL_REJECT_CODE = 11128;
43
44
  /** L2 降级重试的 note 留痕(契约 §2.1.6 静态表)。 */
@@ -127,6 +128,8 @@ let ProxyService = ProxyService_1 = class ProxyService {
127
128
  /** [v0.6.0] Qoder 凭证与会话缓存。无依赖(只读数据目录),直接实例化 —— 不占 DI 装配,
128
129
  三个手动 new ProxyService 的 wiring spec 也不需要跟着改构造。 */
129
130
  qoderCredentials = new credential_1.QoderCredentialStore();
131
+ /** [v1.6.27] 11128 渠道熔断(内存态,按提供商;重启即重学,见 channel-breaker.ts)。 */
132
+ channelBreaker = new channel_breaker_1.ChannelBreaker();
130
133
  constructor(config, catalog, log, providers, minimaxTokens, wbOauth) {
131
134
  this.config = config;
132
135
  this.catalog = catalog;
@@ -209,12 +212,19 @@ let ProxyService = ProxyService_1 = class ProxyService {
209
212
  message: '没有生效中的提供商 —— 到「提供商管理」启用至少一家。',
210
213
  });
211
214
  }
215
+ /* [2026-10-17] 绑定值改为**双匹配**:新绑定存提供商 ID(唯一,删除重建同名不会错绑——
216
+ 用户报告:删 MiniMax Code 后重建同名,密钥仍绑定旧名 = 名字碰撞)。旧数据存的是
217
+ 名字,按名兜底。两者共用同一条优先链语义(只排序,不过滤)。 */
218
+ const bindById = new Map(actives.map((p) => [p.id, p]));
212
219
  const byName = new Map(actives.map((p) => [p.name, p]));
213
- /* 候选提供商顺序:密钥优先级链(绑定名的启用户)在前,其余启用户按创建序垫后。 */
220
+ const resolveBinding = (v) => bindById.get(v) ?? byName.get(v);
214
221
  const trimmed = input.trim();
215
222
  const ordered = [
216
- ...(keyClients ?? []).map((n) => byName.get(n)).filter((p) => p !== undefined),
217
- ...actives.filter((p) => !(keyClients ?? []).includes(p.name)),
223
+ ...(keyClients ?? []).map((v) => resolveBinding(v)).filter((p) => p !== undefined),
224
+ ...actives.filter((p) => {
225
+ const binds = keyClients ?? [];
226
+ return !binds.includes(p.id) && !binds.includes(p.name);
227
+ }),
218
228
  ];
219
229
  /* ★ 两轮解析(跨全部候选,不是单家内部):
220
230
  第一轮全集合**精确 id**(大小写敏感)—— 多提供商同存时 `MiniMax-M3`
@@ -300,7 +310,7 @@ let ProxyService = ProxyService_1 = class ProxyService {
300
310
  return { provider, id: row.id, displayName: row.display_name };
301
311
  }
302
312
  /**
303
- * [v1.6.11] WorkBuddy 出站头(OAuth 形态)—— ck_ 退役后的转发凭证。
313
+ * [v1.6.13] WorkBuddy 出站头(OAuth 形态)—— ck_ 退役后的转发凭证。
304
314
  *
305
315
  * 与旧 `buildUpstreamHeaders` + ck_ 的差异:令牌来自 `WbOauthService`(按需刷新),
306
316
  * 并在指纹头之外补账号级设备头(x-user-id / x-machine-id / x-session-id,按 uid
@@ -414,9 +424,24 @@ let ProxyService = ProxyService_1 = class ProxyService {
414
424
  if (routed.provider.protocol === 'qoder') {
415
425
  return await this.doCallQoder(ctx, body, rewritten.body, cfg, slot, routed.provider);
416
426
  }
417
- /* [v1.6.11] openai(WorkBuddy)转发改走 **OAuth** —— ck_ 已退役。
427
+ /* [v1.6.13] openai(WorkBuddy)转发改走 **OAuth** —— ck_ 已退役。
418
428
  令牌与账号级设备头由 WbOauthService 维护(与独立版 wb 工具同一形态):
419
429
  access token 按需刷新,machine/session id 按 uid 稳定派生。 */
430
+ /* [v1.6.27] 11128 渠道熔断闸:阵发期(见 channel-breaker.ts 头注)快速失败。
431
+ 放在并发槽语义之后、发请求之前 —— 槽在 callUpstream 已取得,这里立即释放,
432
+ 净占用微秒级;快速失败行不带上游业务码(上游根本没回,守反代保真铁律)。 */
433
+ const breakerRemainMs = this.channelBreaker.blocked(routed.provider.id);
434
+ if (breakerRemainMs !== null) {
435
+ slot.release();
436
+ const retryAfterSec = Math.max(1, Math.ceil(breakerRemainMs / 1000));
437
+ return this.fail(ctx, new errors_1.RelayHttpError({
438
+ httpStatus: 502,
439
+ code: 'upstream_channel_blocked',
440
+ message: `上游渠道校验持续拒绝(11128 阵发),已启用熔断保护:预计约 ${retryAfterSec} 秒后自动放行探测请求,` +
441
+ '无需改动任何配置,请稍后重试。',
442
+ headers: { 'retry-after': String(retryAfterSec) },
443
+ }), 'breaker_open');
444
+ }
420
445
  const wbHeaders = await this.buildWorkBuddyHeaders(ctx, routed.provider, incomingHeaders, cfg, slot);
421
446
  if ('error' in wbHeaders)
422
447
  return wbHeaders.error;
@@ -535,6 +560,24 @@ let ProxyService = ProxyService_1 = class ProxyService {
535
560
  slot.release();
536
561
  const failure = (0, pipeline_1.interpretUpstreamFailure)((0, pipeline_1.parseUpstreamFailure)(res.status, raw));
537
562
  this.applyModelSideEffect(failure.upstreamCode, routed.provider.id, routed.id);
563
+ /* [v1.6.27] 熔断计数只在「渠道级」信号上:降级重试(中性提示)也是 11128 才算 ——
564
+ 只拦下模板改写、降级能过的属于内容级问题,计入会把一个客户端的请求特征
565
+ 变成全提供商的闸门。反之任何非 11128 的业务失败(校验链路已工作到深层,
566
+ 如 11115)与成功一样视为渠道健康,全复位。 */
567
+ if (failure.upstreamCode === CHANNEL_REJECT_CODE) {
568
+ if (ctx.notes.includes(SYSPROMPT_DEGRADED_NOTE)) {
569
+ const t = this.channelBreaker.recordChannelRejection(routed.provider.id);
570
+ if (t.opened) {
571
+ this.logger.warn(`11128 渠道熔断开闸:provider=${routed.provider.name} 连续${channel_breaker_1.BREAKER_THRESHOLD}次渠道级拒绝,` +
572
+ `冷却 ${Math.round(t.cooldownMs / 1000)}s(第 ${t.opens} 次开闸)`);
573
+ }
574
+ }
575
+ // 未带降级标记的 11128(rewrite=off 或降级前的首败):不计入也不复位 ——
576
+ // 它区分不了内容级/渠道级,对熔断状态没有信息量。
577
+ }
578
+ else if (failure.upstreamCode !== null) {
579
+ this.channelBreaker.recordHealthy(routed.provider.id);
580
+ }
538
581
  return this.fail(ctx, failure, failure.code);
539
582
  }
540
583
  }
@@ -547,6 +590,8 @@ let ProxyService = ProxyService_1 = class ProxyService {
547
590
  message: '上游返回 200 但响应体为空,无法读取流。',
548
591
  }), 'upstream empty body');
549
592
  }
593
+ /* 上游接受了本次请求(含降级重试后被接受)= 渠道健康,熔断状态全复位。 */
594
+ this.channelBreaker.recordHealthy(routed.provider.id);
550
595
  return {
551
596
  traceId: ctx.traceId,
552
597
  status: 200,
@@ -12,13 +12,22 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
12
  return function (target, key) { decorator(target, key, paramIndex); }
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.RequestsRepository = void 0;
15
+ exports.RequestsRepository = exports.ORPHANED_KEY_NOTE = void 0;
16
16
  const common_1 = require("@nestjs/common");
17
17
  const db_1 = require("../db");
18
18
  const cached_stmt_1 = require("../cached-stmt");
19
19
  const key_usage_cache_1 = require("../key-usage-cache");
20
20
  const aggregates_repo_1 = require("./aggregates.repo");
21
21
  const SORT_WHITELIST = new Set(['ts', 'ms', 'credits', 'status', 'prompt_tokens', 'completion_tokens']);
22
+ /** [v1.6.28] 落库时发现密钥已被删除(在途请求晚于删除)后追加的标记,见 writeLog。 */
23
+ exports.ORPHANED_KEY_NOTE = 'key_deleted_inflight';
24
+ /** better-sqlite3 的 FK 违例:code 是 SQLITE_CONSTRAINT_FOREIGNKEY(787),message 含 FOREIGN KEY。 */
25
+ function isForeignKeyFailure(err) {
26
+ const e = err;
27
+ if (!e)
28
+ return false;
29
+ return e.code === 'SQLITE_CONSTRAINT_FOREIGNKEY' || /FOREIGN KEY constraint failed/i.test(String(e.message ?? ''));
30
+ }
22
31
  let RequestsRepository = class RequestsRepository {
23
32
  db;
24
33
  aggregates;
@@ -57,19 +66,45 @@ let RequestsRepository = class RequestsRepository {
57
66
  }
58
67
  /** 一次请求 = 一行。与 aggregates 的递增在同一事务内(api-contract §6)。 */
59
68
  log(row) {
60
- const id = this.logTx(row);
61
- // 事务已提交才更新用量缓存(事务内更新的话,回滚会让缓存多计)
62
- this.usage.onInsert(row.keyId, row.totalTokens, row.remoteIp);
69
+ const { id, effectiveKeyId } = this.logTx(row);
70
+ // 事务已提交才更新用量缓存(事务内更新的话,回滚会让缓存多计)。
71
+ // 密钥已在途删除时 effectiveKeyId 为 null:那份计数已无归属,不再喂给缓存。
72
+ this.usage.onInsert(effectiveKeyId, row.totalTokens, row.remoteIp);
63
73
  return id;
64
74
  }
65
75
  /** 事务体:插入请求行 + 5 个聚合桶递增(由构造期建好的 logTx 包在同一事务里执行)。 */
66
76
  writeLog(row) {
67
- const info = this.insertStmt.run({
68
- ...row,
69
- reasoningTokens: row.reasoningTokens ?? 0,
70
- stream: row.stream ? 1 : 0,
71
- createdAt: Date.now(),
72
- });
77
+ let info;
78
+ let effectiveKeyId = row.keyId;
79
+ try {
80
+ info = this.insertStmt.run({
81
+ ...row,
82
+ reasoningTokens: row.reasoningTokens ?? 0,
83
+ stream: row.stream ? 1 : 0,
84
+ createdAt: Date.now(),
85
+ });
86
+ }
87
+ catch (err) {
88
+ /* [v1.6.28] 密钥在请求**在途期间**被删:`key_id` 的外键是 ON DELETE SET NULL,
89
+ 但那只在删除时清理**已有**子行;本请求晚于删除落库,INSERT 指向已消失的父行
90
+ ⇒ FOREIGN KEY constraint failed,整行审计(tokens/积分/状态/耗时)被丢掉。
91
+ (生产实证:2026-10-17 探针在流未结束时删临时密钥。)
92
+ 处置:把 key_id 降级为 NULL 重试一次,保住整行。client_id / key_hint 是文本
93
+ 快照、不受外键约束,仍能说明「这条是谁发的」—— 不改成 unidentified,那样会
94
+ 把已识别的流量错记成未识别,污染客户端排行。行上追加标记便于事后辨认。 */
95
+ if (!isForeignKeyFailure(err) || row.keyId === null)
96
+ throw err;
97
+ effectiveKeyId = null;
98
+ info = this.insertStmt.run({
99
+ ...row,
100
+ keyId: null,
101
+ keyHint: row.keyHint ?? null,
102
+ note: [row.note, exports.ORPHANED_KEY_NOTE].filter(Boolean).join(','),
103
+ reasoningTokens: row.reasoningTokens ?? 0,
104
+ stream: row.stream ? 1 : 0,
105
+ createdAt: Date.now(),
106
+ });
107
+ }
73
108
  const errors = row.status >= 400 ? 1 : 0;
74
109
  const delta = {
75
110
  requests: 1,
@@ -90,7 +125,7 @@ let RequestsRepository = class RequestsRepository {
90
125
  this.aggregates.upsert(hour, 'status', String(row.status), delta);
91
126
  this.aggregates.upsert(hour, 'total', '-', delta);
92
127
  this.aggregates.upsert(day, 'total', '-', delta);
93
- return Number(info.lastInsertRowid);
128
+ return { id: Number(info.lastInsertRowid), effectiveKeyId };
94
129
  }
95
130
  buildWhere(q) {
96
131
  const clauses = [];
@@ -12,4 +12,4 @@ exports.VERSION = void 0;
12
12
  * `version` 一致,不一致直接 fail build —— 两处版本永远不会悄悄分叉。
13
13
  * 升版本 = 同时改 package.json 与这里(构建会逼你对齐)。
14
14
  */
15
- exports.VERSION = '1.6.18';
15
+ exports.VERSION = '1.6.20';
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygate-server",
3
- "version": "1.6.18",
3
+ "version": "1.6.20",
4
4
  "private": true,
5
5
  "description": "驿站 · Relay — OpenAI 兼容反向代理与控制台(后端)",
6
6
  "engines": {
@@ -0,0 +1,5 @@
1
+ import{D as e,J as t,K as n,S as r,_ as i,a,g as o,h as s,it as c,n as l,t as u,tt as d,z as f}from"./vendor-befEYGHM.js";import{A as p,D as m,M as h,a as g,c as _,g as v,j as y,m as b,r as x,v as S,y as C}from"./index-DO57ipCG.js";import{a as w,c as T,l as E,n as D,o as O,r as k,s as A,t as j}from"./Select-CU6tc1OR.js";import{t as M}from"./Modal-B-rIrvXS.js";import{n as N}from"./PageSkeleton-DG-Dj4_9.js";var P=c(),F=d(),I={live:`live`,off:`off`,exp:`exp`};function L({rows:e,clientByKey:t,providerNameById:n,pendingId:r,onToggle:i,onEdit:c,onDelete:u}){return(0,F.jsx)(`div`,{className:`tbl-scroll`,children:(0,F.jsxs)(`table`,{className:`tbl`,children:[(0,F.jsxs)(`caption`,{className:`sr-only`,children:[`API Key 列表,共 `,e.length,` 把密钥`]}),(0,F.jsx)(`thead`,{children:(0,F.jsxs)(`tr`,{children:[(0,F.jsx)(`th`,{scope:`col`,children:`名称 / 密钥`}),(0,F.jsx)(`th`,{scope:`col`,children:`绑定提供商`}),(0,F.jsx)(`th`,{scope:`col`,children:`权限范围`}),(0,F.jsx)(`th`,{scope:`col`,className:`r`,children:`近24h请求`}),(0,F.jsx)(`th`,{scope:`col`,className:`r`,children:`最后使用`}),(0,F.jsx)(`th`,{scope:`col`,children:`状态 / 到期`}),(0,F.jsx)(`th`,{scope:`col`,className:`r`,style:{width:184},children:`操作`})]})}),(0,F.jsx)(`tbody`,{children:e.map(e=>{let d=p(e.enabled,e.expiresAt),f=r===e.id,h=t?.get(e.id),_=e.clients??[];return(0,F.jsxs)(`tr`,{children:[(0,F.jsxs)(`td`,{children:[(0,F.jsx)(`div`,{className:`td-strong`,children:e.name}),(0,F.jsx)(`div`,{className:`td-sub td-mono`,children:e.hint})]}),(0,F.jsx)(`td`,{children:_.length>0?(0,F.jsx)(g,{content:`绑定提供商(路由优先级从高到低):\n${_.map(e=>n?.get(e)??e).join(`
2
+ `)}`,children:(0,F.jsx)(`span`,{className:`key-bindings`,children:(()=>{let e=_[0],t=n?.get(e)??e,r=h?.name===t?h:void 0;return(0,F.jsxs)(F.Fragment,{children:[r?(0,F.jsx)(`a`,{href:`#/clients?clientId=${encodeURIComponent(r.clientId)}`,children:t}):t,_.length>1?(0,F.jsx)(`i`,{className:`kb-rank`,children:`优先`}):null]})})()})}):h?(0,F.jsx)(`a`,{href:`#/clients?clientId=${encodeURIComponent(h.clientId)}`,children:h.name}):(0,F.jsx)(`span`,{className:`td-sub`,children:`未绑定`})}),(0,F.jsx)(`td`,{children:C(e.scope)}),(0,F.jsx)(`td`,{className:`r td-mono`,children:v(e.requests24h)}),(0,F.jsx)(`td`,{className:`r td-mono`,children:S(e.lastSeenAt)}),(0,F.jsxs)(`td`,{children:[(0,F.jsxs)(`span`,{style:{display:`inline-flex`,alignItems:`center`,gap:8,whiteSpace:`nowrap`},children:[(0,F.jsx)(E,{tone:I[d],children:m[d]}),(0,F.jsx)(`span`,{className:`td-sub td-mono`,children:e.expiresAt?`至 ${b(e.expiresAt)}`:`永不过期`})]}),e.maxIps!==null||e.tokenQuota!==null?(0,F.jsxs)(`div`,{className:`td-sub td-mono`,style:{marginTop:2},children:[e.maxIps===null?null:`IP ${e.usedIps??0}/${e.maxIps}`,e.maxIps!==null&&e.tokenQuota!==null?` · `:null,e.tokenQuota===null?null:`${v(e.usedTokens??0)}/${v(e.tokenQuota)} tok`,e.ipWhitelist&&e.ipWhitelist.length>0?` · 白名单 ${e.ipWhitelist.length} 条`:null]}):null]}),(0,F.jsx)(`td`,{className:`r`,children:(0,F.jsxs)(`span`,{style:{display:`inline-flex`,gap:8,justifyContent:`flex-start`},children:[(0,F.jsx)(g,{content:`改名 / 改绑定 / 访问控制(不动权限范围,密钥与哈希不受影响)`,children:f?(0,F.jsx)(`span`,{tabIndex:0,children:(0,F.jsxs)(`button`,{type:`button`,className:`mini-pill`,disabled:f,onClick:()=>c(e),children:[(0,F.jsx)(l,{size:12}),`修改`]})}):(0,F.jsxs)(`button`,{type:`button`,className:`mini-pill`,onClick:()=>c(e),children:[(0,F.jsx)(l,{size:12}),`修改`]})}),(0,F.jsx)(g,{content:d===`exp`?`已过期,需重新创建`:e.enabled?`停用后立即失效`:`重新启用`,children:f||d===`exp`?(0,F.jsx)(`span`,{tabIndex:0,children:(0,F.jsxs)(`button`,{type:`button`,className:`mini-pill`,disabled:!0,onClick:()=>i(e),children:[e.enabled?(0,F.jsx)(o,{size:12}):(0,F.jsx)(s,{size:12}),e.enabled?`停用`:`启用`]})}):(0,F.jsxs)(`button`,{type:`button`,className:`mini-pill`,onClick:()=>i(e),children:[e.enabled?(0,F.jsx)(o,{size:12}):(0,F.jsx)(s,{size:12}),e.enabled?`停用`:`启用`]})}),(0,F.jsxs)(`button`,{type:`button`,className:`mini-pill danger`,disabled:f,onClick:()=>u(e),children:[(0,F.jsx)(a,{size:12}),`删除`]})]})})]},e.id)})})]})})}var R={ipText:``,maxIps:``,tokenQuota:``};function z(e){return{ipText:e.ipWhitelist.join(`
3
+ `),maxIps:e.maxIps===null?``:String(e.maxIps),tokenQuota:e.tokenQuota===null?``:String(e.tokenQuota)}}function B(e){return e.ipText.trim()!==``||e.maxIps.trim()!==``||e.tokenQuota.trim()!==``}function V(e){let t=e.trim();if(!t)return null;let n=t.indexOf(`/`);if(n>0){let e=t.slice(0,n),r=t.slice(n+1);return!/^\d{1,2}$/.test(r)||Number(r)>32?`「${t}」的掩码位数只能是 0-32`:H(e)?null:`「${t}」CIDR 只支持 IPv4 网段`}return H(t)||t.includes(`:`)&&/^[0-9a-fA-F:]+$/.test(t)?null:`「${t}」不是合法的 IP 地址(支持 IPv4、IPv6、IPv4 CIDR)`}function H(e){let t=e.split(`.`);return t.length===4&&t.every(e=>/^\d{1,3}$/.test(e)&&Number(e)<=255)}function U(e){let t=e.ipText.split(/[\n,]+/).map(e=>e.trim()).filter(e=>e!==``);for(let e of t){let t=V(e);if(t)return{error:`IP 白名单:${t}`}}let n=null;if(e.maxIps.trim()!==``&&(n=Number(e.maxIps),!Number.isInteger(n)||n<1))return{error:`最大 IP 数必须是正整数(留空 = 不限)。`};let r=null;return e.tokenQuota.trim()!==``&&(r=Number(e.tokenQuota),!Number.isInteger(r)||r<1)?{error:`Token 配额必须是正整数(留空 = 不限)。`}:{ipWhitelist:t,maxIps:n,tokenQuota:r}}function W(e){let t=[],n=e.ipText.split(/[\n,]+/).map(e=>e.trim()).filter(e=>e!==``).length;return n>0&&t.push(`白名单 ${n} 条`),e.maxIps.trim()!==``&&t.push(`最大 ${e.maxIps.trim()} IP`),e.tokenQuota.trim()!==``&&t.push(`配额 ${Number(e.tokenQuota).toLocaleString()} tok`),t.length>0?t.join(` · `):`未限制`}function G({draft:e,onChange:t,defaultOpen:r=!1}){let[i,a]=(0,P.useState)(r||B(e));return(0,F.jsxs)(`div`,{className:`ov-field`,children:[(0,F.jsxs)(`button`,{type:`button`,className:`policy-toggle`,"aria-expanded":i,onClick:()=>a(e=>!e),children:[(0,F.jsx)(n,{size:14,"aria-hidden":!0}),(0,F.jsxs)(`span`,{children:[`访问控制`,i?``:`(可选,留空 = 不限)`]}),i?null:(0,F.jsx)(`span`,{className:`policy-sum`,children:W(e)})]}),i?(0,F.jsxs)(F.Fragment,{children:[(0,F.jsx)(`textarea`,{className:`field-input text`,style:{width:`100%`,minHeight:48,resize:`vertical`,fontFamily:`var(--font-mono)`,marginTop:8},value:e.ipText,placeholder:`IP 白名单,每行一个:
4
+ 203.0.113.7
5
+ 10.0.0.0/8`,onChange:n=>t({...e,ipText:n.target.value})}),(0,F.jsxs)(`div`,{style:{display:`flex`,gap:12,marginTop:8},children:[(0,F.jsxs)(`label`,{style:{flex:1,display:`flex`,flexDirection:`column`,gap:4},children:[(0,F.jsx)(`span`,{className:`ov-hint`,children:`最大 IP 数(留空 = 不限)`}),(0,F.jsx)(`input`,{className:`field-input text`,style:{width:`100%`},type:`number`,min:1,value:e.maxIps,placeholder:`如 3`,onChange:n=>t({...e,maxIps:n.target.value})})]}),(0,F.jsxs)(`label`,{style:{flex:1,display:`flex`,flexDirection:`column`,gap:4},children:[(0,F.jsx)(`span`,{className:`ov-hint`,children:`Token 配额(留空 = 不限)`}),(0,F.jsx)(`input`,{className:`field-input text`,style:{width:`100%`},type:`number`,min:1,value:e.tokenQuota,placeholder:`如 1000000`,onChange:n=>t({...e,tokenQuota:n.target.value})})]})]}),(0,F.jsxs)(`div`,{className:`ov-hint`,style:{marginTop:6},children:[`白名单只放行名单内来源;最大 IP 数挡密钥转播后的`,(0,F.jsx)(`b`,{children:`新增`}),`来源;配额按累计 tokens 软校验(最后一笔可超出)。`]})]}):null]})}var K=[{value:`all`,label:`全部模型`},{value:`family`,label:`仅某系`},{value:`tag`,label:`仅某标签`},{value:`models`,label:`指定模型`}],q=[`kimi`,`deepseek`,`glm`,`minimax`,`hy4`,`hy3`,`space`,`step`],J=[`限时免费`,`限时折扣`,`夜间折扣`,`订阅优先`];function Y({onClose:e,onCreated:t}){let[n,i]=(0,P.useState)(``),[a,o]=(0,P.useState)(`all`),[s,c]=(0,P.useState)(q[0]),[l,u]=(0,P.useState)(J[0]),[d,f]=(0,P.useState)([]),p=y(()=>_.catalog(),[],{cacheKey:`catalog`}),m=y(()=>_.providers(),[],{cacheKey:`providers`}),[g,v]=(0,P.useState)([]),[b,S]=(0,P.useState)(R),[C,w]=(0,P.useState)(!1),[T,E]=(0,P.useState)(null),[D,O]=(0,P.useState)(null),A=(p.data?.models??[]).filter(e=>e.enabled),N=e=>f(t=>t.includes(e)?t.filter(t=>t!==e):[...t,e]),I=a===`all`?{type:`all`}:a===`family`?{type:`family`,value:s}:a===`tag`?{type:`tag`,value:l}:{type:`models`,value:d},L=a===`models`&&d.length===0,z=(m.data?.items??[]).map(e=>({value:e.id,label:e.active?`${e.name}(已启用)`:e.name})).sort((e,t)=>e.label.localeCompare(t.label,`zh-Hans-CN`)),B=e=>v(t=>t.includes(e)?t.filter(t=>t!==e):[...t,e]),V=(e,t)=>v(n=>{let r=[...n],i=e+t;return i<0||i>=r.length?n:([r[e],r[i]]=[r[i],r[e]],r)}),H=e=>z.find(t=>t.value===e)?.label??e,W=!!n.trim()&&!C&&!L;return D?(0,F.jsxs)(M,{title:`密钥已创建`,onClose:e,footer:(0,F.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:e,children:`我已保存,关闭`}),children:[(0,F.jsxs)(`div`,{className:`ov-lose`,children:[(0,F.jsx)(`b`,{children:`会失去什么:`}),D.warning]}),(0,F.jsx)(X,{value:D.key}),(0,F.jsxs)(`div`,{className:`ov-hint`,children:[`服务端只保存哈希。这串明文不会再出现,丢失只能删除重建。 下游可用 `,(0,F.jsx)(`span`,{className:`code-chip`,children:`GET /v1/usage`}),`(带这把密钥)自查配额余量与有效期 —— 纯只读,不产生调用。`]})]}):(0,F.jsxs)(M,{title:`新建密钥`,onClose:e,footer:(0,F.jsxs)(F.Fragment,{children:[(0,F.jsx)(`button`,{type:`button`,className:`btn btn-quiet btn-sm`,onClick:e,children:`取消`}),(0,F.jsxs)(`button`,{type:`submit`,form:`create-key-form`,className:`btn btn-primary btn-sm`,disabled:!W,children:[C?(0,F.jsx)(r,{size:14}):null,C?`创建中…`:`创建密钥`]})]}),children:[(0,F.jsx)(`p`,{className:`ov-sub`,children:`给一个提供商或用途起名。绑定提供商名后,识别规则第 2 条「密钥归属」就能直接命中。`}),(0,F.jsxs)(`form`,{id:`create-key-form`,onSubmit:async e=>{if(e.preventDefault(),!W)return;let r=U(b);if(`error`in r){E(new h(r.error,400,0));return}w(!0),E(null);try{let e=await _.createKey({name:n.trim(),scope:I,clients:g,client:g[0]??null,ipWhitelist:r.ipWhitelist,maxIps:r.maxIps,tokenQuota:r.tokenQuota});O(e),t()}catch(e){E(e instanceof h?e:new h(String(e),0,0))}finally{w(!1)}},style:{display:`flex`,flexDirection:`column`,gap:16},children:[(0,F.jsxs)(`div`,{className:`ov-field`,children:[(0,F.jsx)(`label`,{htmlFor:`ck-name`,children:`密钥名称`}),(0,F.jsx)(`input`,{id:`ck-name`,className:`field-input text`,style:{width:`100%`},value:n,autoFocus:!0,placeholder:`例如:Cursor · 笔记本`,onChange:e=>i(e.target.value)})]}),(0,F.jsxs)(`div`,{className:`ov-field`,children:[(0,F.jsx)(`label`,{children:`权限范围`}),(0,F.jsx)(j,{value:a,options:K,onChange:o,width:180}),a===`family`?(0,F.jsx)(j,{value:s,options:q.map(e=>({value:e,label:`${e} 系`})),onChange:c,width:180}):null,a===`tag`?(0,F.jsx)(j,{value:l,options:J.map(e=>({value:e,label:e})),onChange:u,width:180}):null,a===`models`?(0,F.jsx)(`div`,{className:`scope-models`,children:p.loading?(0,F.jsx)(`div`,{className:`ov-hint`,children:`正在读取模型清单…`}):A.length===0?(0,F.jsx)(`div`,{className:`ov-hint`,children:`模型清单为空。先去「提供商」面板执行一次「获取模型」。`}):(0,F.jsxs)(F.Fragment,{children:[(0,F.jsxs)(`div`,{className:`ov-hint`,children:[`已选 `,d.length,` 个`,L?` —— 至少选一个才能创建`:``]}),(0,F.jsx)(`div`,{className:`scope-models-list`,children:A.map(e=>(0,F.jsxs)(`label`,{className:`scope-model`,children:[(0,F.jsx)(`input`,{type:`checkbox`,checked:d.includes(e.id),onChange:()=>N(e.id)}),(0,F.jsx)(`span`,{className:`code-chip`,children:e.id}),(0,F.jsx)(`i`,{children:e.displayName})]},e.id))})]})}):null]}),(0,F.jsxs)(`div`,{className:`ov-field`,children:[(0,F.jsx)(`label`,{children:`绑定提供商(可多选,可排序)`}),m.loading?(0,F.jsx)(`div`,{className:`ov-hint`,children:`正在读取提供商…`}):z.length===0?(0,F.jsx)(`div`,{className:`ov-hint`,children:`还没有登记任何提供商 —— 先去「提供商管理」页登记。`}):(0,F.jsxs)(F.Fragment,{children:[(0,F.jsx)(`div`,{className:`pick-list`,children:z.map(e=>{let t=g.includes(e.value);return(0,F.jsxs)(`label`,{className:`pick-row${t?` on`:``}`,children:[(0,F.jsx)(`input`,{type:`checkbox`,checked:t,onChange:()=>B(e.value)}),(0,F.jsx)(`span`,{className:`pick-name`,children:e.label})]},e.value)})}),(0,F.jsxs)(`div`,{className:`picked-head`,children:[(0,F.jsxs)(`span`,{children:[`已选 `,g.length,` 个`]}),g.length>1?(0,F.jsx)(`span`,{className:`picked-tip`,children:`用 ↑↓ 调整优先级`}):null]}),g.length>0?(0,F.jsx)(`div`,{className:`picked-list`,children:g.map((e,t)=>(0,F.jsxs)(`div`,{className:`picked-item`,children:[(0,F.jsx)(`span`,{className:`picked-rank`,children:t+1}),(0,F.jsx)(`span`,{className:`picked-name`,children:H(e)}),(0,F.jsxs)(`span`,{className:`picked-actions`,children:[(0,F.jsx)(`button`,{type:`button`,className:`pick-mv`,disabled:t===0,onClick:()=>V(t,-1),"aria-label":`上移`,children:`↑`}),(0,F.jsx)(`button`,{type:`button`,className:`pick-mv`,disabled:t===g.length-1,onClick:()=>V(t,1),"aria-label":`下移`,children:`↓`}),(0,F.jsx)(`button`,{type:`button`,className:`pick-rm`,onClick:()=>B(e),"aria-label":`移除`,children:`×`})]})]},e))}):null,(0,F.jsx)(`div`,{className:`ov-hint`,children:`勾选后这个 Key 的请求会归到对应提供商名下;第 1 位是首选(用量归因与后续路由都优先用它)。`})]})]}),(0,F.jsx)(G,{draft:b,onChange:S}),T?(0,F.jsx)(x,{tone:`error`,children:k(T)}):null]})]})}function X({value:e}){let[n,r]=(0,P.useState)(!1),i=async()=>{try{await navigator.clipboard.writeText(e),r(!0),setTimeout(()=>r(!1),2e3)}catch{r(!1)}};return(0,F.jsxs)(`div`,{className:`ov-field`,children:[(0,F.jsx)(`label`,{children:`完整密钥(只此一次)`}),(0,F.jsxs)(`div`,{className:`key-plain`,children:[(0,F.jsx)(`span`,{style:{flex:1},children:e}),(0,F.jsxs)(`button`,{type:`button`,className:`copy`,"aria-label":n?`已复制完整密钥`:`复制完整密钥`,onClick:()=>void i(),children:[n?(0,F.jsx)(t,{size:13}):(0,F.jsx)(f,{size:13}),n?`已复制`:`复制`]})]})]})}function Z({row:e,onClose:t,onSaved:n}){let[i,a]=(0,P.useState)(e.name),[o,s]=(0,P.useState)(e.clients??(e.client?[e.client]:[])),[c,l]=(0,P.useState)(()=>z({ipWhitelist:e.ipWhitelist??[],maxIps:e.maxIps??null,tokenQuota:e.tokenQuota??null})),[u,d]=(0,P.useState)(!1),[f,p]=(0,P.useState)(null),m=y(()=>_.providers(),[],{cacheKey:`providers`}),g=(()=>{let e=(m.data?.items??[]).map(e=>({value:e.id,label:e.active?`${e.name}(已启用)`:e.name})).sort((e,t)=>e.label.localeCompare(t.label,`zh-Hans-CN`));for(let t of o)e.some(e=>e.value===t)||e.push({value:t,label:`${t}(已删除,当前不在列表)`});return e})(),v=e=>s(t=>t.includes(e)?t.filter(t=>t!==e):[...t,e]),b=(e,t)=>s(n=>{let r=[...n],i=e+t;return i<0||i>=r.length?n:([r[e],r[i]]=[r[i],r[e]],r)}),S=e=>g.find(t=>t.value===e)?.label??e,C=i.trim(),w=C!==e.name,T=e.clients??(e.client?[e.client]:[]),E=JSON.stringify(o)!==JSON.stringify(T),D=U(c),O=!(`error`in D)&&(JSON.stringify(D.ipWhitelist)!==JSON.stringify(e.ipWhitelist??[])||D.maxIps!==(e.maxIps??null)||D.tokenQuota!==(e.tokenQuota??null)),A=!!C&&(w||E||O)&&!u;return(0,F.jsxs)(M,{title:`修改密钥`,onClose:t,footer:(0,F.jsxs)(F.Fragment,{children:[(0,F.jsx)(`button`,{type:`button`,className:`btn btn-quiet btn-sm`,onClick:t,children:`取消`}),(0,F.jsxs)(`button`,{type:`submit`,form:`edit-key-form`,className:`btn btn-primary btn-sm`,disabled:!A,children:[u?(0,F.jsx)(r,{size:14}):null,u?`保存中…`:`保存`]})]}),children:[(0,F.jsx)(`p`,{className:`ov-sub`,children:`改的是「叫什么」和「绑哪家」,不动作废 —— 这把密钥的明文和哈希都不受影响,已接入的客户端无需更换。`}),(0,F.jsxs)(`form`,{id:`edit-key-form`,onSubmit:async r=>{if(r.preventDefault(),A){if(`error`in D){p(new h(D.error,400,0));return}d(!0),p(null);try{let r={};w&&(r.name=C),E&&(r.clients=o),O&&(r.ipWhitelist=D.ipWhitelist,r.maxIps=D.maxIps,r.tokenQuota=D.tokenQuota),await _.patchKey(e.id,r),n(),t()}catch(e){p(e instanceof h?e:new h(String(e),0,0))}finally{d(!1)}}},style:{display:`flex`,flexDirection:`column`,gap:16},children:[(0,F.jsxs)(`div`,{className:`ov-field`,children:[(0,F.jsx)(`label`,{htmlFor:`ek-name`,children:`密钥名称`}),(0,F.jsx)(`input`,{id:`ek-name`,className:`field-input text`,style:{width:`100%`},value:i,autoFocus:!0,onChange:e=>a(e.target.value)}),(0,F.jsxs)(`div`,{className:`ov-hint`,children:[`当前:`,e.name,` · `,e.hint]})]}),(0,F.jsxs)(`div`,{className:`ov-field`,children:[(0,F.jsx)(`label`,{children:`绑定提供商(可多选,可排序)`}),m.loading?(0,F.jsx)(`div`,{className:`ov-hint`,children:`正在读取提供商…`}):g.length===0?(0,F.jsx)(`div`,{className:`ov-hint`,children:`还没有登记任何提供商 —— 先去「提供商管理」页登记。`}):(0,F.jsxs)(F.Fragment,{children:[(0,F.jsx)(`div`,{className:`pick-list`,children:g.map(e=>{let t=o.includes(e.value);return(0,F.jsxs)(`label`,{className:`pick-row${t?` on`:``}`,children:[(0,F.jsx)(`input`,{type:`checkbox`,checked:t,onChange:()=>v(e.value)}),(0,F.jsx)(`span`,{className:`pick-name`,children:e.label})]},e.value)})}),(0,F.jsxs)(`div`,{className:`picked-head`,children:[(0,F.jsxs)(`span`,{children:[`已选 `,o.length,` 个`]}),o.length>1?(0,F.jsx)(`span`,{className:`picked-tip`,children:`用 ↑↓ 调整优先级`}):null]}),o.length>0?(0,F.jsx)(`div`,{className:`picked-list`,children:o.map((e,t)=>(0,F.jsxs)(`div`,{className:`picked-item`,children:[(0,F.jsx)(`span`,{className:`picked-rank`,children:t+1}),(0,F.jsx)(`span`,{className:`picked-name`,children:S(e)}),(0,F.jsxs)(`span`,{className:`picked-actions`,children:[(0,F.jsx)(`button`,{type:`button`,className:`pick-mv`,disabled:t===0,onClick:()=>b(t,-1),"aria-label":`上移`,children:`↑`}),(0,F.jsx)(`button`,{type:`button`,className:`pick-mv`,disabled:t===o.length-1,onClick:()=>b(t,1),"aria-label":`下移`,children:`↓`}),(0,F.jsx)(`button`,{type:`button`,className:`pick-rm`,onClick:()=>v(e),"aria-label":`移除`,children:`×`})]})]},e))}):null,(0,F.jsx)(`div`,{className:`ov-hint`,children:`全部取消即解绑。绑定只影响用量归因(第 1 位是首选),不改变权限范围。`})]})]}),(0,F.jsx)(G,{draft:c,onChange:l}),(e.maxIps!==null||e.tokenQuota!==null)&&(0,F.jsxs)(`div`,{className:`ov-hint`,children:[`当前累计:`,e.maxIps===null?``:`${e.usedIps??0} / ${e.maxIps} 个来源 IP`,e.maxIps!==null&&e.tokenQuota!==null?` · `:``,e.tokenQuota===null?``:`${(e.usedTokens??0).toLocaleString()} / ${e.tokenQuota.toLocaleString()} tokens`]}),(0,F.jsxs)(`div`,{className:`ov-hint`,children:[`权限范围(`,(0,F.jsx)(`b`,{children:`不可改`}),`):它是授权面,要换范围删除后重建 —— 明文只显示一次,本就该重新分发。`]}),f?(0,F.jsx)(x,{tone:`error`,children:k(f)}):null]})]})}function Q(){return new URLSearchParams(window.location.hash.split(`?`)[1]??``).get(`clientId`)||null}function $(){let t=y(()=>_.keys(),[],{cacheKey:`keys`}),n=y(()=>_.clients(`30d`),[],{cacheKey:`clients:30d`}),a=y(()=>_.providers(),[],{cacheKey:`providers`}),o=new Map((a.data?.items??[]).map(e=>[e.id,e.name])),[s,c]=(0,P.useState)(Q),[l,d]=(0,P.useState)(!1),[f,m]=(0,P.useState)(null),[g,b]=(0,P.useState)(null),[S,C]=(0,P.useState)(null),[E,j]=(0,P.useState)(null);(0,P.useEffect)(()=>{let e=()=>c(Q());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]);let I=(0,P.useMemo)(()=>{let e=new Map;for(let t of n.data?.items??[]){let n={clientId:t.clientId,name:t.client};e.set(t.clientId,n);for(let r of t.boundKeyIds)e.set(r,n)}return e},[n.data]),R=n.data?.items.find(e=>e.clientId===s),z=t.data??[],B=s?z.filter(e=>e.id===s||R?.boundKeyIds.includes(e.id)):z,V=!!(s&&n.initial&&n.loading),H={total:B.length,live:0,off:0,exp:0};for(let e of B)H[p(e.enabled,e.expiresAt)]+=1;let U=e=>t.data&&!V?v(e):`--`,W=()=>{t.reload(),n.reload()},G=async e=>{if(!f){m(e.id),j(null);try{await _.patchKey(e.id,{enabled:!e.enabled}),t.reload()}catch(e){j(e instanceof h?e:new h(String(e),0,0))}finally{m(null)}}},K=async()=>{if(!S||f)return;let e=S;m(e.id),j(null);try{await _.deleteKey(e.id),C(null),W()}catch(e){j(e instanceof h?e:new h(String(e),0,0)),C(null)}finally{m(null)}},q=()=>{window.location.hash=`#/keys`,c(null)};return(0,F.jsxs)(`div`,{className:`view on`,children:[(0,F.jsx)(A,{title:`API Keys`,actions:(0,F.jsxs)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:()=>d(!0),children:[(0,F.jsx)(i,{size:14}),`新建API Key`]})}),(0,F.jsx)(`div`,{className:`view-body stack`,children:t.initial&&t.loading?(0,F.jsx)(N,{}):(0,F.jsxs)(F.Fragment,{children:[E?(0,F.jsx)(x,{tone:`error`,children:k(E)}):null,(0,F.jsx)(O,{items:[{label:`密钥总数`,value:U(H.total),detail:s?`客户端筛选下 ${U(H.total)} 把`:`全部密钥`},{label:`启用`,value:U(H.live),tone:`live`,detail:`未过期且启用`},{label:`停用`,value:U(H.off),detail:`已停用且未过期`},{label:`过期`,value:U(H.exp),tone:`amber`,detail:`expiresAt 已过`}]}),s?(0,F.jsxs)(`div`,{className:`note`,children:[(0,F.jsxs)(`span`,{children:[`关联客户端:`,R?(0,F.jsx)(`a`,{href:`#/clients?clientId=${encodeURIComponent(R.clientId)}`,children:R.client}):`未绑定`]}),(0,F.jsxs)(`button`,{type:`button`,className:`btn btn-quiet btn-sm`,onClick:q,children:[(0,F.jsx)(u,{size:14}),`清除筛选`]})]}):null,n.error?(0,F.jsx)(D,{kind:`error`,title:`客户端关联数据没取到`,detail:k(n.error),onRetry:n.reload}):null,(0,F.jsxs)(w,{title:`密钥列表`,aside:(0,F.jsxs)(T,{children:[U(B.length),` 个 · 明文仅在创建时显示一次`]}),children:[t.error?(0,F.jsx)(D,{kind:`error`,title:`密钥数据没取到`,detail:k(t.error),onRetry:t.reload}):V?(0,F.jsx)(D,{kind:`loading`,title:`正在读取客户端关联…`}):B.length===0?s?(0,F.jsx)(D,{kind:`empty`,title:`没有关联的密钥`,children:(0,F.jsxs)(`button`,{type:`button`,className:`btn btn-quiet btn-sm`,onClick:q,children:[(0,F.jsx)(u,{size:14}),`清除筛选`]})}):(0,F.jsx)(D,{kind:`empty`,title:`还没有任何密钥`,detail:`没有密钥时 /v1/* 会拒绝所有请求。`,children:(0,F.jsxs)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:()=>d(!0),children:[(0,F.jsx)(e,{size:14}),`创建第一把密钥`]})}):(0,F.jsx)(L,{rows:B,clientByKey:I,providerNameById:o,pendingId:f,onToggle:e=>void G(e),onEdit:b,onDelete:C}),(0,F.jsxs)(`div`,{className:`footer-note`,children:[`完整密钥只在创建时显示一次,服务端只保存哈希;停用后立即失效,可随时重新启用。改名与改绑定不会动作废 —— 已接入的客户端不受影响。`,(0,F.jsx)(`br`,{}),`上面三格互斥、可相加等于总数:`,(0,F.jsx)(`b`,{children:`过期优先于启停判断`}),`(expiresAt 已过即归过期,不再看启用状态),所以一把同时「启用」又「过期」的密钥只会计进「过期」。`]})]})]})}),l?(0,F.jsx)(Y,{onClose:()=>d(!1),onCreated:W}):null,g?(0,F.jsx)(Z,{row:g,onClose:()=>b(null),onSaved:W}):null,S?(0,F.jsx)(M,{title:`删除这把密钥?`,onClose:()=>{f||C(null)},footer:(0,F.jsxs)(F.Fragment,{children:[(0,F.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,disabled:!!f,onClick:()=>C(null),children:`取消(保留)`}),(0,F.jsxs)(`button`,{type:`button`,className:`btn btn-danger btn-sm`,disabled:!!f,onClick:()=>void K(),children:[f?(0,F.jsx)(r,{size:14,className:`spin`}):null,f?`删除中…`:`确认删除`]})]}),children:(0,F.jsxs)(`div`,{className:`ov-lose`,children:[(0,F.jsx)(`b`,{children:`会失去什么:`}),`密钥「`,S.name,`」(`,S.hint,`)立即失效,任何还在用它的客户端会立刻收到 401, 需要重新建密钥并逐个替换。此操作不可撤销。`]})}):null]})}export{$ as ApiKeysPage};
@@ -1 +1 @@
1
- import{tt as e}from"./vendor-befEYGHM.js";import{_ as t,d as n,g as r,p as i,x as a}from"./index-BcWmrhMD.js";var o=e();function s({metrics:e}){return(0,o.jsxs)(`div`,{className:`credit-banner`,children:[(0,o.jsxs)(`div`,{className:`cb-left`,children:[(0,o.jsx)(`div`,{className:`cb-label`,children:`本窗口总 tokens`}),(0,o.jsx)(`div`,{children:(0,o.jsx)(`span`,{className:`cb-num`,children:a(e.totalTokens)})}),(0,o.jsx)(`div`,{className:`cb-why`,children:(0,o.jsxs)(`span`,{children:[`输入 `,a(e.promptTokens),` · 输出 `,a(e.completionTokens)]})})]}),(0,o.jsxs)(`div`,{className:`cb-mid`,children:[(0,o.jsx)(`div`,{className:`row`,children:(0,o.jsxs)(`span`,{className:`t`,children:[`经本服务转发已用 `,i(e.creditsUsed),` 积分`]})}),(0,o.jsxs)(`div`,{className:`cb-note`,children:[`仅统计经本服务转发的请求(取上游实扣 usage.credit;上游未回报的行积分未知,不计入合计`,e.creditsUnknownCount>0?`,本窗口 ${r(e.creditsUnknownCount)} 条`:``,`)。 官方客户端直连的消耗不计入 —— 账户总消耗与余额以官方客户端为准。`]})]}),(0,o.jsxs)(`div`,{className:`cb-right`,children:[(0,o.jsx)(`div`,{className:`cb-label`,children:`缓存命中率`}),(0,o.jsx)(`div`,{className:`cb-num`,style:{fontSize:22},children:n(e.cacheHitRate)}),(0,o.jsxs)(`div`,{className:`cb-note`,children:[a(e.cachedTokens),` / `,a(e.promptTokens),` 输入 tokens`]})]})]})}var c={credits:`消耗积分`,tokens:`Token 消耗`,requests:`请求次数`};function l(e,t){return e[t]??0}function u(e,t){return t===`credits`?i(e):t===`tokens`?a(e):r(e)}function d(e){return e===`credits`?`积分`:e===`tokens`?`tokens`:`次`}function f({rows:e,metric:n,windowLabel:r,total:i,totalCount:a}){let s=e.slice(0,6),f=Math.max(0,a-s.length),p=Math.max(0,i);return(0,o.jsxs)(`div`,{className:`client-metric-tip`,children:[(0,o.jsxs)(`div`,{className:`client-metric-tip-head`,children:[(0,o.jsxs)(`div`,{children:[(0,o.jsx)(`strong`,{children:`客户端拆分`}),(0,o.jsxs)(`span`,{children:[r,` · `,c[n]]})]}),(0,o.jsxs)(`span`,{className:`client-metric-tip-tag`,children:[`TOP `,s.length]})]}),(0,o.jsxs)(`div`,{className:`client-metric-tip-total`,children:[(0,o.jsx)(`span`,{children:`窗口合计`}),(0,o.jsxs)(`b`,{children:[u(p,n),` `,(0,o.jsx)(`em`,{children:d(n)})]})]}),(0,o.jsx)(`div`,{className:`client-metric-tip-list`,children:s.map((e,r)=>{let i=l(e,n),a=p>0?i/p:0;return(0,o.jsxs)(`div`,{className:`client-metric-tip-row`,children:[(0,o.jsxs)(`div`,{className:`client-metric-tip-row-top`,children:[(0,o.jsx)(`span`,{className:`client-metric-tip-rank`,children:String(r+1).padStart(2,`0`)}),(0,o.jsx)(`span`,{className:`client-metric-tip-name`,children:e.client??`未归属`}),(0,o.jsxs)(`b`,{children:[u(i,n),` `,(0,o.jsx)(`em`,{children:d(n)})]})]}),(0,o.jsxs)(`div`,{className:`client-metric-tip-row-bottom`,children:[(0,o.jsx)(`span`,{className:`client-metric-tip-track`,children:(0,o.jsx)(`i`,{style:{width:`${Math.max(0,Math.min(100,a*100))}%`}})}),(0,o.jsx)(`span`,{children:t(a)})]})]},e.clientId??e.client??r)})}),f>0?(0,o.jsxs)(`div`,{className:`client-metric-tip-note`,children:[`另有 `,f,` 个客户端未展开,完整明细见按客户端列表。`]}):null]})}export{s as n,f as t};
1
+ import{tt as e}from"./vendor-befEYGHM.js";import{_ as t,d as n,g as r,p as i,x as a}from"./index-DO57ipCG.js";var o=e();function s({metrics:e}){return(0,o.jsxs)(`div`,{className:`credit-banner`,children:[(0,o.jsxs)(`div`,{className:`cb-left`,children:[(0,o.jsx)(`div`,{className:`cb-label`,children:`本窗口总 tokens`}),(0,o.jsx)(`div`,{children:(0,o.jsx)(`span`,{className:`cb-num`,children:a(e.totalTokens)})}),(0,o.jsx)(`div`,{className:`cb-why`,children:(0,o.jsxs)(`span`,{children:[`输入 `,a(e.promptTokens),` · 输出 `,a(e.completionTokens)]})})]}),(0,o.jsxs)(`div`,{className:`cb-mid`,children:[(0,o.jsx)(`div`,{className:`row`,children:(0,o.jsxs)(`span`,{className:`t`,children:[`经本服务转发已用 `,i(e.creditsUsed),` 积分`]})}),(0,o.jsxs)(`div`,{className:`cb-note`,children:[`仅统计经本服务转发的请求(取上游实扣 usage.credit;上游未回报的行积分未知,不计入合计`,e.creditsUnknownCount>0?`,本窗口 ${r(e.creditsUnknownCount)} 条`:``,`)。 官方客户端直连的消耗不计入 —— 账户总消耗与余额以官方客户端为准。`]})]}),(0,o.jsxs)(`div`,{className:`cb-right`,children:[(0,o.jsx)(`div`,{className:`cb-label`,children:`缓存命中率`}),(0,o.jsx)(`div`,{className:`cb-num`,style:{fontSize:22},children:n(e.cacheHitRate)}),(0,o.jsxs)(`div`,{className:`cb-note`,children:[a(e.cachedTokens),` / `,a(e.promptTokens),` 输入 tokens`]})]})]})}var c={credits:`消耗积分`,tokens:`Token 消耗`,requests:`请求次数`};function l(e,t){return e[t]??0}function u(e,t){return t===`credits`?i(e):t===`tokens`?a(e):r(e)}function d(e){return e===`credits`?`积分`:e===`tokens`?`tokens`:`次`}function f({rows:e,metric:n,windowLabel:r,total:i,totalCount:a}){let s=e.slice(0,6),f=Math.max(0,a-s.length),p=Math.max(0,i);return(0,o.jsxs)(`div`,{className:`client-metric-tip`,children:[(0,o.jsxs)(`div`,{className:`client-metric-tip-head`,children:[(0,o.jsxs)(`div`,{children:[(0,o.jsx)(`strong`,{children:`客户端拆分`}),(0,o.jsxs)(`span`,{children:[r,` · `,c[n]]})]}),(0,o.jsxs)(`span`,{className:`client-metric-tip-tag`,children:[`TOP `,s.length]})]}),(0,o.jsxs)(`div`,{className:`client-metric-tip-total`,children:[(0,o.jsx)(`span`,{children:`窗口合计`}),(0,o.jsxs)(`b`,{children:[u(p,n),` `,(0,o.jsx)(`em`,{children:d(n)})]})]}),(0,o.jsx)(`div`,{className:`client-metric-tip-list`,children:s.map((e,r)=>{let i=l(e,n),a=p>0?i/p:0;return(0,o.jsxs)(`div`,{className:`client-metric-tip-row`,children:[(0,o.jsxs)(`div`,{className:`client-metric-tip-row-top`,children:[(0,o.jsx)(`span`,{className:`client-metric-tip-rank`,children:String(r+1).padStart(2,`0`)}),(0,o.jsx)(`span`,{className:`client-metric-tip-name`,children:e.client??`未归属`}),(0,o.jsxs)(`b`,{children:[u(i,n),` `,(0,o.jsx)(`em`,{children:d(n)})]})]}),(0,o.jsxs)(`div`,{className:`client-metric-tip-row-bottom`,children:[(0,o.jsx)(`span`,{className:`client-metric-tip-track`,children:(0,o.jsx)(`i`,{style:{width:`${Math.max(0,Math.min(100,a*100))}%`}})}),(0,o.jsx)(`span`,{children:t(a)})]})]},e.clientId??e.client??r)})}),f>0?(0,o.jsxs)(`div`,{className:`client-metric-tip-note`,children:[`另有 `,f,` 个客户端未展开,完整明细见按客户端列表。`]}):null]})}export{s as n,f as t};
@@ -1 +1 @@
1
- import{H as e,I as t,J as n,L as r,Q as i,S as a,U as o,a as s,it as c,m as l,n as u,tt as d,z as f}from"./vendor-befEYGHM.js";import{M as p,a as m,c as h,g,j as _,k as v,p as y,r as b,t as x,v as S}from"./index-BcWmrhMD.js";import{a as C,c as w,n as T,r as E,s as D,t as O}from"./Select-Bfquh0d_.js";import{t as k}from"./Modal-B-rIrvXS.js";import{t as A}from"./PageSkeleton-DG-Dj4_9.js";import{n as j,t as M}from"./sharedWindow-FDfq75PI.js";import{t as N}from"./Drawer-CleQGhRU.js";import{t as P}from"./Switch-0KA1AbiV.js";var F=c(),I=d();function L({rows:e,byProvider:t,busyId:n,onEdit:r,onModels:a,onSignin:o,onGrowthRun:c,onOauthImport:d,onActivate:f,onDeactivate:p,onDelete:h}){let _=()=>new Intl.DateTimeFormat(`en-CA`,{timeZone:`Asia/Shanghai`,year:`numeric`,month:`2-digit`,day:`2-digit`}).format(new Date),v=new Map((t??[]).map(e=>[e.providerId??``,e]));return(0,I.jsx)(`div`,{className:`tbl-scroll`,children:(0,I.jsxs)(`table`,{className:`tbl`,children:[(0,I.jsxs)(`caption`,{className:`sr-only`,children:[`提供商列表,共 `,e.length,` 家提供商`]}),(0,I.jsx)(`thead`,{children:(0,I.jsxs)(`tr`,{children:[(0,I.jsx)(`th`,{scope:`col`,children:`提供商`}),(0,I.jsx)(`th`,{scope:`col`,children:`密钥`}),(0,I.jsx)(`th`,{scope:`col`,className:`r`,children:`请求次数`}),(0,I.jsx)(`th`,{scope:`col`,className:`r`,children:`消耗积分`}),(0,I.jsx)(`th`,{scope:`col`,className:`r`,children:`最后活跃`}),(0,I.jsx)(`th`,{scope:`col`,className:`r`,children:`操作`})]})}),(0,I.jsx)(`tbody`,{children:e.map(t=>{let b=n===t.id;return(0,I.jsxs)(`tr`,{className:`prov-row`,children:[(0,I.jsxs)(`td`,{className:`td-strong`,children:[t.name,t.active?(0,I.jsx)(w,{tone:`live`,children:`已启用`}):(0,I.jsx)(w,{tone:`ghost`,children:`未启用`}),t.protocol===`anthropic`?(0,I.jsx)(m,{content:`Anthropic Messages 方言(mavis 网关):模型目录用内置清单,凭证为 refresh token 自动换新`,children:(0,I.jsx)(`span`,{className:`pill pill-ghost`,children:`Anthropic`})}):t.protocol===`qoder`?(0,I.jsx)(m,{content:`Qoder CN 网关(COSY 协议):免费档 billable:false 实扣 0;目录从网关 /model/list 拉取;凭证为客户端身份 JSON(不自动刷新)`,children:(0,I.jsx)(`span`,{className:`pill pill-ghost`,children:`Qoder`})}):null,(0,I.jsx)(`div`,{className:`td-sub td-mono`,children:(0,I.jsx)(m,{content:(0,I.jsx)(I.Fragment,{children:(0,I.jsxs)(`div`,{className:`client-model-tip`,children:[(0,I.jsxs)(`div`,{className:`client-model-tip-head`,children:[(0,I.jsxs)(`div`,{className:`client-model-tip-title`,children:[(0,I.jsx)(`strong`,{children:t.name}),(0,I.jsx)(`span`,{children:t.baseUrl})]}),(0,I.jsx)(`span`,{className:`client-model-tip-tag`,children:t.active?`已启用`:`未启用`})]}),(0,I.jsxs)(`div`,{className:`client-model-tip-summary`,children:[(0,I.jsxs)(`div`,{children:[(0,I.jsx)(`span`,{children:`模型总数`}),(0,I.jsx)(`b`,{children:g(t.modelCount??0)})]}),(0,I.jsxs)(`div`,{children:[(0,I.jsx)(`span`,{children:`已启用`}),(0,I.jsx)(`b`,{children:g(t.modelEnabled??0)})]})]}),(()=>{let e=t.credits?.available?{remaining:t.credits.remaining,consumed:t.credits.consumed}:t.wbCredits?{remaining:t.wbCredits.remaining,consumed:t.wbCredits.consumed}:t.qoderCredits?{remaining:t.qoderCredits.remaining,consumed:t.qoderCredits.consumed}:null;return e&&(e.remaining!==null||e.consumed!==null)?(0,I.jsxs)(`div`,{className:`client-metric-tip-total`,children:[(0,I.jsx)(`span`,{children:`剩余积分`}),(0,I.jsxs)(`b`,{children:[y(e.remaining??0),` `,(0,I.jsxs)(`em`,{children:[`已用 `,y(e.consumed??0)]})]})]}):null})(),t.credits?.stale||t.qoderCredits?.stale?(0,I.jsx)(`div`,{className:`client-model-tip-note`,children:`⚠ 积分数据偏旧(账本快照超时)`}):null]})}),children:(0,I.jsxs)(`span`,{className:`prov-model-count`,children:[g(t.modelCount??0),` 个模型`]})})})]}),(0,I.jsxs)(`td`,{className:`td-cred`,children:[(0,I.jsx)(m,{content:`供应商访问密钥(掩码显示;全量只存在服务器上)`,children:(0,I.jsx)(`span`,{className:`cred-mask`,children:t.credentialMask})}),(0,I.jsx)(`span`,{className:`up-badge${t.configured?``:` warn`}`,children:t.configured?`有效`:`未配置`})]}),(0,I.jsx)(`td`,{className:`r td-mono`,children:t.active?g(v.get(t.id)?.requests??0):`—`}),(0,I.jsx)(`td`,{className:`r td-mono`,children:t.active?y(v.get(t.id)?.credits??0):`—`}),(0,I.jsx)(`td`,{className:`r td-mono`,children:t.active?S(v.get(t.id)?.lastSeenAt??null):`—`}),(0,I.jsx)(`td`,{className:`r`,children:(0,I.jsxs)(`span`,{className:`prov-row-actions`,children:[(0,I.jsx)(m,{content:`改名称与上游地址;凭证可在抽屉内「重新导入授权」`,children:(0,I.jsxs)(`button`,{type:`button`,className:`mini-pill`,disabled:b,onClick:()=>r(t),children:[(0,I.jsx)(u,{size:12}),`配置`]})}),(0,I.jsx)(m,{content:`查看这家自己的模型清单(启停只作用于该提供商分区)`,children:(0,I.jsxs)(`button`,{type:`button`,className:`mini-pill`,"aria-disabled":b||!t.active,onClick:()=>{!b&&t.active&&a(t)},children:[(0,I.jsx)(l,{size:12}),`模型列表`]})}),t.credKind===`oauth_minimax`||t.protocol===`qoder`?(0,I.jsx)(m,{content:(()=>{let e=t.protocol===`qoder`?`10:05`:`08:05`,n=t.signin?.checkedOn?`最近签到 ${t.signin.checkedOn}${t.signin.lastPoints?` · +${t.signin.lastPoints} 积分`:``}(relay 每天 ${e} 自动签)`:`今天还没签到 —— 点一下立即签(relay 每天 ${e} 也会自动签)`;return t.protocol===`qoder`?(0,I.jsxs)(I.Fragment,{children:[n,(0,I.jsx)(`br`,{}),`Qoder 官方标注:每日 10:00(UTC+8)刷新,领取后 30 天有效。`]}):n})(),children:(0,I.jsxs)(`button`,{type:`button`,className:`mini-pill`,"aria-disabled":b||t.signin?.checkedOn===_(),onClick:()=>{b||t.signin?.checkedOn===_()||o(t)},children:[(0,I.jsx)(i,{size:12}),t.signin?.checkedOn===_()?`已签到`:`签到`]})}):null,t.protocol===`openai`?t.wbOAuth?.configured?(0,I.jsx)(m,{content:t.wbSignin?.checkedOn===_()?`今天已签到过(明天 08:05 自动跑;任务面板在设置 → 提供商健康)`:`立即签到 + 顺带跑一轮成长任务与连登兑换(每天 08:05 也会自动跑)。转发不受影响。`,children:(0,I.jsxs)(`button`,{type:`button`,className:`mini-pill`,"aria-disabled":b||t.wbSignin?.checkedOn===_(),onClick:()=>{!b&&t.wbSignin?.checkedOn!==_()&&c(t)},children:[(0,I.jsx)(i,{size:12}),t.wbSignin?.checkedOn===_()?`已签到`:`签到`]})}):(0,I.jsx)(m,{content:`导入桌面端 OAuth 凭证后可代领签到/任务(上游不允许 API 密钥代领)。粘贴 tools/wb-oauth-export.mjs 的输出;转发仍走 API 密钥。`,children:(0,I.jsxs)(`button`,{type:`button`,className:`mini-pill`,"aria-disabled":b,onClick:()=>{b||d(t)},children:[(0,I.jsx)(i,{size:12}),`导入OAuth`]})}):null,t.active?(0,I.jsx)(m,{content:`退出出站路由(目录与凭证保留,可再启用)`,children:(0,I.jsx)(`button`,{type:`button`,className:`mini-pill`,"aria-disabled":b,onClick:()=>{b||p(t)},children:`停用`})}):(0,I.jsx)(`button`,{type:`button`,className:`mini-pill`,disabled:b,onClick:()=>f(t),children:`启用`}),(0,I.jsx)(m,{content:t.active?`不能删除已启用的提供商 —— 先停用它`:e.length<=1?`至少保留一个提供商`:`删除这个提供商(凭证文件一并删除)`,children:(0,I.jsxs)(`button`,{type:`button`,className:`mini-pill danger`,"aria-disabled":b||t.active||e.length<=1,onClick:()=>{!b&&!t.active&&e.length>1&&h(t)},children:[(0,I.jsx)(s,{size:12}),`删除`]})})]})})]},t.id)})})]})})}function R({command:e}){let[t,r]=(0,F.useState)(!1);if(!e)return null;let i=async()=>{try{await navigator.clipboard.writeText(e),r(!0),setTimeout(()=>r(!1),2e3)}catch{r(!1)}};return(0,I.jsxs)(`div`,{className:`cred-guide-cmd`,children:[(0,I.jsx)(`code`,{children:e}),(0,I.jsxs)(`button`,{type:`button`,className:`copy`,"aria-label":t?`已复制命令`:`复制命令`,onClick:()=>void i(),children:[t?(0,I.jsx)(n,{size:13}):(0,I.jsx)(f,{size:13}),t?`已复制`:`复制`]})]})}var z=[{protocol:`openai`,label:`WorkBuddy`,desc:`OpenAI 方言`,needsAuth:!0},{protocol:`anthropic`,label:`MiniMax Code`,desc:`Anthropic 方言`,needsAuth:!0},{protocol:`qoder`,label:`Qoder CN`,desc:`COSY 协议`,needsAuth:!0}],B={openai:`https://copilot.tencent.com`,anthropic:`https://agent.minimax.cn`,qoder:`https://gateway.qoder.com.cn`},V=2500,H=3e5;function U({provider:e,onClose:n,onImported:r}){let i=x(),[o,s]=(0,F.useState)(e?`confirm`:`pick`),[c,l]=(0,F.useState)(e?.protocol??`anthropic`),[u,d]=(0,F.useState)(e?.name??``),[f,m]=(0,F.useState)(``),[g,_]=(0,F.useState)(``),[v,y]=(0,F.useState)(!1),[S,C]=(0,F.useState)(null),w=(0,F.useRef)(e?.id??null),T=(0,F.useRef)(!1),D=(0,F.useRef)(!1),O=z.find(e=>e.protocol===c);(0,F.useEffect)(()=>()=>{D.current=!0;let e=w.current;e&&h.providerImportDeviceCancel(e).catch(()=>void 0)},[]);let A=(0,F.useCallback)(async()=>{y(!0),C(null);try{let e=w.current;e||(e=(await h.createProvider({name:u.trim()||O.label,baseUrl:B[c]??B.openai,credential:``,protocol:c})).item.id,w.current=e,T.current=!0);let t=await h.providerImportDeviceStart(e);if(!t.ok||!t.authUrl){C(new p(t.message??`申请授权失败`,0,0)),s(`confirm`),y(!1);return}m(t.authUrl),_(t.userCode??``),window.open(t.authUrl,`_blank`,`noopener`),s(`waiting`),y(!1);let n=Date.now()+H;for(;!D.current&&Date.now()<n;){if(await new Promise(e=>setTimeout(e,V)),D.current)return;let t=await h.providerImportDevicePoll(e);if(D.current)return;if(t.status===`done`){s(`done`),i.push({tone:`success`,message:`${u.trim()||O.label} 授权已导入。`}),r();return}if(t.status===`error`){C(new p(t.message??`授权失败`,0,0)),s(`confirm`);return}}D.current||(C(new p(`授权超时(5 分钟)—— 请重新发起导入。`,0,0)),s(`confirm`))}catch(e){C(e instanceof p?e:new p(String(e),0,0)),s(`confirm`),y(!1)}},[O.label,u,r,c,i]);if(o===`done`)return(0,I.jsxs)(k,{title:`授权已导入`,onClose:n,footer:(0,I.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:n,children:`完成`}),children:[(0,I.jsx)(b,{tone:`success`,children:(0,I.jsxs)(`span`,{children:[u.trim()||O.label,` 的授权已通过官方通道导入。`]})}),e?null:(0,I.jsxs)(`p`,{className:`ov-sub`,style:{marginTop:12},children:[`回到列表点`,(0,I.jsx)(`b`,{children:`试通`}),`确认可用,再按需启用(启用后它才参与出站路由)。`]})]});if(o===`waiting`)return(0,I.jsx)(k,{title:`等待你完成授权`,onClose:n,children:(0,I.jsxs)(`div`,{className:`cred-guide`,children:[(0,I.jsxs)(`div`,{className:`cred-guide-note`,children:[`已为你打开 `,(0,I.jsx)(`b`,{children:O.label}),` 的授权页 —— `,(0,I.jsx)(`b`,{children:`请在浏览器里完成授权`}),`, 本窗口会自动检测结果,无需回到终端。`]}),g?(0,I.jsxs)(`div`,{className:`cred-guide-note`,children:[`若页面要求输入短码:`,(0,I.jsx)(`span`,{className:`code-chip`,children:g})]}):null,(0,I.jsx)(R,{command:f}),(0,I.jsxs)(`div`,{className:`cred-guide-note`,style:{display:`flex`,alignItems:`center`,gap:8},children:[(0,I.jsx)(a,{size:14}),`正在等待授权完成…(最多 5 分钟;没弹出浏览器就点上面的链接)`]}),S?(0,I.jsx)(b,{tone:`error`,children:E(S)}):null]})});let j=o===`confirm`;return(0,I.jsx)(k,{title:e?`重新导入授权 · ${e.name}`:`导入客户端`,onClose:n,footer:(0,I.jsxs)(I.Fragment,{children:[(0,I.jsx)(`button`,{type:`button`,className:`btn btn-quiet btn-sm`,onClick:n,children:`取消`}),(0,I.jsxs)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,disabled:v,onClick:()=>{if(!j){s(`confirm`);return}A()},children:[v?(0,I.jsx)(a,{size:14}):(0,I.jsx)(t,{size:14}),v?`正在申请授权…`:j?`确定,前往授权页`:`导入`]})]}),children:j?(0,I.jsxs)(I.Fragment,{children:[(0,I.jsx)(b,{tone:`warn`,children:(0,I.jsxs)(`span`,{children:[`即将跳转至 `,(0,I.jsx)(`b`,{children:O.label}),` 的授权页面(`,(0,I.jsx)(`b`,{children:`新标签页`}),`)。 请在页面里登录并完成授权,授权完成后本窗口会自动检测。`]})}),(0,I.jsxs)(`div`,{className:`cred-guide-note`,style:{marginTop:12},children:[`授权只用于取本工具的凭证;令牌保存在`,(0,I.jsx)(`b`,{children:`本机服务端`}),`(0600),不会回传到浏览器。`]}),c===`anthropic`?(0,I.jsxs)(`div`,{className:`cred-guide-note`,style:{marginTop:8},children:[`⚠️ MiniMax 的凭证是`,(0,I.jsx)(`b`,{children:`同账号单会话`}),`:请用`,(0,I.jsx)(`b`,{children:`专用账号`}),`, 否则会与日常客户端互相顶下线。`]}):null,S?(0,I.jsx)(b,{tone:`error`,children:E(S)}):null]}):(0,I.jsxs)(I.Fragment,{children:[(0,I.jsxs)(`p`,{className:`ov-sub`,children:[`选一个客户端,用它的`,(0,I.jsx)(`b`,{children:`官方授权`}),`拿到凭证(凭证在客户端登录态里,用户拿不到、无需手填)。`]}),(0,I.jsxs)(`div`,{className:`ov-field`,children:[(0,I.jsx)(`label`,{children:`客户端类型`}),(0,I.jsx)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:6},children:z.map(e=>(0,I.jsxs)(`label`,{style:{display:`flex`,alignItems:`center`,gap:8,cursor:`pointer`},children:[(0,I.jsx)(`input`,{type:`radio`,name:`import-client`,checked:c===e.protocol,onChange:()=>l(e.protocol)}),(0,I.jsxs)(`span`,{children:[(0,I.jsx)(`b`,{children:e.label}),(0,I.jsx)(`span`,{className:`ov-hint`,style:{marginLeft:8},children:e.desc})]})]},e.protocol))})]}),(0,I.jsxs)(`div`,{className:`ov-field`,children:[(0,I.jsx)(`label`,{htmlFor:`imp-name`,children:`名称(可留空)`}),(0,I.jsx)(`input`,{id:`imp-name`,className:`field-input text`,style:{width:`100%`},value:u,placeholder:O.label,onChange:e=>d(e.target.value)})]})]})})}var W={upstream:`上游`,seed:`种子`,manual:`手工`},G={upstream:`从上游模型目录拉取到的`,seed:`来自本地种子清单(上游拉取失败时的回落)`,manual:`人工添加的`};function K({catalog:t,catalogLoading:n,catalogMsg:r,catalogError:i,onCatalogRefresh:s,onCatalogReload:c,onModelToggled:u}){let[d,f]=(0,F.useState)(null),p=async e=>{f(e.id);try{await u(e.id,!e.enabled)}finally{f(null)}};return(0,I.jsxs)(`div`,{className:`up-col`,children:[(0,I.jsxs)(`div`,{className:`up-models-head`,children:[(0,I.jsxs)(`div`,{children:[(0,I.jsx)(`b`,{children:`可用模型`}),(0,I.jsx)(`span`,{children:t?`${t.models.length} 个 · ${t.models.filter(e=>e.enabled).length} 个已启用`:`尚未获取`})]}),(0,I.jsx)(`div`,{className:`up-models-actions`,children:n?(0,I.jsxs)(`button`,{type:`button`,className:`btn btn-quiet btn-sm`,disabled:!0,children:[(0,I.jsx)(a,{size:14,className:`spin`,style:{animation:`spin 1s linear infinite`}}),`正在实测模型…`]}):(0,I.jsxs)(`button`,{type:`button`,className:`btn btn-quiet btn-sm`,onClick:s,children:[(0,I.jsx)(l,{size:14}),`获取模型`]})})]}),n?(0,I.jsxs)(`div`,{className:`up-progress`,role:`status`,children:[(0,I.jsx)(a,{size:14,className:`spin`,style:{animation:`spin 1s linear infinite`},"aria-hidden":!0}),(0,I.jsxs)(`span`,{children:[`正在逐个实测模型(每个 1 token)…这一步会真打上游,`,(0,I.jsx)(`b`,{children:`可能要几十秒`}),`,请不要关掉页面; 完成后结果会出现在这里,模型清单也会自动刷新。`]})]}):null,r?(0,I.jsx)(b,{tone:r.kind===`error`?`error`:r.kind===`partial`?`warn`:`success`,children:r.text}):null,i?(0,I.jsx)(T,{kind:`error`,title:`模型清单没拉到`,detail:E(i),onRetry:c}):n&&!t?(0,I.jsx)(T,{kind:`loading`,title:`正在向上游获取模型…`}):!t||t.models.length===0?(0,I.jsx)(T,{kind:`empty`,title:`还没有模型清单`,detail:`启用提供商后会自动同步一次它自己的模型清单;这里也可以手动点「获取模型」逐项实测是否真的能调通。`}):(0,I.jsx)(`div`,{className:`up-models`,children:t.models.map(t=>(0,I.jsxs)(`div`,{className:`up-model${t.enabled?``:` off`}`,children:[(0,I.jsxs)(`div`,{className:`um-info`,children:[(0,I.jsxs)(`b`,{children:[(0,I.jsx)(`span`,{className:`um-k`,children:`模型名:`}),t.displayName,(0,I.jsx)(m,{content:G[t.source],children:(0,I.jsx)(`span`,{className:`um-src src-${t.source}`,children:W[t.source]})}),t.tag?(0,I.jsx)(m,{content:`上游计价标签,随目录同步更新`,children:(0,I.jsx)(`span`,{className:`mu-tag`,children:t.tag})}):null]}),(0,I.jsxs)(`i`,{className:`code-chip`,children:[(0,I.jsx)(`span`,{className:`um-k`,children:`模型ID:`}),t.id]})]}),(0,I.jsxs)(`div`,{className:`um-meta`,children:[t.verified?(0,I.jsx)(m,{content:`实测可调通`,children:(0,I.jsxs)(`span`,{className:`um-ok`,children:[(0,I.jsx)(o,{size:12}),`可用`]})}):(0,I.jsx)(m,{content:`实测未通过(可能未订阅)`,children:(0,I.jsxs)(`span`,{className:`um-bad`,children:[(0,I.jsx)(e,{size:12}),`未通过`]})}),t.multiplier==null?(0,I.jsx)(m,{content:`接口与本地快照都没有该模型的倍率 —— 不编造`,children:(0,I.jsx)(`span`,{className:`um-mult unknown`,children:`倍率未知`})}):(0,I.jsx)(m,{content:t.source===`upstream`?`倍率取自上游 /v3/config 接口实时值,随目录刷新与每日同步更新。`:`接口未覆盖该模型,倍率为内置快照,可能随官方调价过期。`,children:(0,I.jsxs)(`span`,{className:`um-mult`,children:[`×`,t.multiplier]})}),(0,I.jsx)(P,{checked:t.enabled,disabled:d===t.id,label:`${t.enabled?`停用`:`启用`} ${t.displayName}`,onChange:()=>void p(t)})]})]},t.id))}),(0,I.jsxs)(`div`,{className:`up-foot`,children:[`实测会真打上游、消耗少量积分(每模型 1 token)。上游不接受的模型会标「未通过」, 但`,(0,I.jsx)(`b`,{children:`不删除`}),` —— 订阅以后可能又开,随时可以再拉一次。 倍率以 /v3/config 接口实时值为准(目录刷新与每日自动同步都会更新), 接口未覆盖的模型回落内置快照。`,(0,I.jsx)(`br`,{}),`启用提供商时会同步这家自己的清单;已有提供商的目录互不覆盖,启用/停用选择也分别保留。`]})]})}function q({window:e}){let t=_(()=>h.providers(),[],{cacheKey:`providers`}),[n,i]=(0,F.useState)(null),o=_(()=>h.catalog(n?.id),[n?.id],{cacheKey:n?.id?`catalog:${n.id}`:`catalog`}),s=_(()=>h.clients(e),[e],{cacheKey:`clients:${e}`}),[c,l]=(0,F.useState)(!1),[u,d]=(0,F.useState)(null),[f,g]=(0,F.useState)(null),[v,y]=(0,F.useState)(null),[S,D]=(0,F.useState)(null),[O,j]=(0,F.useState)(null),[M,P]=(0,F.useState)(null),[R,z]=(0,F.useState)(!1),B=x(),V=t.data?.items??[],H=s.data?.byProvider??[],W=()=>{t.reload(),o.reload(),s.reload()},G=async e=>{g(e.id),j(null);try{await h.activateProvider(e.id),P(null),W()}catch(e){j(e instanceof p?e:new p(String(e),0,0))}finally{g(null)}},q=async e=>{g(e.id),j(null);try{await h.deactivateProvider(e.id),W()}catch(e){j(e instanceof p?e:new p(String(e),0,0))}finally{g(null)}},Y=async e=>{g(e.id),j(null);try{let n=await h.providerSigninNow(e.id);P({kind:n.ok?`ok`:`error`,text:n.message}),n.ok?B.push(n.outcome===`none`?{tone:`info`,message:`${e.name}:${n.message}`}:{tone:`success`,message:`${e.name} 签到成功${n.points?` · +${n.points} 积分`:``}。`}):B.push({tone:`error`,message:`${e.name} 签到失败:${n.message}`}),t.reload()}catch(t){let n=t instanceof p?t:new p(String(t),0,0);j(n),B.push({tone:`error`,message:`${e.name} 签到失败:${n.message||`请求未完成`}`})}finally{g(null)}},X=async e=>{g(e.id),j(null);try{let n=await h.providerGrowthRun(e.id),r=n.tasks?.filter(e=>e.outcome===`claimed`).length??0,i=n.tasks?.filter(e=>e.outcome===`already`).length??0,a=n.tasks?.filter(e=>e.outcome===`skipped`).length??0,o=n.ok?`领取完成:签到 ${n.signin?.points??0} 积分;任务新领 ${r}、已领过 ${i}、待完成 ${a};本轮合计 +${n.creditsGained??0}。`:n.message??`领取失败。`;P({kind:n.ok?`ok`:`error`,text:o}),B.push(n.ok?{tone:`success`,message:`${e.name}:${o}`}:{tone:`error`,message:`${e.name} 领取失败:${n.message??`未知原因`}`}),t.reload()}catch(t){let n=t instanceof p?t:new p(String(t),0,0);j(n),B.push({tone:`error`,message:`${e.name} 领取失败:${n.message||`请求未完成`}`})}finally{g(null)}},Z=async e=>{let n=globalThis.prompt?.(`粘贴 WorkBuddy OAuth 凭证 JSON(tools/wb-oauth-export.mjs 的输出):`,``)??null;if(!n)return;let r;try{r=JSON.parse(n)}catch{P({kind:`error`,text:`不是合法 JSON —— 请完整复制导出文件内容。`});return}g(e.id),j(null);try{let n=await h.providerOauthImport(e.id,r);P({kind:n.ok?`ok`:`error`,text:n.message}),t.reload()}catch(e){j(e instanceof p?e:new p(String(e),0,0))}finally{g(null)}},Q=async()=>{if(!S||f)return;let e=S;g(e.id),j(null);try{await h.deleteProvider(e.id),D(null),W()}catch(e){j(e instanceof p?e:new p(String(e),0,0)),D(null)}finally{g(null)}},ee=async()=>{if(!R){z(!0),P(null),j(null);try{let e=await h.refreshCatalog(),r=e.configPulled===!1?` 倍率未从接口取到,沿用本地快照。`:e.multipliersUpdated?` 倍率已按接口同步(${e.multipliersUpdated} 项有更新)。`:``,i=e.perProvider?.find(e=>e.id===n?.id),a=i?.checked??e.checked,s=i?.ok??e.ok,c=i?.failed.length??e.failed.length;P(c>0?{kind:`partial`,text:`已实测 ${a} 个模型:${s} 个可用、${c} 个未通过(可能未订阅)。未通过的不会删除,订阅后可再拉一次。${r}`}:{kind:`ok`,text:`已实测 ${a} 个模型,全部可用。${r}`}),o.reload(),t.reload()}catch(e){let t=e instanceof p?e:new p(String(e),0,0);P({kind:`error`,text:`获取模型失败:${E(t)}`})}finally{z(!1)}}},te=async(e,t)=>{j(null);try{await h.setModelEnabled(e,t,n?.id),o.reload()}catch(e){j(e instanceof p?e:new p(String(e),0,0))}},$=V.filter(e=>e.active).length;return(0,I.jsxs)(I.Fragment,{children:[O?(0,I.jsx)(b,{tone:`error`,children:E(O)}):null,s.error?(0,I.jsxs)(b,{tone:`error`,actions:(0,I.jsx)(`button`,{type:`button`,className:`btn btn-quiet btn-sm`,onClick:s.reload,children:`重试`}),children:[`提供商用量没取到:`,E(s.error)]}):null,(0,I.jsxs)(C,{title:`提供商`,aside:(0,I.jsxs)(`span`,{className:`panel-aside-row`,children:[$>0?(0,I.jsxs)(w,{tone:`live`,dot:!0,children:[`已启用 `,$,` 家`]}):null,(0,I.jsx)(m,{content:`选一个客户端(WorkBuddy / MiniMax Code / Qoder CN),用它的官方授权导入凭证`,children:(0,I.jsxs)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:()=>l(!0),children:[(0,I.jsx)(r,{size:14}),`导入客户端`]})})]}),padded:!1,children:[t.initial&&t.loading?(0,I.jsx)(`div`,{style:{padding:`0 20px 18px`},children:(0,I.jsx)(A,{})}):t.error?(0,I.jsx)(`div`,{style:{padding:`0 20px 18px`},children:(0,I.jsx)(T,{kind:`error`,title:`提供商列表没取到`,detail:E(t.error),onRetry:t.reload})}):V.length===0?(0,I.jsx)(`div`,{style:{padding:`0 20px 18px`},children:(0,I.jsx)(T,{kind:`empty`,title:`还没有任何提供商`,detail:`先登记一个上游,再按需启用它的出站路由。`})}):(0,I.jsx)(L,{rows:V,byProvider:H,busyId:f,onEdit:y,onSignin:e=>void Y(e),onGrowthRun:e=>void X(e),onOauthImport:e=>void Z(e),onModels:e=>{P(null),i(e)},onActivate:e=>void G(e),onDeactivate:e=>void q(e),onDelete:D}),(0,I.jsxs)(`div`,{className:`footer-note`,style:{padding:`0 20px 18px`},children:[`启用提供商会让它加入出站路由,并同步它自己的模型清单;已启用的提供商可以并行生效,模型按各自目录路由。 三家凭证都来自各客户端的`,(0,I.jsx)(`b`,{children:`官方授权`}),`(用户无需也无法手填)——点「重新导入授权」即可换账号或刷新令牌; 令牌只存在服务端,任何接口都读不回完整值。`,(0,I.jsx)(`br`,{}),`行内三列用量按`,(0,I.jsx)(`b`,{children:`提供商`}),`归属:由「模型 id → 生效目录的提供商」解析(请求记录本身没有 provider 维度)。模型不属于任何生效目录的行归到「未归属」,不冒充某一家。`]})]}),c?(0,I.jsx)(U,{onClose:()=>{l(!1),W()},onImported:()=>W()}):null,u?(0,I.jsx)(U,{provider:{id:u.id,name:u.name,protocol:u.protocol},onClose:()=>{d(null),W()},onImported:()=>W()}):null,v?(0,I.jsx)(J,{provider:v,onClose:()=>y(null),onSaved:()=>{y(null),W()},onReimport:()=>{let e=v;y(null),e&&d(e)}}):null,n?(0,I.jsx)(N,{title:`模型列表 · ${n.name}`,subtitle:n.baseUrl,onClose:()=>i(null),children:(0,I.jsx)(`div`,{className:`drawer-body`,children:(0,I.jsx)(K,{catalog:o.data,catalogLoading:R,catalogMsg:M,catalogError:o.error,onCatalogRefresh:()=>void ee(),onCatalogReload:o.reload,onModelToggled:te})})}):null,S?(0,I.jsx)(k,{title:`删除这个提供商?`,onClose:()=>{f||D(null)},footer:(0,I.jsxs)(I.Fragment,{children:[(0,I.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,disabled:!!f,onClick:()=>D(null),children:`取消(保留)`}),(0,I.jsxs)(`button`,{type:`button`,className:`btn btn-danger btn-sm`,disabled:!!f,onClick:()=>void Q(),children:[f?(0,I.jsx)(a,{size:14,className:`spin`}):null,f?`删除中…`:`确认删除`]})]}),children:(0,I.jsxs)(`div`,{className:`ov-lose`,children:[(0,I.jsx)(`b`,{children:`会失去什么:`}),`提供商「`,S.name,`」(`,S.baseUrl,`)的登记与它的凭证文件一并删除, 需要重新登记并填一次 ck_ 密钥。此操作不可撤销。`]})}):null]})}function J({provider:e,onClose:t,onSaved:i,onReimport:o}){let[s,c]=(0,F.useState)(e.name),[l,u]=(0,F.useState)(e.baseUrl),[d,f]=(0,F.useState)(!1),[g,_]=(0,F.useState)(!1),[v,y]=(0,F.useState)(null),[x,S]=(0,F.useState)(null),[C,w]=(0,F.useState)(!1),[T,D]=(0,F.useState)(null),O=async()=>{if(!C){w(!0),D(null),y(null);try{let t=await h.providerImportLocal(e.id);if(t.ok)D({kind:`ok`,text:t.message+(t.detail?`\n${t.detail}`:``)}),i();else if(t.outcome===`unavailable`){let t=e.protocol===`qoder`?`qoder`:`minimax`;D({kind:`unavailable`,text:`本服务所在机器上读不到客户端登录态(远程部署的正常情形)。`,command:`node tools/import-credential-push.mjs ${location.origin} ${t} --provider ${e.id}`})}else D({kind:`error`,text:t.message+(t.detail?`\n${t.detail}`:``)})}catch(e){y(e instanceof p?e:new p(String(e),0,0))}finally{w(!1)}}},k=async t=>{if(t.preventDefault(),!d){f(!0),y(null);try{let t={};s.trim()!==e.name&&(t.name=s.trim()),l.trim()!==e.baseUrl&&(t.baseUrl=l.trim()),Object.keys(t).length>0&&await h.patchProvider(e.id,t),i()}catch(e){y(e instanceof p?e:new p(String(e),0,0))}finally{f(!1)}}},A=async()=>{if(!g){_(!0),y(null),S(null);try{S(await h.testProvider(e.id,void 0))}catch(e){y(e instanceof p?e:new p(String(e),0,0))}finally{_(!1)}}};return(0,I.jsx)(N,{title:`配置提供商 · ${e.name}`,subtitle:e.baseUrl,onClose:t,children:(0,I.jsxs)(`div`,{className:`drawer-body`,children:[v?(0,I.jsx)(b,{tone:`error`,children:E(v)}):null,(0,I.jsxs)(`form`,{onSubmit:e=>void k(e),className:`prov-edit`,style:{marginTop:6},children:[(0,I.jsxs)(`div`,{className:`up-row`,children:[(0,I.jsx)(`div`,{className:`up-lab`,children:`名称`}),(0,I.jsx)(`input`,{className:`field-input text`,style:{width:`100%`},value:s,onChange:e=>c(e.target.value),"aria-label":`提供商名称`})]}),(0,I.jsxs)(`div`,{className:`up-row`,children:[(0,I.jsx)(`div`,{className:`up-lab`,children:`地址`}),(0,I.jsx)(`input`,{className:`field-input text`,style:{width:`100%`},value:l,placeholder:`https://api.example.com`,onChange:e=>u(e.target.value),"aria-label":`提供商地址`})]}),(0,I.jsxs)(`div`,{className:`up-row`,children:[(0,I.jsx)(`div`,{className:`up-lab`,children:`凭证`}),(0,I.jsxs)(`div`,{className:`cred-guide`,children:[(0,I.jsx)(`div`,{className:`cred-guide-note`,children:e.protocol===`openai`?(0,I.jsxs)(I.Fragment,{children:[`凭证来自 WorkBuddy `,(0,I.jsx)(`b`,{children:`官方 OAuth 授权`}),`(桌面端 / 插件同一条通道),无需手填。`]}):e.protocol===`anthropic`?(0,I.jsxs)(I.Fragment,{children:[`凭证是客户端登录态里的 `,(0,I.jsx)(`span`,{className:`code-chip`,children:`mmort_…`}),` refresh token。 请用`,(0,I.jsx)(`b`,{children:`专用账号`}),`(与桌面端共用会互相顶下线)。`]}):(0,I.jsxs)(I.Fragment,{children:[`凭证来自 Qoder 客户端登录态(一段`,(0,I.jsx)(`b`,{children:`身份 JSON`}),`)。桌面客户端与它`,(0,I.jsx)(`b`,{children:`互不影响`}),`。`]})}),(0,I.jsxs)(`div`,{className:`cred-guide-actions`,children:[(0,I.jsxs)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,disabled:d,onClick:()=>o(),children:[(0,I.jsx)(r,{size:14}),`重新导入授权`]}),(0,I.jsxs)(`button`,{type:`button`,className:`btn btn-quiet btn-sm`,disabled:C,onClick:()=>void O(),children:[C?(0,I.jsx)(a,{size:14}):(0,I.jsx)(r,{size:14}),`从本机客户端读取`]})]}),T?(0,I.jsx)(b,{tone:T.kind===`ok`?`success`:T.kind===`unavailable`?`warn`:`error`,children:(0,I.jsxs)(`span`,{children:[T.text,T.command?(0,I.jsxs)(I.Fragment,{children:[(0,I.jsx)(`br`,{}),`在`,(0,I.jsx)(`b`,{children:`装了客户端的那台机器`}),`的仓库目录下跑:`,(0,I.jsx)(R,{command:T.command})]}):null]})}):null,e.protocol===`qoder`?(0,I.jsxs)(`div`,{className:`cred-guide-note`,children:[`本服务`,(0,I.jsx)(`b`,{children:`不自动刷新`}),` Qoder 令牌;过期后重跑一次导入即可。`]}):null]})]}),(0,I.jsxs)(`div`,{className:`up-actions`,children:[(0,I.jsxs)(`button`,{type:`submit`,className:`btn btn-primary btn-sm`,disabled:d,children:[d?(0,I.jsx)(a,{size:14}):(0,I.jsx)(n,{size:14}),`保存`]}),(0,I.jsx)(m,{content:`用已导入的授权测这家上游是否还通`,children:(0,I.jsxs)(`button`,{type:`button`,className:`btn btn-quiet btn-sm`,disabled:g,onClick:()=>void A(),children:[g?(0,I.jsx)(a,{size:14}):null,`测试`]})})]})]}),x?(0,I.jsx)(b,{tone:x.ok?`success`:`warn`,children:(0,I.jsx)(`span`,{children:x.ok?(0,I.jsxs)(I.Fragment,{children:[`通了:`,e.name,` 接受了这次 1 token 探测(`,x.elapsedMs,`ms)。`]}):x.httpStatus===null?(0,I.jsxs)(I.Fragment,{children:[`没连上(网络层):`,x.reason??`未知原因`,`。先查地址与出网,再看凭证。`]}):(0,I.jsxs)(I.Fragment,{children:[`连上了但被拒(HTTP `,x.httpStatus,x.upstreamCode===null?``:` · 业务码 ${x.upstreamCode}`,`)。`,x.upstreamCode===11102?`业务码 11102 = 账号没订阅这个模型。key 可能是好的,换个模型再测或直接切换。`:`通常是凭证问题;换一把再测。`]})})}):null,(0,I.jsx)(`div`,{className:`up-note`,style:{marginTop:18},children:(0,I.jsxs)(`span`,{children:[`凭证只存在`,(0,I.jsx)(`b`,{children:`服务端`}),`:界面只显示账号摘要或掩码,任何接口都读不回完整值。`]})})]})})}function Y(){let[e,t]=(0,F.useState)(()=>M()??`today`);return(0,I.jsxs)(`div`,{className:`view on`,children:[(0,I.jsx)(D,{title:`提供商管理`,actions:(0,I.jsx)(O,{value:e,options:v,onChange:e=>{j(e),t(e)},width:148})}),(0,I.jsx)(`div`,{className:`view-body stack`,children:(0,I.jsx)(q,{window:e})})]})}export{Y as ClientsPage};
1
+ import{H as e,I as t,J as n,L as r,Q as i,S as a,U as o,a as s,it as c,m as l,n as u,tt as d,z as f}from"./vendor-befEYGHM.js";import{M as p,a as m,c as h,g,j as _,k as v,p as y,r as b,t as x,v as S}from"./index-DO57ipCG.js";import{a as C,c as w,n as T,r as E,s as D,t as O}from"./Select-CU6tc1OR.js";import{t as k}from"./Modal-B-rIrvXS.js";import{t as A}from"./PageSkeleton-DG-Dj4_9.js";import{n as j,t as M}from"./sharedWindow-FDfq75PI.js";import{t as N}from"./Drawer-CleQGhRU.js";import{t as P}from"./Switch-0KA1AbiV.js";var F=c(),I=d();function L({rows:e,byProvider:t,busyId:n,onEdit:r,onModels:a,onSignin:o,onGrowthRun:c,onOauthImport:d,onActivate:f,onDeactivate:p,onDelete:h}){let _=()=>new Intl.DateTimeFormat(`en-CA`,{timeZone:`Asia/Shanghai`,year:`numeric`,month:`2-digit`,day:`2-digit`}).format(new Date),v=new Map((t??[]).map(e=>[e.providerId??``,e]));return(0,I.jsx)(`div`,{className:`tbl-scroll`,children:(0,I.jsxs)(`table`,{className:`tbl`,children:[(0,I.jsxs)(`caption`,{className:`sr-only`,children:[`提供商列表,共 `,e.length,` 家提供商`]}),(0,I.jsx)(`thead`,{children:(0,I.jsxs)(`tr`,{children:[(0,I.jsx)(`th`,{scope:`col`,children:`提供商`}),(0,I.jsx)(`th`,{scope:`col`,children:`密钥`}),(0,I.jsx)(`th`,{scope:`col`,className:`r`,children:`请求次数`}),(0,I.jsx)(`th`,{scope:`col`,className:`r`,children:`消耗积分`}),(0,I.jsx)(`th`,{scope:`col`,className:`r`,children:`最后活跃`}),(0,I.jsx)(`th`,{scope:`col`,className:`r`,children:`操作`})]})}),(0,I.jsx)(`tbody`,{children:e.map(t=>{let b=n===t.id;return(0,I.jsxs)(`tr`,{className:`prov-row`,children:[(0,I.jsxs)(`td`,{className:`td-strong`,children:[t.name,t.active?(0,I.jsx)(w,{tone:`live`,children:`已启用`}):(0,I.jsx)(w,{tone:`ghost`,children:`未启用`}),t.protocol===`anthropic`?(0,I.jsx)(m,{content:`Anthropic Messages 方言(mavis 网关):模型目录用内置清单,凭证为 refresh token 自动换新`,children:(0,I.jsx)(`span`,{className:`pill pill-ghost`,children:`Anthropic`})}):t.protocol===`qoder`?(0,I.jsx)(m,{content:`Qoder CN 网关(COSY 协议):免费档 billable:false 实扣 0;目录从网关 /model/list 拉取;凭证为客户端身份 JSON(不自动刷新)`,children:(0,I.jsx)(`span`,{className:`pill pill-ghost`,children:`Qoder`})}):null,(0,I.jsx)(`div`,{className:`td-sub td-mono`,children:(0,I.jsx)(m,{content:(0,I.jsx)(I.Fragment,{children:(0,I.jsxs)(`div`,{className:`client-model-tip`,children:[(0,I.jsxs)(`div`,{className:`client-model-tip-head`,children:[(0,I.jsxs)(`div`,{className:`client-model-tip-title`,children:[(0,I.jsx)(`strong`,{children:t.name}),(0,I.jsx)(`span`,{children:t.baseUrl})]}),(0,I.jsx)(`span`,{className:`client-model-tip-tag`,children:t.active?`已启用`:`未启用`})]}),(0,I.jsxs)(`div`,{className:`client-model-tip-summary`,children:[(0,I.jsxs)(`div`,{children:[(0,I.jsx)(`span`,{children:`模型总数`}),(0,I.jsx)(`b`,{children:g(t.modelCount??0)})]}),(0,I.jsxs)(`div`,{children:[(0,I.jsx)(`span`,{children:`已启用`}),(0,I.jsx)(`b`,{children:g(t.modelEnabled??0)})]})]}),(()=>{let e=t.credits?.available?{remaining:t.credits.remaining,consumed:t.credits.consumed}:t.wbCredits?{remaining:t.wbCredits.remaining,consumed:t.wbCredits.consumed}:t.qoderCredits?{remaining:t.qoderCredits.remaining,consumed:t.qoderCredits.consumed}:null;return e&&(e.remaining!==null||e.consumed!==null)?(0,I.jsxs)(`div`,{className:`client-metric-tip-total`,children:[(0,I.jsx)(`span`,{children:`剩余积分`}),(0,I.jsxs)(`b`,{children:[y(e.remaining??0),` `,(0,I.jsxs)(`em`,{children:[`已用 `,y(e.consumed??0)]})]})]}):null})(),t.credits?.stale||t.qoderCredits?.stale?(0,I.jsx)(`div`,{className:`client-model-tip-note`,children:`⚠ 积分数据偏旧(账本快照超时)`}):null]})}),children:(0,I.jsxs)(`span`,{className:`prov-model-count`,children:[g(t.modelCount??0),` 个模型`]})})})]}),(0,I.jsxs)(`td`,{className:`td-cred`,children:[(0,I.jsx)(m,{content:`供应商访问密钥(掩码显示;全量只存在服务器上)`,children:(0,I.jsx)(`span`,{className:`cred-mask`,children:t.credentialMask})}),(0,I.jsx)(`span`,{className:`up-badge${t.configured?``:` warn`}`,children:t.configured?`有效`:`未配置`})]}),(0,I.jsx)(`td`,{className:`r td-mono`,children:t.active?g(v.get(t.id)?.requests??0):`—`}),(0,I.jsx)(`td`,{className:`r td-mono`,children:t.active?y(v.get(t.id)?.credits??0):`—`}),(0,I.jsx)(`td`,{className:`r td-mono`,children:t.active?S(v.get(t.id)?.lastSeenAt??null):`—`}),(0,I.jsx)(`td`,{className:`r`,children:(0,I.jsxs)(`span`,{className:`prov-row-actions`,children:[(0,I.jsx)(m,{content:`改名称与上游地址;凭证可在抽屉内「重新导入授权」`,children:(0,I.jsxs)(`button`,{type:`button`,className:`mini-pill`,disabled:b,onClick:()=>r(t),children:[(0,I.jsx)(u,{size:12}),`配置`]})}),(0,I.jsx)(m,{content:`查看这家自己的模型清单(启停只作用于该提供商分区)`,children:(0,I.jsxs)(`button`,{type:`button`,className:`mini-pill`,"aria-disabled":b||!t.active,onClick:()=>{!b&&t.active&&a(t)},children:[(0,I.jsx)(l,{size:12}),`模型列表`]})}),t.credKind===`oauth_minimax`||t.protocol===`qoder`?(0,I.jsx)(m,{content:(()=>{let e=t.protocol===`qoder`?`10:05`:`08:05`,n=t.signin?.checkedOn?`最近签到 ${t.signin.checkedOn}${t.signin.lastPoints?` · +${t.signin.lastPoints} 积分`:``}(relay 每天 ${e} 自动签)`:`今天还没签到 —— 点一下立即签(relay 每天 ${e} 也会自动签)`;return t.protocol===`qoder`?(0,I.jsxs)(I.Fragment,{children:[n,(0,I.jsx)(`br`,{}),`Qoder 官方标注:每日 10:00(UTC+8)刷新,领取后 30 天有效。`]}):n})(),children:(0,I.jsxs)(`button`,{type:`button`,className:`mini-pill`,"aria-disabled":b||t.signin?.checkedOn===_(),onClick:()=>{b||t.signin?.checkedOn===_()||o(t)},children:[(0,I.jsx)(i,{size:12}),t.signin?.checkedOn===_()?`已签到`:`签到`]})}):null,t.protocol===`openai`?t.wbOAuth?.configured?(0,I.jsx)(m,{content:t.wbSignin?.checkedOn===_()?`今天已签到过(明天 08:05 自动跑;任务面板在设置 → 提供商健康)`:`立即签到 + 顺带跑一轮成长任务与连登兑换(每天 08:05 也会自动跑)。转发不受影响。`,children:(0,I.jsxs)(`button`,{type:`button`,className:`mini-pill`,"aria-disabled":b||t.wbSignin?.checkedOn===_(),onClick:()=>{!b&&t.wbSignin?.checkedOn!==_()&&c(t)},children:[(0,I.jsx)(i,{size:12}),t.wbSignin?.checkedOn===_()?`已签到`:`签到`]})}):(0,I.jsx)(m,{content:`导入桌面端 OAuth 凭证后可代领签到/任务(上游不允许 API 密钥代领)。粘贴 tools/wb-oauth-export.mjs 的输出;转发仍走 API 密钥。`,children:(0,I.jsxs)(`button`,{type:`button`,className:`mini-pill`,"aria-disabled":b,onClick:()=>{b||d(t)},children:[(0,I.jsx)(i,{size:12}),`导入OAuth`]})}):null,t.active?(0,I.jsx)(m,{content:`退出出站路由(目录与凭证保留,可再启用)`,children:(0,I.jsx)(`button`,{type:`button`,className:`mini-pill`,"aria-disabled":b,onClick:()=>{b||p(t)},children:`停用`})}):(0,I.jsx)(`button`,{type:`button`,className:`mini-pill`,disabled:b,onClick:()=>f(t),children:`启用`}),(0,I.jsx)(m,{content:t.active?`不能删除已启用的提供商 —— 先停用它`:e.length<=1?`至少保留一个提供商`:`删除这个提供商(凭证文件一并删除)`,children:(0,I.jsxs)(`button`,{type:`button`,className:`mini-pill danger`,"aria-disabled":b||t.active||e.length<=1,onClick:()=>{!b&&!t.active&&e.length>1&&h(t)},children:[(0,I.jsx)(s,{size:12}),`删除`]})})]})})]},t.id)})})]})})}function R({command:e}){let[t,r]=(0,F.useState)(!1);if(!e)return null;let i=async()=>{try{await navigator.clipboard.writeText(e),r(!0),setTimeout(()=>r(!1),2e3)}catch{r(!1)}};return(0,I.jsxs)(`div`,{className:`cred-guide-cmd`,children:[(0,I.jsx)(`code`,{children:e}),(0,I.jsxs)(`button`,{type:`button`,className:`copy`,"aria-label":t?`已复制命令`:`复制命令`,onClick:()=>void i(),children:[t?(0,I.jsx)(n,{size:13}):(0,I.jsx)(f,{size:13}),t?`已复制`:`复制`]})]})}var z=[{protocol:`openai`,label:`WorkBuddy`,desc:`OpenAI 方言`,needsAuth:!0},{protocol:`anthropic`,label:`MiniMax Code`,desc:`Anthropic 方言`,needsAuth:!0},{protocol:`qoder`,label:`Qoder CN`,desc:`COSY 协议`,needsAuth:!0}],B={openai:`https://copilot.tencent.com`,anthropic:`https://agent.minimax.cn`,qoder:`https://gateway.qoder.com.cn`},V=2500,H=3e5;function U({provider:e,onClose:n,onImported:r}){let i=x(),[o,s]=(0,F.useState)(e?`confirm`:`pick`),[c,l]=(0,F.useState)(e?.protocol??`anthropic`),[u,d]=(0,F.useState)(e?.name??``),[f,m]=(0,F.useState)(``),[g,_]=(0,F.useState)(``),[v,y]=(0,F.useState)(!1),[S,C]=(0,F.useState)(null),w=(0,F.useRef)(e?.id??null),T=(0,F.useRef)(!1),D=(0,F.useRef)(!1),O=z.find(e=>e.protocol===c);(0,F.useEffect)(()=>()=>{D.current=!0;let e=w.current;e&&h.providerImportDeviceCancel(e).catch(()=>void 0)},[]);let A=(0,F.useCallback)(async()=>{y(!0),C(null);try{let e=w.current;e||(e=(await h.createProvider({name:u.trim()||O.label,baseUrl:B[c]??B.openai,credential:``,protocol:c})).item.id,w.current=e,T.current=!0);let t=await h.providerImportDeviceStart(e);if(!t.ok||!t.authUrl){C(new p(t.message??`申请授权失败`,0,0)),s(`confirm`),y(!1);return}m(t.authUrl),_(t.userCode??``),window.open(t.authUrl,`_blank`,`noopener`),s(`waiting`),y(!1);let n=Date.now()+H;for(;!D.current&&Date.now()<n;){if(await new Promise(e=>setTimeout(e,V)),D.current)return;let t=await h.providerImportDevicePoll(e);if(D.current)return;if(t.status===`done`){s(`done`),i.push({tone:`success`,message:`${u.trim()||O.label} 授权已导入。`}),r();return}if(t.status===`error`){C(new p(t.message??`授权失败`,0,0)),s(`confirm`);return}}D.current||(C(new p(`授权超时(5 分钟)—— 请重新发起导入。`,0,0)),s(`confirm`))}catch(e){C(e instanceof p?e:new p(String(e),0,0)),s(`confirm`),y(!1)}},[O.label,u,r,c,i]);if(o===`done`)return(0,I.jsxs)(k,{title:`授权已导入`,onClose:n,footer:(0,I.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:n,children:`完成`}),children:[(0,I.jsx)(b,{tone:`success`,children:(0,I.jsxs)(`span`,{children:[u.trim()||O.label,` 的授权已通过官方通道导入。`]})}),e?null:(0,I.jsxs)(`p`,{className:`ov-sub`,style:{marginTop:12},children:[`回到列表点`,(0,I.jsx)(`b`,{children:`试通`}),`确认可用,再按需启用(启用后它才参与出站路由)。`]})]});if(o===`waiting`)return(0,I.jsx)(k,{title:`等待你完成授权`,onClose:n,children:(0,I.jsxs)(`div`,{className:`cred-guide`,children:[(0,I.jsxs)(`div`,{className:`cred-guide-note`,children:[`已为你打开 `,(0,I.jsx)(`b`,{children:O.label}),` 的授权页 —— `,(0,I.jsx)(`b`,{children:`请在浏览器里完成授权`}),`, 本窗口会自动检测结果,无需回到终端。`]}),g?(0,I.jsxs)(`div`,{className:`cred-guide-note`,children:[`若页面要求输入短码:`,(0,I.jsx)(`span`,{className:`code-chip`,children:g})]}):null,(0,I.jsx)(R,{command:f}),(0,I.jsxs)(`div`,{className:`cred-guide-note`,style:{display:`flex`,alignItems:`center`,gap:8},children:[(0,I.jsx)(a,{size:14}),`正在等待授权完成…(最多 5 分钟;没弹出浏览器就点上面的链接)`]}),S?(0,I.jsx)(b,{tone:`error`,children:E(S)}):null]})});let j=o===`confirm`;return(0,I.jsx)(k,{title:e?`重新导入授权 · ${e.name}`:`导入客户端`,onClose:n,footer:(0,I.jsxs)(I.Fragment,{children:[(0,I.jsx)(`button`,{type:`button`,className:`btn btn-quiet btn-sm`,onClick:n,children:`取消`}),(0,I.jsxs)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,disabled:v,onClick:()=>{if(!j){s(`confirm`);return}A()},children:[v?(0,I.jsx)(a,{size:14}):(0,I.jsx)(t,{size:14}),v?`正在申请授权…`:j?`确定,前往授权页`:`导入`]})]}),children:j?(0,I.jsxs)(I.Fragment,{children:[(0,I.jsx)(b,{tone:`warn`,children:(0,I.jsxs)(`span`,{children:[`即将跳转至 `,(0,I.jsx)(`b`,{children:O.label}),` 的授权页面(`,(0,I.jsx)(`b`,{children:`新标签页`}),`)。 请在页面里登录并完成授权,授权完成后本窗口会自动检测。`]})}),(0,I.jsxs)(`div`,{className:`cred-guide-note`,style:{marginTop:12},children:[`授权只用于取本工具的凭证;令牌保存在`,(0,I.jsx)(`b`,{children:`本机服务端`}),`(0600),不会回传到浏览器。`]}),c===`anthropic`?(0,I.jsxs)(`div`,{className:`cred-guide-note`,style:{marginTop:8},children:[`⚠️ MiniMax 的凭证是`,(0,I.jsx)(`b`,{children:`同账号单会话`}),`:请用`,(0,I.jsx)(`b`,{children:`专用账号`}),`, 否则会与日常客户端互相顶下线。`]}):null,S?(0,I.jsx)(b,{tone:`error`,children:E(S)}):null]}):(0,I.jsxs)(I.Fragment,{children:[(0,I.jsxs)(`p`,{className:`ov-sub`,children:[`选一个客户端,用它的`,(0,I.jsx)(`b`,{children:`官方授权`}),`拿到凭证(凭证在客户端登录态里,用户拿不到、无需手填)。`]}),(0,I.jsxs)(`div`,{className:`ov-field`,children:[(0,I.jsx)(`label`,{children:`客户端类型`}),(0,I.jsx)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:6},children:z.map(e=>(0,I.jsxs)(`label`,{style:{display:`flex`,alignItems:`center`,gap:8,cursor:`pointer`},children:[(0,I.jsx)(`input`,{type:`radio`,name:`import-client`,checked:c===e.protocol,onChange:()=>l(e.protocol)}),(0,I.jsxs)(`span`,{children:[(0,I.jsx)(`b`,{children:e.label}),(0,I.jsx)(`span`,{className:`ov-hint`,style:{marginLeft:8},children:e.desc})]})]},e.protocol))})]}),(0,I.jsxs)(`div`,{className:`ov-field`,children:[(0,I.jsx)(`label`,{htmlFor:`imp-name`,children:`名称(可留空)`}),(0,I.jsx)(`input`,{id:`imp-name`,className:`field-input text`,style:{width:`100%`},value:u,placeholder:O.label,onChange:e=>d(e.target.value)})]})]})})}var W={upstream:`上游`,seed:`种子`,manual:`手工`},G={upstream:`从上游模型目录拉取到的`,seed:`来自本地种子清单(上游拉取失败时的回落)`,manual:`人工添加的`};function K({catalog:t,catalogLoading:n,catalogMsg:r,catalogError:i,onCatalogRefresh:s,onCatalogReload:c,onModelToggled:u}){let[d,f]=(0,F.useState)(null),p=async e=>{f(e.id);try{await u(e.id,!e.enabled)}finally{f(null)}};return(0,I.jsxs)(`div`,{className:`up-col`,children:[(0,I.jsxs)(`div`,{className:`up-models-head`,children:[(0,I.jsxs)(`div`,{children:[(0,I.jsx)(`b`,{children:`可用模型`}),(0,I.jsx)(`span`,{children:t?`${t.models.length} 个 · ${t.models.filter(e=>e.enabled).length} 个已启用`:`尚未获取`})]}),(0,I.jsx)(`div`,{className:`up-models-actions`,children:n?(0,I.jsxs)(`button`,{type:`button`,className:`btn btn-quiet btn-sm`,disabled:!0,children:[(0,I.jsx)(a,{size:14,className:`spin`,style:{animation:`spin 1s linear infinite`}}),`正在实测模型…`]}):(0,I.jsxs)(`button`,{type:`button`,className:`btn btn-quiet btn-sm`,onClick:s,children:[(0,I.jsx)(l,{size:14}),`获取模型`]})})]}),n?(0,I.jsxs)(`div`,{className:`up-progress`,role:`status`,children:[(0,I.jsx)(a,{size:14,className:`spin`,style:{animation:`spin 1s linear infinite`},"aria-hidden":!0}),(0,I.jsxs)(`span`,{children:[`正在逐个实测模型(每个 1 token)…这一步会真打上游,`,(0,I.jsx)(`b`,{children:`可能要几十秒`}),`,请不要关掉页面; 完成后结果会出现在这里,模型清单也会自动刷新。`]})]}):null,r?(0,I.jsx)(b,{tone:r.kind===`error`?`error`:r.kind===`partial`?`warn`:`success`,children:r.text}):null,i?(0,I.jsx)(T,{kind:`error`,title:`模型清单没拉到`,detail:E(i),onRetry:c}):n&&!t?(0,I.jsx)(T,{kind:`loading`,title:`正在向上游获取模型…`}):!t||t.models.length===0?(0,I.jsx)(T,{kind:`empty`,title:`还没有模型清单`,detail:`启用提供商后会自动同步一次它自己的模型清单;这里也可以手动点「获取模型」逐项实测是否真的能调通。`}):(0,I.jsx)(`div`,{className:`up-models`,children:t.models.map(t=>(0,I.jsxs)(`div`,{className:`up-model${t.enabled?``:` off`}`,children:[(0,I.jsxs)(`div`,{className:`um-info`,children:[(0,I.jsxs)(`b`,{children:[(0,I.jsx)(`span`,{className:`um-k`,children:`模型名:`}),t.displayName,(0,I.jsx)(m,{content:G[t.source],children:(0,I.jsx)(`span`,{className:`um-src src-${t.source}`,children:W[t.source]})}),t.tag?(0,I.jsx)(m,{content:`上游计价标签,随目录同步更新`,children:(0,I.jsx)(`span`,{className:`mu-tag`,children:t.tag})}):null]}),(0,I.jsxs)(`i`,{className:`code-chip`,children:[(0,I.jsx)(`span`,{className:`um-k`,children:`模型ID:`}),t.id]})]}),(0,I.jsxs)(`div`,{className:`um-meta`,children:[t.verified?(0,I.jsx)(m,{content:`实测可调通`,children:(0,I.jsxs)(`span`,{className:`um-ok`,children:[(0,I.jsx)(o,{size:12}),`可用`]})}):(0,I.jsx)(m,{content:`实测未通过(可能未订阅)`,children:(0,I.jsxs)(`span`,{className:`um-bad`,children:[(0,I.jsx)(e,{size:12}),`未通过`]})}),t.multiplier==null?(0,I.jsx)(m,{content:`接口与本地快照都没有该模型的倍率 —— 不编造`,children:(0,I.jsx)(`span`,{className:`um-mult unknown`,children:`倍率未知`})}):(0,I.jsx)(m,{content:t.source===`upstream`?`倍率取自上游 /v3/config 接口实时值,随目录刷新与每日同步更新。`:`接口未覆盖该模型,倍率为内置快照,可能随官方调价过期。`,children:(0,I.jsxs)(`span`,{className:`um-mult`,children:[`×`,t.multiplier]})}),(0,I.jsx)(P,{checked:t.enabled,disabled:d===t.id,label:`${t.enabled?`停用`:`启用`} ${t.displayName}`,onChange:()=>void p(t)})]})]},t.id))}),(0,I.jsxs)(`div`,{className:`up-foot`,children:[`实测会真打上游、消耗少量积分(每模型 1 token)。上游不接受的模型会标「未通过」, 但`,(0,I.jsx)(`b`,{children:`不删除`}),` —— 订阅以后可能又开,随时可以再拉一次。 倍率以 /v3/config 接口实时值为准(目录刷新与每日自动同步都会更新), 接口未覆盖的模型回落内置快照。`,(0,I.jsx)(`br`,{}),`启用提供商时会同步这家自己的清单;已有提供商的目录互不覆盖,启用/停用选择也分别保留。`]})]})}function q({window:e}){let t=_(()=>h.providers(),[],{cacheKey:`providers`}),[n,i]=(0,F.useState)(null),o=_(()=>h.catalog(n?.id),[n?.id],{cacheKey:n?.id?`catalog:${n.id}`:`catalog`}),s=_(()=>h.clients(e),[e],{cacheKey:`clients:${e}`}),[c,l]=(0,F.useState)(!1),[u,d]=(0,F.useState)(null),[f,g]=(0,F.useState)(null),[v,y]=(0,F.useState)(null),[S,D]=(0,F.useState)(null),[O,j]=(0,F.useState)(null),[M,P]=(0,F.useState)(null),[R,z]=(0,F.useState)(!1),B=x(),V=t.data?.items??[],H=s.data?.byProvider??[],W=()=>{t.reload(),o.reload(),s.reload()},G=async e=>{g(e.id),j(null);try{await h.activateProvider(e.id),P(null),W()}catch(e){j(e instanceof p?e:new p(String(e),0,0))}finally{g(null)}},q=async e=>{g(e.id),j(null);try{await h.deactivateProvider(e.id),W()}catch(e){j(e instanceof p?e:new p(String(e),0,0))}finally{g(null)}},Y=async e=>{g(e.id),j(null);try{let n=await h.providerSigninNow(e.id);P({kind:n.ok?`ok`:`error`,text:n.message}),n.ok?B.push(n.outcome===`none`?{tone:`info`,message:`${e.name}:${n.message}`}:{tone:`success`,message:`${e.name} 签到成功${n.points?` · +${n.points} 积分`:``}。`}):B.push({tone:`error`,message:`${e.name} 签到失败:${n.message}`}),t.reload()}catch(t){let n=t instanceof p?t:new p(String(t),0,0);j(n),B.push({tone:`error`,message:`${e.name} 签到失败:${n.message||`请求未完成`}`})}finally{g(null)}},X=async e=>{g(e.id),j(null);try{let n=await h.providerGrowthRun(e.id),r=n.tasks?.filter(e=>e.outcome===`claimed`).length??0,i=n.tasks?.filter(e=>e.outcome===`already`).length??0,a=n.tasks?.filter(e=>e.outcome===`skipped`).length??0,o=n.ok?`领取完成:签到 ${n.signin?.points??0} 积分;任务新领 ${r}、已领过 ${i}、待完成 ${a};本轮合计 +${n.creditsGained??0}。`:n.message??`领取失败。`;P({kind:n.ok?`ok`:`error`,text:o}),B.push(n.ok?{tone:`success`,message:`${e.name}:${o}`}:{tone:`error`,message:`${e.name} 领取失败:${n.message??`未知原因`}`}),t.reload()}catch(t){let n=t instanceof p?t:new p(String(t),0,0);j(n),B.push({tone:`error`,message:`${e.name} 领取失败:${n.message||`请求未完成`}`})}finally{g(null)}},Z=async e=>{let n=globalThis.prompt?.(`粘贴 WorkBuddy OAuth 凭证 JSON(tools/wb-oauth-export.mjs 的输出):`,``)??null;if(!n)return;let r;try{r=JSON.parse(n)}catch{P({kind:`error`,text:`不是合法 JSON —— 请完整复制导出文件内容。`});return}g(e.id),j(null);try{let n=await h.providerOauthImport(e.id,r);P({kind:n.ok?`ok`:`error`,text:n.message}),t.reload()}catch(e){j(e instanceof p?e:new p(String(e),0,0))}finally{g(null)}},Q=async()=>{if(!S||f)return;let e=S;g(e.id),j(null);try{await h.deleteProvider(e.id),D(null),W()}catch(e){j(e instanceof p?e:new p(String(e),0,0)),D(null)}finally{g(null)}},ee=async()=>{if(!R){z(!0),P(null),j(null);try{let e=await h.refreshCatalog(),r=e.configPulled===!1?` 倍率未从接口取到,沿用本地快照。`:e.multipliersUpdated?` 倍率已按接口同步(${e.multipliersUpdated} 项有更新)。`:``,i=e.perProvider?.find(e=>e.id===n?.id),a=i?.checked??e.checked,s=i?.ok??e.ok,c=i?.failed.length??e.failed.length;P(c>0?{kind:`partial`,text:`已实测 ${a} 个模型:${s} 个可用、${c} 个未通过(可能未订阅)。未通过的不会删除,订阅后可再拉一次。${r}`}:{kind:`ok`,text:`已实测 ${a} 个模型,全部可用。${r}`}),o.reload(),t.reload()}catch(e){let t=e instanceof p?e:new p(String(e),0,0);P({kind:`error`,text:`获取模型失败:${E(t)}`})}finally{z(!1)}}},te=async(e,t)=>{j(null);try{await h.setModelEnabled(e,t,n?.id),o.reload()}catch(e){j(e instanceof p?e:new p(String(e),0,0))}},$=V.filter(e=>e.active).length;return(0,I.jsxs)(I.Fragment,{children:[O?(0,I.jsx)(b,{tone:`error`,children:E(O)}):null,s.error?(0,I.jsxs)(b,{tone:`error`,actions:(0,I.jsx)(`button`,{type:`button`,className:`btn btn-quiet btn-sm`,onClick:s.reload,children:`重试`}),children:[`提供商用量没取到:`,E(s.error)]}):null,(0,I.jsxs)(C,{title:`提供商`,aside:(0,I.jsxs)(`span`,{className:`panel-aside-row`,children:[$>0?(0,I.jsxs)(w,{tone:`live`,dot:!0,children:[`已启用 `,$,` 家`]}):null,(0,I.jsx)(m,{content:`选一个客户端(WorkBuddy / MiniMax Code / Qoder CN),用它的官方授权导入凭证`,children:(0,I.jsxs)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:()=>l(!0),children:[(0,I.jsx)(r,{size:14}),`导入客户端`]})})]}),padded:!1,children:[t.initial&&t.loading?(0,I.jsx)(`div`,{style:{padding:`0 20px 18px`},children:(0,I.jsx)(A,{})}):t.error?(0,I.jsx)(`div`,{style:{padding:`0 20px 18px`},children:(0,I.jsx)(T,{kind:`error`,title:`提供商列表没取到`,detail:E(t.error),onRetry:t.reload})}):V.length===0?(0,I.jsx)(`div`,{style:{padding:`0 20px 18px`},children:(0,I.jsx)(T,{kind:`empty`,title:`还没有任何提供商`,detail:`先登记一个上游,再按需启用它的出站路由。`})}):(0,I.jsx)(L,{rows:V,byProvider:H,busyId:f,onEdit:y,onSignin:e=>void Y(e),onGrowthRun:e=>void X(e),onOauthImport:e=>void Z(e),onModels:e=>{P(null),i(e)},onActivate:e=>void G(e),onDeactivate:e=>void q(e),onDelete:D}),(0,I.jsxs)(`div`,{className:`footer-note`,style:{padding:`0 20px 18px`},children:[`启用提供商会让它加入出站路由,并同步它自己的模型清单;已启用的提供商可以并行生效,模型按各自目录路由。 三家凭证都来自各客户端的`,(0,I.jsx)(`b`,{children:`官方授权`}),`(用户无需也无法手填)——点「重新导入授权」即可换账号或刷新令牌; 令牌只存在服务端,任何接口都读不回完整值。`,(0,I.jsx)(`br`,{}),`行内三列用量按`,(0,I.jsx)(`b`,{children:`提供商`}),`归属:由「模型 id → 生效目录的提供商」解析(请求记录本身没有 provider 维度)。模型不属于任何生效目录的行归到「未归属」,不冒充某一家。`]})]}),c?(0,I.jsx)(U,{onClose:()=>{l(!1),W()},onImported:()=>W()}):null,u?(0,I.jsx)(U,{provider:{id:u.id,name:u.name,protocol:u.protocol},onClose:()=>{d(null),W()},onImported:()=>W()}):null,v?(0,I.jsx)(J,{provider:v,onClose:()=>y(null),onSaved:()=>{y(null),W()},onReimport:()=>{let e=v;y(null),e&&d(e)}}):null,n?(0,I.jsx)(N,{title:`模型列表 · ${n.name}`,subtitle:n.baseUrl,onClose:()=>i(null),children:(0,I.jsx)(`div`,{className:`drawer-body`,children:(0,I.jsx)(K,{catalog:o.data,catalogLoading:R,catalogMsg:M,catalogError:o.error,onCatalogRefresh:()=>void ee(),onCatalogReload:o.reload,onModelToggled:te})})}):null,S?(0,I.jsx)(k,{title:`删除这个提供商?`,onClose:()=>{f||D(null)},footer:(0,I.jsxs)(I.Fragment,{children:[(0,I.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,disabled:!!f,onClick:()=>D(null),children:`取消(保留)`}),(0,I.jsxs)(`button`,{type:`button`,className:`btn btn-danger btn-sm`,disabled:!!f,onClick:()=>void Q(),children:[f?(0,I.jsx)(a,{size:14,className:`spin`}):null,f?`删除中…`:`确认删除`]})]}),children:(0,I.jsxs)(`div`,{className:`ov-lose`,children:[(0,I.jsx)(`b`,{children:`会失去什么:`}),`提供商「`,S.name,`」(`,S.baseUrl,`)的登记与它的凭证文件一并删除, 需要重新登记并填一次 ck_ 密钥。此操作不可撤销。`]})}):null]})}function J({provider:e,onClose:t,onSaved:i,onReimport:o}){let[s,c]=(0,F.useState)(e.name),[l,u]=(0,F.useState)(e.baseUrl),[d,f]=(0,F.useState)(!1),[g,_]=(0,F.useState)(!1),[v,y]=(0,F.useState)(null),[x,S]=(0,F.useState)(null),[C,w]=(0,F.useState)(!1),[T,D]=(0,F.useState)(null),O=async()=>{if(!C){w(!0),D(null),y(null);try{let t=await h.providerImportLocal(e.id);if(t.ok)D({kind:`ok`,text:t.message+(t.detail?`\n${t.detail}`:``)}),i();else if(t.outcome===`unavailable`){let t=e.protocol===`qoder`?`qoder`:`minimax`;D({kind:`unavailable`,text:`本服务所在机器上读不到客户端登录态(远程部署的正常情形)。`,command:`node tools/import-credential-push.mjs ${location.origin} ${t} --provider ${e.id}`})}else D({kind:`error`,text:t.message+(t.detail?`\n${t.detail}`:``)})}catch(e){y(e instanceof p?e:new p(String(e),0,0))}finally{w(!1)}}},k=async t=>{if(t.preventDefault(),!d){f(!0),y(null);try{let t={};s.trim()!==e.name&&(t.name=s.trim()),l.trim()!==e.baseUrl&&(t.baseUrl=l.trim()),Object.keys(t).length>0&&await h.patchProvider(e.id,t),i()}catch(e){y(e instanceof p?e:new p(String(e),0,0))}finally{f(!1)}}},A=async()=>{if(!g){_(!0),y(null),S(null);try{S(await h.testProvider(e.id,void 0))}catch(e){y(e instanceof p?e:new p(String(e),0,0))}finally{_(!1)}}};return(0,I.jsx)(N,{title:`配置提供商 · ${e.name}`,subtitle:e.baseUrl,onClose:t,children:(0,I.jsxs)(`div`,{className:`drawer-body`,children:[v?(0,I.jsx)(b,{tone:`error`,children:E(v)}):null,(0,I.jsxs)(`form`,{onSubmit:e=>void k(e),className:`prov-edit`,style:{marginTop:6},children:[(0,I.jsxs)(`div`,{className:`up-row`,children:[(0,I.jsx)(`div`,{className:`up-lab`,children:`名称`}),(0,I.jsx)(`input`,{className:`field-input text`,style:{width:`100%`},value:s,onChange:e=>c(e.target.value),"aria-label":`提供商名称`})]}),(0,I.jsxs)(`div`,{className:`up-row`,children:[(0,I.jsx)(`div`,{className:`up-lab`,children:`地址`}),(0,I.jsx)(`input`,{className:`field-input text`,style:{width:`100%`},value:l,placeholder:`https://api.example.com`,onChange:e=>u(e.target.value),"aria-label":`提供商地址`})]}),(0,I.jsxs)(`div`,{className:`up-row`,children:[(0,I.jsx)(`div`,{className:`up-lab`,children:`凭证`}),(0,I.jsxs)(`div`,{className:`cred-guide`,children:[(0,I.jsx)(`div`,{className:`cred-guide-note`,children:e.protocol===`openai`?(0,I.jsxs)(I.Fragment,{children:[`凭证来自 WorkBuddy `,(0,I.jsx)(`b`,{children:`官方 OAuth 授权`}),`(桌面端 / 插件同一条通道),无需手填。`]}):e.protocol===`anthropic`?(0,I.jsxs)(I.Fragment,{children:[`凭证是客户端登录态里的 `,(0,I.jsx)(`span`,{className:`code-chip`,children:`mmort_…`}),` refresh token。 请用`,(0,I.jsx)(`b`,{children:`专用账号`}),`(与桌面端共用会互相顶下线)。`]}):(0,I.jsxs)(I.Fragment,{children:[`凭证来自 Qoder 客户端登录态(一段`,(0,I.jsx)(`b`,{children:`身份 JSON`}),`)。桌面客户端与它`,(0,I.jsx)(`b`,{children:`互不影响`}),`。`]})}),(0,I.jsxs)(`div`,{className:`cred-guide-actions`,children:[(0,I.jsxs)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,disabled:d,onClick:()=>o(),children:[(0,I.jsx)(r,{size:14}),`重新导入授权`]}),(0,I.jsxs)(`button`,{type:`button`,className:`btn btn-quiet btn-sm`,disabled:C,onClick:()=>void O(),children:[C?(0,I.jsx)(a,{size:14}):(0,I.jsx)(r,{size:14}),`从本机客户端读取`]})]}),T?(0,I.jsx)(b,{tone:T.kind===`ok`?`success`:T.kind===`unavailable`?`warn`:`error`,children:(0,I.jsxs)(`span`,{children:[T.text,T.command?(0,I.jsxs)(I.Fragment,{children:[(0,I.jsx)(`br`,{}),`在`,(0,I.jsx)(`b`,{children:`装了客户端的那台机器`}),`的仓库目录下跑:`,(0,I.jsx)(R,{command:T.command})]}):null]})}):null,e.protocol===`qoder`?(0,I.jsxs)(`div`,{className:`cred-guide-note`,children:[`本服务`,(0,I.jsx)(`b`,{children:`不自动刷新`}),` Qoder 令牌;过期后重跑一次导入即可。`]}):null]})]}),(0,I.jsxs)(`div`,{className:`up-actions`,children:[(0,I.jsxs)(`button`,{type:`submit`,className:`btn btn-primary btn-sm`,disabled:d,children:[d?(0,I.jsx)(a,{size:14}):(0,I.jsx)(n,{size:14}),`保存`]}),(0,I.jsx)(m,{content:`用已导入的授权测这家上游是否还通`,children:(0,I.jsxs)(`button`,{type:`button`,className:`btn btn-quiet btn-sm`,disabled:g,onClick:()=>void A(),children:[g?(0,I.jsx)(a,{size:14}):null,`测试`]})})]})]}),x?(0,I.jsx)(b,{tone:x.ok?`success`:`warn`,children:(0,I.jsx)(`span`,{children:x.ok?(0,I.jsxs)(I.Fragment,{children:[`通了:`,e.name,` 接受了这次 1 token 探测(`,x.elapsedMs,`ms)。`]}):x.httpStatus===null?(0,I.jsxs)(I.Fragment,{children:[`没连上(网络层):`,x.reason??`未知原因`,`。先查地址与出网,再看凭证。`]}):(0,I.jsxs)(I.Fragment,{children:[`连上了但被拒(HTTP `,x.httpStatus,x.upstreamCode===null?``:` · 业务码 ${x.upstreamCode}`,`)。`,x.upstreamCode===11102?`业务码 11102 = 账号没订阅这个模型。key 可能是好的,换个模型再测或直接切换。`:`通常是凭证问题;换一把再测。`]})})}):null,(0,I.jsx)(`div`,{className:`up-note`,style:{marginTop:18},children:(0,I.jsxs)(`span`,{children:[`凭证只存在`,(0,I.jsx)(`b`,{children:`服务端`}),`:界面只显示账号摘要或掩码,任何接口都读不回完整值。`]})})]})})}function Y(){let[e,t]=(0,F.useState)(()=>M()??`today`);return(0,I.jsxs)(`div`,{className:`view on`,children:[(0,I.jsx)(D,{title:`提供商管理`,actions:(0,I.jsx)(O,{value:e,options:v,onChange:e=>{j(e),t(e)},width:148})}),(0,I.jsx)(`div`,{className:`view-body stack`,children:(0,I.jsx)(q,{window:e})})]})}export{Y as ClientsPage};
@@ -1,4 +1,4 @@
1
- import{b as e,it as t,t as n,tt as r}from"./vendor-befEYGHM.js";import{c as i,i as a,n as o,o as s,s as c,x as l}from"./index-BcWmrhMD.js";import{t as u}from"./accessUrl-DbL5DLDz.js";var d=t(),f=`1.6.18`,p={totalTokens:193e4,cacheHitRate:62.4,requestsToday:1284,successRate:99.7};function m(e,t=0){let n=e.toFixed(t).split(`.`);return n[0]=n[0].replace(/\B(?=(\d{3})+(?!\d))/g,`,`),n.join(`.`)}function h(e,t=1){return e==null?`—`:`${e.toFixed(t)}%`}function g(e){if(e&&typeof e==`object`){let t=e;return{ok:t.ok!==!1,version:typeof t.version==`string`?t.version:void 0}}return{ok:!0}}function _(){let e=c(),[t,n]=(0,d.useState)(!0),[r,a]=(0,d.useState)(f),[o,s]=(0,d.useState)(p),[l,u]=(0,d.useState)(!1);return(0,d.useEffect)(()=>{let e=!0,t=async()=>{try{let t=await fetch(`/healthz`,{headers:{Accept:`application/json`}});if(!t.ok)throw Error(`healthz ${t.status}`);let r=g(await t.json());if(!e)return;n(r.ok),r.version&&a(r.version)}catch{e&&n(!1)}};t();let r=window.setInterval(()=>void t(),3e4);return()=>{e=!1,window.clearInterval(r)}},[]),(0,d.useEffect)(()=>{if(e.status!==`authed`){u(!1),s(p);return}let t=!0;return i.overview(`24h`).then(e=>{t&&(s({totalTokens:e.metrics.totalTokens,cacheHitRate:e.metrics.cacheHitRate*100,requestsToday:e.metrics.requests,successRate:e.metrics.successRate*100}),u(!0))}).catch(()=>{t&&(u(!1),s(p))}),()=>{t=!1}},[e.status]),{online:t,version:r,real:l,numbers:o}}function v(){var e=window.matchMedia(`(prefers-reduced-motion: reduce)`).matches,t=!0,n=[],r=[],i={totalTokens:`本窗口 tokens`,cacheHitRate:`缓存命中率`,requestsToday:`今日请求`,successRate:`今日成功率`};function a(){var e=window.RelayDemo;if(e&&e.summary){var t=[];[].forEach.call(document.querySelectorAll(`[data-demo]`),function(n){var r=n.getAttribute(`data-demo`),a=e.summary[r];if(typeof a==`number`){var o=parseInt(n.getAttribute(`data-dec`)||`0`,10),s=n.textContent.trim().replace(/,/g,``);s!==``&&Math.abs(parseFloat(s)-a)>10**-o/2&&t.push((i[r]||r)+`:页面上写的 `+s+`,数据层是 `+a),n.setAttribute(`data-count`,a),n.textContent=p(a,o)}}),t.length&&window.console&&console.warn(`[page] 首页演示数字与 demo-data 不一致(已按数据层显示):
1
+ import{b as e,it as t,t as n,tt as r}from"./vendor-befEYGHM.js";import{c as i,i as a,n as o,o as s,s as c,x as l}from"./index-DO57ipCG.js";import{t as u}from"./accessUrl-DbL5DLDz.js";var d=t(),f=`1.6.20`,p={totalTokens:193e4,cacheHitRate:62.4,requestsToday:1284,successRate:99.7};function m(e,t=0){let n=e.toFixed(t).split(`.`);return n[0]=n[0].replace(/\B(?=(\d{3})+(?!\d))/g,`,`),n.join(`.`)}function h(e,t=1){return e==null?`—`:`${e.toFixed(t)}%`}function g(e){if(e&&typeof e==`object`){let t=e;return{ok:t.ok!==!1,version:typeof t.version==`string`?t.version:void 0}}return{ok:!0}}function _(){let e=c(),[t,n]=(0,d.useState)(!0),[r,a]=(0,d.useState)(f),[o,s]=(0,d.useState)(p),[l,u]=(0,d.useState)(!1);return(0,d.useEffect)(()=>{let e=!0,t=async()=>{try{let t=await fetch(`/healthz`,{headers:{Accept:`application/json`}});if(!t.ok)throw Error(`healthz ${t.status}`);let r=g(await t.json());if(!e)return;n(r.ok),r.version&&a(r.version)}catch{e&&n(!1)}};t();let r=window.setInterval(()=>void t(),3e4);return()=>{e=!1,window.clearInterval(r)}},[]),(0,d.useEffect)(()=>{if(e.status!==`authed`){u(!1),s(p);return}let t=!0;return i.overview(`24h`).then(e=>{t&&(s({totalTokens:e.metrics.totalTokens,cacheHitRate:e.metrics.cacheHitRate*100,requestsToday:e.metrics.requests,successRate:e.metrics.successRate*100}),u(!0))}).catch(()=>{t&&(u(!1),s(p))}),()=>{t=!1}},[e.status]),{online:t,version:r,real:l,numbers:o}}function v(){var e=window.matchMedia(`(prefers-reduced-motion: reduce)`).matches,t=!0,n=[],r=[],i={totalTokens:`本窗口 tokens`,cacheHitRate:`缓存命中率`,requestsToday:`今日请求`,successRate:`今日成功率`};function a(){var e=window.RelayDemo;if(e&&e.summary){var t=[];[].forEach.call(document.querySelectorAll(`[data-demo]`),function(n){var r=n.getAttribute(`data-demo`),a=e.summary[r];if(typeof a==`number`){var o=parseInt(n.getAttribute(`data-dec`)||`0`,10),s=n.textContent.trim().replace(/,/g,``);s!==``&&Math.abs(parseFloat(s)-a)>10**-o/2&&t.push((i[r]||r)+`:页面上写的 `+s+`,数据层是 `+a),n.setAttribute(`data-count`,a),n.textContent=p(a,o)}}),t.length&&window.console&&console.warn(`[page] 首页演示数字与 demo-data 不一致(已按数据层显示):
2
2
  · `+t.join(`
3
3
  · `))}}function o(e){var t=!1;return function(){t||(t=!0,requestAnimationFrame(function(){t=!1,e()}))}}function s(){var e=[].slice.call(document.querySelectorAll(`.reveal`));if(e.length){if(!(`IntersectionObserver`in window)){e.forEach(function(e){e.classList.add(`in`)});return}var t=new IntersectionObserver(function(e){e.forEach(function(e){e.isIntersecting&&(e.target.classList.add(`in`),t.unobserve(e.target))})},{rootMargin:`0px 0px -10% 0px`,threshold:.1});r.push(t),e.forEach(function(e){t.observe(e)})}}var c=document.querySelector(`.nav`);function l(){c&&c.classList.toggle(`stuck`,window.pageYOffset>28)}var u=[].slice.call(document.querySelectorAll(`.rail a`)),d=u.map(function(e){return document.querySelector(e.getAttribute(`href`))});function f(){if(u.length){for(var e=window.pageYOffset+window.innerHeight*.42,t=0,n=0;n<d.length;n+=1){var r=d[n];r&&r.offsetTop<=e&&(t=n)}for(var i=0;i<u.length;i+=1)u[i].classList.toggle(`on`,i===t)}}function p(e,t){var n=e.toFixed(t).split(`.`);return n[0]=n[0].replace(/\B(?=(\d{3})+(?!\d))/g,`,`),n.join(`.`)}function m(e){var n=parseFloat(e.getAttribute(`data-count`));if(isNaN(n))return;var r=parseInt(e.getAttribute(`data-dec`)||`0`,10),i=1500,a=0;function o(s){if(t){a||=s;var c=Math.min(1,(s-a)/i);e.textContent=p(n*(c===1?1:1-2**(-10*c)),r),c<1?requestAnimationFrame(o):e.textContent=p(n,r)}}requestAnimationFrame(o)}function h(){var t=[].slice.call(document.querySelectorAll(`[data-count]`));if(t.length){if(e||!(`IntersectionObserver`in window)){t.forEach(function(e){var t=parseFloat(e.getAttribute(`data-count`)),n=parseInt(e.getAttribute(`data-dec`)||`0`,10);isNaN(t)||(e.textContent=p(t,n))});return}var n=new IntersectionObserver(function(e){e.forEach(function(e){e.isIntersecting&&(m(e.target),n.unobserve(e.target))})},{threshold:.6});r.push(n),t.forEach(function(e){n.observe(e)})}}function g(){var i=[].slice.call(document.querySelectorAll(`[data-type]`));if(i.length&&!e&&`IntersectionObserver`in window){var a=new IntersectionObserver(function(e){e.forEach(function(e){if(e.isIntersecting){var r=e.target;a.unobserve(r);var i=r.getAttribute(`data-type`),o=0;r.textContent=``,r.classList.add(`typing`),(function e(){if(t){if(o>i.length){r.classList.remove(`typing`);return}r.textContent=i.slice(0,o),o+=1,n.push(window.setTimeout(e,34))}})()}})},{threshold:.8});r.push(a),i.forEach(function(e){a.observe(e)})}}var _=o(function(){l(),f()}),v=o(function(){f()}),y=function(){l(),f()};return a(),s(),h(),g(),l(),f(),window.addEventListener(`scroll`,_,{passive:!0}),window.addEventListener(`resize`,v),window.addEventListener(`load`,y),function(){t=!1,window.removeEventListener(`scroll`,_),window.removeEventListener(`resize`,v),window.removeEventListener(`load`,y),r.forEach(function(e){e.disconnect()}),n.forEach(function(e){clearTimeout(e)}),r=[],n=[]}}var y=r(),b=[{href:`#top`,label:`概览`},{href:`#ways`,label:`运行方式`},{href:`#quickstart`,label:`快速开始`},{href:`#clients`,label:`客户端配置`},{href:`#status`,label:`运行状态`},{href:`#cta`,label:`开始使用`}];function x(){return(0,y.jsx)(`nav`,{className:`rail`,"aria-label":`章节`,children:b.map(e=>(0,y.jsxs)(`a`,{href:e.href,children:[(0,y.jsx)(`i`,{}),(0,y.jsx)(`b`,{children:e.label})]},e.href))})}function S({version:t}){let[r,i]=(0,d.useState)(!1),o=()=>i(!1);return(0,y.jsxs)(`nav`,{className:`nav${r?` menu-open`:``}`,children:[(0,y.jsxs)(`a`,{className:`brand`,href:`#top`,onClick:o,children:[(0,y.jsx)(s,{size:28,id:`relay-mark-nav`}),(0,y.jsx)(`b`,{children:`途关`}),(0,y.jsx)(`span`,{children:`Waygate`})]}),(0,y.jsxs)(`div`,{className:`nav-links`,children:[(0,y.jsx)(`a`,{href:`#ways`,onClick:o,children:`运行方式`}),(0,y.jsx)(`a`,{href:`#quickstart`,onClick:o,children:`快速开始`}),(0,y.jsx)(`a`,{href:`#clients`,onClick:o,children:`客户端配置`}),(0,y.jsx)(`a`,{href:`#status`,onClick:o,children:`运行状态`})]}),(0,y.jsxs)(`div`,{className:`nav-right`,style:{display:`flex`,alignItems:`center`,gap:14,marginLeft:`auto`},children:[(0,y.jsxs)(`span`,{className:`pill pill-brand`,children:[`v`,t]}),(0,y.jsx)(`a`,{className:`btn btn-primary btn-sm`,href:a(),children:`打开控制台`}),(0,y.jsx)(`button`,{type:`button`,className:`nav-menu-toggle`,"aria-label":r?`关闭导航菜单`:`打开导航菜单`,"aria-controls":`landing-mobile-nav`,"aria-expanded":r,onClick:()=>i(e=>!e),children:r?(0,y.jsx)(n,{size:18,"aria-hidden":`true`}):(0,y.jsx)(e,{size:18,"aria-hidden":`true`})})]}),(0,y.jsxs)(`div`,{id:`landing-mobile-nav`,className:`mobile-nav`,"aria-hidden":!r,children:[(0,y.jsx)(`a`,{href:`#ways`,onClick:o,children:`运行方式`}),(0,y.jsx)(`a`,{href:`#quickstart`,onClick:o,children:`快速开始`}),(0,y.jsx)(`a`,{href:`#clients`,onClick:o,children:`客户端配置`}),(0,y.jsx)(`a`,{href:`#status`,onClick:o,children:`运行状态`})]})]})}function C(e,t=0,n){if(e==null)return{children:`—`};let r={"data-count":e,children:m(e,t)};return t&&(r[`data-dec`]=t),n&&(r[`data-demo`]=n),r}var w={live:`var(--signal-live)`,offline:`var(--warn-amber)`};function T(e=window.location.hostname){return/^\d{1,3}(\.\d{1,3}){3}$/.test(e)||e.includes(`:`)}function E(e){return T(e)?`自签证书`:`公共 CA`}var D=e=>e;function O({text:e,className:t,label:n,idle:r=`复制`,done:i=`已复制`}){let[a,o]=(0,d.useState)(!1),s=(0,d.useRef)(null);(0,d.useEffect)(()=>()=>{s.current&&window.clearTimeout(s.current)},[]);let c=(0,d.useCallback)(()=>{let t=navigator.clipboard;t&&typeof t.writeText==`function`&&t.writeText(e).then(()=>{o(!0),s.current&&window.clearTimeout(s.current),s.current=window.setTimeout(()=>o(!1),1200)},()=>void 0)},[e]);return(0,y.jsx)(`button`,{className:t,type:`button`,"aria-label":n,onClick:c,children:a?i:r})}var k=`{"x":0,"y":0,"z":0,"rx":0,"ry":0,"rz":0,"fog":0.16,"dim":0.90}`,A=[`# 克隆并构建`,`git clone <仓库地址> && cd relay-station`,`npm ci --prefix server && npm run build --prefix server`,`npm ci --prefix web && npm run build --prefix web`,``,`# 启动 systemd 服务(TLS 由 nginx 终止)`,`sudo systemctl start relay`].join(`
4
4
  `);function j({status:e}){let{online:t,real:n,numbers:r}=e,i=u(),a=r.totalTokens==null?`—`:l(r.totalTokens),o=t?void 0:D({color:`var(--warn-amber)`,background:`color-mix(in srgb, var(--warn-amber) 7%, transparent)`,borderColor:`var(--warn-amber-line)`});return(0,y.jsxs)(`header`,{className:`hero`,id:`top`,"data-cam":k,children:[(0,y.jsxs)(`div`,{className:`wrap`,children:[(0,y.jsxs)(`div`,{className:`hero-grid`,children:[(0,y.jsxs)(`div`,{className:`hero-inner`,children:[(0,y.jsxs)(`div`,{className:`hero-badge reveal`,style:o,children:[(0,y.jsx)(`i`,{className:t?`dot dot-live`:`dot`,style:{color:t?w.live:w.offline}}),t?`服务运行中`:`服务离线`,` · 本窗口 tokens`,` `,(0,y.jsx)(`span`,{className:`n`,children:a}),n?null:`(示例)`]}),(0,y.jsxs)(`div`,{className:`hero-title reveal`,children:[(0,y.jsxs)(`h1`,{className:`hero-h1 display-1`,children:[(0,y.jsx)(`span`,{className:`rv-line`,children:(0,y.jsx)(`span`,{style:D({"--i":0}),children:`把自己的订阅积分`})}),(0,y.jsx)(`span`,{className:`rv-line`,children:(0,y.jsxs)(`span`,{style:D({"--i":1}),children:[`变成一个 `,(0,y.jsx)(`span`,{className:`mk grad-text`,children:`base_url`})]})})]}),(0,y.jsx)(`div`,{className:`hero-flip`,"aria-hidden":`true`,children:(0,y.jsxs)(`h1`,{className:`hero-h1 display-1`,children:[`把自己的订阅积分`,(0,y.jsx)(`br`,{}),`变成一个 `,(0,y.jsx)(`span`,{className:`mk grad-text`,children:`base_url`})]})})]}),(0,y.jsx)(`p`,{className:`hero-sub reveal`,style:D({"--i":3}),children:`部署在你自己机器上,把你的 AI 订阅积分(WorkBuddy / MiniMax / Qoder)暴露成 OpenAI 兼容接口。 IDE、聊天客户端、手机、脚本共用同一个 Key,积分消耗实时可见。`}),(0,y.jsxs)(`div`,{className:`hero-acts reveal`,style:D({"--i":4}),children:[(0,y.jsx)(O,{className:`btn btn-primary`,text:A,label:`复制部署命令`,idle:`复制部署命令`}),(0,y.jsx)(`a`,{className:`btn btn-ghost`,href:`#clients`,children:`查看客户端配置`})]}),(0,y.jsxs)(`div`,{className:`trust reveal`,style:D({"--i":5}),children:[(0,y.jsx)(`span`,{children:`只监听本机回环`}),(0,y.jsx)(`span`,{children:`外发直连上游,无中转`}),(0,y.jsx)(`span`,{children:`请求日志保存在本地`})]})]}),(0,y.jsx)(`aside`,{className:`hero-side reveal`,style:D({"--i":2}),children:(0,y.jsxs)(`div`,{className:`glass access`,children:[(0,y.jsxs)(`div`,{className:`access-h`,children:[(0,y.jsx)(`span`,{className:`eyebrow`,style:D({"--c":`var(--text-ghost)`}),children:`接入地址`}),(0,y.jsxs)(`span`,{className:t?`pill pill-live`:`pill pill-amber`,children:[(0,y.jsx)(`i`,{className:t?`dot dot-live`:`dot`}),t?`可用`:`离线`]})]}),(0,y.jsxs)(`div`,{className:`access-url`,children:[(0,y.jsx)(`code`,{children:i}),(0,y.jsx)(O,{className:`copy`,text:i,label:`复制接入地址`})]}),(0,y.jsx)(`div`,{className:`access-note`,children:`把它填进任何支持 OpenAI 兼容协议的工具`}),(0,y.jsxs)(`dl`,{className:`kv`,children:[(0,y.jsxs)(`div`,{children:[(0,y.jsx)(`dt`,{children:`协议`}),(0,y.jsx)(`dd`,{children:`OpenAI v1 · Anthropic`})]}),(0,y.jsxs)(`div`,{children:[(0,y.jsx)(`dt`,{children:`日志保留`}),(0,y.jsx)(`dd`,{children:`默认 30 天`})]}),(0,y.jsxs)(`div`,{children:[(0,y.jsx)(`dt`,{children:`TLS`}),(0,y.jsx)(`dd`,{children:E()})]}),(0,y.jsxs)(`div`,{children:[(0,y.jsx)(`dt`,{children:`部署形态`}),(0,y.jsx)(`dd`,{children:`systemd + nginx`})]})]})]})})]}),(0,y.jsxs)(`div`,{className:`panel-wrap glass reveal`,style:D({"--i":6}),children:[(0,y.jsxs)(`div`,{className:`panel-left`,children:[(0,y.jsxs)(`div`,{className:`winbar`,children:[(0,y.jsx)(`i`,{style:{background:`var(--danger-red)`}}),(0,y.jsx)(`i`,{style:{background:`var(--warn-amber)`}}),(0,y.jsx)(`i`,{style:{background:`var(--signal-live)`}}),(0,y.jsx)(`span`,{className:`fname`,children:`deploy.sh`}),(0,y.jsx)(`span`,{className:`wtag`,children:`SHELL`}),(0,y.jsx)(`span`,{className:`wsp`}),(0,y.jsx)(O,{className:`wbtn`,text:A})]}),(0,y.jsxs)(`div`,{className:`panel-code`,children:[(0,y.jsxs)(`div`,{className:`cl`,children:[(0,y.jsx)(`i`,{children:`1`}),(0,y.jsx)(`span`,{children:(0,y.jsx)(`span`,{className:`c`,children:`# Ubuntu + systemd(TLS 由 nginx 终止)`})})]}),(0,y.jsxs)(`div`,{className:`cl`,children:[(0,y.jsx)(`i`,{children:`2`}),(0,y.jsx)(`span`,{children:(0,y.jsx)(`span`,{className:`fn`,children:`$ sudo systemctl start relay`})})]}),(0,y.jsxs)(`div`,{className:`cl`,children:[(0,y.jsx)(`i`,{children:`3`}),(0,y.jsx)(`span`,{children:(0,y.jsx)(`span`,{className:`fn`,children:`$ sudo systemctl status relay`})})]}),(0,y.jsxs)(`div`,{className:`cl`,children:[(0,y.jsx)(`i`,{children:`4`}),(0,y.jsx)(`span`,{children:(0,y.jsx)(`span`,{className:`fn`,children:`$ sudo systemctl restart relay`})})]}),(0,y.jsxs)(`div`,{className:`cl hl`,children:[(0,y.jsx)(`i`,{children:`5`}),(0,y.jsx)(`span`,{children:(0,y.jsx)(`span`,{className:`c`,children:`# 首次启动打印管理口令(仅此一次)`})})]}),(0,y.jsxs)(`div`,{className:`cl`,children:[(0,y.jsx)(`i`,{children:`6`}),(0,y.jsx)(`span`,{children:(0,y.jsx)(`span`,{className:`c`,children:`# API Key 在控制台「客户端与密钥」页新建`})})]})]})]}),(0,y.jsxs)(`div`,{className:`panel-right`,children:[(0,y.jsxs)(`div`,{className:`pr-head`,children:[(0,y.jsx)(`h4`,{children:`连接状态`}),(0,y.jsx)(`span`,{className:`pr-count`,children:t?`示例 · 3 个客户端`:`服务离线`})]}),(0,y.jsxs)(`div`,{className:`conn`,children:[(0,y.jsx)(`i`,{className:`dot dot-live`,style:{color:w.live}}),(0,y.jsx)(`span`,{className:`cname`,children:`Claude Code`}),(0,y.jsx)(`span`,{className:`dev`,children:`本机`}),(0,y.jsx)(`span`,{className:`lat`,children:`12ms`}),(0,y.jsx)(`span`,{className:`pill pill-live`,children:`在线`})]}),(0,y.jsxs)(`div`,{className:`conn`,children:[(0,y.jsx)(`i`,{className:`dot dot-live`,style:{color:w.live}}),(0,y.jsx)(`span`,{className:`cname`,children:`Cherry Studio`}),(0,y.jsx)(`span`,{className:`dev`,children:`桌面`}),(0,y.jsx)(`span`,{className:`lat`,children:`48ms`}),(0,y.jsx)(`span`,{className:`pill pill-live`,children:`在线`})]}),(0,y.jsxs)(`div`,{className:`conn`,children:[(0,y.jsx)(`i`,{className:`dot`,style:{color:w.offline}}),(0,y.jsx)(`span`,{className:`cname`,children:`Cursor`}),(0,y.jsx)(`span`,{className:`dev`,children:`笔记本`}),(0,y.jsx)(`span`,{className:`lat`,children:`—`}),(0,y.jsx)(`span`,{className:`pill pill-amber`,children:`待机`})]}),(0,y.jsxs)(`div`,{className:`panel-metrics`,children:[(0,y.jsxs)(`div`,{children:[(0,y.jsx)(`div`,{className:`k`,children:`本窗口 tokens`}),(0,y.jsx)(`div`,{className:`v num live`,style:{color:w.live},children:a})]}),(0,y.jsxs)(`div`,{children:[(0,y.jsx)(`div`,{className:`k`,children:`缓存命中率`}),(0,y.jsx)(`div`,{className:`v num`,children:h(r.cacheHitRate)})]}),(0,y.jsxs)(`div`,{children:[(0,y.jsx)(`div`,{className:`k`,children:`今日请求`}),(0,y.jsx)(`div`,{className:`v num`,children:r.requestsToday==null?`—`:m(r.requestsToday)})]})]})]})]})]}),(0,y.jsxs)(`div`,{className:`scroll-cue`,children:[(0,y.jsx)(`span`,{className:`tick`}),`SCROLL`]})]})}var M=`{"x":-26,"y":-16,"z":-150,"rx":2.5,"ry":3,"rz":0,"fog":0.30,"dim":0.44}`;function N(){return(0,y.jsx)(`section`,{className:`strip`,"data-cam":M,children:(0,y.jsxs)(`div`,{className:`wrap reveal`,children:[(0,y.jsxs)(`div`,{className:`cap`,children:[`任何支持 `,(0,y.jsx)(`b`,{children:`OpenAI 兼容协议`}),` 的工具,填上地址就能用`]}),(0,y.jsxs)(`div`,{className:`logos`,children:[(0,y.jsx)(`span`,{children:`Claude Code`}),(0,y.jsx)(`span`,{children:`Cursor`}),(0,y.jsx)(`span`,{children:`Cherry Studio`}),(0,y.jsx)(`span`,{children:`ChatBox`}),(0,y.jsx)(`span`,{children:`LobeChat`}),(0,y.jsx)(`span`,{children:`Continue`}),(0,y.jsx)(`span`,{className:`cn`,children:`你的脚本`})]})]})})}var P=`{"x":-118,"y":-38,"z":-310,"rx":5,"ry":7,"rz":-1.5,"fog":0.44,"dim":0.40}`,F={color:`var(--accent-cyan)`},I={fill:`none`,stroke:`currentColor`,strokeWidth:1.6};function L(){return(0,y.jsx)(`section`,{className:`section`,id:`ways`,"data-cam":P,children:(0,y.jsx)(`div`,{className:`wrap`,children:(0,y.jsxs)(`div`,{className:`split`,children:[(0,y.jsxs)(`div`,{className:`split-l reveal`,children:[(0,y.jsxs)(`div`,{className:`eyebrow`,children:[(0,y.jsx)(`span`,{className:`idx`,children:`02`}),`运行方式`]}),(0,y.jsxs)(`h2`,{className:`display-2`,children:[`一个进程,`,(0,y.jsx)(`br`,{}),`管住所有客户端的积分`]}),(0,y.jsx)(`p`,{children:`不用为每个客户端单独登录、单独算积分。服务跑在你自己的机器上,所有请求走同一个出口。`})]}),(0,y.jsxs)(`ul`,{className:`linelist`,children:[(0,y.jsxs)(`li`,{className:`reveal`,children:[(0,y.jsx)(`span`,{className:`ll-ico`,children:(0,y.jsxs)(`svg`,{width:`20`,height:`20`,viewBox:`0 0 20 20`,"aria-hidden":`true`,style:F,...I,children:[(0,y.jsx)(`circle`,{cx:`6.5`,cy:`10`,r:`3.5`}),(0,y.jsx)(`path`,{d:`M10 10h8M15 10v3M17.5 10v3`,strokeLinecap:`round`})]})}),(0,y.jsxs)(`span`,{className:`ll-tx`,children:[(0,y.jsx)(`b`,{children:`一个 Key,多端共用`}),(0,y.jsx)(`i`,{children:`桌面客户端、IDE、手机、脚本共用同一个密钥,控制台按客户端拆开看用量。`})]})]}),(0,y.jsxs)(`li`,{className:`reveal`,style:D({"--i":1}),children:[(0,y.jsx)(`span`,{className:`ll-ico`,children:(0,y.jsxs)(`svg`,{width:`20`,height:`20`,viewBox:`0 0 20 20`,"aria-hidden":`true`,style:F,...I,children:[(0,y.jsx)(`path`,{d:`M4 10h4M12 10h4`,strokeLinecap:`round`}),(0,y.jsx)(`circle`,{cx:`10`,cy:`10`,r:`2`}),(0,y.jsx)(`path`,{d:`M12 10V5.5M12 10v4.5`,strokeLinecap:`round`})]})}),(0,y.jsxs)(`span`,{className:`ll-tx`,children:[(0,y.jsx)(`b`,{children:`请求只在你自己的机器上流转`}),(0,y.jsx)(`i`,{children:`应用默认只监听本机回环,对外由你自己的 nginx 暴露,没有第三方中转。`})]})]}),(0,y.jsxs)(`li`,{className:`reveal`,style:D({"--i":2}),children:[(0,y.jsx)(`span`,{className:`ll-ico`,children:(0,y.jsxs)(`svg`,{width:`20`,height:`20`,viewBox:`0 0 20 20`,"aria-hidden":`true`,style:F,...I,children:[(0,y.jsx)(`rect`,{x:`2`,y:`4.5`,width:`16`,height:`11`,rx:`2.5`}),(0,y.jsx)(`path`,{d:`M2 8.5h16`}),(0,y.jsx)(`path`,{d:`M4.5 13h4`,strokeLinecap:`round`})]})}),(0,y.jsxs)(`span`,{className:`ll-tx`,children:[(0,y.jsx)(`b`,{children:`用量与消耗实时同步`}),(0,y.jsx)(`i`,{children:`本窗口总 tokens、缓存命中率、每条请求消耗多少积分,控制台里一眼看清。`})]})]}),(0,y.jsxs)(`li`,{className:`reveal`,style:D({"--i":3}),children:[(0,y.jsx)(`span`,{className:`ll-ico`,children:(0,y.jsx)(`svg`,{width:`20`,height:`20`,viewBox:`0 0 20 20`,"aria-hidden":`true`,style:F,...I,children:(0,y.jsx)(`path`,{d:`M10 2.5 16.5 5v5c0 4-2.8 6.4-6.5 7.5C6.3 16.4 3.5 14 3.5 10V5L10 2.5Z`,strokeLinejoin:`round`})})}),(0,y.jsxs)(`span`,{className:`ll-tx`,children:[(0,y.jsx)(`b`,{children:`请求日志与超限保护`}),(0,y.jsx)(`i`,{children:`每条请求都落库、可回溯;超过并发上限时直接回 429,失败不会被静默吞掉。`})]})]})]})]})})})}var R=`{"x":58,"y":-70,"z":-480,"rx":10,"ry":-5,"rz":1.5,"fog":0.52,"dim":0.36}`;function z({version:e}){return(0,y.jsx)(`section`,{className:`section`,id:`quickstart`,"data-cam":R,children:(0,y.jsxs)(`div`,{className:`wrap`,children:[(0,y.jsxs)(`div`,{className:`sec-head reveal`,children:[(0,y.jsxs)(`div`,{children:[(0,y.jsxs)(`div`,{className:`eyebrow`,children:[(0,y.jsx)(`span`,{className:`idx`,children:`03`}),`快速开始`]}),(0,y.jsx)(`h2`,{className:`display-2`,style:{marginTop:20},children:`三步,十分钟内跑起来`})]}),(0,y.jsx)(`p`,{className:`lead`,children:`自己的一台机器就够。首次启动打印管理口令(只显示一次),客户端的 Key 在控制台新建。`})]}),(0,y.jsxs)(`ol`,{className:`steps`,children:[(0,y.jsxs)(`li`,{className:`reveal`,children:[(0,y.jsx)(`div`,{className:`st-n`,children:`01`}),(0,y.jsx)(`h3`,{children:`部署服务`}),(0,y.jsx)(`p`,{children:`克隆仓库,构建前后端,装成 systemd 服务(nginx 在外层终止 TLS)。首次启动即打印管理口令。`}),(0,y.jsx)(`div`,{className:`cmd`,style:{marginTop:8},children:`git clone … → npm ci → npm run build → systemctl start`})]}),(0,y.jsxs)(`li`,{className:`reveal`,style:D({"--i":1}),children:[(0,y.jsx)(`div`,{className:`st-n`,children:`02`}),(0,y.jsx)(`h3`,{children:`生成密钥`}),(0,y.jsx)(`p`,{children:`在控制台「客户端与密钥」页新建密钥,按客户端命名,可以随时停用或删除。`}),(0,y.jsx)(`div`,{className:`cmd`,children:`控制台 → 客户端与密钥 → 新建密钥`})]}),(0,y.jsxs)(`li`,{className:`reveal`,style:D({"--i":2}),children:[(0,y.jsx)(`div`,{className:`st-n`,children:`03`}),(0,y.jsx)(`h3`,{children:`填进客户端`}),(0,y.jsx)(`p`,{children:`把 base_url 填成控制台的同源地址,模型名照抄,其余代码不用改。`})]})]}),(0,y.jsxs)(`div`,{className:`term reveal`,children:[(0,y.jsxs)(`div`,{className:`term-bar`,children:[(0,y.jsx)(`i`,{className:`dot dot-live`,style:{color:`var(--signal-live)`}}),(0,y.jsx)(`span`,{children:`首次启动输出`}),(0,y.jsxs)(`span`,{className:`term-tag`,children:[`relay · v`,e]})]}),(0,y.jsxs)(`div`,{className:`term-body`,children:[(0,y.jsxs)(`div`,{className:`tl`,children:[(0,y.jsx)(`span`,{className:`pr`,children:`$`}),` sudo systemctl start relay`]}),(0,y.jsx)(`div`,{className:`tl out`,children:`Waygate · 首次启动,已生成管理口令(此口令只显示这一次)`}),(0,y.jsxs)(`div`,{className:`tl out`,children:[`口令:`,(0,y.jsx)(`span`,{className:`hl`,children:`••••••••••••••••`})]}),(0,y.jsx)(`div`,{className:`tl out`,children:`请立即复制保存。忘记口令时:控制台「设置 → 管理口令」重置。`}),(0,y.jsx)(`div`,{className:`tl ok`,children:`Waygate 已在 http://127.0.0.1:8787 就绪(默认仅回环,不对公网直接暴露)`})]})]})]})})}var B=`{"x":148,"y":-94,"z":-630,"rx":6,"ry":-11,"rz":-2.5,"fog":0.60,"dim":0.34}`,V=`sk-waygate-4f9c2a7e1b83d04f391c7a2e`,H=u(),U=u().replace(/\/v1$/,``),W=[{k:`兼容协议`,v:`OpenAI v1 · Anthropic`},{k:`接入方式`,v:`HTTPS · ${E()}`},{k:`统计口径`,v:`本机日志聚合`},{k:`日志存储`,v:`本地`}];function G(){return(0,y.jsx)(`section`,{className:`section`,id:`clients`,"data-cam":B,children:(0,y.jsxs)(`div`,{className:`wrap`,children:[(0,y.jsxs)(`div`,{className:`sec-head reveal`,children:[(0,y.jsxs)(`div`,{children:[(0,y.jsxs)(`div`,{className:`eyebrow`,children:[(0,y.jsx)(`span`,{className:`idx`,children:`04`}),`客户端配置`]}),(0,y.jsx)(`h2`,{className:`display-2`,style:{marginTop:20},children:`四处填好,立刻能用`})]}),(0,y.jsx)(`p`,{className:`lead`,children:`所有客户端都只需要一个地址加一个密钥。下面是最常见的四种接法。`})]}),(0,y.jsxs)(`div`,{className:`grid-2`,children:[(0,y.jsxs)(`div`,{className:`glass glass-soft cfg reveal`,children:[(0,y.jsx)(`h3`,{children:`编程工具 / IDE`}),(0,y.jsx)(`div`,{className:`sub`,children:`Continue · Cline · 任意 OpenAI 兼容插件`}),(0,y.jsxs)(`div`,{className:`code`,children:[`OPENAI_BASE_URL=`,(0,y.jsx)(`span`,{className:`s`,children:H}),`