dsh-cliproxyapi-pro 1.6.0 → 1.7.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/CHANGELOG.md +88 -58
- package/README.en.md +52 -68
- package/README.md +53 -67
- package/lib/client.js +2732 -618
- package/lib/index.js +146 -173
- package/lib/quota.js +1646 -0
- package/lib/updater.js +84 -20
- package/locale/en.json +2 -2
- package/locale/zh.json +2 -2
- package/package.json +9 -4
package/lib/client.js
CHANGED
|
@@ -26,6 +26,9 @@ window.__ModuleLoader__.load({
|
|
|
26
26
|
updateCheck: "api/dsh-cliproxyapi/updates/check",
|
|
27
27
|
updateSave: "api/dsh-cliproxyapi/updates/save",
|
|
28
28
|
updateDownload: "api/dsh-cliproxyapi/updates/download",
|
|
29
|
+
quotaStatus: "api/dsh-cliproxyapi/quota/status",
|
|
30
|
+
quotaRefresh: "api/dsh-cliproxyapi/quota/refresh",
|
|
31
|
+
quotaSave: "api/dsh-cliproxyapi/quota/save",
|
|
29
32
|
};
|
|
30
33
|
|
|
31
34
|
// Public, versioned dshmarket Update API (UPDATE-API-V1.md in dshmarket).
|
|
@@ -41,48 +44,32 @@ window.__ModuleLoader__.load({
|
|
|
41
44
|
|
|
42
45
|
const zh = {
|
|
43
46
|
nav: "CLIProxyAPI",
|
|
44
|
-
title: "CLIProxyAPI 自动发现与模型筛选",
|
|
45
|
-
subtitle:
|
|
46
|
-
"自动连接 CLIProxyAPI 服务,勾选你希望在对话框模型选择器中显示的模型,自动同步上下文长度、视觉与推理档位。",
|
|
47
47
|
statusConnected: "已连接并同步",
|
|
48
48
|
statusProbing: "正在探测同步…",
|
|
49
49
|
statusError: "连接异常",
|
|
50
50
|
statusDisabled: "已停用",
|
|
51
51
|
statusIdle: "等待同步",
|
|
52
|
-
endpointLabel: "当前活动节点",
|
|
53
|
-
endpointNone: "未连接(将依次尝试配置地址与本地 8317 端口)",
|
|
54
|
-
modelsCountLabel: "选择器展示模型",
|
|
55
|
-
refreshedAtLabel: "上次同步时间",
|
|
56
52
|
refreshBtn: "立即探测刷新",
|
|
57
53
|
refreshingBtn: "正在探测…",
|
|
58
|
-
saveBtn: "
|
|
54
|
+
saveBtn: "保存并应用",
|
|
59
55
|
savingBtn: "正在应用…",
|
|
60
|
-
|
|
61
|
-
unsavedChanges: "有未保存的勾选更改",
|
|
56
|
+
unsavedChanges: "有未保存的更改",
|
|
62
57
|
savedToast: "已保存并同步到对话框模型选择器",
|
|
63
|
-
sectionConnection: "连接与认证配置",
|
|
64
58
|
accountsLabel: "账号",
|
|
65
59
|
addAccountBtn: "+ 添加账号",
|
|
66
|
-
addingAccountBtn: "正在添加…",
|
|
67
60
|
removeAccountBtn: "删除此账号",
|
|
68
61
|
removeConfirm: "确定删除账号「{name}」吗?它同步到模型选择器中的模型路由也会一并移除(已保存的密钥不会删除)。",
|
|
69
|
-
|
|
70
|
-
accountOffBadge: "未启用",
|
|
71
|
-
addedToast: "已添加新账号(默认未启用)。填写服务地址与密钥后,打开「启用此账号」并保存。",
|
|
62
|
+
addedToast: "账号已添加并启用,已按每个家族勾选最新模型。",
|
|
72
63
|
removedToast: "账号已删除,其模型已从模型选择器中移除",
|
|
73
|
-
disabledHint:
|
|
74
|
-
"该账号未启用,它的模型不会出现在对话框模型选择器中。可先填写地址与密钥并点击「立即探测刷新」预览模型列表,勾选后再开启「启用此账号」。",
|
|
64
|
+
disabledHint: "未启用:模型不在模型选择器中,配置已保留。",
|
|
75
65
|
enabledLabel: "启用此账号",
|
|
76
|
-
enabledHint: "
|
|
77
|
-
baseUrlLabel: "
|
|
78
|
-
baseUrlPlaceholder: "
|
|
79
|
-
|
|
80
|
-
apiKeyLabel: "API Key 密钥",
|
|
66
|
+
enabledHint: "开启后,该账号勾选的模型会出现在对话框模型选择器中;关闭后移除这些模型,配置保留。",
|
|
67
|
+
baseUrlLabel: "服务地址",
|
|
68
|
+
baseUrlPlaceholder: "如 http://127.0.0.1:8317,无需加 /v1;留空自动发现",
|
|
69
|
+
apiKeyLabel: "API Key",
|
|
81
70
|
apiKeySetPlaceholder: "已配置密钥(留空保持不变,输入新值可覆盖)",
|
|
82
71
|
apiKeyUnsetPlaceholder: "输入 CLIProxyAPI 访问密钥(本地免密模式可留空)",
|
|
83
|
-
apiKeyHint: "
|
|
84
|
-
apiKeyConfiguredBadge: "密钥已配置",
|
|
85
|
-
apiKeyUnsetBadge: "未设置密钥",
|
|
72
|
+
apiKeyHint: "保存在 DSH 凭证服务({ref}),不会写入配置文件。本地免密的 CPA 可留空。",
|
|
86
73
|
clearKeyBtn: "清除密钥",
|
|
87
74
|
protocolLabel: "接口协议",
|
|
88
75
|
protocolHint: "自动模式下每个模型走所属家族的原生协议,减少 CPA 中转转换;Gemini 等 DSH 不支持原生协议的模型走 OpenAI Chat。",
|
|
@@ -95,22 +82,19 @@ window.__ModuleLoader__.load({
|
|
|
95
82
|
modelProtocolDefault: "跟随全局设置",
|
|
96
83
|
modelProtocolHint: "原生协议:",
|
|
97
84
|
sectionAdvanced: "高级路由与规则过滤",
|
|
98
|
-
providerIdLabel: "
|
|
85
|
+
providerIdLabel: "路由 ID",
|
|
99
86
|
providerIdHint: "写入 llm-pi-ai 的提供商路由标识,每个账号必须不同(首个账号默认 cli-proxy,与现有配置无缝衔接)。",
|
|
100
|
-
apiKeyEnvLabel: "密钥引用名
|
|
87
|
+
apiKeyEnvLabel: "密钥引用名",
|
|
101
88
|
apiKeyEnvHint: "密钥在 DSH 凭证服务中的名称,每个账号建议使用不同的引用名。",
|
|
102
|
-
displayNameLabel: "账号名称
|
|
89
|
+
displayNameLabel: "账号名称",
|
|
103
90
|
displayNameHint: "用于区分账号,并作为 DSH 对话框模型选择器中的分组名称。",
|
|
104
|
-
includePrefixLabel: "
|
|
91
|
+
includePrefixLabel: "仅包含关键词",
|
|
105
92
|
includePrefixPlaceholder: "例如: gpt-6, claude-opus, gemini-4 (多个用逗号分隔)",
|
|
106
93
|
includePrefixHint: "附加规则:只允许模型 ID 包含指定关键词的模型。",
|
|
107
|
-
excludePrefixLabel: "
|
|
94
|
+
excludePrefixLabel: "排除关键词",
|
|
108
95
|
excludePrefixPlaceholder: "例如: tts, image, video, zj/ (多个用逗号分隔)",
|
|
109
96
|
excludePrefixHint: "附加规则:排除包含指定关键词的模型 ID。",
|
|
110
|
-
refreshIntervalLabel: "自动刷新间隔
|
|
111
|
-
sectionModels: "模型显示筛选(仅勾选的模型会在回答选择器中显示)",
|
|
112
|
-
sectionModelsHint:
|
|
113
|
-
"点击模型行即可勾选或取消。不勾选任何模型(全部清空)时默认不限制;勾选后仅展示选中的模型。",
|
|
97
|
+
refreshIntervalLabel: "自动刷新间隔",
|
|
114
98
|
searchModelsPlaceholder: "搜索模型 ID 或名称(如 claude-opus, gpt-6, gemini-4, swe)…",
|
|
115
99
|
filterAll: "全部发现",
|
|
116
100
|
filterSelected: "已勾选",
|
|
@@ -119,7 +103,7 @@ window.__ModuleLoader__.load({
|
|
|
119
103
|
selectAllVisibleBtn: "全选当前列表",
|
|
120
104
|
deselectAllVisibleBtn: "取消当前列表",
|
|
121
105
|
selectRecommendedBtn: "恢复推荐精选",
|
|
122
|
-
selectRecommendedHint: "
|
|
106
|
+
selectRecommendedHint: "按该服务的模型目录,每个模型家族勾选最新的一个。",
|
|
123
107
|
badgeVision: "视觉 Image",
|
|
124
108
|
badgeReasoning: "推理",
|
|
125
109
|
badgeSelected: "已启用",
|
|
@@ -133,7 +117,7 @@ window.__ModuleLoader__.load({
|
|
|
133
117
|
imageInputLabel: "支持图片输入",
|
|
134
118
|
reasoningLevelsLabel: "思考等级",
|
|
135
119
|
resetParamsBtn: "恢复自动探测",
|
|
136
|
-
paramsHint: "留空并保存即恢复 CLIProxyAPI
|
|
120
|
+
paramsHint: "留空并保存即恢复 CLIProxyAPI 自动探测值。",
|
|
137
121
|
discoveredValue: "探测值",
|
|
138
122
|
updateTitle: "插件更新",
|
|
139
123
|
updatePageHint: "这里只管理插件自身的更新。CLIProxyAPI 的账号、模型和协议设置位于「设置」左侧导航的「CLIProxyAPI」页。",
|
|
@@ -255,52 +239,147 @@ window.__ModuleLoader__.load({
|
|
|
255
239
|
updateMarketFailed: "插件市场安装失败。可在插件市场中查看详情,稍后重试,或使用离线包下载。",
|
|
256
240
|
updateAutoInstallLabel: "自动安装更新",
|
|
257
241
|
updateAutoInstallHint: "自动检查发现新版本后,通过 DSH 插件市场自动安装(需开启自动检查并安装 dshmarket,仅在 DSH 页面打开时进行;同一版本 6 小时内最多尝试一次)。生效可能需要重启 DSH 或刷新页面。",
|
|
242
|
+
tabsLabel: "账号页面",
|
|
243
|
+
tabSettings: "设置",
|
|
244
|
+
tabQuota: "额度",
|
|
245
|
+
qTitle: "CPA 渠道额度",
|
|
246
|
+
qSubtitle: "填写管理密钥后,查看 CPA 上每个渠道凭证的剩余额度;对话框模型选择器旁也会显示当前模型的额度。",
|
|
247
|
+
qAccountsLabel: "选择账号",
|
|
248
|
+
qNoAccounts: "还没有 CLIProxyAPI 账号,请先在「设置」页添加并连接。",
|
|
249
|
+
qAccountDisabled: "账号未启用",
|
|
250
|
+
qKeyLabel: "管理密钥",
|
|
251
|
+
qKeyHint: "保存在 DSH 凭证服务({ref}),只由宿主使用,不会返回到页面。远程 CPA 需开启 remote-management.allow-remote。",
|
|
252
|
+
qKeySetPlaceholder: "已保存管理密钥(留空保持不变,输入新值可覆盖)",
|
|
253
|
+
qKeyUnsetPlaceholder: "CPA 配置中的 remote-management.secret-key",
|
|
254
|
+
qKeySave: "保存",
|
|
255
|
+
qKeySaving: "正在保存…",
|
|
256
|
+
qKeyClear: "清除密钥",
|
|
257
|
+
qKeyClearConfirm: "确定清除账号「{name}」的管理密钥吗?清除后不再读取它的额度。",
|
|
258
|
+
qKeyStatusSet: "已配置",
|
|
259
|
+
qKeyStatusUnset: "未配置",
|
|
260
|
+
qDataLabel: "额度数据",
|
|
261
|
+
qTotalCreds: "{n} 个凭证",
|
|
262
|
+
qKeySaved: "管理密钥已保存",
|
|
263
|
+
qKeyCleared: "管理密钥已清除",
|
|
264
|
+
qRefresh: "刷新额度",
|
|
265
|
+
qRefreshing: "正在刷新…",
|
|
266
|
+
qUpdated: "{time}更新",
|
|
267
|
+
qNeverUpdated: "尚未读取额度",
|
|
268
|
+
qAgoJustNow: "刚刚",
|
|
269
|
+
qAgoSeconds: "{n} 秒前",
|
|
270
|
+
qAgoMinutes: "{n} 分钟前",
|
|
271
|
+
qAgoHours: "{n} 小时前",
|
|
272
|
+
qAgoDays: "{n} 天前",
|
|
273
|
+
qStatHealthy: "充足",
|
|
274
|
+
qStatLow: "偏低",
|
|
275
|
+
qStatEmpty: "耗尽",
|
|
276
|
+
qStatIssue: "停用/异常",
|
|
277
|
+
qStatUnknown: "无额度数据",
|
|
278
|
+
qFilterAll: "全部渠道",
|
|
279
|
+
qSearchPlaceholder: "搜索邮箱、名称或模型…",
|
|
280
|
+
qNoCredentials: "该 CPA 服务器上没有凭证(auth files)。",
|
|
281
|
+
qNoMatch: "没有符合筛选条件的凭证。",
|
|
282
|
+
qShowModels: "服务 {n} 个模型",
|
|
283
|
+
qHideModels: "收起模型列表",
|
|
284
|
+
qModelsUnknown: "模型列表不可用",
|
|
285
|
+
qStatusActive: "可用",
|
|
286
|
+
qStatusDisabled: "已停用",
|
|
287
|
+
qStatusUnavailable: "暂不可用",
|
|
288
|
+
qStatusError: "异常",
|
|
289
|
+
qQuotaUnsupported: "此渠道没有可查询的额度接口,仅显示账号状态",
|
|
290
|
+
qQuotaError: "额度查询失败",
|
|
291
|
+
qQuotaNone: "上游未返回额度窗口",
|
|
292
|
+
qRecent: "近期请求:成功 {success} · 失败 {failed}",
|
|
293
|
+
qWin5h: "5 小时",
|
|
294
|
+
qWin7d: "每周",
|
|
295
|
+
qWin30d: "每月",
|
|
296
|
+
qWin1d: "每日",
|
|
297
|
+
qWinOther: "额度",
|
|
298
|
+
qResetIn: "{time}后重置",
|
|
299
|
+
qResetSoon: "即将重置",
|
|
300
|
+
qDurDH: "{d}天{h}小时",
|
|
301
|
+
qDurHM: "{h}小时{m}分",
|
|
302
|
+
qDurM: "{m}分钟",
|
|
303
|
+
qErrUnauthorized: "管理密钥无效(401)。为避免 CPA 因错误密钥封禁地址,已暂停自动刷新;更新密钥或手动刷新后重试。",
|
|
304
|
+
qErrForbidden: "CPA 拒绝了管理请求(403):请确认 CPA 已开启 remote-management.allow-remote,且密钥正确。",
|
|
305
|
+
qErrNetwork: "无法连接 CPA 管理接口,请检查服务地址与网络。",
|
|
306
|
+
qErrTimeout: "CPA 管理接口响应超时,请稍后重试。",
|
|
307
|
+
qErrHttp: "CPA 管理接口返回错误(HTTP {status})。",
|
|
308
|
+
qErrInvalid: "CPA 管理接口返回了无法识别的数据。",
|
|
309
|
+
qErrNoEndpoint: "该账号还没有可用的服务地址,请先在「设置」页连接 CPA。",
|
|
310
|
+
qErrLoad: "无法读取额度数据:{error}",
|
|
311
|
+
qStale: "以下为上次成功读取的数据。",
|
|
312
|
+
qMapTitle: "模型 → 渠道",
|
|
313
|
+
qMapHint: "该账号同步到模型选择器的每个模型,实际由哪些渠道凭证提供服务。模型选择器旁的额度按此匹配。",
|
|
314
|
+
qMapNone: "未匹配到凭证",
|
|
315
|
+
qMapEmpty: "该账号还没有同步到模型选择器的模型。",
|
|
316
|
+
qMatchModelList: "按模型列表",
|
|
317
|
+
qMatchPrefix: "按前缀",
|
|
318
|
+
qMatchFamily: "按家族推断",
|
|
319
|
+
qBadgeLabel: "{model} 的 CPA 额度",
|
|
320
|
+
qPeekCreds: "{n} 个凭证可服务此模型",
|
|
321
|
+
qPeekNoCreds: "CPA 上没有凭证提供这个模型。",
|
|
322
|
+
qPeekLoading: "正在读取额度…",
|
|
323
|
+
qPeekFooter: "完整列表见 设置 → CLIProxyAPI → 额度",
|
|
324
|
+
qPeekBest: "最充足",
|
|
325
|
+
infoLabel: "说明",
|
|
326
|
+
tabConnection: "连接",
|
|
327
|
+
tabModels: "模型",
|
|
328
|
+
enableShort: "启用",
|
|
329
|
+
moreActions: "更多操作",
|
|
330
|
+
testBtn: "测试连接",
|
|
331
|
+
testingBtn: "正在测试…",
|
|
332
|
+
testOk: "连接成功:{endpoint} · {n} 个模型",
|
|
333
|
+
testFail: "连接失败:{error}",
|
|
334
|
+
discardBtn: "放弃更改",
|
|
335
|
+
discardConfirm: "当前账号有未保存的更改,确定放弃吗?",
|
|
336
|
+
saveBarLabel: "保存",
|
|
337
|
+
modelsEmptyRule: "只有勾选的模型会出现在模型选择器中;一个都不勾选时显示全部模型。",
|
|
338
|
+
addTitle: "添加账号",
|
|
339
|
+
addNamePlaceholder: "如:家里的 CPA",
|
|
340
|
+
cancelBtn: "取消",
|
|
341
|
+
addAnywayBtn: "仍然添加(不启用)",
|
|
342
|
+
connectBtn: "连接",
|
|
343
|
+
connectingBtn: "正在连接…",
|
|
344
|
+
addedDisabledToast: "账号已添加但未启用。修正连接信息后,在账号标题旁打开「启用」。",
|
|
345
|
+
clearKeyConfirm: "确定清除该账号的 API Key 吗?",
|
|
346
|
+
clearedKeyToast: "API Key 已清除",
|
|
347
|
+
enabledToast: "已启用,模型已同步到模型选择器",
|
|
348
|
+
disabledToast: "已停用,模型已从模型选择器移除",
|
|
349
|
+
modelsCountShort: "{n}/{total} 个模型",
|
|
350
|
+
secondsUnit: "秒 · 所有账号共用",
|
|
351
|
+
paramsHintShort: "修改后点底部「保存并应用」生效。",
|
|
352
|
+
qProviderOther: "其他",
|
|
258
353
|
};
|
|
259
354
|
|
|
260
355
|
const en = {
|
|
261
356
|
nav: "CLIProxyAPI",
|
|
262
|
-
title: "CLIProxyAPI Auto-Discovery & Model Filter",
|
|
263
|
-
subtitle:
|
|
264
|
-
"Connect to CLIProxyAPI and check the exact models you want displayed in the DSH conversation model picker.",
|
|
265
357
|
statusConnected: "Connected & Synced",
|
|
266
358
|
statusProbing: "Probing…",
|
|
267
359
|
statusError: "Connection Error",
|
|
268
360
|
statusDisabled: "Disabled",
|
|
269
361
|
statusIdle: "Idle",
|
|
270
|
-
endpointLabel: "Active Endpoint",
|
|
271
|
-
endpointNone: "Not connected (tries configured URL then localhost:8317)",
|
|
272
|
-
modelsCountLabel: "Models in Picker",
|
|
273
|
-
refreshedAtLabel: "Last Synced",
|
|
274
362
|
refreshBtn: "Probe & Refresh",
|
|
275
363
|
refreshingBtn: "Probing…",
|
|
276
364
|
saveBtn: "Save & Apply",
|
|
277
365
|
savingBtn: "Applying…",
|
|
278
|
-
|
|
279
|
-
unsavedChanges: "Unsaved selection changes",
|
|
366
|
+
unsavedChanges: "Unsaved changes",
|
|
280
367
|
savedToast: "Saved and synchronized to the DSH model picker",
|
|
281
|
-
sectionConnection: "Connection & Authentication",
|
|
282
368
|
accountsLabel: "Accounts",
|
|
283
369
|
addAccountBtn: "+ Add Account",
|
|
284
|
-
addingAccountBtn: "Adding…",
|
|
285
370
|
removeAccountBtn: "Delete Account",
|
|
286
371
|
removeConfirm: "Delete account \"{name}\"? Its model routes are removed from the model picker too (the stored key is kept).",
|
|
287
|
-
|
|
288
|
-
accountOffBadge: "Off",
|
|
289
|
-
addedToast: "Account added (disabled). Fill in its URL and key, then turn on Enable This Account and save.",
|
|
372
|
+
addedToast: "Account added and enabled; the newest model of each family is checked.",
|
|
290
373
|
removedToast: "Account deleted and its models removed from the picker",
|
|
291
|
-
disabledHint:
|
|
292
|
-
"This account is disabled, so its models are not in the model picker. Fill in URL and key, click Probe & Refresh to preview its models, check the ones you want, then turn on Enable This Account.",
|
|
374
|
+
disabledHint: "Disabled: its models are not in the model picker; settings are kept.",
|
|
293
375
|
enabledLabel: "Enable This Account",
|
|
294
|
-
enabledHint: "
|
|
295
|
-
baseUrlLabel: "
|
|
296
|
-
baseUrlPlaceholder: "
|
|
297
|
-
baseUrlHint: "Root URL (e.g. http://127.0.0.1:8317); /v1 is handled automatically.",
|
|
376
|
+
enabledHint: "When on, this account's checked models appear in the composer model picker; turning it off removes them and keeps the settings.",
|
|
377
|
+
baseUrlLabel: "Base URL",
|
|
378
|
+
baseUrlPlaceholder: "e.g. http://127.0.0.1:8317, no /v1 needed; blank = auto-discover",
|
|
298
379
|
apiKeyLabel: "API Key",
|
|
299
380
|
apiKeySetPlaceholder: "Key is stored (leave blank to keep, or type a new key)",
|
|
300
381
|
apiKeyUnsetPlaceholder: "Enter CLIProxyAPI Bearer key (optional for local no-auth)",
|
|
301
|
-
apiKeyHint: "Stored
|
|
302
|
-
apiKeyConfiguredBadge: "Configured",
|
|
303
|
-
apiKeyUnsetBadge: "Not Set",
|
|
382
|
+
apiKeyHint: "Stored in the DSH credentials service ({ref}), never in settings files. Leave blank for a local CPA without a key.",
|
|
304
383
|
clearKeyBtn: "Clear Key",
|
|
305
384
|
protocolLabel: "Wire Protocol",
|
|
306
385
|
protocolHint: "Auto sends each model over its family's own protocol to avoid CPA translation; models DSH cannot speak natively (such as Gemini) use OpenAI Chat.",
|
|
@@ -313,22 +392,19 @@ window.__ModuleLoader__.load({
|
|
|
313
392
|
modelProtocolDefault: "Follow global setting",
|
|
314
393
|
modelProtocolHint: "Native protocol: ",
|
|
315
394
|
sectionAdvanced: "Advanced Routing & Keyword Rules",
|
|
316
|
-
providerIdLabel: "
|
|
395
|
+
providerIdLabel: "Route ID",
|
|
317
396
|
providerIdHint: "Provider key under llm-pi-ai; must differ per account (first account: cli-proxy).",
|
|
318
397
|
apiKeyEnvLabel: "Credential Ref",
|
|
319
398
|
apiKeyEnvHint: "Name of the key in the DSH credentials service; use a distinct one per account.",
|
|
320
399
|
displayNameLabel: "Account Name",
|
|
321
400
|
displayNameHint: "Tells accounts apart and titles their group in the DSH model selector.",
|
|
322
|
-
includePrefixLabel: "Include
|
|
401
|
+
includePrefixLabel: "Include Keywords",
|
|
323
402
|
includePrefixPlaceholder: "e.g. gpt-6, claude-opus, gemini-4 (comma-separated)",
|
|
324
403
|
includePrefixHint: "Additional rule: only sync models matching these substrings.",
|
|
325
|
-
excludePrefixLabel: "Exclude
|
|
404
|
+
excludePrefixLabel: "Exclude Keywords",
|
|
326
405
|
excludePrefixPlaceholder: "e.g. tts, image, video, zj/ (comma-separated)",
|
|
327
406
|
excludePrefixHint: "Additional rule: hide models matching any of these substrings.",
|
|
328
|
-
refreshIntervalLabel: "Auto Refresh
|
|
329
|
-
sectionModels: "Model Picker Filter (Only checked models appear in DSH)",
|
|
330
|
-
sectionModelsHint:
|
|
331
|
-
"Click any model row to toggle selection. Only checked models will be shown in the conversation model selector.",
|
|
407
|
+
refreshIntervalLabel: "Auto Refresh",
|
|
332
408
|
searchModelsPlaceholder: "Search model ID or name (e.g. claude-opus, gpt-6, gemini-4)…",
|
|
333
409
|
filterAll: "All Discovered",
|
|
334
410
|
filterSelected: "Checked",
|
|
@@ -337,7 +413,7 @@ window.__ModuleLoader__.load({
|
|
|
337
413
|
selectAllVisibleBtn: "Check Visible",
|
|
338
414
|
deselectAllVisibleBtn: "Uncheck Visible",
|
|
339
415
|
selectRecommendedBtn: "Reset to Recommended",
|
|
340
|
-
selectRecommendedHint: "Check the newest model of each family in this server's catalog",
|
|
416
|
+
selectRecommendedHint: "Check the newest model of each family in this server's catalog.",
|
|
341
417
|
badgeVision: "Image",
|
|
342
418
|
badgeReasoning: "Reasoning",
|
|
343
419
|
badgeSelected: "Active",
|
|
@@ -351,7 +427,7 @@ window.__ModuleLoader__.load({
|
|
|
351
427
|
imageInputLabel: "Image Input",
|
|
352
428
|
reasoningLevelsLabel: "Thinking Levels",
|
|
353
429
|
resetParamsBtn: "Reset to Discovered",
|
|
354
|
-
paramsHint: "Clear a value and save to restore the value discovered from CLIProxyAPI.
|
|
430
|
+
paramsHint: "Clear a value and save to restore the value discovered from CLIProxyAPI.",
|
|
355
431
|
discoveredValue: "Discovered",
|
|
356
432
|
updateTitle: "Plugin Updates",
|
|
357
433
|
updatePageHint: "This page only manages updates of the plugin itself. CLIProxyAPI accounts, models and protocol settings are on the \"CLIProxyAPI\" page in the Settings dialog's left navigation.",
|
|
@@ -473,6 +549,117 @@ window.__ModuleLoader__.load({
|
|
|
473
549
|
updateMarketFailed: "The plugin market installation failed. See the plugin market for details, retry later, or use the offline package download.",
|
|
474
550
|
updateAutoInstallLabel: "Automatically Install Updates",
|
|
475
551
|
updateAutoInstallHint: "Install versions found by automatic checks through the DSH plugin market (requires Auto Check and dshmarket; runs only while a DSH page is open, at most once per version every 6 hours). A DSH restart or page refresh may be needed for it to take effect.",
|
|
552
|
+
tabsLabel: "Account pages",
|
|
553
|
+
tabSettings: "Settings",
|
|
554
|
+
tabQuota: "Quota",
|
|
555
|
+
qTitle: "CPA Channel Quota",
|
|
556
|
+
qSubtitle: "With a management key, see the remaining quota of every channel credential on CPA; the composer also shows the current model's quota next to the model picker.",
|
|
557
|
+
qAccountsLabel: "Choose account",
|
|
558
|
+
qNoAccounts: "No CLIProxyAPI account yet. Add and connect one on the Settings tab first.",
|
|
559
|
+
qAccountDisabled: "Account disabled",
|
|
560
|
+
qKeyLabel: "Management Key",
|
|
561
|
+
qKeyHint: "Stored in the DSH credentials service ({ref}), used only by the host and never sent back to the page. A remote CPA needs remote-management.allow-remote.",
|
|
562
|
+
qKeySetPlaceholder: "Management key stored (leave blank to keep, or type a new one)",
|
|
563
|
+
qKeyUnsetPlaceholder: "remote-management.secret-key from the CPA config",
|
|
564
|
+
qKeySave: "Save",
|
|
565
|
+
qKeySaving: "Saving…",
|
|
566
|
+
qKeyClear: "Clear Key",
|
|
567
|
+
qKeyClearConfirm: "Clear the management key of account \"{name}\"? Its quota is no longer read afterwards.",
|
|
568
|
+
qKeyStatusSet: "Set",
|
|
569
|
+
qKeyStatusUnset: "Not set",
|
|
570
|
+
qDataLabel: "Quota data",
|
|
571
|
+
qTotalCreds: "{n} credentials",
|
|
572
|
+
qKeySaved: "Management key saved",
|
|
573
|
+
qKeyCleared: "Management key cleared",
|
|
574
|
+
qRefresh: "Refresh Quota",
|
|
575
|
+
qRefreshing: "Refreshing…",
|
|
576
|
+
qUpdated: "Updated {time}",
|
|
577
|
+
qNeverUpdated: "Quota not loaded yet",
|
|
578
|
+
qAgoJustNow: "just now",
|
|
579
|
+
qAgoSeconds: "{n}s ago",
|
|
580
|
+
qAgoMinutes: "{n} min ago",
|
|
581
|
+
qAgoHours: "{n} h ago",
|
|
582
|
+
qAgoDays: "{n} d ago",
|
|
583
|
+
qStatHealthy: "Healthy",
|
|
584
|
+
qStatLow: "Low",
|
|
585
|
+
qStatEmpty: "Exhausted",
|
|
586
|
+
qStatIssue: "Off / Error",
|
|
587
|
+
qStatUnknown: "No quota data",
|
|
588
|
+
qFilterAll: "All channels",
|
|
589
|
+
qSearchPlaceholder: "Search email, name or model…",
|
|
590
|
+
qNoCredentials: "This CPA server has no credentials (auth files).",
|
|
591
|
+
qNoMatch: "No credential matches the filters.",
|
|
592
|
+
qShowModels: "Serves {n} models",
|
|
593
|
+
qHideModels: "Hide models",
|
|
594
|
+
qModelsUnknown: "Model list unavailable",
|
|
595
|
+
qStatusActive: "Active",
|
|
596
|
+
qStatusDisabled: "Disabled",
|
|
597
|
+
qStatusUnavailable: "Unavailable",
|
|
598
|
+
qStatusError: "Error",
|
|
599
|
+
qQuotaUnsupported: "No quota endpoint for this channel; showing account status only",
|
|
600
|
+
qQuotaError: "Quota query failed",
|
|
601
|
+
qQuotaNone: "Upstream returned no quota window",
|
|
602
|
+
qRecent: "Recent requests: {success} ok · {failed} failed",
|
|
603
|
+
qWin5h: "5 hours",
|
|
604
|
+
qWin7d: "Weekly",
|
|
605
|
+
qWin30d: "Monthly",
|
|
606
|
+
qWin1d: "Daily",
|
|
607
|
+
qWinOther: "Quota",
|
|
608
|
+
qResetIn: "resets in {time}",
|
|
609
|
+
qResetSoon: "resetting now",
|
|
610
|
+
qDurDH: "{d}d {h}h",
|
|
611
|
+
qDurHM: "{h}h {m}m",
|
|
612
|
+
qDurM: "{m}m",
|
|
613
|
+
qErrUnauthorized: "The management key was rejected (401). Automatic refresh is paused so CPA does not ban this address for bad keys; update the key or refresh manually to retry.",
|
|
614
|
+
qErrForbidden: "CPA refused the management request (403). Make sure remote-management.allow-remote is on and the key is correct.",
|
|
615
|
+
qErrNetwork: "Could not reach the CPA management API. Check the server address and network.",
|
|
616
|
+
qErrTimeout: "The CPA management API timed out. Retry shortly.",
|
|
617
|
+
qErrHttp: "The CPA management API returned an error (HTTP {status}).",
|
|
618
|
+
qErrInvalid: "The CPA management API returned data that could not be read.",
|
|
619
|
+
qErrNoEndpoint: "This account has no usable server address yet. Connect CPA on the Settings tab first.",
|
|
620
|
+
qErrLoad: "Could not load quota data: {error}",
|
|
621
|
+
qStale: "Showing the last successful data below.",
|
|
622
|
+
qMapTitle: "Model → Channel",
|
|
623
|
+
qMapHint: "Which channel credentials actually serve each model this account puts in the model picker. The composer quota badge uses exactly this mapping.",
|
|
624
|
+
qMapNone: "No matching credential",
|
|
625
|
+
qMapEmpty: "This account has no models in the model picker yet.",
|
|
626
|
+
qMatchModelList: "by model list",
|
|
627
|
+
qMatchPrefix: "by prefix",
|
|
628
|
+
qMatchFamily: "inferred from family",
|
|
629
|
+
qBadgeLabel: "CPA quota of {model}",
|
|
630
|
+
qPeekCreds: "{n} credential(s) serve this model",
|
|
631
|
+
qPeekNoCreds: "No CPA credential serves this model.",
|
|
632
|
+
qPeekLoading: "Loading quota…",
|
|
633
|
+
qPeekFooter: "Full list in Settings → CLIProxyAPI → Quota",
|
|
634
|
+
qPeekBest: "Best",
|
|
635
|
+
infoLabel: "More info",
|
|
636
|
+
tabConnection: "Connection",
|
|
637
|
+
tabModels: "Models",
|
|
638
|
+
enableShort: "Enabled",
|
|
639
|
+
moreActions: "More actions",
|
|
640
|
+
testBtn: "Test Connection",
|
|
641
|
+
testingBtn: "Testing…",
|
|
642
|
+
testOk: "Connected: {endpoint} · {n} models",
|
|
643
|
+
testFail: "Connection failed: {error}",
|
|
644
|
+
discardBtn: "Discard",
|
|
645
|
+
discardConfirm: "This account has unsaved changes. Discard them?",
|
|
646
|
+
saveBarLabel: "Save",
|
|
647
|
+
modelsEmptyRule: "Only checked models appear in the model picker; with none checked, all models are shown.",
|
|
648
|
+
addTitle: "Add Account",
|
|
649
|
+
addNamePlaceholder: "e.g. Home CPA",
|
|
650
|
+
cancelBtn: "Cancel",
|
|
651
|
+
addAnywayBtn: "Add Anyway (Disabled)",
|
|
652
|
+
connectBtn: "Connect",
|
|
653
|
+
connectingBtn: "Connecting…",
|
|
654
|
+
addedDisabledToast: "Account added but disabled. Fix the connection, then turn on Enabled next to the account title.",
|
|
655
|
+
clearKeyConfirm: "Clear this account's API key?",
|
|
656
|
+
clearedKeyToast: "API key cleared",
|
|
657
|
+
enabledToast: "Enabled; its models are synced to the model picker",
|
|
658
|
+
disabledToast: "Disabled; its models were removed from the model picker",
|
|
659
|
+
modelsCountShort: "{n}/{total} models",
|
|
660
|
+
secondsUnit: "s · all accounts",
|
|
661
|
+
paramsHintShort: "Click Save & Apply at the bottom to apply changes.",
|
|
662
|
+
qProviderOther: "Other",
|
|
476
663
|
};
|
|
477
664
|
|
|
478
665
|
const CSS = `
|
|
@@ -1039,76 +1226,707 @@ window.__ModuleLoader__.load({
|
|
|
1039
1226
|
flex-wrap: wrap;
|
|
1040
1227
|
}
|
|
1041
1228
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
if (!res.ok || payload.ok === false) {
|
|
1082
|
-
throw new Error(payload.error || `HTTP ${res.status}`);
|
|
1083
|
-
}
|
|
1084
|
-
return payload;
|
|
1085
|
-
}
|
|
1229
|
+
/* ---- Section tabs: DSH segmented control ---- */
|
|
1230
|
+
.dcpa-tabs {
|
|
1231
|
+
align-self: flex-start;
|
|
1232
|
+
display: inline-grid;
|
|
1233
|
+
grid-auto-flow: column;
|
|
1234
|
+
grid-auto-columns: 1fr;
|
|
1235
|
+
gap: 2px;
|
|
1236
|
+
padding: 4px;
|
|
1237
|
+
border-radius: var(--dsw-radius-md, 10px);
|
|
1238
|
+
background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2));
|
|
1239
|
+
}
|
|
1240
|
+
.dcpa-tab, .dcpa-q-seg-tab {
|
|
1241
|
+
box-sizing: border-box;
|
|
1242
|
+
height: 28px;
|
|
1243
|
+
padding: 0 16px;
|
|
1244
|
+
border: 0;
|
|
1245
|
+
border-radius: var(--dsw-radius-sm, 8px);
|
|
1246
|
+
background: transparent;
|
|
1247
|
+
color: var(--dsw-alias-label-secondary);
|
|
1248
|
+
font: inherit;
|
|
1249
|
+
font-size: 13px;
|
|
1250
|
+
line-height: 20px;
|
|
1251
|
+
font-weight: 500;
|
|
1252
|
+
white-space: nowrap;
|
|
1253
|
+
cursor: pointer;
|
|
1254
|
+
transition: color 0.12s ease, background 0.12s ease;
|
|
1255
|
+
}
|
|
1256
|
+
.dcpa-tab:hover, .dcpa-q-seg-tab:hover { color: var(--dsw-alias-label-primary); }
|
|
1257
|
+
.dcpa-tab[aria-selected="true"], .dcpa-q-seg-tab[aria-selected="true"] {
|
|
1258
|
+
color: var(--dsw-alias-label-primary);
|
|
1259
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
1260
|
+
box-shadow: var(--dsw-elevation-soft, 0 1px 2px var(--dsw-alias-border-l1));
|
|
1261
|
+
}
|
|
1262
|
+
.dcpa-tabpanel[hidden] { display: none; }
|
|
1263
|
+
.dcpa-tab:focus-visible, .dcpa-q-seg-tab:focus-visible, .dcpa-q button:focus-visible, .dcpa-q summary:focus-visible,
|
|
1264
|
+
.dcpa-q-badge:focus-visible, .dcpa-q-pop button:focus-visible {
|
|
1265
|
+
outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-brand-primary));
|
|
1266
|
+
outline-offset: -2px;
|
|
1267
|
+
}
|
|
1086
1268
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1269
|
+
/* ---- Quota page: flat DSH settings rows ---- */
|
|
1270
|
+
.dcpa-q {
|
|
1271
|
+
display: flex;
|
|
1272
|
+
flex-direction: column;
|
|
1273
|
+
color: var(--dsw-alias-label-primary);
|
|
1274
|
+
font-size: 14px;
|
|
1275
|
+
line-height: 22px;
|
|
1276
|
+
}
|
|
1277
|
+
.dcpa-q-head { padding: 4px 0 12px; }
|
|
1278
|
+
.dcpa-q-h { margin: 0; font-size: 16px; font-weight: 500; line-height: 24px; }
|
|
1279
|
+
.dcpa-q-desc {
|
|
1280
|
+
margin: 4px 0 0;
|
|
1281
|
+
font-size: 12px;
|
|
1282
|
+
line-height: 18px;
|
|
1283
|
+
color: var(--dsw-alias-label-secondary);
|
|
1284
|
+
}
|
|
1285
|
+
.dcpa-q-seg {
|
|
1286
|
+
align-self: flex-start;
|
|
1287
|
+
display: inline-flex;
|
|
1288
|
+
flex-wrap: wrap;
|
|
1289
|
+
gap: 2px;
|
|
1290
|
+
padding: 4px;
|
|
1291
|
+
margin: 4px 0 8px;
|
|
1292
|
+
border-radius: var(--dsw-radius-md, 10px);
|
|
1293
|
+
background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2));
|
|
1294
|
+
}
|
|
1295
|
+
.dcpa-q-seg-tab { display: inline-flex; align-items: center; gap: 6px; padding: 0 12px; }
|
|
1296
|
+
.dcpa-q-seg-note { font-weight: 400; color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary)); }
|
|
1297
|
+
.dcpa-q-row {
|
|
1298
|
+
display: flex;
|
|
1299
|
+
align-items: center;
|
|
1300
|
+
justify-content: space-between;
|
|
1301
|
+
gap: 24px;
|
|
1302
|
+
padding: 16px 0;
|
|
1303
|
+
border-bottom: 0.5px solid var(--dsw-alias-border-l2);
|
|
1304
|
+
}
|
|
1305
|
+
.dcpa-q-row-main { min-width: 0; }
|
|
1306
|
+
.dcpa-q-title { font-size: 14px; line-height: 20px; }
|
|
1307
|
+
.dcpa-q-sub {
|
|
1308
|
+
margin-top: 4px;
|
|
1309
|
+
font-size: 12px;
|
|
1310
|
+
line-height: 18px;
|
|
1311
|
+
color: var(--dsw-alias-label-secondary);
|
|
1312
|
+
overflow-wrap: anywhere;
|
|
1313
|
+
}
|
|
1314
|
+
.dcpa-q-actions { display: flex; align-items: center; gap: 8px; flex: none; }
|
|
1315
|
+
.dcpa-q-form {
|
|
1316
|
+
display: flex;
|
|
1317
|
+
align-items: center;
|
|
1318
|
+
gap: 8px;
|
|
1319
|
+
padding: 0 0 16px;
|
|
1320
|
+
border-bottom: 0.5px solid var(--dsw-alias-border-l2);
|
|
1321
|
+
}
|
|
1322
|
+
.dcpa-q-input {
|
|
1323
|
+
flex: 1;
|
|
1324
|
+
min-width: 0;
|
|
1325
|
+
box-sizing: border-box;
|
|
1326
|
+
height: 34px;
|
|
1327
|
+
padding: 0 12px;
|
|
1328
|
+
border: 0.5px solid var(--dsw-alias-border-l4, var(--dsw-alias-border-l2));
|
|
1329
|
+
border-radius: var(--dsw-radius-md, 10px);
|
|
1330
|
+
background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-base));
|
|
1331
|
+
color: var(--dsw-alias-label-primary);
|
|
1332
|
+
font: inherit;
|
|
1333
|
+
font-size: 13px;
|
|
1334
|
+
outline: none;
|
|
1335
|
+
}
|
|
1336
|
+
.dcpa-q-input:focus { border-color: var(--dsw-alias-state-business-primary, var(--dsw-alias-brand-primary)); }
|
|
1337
|
+
.dcpa-q-input::placeholder { color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary)); }
|
|
1338
|
+
.dcpa-q-btn {
|
|
1339
|
+
box-sizing: border-box;
|
|
1340
|
+
display: inline-flex;
|
|
1341
|
+
align-items: center;
|
|
1342
|
+
justify-content: center;
|
|
1343
|
+
gap: 4px;
|
|
1344
|
+
height: 28px;
|
|
1345
|
+
padding: 0 10px;
|
|
1346
|
+
border: none;
|
|
1347
|
+
border-radius: var(--dsw-radius-sm, 8px);
|
|
1348
|
+
background: transparent;
|
|
1349
|
+
color: var(--dsw-alias-label-primary);
|
|
1350
|
+
font: inherit;
|
|
1351
|
+
font-size: 12px;
|
|
1352
|
+
line-height: 18px;
|
|
1353
|
+
white-space: nowrap;
|
|
1354
|
+
cursor: pointer;
|
|
1355
|
+
}
|
|
1356
|
+
.dcpa-q-btn:disabled { cursor: not-allowed; opacity: 0.4; }
|
|
1357
|
+
.dcpa-q-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2)); }
|
|
1358
|
+
.dcpa-q-btn[data-variant="primary"] {
|
|
1359
|
+
height: 34px;
|
|
1360
|
+
padding: 0 14px;
|
|
1361
|
+
border-radius: var(--dsw-radius-md, 10px);
|
|
1362
|
+
background: var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary));
|
|
1363
|
+
color: var(--dsw-alias-label-primary-foreground, var(--dsw-alias-bg-base));
|
|
1364
|
+
}
|
|
1365
|
+
.dcpa-q-btn[data-variant="primary"]:hover:not(:disabled) {
|
|
1366
|
+
background: var(--dsw-alias-button-primary-hover, var(--dsw-alias-brand-primary));
|
|
1367
|
+
}
|
|
1368
|
+
.dcpa-q-btn[data-variant="outline"] { border: 0.5px solid var(--dsw-alias-border-l3, var(--dsw-alias-border-l2)); }
|
|
1369
|
+
.dcpa-q-btn[data-variant="danger"] { color: var(--dsw-alias-state-error-primary); }
|
|
1370
|
+
.dcpa-q-btn[data-variant="danger"]:hover:not(:disabled) {
|
|
1371
|
+
background: var(--dsw-alias-interactive-bg-hover-danger, var(--dsw-alias-bg-layer-2));
|
|
1372
|
+
}
|
|
1373
|
+
.dcpa-q-tag {
|
|
1374
|
+
display: inline-flex;
|
|
1375
|
+
align-items: center;
|
|
1376
|
+
border-radius: 999px;
|
|
1377
|
+
padding: 1px 8px;
|
|
1378
|
+
font-size: 11px;
|
|
1379
|
+
line-height: 17px;
|
|
1380
|
+
font-weight: 500;
|
|
1381
|
+
white-space: nowrap;
|
|
1382
|
+
border: 0.5px solid var(--dsw-alias-border-l4, var(--dsw-alias-border-l2));
|
|
1383
|
+
color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));
|
|
1384
|
+
}
|
|
1385
|
+
.dcpa-q-tag[data-tone="success"] {
|
|
1386
|
+
border-color: transparent;
|
|
1387
|
+
background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent);
|
|
1388
|
+
color: var(--dsw-alias-state-success-primary);
|
|
1389
|
+
}
|
|
1390
|
+
.dcpa-q-tag[data-tone="warning"] {
|
|
1391
|
+
border-color: transparent;
|
|
1392
|
+
background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent);
|
|
1393
|
+
color: var(--dsw-alias-state-warn-primary);
|
|
1394
|
+
}
|
|
1395
|
+
.dcpa-q-tag[data-tone="danger"] {
|
|
1396
|
+
border-color: transparent;
|
|
1397
|
+
background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);
|
|
1398
|
+
color: var(--dsw-alias-state-error-primary);
|
|
1399
|
+
}
|
|
1400
|
+
.dcpa-q-tag[data-tone="info"] {
|
|
1401
|
+
border-color: transparent;
|
|
1402
|
+
background: color-mix(in srgb, var(--dsw-alias-state-business-primary, var(--dsw-alias-brand-primary)) 10%, transparent);
|
|
1403
|
+
color: var(--dsw-alias-state-business-primary, var(--dsw-alias-brand-primary));
|
|
1404
|
+
}
|
|
1405
|
+
.dcpa-q-msg { margin: 12px 0 0; font-size: 12px; line-height: 18px; }
|
|
1406
|
+
.dcpa-q-msg[data-tone="error"] { color: var(--dsw-alias-state-error-primary); }
|
|
1407
|
+
.dcpa-q-msg[data-tone="ok"] { color: var(--dsw-alias-state-success-primary); }
|
|
1408
|
+
.dcpa-q-summary { display: flex; flex-direction: column; gap: 8px; padding: 20px 0 4px; }
|
|
1409
|
+
.dcpa-q-dist {
|
|
1410
|
+
display: flex;
|
|
1411
|
+
gap: 2px;
|
|
1412
|
+
height: 6px;
|
|
1413
|
+
border-radius: 3px;
|
|
1414
|
+
overflow: hidden;
|
|
1415
|
+
background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2));
|
|
1416
|
+
}
|
|
1417
|
+
.dcpa-q-dist span { display: block; height: 100%; }
|
|
1418
|
+
.dcpa-q-legend {
|
|
1419
|
+
display: flex;
|
|
1420
|
+
flex-wrap: wrap;
|
|
1421
|
+
gap: 4px 16px;
|
|
1422
|
+
font-size: 12px;
|
|
1423
|
+
line-height: 18px;
|
|
1424
|
+
color: var(--dsw-alias-label-secondary);
|
|
1425
|
+
}
|
|
1426
|
+
.dcpa-q-legend span { display: inline-flex; align-items: center; gap: 6px; }
|
|
1427
|
+
.dcpa-q-legend b { font-weight: 500; color: var(--dsw-alias-label-primary); font-variant-numeric: tabular-nums; }
|
|
1428
|
+
.dcpa-q-swatch { width: 8px; height: 8px; border-radius: 2px; }
|
|
1429
|
+
[data-level="ok"] > .dcpa-q-swatch, .dcpa-q-dist [data-level="ok"] { background: var(--dsw-alias-state-success-primary); }
|
|
1430
|
+
[data-level="low"] > .dcpa-q-swatch, .dcpa-q-dist [data-level="low"] { background: var(--dsw-alias-state-warn-primary); }
|
|
1431
|
+
[data-level="empty"] > .dcpa-q-swatch, .dcpa-q-dist [data-level="empty"] { background: var(--dsw-alias-state-error-primary); }
|
|
1432
|
+
[data-level="issue"] > .dcpa-q-swatch, .dcpa-q-dist [data-level="issue"] { background: var(--dsw-alias-state-idle-primary); }
|
|
1433
|
+
[data-level="unknown"] > .dcpa-q-swatch, .dcpa-q-dist [data-level="unknown"] { background: var(--dsw-alias-border-l3, var(--dsw-alias-border-l2)); }
|
|
1434
|
+
.dcpa-q-tools { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 16px 0 0; }
|
|
1435
|
+
.dcpa-q-filter {
|
|
1436
|
+
height: 28px;
|
|
1437
|
+
padding: 0 10px;
|
|
1438
|
+
border: 0;
|
|
1439
|
+
border-radius: var(--dsw-radius-sm, 8px);
|
|
1440
|
+
background: transparent;
|
|
1441
|
+
color: var(--dsw-alias-label-secondary);
|
|
1442
|
+
font: inherit;
|
|
1443
|
+
font-size: 13px;
|
|
1444
|
+
cursor: pointer;
|
|
1445
|
+
}
|
|
1446
|
+
.dcpa-q-filter:hover { background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2)); }
|
|
1447
|
+
.dcpa-q-filter[aria-pressed="true"] {
|
|
1448
|
+
color: var(--dsw-alias-label-primary);
|
|
1449
|
+
background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2));
|
|
1450
|
+
font-weight: 500;
|
|
1451
|
+
}
|
|
1452
|
+
.dcpa-q-filter small { margin-left: 4px; font-size: 11px; color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary)); }
|
|
1453
|
+
.dcpa-q-tools .dcpa-q-input { flex: 0 1 220px; height: 28px; margin-left: auto; border-radius: var(--dsw-radius-sm, 8px); }
|
|
1454
|
+
.dcpa-q-group-title {
|
|
1455
|
+
display: flex;
|
|
1456
|
+
align-items: baseline;
|
|
1457
|
+
gap: 6px;
|
|
1458
|
+
padding: 20px 0 4px;
|
|
1459
|
+
font-size: 12px;
|
|
1460
|
+
line-height: 18px;
|
|
1461
|
+
font-weight: 500;
|
|
1462
|
+
color: var(--dsw-alias-label-secondary);
|
|
1463
|
+
}
|
|
1464
|
+
.dcpa-q-group-title small { font-weight: 400; color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary)); }
|
|
1465
|
+
.dcpa-q-cred {
|
|
1466
|
+
display: grid;
|
|
1467
|
+
grid-template-columns: minmax(160px, 1fr) auto;
|
|
1468
|
+
align-items: center;
|
|
1469
|
+
gap: 8px 24px;
|
|
1470
|
+
padding: 14px 0;
|
|
1471
|
+
border-bottom: 0.5px solid var(--dsw-alias-border-l2);
|
|
1472
|
+
}
|
|
1473
|
+
@media (max-width: 640px) { .dcpa-q-cred { grid-template-columns: 1fr; } }
|
|
1474
|
+
.dcpa-q-cred[data-status="disabled"] .dcpa-q-who, .dcpa-q-cred[data-status="unavailable"] .dcpa-q-who { opacity: 0.55; }
|
|
1475
|
+
.dcpa-q-who { min-width: 0; }
|
|
1476
|
+
.dcpa-q-name {
|
|
1477
|
+
font-size: 14px;
|
|
1478
|
+
line-height: 20px;
|
|
1479
|
+
overflow: hidden;
|
|
1480
|
+
text-overflow: ellipsis;
|
|
1481
|
+
white-space: nowrap;
|
|
1482
|
+
}
|
|
1483
|
+
.dcpa-q-meta {
|
|
1484
|
+
display: flex;
|
|
1485
|
+
align-items: center;
|
|
1486
|
+
flex-wrap: wrap;
|
|
1487
|
+
gap: 6px;
|
|
1488
|
+
margin-top: 4px;
|
|
1489
|
+
font-size: 12px;
|
|
1490
|
+
line-height: 18px;
|
|
1491
|
+
color: var(--dsw-alias-label-secondary);
|
|
1492
|
+
}
|
|
1493
|
+
.dcpa-q-link {
|
|
1494
|
+
padding: 0;
|
|
1495
|
+
border: 0;
|
|
1496
|
+
background: transparent;
|
|
1497
|
+
color: var(--dsw-alias-label-secondary);
|
|
1498
|
+
font: inherit;
|
|
1499
|
+
cursor: pointer;
|
|
1500
|
+
text-decoration: underline;
|
|
1501
|
+
text-decoration-color: var(--dsw-alias-border-l3, var(--dsw-alias-border-l2));
|
|
1502
|
+
text-underline-offset: 3px;
|
|
1503
|
+
}
|
|
1504
|
+
.dcpa-q-link:hover { color: var(--dsw-alias-label-primary); }
|
|
1505
|
+
.dcpa-q-meters { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 10px 20px; }
|
|
1506
|
+
@media (max-width: 640px) { .dcpa-q-meters { justify-content: flex-start; } }
|
|
1507
|
+
.dcpa-q-meter { width: 128px; display: flex; flex-direction: column; gap: 4px; }
|
|
1508
|
+
.dcpa-q-meter-top {
|
|
1509
|
+
display: flex;
|
|
1510
|
+
align-items: baseline;
|
|
1511
|
+
justify-content: space-between;
|
|
1512
|
+
gap: 6px;
|
|
1513
|
+
font-size: 12px;
|
|
1514
|
+
line-height: 16px;
|
|
1515
|
+
color: var(--dsw-alias-label-secondary);
|
|
1516
|
+
}
|
|
1517
|
+
.dcpa-q-meter-top span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1518
|
+
.dcpa-q-pct {
|
|
1519
|
+
font-size: 13px;
|
|
1520
|
+
font-weight: 500;
|
|
1521
|
+
font-variant-numeric: tabular-nums;
|
|
1522
|
+
color: var(--dsw-alias-label-primary);
|
|
1523
|
+
}
|
|
1524
|
+
.dcpa-q-meter[data-level="low"] .dcpa-q-pct { color: var(--dsw-alias-state-warn-primary); }
|
|
1525
|
+
.dcpa-q-meter[data-level="empty"] .dcpa-q-pct { color: var(--dsw-alias-state-error-primary); }
|
|
1526
|
+
.dcpa-q-track {
|
|
1527
|
+
height: 4px;
|
|
1528
|
+
border-radius: 2px;
|
|
1529
|
+
background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2));
|
|
1530
|
+
overflow: hidden;
|
|
1531
|
+
}
|
|
1532
|
+
.dcpa-q-fill { display: block; height: 100%; border-radius: 2px; background: var(--dsw-alias-state-idle-primary); }
|
|
1533
|
+
[data-level="ok"] > .dcpa-q-track > .dcpa-q-fill { background: var(--dsw-alias-state-success-primary); }
|
|
1534
|
+
[data-level="mid"] > .dcpa-q-track > .dcpa-q-fill { background: var(--dsw-alias-state-success-primary); opacity: 0.7; }
|
|
1535
|
+
[data-level="low"] > .dcpa-q-track > .dcpa-q-fill { background: var(--dsw-alias-state-warn-primary); }
|
|
1536
|
+
[data-level="empty"] > .dcpa-q-track > .dcpa-q-fill { background: var(--dsw-alias-state-error-primary); }
|
|
1537
|
+
.dcpa-q-reset { font-size: 11px; line-height: 14px; color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary)); }
|
|
1538
|
+
.dcpa-q-note {
|
|
1539
|
+
margin: 0;
|
|
1540
|
+
font-size: 12px;
|
|
1541
|
+
line-height: 18px;
|
|
1542
|
+
color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));
|
|
1543
|
+
text-align: right;
|
|
1544
|
+
}
|
|
1545
|
+
.dcpa-q-note[data-tone="error"] { color: var(--dsw-alias-state-error-primary); }
|
|
1546
|
+
.dcpa-q-models { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 4px; }
|
|
1547
|
+
.dcpa-q-models code {
|
|
1548
|
+
padding: 1px 8px;
|
|
1549
|
+
border-radius: 999px;
|
|
1550
|
+
font-family: var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
|
|
1551
|
+
font-size: 11px;
|
|
1552
|
+
line-height: 17px;
|
|
1553
|
+
background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2));
|
|
1554
|
+
color: var(--dsw-alias-label-secondary);
|
|
1555
|
+
}
|
|
1556
|
+
.dcpa-q-models code[data-picker="true"] { color: var(--dsw-alias-label-primary); }
|
|
1557
|
+
.dcpa-q-map { margin-top: 8px; }
|
|
1558
|
+
.dcpa-q-map > summary {
|
|
1559
|
+
display: flex;
|
|
1560
|
+
align-items: center;
|
|
1561
|
+
justify-content: space-between;
|
|
1562
|
+
gap: 12px;
|
|
1563
|
+
padding: 16px 0;
|
|
1564
|
+
list-style: none;
|
|
1565
|
+
cursor: pointer;
|
|
1566
|
+
border-bottom: 0.5px solid var(--dsw-alias-border-l2);
|
|
1567
|
+
}
|
|
1568
|
+
.dcpa-q-map > summary::-webkit-details-marker { display: none; }
|
|
1569
|
+
.dcpa-q-map > summary::after {
|
|
1570
|
+
content: "";
|
|
1571
|
+
flex: none;
|
|
1572
|
+
width: 7px;
|
|
1573
|
+
height: 7px;
|
|
1574
|
+
margin-right: 4px;
|
|
1575
|
+
border-right: 1.5px solid var(--dsw-alias-label-secondary);
|
|
1576
|
+
border-bottom: 1.5px solid var(--dsw-alias-label-secondary);
|
|
1577
|
+
transform: rotate(45deg);
|
|
1578
|
+
}
|
|
1579
|
+
.dcpa-q-map[open] > summary::after { transform: rotate(-135deg); }
|
|
1580
|
+
.dcpa-q-map-row {
|
|
1581
|
+
display: grid;
|
|
1582
|
+
grid-template-columns: minmax(150px, 1fr) minmax(0, 1.5fr) 48px;
|
|
1583
|
+
align-items: center;
|
|
1584
|
+
gap: 12px;
|
|
1585
|
+
padding: 10px 0;
|
|
1586
|
+
border-bottom: 0.5px solid var(--dsw-alias-border-l1);
|
|
1587
|
+
font-size: 13px;
|
|
1588
|
+
line-height: 20px;
|
|
1589
|
+
}
|
|
1590
|
+
.dcpa-q-map-model {
|
|
1591
|
+
font-family: var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
|
|
1592
|
+
font-size: 12px;
|
|
1593
|
+
overflow-wrap: anywhere;
|
|
1594
|
+
}
|
|
1595
|
+
.dcpa-q-map-creds { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; min-width: 0; color: var(--dsw-alias-label-secondary); font-size: 12px; }
|
|
1596
|
+
.dcpa-q-map-pct { text-align: right; }
|
|
1096
1597
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1598
|
+
/* ---- Account page: bar, header, tabs, form rows, save bar ---- */
|
|
1599
|
+
.dcpa-acct-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
1600
|
+
.dcpa-acct-bar .dcpa-q-seg { margin: 0; }
|
|
1601
|
+
.dcpa-q-seg-tab .dcpa-dot { width: 6px; height: 6px; }
|
|
1602
|
+
.dcpa-account { display: flex; flex-direction: column; }
|
|
1603
|
+
.dcpa-acct-head {
|
|
1604
|
+
display: flex;
|
|
1605
|
+
align-items: center;
|
|
1606
|
+
justify-content: space-between;
|
|
1607
|
+
gap: 16px;
|
|
1608
|
+
padding: 16px 0 12px;
|
|
1609
|
+
}
|
|
1610
|
+
.dcpa-status-line { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
1611
|
+
.dcpa-acct-tabs { margin: 8px 0 4px; }
|
|
1612
|
+
.dcpa-tabbody { display: flex; flex-direction: column; }
|
|
1613
|
+
.dcpa-frow {
|
|
1614
|
+
display: grid;
|
|
1615
|
+
grid-template-columns: 168px minmax(0, 1fr);
|
|
1616
|
+
align-items: center;
|
|
1617
|
+
gap: 8px 16px;
|
|
1618
|
+
padding: 12px 0;
|
|
1619
|
+
border-bottom: 0.5px solid var(--dsw-alias-border-l2);
|
|
1620
|
+
}
|
|
1621
|
+
@media (max-width: 620px) { .dcpa-frow { grid-template-columns: 1fr; } }
|
|
1622
|
+
.dcpa-frow-label {
|
|
1623
|
+
display: flex;
|
|
1624
|
+
align-items: center;
|
|
1625
|
+
gap: 4px;
|
|
1626
|
+
font-size: 14px;
|
|
1627
|
+
line-height: 20px;
|
|
1628
|
+
color: var(--dsw-alias-label-primary);
|
|
1629
|
+
}
|
|
1630
|
+
.dcpa-frow-control { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
|
1631
|
+
.dcpa-frow-control > .dcpa-q-input { flex: none; }
|
|
1632
|
+
.dcpa-frow-control .dcpa-q-msg { margin: 0; }
|
|
1633
|
+
.dcpa-inline { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
1634
|
+
.dcpa-inline .dcpa-q-input { flex: 1; }
|
|
1635
|
+
.dcpa-inline-narrow .dcpa-q-input { flex: 0 0 120px; }
|
|
1636
|
+
select.dcpa-q-input { appearance: auto; padding-right: 8px; }
|
|
1637
|
+
.dcpa-adv > summary {
|
|
1638
|
+
padding: 14px 0;
|
|
1639
|
+
font-size: 13px;
|
|
1640
|
+
color: var(--dsw-alias-label-secondary);
|
|
1641
|
+
cursor: pointer;
|
|
1642
|
+
border-bottom: 0.5px solid var(--dsw-alias-border-l2);
|
|
1643
|
+
}
|
|
1644
|
+
.dcpa-adv > summary:hover { color: var(--dsw-alias-label-primary); }
|
|
1645
|
+
.dcpa-panel { display: flex; flex-direction: column; padding-top: 16px; }
|
|
1646
|
+
.dcpa-form-actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 16px; }
|
|
1647
|
+
.dcpa-switch:disabled { opacity: 0.5; cursor: default; }
|
|
1648
|
+
.dcpa-menu { position: relative; display: inline-flex; }
|
|
1649
|
+
.dcpa-menu-btn { width: 28px; padding: 0; font-size: 16px; letter-spacing: 1px; }
|
|
1650
|
+
.dcpa-menu-pop {
|
|
1651
|
+
position: absolute;
|
|
1652
|
+
top: calc(100% + 4px);
|
|
1653
|
+
right: 0;
|
|
1654
|
+
z-index: 20;
|
|
1655
|
+
isolation: isolate;
|
|
1656
|
+
min-width: 160px;
|
|
1657
|
+
padding: 4px;
|
|
1658
|
+
display: flex;
|
|
1659
|
+
flex-direction: column;
|
|
1660
|
+
border-radius: var(--dsw-radius-lg, 12px);
|
|
1661
|
+
--dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
|
|
1662
|
+
box-shadow: var(--dsw-elevation-prominent, 0 8px 24px var(--dsw-alias-border-l2));
|
|
1663
|
+
}
|
|
1664
|
+
.dcpa-menu-pop::before, .dcpa-info-pop::before {
|
|
1665
|
+
content: "";
|
|
1666
|
+
position: absolute;
|
|
1667
|
+
inset: 0;
|
|
1668
|
+
z-index: -1;
|
|
1669
|
+
border-radius: inherit;
|
|
1670
|
+
background: var(--dsw-menu-surface-fill, var(--dsw-alias-bg-layer-1));
|
|
1671
|
+
backdrop-filter: var(--dsw-menu-backdrop-filter, none);
|
|
1672
|
+
}
|
|
1673
|
+
.dcpa-menu-item {
|
|
1674
|
+
height: 32px;
|
|
1675
|
+
padding: 0 10px;
|
|
1676
|
+
border: 0;
|
|
1677
|
+
border-radius: var(--dsw-radius-md, 10px);
|
|
1678
|
+
background: transparent;
|
|
1679
|
+
color: var(--dsw-alias-label-primary);
|
|
1680
|
+
font: inherit;
|
|
1681
|
+
font-size: 13px;
|
|
1682
|
+
text-align: left;
|
|
1683
|
+
cursor: pointer;
|
|
1684
|
+
white-space: nowrap;
|
|
1685
|
+
}
|
|
1686
|
+
.dcpa-menu-item:hover:not(:disabled), .dcpa-menu-item:focus-visible { background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2)); outline: none; }
|
|
1687
|
+
.dcpa-menu-item:disabled { opacity: 0.4; cursor: default; }
|
|
1688
|
+
.dcpa-menu-item[data-variant="danger"] { color: var(--dsw-alias-state-error-primary); }
|
|
1689
|
+
.dcpa-info { position: relative; display: inline-flex; align-items: center; }
|
|
1690
|
+
.dcpa-info-btn {
|
|
1691
|
+
box-sizing: border-box;
|
|
1692
|
+
width: 16px;
|
|
1693
|
+
height: 16px;
|
|
1694
|
+
padding: 0;
|
|
1695
|
+
border: 1px solid var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));
|
|
1696
|
+
border-radius: 50%;
|
|
1697
|
+
background: transparent;
|
|
1698
|
+
color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));
|
|
1699
|
+
font: 600 10px/14px Georgia, "Times New Roman", serif;
|
|
1700
|
+
font-style: italic;
|
|
1701
|
+
text-align: center;
|
|
1702
|
+
cursor: help;
|
|
1703
|
+
}
|
|
1704
|
+
.dcpa-info-btn:hover, .dcpa-info-btn[aria-expanded="true"] {
|
|
1705
|
+
border-color: var(--dsw-alias-label-secondary);
|
|
1706
|
+
color: var(--dsw-alias-label-primary);
|
|
1707
|
+
}
|
|
1708
|
+
.dcpa-info-btn:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-brand-primary)); outline-offset: 1px; }
|
|
1709
|
+
.dcpa-info-pop {
|
|
1710
|
+
position: absolute;
|
|
1711
|
+
top: calc(100% + 6px);
|
|
1712
|
+
left: -8px;
|
|
1713
|
+
z-index: 30;
|
|
1714
|
+
isolation: isolate;
|
|
1715
|
+
width: max-content;
|
|
1716
|
+
max-width: 280px;
|
|
1717
|
+
padding: 8px 10px;
|
|
1718
|
+
border-radius: var(--dsw-radius-md, 10px);
|
|
1719
|
+
--dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
|
|
1720
|
+
box-shadow: var(--dsw-elevation-prominent, 0 8px 24px var(--dsw-alias-border-l2));
|
|
1721
|
+
color: var(--dsw-alias-label-primary);
|
|
1722
|
+
font-size: 12px;
|
|
1723
|
+
line-height: 18px;
|
|
1724
|
+
font-weight: 400;
|
|
1725
|
+
white-space: normal;
|
|
1726
|
+
text-align: left;
|
|
1727
|
+
}
|
|
1728
|
+
.dcpa-info-pop[data-align="end"] { left: auto; right: -8px; }
|
|
1729
|
+
.dcpa-savebar {
|
|
1730
|
+
position: sticky;
|
|
1731
|
+
bottom: 0;
|
|
1732
|
+
z-index: 10;
|
|
1733
|
+
display: flex;
|
|
1734
|
+
align-items: center;
|
|
1735
|
+
justify-content: space-between;
|
|
1736
|
+
gap: 12px;
|
|
1737
|
+
margin-top: 16px;
|
|
1738
|
+
padding: 12px 0;
|
|
1739
|
+
border-top: 0.5px solid var(--dsw-alias-border-l2);
|
|
1740
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
1741
|
+
}
|
|
1742
|
+
.dcpa-savebar-text { font-size: 13px; color: var(--dsw-alias-label-secondary); }
|
|
1743
|
+
.dcpa-savebar-text[data-tone="ok"] { color: var(--dsw-alias-state-success-primary); }
|
|
1744
|
+
.dcpa-savebar-text[data-tone="error"] { color: var(--dsw-alias-state-error-primary); }
|
|
1745
|
+
/* Model tab: same flat rows as the other tabs. */
|
|
1746
|
+
.dcpa-models { gap: 10px; padding-top: 12px; }
|
|
1747
|
+
.dcpa-models .dcpa-input { height: 34px; border-radius: var(--dsw-radius-md, 10px); background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-base)); border-color: var(--dsw-alias-border-l4, var(--dsw-alias-border-l2)); }
|
|
1748
|
+
.dcpa-models .dcpa-btn { border: 0; background: transparent; height: 28px; border-radius: var(--dsw-radius-sm, 8px); }
|
|
1749
|
+
.dcpa-models .dcpa-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2)); }
|
|
1750
|
+
.dcpa-models .dcpa-seg { background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2)); border: 0; padding: 3px; }
|
|
1751
|
+
.dcpa-models .dcpa-seg-btn[data-active="true"] { background: var(--dsw-alias-bg-layer-1); box-shadow: var(--dsw-elevation-soft, none); }
|
|
1752
|
+
.dcpa-models .dcpa-chip { border: 0; background: transparent; }
|
|
1753
|
+
.dcpa-models .dcpa-chip[data-active="true"] { background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2)); color: var(--dsw-alias-label-primary); }
|
|
1754
|
+
.dcpa-models .dcpa-model-list { max-height: none; overflow: visible; gap: 0; }
|
|
1755
|
+
.dcpa-models .dcpa-model-row,
|
|
1756
|
+
.dcpa-models .dcpa-model-row[data-checked="true"] {
|
|
1757
|
+
padding: 10px 2px;
|
|
1758
|
+
border: 0;
|
|
1759
|
+
border-bottom: 0.5px solid var(--dsw-alias-border-l2);
|
|
1760
|
+
border-radius: 0;
|
|
1761
|
+
background: transparent;
|
|
1762
|
+
}
|
|
1763
|
+
.dcpa-models .dcpa-model-row:hover { background: var(--dsw-alias-interactive-bg-hover, transparent); }
|
|
1764
|
+
.dcpa-models .dcpa-badge { border: 0; background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2)); border-radius: 999px; padding: 1px 8px; }
|
|
1765
|
+
.dcpa-models .dcpa-editor { border-top: 0; padding: 4px 0 6px 25px; }
|
|
1766
|
+
/* ---- Composer badge: same metrics as the model selector trigger ---- */
|
|
1767
|
+
.dcpa-q-wrap { display: inline-flex; align-items: center; flex: none; }
|
|
1768
|
+
.dcpa-q-badge {
|
|
1769
|
+
box-sizing: border-box;
|
|
1770
|
+
display: inline-flex;
|
|
1771
|
+
align-items: center;
|
|
1772
|
+
gap: 5px;
|
|
1773
|
+
height: 28px;
|
|
1774
|
+
padding: 0 6px;
|
|
1775
|
+
border: none;
|
|
1776
|
+
border-radius: var(--dsw-radius-sm, 8px);
|
|
1777
|
+
background: transparent;
|
|
1778
|
+
color: var(--dsw-alias-label-secondary);
|
|
1779
|
+
font: inherit;
|
|
1780
|
+
font-size: 13px;
|
|
1781
|
+
line-height: 20px;
|
|
1782
|
+
cursor: pointer;
|
|
1783
|
+
white-space: nowrap;
|
|
1784
|
+
}
|
|
1785
|
+
.dcpa-q-badge:hover, .dcpa-q-badge[aria-expanded="true"] {
|
|
1786
|
+
background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2));
|
|
1787
|
+
}
|
|
1788
|
+
.dcpa-q-badge-pct { font-variant-numeric: tabular-nums; }
|
|
1789
|
+
.dcpa-q-badge[data-level="low"] .dcpa-q-badge-pct { color: var(--dsw-alias-state-warn-primary); }
|
|
1790
|
+
.dcpa-q-badge[data-level="empty"] .dcpa-q-badge-pct { color: var(--dsw-alias-state-error-primary); }
|
|
1791
|
+
.dcpa-q-bars { display: inline-flex; align-items: flex-end; gap: 1.5px; height: 11px; }
|
|
1792
|
+
.dcpa-q-bars i { display: block; width: 3px; border-radius: 1px; background: var(--dsw-alias-label-tertiary, var(--dsw-alias-border-l2)); opacity: 0.45; }
|
|
1793
|
+
.dcpa-q-bars i:nth-child(1) { height: 5px; }
|
|
1794
|
+
.dcpa-q-bars i:nth-child(2) { height: 8px; }
|
|
1795
|
+
.dcpa-q-bars i:nth-child(3) { height: 11px; }
|
|
1796
|
+
.dcpa-q-bars[data-level="ok"] i { background: var(--dsw-alias-state-success-primary); opacity: 1; }
|
|
1797
|
+
.dcpa-q-bars[data-level="mid"] i:nth-child(-n+2) { background: var(--dsw-alias-state-success-primary); opacity: 1; }
|
|
1798
|
+
.dcpa-q-bars[data-level="low"] i:nth-child(1) { background: var(--dsw-alias-state-warn-primary); opacity: 1; }
|
|
1799
|
+
.dcpa-q-bars[data-level="empty"] i { background: var(--dsw-alias-state-error-primary); }
|
|
1800
|
+
|
|
1801
|
+
/* ---- Composer peek: the DSH menu surface ---- */
|
|
1802
|
+
.dcpa-q-pop {
|
|
1803
|
+
position: fixed;
|
|
1804
|
+
z-index: 1100;
|
|
1805
|
+
isolation: isolate;
|
|
1806
|
+
box-sizing: border-box;
|
|
1807
|
+
width: 340px;
|
|
1808
|
+
max-width: calc(100vw - 32px);
|
|
1809
|
+
max-height: min(440px, calc(100vh - 96px));
|
|
1810
|
+
overflow-y: auto;
|
|
1811
|
+
padding: 4px;
|
|
1812
|
+
border: 0;
|
|
1813
|
+
border-radius: var(--dsw-radius-lg, 12px);
|
|
1814
|
+
--dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
|
|
1815
|
+
box-shadow: var(--dsw-elevation-prominent, 0 8px 24px var(--dsw-alias-border-l2));
|
|
1816
|
+
color: var(--dsw-alias-label-primary);
|
|
1817
|
+
font-size: 13px;
|
|
1818
|
+
line-height: 20px;
|
|
1819
|
+
text-align: left;
|
|
1820
|
+
}
|
|
1821
|
+
.dcpa-q-pop::before {
|
|
1822
|
+
content: "";
|
|
1823
|
+
position: absolute;
|
|
1824
|
+
inset: 0;
|
|
1825
|
+
z-index: -1;
|
|
1826
|
+
border-radius: inherit;
|
|
1827
|
+
background: var(--dsw-menu-surface-fill, var(--dsw-alias-bg-layer-1));
|
|
1828
|
+
backdrop-filter: var(--dsw-menu-backdrop-filter, none);
|
|
1829
|
+
pointer-events: none;
|
|
1830
|
+
}
|
|
1831
|
+
.dcpa-q-pop-head { padding: 8px 8px 6px; }
|
|
1832
|
+
.dcpa-q-pop-model { font-weight: 500; overflow-wrap: anywhere; }
|
|
1833
|
+
.dcpa-q-pop-sub { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }
|
|
1834
|
+
.dcpa-q-pop-cred {
|
|
1835
|
+
display: flex;
|
|
1836
|
+
flex-direction: column;
|
|
1837
|
+
gap: 8px;
|
|
1838
|
+
padding: 8px;
|
|
1839
|
+
border-radius: var(--dsw-radius-md, 10px);
|
|
1840
|
+
}
|
|
1841
|
+
.dcpa-q-pop-cred:hover { background: var(--dsw-alias-interactive-bg-hover, transparent); }
|
|
1842
|
+
.dcpa-q-pop-who { display: flex; align-items: center; gap: 6px; min-width: 0; font-size: 13px; }
|
|
1843
|
+
.dcpa-q-pop-who .dcpa-q-name { font-size: 13px; flex: 0 1 auto; }
|
|
1844
|
+
.dcpa-q-pop-cred .dcpa-q-meters { justify-content: flex-start; gap: 8px 16px; }
|
|
1845
|
+
.dcpa-q-pop-cred .dcpa-q-meter { width: 146px; }
|
|
1846
|
+
.dcpa-q-pop-cred .dcpa-q-note { text-align: left; }
|
|
1847
|
+
.dcpa-q-pop-text { margin: 0; padding: 4px 8px 8px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }
|
|
1848
|
+
.dcpa-q-pop-text[data-tone="error"] { color: var(--dsw-alias-state-error-primary); }
|
|
1849
|
+
.dcpa-q-pop-foot {
|
|
1850
|
+
display: flex;
|
|
1851
|
+
align-items: center;
|
|
1852
|
+
justify-content: space-between;
|
|
1853
|
+
gap: 8px;
|
|
1854
|
+
margin-top: 4px;
|
|
1855
|
+
padding: 6px 4px 2px 8px;
|
|
1856
|
+
border-top: 0.5px solid var(--dsw-alias-border-l1);
|
|
1857
|
+
font-size: 12px;
|
|
1858
|
+
color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));
|
|
1859
|
+
}
|
|
1860
|
+
`;
|
|
1861
|
+
|
|
1862
|
+
const REASONING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
1863
|
+
const PROTOCOL_NAME_KEYS = {
|
|
1864
|
+
"openai-completions": "protocolNameCompletions",
|
|
1865
|
+
"openai-responses": "protocolNameResponses",
|
|
1866
|
+
"anthropic-messages": "protocolNameMessages",
|
|
1867
|
+
};
|
|
1868
|
+
const PROTOCOLS = Object.keys(PROTOCOL_NAME_KEYS);
|
|
1869
|
+
|
|
1870
|
+
function cloneOverrides(value) {
|
|
1871
|
+
const out = {};
|
|
1872
|
+
for (const [id, entry] of Object.entries(value ?? {})) {
|
|
1873
|
+
out[id] = {
|
|
1874
|
+
...entry,
|
|
1875
|
+
...(Array.isArray(entry.reasoningLevels) ? { reasoningLevels: [...entry.reasoningLevels] } : {}),
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
return out;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
function overridesKey(value) {
|
|
1882
|
+
return JSON.stringify(value ?? {});
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
function formatTokens(n) {
|
|
1886
|
+
if (typeof n !== "number" || !Number.isFinite(n)) return "";
|
|
1887
|
+
if (n >= 1000000) return `${(n / 1000000).toFixed(n % 1000000 === 0 ? 0 : 1)}M`;
|
|
1888
|
+
if (n >= 1000) return `${Math.round(n / 1000)}K`;
|
|
1889
|
+
return String(n);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
async function postJson(url, body) {
|
|
1893
|
+
const res = await fetch(url, {
|
|
1894
|
+
method: "POST",
|
|
1895
|
+
headers: { "content-type": "application/json" },
|
|
1896
|
+
body: JSON.stringify(body ?? {}),
|
|
1897
|
+
});
|
|
1898
|
+
const payload = await res.json();
|
|
1899
|
+
if (!res.ok || payload.ok === false) {
|
|
1900
|
+
throw new Error(payload.error || `HTTP ${res.status}`);
|
|
1901
|
+
}
|
|
1902
|
+
return payload;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
function stateDotClass(state) {
|
|
1906
|
+
return state === "connected"
|
|
1907
|
+
? "dcpa-dot dcpa-dot-connected"
|
|
1908
|
+
: state === "probing"
|
|
1909
|
+
? "dcpa-dot dcpa-dot-probing"
|
|
1910
|
+
: state === "error"
|
|
1911
|
+
? "dcpa-dot dcpa-dot-error"
|
|
1912
|
+
: "dcpa-dot dcpa-dot-idle";
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
const SERVICE_UNAVAILABLE_STATUS = new Set([404, 405, 501]);
|
|
1916
|
+
const UPDATE_ERROR_KEYS = {
|
|
1917
|
+
"network-error": "updateNetworkError",
|
|
1918
|
+
"invalid-metadata": "updateMetadataError",
|
|
1919
|
+
"invalid-response": "updateMetadataError",
|
|
1920
|
+
"service-unavailable": "updateServiceUnavailableError",
|
|
1921
|
+
"forbidden-origin": "updateOriginError",
|
|
1922
|
+
"channel-unavailable": "updateChannelUnavailable",
|
|
1923
|
+
"unavailable-channel": "updateChannelUnavailable",
|
|
1924
|
+
"integrity-error": "updateIntegrityError",
|
|
1925
|
+
"invalid-package": "updateInvalidPackage",
|
|
1926
|
+
"storage-error": "updateStorageError",
|
|
1927
|
+
busy: "updateBusyError",
|
|
1928
|
+
"stale-version": "updateStaleVersion",
|
|
1929
|
+
disposed: "updateDisposedError",
|
|
1112
1930
|
"linked-install": "updateLinkedHint",
|
|
1113
1931
|
"unsupported-host": "updateUnsupportedHint",
|
|
1114
1932
|
"not-installed": "updateNotInstalledHint",
|
|
@@ -1925,19 +2743,113 @@ window.__ModuleLoader__.load({
|
|
|
1925
2743
|
}
|
|
1926
2744
|
|
|
1927
2745
|
/**
|
|
1928
|
-
*
|
|
1929
|
-
*
|
|
1930
|
-
*
|
|
2746
|
+
* Small "i" help button: the explanation opens on hover, keyboard focus or
|
|
2747
|
+
* click (touch), closes on Escape / leaving. The text stays in the locale
|
|
2748
|
+
* dictionaries and is announced through aria-describedby.
|
|
2749
|
+
*/
|
|
2750
|
+
function InfoTip({ t, text }) {
|
|
2751
|
+
const [open, setOpen] = useState(false);
|
|
2752
|
+
const [alignEnd, setAlignEnd] = useState(false);
|
|
2753
|
+
const ref = useRef(null);
|
|
2754
|
+
const id = useMemo(() => `dcpa-tip-${Math.random().toString(36).slice(2, 9)}`, []);
|
|
2755
|
+
if (!text) return null;
|
|
2756
|
+
// Open towards the roomier side of the surrounding page.
|
|
2757
|
+
const show = () => {
|
|
2758
|
+
const el = ref.current;
|
|
2759
|
+
const box = el?.closest?.(".dcpa-wrap, .dcpa-q-pop")?.getBoundingClientRect?.();
|
|
2760
|
+
const r = el?.getBoundingClientRect?.();
|
|
2761
|
+
if (box && r) setAlignEnd(r.left - box.left > box.width / 2);
|
|
2762
|
+
setOpen(true);
|
|
2763
|
+
};
|
|
2764
|
+
return h(
|
|
2765
|
+
"span",
|
|
2766
|
+
{
|
|
2767
|
+
ref,
|
|
2768
|
+
className: "dcpa-info",
|
|
2769
|
+
onMouseEnter: show,
|
|
2770
|
+
onMouseLeave: () => setOpen(false),
|
|
2771
|
+
},
|
|
2772
|
+
h(
|
|
2773
|
+
"button",
|
|
2774
|
+
{
|
|
2775
|
+
type: "button",
|
|
2776
|
+
className: "dcpa-info-btn",
|
|
2777
|
+
"aria-label": t("infoLabel"),
|
|
2778
|
+
"aria-describedby": open ? id : undefined,
|
|
2779
|
+
"aria-expanded": open,
|
|
2780
|
+
onClick: (e) => {
|
|
2781
|
+
e.preventDefault();
|
|
2782
|
+
e.stopPropagation();
|
|
2783
|
+
if (open) setOpen(false);
|
|
2784
|
+
else show();
|
|
2785
|
+
},
|
|
2786
|
+
onFocus: show,
|
|
2787
|
+
onBlur: () => setOpen(false),
|
|
2788
|
+
onKeyDown: (e) => {
|
|
2789
|
+
if (e.key === "Escape") setOpen(false);
|
|
2790
|
+
},
|
|
2791
|
+
},
|
|
2792
|
+
"i"
|
|
2793
|
+
),
|
|
2794
|
+
open ? h("span", { id, role: "tooltip", className: "dcpa-info-pop", "data-align": alignEnd ? "end" : "start" }, text) : null
|
|
2795
|
+
);
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
/** One labelled form row: label (+ help) on the left, control on the right. */
|
|
2799
|
+
function FieldRow({ t, label, help, htmlFor, children, stack = false }) {
|
|
2800
|
+
return h(
|
|
2801
|
+
"div",
|
|
2802
|
+
{ className: stack ? "dcpa-frow dcpa-frow-stack" : "dcpa-frow" },
|
|
2803
|
+
h(
|
|
2804
|
+
"div",
|
|
2805
|
+
{ className: "dcpa-frow-label" },
|
|
2806
|
+
htmlFor ? h("label", { htmlFor }, label) : h("span", null, label),
|
|
2807
|
+
help ? h(InfoTip, { t, text: help }) : null
|
|
2808
|
+
),
|
|
2809
|
+
h("div", { className: "dcpa-frow-control" }, children)
|
|
2810
|
+
);
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
function accountStateLabel(t, state) {
|
|
2814
|
+
return state === "connected"
|
|
2815
|
+
? t("statusConnected")
|
|
2816
|
+
: state === "probing"
|
|
2817
|
+
? t("statusProbing")
|
|
2818
|
+
: state === "error"
|
|
2819
|
+
? t("statusError")
|
|
2820
|
+
: state === "disabled"
|
|
2821
|
+
? t("statusDisabled")
|
|
2822
|
+
: t("statusIdle");
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
/** Lowest remaining percentage among an account's usable credentials (tab hint). */
|
|
2826
|
+
function accountQuotaFloor(quotaData, accountId) {
|
|
2827
|
+
const account = (quotaData?.accounts ?? []).find((a) => a.accountId === accountId);
|
|
2828
|
+
if (!account || account.keyConfigured !== true) return null;
|
|
2829
|
+
const values = (account.credentials ?? [])
|
|
2830
|
+
.filter((c) => c.status === "active")
|
|
2831
|
+
.map((c) => credentialHeadroom(c))
|
|
2832
|
+
.filter((v) => v !== null);
|
|
2833
|
+
return values.length > 0 ? Math.min(...values) : null;
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
/**
|
|
2837
|
+
* The CLIProxyAPI settings section: one account bar for every tab, then the
|
|
2838
|
+
* selected account's header, its 连接 / 模型 / 额度 tabs and one save bar.
|
|
2839
|
+
* Owns the account server data; the add-account form creates an account only
|
|
2840
|
+
* when the user connects it.
|
|
1931
2841
|
*/
|
|
1932
2842
|
function CpaSettingsPanel({ t }) {
|
|
1933
2843
|
const [loading, setLoading] = useState(true);
|
|
1934
|
-
const [busy, setBusy] = useState(null); // null | "refresh" | "save" | "add" | "remove"
|
|
1935
|
-
const [
|
|
1936
|
-
const [
|
|
2844
|
+
const [busy, setBusy] = useState(null); // null | "refresh" | "save" | "patch" | "add" | "remove" | "test"
|
|
2845
|
+
const [loadError, setLoadError] = useState(null);
|
|
2846
|
+
const [notice, setNotice] = useState(null); // { tone: "ok" | "error", text }
|
|
1937
2847
|
const [data, setData] = useState(null);
|
|
1938
2848
|
const [activeId, setActiveId] = useState(null);
|
|
1939
2849
|
// Bumped whenever the server's saved config should replace the drafts.
|
|
1940
2850
|
const [rev, setRev] = useState(0);
|
|
2851
|
+
const [adding, setAdding] = useState(false);
|
|
2852
|
+
const dirtyRef = useRef(false);
|
|
1941
2853
|
|
|
1942
2854
|
const accounts = Array.isArray(data?.accounts) ? data.accounts : [];
|
|
1943
2855
|
const active = accounts.find((a) => a.id === activeId) ?? accounts[0];
|
|
@@ -1951,9 +2863,9 @@ window.__ModuleLoader__.load({
|
|
|
1951
2863
|
}
|
|
1952
2864
|
setData(body);
|
|
1953
2865
|
setRev((r) => r + 1);
|
|
1954
|
-
|
|
2866
|
+
setLoadError(null);
|
|
1955
2867
|
} catch (err) {
|
|
1956
|
-
|
|
2868
|
+
setLoadError(err instanceof Error ? err.message : String(err));
|
|
1957
2869
|
} finally {
|
|
1958
2870
|
setLoading(false);
|
|
1959
2871
|
}
|
|
@@ -1963,123 +2875,153 @@ window.__ModuleLoader__.load({
|
|
|
1963
2875
|
void fetchStatus();
|
|
1964
2876
|
}, [fetchStatus]);
|
|
1965
2877
|
|
|
2878
|
+
// Success messages fade after a few seconds; errors stay until the next action.
|
|
2879
|
+
useEffect(() => {
|
|
2880
|
+
if (notice?.tone !== "ok") return undefined;
|
|
2881
|
+
const id = setTimeout(() => setNotice(null), 4000);
|
|
2882
|
+
return () => clearTimeout(id);
|
|
2883
|
+
}, [notice]);
|
|
2884
|
+
|
|
2885
|
+
const confirmDiscard = useCallback(() => {
|
|
2886
|
+
if (!dirtyRef.current) return true;
|
|
2887
|
+
if (typeof window === "undefined" || typeof window.confirm !== "function") return true;
|
|
2888
|
+
return window.confirm(t("discardConfirm"));
|
|
2889
|
+
}, [t]);
|
|
2890
|
+
|
|
1966
2891
|
const run = useCallback(async (kind, fn) => {
|
|
1967
2892
|
setBusy(kind);
|
|
1968
|
-
|
|
1969
|
-
setToast(null);
|
|
2893
|
+
setNotice(null);
|
|
1970
2894
|
try {
|
|
1971
2895
|
return await fn();
|
|
1972
2896
|
} catch (err) {
|
|
1973
|
-
|
|
2897
|
+
setNotice({ tone: "error", text: err instanceof Error ? err.message : String(err) });
|
|
1974
2898
|
return undefined;
|
|
1975
2899
|
} finally {
|
|
1976
2900
|
setBusy(null);
|
|
1977
2901
|
}
|
|
1978
2902
|
}, []);
|
|
1979
2903
|
|
|
1980
|
-
const
|
|
1981
|
-
(
|
|
2904
|
+
const selectAccount = (id) => {
|
|
2905
|
+
if (id === active?.id && !adding) return;
|
|
2906
|
+
if (!confirmDiscard()) return;
|
|
2907
|
+
dirtyRef.current = false;
|
|
2908
|
+
setAdding(false);
|
|
2909
|
+
setNotice(null);
|
|
2910
|
+
setActiveId(id);
|
|
2911
|
+
setRev((r) => r + 1);
|
|
2912
|
+
};
|
|
2913
|
+
|
|
2914
|
+
const actions = {
|
|
2915
|
+
refresh: (accountId) =>
|
|
1982
2916
|
run("refresh", async () => {
|
|
1983
2917
|
setData(await postJson(API.refresh, { accountId }));
|
|
1984
2918
|
}),
|
|
1985
|
-
|
|
1986
|
-
);
|
|
1987
|
-
|
|
1988
|
-
const onSave = useCallback(
|
|
1989
|
-
(accountId, payload) =>
|
|
2919
|
+
save: (accountId, payload) =>
|
|
1990
2920
|
run("save", async () => {
|
|
1991
2921
|
setData(await postJson(API.save, { ...payload, accountId }));
|
|
2922
|
+
dirtyRef.current = false;
|
|
1992
2923
|
setRev((r) => r + 1);
|
|
1993
|
-
|
|
2924
|
+
setNotice({ tone: "ok", text: t("savedToast") });
|
|
1994
2925
|
}),
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
run("add", async () => {
|
|
2001
|
-
const body = await postJson(API.addAccount, {});
|
|
2002
|
-
setData(body);
|
|
2003
|
-
if (body.createdId) setActiveId(body.createdId);
|
|
2004
|
-
setRev((r) => r + 1);
|
|
2005
|
-
setToast(t("addedToast"));
|
|
2926
|
+
// One field, applied at once (enable switch, clear key); drafts stay.
|
|
2927
|
+
patch: (accountId, patch, okText) =>
|
|
2928
|
+
run("patch", async () => {
|
|
2929
|
+
setData(await postJson(API.save, { ...patch, accountId }));
|
|
2930
|
+
if (okText) setNotice({ tone: "ok", text: okText });
|
|
2006
2931
|
}),
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
const onRemove = useCallback(
|
|
2011
|
-
(account) => {
|
|
2932
|
+
test: (accountId, draft) =>
|
|
2933
|
+
run("test", async () => (await postJson(API.testAccount, { ...(accountId ? { accountId } : {}), ...draft })).test),
|
|
2934
|
+
remove: (account) => {
|
|
2012
2935
|
const name = account.config?.displayName || account.id;
|
|
2013
|
-
if (typeof window !== "undefined" && !window.confirm(t
|
|
2014
|
-
return;
|
|
2015
|
-
}
|
|
2936
|
+
if (typeof window !== "undefined" && typeof window.confirm === "function" && !window.confirm(fmt(t, "removeConfirm", { name }))) return;
|
|
2016
2937
|
void run("remove", async () => {
|
|
2017
2938
|
const body = await postJson(API.removeAccount, { accountId: account.id });
|
|
2939
|
+
dirtyRef.current = false;
|
|
2018
2940
|
setData(body);
|
|
2019
2941
|
setActiveId(body.accounts?.[0]?.id ?? null);
|
|
2020
2942
|
setRev((r) => r + 1);
|
|
2021
|
-
|
|
2943
|
+
setNotice({ tone: "ok", text: t("removedToast") });
|
|
2022
2944
|
});
|
|
2023
2945
|
},
|
|
2024
|
-
|
|
2025
|
-
|
|
2946
|
+
// Reload the drafts from the saved config.
|
|
2947
|
+
discard: () => {
|
|
2948
|
+
dirtyRef.current = false;
|
|
2949
|
+
setNotice(null);
|
|
2950
|
+
setRev((r) => r + 1);
|
|
2951
|
+
},
|
|
2952
|
+
add: (draft, enabled) =>
|
|
2953
|
+
run("add", async () => {
|
|
2954
|
+
const body = await postJson(API.addAccount, { ...draft, enabled });
|
|
2955
|
+
setData(body);
|
|
2956
|
+
if (body.createdId) setActiveId(body.createdId);
|
|
2957
|
+
setAdding(false);
|
|
2958
|
+
setRev((r) => r + 1);
|
|
2959
|
+
setNotice({ tone: "ok", text: enabled ? t("addedToast") : t("addedDisabledToast") });
|
|
2960
|
+
}),
|
|
2961
|
+
};
|
|
2962
|
+
|
|
2963
|
+
const showAdd = adding || (!loading && data !== null && accounts.length === 0);
|
|
2026
2964
|
|
|
2027
2965
|
return h(
|
|
2028
2966
|
"div",
|
|
2029
|
-
{ className: "dcpa-wrap", "data-dsh-plugin": MODULE_ID },
|
|
2967
|
+
{ className: "dcpa-wrap dcpa-q", "data-dsh-plugin": MODULE_ID },
|
|
2030
2968
|
h(
|
|
2031
2969
|
"div",
|
|
2032
|
-
{ className: "dcpa-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
className: "dcpa-account-chip",
|
|
2054
|
-
"aria-selected": active?.id === a.id,
|
|
2055
|
-
"data-active": active?.id === a.id ? "true" : "false",
|
|
2056
|
-
"data-enabled": a.config?.enabled === false ? "false" : "true",
|
|
2057
|
-
title: a.config?.provider,
|
|
2058
|
-
onClick: () => setActiveId(a.id),
|
|
2059
|
-
},
|
|
2060
|
-
h("span", { className: stateDotClass(a.status?.state) }),
|
|
2061
|
-
a.config?.displayName || a.id,
|
|
2062
|
-
a.config?.enabled === false
|
|
2063
|
-
? h("span", { className: "dcpa-badge" }, t("accountOffBadge"))
|
|
2064
|
-
: null
|
|
2970
|
+
{ className: "dcpa-acct-bar" },
|
|
2971
|
+
accounts.length > 0
|
|
2972
|
+
? h(
|
|
2973
|
+
"div",
|
|
2974
|
+
{ className: "dcpa-q-seg", role: "tablist", "aria-label": t("accountsLabel") },
|
|
2975
|
+
accounts.map((a) =>
|
|
2976
|
+
h(
|
|
2977
|
+
"button",
|
|
2978
|
+
{
|
|
2979
|
+
key: a.id,
|
|
2980
|
+
type: "button",
|
|
2981
|
+
role: "tab",
|
|
2982
|
+
className: "dcpa-q-seg-tab",
|
|
2983
|
+
"aria-selected": !showAdd && active?.id === a.id,
|
|
2984
|
+
title: a.config?.provider,
|
|
2985
|
+
onClick: () => selectAccount(a.id),
|
|
2986
|
+
},
|
|
2987
|
+
h("span", { className: stateDotClass(a.config?.enabled === false ? "idle" : a.status?.state) }),
|
|
2988
|
+
a.config?.displayName || a.id
|
|
2989
|
+
)
|
|
2990
|
+
)
|
|
2065
2991
|
)
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2992
|
+
: null,
|
|
2993
|
+
h(
|
|
2994
|
+
"button",
|
|
2995
|
+
{
|
|
2996
|
+
type: "button",
|
|
2997
|
+
className: "dcpa-q-btn",
|
|
2998
|
+
"data-variant": "outline",
|
|
2999
|
+
disabled: busy !== null || loading || showAdd,
|
|
3000
|
+
onClick: () => {
|
|
3001
|
+
if (!confirmDiscard()) return;
|
|
3002
|
+
dirtyRef.current = false;
|
|
3003
|
+
setNotice(null);
|
|
3004
|
+
setAdding(true);
|
|
2074
3005
|
},
|
|
2075
|
-
|
|
2076
|
-
)
|
|
2077
|
-
)
|
|
2078
|
-
!loading && accounts.length === 0 ? h("p", { className: "dcpa-hint" }, t("noAccounts")) : null,
|
|
2079
|
-
error ? h("p", { className: "dcpa-error", role: "alert" }, error) : null,
|
|
2080
|
-
toast ? h("p", { className: "dcpa-success", role: "status" }, toast) : null
|
|
3006
|
+
},
|
|
3007
|
+
t("addAccountBtn")
|
|
3008
|
+
)
|
|
2081
3009
|
),
|
|
2082
|
-
|
|
3010
|
+
loadError ? h("p", { className: "dcpa-q-msg", "data-tone": "error", role: "alert" }, loadError) : null,
|
|
3011
|
+
showAdd
|
|
3012
|
+
? h(AddAccountForm, {
|
|
3013
|
+
t,
|
|
3014
|
+
busy,
|
|
3015
|
+
notice,
|
|
3016
|
+
canCancel: accounts.length > 0,
|
|
3017
|
+
onCancel: () => {
|
|
3018
|
+
setAdding(false);
|
|
3019
|
+
setNotice(null);
|
|
3020
|
+
},
|
|
3021
|
+
onTest: (draft) => actions.test(undefined, draft),
|
|
3022
|
+
onAdd: actions.add,
|
|
3023
|
+
})
|
|
3024
|
+
: active
|
|
2083
3025
|
? h(AccountPanel, {
|
|
2084
3026
|
key: active.id,
|
|
2085
3027
|
t,
|
|
@@ -2087,18 +3029,105 @@ window.__ModuleLoader__.load({
|
|
|
2087
3029
|
global: data?.global ?? {},
|
|
2088
3030
|
rev,
|
|
2089
3031
|
busy,
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
3032
|
+
notice,
|
|
3033
|
+
actions,
|
|
3034
|
+
onDirtyChange: (dirty) => {
|
|
3035
|
+
dirtyRef.current = dirty;
|
|
3036
|
+
},
|
|
2093
3037
|
})
|
|
2094
3038
|
: null
|
|
2095
3039
|
);
|
|
2096
3040
|
}
|
|
2097
3041
|
|
|
2098
|
-
|
|
3042
|
+
/** Add an account: name, address, key → test, then create enabled (or disabled on request). */
|
|
3043
|
+
function AddAccountForm({ t, busy, notice, canCancel, onCancel, onTest, onAdd }) {
|
|
3044
|
+
const [displayName, setDisplayName] = useState("");
|
|
3045
|
+
const [baseURL, setBaseURL] = useState("");
|
|
3046
|
+
const [apiKey, setApiKey] = useState("");
|
|
3047
|
+
const [failure, setFailure] = useState(null);
|
|
3048
|
+
|
|
3049
|
+
const draft = () => ({
|
|
3050
|
+
...(displayName.trim() ? { displayName: displayName.trim() } : {}),
|
|
3051
|
+
baseURL: baseURL.trim(),
|
|
3052
|
+
...(apiKey.trim() ? { apiKey: apiKey.trim() } : {}),
|
|
3053
|
+
});
|
|
3054
|
+
const connect = async () => {
|
|
3055
|
+
setFailure(null);
|
|
3056
|
+
const result = await onTest({ baseURL: baseURL.trim(), apiKey: apiKey.trim() });
|
|
3057
|
+
if (result === undefined) return;
|
|
3058
|
+
if (result.ok) await onAdd(draft(), true);
|
|
3059
|
+
else setFailure(result.error || t("statusError"));
|
|
3060
|
+
};
|
|
3061
|
+
|
|
3062
|
+
return h(
|
|
3063
|
+
"div",
|
|
3064
|
+
{ className: "dcpa-panel" },
|
|
3065
|
+
h("h3", { className: "dcpa-q-h" }, t("addTitle")),
|
|
3066
|
+
h(
|
|
3067
|
+
FieldRow,
|
|
3068
|
+
{ t, label: t("displayNameLabel"), help: t("displayNameHint"), htmlFor: "dcpa-add-name" },
|
|
3069
|
+
h("input", {
|
|
3070
|
+
id: "dcpa-add-name",
|
|
3071
|
+
className: "dcpa-q-input",
|
|
3072
|
+
value: displayName,
|
|
3073
|
+
placeholder: t("addNamePlaceholder"),
|
|
3074
|
+
onChange: (e) => setDisplayName(e.target.value),
|
|
3075
|
+
})
|
|
3076
|
+
),
|
|
3077
|
+
h(
|
|
3078
|
+
FieldRow,
|
|
3079
|
+
{ t, label: t("baseUrlLabel"), htmlFor: "dcpa-add-url" },
|
|
3080
|
+
h("input", {
|
|
3081
|
+
id: "dcpa-add-url",
|
|
3082
|
+
className: "dcpa-q-input",
|
|
3083
|
+
value: baseURL,
|
|
3084
|
+
placeholder: t("baseUrlPlaceholder"),
|
|
3085
|
+
onChange: (e) => setBaseURL(e.target.value),
|
|
3086
|
+
})
|
|
3087
|
+
),
|
|
3088
|
+
h(
|
|
3089
|
+
FieldRow,
|
|
3090
|
+
{ t, label: t("apiKeyLabel"), help: t("apiKeyHint"), htmlFor: "dcpa-add-key" },
|
|
3091
|
+
h("input", {
|
|
3092
|
+
id: "dcpa-add-key",
|
|
3093
|
+
className: "dcpa-q-input",
|
|
3094
|
+
type: "password",
|
|
3095
|
+
autoComplete: "new-password",
|
|
3096
|
+
value: apiKey,
|
|
3097
|
+
placeholder: t("apiKeyUnsetPlaceholder"),
|
|
3098
|
+
onChange: (e) => setApiKey(e.target.value),
|
|
3099
|
+
})
|
|
3100
|
+
),
|
|
3101
|
+
failure ? h("p", { className: "dcpa-q-msg", "data-tone": "error", role: "alert" }, fmt(t, "testFail", { error: failure })) : null,
|
|
3102
|
+
notice?.tone === "error" ? h("p", { className: "dcpa-q-msg", "data-tone": "error", role: "alert" }, notice.text) : null,
|
|
3103
|
+
h(
|
|
3104
|
+
"div",
|
|
3105
|
+
{ className: "dcpa-form-actions" },
|
|
3106
|
+
canCancel ? h("button", { type: "button", className: "dcpa-q-btn", disabled: busy !== null, onClick: onCancel }, t("cancelBtn")) : null,
|
|
3107
|
+
failure
|
|
3108
|
+
? h(
|
|
3109
|
+
"button",
|
|
3110
|
+
{ type: "button", className: "dcpa-q-btn", "data-variant": "outline", disabled: busy !== null, onClick: () => void onAdd(draft(), false) },
|
|
3111
|
+
t("addAnywayBtn")
|
|
3112
|
+
)
|
|
3113
|
+
: null,
|
|
3114
|
+
h(
|
|
3115
|
+
"button",
|
|
3116
|
+
{ type: "button", className: "dcpa-q-btn", "data-variant": "primary", disabled: busy !== null, onClick: () => void connect() },
|
|
3117
|
+
busy === "test" || busy === "add" ? t("connectingBtn") : t("connectBtn")
|
|
3118
|
+
)
|
|
3119
|
+
)
|
|
3120
|
+
);
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
function AccountPanel({ t, account, global, rev, busy, notice, actions, onDirtyChange }) {
|
|
2099
3124
|
const syncing = busy === "refresh";
|
|
2100
3125
|
const saving = busy === "save";
|
|
2101
|
-
|
|
3126
|
+
|
|
3127
|
+
const [tab, setTab] = useState("connection");
|
|
3128
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
3129
|
+
const menuRef = useRef(null);
|
|
3130
|
+
const [testResult, setTestResult] = useState(null);
|
|
2102
3131
|
|
|
2103
3132
|
// Model filter UI state
|
|
2104
3133
|
const [search, setSearch] = useState("");
|
|
@@ -2111,7 +3140,6 @@ window.__ModuleLoader__.load({
|
|
|
2111
3140
|
const [editingId, setEditingId] = useState(null);
|
|
2112
3141
|
|
|
2113
3142
|
// Form draft state
|
|
2114
|
-
const [enabled, setEnabled] = useState(true);
|
|
2115
3143
|
const [baseURL, setBaseURL] = useState("");
|
|
2116
3144
|
const [apiKeyDraft, setApiKeyDraft] = useState("");
|
|
2117
3145
|
// "auto" or the protocol every model is fixed to.
|
|
@@ -2124,20 +3152,32 @@ window.__ModuleLoader__.load({
|
|
|
2124
3152
|
const [refreshIntervalSeconds, setRefreshIntervalSeconds] = useState("300");
|
|
2125
3153
|
const [probeTimeoutMs, setProbeTimeoutMs] = useState("5000");
|
|
2126
3154
|
|
|
3155
|
+
const savedConnection = (c = account.config ?? {}) => ({
|
|
3156
|
+
baseURL: c.baseURL ?? "",
|
|
3157
|
+
protocol: c.protocolMode === "fixed" ? c.protocol ?? "openai-completions" : "auto",
|
|
3158
|
+
provider: c.provider ?? "cli-proxy",
|
|
3159
|
+
displayName: c.displayName ?? "CLI Proxy API",
|
|
3160
|
+
apiKeyEnv: c.effectiveApiKeyEnv ?? c.apiKeyEnv ?? "CLI_PROXY_API_KEY",
|
|
3161
|
+
includePrefix: c.includePrefix ?? "",
|
|
3162
|
+
excludePrefix: c.excludePrefix ?? "",
|
|
3163
|
+
refreshIntervalSeconds: String(global?.refreshIntervalSeconds ?? 300),
|
|
3164
|
+
});
|
|
3165
|
+
|
|
2127
3166
|
// Load the drafts from the saved config on mount and after every save.
|
|
2128
3167
|
useEffect(() => {
|
|
2129
3168
|
const c = account.config ?? {};
|
|
2130
|
-
|
|
2131
|
-
setBaseURL(
|
|
2132
|
-
setProtocol(
|
|
2133
|
-
setProvider(
|
|
2134
|
-
setDisplayName(
|
|
2135
|
-
setApiKeyEnv(
|
|
2136
|
-
setIncludePrefix(
|
|
2137
|
-
setExcludePrefix(
|
|
2138
|
-
setRefreshIntervalSeconds(
|
|
3169
|
+
const saved = savedConnection(c);
|
|
3170
|
+
setBaseURL(saved.baseURL);
|
|
3171
|
+
setProtocol(saved.protocol);
|
|
3172
|
+
setProvider(saved.provider);
|
|
3173
|
+
setDisplayName(saved.displayName);
|
|
3174
|
+
setApiKeyEnv(saved.apiKeyEnv);
|
|
3175
|
+
setIncludePrefix(saved.includePrefix);
|
|
3176
|
+
setExcludePrefix(saved.excludePrefix);
|
|
3177
|
+
setRefreshIntervalSeconds(saved.refreshIntervalSeconds);
|
|
2139
3178
|
setProbeTimeoutMs(String(global?.probeTimeoutMs ?? 5000));
|
|
2140
3179
|
setApiKeyDraft("");
|
|
3180
|
+
setTestResult(null);
|
|
2141
3181
|
|
|
2142
3182
|
const sel = Array.isArray(c.selectedModels)
|
|
2143
3183
|
? c.selectedModels
|
|
@@ -2153,53 +3193,46 @@ window.__ModuleLoader__.load({
|
|
|
2153
3193
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2154
3194
|
}, [rev]);
|
|
2155
3195
|
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
selectedIds
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
apiKeyEnv,
|
|
2192
|
-
includePrefix,
|
|
2193
|
-
excludePrefix,
|
|
2194
|
-
refreshIntervalSeconds,
|
|
2195
|
-
probeTimeoutMs,
|
|
2196
|
-
apiKeyDraft,
|
|
2197
|
-
]
|
|
2198
|
-
);
|
|
3196
|
+
useEffect(() => {
|
|
3197
|
+
if (!menuOpen) return undefined;
|
|
3198
|
+
const onDown = (e) => {
|
|
3199
|
+
if (menuRef.current && !menuRef.current.contains(e.target)) setMenuOpen(false);
|
|
3200
|
+
};
|
|
3201
|
+
const onKey = (e) => {
|
|
3202
|
+
if (e.key === "Escape") setMenuOpen(false);
|
|
3203
|
+
};
|
|
3204
|
+
document.addEventListener("mousedown", onDown, true);
|
|
3205
|
+
document.addEventListener("keydown", onKey, true);
|
|
3206
|
+
return () => {
|
|
3207
|
+
document.removeEventListener("mousedown", onDown, true);
|
|
3208
|
+
document.removeEventListener("keydown", onKey, true);
|
|
3209
|
+
};
|
|
3210
|
+
}, [menuOpen]);
|
|
3211
|
+
|
|
3212
|
+
const payload = () => ({
|
|
3213
|
+
baseURL: baseURL.trim(),
|
|
3214
|
+
protocolMode: protocol === "auto" ? "auto" : "fixed",
|
|
3215
|
+
...(protocol === "auto" ? {} : { protocol }),
|
|
3216
|
+
provider: provider.trim() || account.config?.provider || "cli-proxy",
|
|
3217
|
+
displayName: displayName.trim() || account.config?.displayName || "CLI Proxy API",
|
|
3218
|
+
apiKeyEnv: apiKeyEnv.trim() || account.config?.apiKeyEnv || "CLI_PROXY_API_KEY",
|
|
3219
|
+
includePrefix: includePrefix.trim(),
|
|
3220
|
+
excludePrefix: excludePrefix.trim(),
|
|
3221
|
+
// Before the first catalog is seen, an empty draft is not a choice:
|
|
3222
|
+
// leave the selection unset so the first probe can recommend one.
|
|
3223
|
+
...(selectedIds.size > 0 || (account.status?.allModels?.length ?? 0) > 0
|
|
3224
|
+
? { selectedModels: [...selectedIds] }
|
|
3225
|
+
: {}),
|
|
3226
|
+
modelOverrides,
|
|
3227
|
+
refreshIntervalSeconds: Math.max(15, Number(refreshIntervalSeconds) || 300),
|
|
3228
|
+
probeTimeoutMs: Math.max(250, Number(probeTimeoutMs) || 5000),
|
|
3229
|
+
...(apiKeyDraft.trim() !== "" ? { apiKey: apiKeyDraft.trim() } : {}),
|
|
3230
|
+
});
|
|
2199
3231
|
|
|
2200
3232
|
const st = account.status ?? { state: "idle", modelCount: 0, models: [], allModels: [] };
|
|
2201
3233
|
const cred = account.credential ?? { configured: false, ref: apiKeyEnv };
|
|
2202
3234
|
const effectiveUrl = account.config?.effectiveBaseURL || "";
|
|
3235
|
+
const enabled = account.config?.enabled !== false;
|
|
2203
3236
|
|
|
2204
3237
|
const allModels = useMemo(() => {
|
|
2205
3238
|
if (Array.isArray(st.allModels) && st.allModels.length > 0) return st.allModels;
|
|
@@ -2234,9 +3267,25 @@ window.__ModuleLoader__.load({
|
|
|
2234
3267
|
|
|
2235
3268
|
const currentIdsKey = useMemo(() => [...selectedIds].sort().join("\0"), [selectedIds]);
|
|
2236
3269
|
const currentOverridesKey = useMemo(() => overridesKey(modelOverrides), [modelOverrides]);
|
|
2237
|
-
const
|
|
2238
|
-
|
|
2239
|
-
|
|
3270
|
+
const draftConnection = {
|
|
3271
|
+
baseURL,
|
|
3272
|
+
protocol,
|
|
3273
|
+
provider,
|
|
3274
|
+
displayName,
|
|
3275
|
+
apiKeyEnv,
|
|
3276
|
+
includePrefix,
|
|
3277
|
+
excludePrefix,
|
|
3278
|
+
refreshIntervalSeconds,
|
|
3279
|
+
};
|
|
3280
|
+
const dirty =
|
|
3281
|
+
currentIdsKey !== savedIdsKey ||
|
|
3282
|
+
currentOverridesKey !== savedOverridesKey ||
|
|
3283
|
+
apiKeyDraft.trim() !== "" ||
|
|
3284
|
+
JSON.stringify(draftConnection) !== JSON.stringify(savedConnection());
|
|
3285
|
+
|
|
3286
|
+
useEffect(() => {
|
|
3287
|
+
onDirtyChange?.(dirty);
|
|
3288
|
+
}, [dirty, onDirtyChange]);
|
|
2240
3289
|
|
|
2241
3290
|
const toggleModel = useCallback((id) => {
|
|
2242
3291
|
setSelectedIds((prev) => {
|
|
@@ -2282,18 +3331,10 @@ window.__ModuleLoader__.load({
|
|
|
2282
3331
|
};
|
|
2283
3332
|
const merged = { ...current, ...patch };
|
|
2284
3333
|
const normalized = {};
|
|
2285
|
-
if (
|
|
2286
|
-
Number.isInteger(merged.contextWindow) &&
|
|
2287
|
-
merged.contextWindow > 0 &&
|
|
2288
|
-
merged.contextWindow !== discovered.contextWindow
|
|
2289
|
-
) {
|
|
3334
|
+
if (Number.isInteger(merged.contextWindow) && merged.contextWindow > 0 && merged.contextWindow !== discovered.contextWindow) {
|
|
2290
3335
|
normalized.contextWindow = merged.contextWindow;
|
|
2291
3336
|
}
|
|
2292
|
-
if (
|
|
2293
|
-
Number.isInteger(merged.maxTokens) &&
|
|
2294
|
-
merged.maxTokens > 0 &&
|
|
2295
|
-
merged.maxTokens !== discovered.maxTokens
|
|
2296
|
-
) {
|
|
3337
|
+
if (Number.isInteger(merged.maxTokens) && merged.maxTokens > 0 && merged.maxTokens !== discovered.maxTokens) {
|
|
2297
3338
|
normalized.maxTokens = merged.maxTokens;
|
|
2298
3339
|
}
|
|
2299
3340
|
if (merged.image !== discovered.image) normalized.image = merged.image === true;
|
|
@@ -2309,9 +3350,7 @@ window.__ModuleLoader__.load({
|
|
|
2309
3350
|
|
|
2310
3351
|
// The protocol a model would use with the panel's current draft settings.
|
|
2311
3352
|
const effectiveProtocol = useCallback(
|
|
2312
|
-
(m) =>
|
|
2313
|
-
modelOverrides[m.id]?.protocol ??
|
|
2314
|
-
(protocol === "auto" ? m.nativeProtocol ?? "openai-completions" : protocol),
|
|
3353
|
+
(m) => modelOverrides[m.id]?.protocol ?? (protocol === "auto" ? m.nativeProtocol ?? "openai-completions" : protocol),
|
|
2315
3354
|
[modelOverrides, protocol]
|
|
2316
3355
|
);
|
|
2317
3356
|
const protocolName = (p) => t(PROTOCOL_NAME_KEYS[p] ?? "protocolNameCompletions");
|
|
@@ -2324,134 +3363,188 @@ window.__ModuleLoader__.load({
|
|
|
2324
3363
|
});
|
|
2325
3364
|
}, []);
|
|
2326
3365
|
|
|
2327
|
-
const
|
|
3366
|
+
const quotaSnap = useQuota(sharedQuota);
|
|
3367
|
+
const quotaFloor = accountQuotaFloor(quotaSnap.data, account.id);
|
|
2328
3368
|
|
|
2329
|
-
const
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
? t("statusProbing")
|
|
2334
|
-
: st.state === "error"
|
|
2335
|
-
? t("statusError")
|
|
2336
|
-
: st.state === "disabled"
|
|
2337
|
-
? t("statusDisabled")
|
|
2338
|
-
: t("statusIdle");
|
|
3369
|
+
const discard = () => {
|
|
3370
|
+
setTestResult(null);
|
|
3371
|
+
actions.discard();
|
|
3372
|
+
};
|
|
2339
3373
|
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
3374
|
+
const testConnection = async () => {
|
|
3375
|
+
setTestResult(null);
|
|
3376
|
+
const result = await actions.test(account.id, { baseURL: baseURL.trim(), apiKey: apiKeyDraft.trim() });
|
|
3377
|
+
if (result !== undefined) setTestResult(result);
|
|
3378
|
+
};
|
|
3379
|
+
|
|
3380
|
+
const statusParts = [
|
|
3381
|
+
accountStateLabel(t, enabled ? st.state : "disabled"),
|
|
3382
|
+
st.endpoint || effectiveUrl || null,
|
|
3383
|
+
allModels.length > 0 ? fmt(t, "modelsCountShort", { n: st.modelCount ?? 0, total: st.totalDiscovered ?? allModels.length }) : null,
|
|
3384
|
+
].filter(Boolean);
|
|
3385
|
+
|
|
3386
|
+
const tabs = [
|
|
3387
|
+
["connection", t("tabConnection")],
|
|
3388
|
+
["models", `${t("tabModels")} ${selectedIds.size}`],
|
|
3389
|
+
["quota", quotaFloor === null ? t("tabQuota") : `${t("tabQuota")} ${pctText(quotaFloor)}`],
|
|
3390
|
+
];
|
|
3391
|
+
|
|
3392
|
+
const connectionTab = h(
|
|
3393
|
+
"div",
|
|
3394
|
+
{ className: "dcpa-tabbody" },
|
|
2344
3395
|
h(
|
|
2345
|
-
|
|
2346
|
-
{
|
|
3396
|
+
FieldRow,
|
|
3397
|
+
{ t, label: t("displayNameLabel"), help: t("displayNameHint"), htmlFor: `dcpa-name-${account.id}` },
|
|
3398
|
+
h("input", {
|
|
3399
|
+
id: `dcpa-name-${account.id}`,
|
|
3400
|
+
className: "dcpa-q-input",
|
|
3401
|
+
value: displayName,
|
|
3402
|
+
onChange: (e) => setDisplayName(e.target.value),
|
|
3403
|
+
})
|
|
3404
|
+
),
|
|
3405
|
+
h(
|
|
3406
|
+
FieldRow,
|
|
3407
|
+
{ t, label: t("baseUrlLabel"), htmlFor: `dcpa-url-${account.id}` },
|
|
2347
3408
|
h(
|
|
2348
3409
|
"div",
|
|
2349
|
-
{ className: "dcpa-
|
|
2350
|
-
h(
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
),
|
|
3410
|
+
{ className: "dcpa-inline" },
|
|
3411
|
+
h("input", {
|
|
3412
|
+
id: `dcpa-url-${account.id}`,
|
|
3413
|
+
className: "dcpa-q-input",
|
|
3414
|
+
value: baseURL,
|
|
3415
|
+
placeholder: effectiveUrl || t("baseUrlPlaceholder"),
|
|
3416
|
+
onChange: (e) => {
|
|
3417
|
+
setBaseURL(e.target.value);
|
|
3418
|
+
setTestResult(null);
|
|
3419
|
+
},
|
|
3420
|
+
}),
|
|
2360
3421
|
h(
|
|
2361
|
-
"
|
|
2362
|
-
{ className: "dcpa-
|
|
2363
|
-
|
|
2364
|
-
"button",
|
|
2365
|
-
{
|
|
2366
|
-
type: "button",
|
|
2367
|
-
className: "dcpa-btn",
|
|
2368
|
-
disabled: syncing || saving || loading,
|
|
2369
|
-
onClick: onRefreshNow,
|
|
2370
|
-
},
|
|
2371
|
-
syncing ? t("refreshingBtn") : t("refreshBtn")
|
|
2372
|
-
),
|
|
2373
|
-
h(
|
|
2374
|
-
"button",
|
|
2375
|
-
{
|
|
2376
|
-
type: "button",
|
|
2377
|
-
className: "dcpa-btn dcpa-btn-primary",
|
|
2378
|
-
disabled: saving || syncing || loading,
|
|
2379
|
-
onClick: () => onSave(false),
|
|
2380
|
-
},
|
|
2381
|
-
saving ? t("savingBtn") : t("saveBtn")
|
|
2382
|
-
)
|
|
3422
|
+
"button",
|
|
3423
|
+
{ type: "button", className: "dcpa-q-btn", "data-variant": "outline", disabled: busy !== null, onClick: () => void testConnection() },
|
|
3424
|
+
busy === "test" ? t("testingBtn") : t("testBtn")
|
|
2383
3425
|
)
|
|
2384
3426
|
),
|
|
3427
|
+
testResult
|
|
3428
|
+
? h(
|
|
3429
|
+
"p",
|
|
3430
|
+
{ className: "dcpa-q-msg", "data-tone": testResult.ok ? "ok" : "error", role: "status" },
|
|
3431
|
+
testResult.ok
|
|
3432
|
+
? fmt(t, "testOk", { endpoint: testResult.endpoint, n: testResult.modelCount })
|
|
3433
|
+
: fmt(t, "testFail", { error: testResult.error })
|
|
3434
|
+
)
|
|
3435
|
+
: null
|
|
3436
|
+
),
|
|
3437
|
+
h(
|
|
3438
|
+
FieldRow,
|
|
3439
|
+
{ t, label: t("apiKeyLabel"), help: fmt(t, "apiKeyHint", { ref: cred.ref || apiKeyEnv }), htmlFor: `dcpa-key-${account.id}` },
|
|
2385
3440
|
h(
|
|
2386
3441
|
"div",
|
|
2387
|
-
{ className: "dcpa-
|
|
2388
|
-
h(
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
3442
|
+
{ className: "dcpa-inline" },
|
|
3443
|
+
h("input", {
|
|
3444
|
+
id: `dcpa-key-${account.id}`,
|
|
3445
|
+
type: "password",
|
|
3446
|
+
className: "dcpa-q-input",
|
|
3447
|
+
autoComplete: "new-password",
|
|
3448
|
+
value: apiKeyDraft,
|
|
3449
|
+
placeholder: cred.configured ? t("apiKeySetPlaceholder") : t("apiKeyUnsetPlaceholder"),
|
|
3450
|
+
onChange: (e) => {
|
|
3451
|
+
setApiKeyDraft(e.target.value);
|
|
3452
|
+
setTestResult(null);
|
|
3453
|
+
},
|
|
3454
|
+
}),
|
|
3455
|
+
cred.configured
|
|
3456
|
+
? h(
|
|
3457
|
+
"button",
|
|
3458
|
+
{
|
|
3459
|
+
type: "button",
|
|
3460
|
+
className: "dcpa-q-btn",
|
|
3461
|
+
"data-variant": "danger",
|
|
3462
|
+
disabled: busy !== null,
|
|
3463
|
+
onClick: () => {
|
|
3464
|
+
if (typeof window !== "undefined" && typeof window.confirm === "function" && !window.confirm(t("clearKeyConfirm"))) return;
|
|
3465
|
+
void actions.patch(account.id, { clearApiKey: true }, t("clearedKeyToast"));
|
|
3466
|
+
},
|
|
3467
|
+
},
|
|
3468
|
+
t("clearKeyBtn")
|
|
3469
|
+
)
|
|
3470
|
+
: null
|
|
3471
|
+
)
|
|
3472
|
+
),
|
|
3473
|
+
h(
|
|
3474
|
+
FieldRow,
|
|
3475
|
+
{ t, label: t("protocolLabel"), help: t("protocolHint"), htmlFor: `dcpa-proto-${account.id}` },
|
|
3476
|
+
h(
|
|
3477
|
+
"select",
|
|
3478
|
+
{
|
|
3479
|
+
id: `dcpa-proto-${account.id}`,
|
|
3480
|
+
className: "dcpa-q-input",
|
|
3481
|
+
value: protocol,
|
|
3482
|
+
onChange: (e) => setProtocol(e.target.value),
|
|
3483
|
+
},
|
|
3484
|
+
h("option", { value: "auto" }, t("protocolAuto")),
|
|
3485
|
+
PROTOCOLS.map((p) => h("option", { key: p, value: p }, `${t("protocolFixedPrefix")}${protocolName(p)}`))
|
|
3486
|
+
)
|
|
3487
|
+
),
|
|
3488
|
+
h(
|
|
3489
|
+
"details",
|
|
3490
|
+
{ className: "dcpa-adv" },
|
|
3491
|
+
h("summary", null, t("sectionAdvanced")),
|
|
3492
|
+
h(
|
|
3493
|
+
FieldRow,
|
|
3494
|
+
{ t, label: t("providerIdLabel"), help: t("providerIdHint"), htmlFor: `dcpa-route-${account.id}` },
|
|
3495
|
+
h("input", { id: `dcpa-route-${account.id}`, className: "dcpa-q-input", value: provider, onChange: (e) => setProvider(e.target.value) })
|
|
2414
3496
|
),
|
|
2415
|
-
|
|
2416
|
-
|
|
3497
|
+
h(
|
|
3498
|
+
FieldRow,
|
|
3499
|
+
{ t, label: t("apiKeyEnvLabel"), help: t("apiKeyEnvHint"), htmlFor: `dcpa-ref-${account.id}` },
|
|
3500
|
+
h("input", { id: `dcpa-ref-${account.id}`, className: "dcpa-q-input", value: apiKeyEnv, onChange: (e) => setApiKeyEnv(e.target.value) })
|
|
3501
|
+
),
|
|
3502
|
+
h(
|
|
3503
|
+
FieldRow,
|
|
3504
|
+
{ t, label: t("includePrefixLabel"), help: t("includePrefixHint"), htmlFor: `dcpa-inc-${account.id}` },
|
|
3505
|
+
h("input", {
|
|
3506
|
+
id: `dcpa-inc-${account.id}`,
|
|
3507
|
+
className: "dcpa-q-input",
|
|
3508
|
+
value: includePrefix,
|
|
3509
|
+
placeholder: t("includePrefixPlaceholder"),
|
|
3510
|
+
onChange: (e) => setIncludePrefix(e.target.value),
|
|
3511
|
+
})
|
|
3512
|
+
),
|
|
3513
|
+
h(
|
|
3514
|
+
FieldRow,
|
|
3515
|
+
{ t, label: t("excludePrefixLabel"), help: t("excludePrefixHint"), htmlFor: `dcpa-exc-${account.id}` },
|
|
3516
|
+
h("input", {
|
|
3517
|
+
id: `dcpa-exc-${account.id}`,
|
|
3518
|
+
className: "dcpa-q-input",
|
|
3519
|
+
value: excludePrefix,
|
|
3520
|
+
placeholder: t("excludePrefixPlaceholder"),
|
|
3521
|
+
onChange: (e) => setExcludePrefix(e.target.value),
|
|
3522
|
+
})
|
|
3523
|
+
)
|
|
2417
3524
|
),
|
|
2418
|
-
|
|
2419
|
-
// 2. Interactive Model Selection Filter Card
|
|
2420
3525
|
h(
|
|
2421
|
-
|
|
2422
|
-
{
|
|
3526
|
+
FieldRow,
|
|
3527
|
+
{ t, label: t("refreshIntervalLabel"), htmlFor: `dcpa-interval-${account.id}` },
|
|
2423
3528
|
h(
|
|
2424
3529
|
"div",
|
|
2425
|
-
{ className: "dcpa-
|
|
2426
|
-
h(
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
{ className: "dcpa-actions" },
|
|
2439
|
-
selectionDirty
|
|
2440
|
-
? h("span", { className: "dcpa-badge dcpa-badge-warn" }, t("unsavedChanges"))
|
|
2441
|
-
: null,
|
|
2442
|
-
h(
|
|
2443
|
-
"button",
|
|
2444
|
-
{
|
|
2445
|
-
type: "button",
|
|
2446
|
-
className: "dcpa-btn dcpa-btn-primary dcpa-btn-sm",
|
|
2447
|
-
disabled: saving || syncing,
|
|
2448
|
-
onClick: () => onSave(false),
|
|
2449
|
-
},
|
|
2450
|
-
saving ? t("savingBtn") : t("saveSelectionBtn")
|
|
2451
|
-
)
|
|
2452
|
-
)
|
|
2453
|
-
),
|
|
3530
|
+
{ className: "dcpa-inline dcpa-inline-narrow" },
|
|
3531
|
+
h("input", {
|
|
3532
|
+
id: `dcpa-interval-${account.id}`,
|
|
3533
|
+
type: "number",
|
|
3534
|
+
min: "15",
|
|
3535
|
+
className: "dcpa-q-input",
|
|
3536
|
+
value: refreshIntervalSeconds,
|
|
3537
|
+
onChange: (e) => setRefreshIntervalSeconds(e.target.value),
|
|
3538
|
+
}),
|
|
3539
|
+
h("span", { className: "dcpa-q-sub" }, t("secondsUnit"))
|
|
3540
|
+
)
|
|
3541
|
+
)
|
|
3542
|
+
);
|
|
2454
3543
|
|
|
3544
|
+
const modelsTab = h(
|
|
3545
|
+
"div",
|
|
3546
|
+
{ className: "dcpa-tabbody dcpa-models" },
|
|
3547
|
+
h("p", { className: "dcpa-q-sub" }, t("modelsEmptyRule")),
|
|
2455
3548
|
// Search + Segmented Filter + Batch Actions
|
|
2456
3549
|
h(
|
|
2457
3550
|
"div",
|
|
@@ -2517,11 +3610,11 @@ window.__ModuleLoader__.load({
|
|
|
2517
3610
|
type: "button",
|
|
2518
3611
|
className: "dcpa-btn dcpa-btn-sm",
|
|
2519
3612
|
disabled: recommended.length === 0,
|
|
2520
|
-
title: t("selectRecommendedHint"),
|
|
2521
3613
|
onClick: resetRecommended,
|
|
2522
3614
|
},
|
|
2523
3615
|
`${t("selectRecommendedBtn")} (${recommended.length})`
|
|
2524
|
-
)
|
|
3616
|
+
),
|
|
3617
|
+
h(InfoTip, { t, text: t("selectRecommendedHint") })
|
|
2525
3618
|
)
|
|
2526
3619
|
),
|
|
2527
3620
|
|
|
@@ -2779,7 +3872,7 @@ window.__ModuleLoader__.load({
|
|
|
2779
3872
|
`${t("modelProtocolHint")}${protocolName(m.nativeProtocol ?? "openai-completions")}`
|
|
2780
3873
|
)
|
|
2781
3874
|
),
|
|
2782
|
-
h("
|
|
3875
|
+
h("div", { className: "dcpa-q-sub" }, t("paramsHintShort"), h(InfoTip, { t, text: t("paramsHint") })),
|
|
2783
3876
|
hasOverride
|
|
2784
3877
|
? h(
|
|
2785
3878
|
"div",
|
|
@@ -2800,22 +3893,30 @@ window.__ModuleLoader__.load({
|
|
|
2800
3893
|
);
|
|
2801
3894
|
})
|
|
2802
3895
|
)
|
|
2803
|
-
|
|
3896
|
+
);
|
|
2804
3897
|
|
|
2805
|
-
|
|
3898
|
+
return h(
|
|
3899
|
+
"div",
|
|
3900
|
+
{ className: "dcpa-account" },
|
|
2806
3901
|
h(
|
|
2807
3902
|
"div",
|
|
2808
|
-
{ className: "dcpa-
|
|
2809
|
-
h("h4", { className: "dcpa-section-title" }, t("sectionConnection")),
|
|
3903
|
+
{ className: "dcpa-acct-head" },
|
|
2810
3904
|
h(
|
|
2811
3905
|
"div",
|
|
2812
|
-
{ className: "dcpa-
|
|
3906
|
+
{ className: "dcpa-q-row-main" },
|
|
3907
|
+
h("h3", { className: "dcpa-q-h" }, account.config?.displayName || account.id),
|
|
2813
3908
|
h(
|
|
2814
3909
|
"div",
|
|
2815
|
-
|
|
2816
|
-
h("
|
|
2817
|
-
|
|
2818
|
-
)
|
|
3910
|
+
{ className: "dcpa-q-sub dcpa-status-line" },
|
|
3911
|
+
h("span", { className: stateDotClass(enabled ? st.state : "idle") }),
|
|
3912
|
+
statusParts.join(" · ")
|
|
3913
|
+
)
|
|
3914
|
+
),
|
|
3915
|
+
h(
|
|
3916
|
+
"div",
|
|
3917
|
+
{ className: "dcpa-q-actions" },
|
|
3918
|
+
h("span", { className: "dcpa-q-sub" }, t("enableShort")),
|
|
3919
|
+
h(InfoTip, { t, text: t("enabledHint") }),
|
|
2819
3920
|
h(
|
|
2820
3921
|
"button",
|
|
2821
3922
|
{
|
|
@@ -2824,234 +3925,1189 @@ window.__ModuleLoader__.load({
|
|
|
2824
3925
|
className: "dcpa-switch",
|
|
2825
3926
|
"aria-checked": enabled,
|
|
2826
3927
|
"aria-label": t("enabledLabel"),
|
|
2827
|
-
|
|
3928
|
+
disabled: busy !== null,
|
|
3929
|
+
onClick: () => void actions.patch(account.id, { enabled: !enabled }, enabled ? t("disabledToast") : t("enabledToast")),
|
|
2828
3930
|
},
|
|
2829
3931
|
h("span", { className: "dcpa-switch-thumb" })
|
|
2830
|
-
)
|
|
2831
|
-
),
|
|
2832
|
-
h(
|
|
2833
|
-
"div",
|
|
2834
|
-
{ className: "dcpa-field" },
|
|
2835
|
-
h("label", { className: "dcpa-label" }, t("baseUrlLabel")),
|
|
2836
|
-
h("input", {
|
|
2837
|
-
type: "text",
|
|
2838
|
-
className: "dcpa-input",
|
|
2839
|
-
value: baseURL,
|
|
2840
|
-
placeholder: effectiveUrl || t("baseUrlPlaceholder"),
|
|
2841
|
-
onChange: (e) => setBaseURL(e.target.value),
|
|
2842
|
-
}),
|
|
2843
|
-
h("p", { className: "dcpa-hint" }, t("baseUrlHint"))
|
|
2844
|
-
),
|
|
2845
|
-
h(
|
|
2846
|
-
"div",
|
|
2847
|
-
{ className: "dcpa-field" },
|
|
2848
|
-
h(
|
|
2849
|
-
"div",
|
|
2850
|
-
{ className: "dcpa-label-row" },
|
|
2851
|
-
h("label", { className: "dcpa-label" }, `${t("apiKeyLabel")} (${cred.ref || apiKeyEnv})`),
|
|
2852
|
-
h(
|
|
2853
|
-
"span",
|
|
2854
|
-
{ className: cred.configured ? "dcpa-badge dcpa-badge-ok" : "dcpa-badge" },
|
|
2855
|
-
cred.configured ? t("apiKeyConfiguredBadge") : t("apiKeyUnsetBadge")
|
|
2856
|
-
)
|
|
2857
3932
|
),
|
|
2858
|
-
h("input", {
|
|
2859
|
-
type: "password",
|
|
2860
|
-
className: "dcpa-input",
|
|
2861
|
-
autoComplete: "new-password",
|
|
2862
|
-
value: apiKeyDraft,
|
|
2863
|
-
placeholder: cred.configured ? t("apiKeySetPlaceholder") : t("apiKeyUnsetPlaceholder"),
|
|
2864
|
-
onChange: (e) => setApiKeyDraft(e.target.value),
|
|
2865
|
-
}),
|
|
2866
|
-
h("p", { className: "dcpa-hint" }, t("apiKeyHint"))
|
|
2867
|
-
),
|
|
2868
|
-
h(
|
|
2869
|
-
"div",
|
|
2870
|
-
{ className: "dcpa-grid" },
|
|
2871
3933
|
h(
|
|
2872
|
-
"
|
|
2873
|
-
{ className: "dcpa-
|
|
2874
|
-
h("label", { className: "dcpa-label" }, t("protocolLabel")),
|
|
3934
|
+
"span",
|
|
3935
|
+
{ className: "dcpa-menu", ref: menuRef },
|
|
2875
3936
|
h(
|
|
2876
|
-
"
|
|
3937
|
+
"button",
|
|
2877
3938
|
{
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
3939
|
+
type: "button",
|
|
3940
|
+
className: "dcpa-q-btn dcpa-menu-btn",
|
|
3941
|
+
"aria-label": t("moreActions"),
|
|
3942
|
+
"aria-haspopup": "menu",
|
|
3943
|
+
"aria-expanded": menuOpen,
|
|
3944
|
+
onClick: () => setMenuOpen((v) => !v),
|
|
2881
3945
|
},
|
|
2882
|
-
|
|
2883
|
-
PROTOCOLS.map((p) =>
|
|
2884
|
-
h("option", { key: p, value: p }, `${t("protocolFixedPrefix")}${protocolName(p)}`)
|
|
2885
|
-
)
|
|
2886
|
-
),
|
|
2887
|
-
h("p", { className: "dcpa-hint" }, t("protocolHint"))
|
|
2888
|
-
),
|
|
2889
|
-
h(
|
|
2890
|
-
"div",
|
|
2891
|
-
{ className: "dcpa-field" },
|
|
2892
|
-
h("label", { className: "dcpa-label" }, t("displayNameLabel")),
|
|
2893
|
-
h("input", {
|
|
2894
|
-
type: "text",
|
|
2895
|
-
className: "dcpa-input",
|
|
2896
|
-
value: displayName,
|
|
2897
|
-
onChange: (e) => setDisplayName(e.target.value),
|
|
2898
|
-
}),
|
|
2899
|
-
h("p", { className: "dcpa-hint" }, t("displayNameHint"))
|
|
2900
|
-
)
|
|
2901
|
-
),
|
|
2902
|
-
h(
|
|
2903
|
-
"details",
|
|
2904
|
-
null,
|
|
2905
|
-
h(
|
|
2906
|
-
"summary",
|
|
2907
|
-
{ style: { cursor: "pointer", fontSize: "12px", fontWeight: 500 } },
|
|
2908
|
-
t("sectionAdvanced")
|
|
2909
|
-
),
|
|
2910
|
-
h(
|
|
2911
|
-
"div",
|
|
2912
|
-
{ style: { display: "flex", flexDirection: "column", gap: "10px", marginTop: "10px" } },
|
|
2913
|
-
h(
|
|
2914
|
-
"div",
|
|
2915
|
-
{ className: "dcpa-grid" },
|
|
2916
|
-
h(
|
|
2917
|
-
"div",
|
|
2918
|
-
{ className: "dcpa-field" },
|
|
2919
|
-
h("label", { className: "dcpa-label" }, t("includePrefixLabel")),
|
|
2920
|
-
h("input", {
|
|
2921
|
-
type: "text",
|
|
2922
|
-
className: "dcpa-input",
|
|
2923
|
-
value: includePrefix,
|
|
2924
|
-
placeholder: t("includePrefixPlaceholder"),
|
|
2925
|
-
onChange: (e) => setIncludePrefix(e.target.value),
|
|
2926
|
-
}),
|
|
2927
|
-
h("p", { className: "dcpa-hint" }, t("includePrefixHint"))
|
|
2928
|
-
),
|
|
2929
|
-
h(
|
|
2930
|
-
"div",
|
|
2931
|
-
{ className: "dcpa-field" },
|
|
2932
|
-
h("label", { className: "dcpa-label" }, t("excludePrefixLabel")),
|
|
2933
|
-
h("input", {
|
|
2934
|
-
type: "text",
|
|
2935
|
-
className: "dcpa-input",
|
|
2936
|
-
value: excludePrefix,
|
|
2937
|
-
placeholder: t("excludePrefixPlaceholder"),
|
|
2938
|
-
onChange: (e) => setExcludePrefix(e.target.value),
|
|
2939
|
-
}),
|
|
2940
|
-
h("p", { className: "dcpa-hint" }, t("excludePrefixHint"))
|
|
2941
|
-
)
|
|
2942
|
-
),
|
|
2943
|
-
h(
|
|
2944
|
-
"div",
|
|
2945
|
-
{ className: "dcpa-grid" },
|
|
2946
|
-
h(
|
|
2947
|
-
"div",
|
|
2948
|
-
{ className: "dcpa-field" },
|
|
2949
|
-
h("label", { className: "dcpa-label" }, t("providerIdLabel")),
|
|
2950
|
-
h("input", {
|
|
2951
|
-
type: "text",
|
|
2952
|
-
className: "dcpa-input",
|
|
2953
|
-
value: provider,
|
|
2954
|
-
onChange: (e) => setProvider(e.target.value),
|
|
2955
|
-
}),
|
|
2956
|
-
h("p", { className: "dcpa-hint" }, t("providerIdHint"))
|
|
2957
|
-
),
|
|
2958
|
-
h(
|
|
2959
|
-
"div",
|
|
2960
|
-
{ className: "dcpa-field" },
|
|
2961
|
-
h("label", { className: "dcpa-label" }, t("apiKeyEnvLabel")),
|
|
2962
|
-
h("input", {
|
|
2963
|
-
type: "text",
|
|
2964
|
-
className: "dcpa-input",
|
|
2965
|
-
value: apiKeyEnv,
|
|
2966
|
-
onChange: (e) => setApiKeyEnv(e.target.value),
|
|
2967
|
-
}),
|
|
2968
|
-
h("p", { className: "dcpa-hint" }, t("apiKeyEnvHint"))
|
|
2969
|
-
)
|
|
3946
|
+
"⋯"
|
|
2970
3947
|
),
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
3948
|
+
menuOpen
|
|
3949
|
+
? h(
|
|
3950
|
+
"div",
|
|
3951
|
+
{ className: "dcpa-menu-pop", role: "menu" },
|
|
3952
|
+
h(
|
|
3953
|
+
"button",
|
|
3954
|
+
{
|
|
3955
|
+
type: "button",
|
|
3956
|
+
role: "menuitem",
|
|
3957
|
+
className: "dcpa-menu-item",
|
|
3958
|
+
disabled: busy !== null,
|
|
3959
|
+
onClick: () => {
|
|
3960
|
+
setMenuOpen(false);
|
|
3961
|
+
void actions.refresh(account.id);
|
|
3962
|
+
},
|
|
3963
|
+
},
|
|
3964
|
+
syncing ? t("refreshingBtn") : t("refreshBtn")
|
|
3965
|
+
),
|
|
3966
|
+
h(
|
|
3967
|
+
"button",
|
|
3968
|
+
{
|
|
3969
|
+
type: "button",
|
|
3970
|
+
role: "menuitem",
|
|
3971
|
+
className: "dcpa-menu-item",
|
|
3972
|
+
"data-variant": "danger",
|
|
3973
|
+
disabled: busy !== null,
|
|
3974
|
+
onClick: () => {
|
|
3975
|
+
setMenuOpen(false);
|
|
3976
|
+
actions.remove(account);
|
|
3977
|
+
},
|
|
3978
|
+
},
|
|
3979
|
+
t("removeAccountBtn")
|
|
3980
|
+
)
|
|
3981
|
+
)
|
|
3982
|
+
: null
|
|
2986
3983
|
)
|
|
2987
|
-
)
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
3984
|
+
)
|
|
3985
|
+
),
|
|
3986
|
+
!enabled ? h("p", { className: "dcpa-q-msg" }, t("disabledHint")) : null,
|
|
3987
|
+
st.error ? h("p", { className: "dcpa-q-msg", "data-tone": "error", role: "alert" }, st.error) : null,
|
|
3988
|
+
h(
|
|
3989
|
+
"div",
|
|
3990
|
+
{ className: "dcpa-q-seg dcpa-acct-tabs", role: "tablist", "aria-label": t("tabsLabel") },
|
|
3991
|
+
tabs.map(([id, label]) =>
|
|
2991
3992
|
h(
|
|
2992
3993
|
"button",
|
|
2993
3994
|
{
|
|
3995
|
+
key: id,
|
|
3996
|
+
id: `dcpa-tab-${account.id}-${id}`,
|
|
2994
3997
|
type: "button",
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
3998
|
+
role: "tab",
|
|
3999
|
+
className: "dcpa-q-seg-tab",
|
|
4000
|
+
"aria-selected": tab === id,
|
|
4001
|
+
"aria-controls": `dcpa-tabpanel-${account.id}-${id}`,
|
|
4002
|
+
onClick: () => setTab(id),
|
|
2998
4003
|
},
|
|
2999
|
-
|
|
4004
|
+
label
|
|
4005
|
+
)
|
|
4006
|
+
)
|
|
4007
|
+
),
|
|
4008
|
+
h(
|
|
4009
|
+
"div",
|
|
4010
|
+
{ id: `dcpa-tabpanel-${account.id}-${tab}`, role: "tabpanel", "aria-labelledby": `dcpa-tab-${account.id}-${tab}` },
|
|
4011
|
+
tab === "connection" ? connectionTab : tab === "models" ? modelsTab : h(QuotaPanel, { t, store: sharedQuota, accountId: account.id })
|
|
4012
|
+
),
|
|
4013
|
+
dirty || notice
|
|
4014
|
+
? h(
|
|
4015
|
+
"div",
|
|
4016
|
+
{ className: "dcpa-savebar", role: "region", "aria-label": t("saveBarLabel") },
|
|
4017
|
+
h(
|
|
4018
|
+
"span",
|
|
4019
|
+
{ className: "dcpa-savebar-text", "data-tone": notice && !dirty ? notice.tone : undefined, role: notice ? "status" : undefined },
|
|
4020
|
+
dirty ? (notice?.tone === "error" ? notice.text : t("unsavedChanges")) : notice?.text
|
|
4021
|
+
),
|
|
4022
|
+
dirty
|
|
4023
|
+
? h(
|
|
4024
|
+
"div",
|
|
4025
|
+
{ className: "dcpa-q-actions" },
|
|
4026
|
+
h(
|
|
4027
|
+
"button",
|
|
4028
|
+
{
|
|
4029
|
+
type: "button",
|
|
4030
|
+
className: "dcpa-q-btn",
|
|
4031
|
+
disabled: busy !== null,
|
|
4032
|
+
onClick: discard,
|
|
4033
|
+
},
|
|
4034
|
+
t("discardBtn")
|
|
4035
|
+
),
|
|
4036
|
+
h(
|
|
4037
|
+
"button",
|
|
4038
|
+
{
|
|
4039
|
+
type: "button",
|
|
4040
|
+
className: "dcpa-q-btn",
|
|
4041
|
+
"data-variant": "primary",
|
|
4042
|
+
disabled: busy !== null,
|
|
4043
|
+
onClick: () => void actions.save(account.id, payload()),
|
|
4044
|
+
},
|
|
4045
|
+
saving ? t("savingBtn") : t("saveBtn")
|
|
4046
|
+
)
|
|
4047
|
+
)
|
|
4048
|
+
: null
|
|
4049
|
+
)
|
|
4050
|
+
: null
|
|
4051
|
+
);
|
|
4052
|
+
}
|
|
4053
|
+
|
|
4054
|
+
// #region Quota (额度): shared store, model matching, tab page, composer badge
|
|
4055
|
+
|
|
4056
|
+
const QUOTA_POLL_MS = 60 * 1000;
|
|
4057
|
+
// While the host is still collecting a refresh, look again soon.
|
|
4058
|
+
const QUOTA_BUSY_POLL_MS = 4 * 1000;
|
|
4059
|
+
const PROVIDER_NAMES = {
|
|
4060
|
+
claude: "Claude",
|
|
4061
|
+
anthropic: "Claude",
|
|
4062
|
+
codex: "Codex",
|
|
4063
|
+
openai: "OpenAI",
|
|
4064
|
+
devin: "Devin",
|
|
4065
|
+
antigravity: "Antigravity",
|
|
4066
|
+
"gemini-cli": "Gemini CLI",
|
|
4067
|
+
gemini: "Gemini",
|
|
4068
|
+
vertex: "Vertex",
|
|
4069
|
+
aistudio: "AI Studio",
|
|
4070
|
+
kimi: "Kimi",
|
|
4071
|
+
meta: "Meta",
|
|
4072
|
+
xai: "xAI / Grok",
|
|
4073
|
+
qwen: "Qwen",
|
|
4074
|
+
iflow: "iFlow",
|
|
4075
|
+
glm: "GLM",
|
|
4076
|
+
deepseek: "DeepSeek",
|
|
4077
|
+
copilot: "GitHub Copilot",
|
|
4078
|
+
};
|
|
4079
|
+
const WINDOW_KEYS = { "5h": "qWin5h", "7d": "qWin7d", "30d": "qWin30d", "1d": "qWin1d" };
|
|
4080
|
+
const QUOTA_ERROR_KEYS = {
|
|
4081
|
+
unauthorized: "qErrUnauthorized",
|
|
4082
|
+
forbidden: "qErrForbidden",
|
|
4083
|
+
network: "qErrNetwork",
|
|
4084
|
+
timeout: "qErrTimeout",
|
|
4085
|
+
http: "qErrHttp",
|
|
4086
|
+
"invalid-response": "qErrInvalid",
|
|
4087
|
+
"no-endpoint": "qErrNoEndpoint",
|
|
4088
|
+
};
|
|
4089
|
+
const STATUS_KEYS = {
|
|
4090
|
+
active: "qStatusActive",
|
|
4091
|
+
disabled: "qStatusDisabled",
|
|
4092
|
+
unavailable: "qStatusUnavailable",
|
|
4093
|
+
error: "qStatusError",
|
|
4094
|
+
};
|
|
4095
|
+
const MATCH_KEYS = { "model-list": "qMatchModelList", prefix: "qMatchPrefix", family: "qMatchFamily" };
|
|
4096
|
+
|
|
4097
|
+
function fmt(t, key, vars = {}) {
|
|
4098
|
+
let out = String(t(key));
|
|
4099
|
+
for (const [name, value] of Object.entries(vars)) out = out.split(`{${name}}`).join(String(value));
|
|
4100
|
+
return out;
|
|
4101
|
+
}
|
|
4102
|
+
|
|
4103
|
+
function providerName(t, provider) {
|
|
4104
|
+
const key = String(provider ?? "").toLowerCase();
|
|
4105
|
+
return PROVIDER_NAMES[key] ?? (key === "" ? t("qProviderOther") : key);
|
|
4106
|
+
}
|
|
4107
|
+
|
|
4108
|
+
function quotaErrorText(t, account) {
|
|
4109
|
+
const key = QUOTA_ERROR_KEYS[account?.errorCode];
|
|
4110
|
+
if (key === undefined) return null;
|
|
4111
|
+
return fmt(t, key, { status: account.errorStatus ?? "?" });
|
|
4112
|
+
}
|
|
4113
|
+
|
|
4114
|
+
function windowLabel(t, w) {
|
|
4115
|
+
const base = WINDOW_KEYS[w.kind] !== undefined ? t(WINDOW_KEYS[w.kind]) : w.label || t("qWinOther");
|
|
4116
|
+
return w.group ? `${w.group} · ${base}` : base;
|
|
4117
|
+
}
|
|
4118
|
+
|
|
4119
|
+
function durationText(t, ms) {
|
|
4120
|
+
const total = Math.max(1, Math.ceil(ms / 60000));
|
|
4121
|
+
const d = Math.floor(total / 1440);
|
|
4122
|
+
const hrs = Math.floor((total % 1440) / 60);
|
|
4123
|
+
const m = total % 60;
|
|
4124
|
+
if (d > 0) return fmt(t, "qDurDH", { d, h: hrs });
|
|
4125
|
+
if (hrs > 0) return fmt(t, "qDurHM", { h: hrs, m });
|
|
4126
|
+
return fmt(t, "qDurM", { m });
|
|
4127
|
+
}
|
|
4128
|
+
|
|
4129
|
+
function resetText(t, iso, now) {
|
|
4130
|
+
const at = Date.parse(iso ?? "");
|
|
4131
|
+
if (!Number.isFinite(at)) return "";
|
|
4132
|
+
return at <= now ? t("qResetSoon") : fmt(t, "qResetIn", { time: durationText(t, at - now) });
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
function agoText(t, iso, now) {
|
|
4136
|
+
const at = Date.parse(iso ?? "");
|
|
4137
|
+
if (!Number.isFinite(at)) return "";
|
|
4138
|
+
const s = Math.max(0, Math.floor((now - at) / 1000));
|
|
4139
|
+
if (s < 10) return t("qAgoJustNow");
|
|
4140
|
+
if (s < 60) return fmt(t, "qAgoSeconds", { n: s });
|
|
4141
|
+
if (s < 3600) return fmt(t, "qAgoMinutes", { n: Math.floor(s / 60) });
|
|
4142
|
+
if (s < 86400) return fmt(t, "qAgoHours", { n: Math.floor(s / 3600) });
|
|
4143
|
+
return fmt(t, "qAgoDays", { n: Math.floor(s / 86400) });
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4146
|
+
function quotaLevel(pct) {
|
|
4147
|
+
if (typeof pct !== "number" || !Number.isFinite(pct)) return "unknown";
|
|
4148
|
+
if (pct <= 0) return "empty";
|
|
4149
|
+
if (pct < 20) return "low";
|
|
4150
|
+
if (pct < 50) return "mid";
|
|
4151
|
+
return "ok";
|
|
4152
|
+
}
|
|
4153
|
+
|
|
4154
|
+
function pctText(pct) {
|
|
4155
|
+
return typeof pct === "number" && Number.isFinite(pct) ? `${Math.round(pct)}%` : "—";
|
|
4156
|
+
}
|
|
4157
|
+
|
|
4158
|
+
function bareModel(id) {
|
|
4159
|
+
const lower = String(id ?? "").toLowerCase();
|
|
4160
|
+
const slash = lower.indexOf("/");
|
|
4161
|
+
return slash >= 0 ? lower.slice(slash + 1) : lower;
|
|
4162
|
+
}
|
|
4163
|
+
|
|
4164
|
+
/** Whether a quota window applies to `modelId` (untagged windows apply to every model). */
|
|
4165
|
+
function windowAppliesTo(w, modelId) {
|
|
4166
|
+
if (!modelId) return true;
|
|
4167
|
+
const full = String(modelId).toLowerCase();
|
|
4168
|
+
const bare = bareModel(modelId);
|
|
4169
|
+
if (Array.isArray(w.modelIds) && w.modelIds.length > 0) {
|
|
4170
|
+
return w.modelIds.some((m) => {
|
|
4171
|
+
const x = String(m).toLowerCase();
|
|
4172
|
+
return x === full || x === bare || bareModel(x) === bare;
|
|
4173
|
+
});
|
|
4174
|
+
}
|
|
4175
|
+
if (Array.isArray(w.modelPrefixes) && w.modelPrefixes.length > 0) {
|
|
4176
|
+
return w.modelPrefixes.some((p) => bare.startsWith(String(p).toLowerCase()));
|
|
4177
|
+
}
|
|
4178
|
+
return true;
|
|
4179
|
+
}
|
|
4180
|
+
|
|
4181
|
+
function credentialWindows(cred, modelId) {
|
|
4182
|
+
const windows = Array.isArray(cred?.windows) ? cred.windows : [];
|
|
4183
|
+
return modelId ? windows.filter((w) => windowAppliesTo(w, modelId)) : windows;
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4186
|
+
/** The tightest remaining percentage of the windows that apply, or null. */
|
|
4187
|
+
function credentialHeadroom(cred, modelId) {
|
|
4188
|
+
const values = credentialWindows(cred, modelId)
|
|
4189
|
+
.map((w) => w.remainingPercent)
|
|
4190
|
+
.filter((v) => typeof v === "number" && Number.isFinite(v));
|
|
4191
|
+
return values.length > 0 ? Math.min(...values) : null;
|
|
4192
|
+
}
|
|
4193
|
+
|
|
4194
|
+
function quotaAccountForRoute(data, route) {
|
|
4195
|
+
const accounts = Array.isArray(data?.accounts) ? data.accounts : [];
|
|
4196
|
+
return accounts.find((a) => Array.isArray(a.routes) && a.routes.includes(route)) ?? null;
|
|
4197
|
+
}
|
|
4198
|
+
|
|
4199
|
+
function byHeadroomDesc(a, b) {
|
|
4200
|
+
const ua = a.cred.status === "active" ? 0 : 1;
|
|
4201
|
+
const ub = b.cred.status === "active" ? 0 : 1;
|
|
4202
|
+
if (ua !== ub) return ua - ub;
|
|
4203
|
+
const ha = a.headroom ?? -1;
|
|
4204
|
+
const hb = b.headroom ?? -1;
|
|
4205
|
+
return hb - ha;
|
|
4206
|
+
}
|
|
4207
|
+
|
|
4208
|
+
/**
|
|
4209
|
+
* Quota for one picker selection: the account owning the route, then
|
|
4210
|
+
* exactly the credentials the host matched for this model id on that
|
|
4211
|
+
* server (modelIndex), never credentials of another account.
|
|
4212
|
+
*/
|
|
4213
|
+
function summarizeModelQuota(data, route, modelId) {
|
|
4214
|
+
const account = quotaAccountForRoute(data, route);
|
|
4215
|
+
if (account === null) return null;
|
|
4216
|
+
const credentials = Array.isArray(account.credentials) ? account.credentials : [];
|
|
4217
|
+
const byId = new Map(credentials.map((c) => [c.id, c]));
|
|
4218
|
+
const ids = Array.isArray(account.modelIndex?.[modelId]) ? account.modelIndex[modelId] : [];
|
|
4219
|
+
const entries = ids
|
|
4220
|
+
.map((id) => byId.get(id))
|
|
4221
|
+
.filter(Boolean)
|
|
4222
|
+
.map((cred) => ({ cred, headroom: credentialHeadroom(cred, modelId), windows: credentialWindows(cred, modelId) }))
|
|
4223
|
+
.sort(byHeadroomDesc);
|
|
4224
|
+
const usable = entries.filter((e) => e.cred.status === "active");
|
|
4225
|
+
const known = usable.map((e) => e.headroom).filter((v) => v !== null);
|
|
4226
|
+
return {
|
|
4227
|
+
account,
|
|
4228
|
+
entries,
|
|
4229
|
+
usableCount: usable.length,
|
|
4230
|
+
best: known.length > 0 ? Math.max(...known) : null,
|
|
4231
|
+
};
|
|
4232
|
+
}
|
|
4233
|
+
|
|
4234
|
+
/** Totals for the overview tiles. */
|
|
4235
|
+
function quotaStats(credentials) {
|
|
4236
|
+
const out = { total: 0, healthy: 0, low: 0, empty: 0, issue: 0, unknown: 0 };
|
|
4237
|
+
for (const cred of credentials) {
|
|
4238
|
+
out.total++;
|
|
4239
|
+
if (cred.status !== "active" || cred.quotaState === "error") {
|
|
4240
|
+
out.issue++;
|
|
4241
|
+
continue;
|
|
4242
|
+
}
|
|
4243
|
+
const level = quotaLevel(credentialHeadroom(cred));
|
|
4244
|
+
if (level === "empty") out.empty++;
|
|
4245
|
+
else if (level === "low") out.low++;
|
|
4246
|
+
else if (level === "ok" || level === "mid") out.healthy++;
|
|
4247
|
+
else out.unknown++;
|
|
4248
|
+
}
|
|
4249
|
+
return out;
|
|
4250
|
+
}
|
|
4251
|
+
|
|
4252
|
+
/**
|
|
4253
|
+
* One page-wide quota client: GET the host snapshot (cached by the host,
|
|
4254
|
+
* which refreshes stale accounts in the background) while anything is
|
|
4255
|
+
* subscribed; POST refresh / key changes. Disposed with the activation.
|
|
4256
|
+
*/
|
|
4257
|
+
function createQuotaStore(io = {}) {
|
|
4258
|
+
const fetchImpl = io.fetch ?? ((...args) => fetch(...args));
|
|
4259
|
+
const setT = io.setTimeout ?? ((fn, ms) => setTimeout(fn, ms));
|
|
4260
|
+
const clearT = io.clearTimeout ?? ((id) => clearTimeout(id));
|
|
4261
|
+
const listeners = new Set();
|
|
4262
|
+
let state = { data: null, loading: false, error: null, busy: null, busyAccount: null, loadedAt: 0 };
|
|
4263
|
+
let timer = null;
|
|
4264
|
+
let disposed = false;
|
|
4265
|
+
let seq = 0;
|
|
4266
|
+
const inflight = new Set();
|
|
4267
|
+
|
|
4268
|
+
const emit = (patch) => {
|
|
4269
|
+
if (disposed) return;
|
|
4270
|
+
state = { ...state, ...patch };
|
|
4271
|
+
for (const fn of [...listeners]) fn();
|
|
4272
|
+
};
|
|
4273
|
+
|
|
4274
|
+
const request = async (url, init) => {
|
|
4275
|
+
const controller = new AbortController();
|
|
4276
|
+
inflight.add(controller);
|
|
4277
|
+
try {
|
|
4278
|
+
const res = await fetchImpl(url, { cache: "no-store", ...init, signal: controller.signal });
|
|
4279
|
+
let body = null;
|
|
4280
|
+
try {
|
|
4281
|
+
body = await res.json();
|
|
4282
|
+
} catch {}
|
|
4283
|
+
if (!res.ok || body === null || body.ok === false) {
|
|
4284
|
+
throw new Error(body?.error || `HTTP ${res.status}`);
|
|
4285
|
+
}
|
|
4286
|
+
return body;
|
|
4287
|
+
} finally {
|
|
4288
|
+
inflight.delete(controller);
|
|
4289
|
+
}
|
|
4290
|
+
};
|
|
4291
|
+
|
|
4292
|
+
const schedule = () => {
|
|
4293
|
+
if (timer !== null) clearT(timer);
|
|
4294
|
+
timer = null;
|
|
4295
|
+
if (disposed || listeners.size === 0) return;
|
|
4296
|
+
const delay = state.data?.refreshing ? QUOTA_BUSY_POLL_MS : QUOTA_POLL_MS;
|
|
4297
|
+
timer = setT(() => {
|
|
4298
|
+
timer = null;
|
|
4299
|
+
void load().finally(schedule);
|
|
4300
|
+
}, delay);
|
|
4301
|
+
};
|
|
4302
|
+
|
|
4303
|
+
const load = async () => {
|
|
4304
|
+
const mine = ++seq;
|
|
4305
|
+
emit({ loading: true });
|
|
4306
|
+
try {
|
|
4307
|
+
const body = await request(API.quotaStatus);
|
|
4308
|
+
if (mine === seq) emit({ data: body.quota ?? null, error: null, loading: false, loadedAt: Date.now() });
|
|
4309
|
+
} catch (error) {
|
|
4310
|
+
if (disposed) return;
|
|
4311
|
+
if (mine === seq) emit({ error: error instanceof Error ? error.message : String(error), loading: false });
|
|
4312
|
+
}
|
|
4313
|
+
};
|
|
4314
|
+
|
|
4315
|
+
const mutate = async (kind, url, payload) => {
|
|
4316
|
+
const mine = ++seq;
|
|
4317
|
+
emit({ busy: kind, busyAccount: payload?.accountId ?? null, error: null });
|
|
4318
|
+
try {
|
|
4319
|
+
const body = await request(url, {
|
|
4320
|
+
method: "POST",
|
|
4321
|
+
headers: { "content-type": "application/json" },
|
|
4322
|
+
body: JSON.stringify(payload ?? {}),
|
|
4323
|
+
});
|
|
4324
|
+
if (mine === seq) emit({ data: body.quota ?? state.data, loadedAt: Date.now() });
|
|
4325
|
+
emit({ busy: null, busyAccount: null });
|
|
4326
|
+
schedule();
|
|
4327
|
+
return { ok: true };
|
|
4328
|
+
} catch (error) {
|
|
4329
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4330
|
+
emit({ busy: null, busyAccount: null, ...(mine === seq ? { error: message } : {}) });
|
|
4331
|
+
return { ok: false, error: message };
|
|
4332
|
+
}
|
|
4333
|
+
};
|
|
4334
|
+
|
|
4335
|
+
return {
|
|
4336
|
+
getSnapshot: () => state,
|
|
4337
|
+
subscribe(fn) {
|
|
4338
|
+
listeners.add(fn);
|
|
4339
|
+
if (listeners.size === 1) {
|
|
4340
|
+
if (state.data === null || Date.now() - state.loadedAt > QUOTA_POLL_MS) void load().finally(schedule);
|
|
4341
|
+
else schedule();
|
|
4342
|
+
}
|
|
4343
|
+
return () => {
|
|
4344
|
+
listeners.delete(fn);
|
|
4345
|
+
if (listeners.size === 0 && timer !== null) {
|
|
4346
|
+
clearT(timer);
|
|
4347
|
+
timer = null;
|
|
4348
|
+
}
|
|
4349
|
+
};
|
|
4350
|
+
},
|
|
4351
|
+
load,
|
|
4352
|
+
refresh: (accountId) => mutate("refresh", API.quotaRefresh, accountId ? { accountId } : {}),
|
|
4353
|
+
saveKey: (accountId, managementKey) => mutate("save", API.quotaSave, { accountId, managementKey }),
|
|
4354
|
+
clearKey: (accountId) => mutate("clear", API.quotaSave, { accountId, clear: true }),
|
|
4355
|
+
dispose() {
|
|
4356
|
+
disposed = true;
|
|
4357
|
+
if (timer !== null) clearT(timer);
|
|
4358
|
+
timer = null;
|
|
4359
|
+
for (const c of inflight) c.abort();
|
|
4360
|
+
inflight.clear();
|
|
4361
|
+
listeners.clear();
|
|
4362
|
+
},
|
|
4363
|
+
};
|
|
4364
|
+
}
|
|
4365
|
+
|
|
4366
|
+
let sharedQuota = null;
|
|
4367
|
+
|
|
4368
|
+
/** Subscribe a component to the shared quota store (null when inactive). */
|
|
4369
|
+
function useQuota(store) {
|
|
4370
|
+
const [, setTick] = useState(0);
|
|
4371
|
+
useEffect(() => {
|
|
4372
|
+
if (store === null || store === undefined) return undefined;
|
|
4373
|
+
let alive = true;
|
|
4374
|
+
const off = store.subscribe(() => {
|
|
4375
|
+
if (alive) setTick((n) => n + 1);
|
|
4376
|
+
});
|
|
4377
|
+
return () => {
|
|
4378
|
+
alive = false;
|
|
4379
|
+
off();
|
|
4380
|
+
};
|
|
4381
|
+
}, [store]);
|
|
4382
|
+
return store ? store.getSnapshot() : { data: null, loading: false, error: null, busy: null };
|
|
4383
|
+
}
|
|
4384
|
+
|
|
4385
|
+
/**
|
|
4386
|
+
* Which sessions currently have a composer badge mounted. The composer
|
|
4387
|
+
* renders conversation.input.right only once the session is bound (its
|
|
4388
|
+
* input state exists), but the New Session hero shows the model selector
|
|
4389
|
+
* earlier. The hero badge (conversation.hero.options) therefore renders
|
|
4390
|
+
* only while no composer badge is mounted for the same session, so the
|
|
4391
|
+
* user always sees exactly one badge. Plain in-memory counts; no DOM.
|
|
4392
|
+
*/
|
|
4393
|
+
function createBadgePresence() {
|
|
4394
|
+
const counts = new Map();
|
|
4395
|
+
const listeners = new Set();
|
|
4396
|
+
const notify = () => {
|
|
4397
|
+
for (const fn of [...listeners]) fn();
|
|
4398
|
+
};
|
|
4399
|
+
return {
|
|
4400
|
+
add(id) {
|
|
4401
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
4402
|
+
notify();
|
|
4403
|
+
let done = false;
|
|
4404
|
+
return () => {
|
|
4405
|
+
if (done) return;
|
|
4406
|
+
done = true;
|
|
4407
|
+
const n = (counts.get(id) ?? 1) - 1;
|
|
4408
|
+
if (n > 0) counts.set(id, n);
|
|
4409
|
+
else counts.delete(id);
|
|
4410
|
+
notify();
|
|
4411
|
+
};
|
|
4412
|
+
},
|
|
4413
|
+
has: (id) => (counts.get(id) ?? 0) > 0,
|
|
4414
|
+
subscribe(fn) {
|
|
4415
|
+
listeners.add(fn);
|
|
4416
|
+
return () => listeners.delete(fn);
|
|
4417
|
+
},
|
|
4418
|
+
};
|
|
4419
|
+
}
|
|
4420
|
+
const composerBadges = createBadgePresence();
|
|
4421
|
+
|
|
4422
|
+
/**
|
|
4423
|
+
* Composer placement announces itself for its session; hero placement
|
|
4424
|
+
* reports whether a composer badge already covers the session.
|
|
4425
|
+
*/
|
|
4426
|
+
function useBadgePlacement(placement, sessionId, presence = composerBadges) {
|
|
4427
|
+
const [, setTick] = useState(0);
|
|
4428
|
+
useEffect(() => {
|
|
4429
|
+
if (typeof sessionId !== "string" || sessionId === "") return undefined;
|
|
4430
|
+
if (placement === "hero") {
|
|
4431
|
+
let alive = true;
|
|
4432
|
+
const off = presence.subscribe(() => {
|
|
4433
|
+
if (alive) setTick((n) => n + 1);
|
|
4434
|
+
});
|
|
4435
|
+
setTick((n) => n + 1);
|
|
4436
|
+
return () => {
|
|
4437
|
+
alive = false;
|
|
4438
|
+
off();
|
|
4439
|
+
};
|
|
4440
|
+
}
|
|
4441
|
+
return presence.add(sessionId);
|
|
4442
|
+
}, [placement, sessionId, presence]);
|
|
4443
|
+
return placement === "hero" && typeof sessionId === "string" && presence.has(sessionId);
|
|
4444
|
+
}
|
|
4445
|
+
|
|
4446
|
+
/**
|
|
4447
|
+
* The session's model-directory store, resolved at render time. DSH caches
|
|
4448
|
+
* a slot entry's inject() result per session binding, and right after a
|
|
4449
|
+
* page load, host restart or session switch `directoryFor()` can still
|
|
4450
|
+
* throw (no scope/binding yet). Resolving inside inject would cache that
|
|
4451
|
+
* miss for good, so inject hands over a resolver and this hook retries
|
|
4452
|
+
* (1 s for the first 30 s, then every 5 s) until the directory exists.
|
|
4453
|
+
*/
|
|
4454
|
+
function useDirectory(direct, resolve) {
|
|
4455
|
+
const [, setTick] = useState(0);
|
|
4456
|
+
let resolved = direct ?? null;
|
|
4457
|
+
if (resolved === null && typeof resolve === "function") {
|
|
4458
|
+
try {
|
|
4459
|
+
resolved = resolve() ?? null;
|
|
4460
|
+
} catch {
|
|
4461
|
+
resolved = null;
|
|
4462
|
+
}
|
|
4463
|
+
}
|
|
4464
|
+
const missing = resolved === null && typeof resolve === "function";
|
|
4465
|
+
useEffect(() => {
|
|
4466
|
+
if (!missing) return undefined;
|
|
4467
|
+
let alive = true;
|
|
4468
|
+
let id = null;
|
|
4469
|
+
let tries = 0;
|
|
4470
|
+
const tick = () => {
|
|
4471
|
+
id = null;
|
|
4472
|
+
if (!alive) return;
|
|
4473
|
+
tries += 1;
|
|
4474
|
+
setTick((n) => n + 1);
|
|
4475
|
+
id = setTimeout(tick, tries < 30 ? 1000 : 5000);
|
|
4476
|
+
};
|
|
4477
|
+
id = setTimeout(tick, 300);
|
|
4478
|
+
return () => {
|
|
4479
|
+
alive = false;
|
|
4480
|
+
if (id !== null) clearTimeout(id);
|
|
4481
|
+
};
|
|
4482
|
+
}, [missing, resolve]);
|
|
4483
|
+
return resolved;
|
|
4484
|
+
}
|
|
4485
|
+
|
|
4486
|
+
/** Re-render every `ms` so relative times stay current. */
|
|
4487
|
+
function useNow(ms) {
|
|
4488
|
+
const [now, setNow] = useState(() => Date.now());
|
|
4489
|
+
useEffect(() => {
|
|
4490
|
+
const id = setInterval(() => setNow(Date.now()), ms);
|
|
4491
|
+
return () => clearInterval(id);
|
|
4492
|
+
}, [ms]);
|
|
4493
|
+
return now;
|
|
4494
|
+
}
|
|
4495
|
+
|
|
4496
|
+
function QuotaMeter({ t, w, now }) {
|
|
4497
|
+
const pct = typeof w.remainingPercent === "number" ? Math.max(0, Math.min(100, w.remainingPercent)) : null;
|
|
4498
|
+
const level = quotaLevel(pct);
|
|
4499
|
+
const reset = resetText(t, w.resetAt, now);
|
|
4500
|
+
const label = windowLabel(t, w);
|
|
4501
|
+
return h(
|
|
4502
|
+
"div",
|
|
4503
|
+
{ className: "dcpa-q-meter", "data-level": level },
|
|
4504
|
+
h(
|
|
4505
|
+
"div",
|
|
4506
|
+
{ className: "dcpa-q-meter-top" },
|
|
4507
|
+
h("span", { title: label }, label),
|
|
4508
|
+
h("span", { className: "dcpa-q-pct" }, pctText(pct))
|
|
4509
|
+
),
|
|
4510
|
+
h(
|
|
4511
|
+
"div",
|
|
4512
|
+
{
|
|
4513
|
+
className: "dcpa-q-track",
|
|
4514
|
+
role: "meter",
|
|
4515
|
+
"aria-label": label,
|
|
4516
|
+
"aria-valuemin": 0,
|
|
4517
|
+
"aria-valuemax": 100,
|
|
4518
|
+
...(pct === null ? {} : { "aria-valuenow": Math.round(pct) }),
|
|
4519
|
+
},
|
|
4520
|
+
h("span", { className: "dcpa-q-fill", style: { width: `${pct ?? 0}%` } })
|
|
4521
|
+
),
|
|
4522
|
+
reset ? h("span", { className: "dcpa-q-reset" }, reset) : null
|
|
4523
|
+
);
|
|
4524
|
+
}
|
|
4525
|
+
|
|
4526
|
+
function CredentialTags({ t, cred }) {
|
|
4527
|
+
return [
|
|
4528
|
+
cred.plan ? h("span", { key: "plan", className: "dcpa-q-tag", "data-tone": "info" }, cred.plan) : null,
|
|
4529
|
+
cred.prefix ? h("span", { key: "prefix", className: "dcpa-q-tag" }, `${cred.prefix}/`) : null,
|
|
4530
|
+
cred.status && cred.status !== "active"
|
|
4531
|
+
? h("span", { key: "status", className: "dcpa-q-tag", "data-tone": "danger" }, t(STATUS_KEYS[cred.status] ?? "qStatusError"))
|
|
4532
|
+
: null,
|
|
4533
|
+
];
|
|
4534
|
+
}
|
|
4535
|
+
|
|
4536
|
+
function CredentialWindows({ t, cred, windows, now }) {
|
|
4537
|
+
if (windows.length > 0) {
|
|
4538
|
+
return h(
|
|
4539
|
+
"div",
|
|
4540
|
+
{ className: "dcpa-q-meters" },
|
|
4541
|
+
windows.map((w, i) => h(QuotaMeter, { key: w.id ?? i, t, w, now }))
|
|
4542
|
+
);
|
|
4543
|
+
}
|
|
4544
|
+
const lines = [
|
|
4545
|
+
cred.quotaState === "error"
|
|
4546
|
+
? h("p", { key: "q", className: "dcpa-q-note", "data-tone": "error" }, t("qQuotaError"))
|
|
4547
|
+
: h("p", { key: "q", className: "dcpa-q-note" }, t(cred.quotaState === "unsupported" ? "qQuotaUnsupported" : "qQuotaNone")),
|
|
4548
|
+
cred.recent ? h("p", { key: "r", className: "dcpa-q-note" }, fmt(t, "qRecent", cred.recent)) : null,
|
|
4549
|
+
];
|
|
4550
|
+
return h("div", { className: "dcpa-q-meters" }, h("div", null, lines));
|
|
4551
|
+
}
|
|
4552
|
+
|
|
4553
|
+
function CredentialRow({ t, cred, now, pickerIds }) {
|
|
4554
|
+
const [open, setOpen] = useState(false);
|
|
4555
|
+
const models = Array.isArray(cred.models) ? cred.models : null;
|
|
4556
|
+
return h(
|
|
4557
|
+
"div",
|
|
4558
|
+
{ className: "dcpa-q-cred", "data-status": cred.status ?? "active" },
|
|
4559
|
+
h(
|
|
4560
|
+
"div",
|
|
4561
|
+
{ className: "dcpa-q-who" },
|
|
4562
|
+
h("div", { className: "dcpa-q-name", title: cred.label || cred.id }, cred.label || cred.id),
|
|
4563
|
+
h(
|
|
4564
|
+
"div",
|
|
4565
|
+
{ className: "dcpa-q-meta" },
|
|
4566
|
+
CredentialTags({ t, cred }),
|
|
4567
|
+
models === null
|
|
4568
|
+
? h("span", null, t("qModelsUnknown"))
|
|
4569
|
+
: h(
|
|
4570
|
+
"button",
|
|
4571
|
+
{ type: "button", className: "dcpa-q-link", "aria-expanded": open, onClick: () => setOpen((v) => !v) },
|
|
4572
|
+
open ? t("qHideModels") : fmt(t, "qShowModels", { n: models.length })
|
|
4573
|
+
),
|
|
4574
|
+
cred.statusMessage ? h("span", { title: cred.statusMessage }, cred.statusMessage) : null
|
|
4575
|
+
)
|
|
4576
|
+
),
|
|
4577
|
+
h(CredentialWindows, { t, cred, windows: credentialWindows(cred), now }),
|
|
4578
|
+
open && models !== null
|
|
4579
|
+
? h(
|
|
4580
|
+
"div",
|
|
4581
|
+
{ className: "dcpa-q-models" },
|
|
4582
|
+
models.map((m) => h("code", { key: m, "data-picker": pickerIds.has(m) ? "true" : "false" }, m))
|
|
4583
|
+
)
|
|
4584
|
+
: null
|
|
4585
|
+
);
|
|
4586
|
+
}
|
|
4587
|
+
|
|
4588
|
+
function ModelChannelMap({ t, account }) {
|
|
4589
|
+
const picker = Array.isArray(account.pickerModels) ? account.pickerModels : [];
|
|
4590
|
+
return h(
|
|
4591
|
+
"details",
|
|
4592
|
+
{ className: "dcpa-q-map" },
|
|
4593
|
+
h(
|
|
4594
|
+
"summary",
|
|
4595
|
+
null,
|
|
4596
|
+
h(
|
|
4597
|
+
"div",
|
|
4598
|
+
{ className: "dcpa-q-row-main" },
|
|
4599
|
+
h("div", { className: "dcpa-q-title" }, t("qMapTitle"), h(InfoTip, { t, text: t("qMapHint") }))
|
|
4600
|
+
)
|
|
4601
|
+
),
|
|
4602
|
+
picker.length === 0
|
|
4603
|
+
? h("p", { className: "dcpa-q-msg" }, t("qMapEmpty"))
|
|
4604
|
+
: picker.map((m) => {
|
|
4605
|
+
const summary = summarizeModelQuota({ accounts: [account] }, m.route, m.id);
|
|
4606
|
+
const entries = summary?.entries ?? [];
|
|
4607
|
+
const matchedBy = account.modelMatch?.[m.id];
|
|
4608
|
+
return h(
|
|
4609
|
+
"div",
|
|
4610
|
+
{ key: `${m.route}:${m.id}`, className: "dcpa-q-map-row" },
|
|
4611
|
+
h("span", { className: "dcpa-q-map-model" }, m.id),
|
|
4612
|
+
h(
|
|
4613
|
+
"span",
|
|
4614
|
+
{ className: "dcpa-q-map-creds" },
|
|
4615
|
+
entries.length === 0
|
|
4616
|
+
? h("span", null, t("qMapNone"))
|
|
4617
|
+
: entries.map(({ cred }) =>
|
|
4618
|
+
h(
|
|
4619
|
+
"span",
|
|
4620
|
+
{ key: cred.id, className: "dcpa-q-tag", "data-tone": cred.status === "active" ? undefined : "danger", title: cred.label || cred.id },
|
|
4621
|
+
`${providerName(t, cred.provider)} · ${cred.label || cred.id}`
|
|
4622
|
+
)
|
|
4623
|
+
),
|
|
4624
|
+
matchedBy && MATCH_KEYS[matchedBy] ? h("span", null, t(MATCH_KEYS[matchedBy])) : null
|
|
4625
|
+
),
|
|
4626
|
+
h("span", { className: "dcpa-q-map-pct dcpa-q-pct" }, pctText(summary?.best))
|
|
4627
|
+
);
|
|
4628
|
+
})
|
|
4629
|
+
);
|
|
4630
|
+
}
|
|
4631
|
+
|
|
4632
|
+
function QuotaSummary({ t, credentials }) {
|
|
4633
|
+
const stats = quotaStats(credentials);
|
|
4634
|
+
const parts = [
|
|
4635
|
+
["ok", stats.healthy, "qStatHealthy"],
|
|
4636
|
+
["low", stats.low, "qStatLow"],
|
|
4637
|
+
["empty", stats.empty, "qStatEmpty"],
|
|
4638
|
+
["issue", stats.issue, "qStatIssue"],
|
|
4639
|
+
["unknown", stats.unknown, "qStatUnknown"],
|
|
4640
|
+
];
|
|
4641
|
+
return h(
|
|
4642
|
+
"div",
|
|
4643
|
+
{ className: "dcpa-q-summary" },
|
|
4644
|
+
h(
|
|
4645
|
+
"div",
|
|
4646
|
+
{ className: "dcpa-q-dist", role: "img", "aria-label": fmt(t, "qTotalCreds", { n: stats.total }) },
|
|
4647
|
+
parts.filter(([, n]) => n > 0).map(([level, n]) => h("span", { key: level, "data-level": level, style: { flex: `${n} 1 0` } }))
|
|
4648
|
+
),
|
|
4649
|
+
h(
|
|
4650
|
+
"div",
|
|
4651
|
+
{ className: "dcpa-q-legend" },
|
|
4652
|
+
h("span", null, fmt(t, "qTotalCreds", { n: stats.total })),
|
|
4653
|
+
parts.filter(([level, n]) => level !== "unknown" || n > 0).map(([level, n, key]) =>
|
|
4654
|
+
h("span", { key: level, "data-level": level }, h("i", { className: "dcpa-q-swatch" }), t(key), h("b", null, String(n)))
|
|
4655
|
+
)
|
|
4656
|
+
)
|
|
4657
|
+
);
|
|
4658
|
+
}
|
|
4659
|
+
|
|
4660
|
+
/** The "额度" tab: flat DSH settings rows, one account at a time. */
|
|
4661
|
+
function QuotaPanel({ t, store = sharedQuota, accountId }) {
|
|
4662
|
+
const snap = useQuota(store);
|
|
4663
|
+
const now = useNow(30 * 1000);
|
|
4664
|
+
const [activeId, setActiveId] = useState(null);
|
|
4665
|
+
const [keyDraft, setKeyDraft] = useState("");
|
|
4666
|
+
const [notice, setNotice] = useState(null);
|
|
4667
|
+
const [providerFilter, setProviderFilter] = useState("all");
|
|
4668
|
+
const [search, setSearch] = useState("");
|
|
4669
|
+
|
|
4670
|
+
const accounts = Array.isArray(snap.data?.accounts) ? snap.data.accounts : [];
|
|
4671
|
+
const scoped = accountId !== undefined;
|
|
4672
|
+
const account = scoped
|
|
4673
|
+
? accounts.find((a) => a.accountId === accountId) ?? null
|
|
4674
|
+
: accounts.find((a) => a.accountId === activeId) ?? accounts[0] ?? null;
|
|
4675
|
+
const credentials = Array.isArray(account?.credentials) ? account.credentials : [];
|
|
4676
|
+
const pickerIds = useMemo(() => new Set((account?.pickerModels ?? []).map((m) => m.id)), [account]);
|
|
4677
|
+
const busy = snap.busy !== null;
|
|
4678
|
+
|
|
4679
|
+
const providers = useMemo(() => {
|
|
4680
|
+
const counts = new Map();
|
|
4681
|
+
for (const c of credentials) counts.set(c.provider || "", (counts.get(c.provider || "") ?? 0) + 1);
|
|
4682
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
4683
|
+
}, [credentials]);
|
|
4684
|
+
|
|
4685
|
+
const groups = useMemo(() => {
|
|
4686
|
+
const q = search.trim().toLowerCase();
|
|
4687
|
+
const map = new Map();
|
|
4688
|
+
for (const c of credentials) {
|
|
4689
|
+
if (providerFilter !== "all" && (c.provider || "") !== providerFilter) continue;
|
|
4690
|
+
if (
|
|
4691
|
+
q !== "" &&
|
|
4692
|
+
!String(c.label ?? "").toLowerCase().includes(q) &&
|
|
4693
|
+
!String(c.id ?? "").toLowerCase().includes(q) &&
|
|
4694
|
+
!(Array.isArray(c.models) && c.models.some((m) => String(m).toLowerCase().includes(q)))
|
|
4695
|
+
) continue;
|
|
4696
|
+
const key = c.provider || "";
|
|
4697
|
+
if (!map.has(key)) map.set(key, []);
|
|
4698
|
+
map.get(key).push(c);
|
|
4699
|
+
}
|
|
4700
|
+
// Problems first, then least remaining, so what needs attention is on top.
|
|
4701
|
+
const rank = (c) => (c.status !== "active" ? 1 : 0);
|
|
4702
|
+
return [...map.entries()].map(([key, list]) => [
|
|
4703
|
+
key,
|
|
4704
|
+
[...list].sort((a, b) => rank(a) - rank(b) || (credentialHeadroom(a) ?? 101) - (credentialHeadroom(b) ?? 101)),
|
|
4705
|
+
]);
|
|
4706
|
+
}, [credentials, providerFilter, search]);
|
|
4707
|
+
|
|
4708
|
+
if (store === null || store === undefined) return null;
|
|
4709
|
+
|
|
4710
|
+
const saveKey = async () => {
|
|
4711
|
+
if (account === null || keyDraft.trim() === "") return;
|
|
4712
|
+
setNotice(null);
|
|
4713
|
+
const result = await store.saveKey(account.accountId, keyDraft.trim());
|
|
4714
|
+
if (result.ok) {
|
|
4715
|
+
setKeyDraft("");
|
|
4716
|
+
setNotice(t("qKeySaved"));
|
|
4717
|
+
}
|
|
4718
|
+
};
|
|
4719
|
+
const clearKey = async () => {
|
|
4720
|
+
if (account === null) return;
|
|
4721
|
+
const name = account.displayName || account.accountId;
|
|
4722
|
+
if (typeof window !== "undefined" && typeof window.confirm === "function" && !window.confirm(fmt(t, "qKeyClearConfirm", { name }))) return;
|
|
4723
|
+
setNotice(null);
|
|
4724
|
+
const result = await store.clearKey(account.accountId);
|
|
4725
|
+
if (result.ok) setNotice(t("qKeyCleared"));
|
|
4726
|
+
};
|
|
4727
|
+
|
|
4728
|
+
const errorText = account ? quotaErrorText(t, account) : null;
|
|
4729
|
+
const refreshing = account?.refreshing === true || (snap.busy === "refresh" && snap.busyAccount === account?.accountId);
|
|
4730
|
+
const shown = groups.reduce((n, [, list]) => n + list.length, 0);
|
|
4731
|
+
|
|
4732
|
+
return h(
|
|
4733
|
+
"div",
|
|
4734
|
+
{ className: "dcpa-q", "data-dsh-plugin": MODULE_ID },
|
|
4735
|
+
scoped
|
|
4736
|
+
? null
|
|
4737
|
+
: h(
|
|
4738
|
+
"div",
|
|
4739
|
+
{ className: "dcpa-q-head" },
|
|
4740
|
+
h("h3", { className: "dcpa-q-h" }, t("qTitle")),
|
|
4741
|
+
h("p", { className: "dcpa-q-desc" }, t("qSubtitle"))
|
|
3000
4742
|
),
|
|
3001
|
-
|
|
3002
|
-
|
|
4743
|
+
scoped && account === null ? h("p", { className: "dcpa-q-msg" }, t("qPeekLoading")) : null,
|
|
4744
|
+
!scoped && accounts.length > 1
|
|
4745
|
+
? h(
|
|
4746
|
+
"div",
|
|
4747
|
+
{ className: "dcpa-q-seg", role: "tablist", "aria-label": t("qAccountsLabel") },
|
|
4748
|
+
accounts.map((a) =>
|
|
4749
|
+
h(
|
|
3003
4750
|
"button",
|
|
3004
4751
|
{
|
|
4752
|
+
key: a.accountId,
|
|
3005
4753
|
type: "button",
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
4754
|
+
role: "tab",
|
|
4755
|
+
className: "dcpa-q-seg-tab",
|
|
4756
|
+
"aria-selected": account?.accountId === a.accountId,
|
|
4757
|
+
onClick: () => {
|
|
4758
|
+
setActiveId(a.accountId);
|
|
4759
|
+
setKeyDraft("");
|
|
4760
|
+
setNotice(null);
|
|
4761
|
+
setProviderFilter("all");
|
|
4762
|
+
setSearch("");
|
|
4763
|
+
},
|
|
3009
4764
|
},
|
|
3010
|
-
|
|
4765
|
+
a.displayName || a.accountId,
|
|
4766
|
+
a.keyConfigured ? null : h("span", { className: "dcpa-q-seg-note" }, t("qKeyStatusUnset"))
|
|
3011
4767
|
)
|
|
3012
|
-
|
|
3013
|
-
|
|
4768
|
+
)
|
|
4769
|
+
)
|
|
4770
|
+
: null,
|
|
4771
|
+
snap.data !== null && accounts.length === 0 ? h("p", { className: "dcpa-q-msg" }, t("qNoAccounts")) : null,
|
|
4772
|
+
account
|
|
4773
|
+
? h(
|
|
4774
|
+
"div",
|
|
4775
|
+
{ className: "dcpa-q-row" },
|
|
4776
|
+
h(
|
|
4777
|
+
"div",
|
|
4778
|
+
{ className: "dcpa-q-row-main" },
|
|
4779
|
+
h(
|
|
4780
|
+
"div",
|
|
4781
|
+
{ className: "dcpa-q-title" },
|
|
4782
|
+
h("label", { htmlFor: `dcpa-q-key-${account.accountId}` }, t("qKeyLabel")),
|
|
4783
|
+
h(InfoTip, { t, text: fmt(t, "qKeyHint", { ref: account.keyRef ?? "" }) })
|
|
4784
|
+
)
|
|
4785
|
+
),
|
|
4786
|
+
h(
|
|
4787
|
+
"div",
|
|
4788
|
+
{ className: "dcpa-q-actions" },
|
|
4789
|
+
h(
|
|
4790
|
+
"span",
|
|
4791
|
+
{ className: "dcpa-q-tag", "data-tone": account.keyConfigured ? "success" : undefined },
|
|
4792
|
+
account.keyConfigured ? t("qKeyStatusSet") : t("qKeyStatusUnset")
|
|
4793
|
+
)
|
|
4794
|
+
)
|
|
4795
|
+
)
|
|
4796
|
+
: null,
|
|
4797
|
+
account
|
|
4798
|
+
? h(
|
|
4799
|
+
"div",
|
|
4800
|
+
{ className: "dcpa-q-form" },
|
|
4801
|
+
h("input", {
|
|
4802
|
+
id: `dcpa-q-key-${account.accountId}`,
|
|
4803
|
+
className: "dcpa-q-input",
|
|
4804
|
+
type: "password",
|
|
4805
|
+
autoComplete: "off",
|
|
4806
|
+
spellCheck: false,
|
|
4807
|
+
value: keyDraft,
|
|
4808
|
+
placeholder: account.keyConfigured ? t("qKeySetPlaceholder") : t("qKeyUnsetPlaceholder"),
|
|
4809
|
+
onChange: (e) => setKeyDraft(e.target.value),
|
|
4810
|
+
onKeyDown: (e) => {
|
|
4811
|
+
if (e.key === "Enter") void saveKey();
|
|
4812
|
+
},
|
|
4813
|
+
}),
|
|
4814
|
+
h(
|
|
4815
|
+
"button",
|
|
4816
|
+
{ type: "button", className: "dcpa-q-btn", "data-variant": "primary", disabled: busy || keyDraft.trim() === "", onClick: () => void saveKey() },
|
|
4817
|
+
snap.busy === "save" ? t("qKeySaving") : t("qKeySave")
|
|
4818
|
+
),
|
|
4819
|
+
account.keyConfigured
|
|
4820
|
+
? h(
|
|
4821
|
+
"button",
|
|
4822
|
+
{ type: "button", className: "dcpa-q-btn", "data-variant": "danger", disabled: busy, onClick: () => void clearKey() },
|
|
4823
|
+
t("qKeyClear")
|
|
4824
|
+
)
|
|
4825
|
+
: null
|
|
4826
|
+
)
|
|
4827
|
+
: null,
|
|
4828
|
+
account && account.keyConfigured
|
|
4829
|
+
? h(
|
|
4830
|
+
"div",
|
|
4831
|
+
{ className: "dcpa-q-row" },
|
|
4832
|
+
h(
|
|
4833
|
+
"div",
|
|
4834
|
+
{ className: "dcpa-q-row-main" },
|
|
4835
|
+
h("div", { className: "dcpa-q-title" }, t("qDataLabel")),
|
|
4836
|
+
h(
|
|
4837
|
+
"div",
|
|
4838
|
+
{ className: "dcpa-q-sub" },
|
|
4839
|
+
refreshing
|
|
4840
|
+
? t("qRefreshing")
|
|
4841
|
+
: account.fetchedAt
|
|
4842
|
+
? fmt(t, "qUpdated", { time: agoText(t, account.fetchedAt, now) })
|
|
4843
|
+
: t("qNeverUpdated"),
|
|
4844
|
+
account.enabled === false ? ` · ${t("qAccountDisabled")}` : ""
|
|
4845
|
+
)
|
|
4846
|
+
),
|
|
4847
|
+
h(
|
|
4848
|
+
"div",
|
|
4849
|
+
{ className: "dcpa-q-actions" },
|
|
4850
|
+
h(
|
|
4851
|
+
"button",
|
|
4852
|
+
{ type: "button", className: "dcpa-q-btn", "data-variant": "outline", disabled: busy || refreshing, onClick: () => void store.refresh(account.accountId) },
|
|
4853
|
+
t("qRefresh")
|
|
4854
|
+
)
|
|
4855
|
+
)
|
|
4856
|
+
)
|
|
4857
|
+
: null,
|
|
4858
|
+
errorText ? h("p", { className: "dcpa-q-msg", "data-tone": "error", role: "alert" }, errorText, credentials.length > 0 ? ` ${t("qStale")}` : "") : null,
|
|
4859
|
+
snap.error ? h("p", { className: "dcpa-q-msg", "data-tone": "error", role: "alert" }, fmt(t, "qErrLoad", { error: snap.error })) : null,
|
|
4860
|
+
notice ? h("p", { className: "dcpa-q-msg", "data-tone": "ok", role: "status" }, notice) : null,
|
|
4861
|
+
account && account.keyConfigured && credentials.length === 0 && account.state === "ok"
|
|
4862
|
+
? h("p", { className: "dcpa-q-msg" }, t("qNoCredentials"))
|
|
4863
|
+
: null,
|
|
4864
|
+
account && account.keyConfigured && credentials.length > 0 ? h(QuotaSummary, { t, credentials }) : null,
|
|
4865
|
+
account && account.keyConfigured && (providers.length > 1 || credentials.length > 8)
|
|
4866
|
+
? h(
|
|
4867
|
+
"div",
|
|
4868
|
+
{ className: "dcpa-q-tools" },
|
|
4869
|
+
providers.length > 1
|
|
4870
|
+
? [["all", credentials.length], ...providers].map(([key, count]) =>
|
|
4871
|
+
h(
|
|
4872
|
+
"button",
|
|
4873
|
+
{
|
|
4874
|
+
key: key || "_",
|
|
4875
|
+
type: "button",
|
|
4876
|
+
className: "dcpa-q-filter",
|
|
4877
|
+
"aria-pressed": providerFilter === key,
|
|
4878
|
+
onClick: () => setProviderFilter(key),
|
|
4879
|
+
},
|
|
4880
|
+
key === "all" ? t("qFilterAll") : providerName(t, key),
|
|
4881
|
+
h("small", null, String(count))
|
|
4882
|
+
)
|
|
4883
|
+
)
|
|
4884
|
+
: null,
|
|
4885
|
+
credentials.length > 8
|
|
4886
|
+
? h("input", {
|
|
4887
|
+
className: "dcpa-q-input",
|
|
4888
|
+
type: "search",
|
|
4889
|
+
value: search,
|
|
4890
|
+
placeholder: t("qSearchPlaceholder"),
|
|
4891
|
+
"aria-label": t("qSearchPlaceholder"),
|
|
4892
|
+
onChange: (e) => setSearch(e.target.value),
|
|
4893
|
+
})
|
|
4894
|
+
: null
|
|
4895
|
+
)
|
|
4896
|
+
: null,
|
|
4897
|
+
account && account.keyConfigured && credentials.length > 0 && shown === 0 ? h("p", { className: "dcpa-q-msg" }, t("qNoMatch")) : null,
|
|
4898
|
+
account && account.keyConfigured
|
|
4899
|
+
? groups.map(([key, list]) =>
|
|
4900
|
+
h(
|
|
4901
|
+
"section",
|
|
4902
|
+
{ key: key || "_", "aria-label": providerName(t, key) },
|
|
4903
|
+
h("div", { className: "dcpa-q-group-title" }, providerName(t, key), h("small", null, String(list.length))),
|
|
4904
|
+
list.map((cred) => h(CredentialRow, { key: cred.id, t, cred, now, pickerIds }))
|
|
4905
|
+
)
|
|
4906
|
+
)
|
|
4907
|
+
: null,
|
|
4908
|
+
account && account.keyConfigured && credentials.length > 0 ? h(ModelChannelMap, { t, account }) : null
|
|
4909
|
+
);
|
|
4910
|
+
}
|
|
4911
|
+
|
|
4912
|
+
/** The CLIProxyAPI settings section: accounts, each with 连接 / 模型 / 额度 tabs. */
|
|
4913
|
+
function CpaSectionPage({ t }) {
|
|
4914
|
+
return h(CpaSettingsPanel, { t });
|
|
4915
|
+
}
|
|
4916
|
+
|
|
4917
|
+
/** Snapshot of the session's model directory (current or pending selection). */
|
|
4918
|
+
function useSelection(directory) {
|
|
4919
|
+
const [, setTick] = useState(0);
|
|
4920
|
+
useEffect(() => {
|
|
4921
|
+
if (!directory || typeof directory.subscribe !== "function") return undefined;
|
|
4922
|
+
let alive = true;
|
|
4923
|
+
const off = directory.subscribe(() => {
|
|
4924
|
+
if (alive) setTick((n) => n + 1);
|
|
4925
|
+
});
|
|
4926
|
+
return () => {
|
|
4927
|
+
alive = false;
|
|
4928
|
+
off?.();
|
|
4929
|
+
};
|
|
4930
|
+
}, [directory]);
|
|
4931
|
+
const snapshot = directory && typeof directory.getSnapshot === "function" ? directory.getSnapshot() : null;
|
|
4932
|
+
const current = snapshot?.pending ?? snapshot?.current ?? null;
|
|
4933
|
+
return current && typeof current.provider === "string" && typeof current.model === "string" ? current : null;
|
|
4934
|
+
}
|
|
4935
|
+
|
|
4936
|
+
/**
|
|
4937
|
+
* Composer badge left of the model picker: signal bars + best remaining
|
|
4938
|
+
* percentage of the credentials that serve the selected model on the
|
|
4939
|
+
* selected model's own CPA account; click opens a peek list. Accounts
|
|
4940
|
+
* without a management key show nothing.
|
|
4941
|
+
*/
|
|
4942
|
+
function QuotaBadge({ t, directory: directProp = null, resolveDirectory = null, store = sharedQuota, sessionId = null, placement = "composer", presence = composerBadges }) {
|
|
4943
|
+
const shadowed = useBadgePlacement(placement, sessionId, presence);
|
|
4944
|
+
const directory = useDirectory(directProp, shadowed ? null : resolveDirectory);
|
|
4945
|
+
const selection = useSelection(shadowed ? null : directory);
|
|
4946
|
+
const snap = useQuota(selection ? store : null);
|
|
4947
|
+
const now = useNow(60 * 1000);
|
|
4948
|
+
const [open, setOpen] = useState(false);
|
|
4949
|
+
const [pos, setPos] = useState(null);
|
|
4950
|
+
const wrapRef = useRef(null);
|
|
4951
|
+
const buttonRef = useRef(null);
|
|
4952
|
+
|
|
4953
|
+
const summary = selection ? summarizeModelQuota(snap.data, selection.provider, selection.model) : null;
|
|
4954
|
+
const visible = !shadowed && summary !== null && summary.account.keyConfigured === true;
|
|
4955
|
+
|
|
4956
|
+
useEffect(() => {
|
|
4957
|
+
if (!open) return undefined;
|
|
4958
|
+
const place = () => {
|
|
4959
|
+
const rect = buttonRef.current?.getBoundingClientRect?.();
|
|
4960
|
+
if (!rect || typeof window === "undefined") return;
|
|
4961
|
+
const width = Math.min(340, window.innerWidth - 32);
|
|
4962
|
+
const left = Math.max(16, Math.min(rect.left, window.innerWidth - width - 16));
|
|
4963
|
+
setPos({ left, bottom: Math.max(16, window.innerHeight - rect.top + 6), width });
|
|
4964
|
+
};
|
|
4965
|
+
place();
|
|
4966
|
+
const onDown = (e) => {
|
|
4967
|
+
if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
|
|
4968
|
+
};
|
|
4969
|
+
const onKey = (e) => {
|
|
4970
|
+
if (e.key === "Escape") {
|
|
4971
|
+
setOpen(false);
|
|
4972
|
+
buttonRef.current?.focus?.();
|
|
4973
|
+
}
|
|
4974
|
+
};
|
|
4975
|
+
document.addEventListener("mousedown", onDown, true);
|
|
4976
|
+
document.addEventListener("keydown", onKey, true);
|
|
4977
|
+
window.addEventListener("resize", place);
|
|
4978
|
+
return () => {
|
|
4979
|
+
document.removeEventListener("mousedown", onDown, true);
|
|
4980
|
+
document.removeEventListener("keydown", onKey, true);
|
|
4981
|
+
window.removeEventListener("resize", place);
|
|
4982
|
+
};
|
|
4983
|
+
}, [open]);
|
|
4984
|
+
|
|
4985
|
+
useEffect(() => {
|
|
4986
|
+
if (!visible && open) setOpen(false);
|
|
4987
|
+
}, [visible, open]);
|
|
4988
|
+
|
|
4989
|
+
if (!visible) return null;
|
|
4990
|
+
const { account, entries, best, usableCount } = summary;
|
|
4991
|
+
const loadingFirst = account.fetchedAt === undefined && (account.state === "loading" || account.refreshing === true || account.state === "idle");
|
|
4992
|
+
const level = quotaLevel(best);
|
|
4993
|
+
const label = fmt(t, "qBadgeLabel", { model: selection.model });
|
|
4994
|
+
const errorText = quotaErrorText(t, account);
|
|
4995
|
+
const refreshing = account.refreshing === true || snap.busy === "refresh";
|
|
4996
|
+
|
|
4997
|
+
const peek = open
|
|
4998
|
+
? h(
|
|
4999
|
+
"div",
|
|
5000
|
+
{
|
|
5001
|
+
className: "dcpa-q-pop",
|
|
5002
|
+
role: "dialog",
|
|
5003
|
+
"aria-label": label,
|
|
5004
|
+
style: pos ? { left: `${pos.left}px`, bottom: `${pos.bottom}px`, width: `${pos.width}px` } : { visibility: "hidden" },
|
|
5005
|
+
},
|
|
3014
5006
|
h(
|
|
3015
|
-
"
|
|
3016
|
-
{
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
5007
|
+
"div",
|
|
5008
|
+
{ className: "dcpa-q-pop-head" },
|
|
5009
|
+
h("div", { className: "dcpa-q-pop-model" }, selection.model),
|
|
5010
|
+
h("div", { className: "dcpa-q-pop-sub" }, `${account.displayName || account.accountId} · ${fmt(t, "qPeekCreds", { n: usableCount })}`)
|
|
5011
|
+
),
|
|
5012
|
+
errorText ? h("p", { className: "dcpa-q-pop-text", "data-tone": "error" }, errorText) : null,
|
|
5013
|
+
loadingFirst ? h("p", { className: "dcpa-q-pop-text" }, t("qPeekLoading")) : null,
|
|
5014
|
+
!loadingFirst && entries.length === 0 ? h("p", { className: "dcpa-q-pop-text" }, t("qPeekNoCreds")) : null,
|
|
5015
|
+
entries.map(({ cred, windows, headroom }, index) =>
|
|
5016
|
+
h(
|
|
5017
|
+
"div",
|
|
5018
|
+
{ key: cred.id, className: "dcpa-q-pop-cred" },
|
|
5019
|
+
h(
|
|
5020
|
+
"div",
|
|
5021
|
+
{ className: "dcpa-q-pop-who" },
|
|
5022
|
+
h("span", { className: "dcpa-q-tag" }, providerName(t, cred.provider)),
|
|
5023
|
+
h("span", { className: "dcpa-q-name", title: cred.label || cred.id }, cred.label || cred.id),
|
|
5024
|
+
index === 0 && cred.status === "active" && headroom !== null && entries.length > 1
|
|
5025
|
+
? h("span", { className: "dcpa-q-tag", "data-tone": "success" }, t("qPeekBest"))
|
|
5026
|
+
: null,
|
|
5027
|
+
CredentialTags({ t, cred })
|
|
5028
|
+
),
|
|
5029
|
+
h(CredentialWindows, { t, cred, windows, now })
|
|
5030
|
+
)
|
|
5031
|
+
),
|
|
5032
|
+
h(
|
|
5033
|
+
"div",
|
|
5034
|
+
{ className: "dcpa-q-pop-foot" },
|
|
5035
|
+
h("span", null, account.fetchedAt ? fmt(t, "qUpdated", { time: agoText(t, account.fetchedAt, now) }) : t("qPeekFooter")),
|
|
5036
|
+
h(
|
|
5037
|
+
"button",
|
|
5038
|
+
{
|
|
5039
|
+
type: "button",
|
|
5040
|
+
className: "dcpa-q-btn",
|
|
5041
|
+
disabled: snap.busy !== null || account.refreshing === true,
|
|
5042
|
+
onClick: () => void store?.refresh(account.accountId),
|
|
5043
|
+
},
|
|
5044
|
+
refreshing ? t("qRefreshing") : t("qRefresh")
|
|
5045
|
+
)
|
|
3023
5046
|
)
|
|
3024
5047
|
)
|
|
3025
|
-
|
|
5048
|
+
: null;
|
|
5049
|
+
|
|
5050
|
+
return h(
|
|
5051
|
+
"span",
|
|
5052
|
+
{ ref: wrapRef, className: "dcpa-q-wrap", "data-placement": placement, "data-dsh-plugin": MODULE_ID },
|
|
5053
|
+
h(
|
|
5054
|
+
"button",
|
|
5055
|
+
{
|
|
5056
|
+
ref: buttonRef,
|
|
5057
|
+
type: "button",
|
|
5058
|
+
className: "dcpa-q-badge",
|
|
5059
|
+
"data-level": level,
|
|
5060
|
+
"aria-label": label,
|
|
5061
|
+
"aria-haspopup": "dialog",
|
|
5062
|
+
"aria-expanded": open,
|
|
5063
|
+
title: label,
|
|
5064
|
+
onClick: () => setOpen((v) => !v),
|
|
5065
|
+
},
|
|
5066
|
+
h("span", { className: "dcpa-q-bars", "data-level": level, "aria-hidden": "true" }, h("i"), h("i"), h("i")),
|
|
5067
|
+
h("span", { className: "dcpa-q-badge-pct" }, loadingFirst ? "…" : pctText(best))
|
|
5068
|
+
),
|
|
5069
|
+
peek
|
|
3026
5070
|
);
|
|
3027
5071
|
}
|
|
3028
5072
|
|
|
5073
|
+
// #endregion
|
|
5074
|
+
|
|
3029
5075
|
const inject = ["slots", "locale"];
|
|
3030
5076
|
|
|
3031
5077
|
function apply(ctx) {
|
|
3032
5078
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-cliproxyapi: dictionaries");
|
|
3033
5079
|
const t = ctx.locale.bind(NS);
|
|
3034
5080
|
|
|
5081
|
+
// Each activation owns its stylesheet. A hot reload can start the new
|
|
5082
|
+
// activation before the old one is disposed; sharing one element would
|
|
5083
|
+
// let the old disposal remove the new activation's styles.
|
|
3035
5084
|
if (typeof document !== "undefined") {
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
if (!styleEl) {
|
|
3039
|
-
styleEl = document.createElement("style");
|
|
3040
|
-
styleEl.id = styleId;
|
|
5085
|
+
ctx.effect(() => {
|
|
5086
|
+
const styleEl = document.createElement("style");
|
|
3041
5087
|
styleEl.dataset.plugin = MODULE_ID;
|
|
3042
5088
|
styleEl.textContent = CSS;
|
|
3043
5089
|
document.head.appendChild(styleEl);
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
}
|
|
3047
|
-
ctx.effect(
|
|
3048
|
-
() => () => {
|
|
3049
|
-
styleEl?.remove();
|
|
3050
|
-
},
|
|
3051
|
-
"dsh-cliproxyapi: styles"
|
|
3052
|
-
);
|
|
5090
|
+
return () => styleEl.remove();
|
|
5091
|
+
}, "dsh-cliproxyapi: styles");
|
|
3053
5092
|
}
|
|
3054
5093
|
|
|
5094
|
+
// List and keyed slots reject a second entry with the same id/key at the
|
|
5095
|
+
// same priority. During a hot reload the previous activation may still
|
|
5096
|
+
// hold it, so register one priority lower instead of losing the entry:
|
|
5097
|
+
// the lowest priority renders, and this entry takes over once the old
|
|
5098
|
+
// activation is disposed. The id/key itself stays stable.
|
|
5099
|
+
const registerListEntry = (slots, options, render) => {
|
|
5100
|
+
let priority = options.priority ?? 0;
|
|
5101
|
+
for (let attempt = 0; ; attempt++) {
|
|
5102
|
+
try {
|
|
5103
|
+
return slots.register(priority === 0 ? options : { ...options, priority }, render);
|
|
5104
|
+
} catch (error) {
|
|
5105
|
+
if (attempt >= 4 || !/already has an? (entry|registration)/.test(String(error?.message ?? error))) throw error;
|
|
5106
|
+
priority += 1;
|
|
5107
|
+
}
|
|
5108
|
+
}
|
|
5109
|
+
};
|
|
5110
|
+
|
|
3055
5111
|
// dshmarket installation client, shared by the card and the opt-in
|
|
3056
5112
|
// background auto-install runner. No DOM writes; timers and requests
|
|
3057
5113
|
// are disposed with this activation (HMR/unmount).
|
|
@@ -3066,11 +5122,67 @@ window.__ModuleLoader__.load({
|
|
|
3066
5122
|
};
|
|
3067
5123
|
}, "dsh-cliproxyapi: market updates");
|
|
3068
5124
|
|
|
5125
|
+
// Quota client shared by the "额度" tab and every composer badge; it
|
|
5126
|
+
// polls only while something is subscribed and aborts on dispose.
|
|
5127
|
+
ctx.effect(() => {
|
|
5128
|
+
const store = createQuotaStore();
|
|
5129
|
+
sharedQuota = store;
|
|
5130
|
+
return () => {
|
|
5131
|
+
store.dispose();
|
|
5132
|
+
if (sharedQuota === store) sharedQuota = null;
|
|
5133
|
+
};
|
|
5134
|
+
}, "dsh-cliproxyapi: quota");
|
|
5135
|
+
|
|
5136
|
+
// Composer: the badge sits in conversation.input.right, which the
|
|
5137
|
+
// composer renders immediately before the model selector. The session's
|
|
5138
|
+
// model directory (ui-model-selection) tells it the selected route/model.
|
|
5139
|
+
// The composer renders that list only once the session is bound, so a
|
|
5140
|
+
// New Session hero (model selector already visible) gets a second entry
|
|
5141
|
+
// in conversation.hero.options; it steps aside while the composer badge
|
|
5142
|
+
// of the same session is mounted (see createBadgePresence).
|
|
5143
|
+
if (typeof ctx.inject === "function") {
|
|
5144
|
+
ctx.inject(["slots", "modelDirectories"], (scope) => {
|
|
5145
|
+
const models = scope.modelDirectories;
|
|
5146
|
+
// Never resolve the directory here: DSH caches this result per
|
|
5147
|
+
// session binding, so an early miss would hide the badge for good.
|
|
5148
|
+
const injectDirectory = (sessionId) => ({
|
|
5149
|
+
resolveDirectory: () => models.directoryFor(sessionId).store,
|
|
5150
|
+
sessionId: typeof sessionId === "string" ? sessionId : null,
|
|
5151
|
+
});
|
|
5152
|
+
const badgeEntry = (name, id, placement) =>
|
|
5153
|
+
scope.slots.inject(name, () =>
|
|
5154
|
+
registerListEntry(
|
|
5155
|
+
scope.slots,
|
|
5156
|
+
{
|
|
5157
|
+
name,
|
|
5158
|
+
id,
|
|
5159
|
+
order: 10000,
|
|
5160
|
+
label: () => t("tabQuota"),
|
|
5161
|
+
locale: NS,
|
|
5162
|
+
inject: injectDirectory,
|
|
5163
|
+
},
|
|
5164
|
+
(props = {}) =>
|
|
5165
|
+
h(QuotaBadge, {
|
|
5166
|
+
t,
|
|
5167
|
+
directory: props.directory ?? null,
|
|
5168
|
+
resolveDirectory: typeof props.resolveDirectory === "function" ? props.resolveDirectory : null,
|
|
5169
|
+
sessionId: props.sessionId ?? null,
|
|
5170
|
+
placement,
|
|
5171
|
+
store: sharedQuota,
|
|
5172
|
+
})
|
|
5173
|
+
)
|
|
5174
|
+
);
|
|
5175
|
+
badgeEntry("conversation.input.right", `${NS}:quota`, "composer");
|
|
5176
|
+
badgeEntry("conversation.hero.options", `${NS}:quota-hero`, "hero");
|
|
5177
|
+
});
|
|
5178
|
+
}
|
|
5179
|
+
|
|
3069
5180
|
// 1. Accounts and models: a page of its own in the Settings dialog's left
|
|
3070
5181
|
// navigation (like dshmarket's "Market" section). The slot owner gives
|
|
3071
5182
|
// unregistered ids the default nav glyph; no DOM is touched here.
|
|
3072
5183
|
ctx.slots.inject("settings.section", () =>
|
|
3073
|
-
|
|
5184
|
+
registerListEntry(
|
|
5185
|
+
ctx.slots,
|
|
3074
5186
|
{
|
|
3075
5187
|
name: "settings.section",
|
|
3076
5188
|
id: SETTINGS_SECTION_ID,
|
|
@@ -3078,13 +5190,14 @@ window.__ModuleLoader__.load({
|
|
|
3078
5190
|
label: () => t("nav"),
|
|
3079
5191
|
locale: NS,
|
|
3080
5192
|
},
|
|
3081
|
-
() => h(
|
|
5193
|
+
() => h(CpaSectionPage, { t })
|
|
3082
5194
|
)
|
|
3083
5195
|
);
|
|
3084
5196
|
|
|
3085
5197
|
// 2. Plugin updates only, on the bundle page in Sidebar -> Plugins.
|
|
3086
5198
|
ctx.slots.inject("plugins.bundle.config", () =>
|
|
3087
|
-
|
|
5199
|
+
registerListEntry(
|
|
5200
|
+
ctx.slots,
|
|
3088
5201
|
{
|
|
3089
5202
|
name: "plugins.bundle.config",
|
|
3090
5203
|
key: MODULE_ID,
|
|
@@ -3096,7 +5209,8 @@ window.__ModuleLoader__.load({
|
|
|
3096
5209
|
|
|
3097
5210
|
// 3. Plugin updates only, as a tab in Settings -> Plugins.
|
|
3098
5211
|
ctx.slots.inject("settings.plugins.tab", () =>
|
|
3099
|
-
|
|
5212
|
+
registerListEntry(
|
|
5213
|
+
ctx.slots,
|
|
3100
5214
|
{
|
|
3101
5215
|
name: "settings.plugins.tab",
|
|
3102
5216
|
id: NS,
|