@mangoc/dsh-sub2api-sync 0.0.0-stage → 0.2.0

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/lib/client.cjs ADDED
@@ -0,0 +1,2416 @@
1
+ "use strict";
2
+ //#region rolldown:runtime
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+
24
+ //#endregion
25
+ const react = __toESM(require("react"));
26
+ const __deepseek_ai_dsh_client_ui_primitives = __toESM(require("@deepseek-ai/dsh-client-ui-primitives"));
27
+ const react_jsx_runtime = __toESM(require("react/jsx-runtime"));
28
+
29
+ //#region src/client/constants.ts
30
+ const PKG_NAME = __DSH_PLUGIN_NAME__;
31
+
32
+ //#endregion
33
+ //#region src/client/locales.ts
34
+ const NS = "sub2api-sync";
35
+ const zh = {
36
+ nav: "Sub2API 同步",
37
+ localOnlyWarning: "当前页面进程本地,配置仅本页生效",
38
+ configUnavailable: "配置暂不可用:宿主未连接或未暴露该设置条目",
39
+ nameLine: "sub2api · {instance}",
40
+ nameLineSite: "{name} · {instance}",
41
+ balanceLabel: "余额 {amount}",
42
+ balanceTitle: "面板账号余额(同步结束自动刷新)",
43
+ signedOut: "未登录",
44
+ signedInAs: "已登录:{email}",
45
+ signIn: "登录",
46
+ signOut: "登出",
47
+ signOutDone: "已登出",
48
+ baseURLLabel: "实例地址",
49
+ baseURLPlaceholder: "https://api.example.com(可带 /v1)",
50
+ save: "保存",
51
+ savedNotice: "已保存",
52
+ writeRefused: "写入未被接受,已恢复为最新状态",
53
+ accountTitle: "面板账号",
54
+ accountSignedOutHint: "登录后可发现分组、勾选同步,自动为每个分组准备 key",
55
+ signInTitle: "登录 sub2api 面板",
56
+ channelLabel: "登录方式",
57
+ channelPassword: "密码登录",
58
+ channelOAuth: "网页授权",
59
+ oauthStart: "在浏览器中授权",
60
+ oauthReadyHint: "该站点支持网页授权登录——点击「在浏览器中授权」跳转完成登录,完成后这里会自动继续。",
61
+ oauthPendingTitle: "等待浏览器授权…",
62
+ oauthPendingHint: "已在新标签页打开 sub2api 授权页——完成授权后这里会自动继续;若浏览器未打开,请检查弹窗拦截后重试。",
63
+ oauthOpenAgain: "重新打开授权页",
64
+ oauthFailed: "网页授权失败:{message}",
65
+ signedInOAuth: "已通过网页授权登录",
66
+ signedInOAuthAccount: "{account} · 已通过网页授权登录",
67
+ emailPlaceholder: "you@example.com",
68
+ passwordLabel: "密码",
69
+ passwordHint: "留空则使用宿主已保存的密码重新登录",
70
+ twoFactorPlaceholder: "6 位数字验证码",
71
+ twoFactorSubmit: "验证",
72
+ back: "返回",
73
+ cancel: "取消",
74
+ close: "关闭",
75
+ signedInNotice: "登录成功",
76
+ groupsRefresh: "刷新",
77
+ groupTypeTitle: "分组类型:{type} · 同步协议 {protocol}",
78
+ syncTitle: "同步",
79
+ syncNow: "立即同步",
80
+ syncing: "同步中…",
81
+ syncDone: "同步完成",
82
+ reportEmpty: "还没有同步过",
83
+ reportMeta: "写入 {writes} 项 · {attempts} 次尝试",
84
+ entryModels: "{name} · {count} 个模型",
85
+ entryError: "{name} · {error}",
86
+ entryRemove: "移除",
87
+ entryRemoveDone: "已移除 {name} 的同步",
88
+ reportRemoved: "已移除:{routes}",
89
+ reportWarningLine: "警告:{message}",
90
+ routesSummary: "已注册 {count} 条路由",
91
+ actionCreated: "新建",
92
+ actionUpdated: "更新",
93
+ actionUnchanged: "无变化",
94
+ actionFailed: "失败",
95
+ actionFailedLabel: "操作失败:{message}",
96
+ bundleTemplateLabel: "供应商名模板",
97
+ bundleTemplateHint: "displayName 模板,必须包含 {keyName} 占位符",
98
+ bundleTemplateInvalid: "模板必须包含 {keyName} 占位符",
99
+ bundleKeyQuotaLabel: "新建 key 配额(USD)",
100
+ bundleKeyQuotaHint: "自动建 key 时应用;留空不设置",
101
+ bundleKeyExpiresLabel: "新建 key 有效期(天)",
102
+ bundleKeyExpiresHint: "自动建 key 时应用;留空不过期",
103
+ bundleIntervalLabel: "自动同步间隔(毫秒)",
104
+ bundleIntervalHint: "至少 60000;留空关闭自动同步",
105
+ bundleIntervalInvalid: "间隔不能小于 60000",
106
+ bundleNumberInvalid: "请输入合法数字",
107
+ bundleDeleteOrphanLabel: "移除分组时吊销远端 key",
108
+ bundleDeleteOrphanHint: "仅对记录过 keyId 的分组生效",
109
+ bundleEnrichLabel: "补全模型元数据",
110
+ bundleEnrichHint: "同步时按模型 id 从 models.dev 补全容量元数据;关闭则仅用内置快照(离线)",
111
+ bundleAllowRemoteLabel: "允许私网设备调用动作接口",
112
+ bundleAllowRemoteWarning: "开启后同源或私网页面可改写模型路由与凭据——仅在可信内网开启",
113
+ bundleAdvancedHint: "protocolOverrides / compatTemplate 高级字段请编辑 cordis.patch.yml",
114
+ badgeManagedTag: "Sub2API 同步",
115
+ badgeManageHint: "管理入口:设置 → Sub2API 同步",
116
+ badgeModels: "{count} 个模型",
117
+ footerSyncedCount: "已同步 {count} 个分组",
118
+ footerSignedOut: "未连接面板",
119
+ footerNeverSynced: "尚未同步",
120
+ footerJustNow: "刚刚",
121
+ footerMinutesAgo: "{count} 分钟前",
122
+ detailBadgeSynced: "已同步 {count} 组",
123
+ detailBadgeSignedOut: "未连接面板",
124
+ detailSectionTitle: "最近同步",
125
+ detailSectionEmpty: "还没有同步过——在设置 → Sub2API 同步 完成首次同步",
126
+ activationTitle: "开始使用 Sub2API 同步",
127
+ activationStep1: "1. 在设置中连接面板(账号密码或网页授权)",
128
+ activationStep2: "2. 勾选要同步的分组(key 自动准备)",
129
+ activationStep3: "3. 点击「立即同步」,模型即可使用",
130
+ activationOpenConfig: "打开配置页",
131
+ activationDismiss: "知道了",
132
+ keysTitle: "Keys",
133
+ createKey: "创建 key",
134
+ keyGroupLabel: "所属分组",
135
+ keyNameLabel: "Key 名称",
136
+ keyNameHint: "默认与分组同名",
137
+ keyGroupPlaceholder: "选择分组",
138
+ keyCreated: "已创建并绑定:{name}",
139
+ keysListEmpty: "还没有 key",
140
+ keysListLoading: "正在读取 keys…",
141
+ syncSelected: "同步所选({count})",
142
+ syncSelectedDone: "同步完成:{ok}/{total} 条成功",
143
+ keysLoadMore: "加载更多(还有 {count} 个)"
144
+ };
145
+ const en = {
146
+ nav: "Sub2API Sync",
147
+ localOnlyWarning: "This page runs process-local; configuration applies to this page only",
148
+ configUnavailable: "Configuration is unavailable: the host is not connected or does not expose this entry",
149
+ nameLine: "sub2api · {instance}",
150
+ nameLineSite: "{name} · {instance}",
151
+ balanceLabel: "Balance {amount}",
152
+ balanceTitle: "Panel account balance (refreshed after each sync)",
153
+ signedOut: "Signed out",
154
+ signedInAs: "Signed in as {email}",
155
+ signIn: "Sign in",
156
+ signOut: "Sign out",
157
+ signOutDone: "Signed out",
158
+ baseURLLabel: "Instance address",
159
+ baseURLPlaceholder: "https://api.example.com (optionally with /v1)",
160
+ save: "Save",
161
+ savedNotice: "Saved",
162
+ writeRefused: "The write was not accepted; the latest state was restored",
163
+ accountTitle: "Panel account",
164
+ accountSignedOutHint: "Sign in to discover groups, pick the ones to sync, and let each group get its key prepared",
165
+ signInTitle: "Sign in to the sub2api panel",
166
+ channelLabel: "Sign-in channel",
167
+ channelPassword: "Password",
168
+ channelOAuth: "Web authorization",
169
+ oauthStart: "Authorize in browser",
170
+ oauthReadyHint: "This site supports web authorization — click \"Authorize in browser\" to sign in; this dialog continues automatically once done.",
171
+ oauthPendingTitle: "Waiting for browser authorization…",
172
+ oauthPendingHint: "The sub2api authorization page opened in a new tab — this dialog continues automatically. If no tab opened, check the pop-up blocker and retry.",
173
+ oauthOpenAgain: "Reopen the authorization page",
174
+ oauthFailed: "Web authorization failed: {message}",
175
+ signedInOAuth: "Signed in via web authorization",
176
+ signedInOAuthAccount: "{account} · Signed in via web authorization",
177
+ emailPlaceholder: "you@example.com",
178
+ passwordLabel: "Password",
179
+ passwordHint: "Leave empty to re-login with the password the host already stored",
180
+ twoFactorPlaceholder: "6-digit code",
181
+ twoFactorSubmit: "Verify",
182
+ back: "Back",
183
+ cancel: "Cancel",
184
+ close: "Close",
185
+ signedInNotice: "Signed in",
186
+ groupsRefresh: "Refresh",
187
+ groupTypeTitle: "Group type: {type} · syncs as {protocol}",
188
+ syncTitle: "Sync",
189
+ syncNow: "Sync now",
190
+ syncing: "Syncing…",
191
+ syncDone: "Sync finished",
192
+ reportEmpty: "Never synced",
193
+ reportMeta: "{writes} write(s) · {attempts} attempt(s)",
194
+ entryModels: "{name} · {count} model(s)",
195
+ entryError: "{name} · {error}",
196
+ entryRemove: "Remove",
197
+ entryRemoveDone: "Removed {name}",
198
+ reportRemoved: "Removed: {routes}",
199
+ reportWarningLine: "Warning: {message}",
200
+ routesSummary: "{count} route(s) registered",
201
+ actionCreated: "created",
202
+ actionUpdated: "updated",
203
+ actionUnchanged: "unchanged",
204
+ actionFailed: "failed",
205
+ actionFailedLabel: "Operation failed: {message}",
206
+ bundleTemplateLabel: "Provider name template",
207
+ bundleTemplateHint: "displayName template; must contain the {keyName} placeholder",
208
+ bundleTemplateInvalid: "The template must contain the {keyName} placeholder",
209
+ bundleKeyQuotaLabel: "Created key quota (USD)",
210
+ bundleKeyQuotaHint: "Applied to auto-created keys; empty leaves it unset",
211
+ bundleKeyExpiresLabel: "Created key expiry (days)",
212
+ bundleKeyExpiresHint: "Applied to auto-created keys; empty means no expiry",
213
+ bundleIntervalLabel: "Auto-sync interval (ms)",
214
+ bundleIntervalHint: "At least 60000; empty disables auto-sync",
215
+ bundleIntervalInvalid: "The interval must be at least 60000",
216
+ bundleNumberInvalid: "Enter a valid number",
217
+ bundleDeleteOrphanLabel: "Revoke the remote key when a group is removed",
218
+ bundleDeleteOrphanHint: "Only affects groups whose keyId was recorded",
219
+ bundleEnrichLabel: "Enrich model metadata",
220
+ bundleEnrichHint: "Fill per-model capacity facts from models.dev at sync; off = bundled snapshot only (offline)",
221
+ bundleAllowRemoteLabel: "Allow private-network devices to call action routes",
222
+ bundleAllowRemoteWarning: "When enabled, same-origin or private-network pages can rewrite model routes and credentials — enable only on a trusted network",
223
+ bundleAdvancedHint: "Edit cordis.patch.yml for the advanced protocolOverrides / compatTemplate fields",
224
+ badgeManagedTag: "Sub2API Sync",
225
+ badgeManageHint: "Manage in Settings → Sub2API Sync",
226
+ badgeModels: "{count} model(s)",
227
+ footerSyncedCount: "{count} group(s) synced",
228
+ footerSignedOut: "Panel not connected",
229
+ footerNeverSynced: "Never synced",
230
+ footerJustNow: "just now",
231
+ footerMinutesAgo: "{count} min ago",
232
+ detailBadgeSynced: "{count} group(s) synced",
233
+ detailBadgeSignedOut: "Panel not connected",
234
+ detailSectionTitle: "Last sync",
235
+ detailSectionEmpty: "Never synced — finish the first sync in Settings → Sub2API Sync",
236
+ activationTitle: "Get started with Sub2API Sync",
237
+ activationStep1: "1. Connect the panel in Settings (account password or web authorization)",
238
+ activationStep2: "2. Check the groups to sync (keys are prepared automatically)",
239
+ activationStep3: "3. Click \"Sync now\" and the models are ready",
240
+ activationOpenConfig: "Open configuration",
241
+ activationDismiss: "Got it",
242
+ keysTitle: "Keys",
243
+ createKey: "Create key",
244
+ keyGroupLabel: "Group",
245
+ keyNameLabel: "Key name",
246
+ keyNameHint: "Defaults to the group name",
247
+ keyGroupPlaceholder: "Pick a group",
248
+ keyCreated: "Created and bound: {name}",
249
+ keysListEmpty: "No keys yet",
250
+ keysListLoading: "Loading keys…",
251
+ syncSelected: "Sync selected ({count})",
252
+ syncSelectedDone: "Synced: {ok}/{total} succeeded",
253
+ keysLoadMore: "Load more ({count} left)"
254
+ };
255
+ const dictionaries = {
256
+ zh,
257
+ en
258
+ };
259
+
260
+ //#endregion
261
+ //#region src/types.ts
262
+ const PLATFORM_LABELS = {
263
+ anthropic: "Anthropic",
264
+ openai: "OpenAI",
265
+ gemini: "Gemini",
266
+ antigravity: "Antigravity",
267
+ grok: "Grok",
268
+ kimi: "Kimi",
269
+ zhipu: "Zhipu GLM",
270
+ deepseek: "DeepSeek",
271
+ composite: "Composite"
272
+ };
273
+ function platformLabel(platform) {
274
+ return PLATFORM_LABELS[platform] ?? platform;
275
+ }
276
+ const OWNED_ROUTE = /^(?:sub2api-[0-9a-f]{10}|sub2api-\d+)$/;
277
+ function isOwnedRoute(route) {
278
+ return OWNED_ROUTE.test(route);
279
+ }
280
+ /** Round constants for SHA-256 (FIPS 180-4 §4.2.2). */
281
+ const SHA256_K = new Uint32Array([
282
+ 1116352408,
283
+ 1899447441,
284
+ 3049323471,
285
+ 3921009573,
286
+ 961987163,
287
+ 1508970993,
288
+ 2453635748,
289
+ 2870763221,
290
+ 3624381080,
291
+ 310598401,
292
+ 607225278,
293
+ 1426881987,
294
+ 1925078388,
295
+ 2162078206,
296
+ 2614888103,
297
+ 3248222580,
298
+ 3835390401,
299
+ 4022224774,
300
+ 264347078,
301
+ 604807628,
302
+ 770255983,
303
+ 1249150122,
304
+ 1555081692,
305
+ 1996064986,
306
+ 2554220882,
307
+ 2821834349,
308
+ 2952996808,
309
+ 3210313671,
310
+ 3336571891,
311
+ 3584528711,
312
+ 113926993,
313
+ 338241895,
314
+ 666307205,
315
+ 773529912,
316
+ 1294757372,
317
+ 1396182291,
318
+ 1695183700,
319
+ 1986661051,
320
+ 2177026350,
321
+ 2456956037,
322
+ 2730485921,
323
+ 2820302411,
324
+ 3259730800,
325
+ 3345764771,
326
+ 3516065817,
327
+ 3600352804,
328
+ 4094571909,
329
+ 275423344,
330
+ 430227734,
331
+ 506948616,
332
+ 659060556,
333
+ 883997877,
334
+ 958139571,
335
+ 1322822218,
336
+ 1537002063,
337
+ 1747873779,
338
+ 1955562222,
339
+ 2024104815,
340
+ 2227730452,
341
+ 2361852424,
342
+ 2428436474,
343
+ 2756734187,
344
+ 3204031479,
345
+ 3329325298
346
+ ]);
347
+
348
+ //#endregion
349
+ //#region src/routes.ts
350
+ const ROUTES = {
351
+ sync: "/plugins/dsh-sub2api-sync/sync",
352
+ status: "/plugins/dsh-sub2api-sync/status",
353
+ login: "/plugins/dsh-sub2api-sync/login",
354
+ login2fa: "/plugins/dsh-sub2api-sync/login/2fa",
355
+ logout: "/plugins/dsh-sub2api-sync/logout",
356
+ groups: "/plugins/dsh-sub2api-sync/groups",
357
+ keys: "/plugins/dsh-sub2api-sync/keys",
358
+ icon: "/plugins/dsh-sub2api-sync/icon",
359
+ site: "/plugins/dsh-sub2api-sync/site",
360
+ account: "/plugins/dsh-sub2api-sync/account",
361
+ resetSync: "/plugins/dsh-sub2api-sync/reset-sync",
362
+ removeKey: "/plugins/dsh-sub2api-sync/remove-key",
363
+ oauthStart: "/plugins/dsh-sub2api-sync/oauth/start",
364
+ oauthProbe: "/plugins/dsh-sub2api-sync/oauth/probe",
365
+ oauthCallback: "/plugins/dsh-sub2api-sync/oauth/callback",
366
+ oauthStatus: "/plugins/dsh-sub2api-sync/oauth/status",
367
+ oauthCancel: "/plugins/dsh-sub2api-sync/oauth/cancel"
368
+ };
369
+
370
+ //#endregion
371
+ //#region src/client/usePluginApi.ts
372
+ async function callJson(path, init) {
373
+ const response = await fetch(path, {
374
+ ...init,
375
+ headers: {
376
+ accept: "application/json",
377
+ ...init?.body !== undefined ? { "content-type": "application/json" } : {},
378
+ ...init?.headers
379
+ }
380
+ });
381
+ let payload;
382
+ try {
383
+ payload = await response.json();
384
+ } catch {
385
+ throw new Error(`HTTP ${response.status}`);
386
+ }
387
+ if (!response.ok) {
388
+ const message = payload?.error;
389
+ throw new Error(typeof message === "string" && message.length > 0 ? message : `HTTP ${response.status}`);
390
+ }
391
+ return payload;
392
+ }
393
+ const post = (body) => ({
394
+ method: "POST",
395
+ ...body !== undefined ? { body: JSON.stringify(body) } : {}
396
+ });
397
+ function createPluginApi() {
398
+ return {
399
+ status: () => callJson(ROUTES.status),
400
+ login: (email, password) => callJson(ROUTES.login, post({
401
+ email,
402
+ ...password !== undefined && password.length > 0 ? { password } : {}
403
+ })),
404
+ login2fa: (code) => callJson(ROUTES.login2fa, post({ code })),
405
+ logout: () => callJson(ROUTES.logout, post()),
406
+ groups: () => callJson(ROUTES.groups),
407
+ listKeys: (page, pageSize) => callJson(`${ROUTES.keys}?page=${page}&pageSize=${pageSize}`),
408
+ createKey: (input) => callJson(ROUTES.keys, post(input)),
409
+ site: () => callJson(ROUTES.site),
410
+ account: () => callJson(ROUTES.account),
411
+ resetSyncState: (previousBaseURL) => callJson(ROUTES.resetSync, post(previousBaseURL !== undefined ? { previousBaseURL } : undefined)),
412
+ removeKey: (keyId) => callJson(ROUTES.removeKey, post({ keyId })),
413
+ sync: (keyIds) => callJson(ROUTES.sync, post(keyIds !== undefined ? { keyIds } : undefined)),
414
+ oauthStart: () => callJson(ROUTES.oauthStart, post()),
415
+ oauthProbe: (address) => callJson(`${ROUTES.oauthProbe}?address=${encodeURIComponent(address)}`),
416
+ oauthStatus: (attemptId) => callJson(attemptId !== undefined ? `${ROUTES.oauthStatus}?attempt=${encodeURIComponent(attemptId)}` : ROUTES.oauthStatus),
417
+ oauthCancel: (attemptId) => callJson(ROUTES.oauthCancel, post({ attemptId }))
418
+ };
419
+ }
420
+
421
+ //#endregion
422
+ //#region src/client/status.ts
423
+ const POLL_MS = 5e3;
424
+ let snapshot;
425
+ let timer;
426
+ const listeners = new Set();
427
+ function publish(next) {
428
+ if (next === snapshot) return;
429
+ snapshot = next;
430
+ for (const listener of listeners) listener();
431
+ }
432
+ /** One poll round; a failed round keeps the last good snapshot (transient). */
433
+ async function poll() {
434
+ try {
435
+ publish(await callJson(ROUTES.status));
436
+ } catch {}
437
+ }
438
+ function subscribe(listener) {
439
+ listeners.add(listener);
440
+ if (listeners.size === 1) {
441
+ void poll();
442
+ timer = setInterval(() => {
443
+ void poll();
444
+ }, POLL_MS);
445
+ }
446
+ return () => {
447
+ listeners.delete(listener);
448
+ if (listeners.size === 0 && timer !== undefined) {
449
+ clearInterval(timer);
450
+ timer = undefined;
451
+ }
452
+ };
453
+ }
454
+ const statusSource = {
455
+ getSnapshot: () => snapshot,
456
+ subscribe
457
+ };
458
+ function useStatus() {
459
+ return (0, react.useSyncExternalStore)(subscribe, getSnapshot);
460
+ }
461
+ function getSnapshot() {
462
+ return snapshot;
463
+ }
464
+ async function refreshStatus() {
465
+ await poll();
466
+ return snapshot;
467
+ }
468
+
469
+ //#endregion
470
+ //#region src/client/styles.ts
471
+ /**
472
+ * The plugin's one stylesheet, injected once as a `<style data-plugin>`
473
+ * element. Values follow the ui-settings-account `AccountSection.module.css`
474
+ * visual spec exactly (design note §4.4) — page root, cards, rows, identity
475
+ * slots, dividers, and actions — so the card sits next to the official
476
+ * Account section without visual drift. Class names carry the `s2s-` prefix
477
+ * because the sheet is global, not module-scoped.
478
+ *
479
+ * @module sub2api-sync/client/styles
480
+ */
481
+ const CSS = `
482
+ .s2s-section {
483
+ display: flex;
484
+ flex-direction: column;
485
+ gap: 16px;
486
+ padding: 8px 0;
487
+ font-size: 13px;
488
+ line-height: 22px;
489
+ color: var(--dsw-alias-label-primary);
490
+ }
491
+
492
+ .s2s-card {
493
+ display: flex;
494
+ flex-direction: column;
495
+ padding: 12px 16px;
496
+ border: 0.5px solid var(--dsw-alias-settings-card-stroke);
497
+ border-radius: var(--dsw-radius-xl);
498
+ background: var(--dsw-alias-settings-card-fill);
499
+ }
500
+
501
+ .s2s-row {
502
+ display: flex;
503
+ align-items: center;
504
+ justify-content: space-between;
505
+ gap: 16px;
506
+ min-height: 40px;
507
+ padding: 6px 0;
508
+ box-sizing: border-box;
509
+ }
510
+
511
+ .s2s-identity {
512
+ display: flex;
513
+ align-items: center;
514
+ gap: 10px;
515
+ min-height: 44px;
516
+ min-width: 0;
517
+ }
518
+
519
+ .s2s-avatar {
520
+ flex: none;
521
+ display: flex;
522
+ align-items: center;
523
+ justify-content: center;
524
+ width: 32px;
525
+ height: 32px;
526
+ border-radius: 50%;
527
+ color: var(--dsw-alias-label-tertiary);
528
+ background: var(--dsw-alias-bg-skeleton);
529
+ }
530
+
531
+ .s2s-avatar .s2s-favicon {
532
+ width: 18px;
533
+ height: 18px;
534
+ border-radius: 4px;
535
+ object-fit: contain;
536
+ }
537
+
538
+ .s2s-actions .s2s-feedback {
539
+ margin-right: 2px;
540
+ white-space: nowrap;
541
+ }
542
+
543
+ .s2s-identityCopy {
544
+ display: flex;
545
+ flex-direction: column;
546
+ min-width: 0;
547
+ }
548
+
549
+ .s2s-name {
550
+ font-size: 14px;
551
+ font-weight: 500;
552
+ overflow: hidden;
553
+ text-overflow: ellipsis;
554
+ white-space: nowrap;
555
+ }
556
+
557
+ .s2s-secondary {
558
+ color: var(--dsw-alias-label-tertiary);
559
+ overflow: hidden;
560
+ text-overflow: ellipsis;
561
+ white-space: nowrap;
562
+ }
563
+
564
+ .s2s-divider {
565
+ border-top: 0.5px solid var(--dsw-alias-border-l2);
566
+ }
567
+
568
+ .s2s-actions {
569
+ display: flex;
570
+ justify-content: flex-end;
571
+ align-items: center;
572
+ gap: 10px;
573
+ flex-wrap: wrap;
574
+ flex: none;
575
+ }
576
+
577
+ /* The account-balance pill in the identity header's action row. */
578
+ .s2s-balance {
579
+ flex: none;
580
+ padding: 3px 10px;
581
+ border: 0.5px solid var(--dsw-alias-border-l2);
582
+ border-radius: 999px;
583
+ background: var(--dsw-alias-bg-secondary);
584
+ color: var(--dsw-alias-label-primary);
585
+ font-size: 12px;
586
+ font-variant-numeric: tabular-nums;
587
+ white-space: nowrap;
588
+ }
589
+
590
+ .s2s-warn {
591
+ margin: 0;
592
+ padding: 8px 12px;
593
+ border: 0.5px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 35%, transparent);
594
+ border-radius: var(--dsw-radius-md);
595
+ color: var(--dsw-alias-state-warn-primary);
596
+ background: var(--dsw-alias-state-warn-tertiary);
597
+ }
598
+
599
+ .s2s-feedback {
600
+ margin: 0;
601
+ padding: 2px 0 6px;
602
+ overflow-wrap: anywhere;
603
+ }
604
+
605
+ .s2s-feedbackOk {
606
+ color: var(--dsw-alias-state-success-primary);
607
+ }
608
+
609
+ .s2s-feedbackError {
610
+ color: var(--dsw-alias-state-error-primary);
611
+ }
612
+
613
+ .s2s-column {
614
+ display: flex;
615
+ flex-direction: column;
616
+ gap: 8px;
617
+ min-width: 0;
618
+ }
619
+
620
+ .s2s-hint {
621
+ color: var(--dsw-alias-label-tertiary);
622
+ }
623
+
624
+ .s2s-inline {
625
+ display: flex;
626
+ align-items: center;
627
+ justify-content: space-between;
628
+ gap: 16px;
629
+ min-height: 40px;
630
+ padding: 6px 0;
631
+ box-sizing: border-box;
632
+ }
633
+
634
+ .s2s-keyRow {
635
+ display: grid;
636
+ /* Columns align across rows: name+checkbox / group / status / type. */
637
+ grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr) minmax(0, 0.8fr) minmax(0, 1fr);
638
+ align-items: center;
639
+ gap: 10px;
640
+ width: 100%;
641
+ min-height: 36px;
642
+ padding: 4px 10px;
643
+ box-sizing: border-box;
644
+ border: 0.5px solid var(--dsw-alias-border-l2);
645
+ border-radius: var(--dsw-radius-md);
646
+ background: transparent;
647
+ color: inherit;
648
+ font: inherit;
649
+ text-align: left;
650
+ cursor: pointer;
651
+ user-select: none;
652
+ }
653
+
654
+ .s2s-keyRow > :last-child {
655
+ justify-self: end;
656
+ text-align: right;
657
+ }
658
+
659
+ .s2s-keyRow:hover {
660
+ background: var(--dsw-alias-interactive-bg-hover);
661
+ }
662
+
663
+ .s2s-keyRow:focus-visible {
664
+ outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
665
+ outline-offset: 2px;
666
+ }
667
+
668
+ .s2s-keyRow[aria-pressed='true'] {
669
+ border-color: var(--dsw-alias-state-business-primary);
670
+ }
671
+
672
+ .s2s-reportLine {
673
+ display: flex;
674
+ align-items: baseline;
675
+ gap: 8px;
676
+ padding: 2px 0;
677
+ min-width: 0;
678
+ }
679
+
680
+ .s2s-reportRemove {
681
+ margin-left: auto;
682
+ flex: none;
683
+ align-self: center;
684
+ }
685
+
686
+ .s2s-reportMark {
687
+ flex: none;
688
+ font-weight: 500;
689
+ }
690
+
691
+ .s2s-reportMark[data-mark='ok']::before {
692
+ content: '\\2713';
693
+ color: var(--dsw-alias-state-success-primary);
694
+ }
695
+
696
+ .s2s-reportMark[data-mark='fail']::before {
697
+ content: '\\2717';
698
+ color: var(--dsw-alias-state-error-primary);
699
+ }
700
+
701
+ .s2s-reportText {
702
+ min-width: 0;
703
+ overflow-wrap: anywhere;
704
+ }
705
+
706
+ .s2s-reportError {
707
+ color: var(--dsw-alias-state-error-primary);
708
+ }
709
+
710
+ .s2s-fieldLabel {
711
+ color: var(--dsw-alias-label-primary);
712
+ flex: none;
713
+ }
714
+
715
+ .s2s-input {
716
+ min-width: 0;
717
+ flex: 1 1 220px;
718
+ }
719
+
720
+ /* Menu anchor wrapper: give the picker trigger the same field-column
721
+ footprint as the sibling Input rows. */
722
+ .s2s-menuAnchor {
723
+ display: flex;
724
+ flex: 1 1 220px;
725
+ min-width: 0;
726
+ }
727
+
728
+ .s2s-menuAnchor > * {
729
+ width: 100%;
730
+ }
731
+
732
+ /* Select-style trigger (group picker): label left, chevron right. */
733
+ .s2s-selectTrigger {
734
+ display: flex;
735
+ align-items: center;
736
+ justify-content: space-between;
737
+ gap: 8px;
738
+ width: 100%;
739
+ }
740
+
741
+ .s2s-selectChevron {
742
+ flex: none;
743
+ opacity: 0.55;
744
+ }
745
+
746
+ /* U2 placements: the provider-card badge, the Models-page footer strip, and
747
+ the Plugins-page activation steps. */
748
+ .s2s-badgeRow {
749
+ display: flex;
750
+ align-items: center;
751
+ flex-wrap: wrap;
752
+ gap: 8px;
753
+ padding-top: 6px;
754
+ }
755
+
756
+ .s2s-badgeMeta {
757
+ color: var(--dsw-alias-label-secondary);
758
+ font-size: 12px;
759
+ line-height: 18px;
760
+ }
761
+
762
+ .s2s-badgeHint {
763
+ color: var(--dsw-alias-label-tertiary);
764
+ font-size: 12px;
765
+ line-height: 18px;
766
+ }
767
+
768
+ .s2s-footerStrip {
769
+ display: flex;
770
+ align-items: center;
771
+ justify-content: space-between;
772
+ gap: 16px;
773
+ min-height: 32px;
774
+ color: var(--dsw-alias-label-secondary);
775
+ font-size: 12px;
776
+ line-height: 18px;
777
+ }
778
+
779
+ .s2s-steps {
780
+ display: flex;
781
+ flex-direction: column;
782
+ gap: 6px;
783
+ margin: 0;
784
+ padding: 0;
785
+ list-style: none;
786
+ color: var(--dsw-alias-label-secondary);
787
+ font-size: 13px;
788
+ line-height: 22px;
789
+ }
790
+ `;
791
+ function ensureCss() {
792
+ if (document.querySelector("style[data-plugin=\"dsh-sub2api-sync\"]") !== null) return;
793
+ const style = document.createElement("style");
794
+ style.dataset.plugin = "dsh-sub2api-sync";
795
+ style.textContent = CSS;
796
+ document.head.appendChild(style);
797
+ }
798
+
799
+ //#endregion
800
+ //#region src/client/useConfigForm.ts
801
+ function useConfigForm(form) {
802
+ const subscribe$1 = (0, react.useMemo)(() => form.subscribe.bind(form), [form]);
803
+ const getSnapshot$1 = (0, react.useMemo)(() => form.getSnapshot.bind(form), [form]);
804
+ return (0, react.useSyncExternalStore)(subscribe$1, getSnapshot$1);
805
+ }
806
+ function setField(form, field, value) {
807
+ return form.set(field, value);
808
+ }
809
+ function unsetField(form, field) {
810
+ return form.unset(field);
811
+ }
812
+ async function pruneInvalidSelection(config, report) {
813
+ const invalid = new Set(report.entries.filter((entry) => entry.invalid === true).map((entry) => entry.groupId));
814
+ if (invalid.size === 0) return false;
815
+ const current = config.getSnapshot().value?.selectedKeys ?? [];
816
+ const next = current.filter((id) => !invalid.has(id));
817
+ if (next.length === current.length) return false;
818
+ await setField(config, "selectedKeys", [...next].sort((a, b) => a - b));
819
+ return true;
820
+ }
821
+ async function dropSelectedKey(config, keyId) {
822
+ const current = config.getSnapshot().value?.selectedKeys ?? [];
823
+ if (!current.includes(keyId)) return false;
824
+ await setField(config, "selectedKeys", current.filter((id) => id !== keyId));
825
+ return true;
826
+ }
827
+
828
+ //#endregion
829
+ //#region src/client/section/SignInDialog.tsx
830
+ const OAUTH_POLL_MS = 2e3;
831
+ function SignInDialog({ open, onClose, api, config, baseURL, writable, onSignedIn, t }) {
832
+ const [phase, setPhase] = (0, react.useState)("password");
833
+ const [channel, setChannel] = (0, react.useState)("password");
834
+ const [address, setAddress] = (0, react.useState)("");
835
+ const [email, setEmail] = (0, react.useState)("");
836
+ const [password, setPassword] = (0, react.useState)("");
837
+ const [code, setCode] = (0, react.useState)("");
838
+ const [busy, setBusy] = (0, react.useState)(false);
839
+ const [error, setError] = (0, react.useState)(undefined);
840
+ const [authorizeUrl, setAuthorizeUrl] = (0, react.useState)(undefined);
841
+ const attemptRef = (0, react.useRef)(undefined);
842
+ const channelTouchedRef = (0, react.useRef)(false);
843
+ (0, react.useEffect)(() => {
844
+ if (open) {
845
+ setAddress(baseURL ?? "");
846
+ channelTouchedRef.current = false;
847
+ return;
848
+ }
849
+ setPhase("password");
850
+ setChannel("password");
851
+ setEmail("");
852
+ setPassword("");
853
+ setCode("");
854
+ setBusy(false);
855
+ setError(undefined);
856
+ setAuthorizeUrl(undefined);
857
+ attemptRef.current = undefined;
858
+ }, [open, baseURL]);
859
+ (0, react.useEffect)(() => {
860
+ if (!open) return;
861
+ const value = address.trim();
862
+ if (!/^https?:\/\//i.test(value)) return;
863
+ if (phase !== "password" && phase !== "oauth") return;
864
+ const timer$1 = setTimeout(() => {
865
+ void api.oauthProbe(value).then((answer) => {
866
+ if (channelTouchedRef.current) return;
867
+ const next = answer.supported ? "oauth" : "password";
868
+ setChannel(next);
869
+ setPhase((prev) => prev === "password" || prev === "oauth" ? next : prev);
870
+ }).catch(() => undefined);
871
+ }, 600);
872
+ return () => {
873
+ clearTimeout(timer$1);
874
+ };
875
+ }, [
876
+ address,
877
+ open,
878
+ phase
879
+ ]);
880
+ (0, react.useEffect)(() => {
881
+ if (phase !== "oauth-pending") return;
882
+ let stopped = false;
883
+ const timer$1 = setInterval(() => {
884
+ const attemptId = attemptRef.current;
885
+ if (attemptId === undefined) return;
886
+ void api.oauthStatus(attemptId).then((status) => {
887
+ if (stopped) return;
888
+ if (status.auth.signedIn && status.auth.kind === "oauth") {
889
+ finish();
890
+ return;
891
+ }
892
+ const attempt = status.attempt;
893
+ if (attempt === undefined) {
894
+ stop();
895
+ setError(t("oauthFailed", { message: "expired" }));
896
+ setPhase("oauth");
897
+ return;
898
+ }
899
+ if (attempt.phase === "completed") {
900
+ finish();
901
+ return;
902
+ }
903
+ if (attempt.phase === "failed") {
904
+ stop();
905
+ setError(t("oauthFailed", { message: attempt.error ?? "unknown" }));
906
+ setPhase("oauth");
907
+ }
908
+ }).catch(() => undefined);
909
+ }, OAUTH_POLL_MS);
910
+ const stop = () => {
911
+ stopped = true;
912
+ clearInterval(timer$1);
913
+ };
914
+ return stop;
915
+ }, [phase]);
916
+ const pickChannel = (next) => {
917
+ channelTouchedRef.current = true;
918
+ setChannel(next);
919
+ setPhase(next);
920
+ setError(undefined);
921
+ };
922
+ const finish = () => {
923
+ onSignedIn();
924
+ onClose();
925
+ };
926
+ /** Commit the address draft before authenticating; aborts the flow on refusal. */
927
+ const commitAddress = async () => {
928
+ const value = address.trim();
929
+ if (value === (baseURL ?? "")) return;
930
+ const accepted = await (value.length === 0 ? unsetField(config, "baseURL") : setField(config, "baseURL", value));
931
+ if (!accepted) throw new Error(t("writeRefused"));
932
+ };
933
+ const run = async (action) => {
934
+ setBusy(true);
935
+ setError(undefined);
936
+ try {
937
+ await action();
938
+ } catch (cause) {
939
+ setError(cause instanceof Error ? cause.message : String(cause));
940
+ } finally {
941
+ setBusy(false);
942
+ }
943
+ };
944
+ const openAuthorizeTab = (url) => {
945
+ if (url === undefined) return;
946
+ window.open(url, "_blank", "noopener");
947
+ };
948
+ const submitPassword = () => {
949
+ void run(async () => {
950
+ await commitAddress();
951
+ const response = await api.login(email.trim(), password.length > 0 ? password : undefined);
952
+ if (response.kind === "2fa") {
953
+ setPhase("2fa");
954
+ return;
955
+ }
956
+ finish();
957
+ });
958
+ };
959
+ const submit2fa = () => {
960
+ void run(async () => {
961
+ await api.login2fa(code.trim());
962
+ finish();
963
+ });
964
+ };
965
+ const startOauth = () => {
966
+ void run(async () => {
967
+ await commitAddress();
968
+ const started = await api.oauthStart();
969
+ attemptRef.current = started.attemptId;
970
+ setAuthorizeUrl(started.authorizeUrl);
971
+ openAuthorizeTab(started.authorizeUrl);
972
+ setPhase("oauth-pending");
973
+ });
974
+ };
975
+ const cancelOauth = () => {
976
+ const attemptId = attemptRef.current;
977
+ attemptRef.current = undefined;
978
+ if (attemptId !== undefined) void api.oauthCancel(attemptId).catch(() => undefined);
979
+ setPhase("oauth");
980
+ };
981
+ const footer = (0, react_jsx_runtime.jsxs)("div", {
982
+ className: "s2s-actions",
983
+ children: [
984
+ phase === "2fa" && (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
985
+ variant: "outline",
986
+ disabled: busy,
987
+ onClick: () => pickChannel("password"),
988
+ children: t("back")
989
+ }),
990
+ phase === "oauth-pending" && (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
991
+ variant: "outline",
992
+ disabled: busy,
993
+ onClick: cancelOauth,
994
+ children: t("back")
995
+ }),
996
+ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
997
+ variant: "ghost",
998
+ disabled: busy,
999
+ onClick: onClose,
1000
+ children: t("cancel")
1001
+ }),
1002
+ phase === "password" && (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
1003
+ variant: "primary",
1004
+ disabled: busy || email.trim().length === 0 || address.trim().length === 0,
1005
+ onClick: submitPassword,
1006
+ children: t("signIn")
1007
+ }),
1008
+ phase === "oauth" && (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
1009
+ variant: "primary",
1010
+ disabled: busy || address.trim().length === 0,
1011
+ onClick: startOauth,
1012
+ children: t("oauthStart")
1013
+ })
1014
+ ]
1015
+ });
1016
+ return (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Modal, {
1017
+ open,
1018
+ onClose,
1019
+ title: t("signInTitle"),
1020
+ closeLabel: t("close"),
1021
+ footer,
1022
+ children: (0, react_jsx_runtime.jsxs)("div", {
1023
+ className: "s2s-column",
1024
+ children: [
1025
+ (0, react_jsx_runtime.jsxs)("div", {
1026
+ className: "s2s-inline",
1027
+ children: [(0, react_jsx_runtime.jsx)("span", {
1028
+ className: "s2s-fieldLabel",
1029
+ children: t("baseURLLabel")
1030
+ }), (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Input, {
1031
+ className: "s2s-input",
1032
+ type: "text",
1033
+ value: address,
1034
+ placeholder: t("baseURLPlaceholder"),
1035
+ disabled: busy || !writable || phase === "2fa" || phase === "oauth-pending",
1036
+ onChange: (event) => {
1037
+ setAddress(event.target.value);
1038
+ }
1039
+ })]
1040
+ }),
1041
+ phase !== "2fa" && phase !== "oauth-pending" && (0, react_jsx_runtime.jsxs)("div", {
1042
+ className: "s2s-inline",
1043
+ children: [(0, react_jsx_runtime.jsx)("span", {
1044
+ className: "s2s-fieldLabel",
1045
+ children: t("channelLabel")
1046
+ }), (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.SegmentedControl, {
1047
+ id: "sub2api-sync-signin-channel",
1048
+ value: channel,
1049
+ options: [{
1050
+ value: "password",
1051
+ label: t("channelPassword")
1052
+ }, {
1053
+ value: "oauth",
1054
+ label: t("channelOAuth")
1055
+ }],
1056
+ onChange: pickChannel,
1057
+ label: t("channelLabel"),
1058
+ disabled: busy
1059
+ })]
1060
+ }),
1061
+ phase === "password" && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1062
+ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Input, {
1063
+ type: "email",
1064
+ value: email,
1065
+ placeholder: t("emailPlaceholder"),
1066
+ disabled: busy,
1067
+ autoFocus: true,
1068
+ onChange: (event) => {
1069
+ setEmail(event.target.value);
1070
+ }
1071
+ }),
1072
+ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Input, {
1073
+ type: "password",
1074
+ value: password,
1075
+ disabled: busy,
1076
+ placeholder: t("passwordLabel"),
1077
+ onChange: (event) => {
1078
+ setPassword(event.target.value);
1079
+ }
1080
+ }),
1081
+ (0, react_jsx_runtime.jsx)("span", {
1082
+ className: "s2s-hint",
1083
+ children: t("passwordHint")
1084
+ })
1085
+ ] }),
1086
+ phase === "oauth" && (0, react_jsx_runtime.jsx)("span", {
1087
+ className: "s2s-hint",
1088
+ children: t("oauthReadyHint")
1089
+ }),
1090
+ phase === "oauth-pending" && (0, react_jsx_runtime.jsxs)("div", {
1091
+ className: "s2s-column",
1092
+ children: [
1093
+ (0, react_jsx_runtime.jsx)("span", {
1094
+ className: "s2s-hint",
1095
+ children: t("oauthPendingTitle")
1096
+ }),
1097
+ (0, react_jsx_runtime.jsx)("span", {
1098
+ className: "s2s-hint",
1099
+ children: t("oauthPendingHint")
1100
+ }),
1101
+ authorizeUrl !== undefined && (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
1102
+ variant: "outline",
1103
+ onClick: () => {
1104
+ openAuthorizeTab(authorizeUrl);
1105
+ },
1106
+ children: t("oauthOpenAgain")
1107
+ })
1108
+ ]
1109
+ }),
1110
+ phase === "2fa" && (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Input, {
1111
+ type: "text",
1112
+ inputMode: "numeric",
1113
+ maxLength: 6,
1114
+ value: code,
1115
+ placeholder: t("twoFactorPlaceholder"),
1116
+ disabled: busy,
1117
+ autoFocus: true,
1118
+ onChange: (event) => {
1119
+ setCode(event.target.value.replace(/\D/g, ""));
1120
+ }
1121
+ }),
1122
+ error !== undefined && (0, react_jsx_runtime.jsx)("p", {
1123
+ className: "s2s-feedback s2s-feedbackError",
1124
+ children: t("actionFailedLabel", { message: error })
1125
+ })
1126
+ ]
1127
+ })
1128
+ });
1129
+ }
1130
+
1131
+ //#endregion
1132
+ //#region src/protocols.ts
1133
+ function protocolForPlatform(platform) {
1134
+ switch (platform) {
1135
+ case "anthropic": return "anthropic-messages";
1136
+ case "openai":
1137
+ case "grok":
1138
+ case "kimi":
1139
+ case "zhipu":
1140
+ case "deepseek": return "openai-responses";
1141
+ case "gemini":
1142
+ case "antigravity":
1143
+ case "composite": return "openai-completions";
1144
+ default: return undefined;
1145
+ }
1146
+ }
1147
+
1148
+ //#endregion
1149
+ //#region src/client/section/KeysCard.tsx
1150
+ const PAGE_SIZE = 5;
1151
+ function KeysCard({ api, config, groups, t }) {
1152
+ const snapshot$1 = useConfigForm(config);
1153
+ const [keys, setKeys] = (0, react.useState)([]);
1154
+ const [page, setPage] = (0, react.useState)(0);
1155
+ const [pages, setPages] = (0, react.useState)(1);
1156
+ const [total, setTotal] = (0, react.useState)(0);
1157
+ const [loading, setLoading] = (0, react.useState)(false);
1158
+ const [listError, setListError] = (0, react.useState)(undefined);
1159
+ const [creating, setCreating] = (0, react.useState)(false);
1160
+ const [groupMenuOpen, setGroupMenuOpen] = (0, react.useState)(false);
1161
+ const [groupId, setGroupId] = (0, react.useState)("");
1162
+ const [name$1, setName] = (0, react.useState)("");
1163
+ const [nameTouched, setNameTouched] = (0, react.useState)(false);
1164
+ const [busy, setBusy] = (0, react.useState)(false);
1165
+ const [syncing, setSyncing] = (0, react.useState)(false);
1166
+ const [feedback, setFeedback] = (0, react.useState)(undefined);
1167
+ const loadPage = (next) => {
1168
+ setLoading(true);
1169
+ setListError(undefined);
1170
+ api.listKeys(next, PAGE_SIZE).then((response) => {
1171
+ setKeys((previous) => next === 1 ? response.keys : [...previous, ...response.keys]);
1172
+ setPages(response.pages);
1173
+ setTotal(response.total);
1174
+ setPage(next);
1175
+ }).catch((cause) => {
1176
+ setListError(cause instanceof Error ? cause.message : String(cause));
1177
+ }).finally(() => {
1178
+ setLoading(false);
1179
+ });
1180
+ };
1181
+ (0, react.useEffect)(() => {
1182
+ loadPage(1);
1183
+ }, [api]);
1184
+ const selectedIds = snapshot$1.value?.selectedKeys ?? [];
1185
+ const toggle = (keyId, next) => {
1186
+ const nextSet = new Set(selectedIds);
1187
+ if (next) nextSet.add(keyId);
1188
+ else nextSet.delete(keyId);
1189
+ void setField(config, "selectedKeys", [...nextSet].sort((a, b) => a - b));
1190
+ };
1191
+ const syncSelected = () => {
1192
+ if (selectedIds.length === 0) return;
1193
+ setSyncing(true);
1194
+ setFeedback(undefined);
1195
+ api.sync([...selectedIds]).then(async (report) => {
1196
+ const okCount = report.report.entries.filter((entry) => entry.action !== "failed").length;
1197
+ setFeedback({
1198
+ tone: okCount === report.report.entries.length ? "ok" : "error",
1199
+ text: t("syncSelectedDone", {
1200
+ ok: okCount,
1201
+ total: report.report.entries.length
1202
+ })
1203
+ });
1204
+ await pruneInvalidSelection(config, report.report);
1205
+ await refreshStatus();
1206
+ }).catch((cause) => {
1207
+ setFeedback({
1208
+ tone: "error",
1209
+ text: cause instanceof Error ? cause.message : String(cause)
1210
+ });
1211
+ }).finally(() => {
1212
+ setSyncing(false);
1213
+ });
1214
+ };
1215
+ const groupNameOf = (gid) => groups.find((group) => group.id === gid)?.name ?? `#${gid}`;
1216
+ /** Pick a group: the name field auto-fills with the group name unless typed. */
1217
+ const pickGroup = (gid) => {
1218
+ setGroupId(gid);
1219
+ if (!nameTouched) setName(groupNameOf(gid));
1220
+ };
1221
+ const closeCreate = () => {
1222
+ setCreating(false);
1223
+ setGroupId("");
1224
+ setName("");
1225
+ setNameTouched(false);
1226
+ };
1227
+ const create = () => {
1228
+ if (groupId === "") return;
1229
+ const finalName = (nameTouched ? name$1.trim() : "") || groupNameOf(groupId);
1230
+ setBusy(true);
1231
+ setFeedback(undefined);
1232
+ api.createKey({
1233
+ groupId,
1234
+ name: finalName
1235
+ }).then(async (response) => {
1236
+ const nextSet = new Set(snapshot$1.value?.selectedKeys ?? []);
1237
+ nextSet.add(response.key.id);
1238
+ await setField(config, "selectedKeys", [...nextSet].sort((a, b) => a - b));
1239
+ setFeedback({
1240
+ tone: "ok",
1241
+ text: t("keyCreated", { name: response.key.name })
1242
+ });
1243
+ closeCreate();
1244
+ loadPage(1);
1245
+ }).catch((cause) => {
1246
+ setFeedback({
1247
+ tone: "error",
1248
+ text: cause instanceof Error ? cause.message : String(cause)
1249
+ });
1250
+ }).finally(() => {
1251
+ setBusy(false);
1252
+ });
1253
+ };
1254
+ const createFooter = (0, react_jsx_runtime.jsxs)("div", {
1255
+ className: "s2s-actions",
1256
+ children: [(0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
1257
+ variant: "ghost",
1258
+ disabled: busy,
1259
+ onClick: closeCreate,
1260
+ children: t("cancel")
1261
+ }), (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
1262
+ variant: "primary",
1263
+ disabled: busy || groupId === "",
1264
+ onClick: create,
1265
+ children: t("createKey")
1266
+ })]
1267
+ });
1268
+ return (0, react_jsx_runtime.jsxs)("section", {
1269
+ className: "s2s-card",
1270
+ children: [
1271
+ (0, react_jsx_runtime.jsxs)("div", {
1272
+ className: "s2s-row",
1273
+ children: [(0, react_jsx_runtime.jsx)("span", {
1274
+ className: "s2s-name",
1275
+ children: t("keysTitle")
1276
+ }), (0, react_jsx_runtime.jsxs)("div", {
1277
+ className: "s2s-actions",
1278
+ children: [
1279
+ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
1280
+ variant: "outline",
1281
+ onClick: () => {
1282
+ loadPage(1);
1283
+ },
1284
+ children: t("groupsRefresh")
1285
+ }),
1286
+ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
1287
+ variant: "primary",
1288
+ disabled: syncing || selectedIds.length === 0,
1289
+ onClick: syncSelected,
1290
+ children: syncing ? t("syncing") : t("syncSelected", { count: selectedIds.length })
1291
+ }),
1292
+ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
1293
+ variant: "primary",
1294
+ onClick: () => {
1295
+ setCreating(true);
1296
+ },
1297
+ children: t("createKey")
1298
+ })
1299
+ ]
1300
+ })]
1301
+ }),
1302
+ feedback !== undefined && (0, react_jsx_runtime.jsx)("p", {
1303
+ className: `s2s-feedback ${feedback.tone === "ok" ? "s2s-feedbackOk" : "s2s-feedbackError"}`,
1304
+ children: feedback.text
1305
+ }),
1306
+ listError !== undefined && (0, react_jsx_runtime.jsx)("p", {
1307
+ className: "s2s-feedback s2s-feedbackError",
1308
+ children: listError
1309
+ }),
1310
+ !loading && keys.length === 0 && listError === undefined && (0, react_jsx_runtime.jsx)("p", {
1311
+ className: "s2s-feedback s2s-hint",
1312
+ children: t("keysListEmpty")
1313
+ }),
1314
+ loading && keys.length === 0 && (0, react_jsx_runtime.jsx)("p", {
1315
+ className: "s2s-feedback s2s-hint",
1316
+ children: t("keysListLoading")
1317
+ }),
1318
+ keys.length > 0 && (0, react_jsx_runtime.jsx)("div", {
1319
+ className: "s2s-column",
1320
+ children: keys.map((key) => {
1321
+ const platform = groups.find((group) => group.id === key.groupId)?.platform;
1322
+ const protocol = platform === undefined ? undefined : protocolForPlatform(platform);
1323
+ return (0, react_jsx_runtime.jsxs)("div", {
1324
+ className: "s2s-keyRow",
1325
+ onClick: (event) => {
1326
+ if (event.target.closest("label") !== null) return;
1327
+ toggle(key.id, !selectedIds.includes(key.id));
1328
+ },
1329
+ children: [
1330
+ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Checkbox, {
1331
+ checked: selectedIds.includes(key.id),
1332
+ onChange: (next) => {
1333
+ toggle(key.id, next);
1334
+ },
1335
+ label: key.name
1336
+ }),
1337
+ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Tag, {
1338
+ tone: "neutral",
1339
+ children: groupNameOf(key.groupId)
1340
+ }),
1341
+ key.status !== undefined ? (0, react_jsx_runtime.jsx)("span", {
1342
+ className: "s2s-badgeHint",
1343
+ children: key.status
1344
+ }) : null,
1345
+ (0, react_jsx_runtime.jsx)("span", {
1346
+ className: "s2s-badgeHint",
1347
+ title: platform === undefined ? undefined : t("groupTypeTitle", {
1348
+ type: platformLabel(platform),
1349
+ protocol: protocol ?? "—"
1350
+ }),
1351
+ children: platform === undefined ? "—" : platformLabel(platform)
1352
+ })
1353
+ ]
1354
+ }, key.id);
1355
+ })
1356
+ }),
1357
+ keys.length > 0 && page < pages && (0, react_jsx_runtime.jsx)("div", {
1358
+ className: "s2s-actions",
1359
+ children: (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
1360
+ variant: "outline",
1361
+ disabled: loading,
1362
+ onClick: () => {
1363
+ loadPage(page + 1);
1364
+ },
1365
+ children: t("keysLoadMore", { count: Math.max(0, Math.min(PAGE_SIZE, total - keys.length)) })
1366
+ })
1367
+ }),
1368
+ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Modal, {
1369
+ open: creating,
1370
+ onClose: closeCreate,
1371
+ title: t("createKey"),
1372
+ closeLabel: t("close"),
1373
+ footer: createFooter,
1374
+ children: (0, react_jsx_runtime.jsxs)("div", {
1375
+ className: "s2s-column",
1376
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1377
+ className: "s2s-inline",
1378
+ children: [(0, react_jsx_runtime.jsx)("span", {
1379
+ className: "s2s-fieldLabel",
1380
+ children: t("keyGroupLabel")
1381
+ }), (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Menu, {
1382
+ open: groupMenuOpen,
1383
+ anchor: (0, react_jsx_runtime.jsx)("span", {
1384
+ className: "s2s-menuAnchor",
1385
+ children: (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
1386
+ variant: "outline",
1387
+ onClick: () => {
1388
+ setGroupMenuOpen(true);
1389
+ },
1390
+ children: (0, react_jsx_runtime.jsxs)("span", {
1391
+ className: "s2s-selectTrigger",
1392
+ children: [(0, react_jsx_runtime.jsx)("span", { children: groupId === "" ? t("keyGroupPlaceholder") : groupNameOf(groupId) }), (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, {
1393
+ size: 14,
1394
+ className: "s2s-selectChevron"
1395
+ })]
1396
+ })
1397
+ })
1398
+ }),
1399
+ items: groups.map((group) => ({
1400
+ id: String(group.id),
1401
+ label: group.name
1402
+ })),
1403
+ selectedId: groupId === "" ? undefined : String(groupId),
1404
+ onSelect: (id) => {
1405
+ pickGroup(Number(id));
1406
+ setGroupMenuOpen(false);
1407
+ },
1408
+ onClose: () => {
1409
+ setGroupMenuOpen(false);
1410
+ }
1411
+ })]
1412
+ }), (0, react_jsx_runtime.jsxs)("div", {
1413
+ className: "s2s-inline",
1414
+ children: [(0, react_jsx_runtime.jsx)("span", {
1415
+ className: "s2s-fieldLabel",
1416
+ children: t("keyNameLabel")
1417
+ }), (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Input, {
1418
+ className: "s2s-input",
1419
+ type: "text",
1420
+ value: nameTouched ? name$1 : groupId === "" ? "" : groupNameOf(groupId),
1421
+ placeholder: t("keyNameHint"),
1422
+ disabled: busy || groupId === "",
1423
+ onChange: (event) => {
1424
+ setName(event.target.value);
1425
+ setNameTouched(true);
1426
+ }
1427
+ })]
1428
+ })]
1429
+ })
1430
+ })
1431
+ ]
1432
+ });
1433
+ }
1434
+
1435
+ //#endregion
1436
+ //#region src/client/section/header.ts
1437
+ function maskEmail(email) {
1438
+ const trimmed = email.trim();
1439
+ const at = trimmed.indexOf("@");
1440
+ if (at <= 0) return trimmed.length > 0 ? `${trimmed.slice(0, 1)}***` : "—";
1441
+ return `${trimmed.slice(0, 1)}***${trimmed.slice(at)}`;
1442
+ }
1443
+ function accountLabelOf(profile) {
1444
+ const username = profile?.username?.trim();
1445
+ if (username !== undefined && username.length > 0) return username;
1446
+ const email = profile?.email?.trim();
1447
+ return email !== undefined && email.length > 0 ? maskEmail(email) : undefined;
1448
+ }
1449
+ function formatBalance(amount) {
1450
+ const digits = amount > 0 && amount < .01 ? 6 : 2;
1451
+ return new Intl.NumberFormat("en-US", {
1452
+ style: "currency",
1453
+ currency: "USD",
1454
+ minimumFractionDigits: digits,
1455
+ maximumFractionDigits: digits
1456
+ }).format(amount);
1457
+ }
1458
+
1459
+ //#endregion
1460
+ //#region src/client/section/Sub2apiSyncSection.tsx
1461
+ function messageOf$1(error) {
1462
+ return error instanceof Error ? error.message : String(error);
1463
+ }
1464
+ /** The instance display host: the URL's host when parseable, else the raw value. */
1465
+ function hostOf(instance) {
1466
+ const trimmed = instance.trim();
1467
+ if (trimmed.length === 0) return "—";
1468
+ try {
1469
+ return new URL(trimmed).host;
1470
+ } catch {
1471
+ return trimmed;
1472
+ }
1473
+ }
1474
+ function actionKeyOf$1(action) {
1475
+ switch (action) {
1476
+ case "created": return "actionCreated";
1477
+ case "updated": return "actionUpdated";
1478
+ case "unchanged": return "actionUnchanged";
1479
+ case "failed": return "actionFailed";
1480
+ }
1481
+ }
1482
+ function actionToneOf(action) {
1483
+ switch (action) {
1484
+ case "created": return "success";
1485
+ case "updated": return "info";
1486
+ case "unchanged": return "outline";
1487
+ case "failed": return "danger";
1488
+ }
1489
+ }
1490
+ function Sub2apiSyncSection({ t, api, config, useStatus: useStatus$1 }) {
1491
+ const status = useStatus$1((value) => value);
1492
+ const snapshot$1 = useConfigForm(config);
1493
+ const configValues = snapshot$1.value;
1494
+ const writable = snapshot$1.writable;
1495
+ const auth = status?.auth;
1496
+ const signedIn = auth?.signedIn ?? false;
1497
+ const syncRunning = status?.syncRunning ?? false;
1498
+ const configuredBaseURL = configValues?.baseURL;
1499
+ const dotState = signedIn ? "done" : "idle";
1500
+ const instanceOrigin = (() => {
1501
+ try {
1502
+ return new URL(status?.instance ?? "").origin;
1503
+ } catch {
1504
+ return "";
1505
+ }
1506
+ })();
1507
+ const iconSrc = instanceOrigin !== "" ? `${ROUTES.icon}?instance=${encodeURIComponent(instanceOrigin)}` : undefined;
1508
+ const [faviconBroken, setFaviconBroken] = (0, react.useState)(false);
1509
+ (0, react.useEffect)(() => {
1510
+ setFaviconBroken(false);
1511
+ }, [iconSrc]);
1512
+ const previousSignedIn = (0, react.useRef)(signedIn);
1513
+ (0, react.useEffect)(() => {
1514
+ const was = previousSignedIn.current;
1515
+ previousSignedIn.current = signedIn;
1516
+ if (was && !signedIn) void setField(config, "selectedKeys", []);
1517
+ }, [signedIn, config]);
1518
+ const instanceMarker = (configValues?.baseURL ?? "").trim().replace(/\/+$/, "").toLowerCase();
1519
+ const previousInstance = (0, react.useRef)(undefined);
1520
+ (0, react.useEffect)(() => {
1521
+ const markerKey = `${PKG_NAME}:last-instance`;
1522
+ let stored;
1523
+ try {
1524
+ stored = window.localStorage.getItem(markerKey) ?? undefined;
1525
+ window.localStorage.setItem(markerKey, instanceMarker);
1526
+ } catch {}
1527
+ const was = previousInstance.current ?? stored;
1528
+ previousInstance.current = instanceMarker;
1529
+ if (was !== undefined && was.length > 0 && was !== instanceMarker) {
1530
+ void setField(config, "selectedKeys", []);
1531
+ void api.resetSyncState(was).catch(() => undefined);
1532
+ }
1533
+ }, [
1534
+ instanceMarker,
1535
+ api,
1536
+ config
1537
+ ]);
1538
+ const [siteName, setSiteName] = (0, react.useState)(undefined);
1539
+ (0, react.useEffect)(() => {
1540
+ if (!signedIn || instanceOrigin === "") {
1541
+ setSiteName(undefined);
1542
+ return;
1543
+ }
1544
+ let alive = true;
1545
+ api.site().then((response) => {
1546
+ if (alive) setSiteName(response.name);
1547
+ }).catch(() => {
1548
+ if (alive) setSiteName(undefined);
1549
+ });
1550
+ return () => {
1551
+ alive = false;
1552
+ };
1553
+ }, [
1554
+ api,
1555
+ signedIn,
1556
+ instanceOrigin
1557
+ ]);
1558
+ const [account, setAccount] = (0, react.useState)(undefined);
1559
+ (0, react.useEffect)(() => {
1560
+ if (!signedIn || syncRunning) return;
1561
+ let alive = true;
1562
+ api.account().then((response) => {
1563
+ if (!alive) return;
1564
+ if (response.error !== undefined) console.warn("[sub2api-sync] account:", response.error);
1565
+ setAccount({
1566
+ username: response.username,
1567
+ email: response.email,
1568
+ balance: response.balance
1569
+ });
1570
+ }).catch((cause) => {
1571
+ console.warn("[sub2api-sync] account fetch failed", cause);
1572
+ if (alive) setAccount(undefined);
1573
+ });
1574
+ return () => {
1575
+ alive = false;
1576
+ };
1577
+ }, [
1578
+ api,
1579
+ signedIn,
1580
+ syncRunning
1581
+ ]);
1582
+ const accountSegment = accountLabelOf(account);
1583
+ const nameLine = siteName !== undefined && siteName.length > 0 ? t("nameLineSite", {
1584
+ name: siteName,
1585
+ instance: hostOf(status?.instance ?? "")
1586
+ }) : t("nameLine", { instance: hostOf(status?.instance ?? "") });
1587
+ const secondary = !signedIn ? t("signedOut") : auth?.kind === "oauth" ? accountSegment !== undefined ? t("signedInOAuthAccount", { account: accountSegment }) : t("signedInOAuth") : t("signedInAs", { email: auth?.email ?? "" });
1588
+ const routeCount = status?.routes.length ?? 0;
1589
+ const selectedCount = configValues?.selectedKeys?.length ?? 0;
1590
+ const report = status?.lastReport;
1591
+ const [signInOpen, setSignInOpen] = (0, react.useState)(false);
1592
+ const [accountBusy, setAccountBusy] = (0, react.useState)(false);
1593
+ const [accountFeedback, setAccountFeedback] = (0, react.useState)(undefined);
1594
+ const [groups, setGroups] = (0, react.useState)(undefined);
1595
+ const fetchGroups = (0, react.useCallback)(() => {
1596
+ api.groups().then((response) => {
1597
+ setGroups(response.groups);
1598
+ }).catch(() => {
1599
+ setGroups(undefined);
1600
+ });
1601
+ }, [api]);
1602
+ (0, react.useEffect)(() => {
1603
+ if (!signedIn) {
1604
+ setGroups(undefined);
1605
+ return;
1606
+ }
1607
+ fetchGroups();
1608
+ }, [signedIn, fetchGroups]);
1609
+ const [syncBusy, setSyncBusy] = (0, react.useState)(false);
1610
+ const [syncFeedback, setSyncFeedback] = (0, react.useState)(undefined);
1611
+ const signOut = () => {
1612
+ setAccountBusy(true);
1613
+ setAccountFeedback(undefined);
1614
+ api.logout().then(() => {
1615
+ setAccountFeedback({
1616
+ tone: "ok",
1617
+ text: t("signOutDone")
1618
+ });
1619
+ return refreshStatus();
1620
+ }).catch((cause) => {
1621
+ setAccountFeedback({
1622
+ tone: "error",
1623
+ text: t("actionFailedLabel", { message: messageOf$1(cause) })
1624
+ });
1625
+ }).finally(() => {
1626
+ setAccountBusy(false);
1627
+ });
1628
+ };
1629
+ const onSignedIn = () => {
1630
+ setAccountFeedback({
1631
+ tone: "ok",
1632
+ text: t("signedInNotice")
1633
+ });
1634
+ void refreshStatus();
1635
+ };
1636
+ const syncNow = () => {
1637
+ setSyncBusy(true);
1638
+ setSyncFeedback(undefined);
1639
+ api.sync().then(async (response) => {
1640
+ setSyncFeedback({
1641
+ tone: "ok",
1642
+ text: t("syncDone")
1643
+ });
1644
+ await pruneInvalidSelection(config, response.report);
1645
+ return refreshStatus();
1646
+ }).catch((cause) => {
1647
+ setSyncFeedback({
1648
+ tone: "error",
1649
+ text: t("actionFailedLabel", { message: messageOf$1(cause) })
1650
+ });
1651
+ }).finally(() => {
1652
+ setSyncBusy(false);
1653
+ });
1654
+ };
1655
+ const [removingRoute, setRemovingRoute] = (0, react.useState)(undefined);
1656
+ const removeRoute = (route) => {
1657
+ const keyId = route.keyId;
1658
+ if (keyId === undefined || removingRoute !== undefined) return;
1659
+ setRemovingRoute(route.route);
1660
+ api.removeKey(keyId).then(async () => {
1661
+ setSyncFeedback({
1662
+ tone: "ok",
1663
+ text: t("entryRemoveDone", { name: route.displayName ?? route.route })
1664
+ });
1665
+ await dropSelectedKey(config, keyId).catch(() => undefined);
1666
+ await refreshStatus();
1667
+ }).catch((cause) => {
1668
+ setSyncFeedback({
1669
+ tone: "error",
1670
+ text: t("actionFailedLabel", { message: messageOf$1(cause) })
1671
+ });
1672
+ }).finally(() => {
1673
+ setRemovingRoute(undefined);
1674
+ });
1675
+ };
1676
+ return (0, react_jsx_runtime.jsxs)("div", {
1677
+ className: "s2s-section",
1678
+ children: [
1679
+ snapshot$1.status === "ready" && !writable && (0, react_jsx_runtime.jsx)("p", {
1680
+ className: "s2s-warn",
1681
+ children: t("localOnlyWarning")
1682
+ }),
1683
+ snapshot$1.status === "unavailable" && (0, react_jsx_runtime.jsx)("p", {
1684
+ className: "s2s-warn",
1685
+ children: t("configUnavailable")
1686
+ }),
1687
+ (0, react_jsx_runtime.jsx)("section", {
1688
+ className: "s2s-card",
1689
+ children: (0, react_jsx_runtime.jsxs)("div", {
1690
+ className: "s2s-row",
1691
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1692
+ className: "s2s-identity",
1693
+ children: [(0, react_jsx_runtime.jsx)("div", {
1694
+ className: "s2s-avatar",
1695
+ children: signedIn && iconSrc !== undefined && !faviconBroken ? (0, react_jsx_runtime.jsx)("img", {
1696
+ className: "s2s-favicon",
1697
+ src: iconSrc,
1698
+ alt: "",
1699
+ onError: () => {
1700
+ setFaviconBroken(true);
1701
+ }
1702
+ }) : (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.StateDot, {
1703
+ state: dotState,
1704
+ size: 16
1705
+ })
1706
+ }), (0, react_jsx_runtime.jsxs)("div", {
1707
+ className: "s2s-identityCopy",
1708
+ children: [(0, react_jsx_runtime.jsx)("span", {
1709
+ className: "s2s-name",
1710
+ children: nameLine
1711
+ }), (0, react_jsx_runtime.jsx)("span", {
1712
+ className: "s2s-secondary",
1713
+ children: secondary
1714
+ })]
1715
+ })]
1716
+ }), (0, react_jsx_runtime.jsxs)("div", {
1717
+ className: "s2s-actions",
1718
+ children: [
1719
+ accountFeedback !== undefined && (0, react_jsx_runtime.jsx)("span", {
1720
+ className: `s2s-feedback ${accountFeedback.tone === "ok" ? "s2s-feedbackOk" : "s2s-feedbackError"}`,
1721
+ children: accountFeedback.text
1722
+ }),
1723
+ signedIn && account?.balance !== undefined && (0, react_jsx_runtime.jsx)("span", {
1724
+ className: "s2s-balance",
1725
+ title: t("balanceTitle"),
1726
+ children: t("balanceLabel", { amount: formatBalance(account.balance) })
1727
+ }),
1728
+ signedIn ? (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
1729
+ variant: "outline",
1730
+ disabled: accountBusy || syncRunning,
1731
+ title: syncRunning ? t("syncing") : undefined,
1732
+ onClick: signOut,
1733
+ children: t("signOut")
1734
+ }) : (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
1735
+ variant: "primary",
1736
+ disabled: accountBusy,
1737
+ onClick: () => {
1738
+ setSignInOpen(true);
1739
+ },
1740
+ children: t("signIn")
1741
+ })
1742
+ ]
1743
+ })]
1744
+ })
1745
+ }),
1746
+ !signedIn && (0, react_jsx_runtime.jsxs)("section", {
1747
+ className: "s2s-card",
1748
+ children: [(0, react_jsx_runtime.jsx)("div", {
1749
+ className: "s2s-row",
1750
+ children: (0, react_jsx_runtime.jsx)("span", {
1751
+ className: "s2s-name",
1752
+ children: t("accountTitle")
1753
+ })
1754
+ }), (0, react_jsx_runtime.jsx)("p", {
1755
+ className: "s2s-feedback s2s-hint",
1756
+ children: t("accountSignedOutHint")
1757
+ })]
1758
+ }),
1759
+ signedIn && (0, react_jsx_runtime.jsx)(KeysCard, {
1760
+ api,
1761
+ config,
1762
+ groups: groups ?? [],
1763
+ t
1764
+ }),
1765
+ (0, react_jsx_runtime.jsxs)("section", {
1766
+ className: "s2s-card",
1767
+ children: [
1768
+ (0, react_jsx_runtime.jsxs)("div", {
1769
+ className: "s2s-row",
1770
+ children: [(0, react_jsx_runtime.jsx)("span", {
1771
+ className: "s2s-name",
1772
+ children: t("syncTitle")
1773
+ }), (0, react_jsx_runtime.jsxs)("div", {
1774
+ className: "s2s-actions",
1775
+ children: [(syncRunning || syncBusy) && (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.TextShimmer, {
1776
+ active: true,
1777
+ children: t("syncing")
1778
+ }), (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
1779
+ variant: "primary",
1780
+ disabled: !signedIn || !writable || syncBusy || syncRunning || selectedCount === 0,
1781
+ onClick: syncNow,
1782
+ children: t("syncNow")
1783
+ })]
1784
+ })]
1785
+ }),
1786
+ routeCount > 0 && (0, react_jsx_runtime.jsx)("p", {
1787
+ className: "s2s-feedback s2s-hint",
1788
+ children: t("routesSummary", { count: routeCount })
1789
+ }),
1790
+ status?.routes.map((route) => (0, react_jsx_runtime.jsxs)("div", {
1791
+ className: "s2s-reportLine",
1792
+ children: [
1793
+ (0, react_jsx_runtime.jsx)("span", {
1794
+ className: "s2s-reportMark",
1795
+ "data-mark": "ok",
1796
+ "aria-hidden": "true"
1797
+ }),
1798
+ (0, react_jsx_runtime.jsxs)("span", {
1799
+ className: "s2s-reportText",
1800
+ children: [
1801
+ t("entryModels", {
1802
+ name: route.displayName ?? route.route,
1803
+ count: route.models
1804
+ }),
1805
+ " ",
1806
+ route.api !== undefined ? (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Tag, {
1807
+ tone: "info",
1808
+ children: route.api
1809
+ }) : null
1810
+ ]
1811
+ }),
1812
+ route.keyId !== undefined && (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
1813
+ variant: "ghost",
1814
+ size: "sm",
1815
+ className: "s2s-reportRemove",
1816
+ disabled: removingRoute !== undefined,
1817
+ onClick: () => {
1818
+ removeRoute(route);
1819
+ },
1820
+ children: t("entryRemove")
1821
+ })
1822
+ ]
1823
+ }, route.route)),
1824
+ syncFeedback !== undefined && (0, react_jsx_runtime.jsx)("p", {
1825
+ className: `s2s-feedback ${syncFeedback.tone === "ok" ? "s2s-feedbackOk" : "s2s-feedbackError"}`,
1826
+ children: syncFeedback.text
1827
+ }),
1828
+ signedIn && (0, react_jsx_runtime.jsx)(SyncReportView, {
1829
+ t,
1830
+ report
1831
+ })
1832
+ ]
1833
+ }),
1834
+ (0, react_jsx_runtime.jsx)(SignInDialog, {
1835
+ open: signInOpen,
1836
+ onClose: () => {
1837
+ setSignInOpen(false);
1838
+ },
1839
+ api,
1840
+ config,
1841
+ baseURL: configuredBaseURL,
1842
+ writable,
1843
+ onSignedIn,
1844
+ t
1845
+ })
1846
+ ]
1847
+ });
1848
+ }
1849
+ /**
1850
+ * The last sync report — a hint about the previous run (meta line, per-entry
1851
+ * outcomes, removals, warnings). No actions live here: removal belongs on
1852
+ * the registered-routes list above, which reflects the live state.
1853
+ */
1854
+ function SyncReportView({ t, report }) {
1855
+ if (report === undefined) return (0, react_jsx_runtime.jsx)("p", {
1856
+ className: "s2s-feedback s2s-hint",
1857
+ children: t("reportEmpty")
1858
+ });
1859
+ return (0, react_jsx_runtime.jsxs)("div", {
1860
+ className: "s2s-column",
1861
+ children: [
1862
+ (0, react_jsx_runtime.jsx)("span", {
1863
+ className: "s2s-hint",
1864
+ children: t("reportMeta", {
1865
+ writes: report.writes,
1866
+ attempts: report.attempts
1867
+ })
1868
+ }),
1869
+ report.entries.map((entry, index) => (0, react_jsx_runtime.jsxs)("div", {
1870
+ className: "s2s-reportLine",
1871
+ children: [(0, react_jsx_runtime.jsx)("span", {
1872
+ className: "s2s-reportMark",
1873
+ "data-mark": entry.action === "failed" ? "fail" : "ok",
1874
+ "aria-hidden": "true"
1875
+ }), (0, react_jsx_runtime.jsxs)("span", {
1876
+ className: `s2s-reportText${entry.action === "failed" ? " s2s-reportError" : ""}`,
1877
+ children: [
1878
+ entry.action === "failed" ? t("entryError", {
1879
+ name: entry.displayName ?? entry.route,
1880
+ error: entry.error ?? ""
1881
+ }) : t("entryModels", {
1882
+ name: entry.displayName ?? entry.route,
1883
+ count: entry.models ?? 0
1884
+ }),
1885
+ " ",
1886
+ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Tag, {
1887
+ tone: actionToneOf(entry.action),
1888
+ children: t(actionKeyOf$1(entry.action))
1889
+ })
1890
+ ]
1891
+ })]
1892
+ }, `${entry.groupId}-${index}`)),
1893
+ report.removed.length > 0 && (0, react_jsx_runtime.jsx)("span", {
1894
+ className: "s2s-reportText",
1895
+ children: t("reportRemoved", { routes: report.removed.join(", ") })
1896
+ }),
1897
+ report.warnings.map((warning, index) => (0, react_jsx_runtime.jsx)("span", {
1898
+ className: "s2s-reportText s2s-reportError",
1899
+ children: t("reportWarningLine", { message: warning })
1900
+ }, index))
1901
+ ]
1902
+ });
1903
+ }
1904
+
1905
+ //#endregion
1906
+ //#region src/client/bundleConfig/BundleConfigCard.tsx
1907
+ function parseNumber(text) {
1908
+ const trimmed = text.trim();
1909
+ if (trimmed.length === 0) return "";
1910
+ const value = Number(trimmed);
1911
+ return Number.isFinite(value) ? value : "invalid";
1912
+ }
1913
+ function messageOf(error) {
1914
+ return error instanceof Error ? error.message : String(error);
1915
+ }
1916
+ function BundleConfigCard({ view, form, t }) {
1917
+ if (view !== "page" || form === undefined) return null;
1918
+ return (0, react_jsx_runtime.jsx)(BundleConfigFields, {
1919
+ form,
1920
+ t
1921
+ });
1922
+ }
1923
+ function BundleConfigFields({ form, t }) {
1924
+ const values = form.state.value ?? {};
1925
+ const writable = form.state.writable;
1926
+ const [busy, setBusy] = (0, react.useState)(false);
1927
+ const [feedback, setFeedback] = (0, react.useState)(undefined);
1928
+ const run = (ops) => {
1929
+ setBusy(true);
1930
+ setFeedback(undefined);
1931
+ form.mutate(ops).then((accepted) => {
1932
+ setFeedback(accepted ? {
1933
+ tone: "ok",
1934
+ text: t("savedNotice")
1935
+ } : {
1936
+ tone: "error",
1937
+ text: t("writeRefused")
1938
+ });
1939
+ }).catch((cause) => {
1940
+ setFeedback({
1941
+ tone: "error",
1942
+ text: t("actionFailedLabel", { message: messageOf(cause) })
1943
+ });
1944
+ }).finally(() => {
1945
+ setBusy(false);
1946
+ });
1947
+ };
1948
+ const [templateDraft, setTemplateDraft] = (0, react.useState)(values.providerNameTemplate ?? "");
1949
+ (0, react.useEffect)(() => {
1950
+ setTemplateDraft(values.providerNameTemplate ?? "");
1951
+ }, [values.providerNameTemplate]);
1952
+ const [quotaDraft, setQuotaDraft] = (0, react.useState)(values.keyQuota === undefined ? "" : String(values.keyQuota));
1953
+ (0, react.useEffect)(() => {
1954
+ setQuotaDraft(values.keyQuota === undefined ? "" : String(values.keyQuota));
1955
+ }, [values.keyQuota]);
1956
+ const [expiryDraft, setExpiryDraft] = (0, react.useState)(values.keyExpiresInDays === undefined ? "" : String(values.keyExpiresInDays));
1957
+ (0, react.useEffect)(() => {
1958
+ setExpiryDraft(values.keyExpiresInDays === undefined ? "" : String(values.keyExpiresInDays));
1959
+ }, [values.keyExpiresInDays]);
1960
+ const [intervalDraft, setIntervalDraft] = (0, react.useState)(values.syncIntervalMs === undefined ? "" : String(values.syncIntervalMs));
1961
+ (0, react.useEffect)(() => {
1962
+ setIntervalDraft(values.syncIntervalMs === undefined ? "" : String(values.syncIntervalMs));
1963
+ }, [values.syncIntervalMs]);
1964
+ const templateInvalid = !templateDraft.includes("{keyName}");
1965
+ const quotaParsed = parseNumber(quotaDraft);
1966
+ const quotaInvalid = quotaParsed === "invalid" || quotaParsed !== "" && quotaParsed < 0;
1967
+ const expiryParsed = parseNumber(expiryDraft);
1968
+ const expiryInvalid = expiryParsed === "invalid" || expiryParsed !== "" && (!Number.isInteger(expiryParsed) || expiryParsed < 1);
1969
+ const intervalParsed = parseNumber(intervalDraft);
1970
+ const intervalInvalid = intervalParsed === "invalid" || intervalParsed !== "" && (!Number.isInteger(intervalParsed) || intervalParsed < 6e4);
1971
+ const numberOp = (field, parsed) => parsed === "" ? {
1972
+ op: "unset",
1973
+ path: [field]
1974
+ } : {
1975
+ op: "set",
1976
+ path: [field],
1977
+ value: parsed
1978
+ };
1979
+ return (0, react_jsx_runtime.jsx)("div", {
1980
+ className: "s2s-section",
1981
+ children: (0, react_jsx_runtime.jsxs)("section", {
1982
+ className: "s2s-card",
1983
+ children: [
1984
+ (0, react_jsx_runtime.jsx)(TextFieldRow, {
1985
+ id: "sub2api-sync-provider-name-template",
1986
+ label: t("bundleTemplateLabel"),
1987
+ value: templateDraft,
1988
+ hint: t("bundleTemplateHint"),
1989
+ invalid: templateInvalid,
1990
+ invalidHint: t("bundleTemplateInvalid"),
1991
+ disabled: !writable || busy,
1992
+ saveDisabled: templateDraft.trim() === (values.providerNameTemplate ?? ""),
1993
+ onChange: setTemplateDraft,
1994
+ saveLabel: t("save"),
1995
+ onSave: () => {
1996
+ run([{
1997
+ op: "set",
1998
+ path: ["providerNameTemplate"],
1999
+ value: templateDraft.trim()
2000
+ }]);
2001
+ }
2002
+ }),
2003
+ (0, react_jsx_runtime.jsx)("div", { className: "s2s-divider" }),
2004
+ (0, react_jsx_runtime.jsx)(TextFieldRow, {
2005
+ id: "sub2api-sync-key-quota",
2006
+ label: t("bundleKeyQuotaLabel"),
2007
+ value: quotaDraft,
2008
+ hint: t("bundleKeyQuotaHint"),
2009
+ invalid: quotaInvalid,
2010
+ invalidHint: t("bundleNumberInvalid"),
2011
+ disabled: !writable || busy,
2012
+ saveDisabled: quotaDraft.trim() === (values.keyQuota === undefined ? "" : String(values.keyQuota)),
2013
+ onChange: setQuotaDraft,
2014
+ saveLabel: t("save"),
2015
+ onSave: () => {
2016
+ run([numberOp("keyQuota", quotaParsed)]);
2017
+ }
2018
+ }),
2019
+ (0, react_jsx_runtime.jsx)("div", { className: "s2s-divider" }),
2020
+ (0, react_jsx_runtime.jsx)(TextFieldRow, {
2021
+ id: "sub2api-sync-key-expiry",
2022
+ label: t("bundleKeyExpiresLabel"),
2023
+ value: expiryDraft,
2024
+ hint: t("bundleKeyExpiresHint"),
2025
+ invalid: expiryInvalid,
2026
+ invalidHint: t("bundleNumberInvalid"),
2027
+ disabled: !writable || busy,
2028
+ saveDisabled: expiryDraft.trim() === (values.keyExpiresInDays === undefined ? "" : String(values.keyExpiresInDays)),
2029
+ onChange: setExpiryDraft,
2030
+ saveLabel: t("save"),
2031
+ onSave: () => {
2032
+ run([numberOp("keyExpiresInDays", expiryParsed)]);
2033
+ }
2034
+ }),
2035
+ (0, react_jsx_runtime.jsx)("div", { className: "s2s-divider" }),
2036
+ (0, react_jsx_runtime.jsx)(TextFieldRow, {
2037
+ id: "sub2api-sync-sync-interval",
2038
+ label: t("bundleIntervalLabel"),
2039
+ value: intervalDraft,
2040
+ hint: t("bundleIntervalHint"),
2041
+ invalid: intervalInvalid,
2042
+ invalidHint: t("bundleIntervalInvalid"),
2043
+ disabled: !writable || busy,
2044
+ saveDisabled: intervalDraft.trim() === (values.syncIntervalMs === undefined ? "" : String(values.syncIntervalMs)),
2045
+ onChange: setIntervalDraft,
2046
+ saveLabel: t("save"),
2047
+ onSave: () => {
2048
+ run([numberOp("syncIntervalMs", intervalParsed)]);
2049
+ }
2050
+ }),
2051
+ (0, react_jsx_runtime.jsx)("div", { className: "s2s-divider" }),
2052
+ (0, react_jsx_runtime.jsx)(SwitchRow, {
2053
+ label: t("bundleDeleteOrphanLabel"),
2054
+ hint: t("bundleDeleteOrphanHint"),
2055
+ checked: values.deleteOrphanKeys === true,
2056
+ disabled: !writable || busy,
2057
+ onChange: (next) => {
2058
+ run([{
2059
+ op: "set",
2060
+ path: ["deleteOrphanKeys"],
2061
+ value: next
2062
+ }]);
2063
+ }
2064
+ }),
2065
+ (0, react_jsx_runtime.jsx)(SwitchRow, {
2066
+ label: t("bundleEnrichLabel"),
2067
+ hint: t("bundleEnrichHint"),
2068
+ checked: values.enrichModelMeta !== false,
2069
+ disabled: !writable || busy,
2070
+ onChange: (next) => {
2071
+ run([{
2072
+ op: "set",
2073
+ path: ["enrichModelMeta"],
2074
+ value: next
2075
+ }]);
2076
+ }
2077
+ }),
2078
+ (0, react_jsx_runtime.jsx)(SwitchRow, {
2079
+ label: t("bundleAllowRemoteLabel"),
2080
+ checked: values.allowRemoteActions === true,
2081
+ disabled: !writable || busy,
2082
+ onChange: (next) => {
2083
+ run([{
2084
+ op: "set",
2085
+ path: ["allowRemoteActions"],
2086
+ value: next
2087
+ }]);
2088
+ }
2089
+ }),
2090
+ (0, react_jsx_runtime.jsx)("p", {
2091
+ className: "s2s-feedback s2s-feedbackError",
2092
+ children: t("bundleAllowRemoteWarning")
2093
+ }),
2094
+ feedback !== undefined && (0, react_jsx_runtime.jsx)("p", {
2095
+ className: `s2s-feedback ${feedback.tone === "ok" ? "s2s-feedbackOk" : "s2s-feedbackError"}`,
2096
+ children: feedback.text
2097
+ }),
2098
+ (0, react_jsx_runtime.jsx)("span", {
2099
+ className: "s2s-hint",
2100
+ children: t("bundleAdvancedHint")
2101
+ })
2102
+ ]
2103
+ })
2104
+ });
2105
+ }
2106
+ /** One labeled text field with its own save control. */
2107
+ function TextFieldRow(props) {
2108
+ return (0, react_jsx_runtime.jsxs)("div", {
2109
+ className: "s2s-column",
2110
+ children: [(0, react_jsx_runtime.jsxs)("div", {
2111
+ className: "s2s-row",
2112
+ children: [(0, react_jsx_runtime.jsx)("label", {
2113
+ className: "s2s-fieldLabel",
2114
+ htmlFor: props.id,
2115
+ children: props.label
2116
+ }), (0, react_jsx_runtime.jsxs)("div", {
2117
+ className: "s2s-actions",
2118
+ children: [(0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Input, {
2119
+ id: props.id,
2120
+ className: "s2s-input",
2121
+ type: "text",
2122
+ value: props.value,
2123
+ disabled: props.disabled,
2124
+ "aria-invalid": props.invalid,
2125
+ onChange: (event) => {
2126
+ props.onChange(event.target.value);
2127
+ }
2128
+ }), (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
2129
+ variant: "primary",
2130
+ disabled: props.disabled || props.saveDisabled || props.invalid,
2131
+ onClick: props.onSave,
2132
+ children: props.saveLabel
2133
+ })]
2134
+ })]
2135
+ }), props.invalid ? (0, react_jsx_runtime.jsx)("span", {
2136
+ className: "s2s-feedback s2s-feedbackError",
2137
+ children: props.invalidHint
2138
+ }) : (0, react_jsx_runtime.jsx)("span", {
2139
+ className: "s2s-hint",
2140
+ children: props.hint
2141
+ })]
2142
+ });
2143
+ }
2144
+ /** One labeled switch row with its hint. */
2145
+ function SwitchRow(props) {
2146
+ return (0, react_jsx_runtime.jsxs)("div", {
2147
+ className: "s2s-column",
2148
+ children: [(0, react_jsx_runtime.jsxs)("div", {
2149
+ className: "s2s-row",
2150
+ children: [(0, react_jsx_runtime.jsx)("span", {
2151
+ className: "s2s-fieldLabel",
2152
+ children: props.label
2153
+ }), (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Switch, {
2154
+ checked: props.checked,
2155
+ disabled: props.disabled,
2156
+ label: props.label,
2157
+ onChange: props.onChange
2158
+ })]
2159
+ }), props.hint !== undefined && (0, react_jsx_runtime.jsx)("span", {
2160
+ className: "s2s-hint",
2161
+ children: props.hint
2162
+ })]
2163
+ });
2164
+ }
2165
+
2166
+ //#endregion
2167
+ //#region src/client/models/badge.ts
2168
+ function isSyncedRoute(routeId) {
2169
+ return isOwnedRoute(routeId);
2170
+ }
2171
+ function badgeModel(routeId, status) {
2172
+ if (status === undefined || !isOwnedRoute(routeId)) return undefined;
2173
+ const route = status.routes.find((candidate) => candidate.route === routeId);
2174
+ if (route === undefined) return undefined;
2175
+ const entry = status.lastReport?.entries.find((candidate) => candidate.route === routeId);
2176
+ return {
2177
+ models: route.models,
2178
+ ...route.displayName !== undefined ? { displayName: route.displayName } : {},
2179
+ ...entry?.action !== undefined ? { lastAction: entry.action } : {},
2180
+ ...entry?.error !== undefined ? { lastError: entry.error } : {},
2181
+ ...status.lastReportAt !== undefined ? { lastReportAt: status.lastReportAt } : {}
2182
+ };
2183
+ }
2184
+ function footerModel(status) {
2185
+ return {
2186
+ signedIn: status?.auth.signedIn === true,
2187
+ groups: status?.routes.length ?? 0,
2188
+ ...status?.lastReportAt !== undefined ? { lastReportAt: status.lastReportAt } : {}
2189
+ };
2190
+ }
2191
+ function relativeMinutes(at, now) {
2192
+ const minutes = Math.max(0, Math.floor((now - at) / 6e4));
2193
+ if (minutes < 1) return { key: "footerJustNow" };
2194
+ return {
2195
+ key: "footerMinutesAgo",
2196
+ count: minutes
2197
+ };
2198
+ }
2199
+
2200
+ //#endregion
2201
+ //#region src/client/models/ProviderBadge.tsx
2202
+ function actionKeyOf(action) {
2203
+ switch (action) {
2204
+ case "created": return "actionCreated";
2205
+ case "updated": return "actionUpdated";
2206
+ case "unchanged": return "actionUnchanged";
2207
+ case "failed": return "actionFailed";
2208
+ }
2209
+ }
2210
+ function ProviderBadge(props) {
2211
+ const { provider, t } = props;
2212
+ if (!isSyncedRoute(provider.provider)) return null;
2213
+ return (0, react_jsx_runtime.jsx)(ProviderBadgeBody, {
2214
+ routeId: provider.provider,
2215
+ t
2216
+ });
2217
+ }
2218
+ function ProviderBadgeBody({ routeId, t }) {
2219
+ const status = useStatus();
2220
+ const badge = badgeModel(routeId, status);
2221
+ if (badge === undefined) return null;
2222
+ return (0, react_jsx_runtime.jsxs)("div", {
2223
+ className: "s2s-badgeRow",
2224
+ children: [
2225
+ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Tag, {
2226
+ tone: "info",
2227
+ children: t("badgeManagedTag")
2228
+ }),
2229
+ (0, react_jsx_runtime.jsxs)("span", {
2230
+ className: "s2s-badgeMeta",
2231
+ children: [t("badgeModels", { count: badge.models }), badge.lastAction !== undefined ? ` · ${t(actionKeyOf(badge.lastAction))}` : ""]
2232
+ }),
2233
+ (0, react_jsx_runtime.jsx)("span", {
2234
+ className: "s2s-badgeHint",
2235
+ children: t("badgeManageHint")
2236
+ })
2237
+ ]
2238
+ });
2239
+ }
2240
+
2241
+ //#endregion
2242
+ //#region src/client/models/ModelsFooter.tsx
2243
+ function ModelsFooter(props) {
2244
+ const { t } = props;
2245
+ const status = useStatus();
2246
+ const footer = footerModel(status);
2247
+ const when = footer.lastReportAt !== undefined ? (() => {
2248
+ const relative = relativeMinutes(footer.lastReportAt, Date.now());
2249
+ return t(relative.key, relative.count !== undefined ? { count: relative.count } : {});
2250
+ })() : t("footerNeverSynced");
2251
+ return (0, react_jsx_runtime.jsxs)("div", {
2252
+ className: "s2s-footerStrip",
2253
+ children: [(0, react_jsx_runtime.jsx)("span", { children: footer.signedIn ? t("footerSyncedCount", { count: footer.groups }) : t("footerSignedOut") }), (0, react_jsx_runtime.jsx)("span", { children: when })]
2254
+ });
2255
+ }
2256
+
2257
+ //#endregion
2258
+ //#region src/client/plugins/Detail.tsx
2259
+ function isOwnBundle(subject) {
2260
+ return subject.kind === "bundle" && subject.pkg.name === PKG_NAME;
2261
+ }
2262
+ function DetailBadge(props) {
2263
+ const { subject, t } = props;
2264
+ if (!isOwnBundle(subject)) return null;
2265
+ return (0, react_jsx_runtime.jsx)(DetailBadgeBody, { t });
2266
+ }
2267
+ function DetailBadgeBody({ t }) {
2268
+ const status = useStatus();
2269
+ const footer = footerModel(status);
2270
+ return (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Tag, {
2271
+ tone: footer.signedIn ? "success" : "quiet",
2272
+ children: footer.signedIn ? t("detailBadgeSynced", { count: footer.groups }) : t("detailBadgeSignedOut")
2273
+ });
2274
+ }
2275
+ function DetailSection(props) {
2276
+ const { subject, t } = props;
2277
+ if (!isOwnBundle(subject)) return null;
2278
+ return (0, react_jsx_runtime.jsx)(DetailSectionBody, { t });
2279
+ }
2280
+ function DetailSectionBody({ t }) {
2281
+ const status = useStatus();
2282
+ const report = status?.lastReport;
2283
+ return (0, react_jsx_runtime.jsxs)("section", {
2284
+ className: "s2s-card",
2285
+ children: [(0, react_jsx_runtime.jsx)("span", {
2286
+ className: "s2s-name",
2287
+ children: t("detailSectionTitle")
2288
+ }), report === undefined ? (0, react_jsx_runtime.jsx)("span", {
2289
+ className: "s2s-badgeHint",
2290
+ children: t("detailSectionEmpty")
2291
+ }) : (0, react_jsx_runtime.jsxs)("ul", {
2292
+ className: "s2s-steps",
2293
+ children: [report.entries.map((entry) => (0, react_jsx_runtime.jsx)("li", {
2294
+ className: "s2s-reportLine",
2295
+ children: (0, react_jsx_runtime.jsx)("span", {
2296
+ className: entry.action === "failed" ? "s2s-reportError" : "s2s-reportText",
2297
+ children: entry.action === "failed" ? t("entryError", {
2298
+ name: entry.displayName ?? entry.route,
2299
+ error: entry.error ?? ""
2300
+ }) : t("entryModels", {
2301
+ name: entry.displayName ?? entry.route,
2302
+ count: entry.models ?? 0
2303
+ })
2304
+ })
2305
+ }, entry.route)), report.removed.length > 0 && (0, react_jsx_runtime.jsx)("li", {
2306
+ className: "s2s-reportLine",
2307
+ children: t("reportRemoved", { routes: report.removed.join(", ") })
2308
+ })]
2309
+ })]
2310
+ });
2311
+ }
2312
+
2313
+ //#endregion
2314
+ //#region src/client/plugins/ActivationGuide.tsx
2315
+ function ActivationGuide(props) {
2316
+ const { t, onDismiss, onOpenDetails } = props;
2317
+ return (0, react_jsx_runtime.jsxs)("div", {
2318
+ className: "s2s-card",
2319
+ children: [
2320
+ (0, react_jsx_runtime.jsx)("span", {
2321
+ className: "s2s-name",
2322
+ children: t("activationTitle")
2323
+ }),
2324
+ (0, react_jsx_runtime.jsxs)("ol", {
2325
+ className: "s2s-steps",
2326
+ children: [
2327
+ (0, react_jsx_runtime.jsx)("li", { children: t("activationStep1") }),
2328
+ (0, react_jsx_runtime.jsx)("li", { children: t("activationStep2") }),
2329
+ (0, react_jsx_runtime.jsx)("li", { children: t("activationStep3") })
2330
+ ]
2331
+ }),
2332
+ (0, react_jsx_runtime.jsxs)("div", {
2333
+ className: "s2s-actions",
2334
+ children: [(0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
2335
+ variant: "primary",
2336
+ onClick: onOpenDetails,
2337
+ children: t("activationOpenConfig")
2338
+ }), (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
2339
+ variant: "ghost",
2340
+ onClick: onDismiss,
2341
+ children: t("activationDismiss")
2342
+ })]
2343
+ })
2344
+ ]
2345
+ });
2346
+ }
2347
+
2348
+ //#endregion
2349
+ //#region src/client/mount.ts
2350
+ function mount(ctx) {
2351
+ ensureCss();
2352
+ ctx.effect(() => ctx.locale.register(NS, dictionaries), "sub2api-sync: dictionaries");
2353
+ const t = ctx.locale.bind(NS);
2354
+ const config = ctx.configForms.get(NS);
2355
+ const sectionOps = {
2356
+ api: createPluginApi(),
2357
+ config,
2358
+ hooks: { status: statusSource }
2359
+ };
2360
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
2361
+ name: "settings.section",
2362
+ id: "sub2api-sync",
2363
+ order: 30,
2364
+ label: () => t("nav"),
2365
+ locale: NS,
2366
+ inject: () => sectionOps
2367
+ }, Sub2apiSyncSection));
2368
+ ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
2369
+ name: "plugins.bundle.config",
2370
+ key: PKG_NAME,
2371
+ locale: NS
2372
+ }, BundleConfigCard));
2373
+ ctx.slots.inject("settings.models.provider-card", () => ctx.slots.register({
2374
+ name: "settings.models.provider-card",
2375
+ key: "llm-pi-ai",
2376
+ locale: NS
2377
+ }, ProviderBadge));
2378
+ ctx.slots.inject("settings.models.footer", () => ctx.slots.register({
2379
+ name: "settings.models.footer",
2380
+ id: "sub2api-sync",
2381
+ order: 10,
2382
+ locale: NS
2383
+ }, ModelsFooter));
2384
+ ctx.slots.inject("plugins.detail.badge", () => ctx.slots.register({
2385
+ name: "plugins.detail.badge",
2386
+ id: "sub2api-sync",
2387
+ locale: NS
2388
+ }, DetailBadge));
2389
+ ctx.slots.inject("plugins.detail.section", () => ctx.slots.register({
2390
+ name: "plugins.detail.section",
2391
+ id: "sub2api-sync",
2392
+ locale: NS
2393
+ }, DetailSection));
2394
+ ctx.slots.inject("plugins.bundle.activation", () => ctx.slots.register({
2395
+ name: "plugins.bundle.activation",
2396
+ key: PKG_NAME,
2397
+ locale: NS
2398
+ }, ActivationGuide));
2399
+ }
2400
+
2401
+ //#endregion
2402
+ //#region src/client/index.tsx
2403
+ const name = "dsh-sub2api-sync-client";
2404
+ const inject = [
2405
+ "slots",
2406
+ "locale",
2407
+ "configForms"
2408
+ ];
2409
+ function apply(ctx) {
2410
+ mount(ctx);
2411
+ }
2412
+
2413
+ //#endregion
2414
+ exports.apply = apply
2415
+ exports.inject = inject
2416
+ exports.name = name