dsh-cliproxyapi-pro 1.5.1 → 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 +93 -51
- package/README.en.md +79 -80
- package/README.md +80 -79
- package/lib/client.js +3371 -653
- package/lib/index.js +170 -182
- package/lib/quota.js +1646 -0
- package/lib/updater.js +90 -23
- package/locale/en.json +2 -2
- package/locale/zh.json +2 -2
- package/package.json +9 -4
package/lib/client.js
CHANGED
|
@@ -12,6 +12,8 @@ window.__ModuleLoader__.load({
|
|
|
12
12
|
const PACKAGE_NAME = "dsh-cliproxyapi-pro";
|
|
13
13
|
const MODULE_ID = "dsh-cliproxyapi-pro";
|
|
14
14
|
const NS = "dsh-cliproxyapi-pro";
|
|
15
|
+
// Stable id of the account/model page in the Settings left navigation.
|
|
16
|
+
const SETTINGS_SECTION_ID = "cliproxyapi";
|
|
15
17
|
|
|
16
18
|
// Document-relative API paths (compatible with <base href="./"> in DSH Web/Desktop)
|
|
17
19
|
const API = {
|
|
@@ -24,52 +26,50 @@ window.__ModuleLoader__.load({
|
|
|
24
26
|
updateCheck: "api/dsh-cliproxyapi/updates/check",
|
|
25
27
|
updateSave: "api/dsh-cliproxyapi/updates/save",
|
|
26
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",
|
|
27
32
|
};
|
|
28
33
|
|
|
34
|
+
// Public, versioned dshmarket Update API (UPDATE-API-V1.md in dshmarket).
|
|
35
|
+
// Installation is delegated to the market; this plugin never runs a
|
|
36
|
+
// package manager. Paths are document-relative like API above.
|
|
37
|
+
const MARKET_API = {
|
|
38
|
+
capabilities: "dsh-market/api/v1/capabilities",
|
|
39
|
+
updates: "dsh-market/api/v1/updates",
|
|
40
|
+
operations: "dsh-market/api/v1/operations",
|
|
41
|
+
restart: "dsh-market/api/v1/restart",
|
|
42
|
+
};
|
|
43
|
+
const MARKET_SCHEMA = "dsh-market/update-api/v1";
|
|
44
|
+
|
|
29
45
|
const zh = {
|
|
30
46
|
nav: "CLIProxyAPI",
|
|
31
|
-
title: "CLIProxyAPI 自动发现与模型筛选",
|
|
32
|
-
subtitle:
|
|
33
|
-
"自动连接 CLIProxyAPI 服务,勾选你希望在对话框模型选择器中显示的模型,自动同步上下文长度、视觉与推理档位。",
|
|
34
47
|
statusConnected: "已连接并同步",
|
|
35
48
|
statusProbing: "正在探测同步…",
|
|
36
49
|
statusError: "连接异常",
|
|
37
50
|
statusDisabled: "已停用",
|
|
38
51
|
statusIdle: "等待同步",
|
|
39
|
-
endpointLabel: "当前活动节点",
|
|
40
|
-
endpointNone: "未连接(将依次尝试配置地址与本地 8317 端口)",
|
|
41
|
-
modelsCountLabel: "选择器展示模型",
|
|
42
|
-
refreshedAtLabel: "上次同步时间",
|
|
43
52
|
refreshBtn: "立即探测刷新",
|
|
44
53
|
refreshingBtn: "正在探测…",
|
|
45
|
-
saveBtn: "
|
|
54
|
+
saveBtn: "保存并应用",
|
|
46
55
|
savingBtn: "正在应用…",
|
|
47
|
-
|
|
48
|
-
unsavedChanges: "有未保存的勾选更改",
|
|
56
|
+
unsavedChanges: "有未保存的更改",
|
|
49
57
|
savedToast: "已保存并同步到对话框模型选择器",
|
|
50
|
-
sectionConnection: "连接与认证配置",
|
|
51
58
|
accountsLabel: "账号",
|
|
52
59
|
addAccountBtn: "+ 添加账号",
|
|
53
|
-
addingAccountBtn: "正在添加…",
|
|
54
60
|
removeAccountBtn: "删除此账号",
|
|
55
61
|
removeConfirm: "确定删除账号「{name}」吗?它同步到模型选择器中的模型路由也会一并移除(已保存的密钥不会删除)。",
|
|
56
|
-
|
|
57
|
-
accountOffBadge: "未启用",
|
|
58
|
-
addedToast: "已添加新账号(默认未启用)。填写服务地址与密钥后,打开「启用此账号」并保存。",
|
|
62
|
+
addedToast: "账号已添加并启用,已按每个家族勾选最新模型。",
|
|
59
63
|
removedToast: "账号已删除,其模型已从模型选择器中移除",
|
|
60
|
-
disabledHint:
|
|
61
|
-
"该账号未启用,它的模型不会出现在对话框模型选择器中。可先填写地址与密钥并点击「立即探测刷新」预览模型列表,勾选后再开启「启用此账号」。",
|
|
64
|
+
disabledHint: "未启用:模型不在模型选择器中,配置已保留。",
|
|
62
65
|
enabledLabel: "启用此账号",
|
|
63
|
-
enabledHint: "
|
|
64
|
-
baseUrlLabel: "
|
|
65
|
-
baseUrlPlaceholder: "
|
|
66
|
-
|
|
67
|
-
apiKeyLabel: "API Key 密钥",
|
|
66
|
+
enabledHint: "开启后,该账号勾选的模型会出现在对话框模型选择器中;关闭后移除这些模型,配置保留。",
|
|
67
|
+
baseUrlLabel: "服务地址",
|
|
68
|
+
baseUrlPlaceholder: "如 http://127.0.0.1:8317,无需加 /v1;留空自动发现",
|
|
69
|
+
apiKeyLabel: "API Key",
|
|
68
70
|
apiKeySetPlaceholder: "已配置密钥(留空保持不变,输入新值可覆盖)",
|
|
69
71
|
apiKeyUnsetPlaceholder: "输入 CLIProxyAPI 访问密钥(本地免密模式可留空)",
|
|
70
|
-
apiKeyHint: "
|
|
71
|
-
apiKeyConfiguredBadge: "密钥已配置",
|
|
72
|
-
apiKeyUnsetBadge: "未设置密钥",
|
|
72
|
+
apiKeyHint: "保存在 DSH 凭证服务({ref}),不会写入配置文件。本地免密的 CPA 可留空。",
|
|
73
73
|
clearKeyBtn: "清除密钥",
|
|
74
74
|
protocolLabel: "接口协议",
|
|
75
75
|
protocolHint: "自动模式下每个模型走所属家族的原生协议,减少 CPA 中转转换;Gemini 等 DSH 不支持原生协议的模型走 OpenAI Chat。",
|
|
@@ -82,22 +82,19 @@ window.__ModuleLoader__.load({
|
|
|
82
82
|
modelProtocolDefault: "跟随全局设置",
|
|
83
83
|
modelProtocolHint: "原生协议:",
|
|
84
84
|
sectionAdvanced: "高级路由与规则过滤",
|
|
85
|
-
providerIdLabel: "
|
|
85
|
+
providerIdLabel: "路由 ID",
|
|
86
86
|
providerIdHint: "写入 llm-pi-ai 的提供商路由标识,每个账号必须不同(首个账号默认 cli-proxy,与现有配置无缝衔接)。",
|
|
87
|
-
apiKeyEnvLabel: "密钥引用名
|
|
87
|
+
apiKeyEnvLabel: "密钥引用名",
|
|
88
88
|
apiKeyEnvHint: "密钥在 DSH 凭证服务中的名称,每个账号建议使用不同的引用名。",
|
|
89
|
-
displayNameLabel: "账号名称
|
|
89
|
+
displayNameLabel: "账号名称",
|
|
90
90
|
displayNameHint: "用于区分账号,并作为 DSH 对话框模型选择器中的分组名称。",
|
|
91
|
-
includePrefixLabel: "
|
|
91
|
+
includePrefixLabel: "仅包含关键词",
|
|
92
92
|
includePrefixPlaceholder: "例如: gpt-6, claude-opus, gemini-4 (多个用逗号分隔)",
|
|
93
93
|
includePrefixHint: "附加规则:只允许模型 ID 包含指定关键词的模型。",
|
|
94
|
-
excludePrefixLabel: "
|
|
94
|
+
excludePrefixLabel: "排除关键词",
|
|
95
95
|
excludePrefixPlaceholder: "例如: tts, image, video, zj/ (多个用逗号分隔)",
|
|
96
96
|
excludePrefixHint: "附加规则:排除包含指定关键词的模型 ID。",
|
|
97
|
-
refreshIntervalLabel: "自动刷新间隔
|
|
98
|
-
sectionModels: "模型显示筛选(仅勾选的模型会在回答选择器中显示)",
|
|
99
|
-
sectionModelsHint:
|
|
100
|
-
"点击模型行即可勾选或取消。不勾选任何模型(全部清空)时默认不限制;勾选后仅展示选中的模型。",
|
|
97
|
+
refreshIntervalLabel: "自动刷新间隔",
|
|
101
98
|
searchModelsPlaceholder: "搜索模型 ID 或名称(如 claude-opus, gpt-6, gemini-4, swe)…",
|
|
102
99
|
filterAll: "全部发现",
|
|
103
100
|
filterSelected: "已勾选",
|
|
@@ -106,7 +103,7 @@ window.__ModuleLoader__.load({
|
|
|
106
103
|
selectAllVisibleBtn: "全选当前列表",
|
|
107
104
|
deselectAllVisibleBtn: "取消当前列表",
|
|
108
105
|
selectRecommendedBtn: "恢复推荐精选",
|
|
109
|
-
selectRecommendedHint: "
|
|
106
|
+
selectRecommendedHint: "按该服务的模型目录,每个模型家族勾选最新的一个。",
|
|
110
107
|
badgeVision: "视觉 Image",
|
|
111
108
|
badgeReasoning: "推理",
|
|
112
109
|
badgeSelected: "已启用",
|
|
@@ -120,9 +117,10 @@ window.__ModuleLoader__.load({
|
|
|
120
117
|
imageInputLabel: "支持图片输入",
|
|
121
118
|
reasoningLevelsLabel: "思考等级",
|
|
122
119
|
resetParamsBtn: "恢复自动探测",
|
|
123
|
-
paramsHint: "留空并保存即恢复 CLIProxyAPI
|
|
120
|
+
paramsHint: "留空并保存即恢复 CLIProxyAPI 自动探测值。",
|
|
124
121
|
discoveredValue: "探测值",
|
|
125
122
|
updateTitle: "插件更新",
|
|
123
|
+
updatePageHint: "这里只管理插件自身的更新。CLIProxyAPI 的账号、模型和协议设置位于「设置」左侧导航的「CLIProxyAPI」页。",
|
|
126
124
|
updateToggleDetails: "展开或收起插件更新设置",
|
|
127
125
|
updateCompactCurrent: "当前 {version}",
|
|
128
126
|
updateCompactAvailable: "有新版本 {version}",
|
|
@@ -133,6 +131,7 @@ window.__ModuleLoader__.load({
|
|
|
133
131
|
updateCompactErrorChannel: "通道未发布",
|
|
134
132
|
updateStatusUnavailableHint: "无法读取更新服务,可能是插件宿主代码尚未重载:重启 DSH(仅刷新页面不够),或检查更新接口是否可用。",
|
|
135
133
|
updateServiceUnavailableError: "更新服务未就绪:当前运行的插件宿主代码仍是旧版,可能尚未重启 DSH。重启 DSH 后重试。",
|
|
134
|
+
updateOriginError: "请求来源校验未通过,更新操作已拒绝。请从 DSH 打开的页面重试;使用反向代理时请保持与页面一致的地址访问。",
|
|
136
135
|
updateSubtitle: "仅更新 CLIProxyAPI 插件自身,不更新 CPA 服务或 DSH 宿主。设置修改后立即保存。",
|
|
137
136
|
updateNewVersion: "新版本",
|
|
138
137
|
updateVersionSummary: "当前版本 {current} · 最新版本 {latest}",
|
|
@@ -195,53 +194,192 @@ window.__ModuleLoader__.load({
|
|
|
195
194
|
updateDownloadPathLabel: "已校验的离线包",
|
|
196
195
|
updateUpgradeCommandLabel: "关闭 DSH 后执行升级命令",
|
|
197
196
|
updateCommandUnavailable: "未生成升级命令。请先确认 DSH 支持的离线包升级方式;升级前关闭 DSH,完成后重新启动。",
|
|
198
|
-
updateOfflineHint: "
|
|
197
|
+
updateOfflineHint: "离线包下载不会自动安装。本插件自身不会运行包管理器、关闭或重启 DSH;一键更新和自动安装都交给 DSH 插件市场(dshmarket)完成。",
|
|
198
|
+
updateCompactInstalled: "{version} 已安装",
|
|
199
|
+
updateMarketLabel: "通过插件市场更新",
|
|
200
|
+
updateMarketDetecting: "正在检测 DSH 插件市场…",
|
|
201
|
+
updateMarketReadyHint: "已检测到 DSH 插件市场(dshmarket),可一键安装新版本。安装和版本解析都由插件市场完成,可能需要重启 DSH 或刷新页面才能生效。",
|
|
202
|
+
updateMarketUnavailableHint: "未检测到 DSH 插件市场(dshmarket)的更新接口。安装插件市场后即可在此一键更新或自动更新;目前可使用下方的离线包下载。",
|
|
203
|
+
updateMarketLinkedHint: "当前为本地链接安装,不通过插件市场安装,以免替换源码链接。",
|
|
204
|
+
updateMarketInstallBtn: "立即更新",
|
|
205
|
+
updateMarketInstalledBtn: "已安装",
|
|
206
|
+
updateMarketInstalling: "正在通过插件市场安装…",
|
|
207
|
+
updateMarketProgress: "正在通过插件市场安装… {percent}%",
|
|
208
|
+
updateMarketRedetectBtn: "重新检测",
|
|
209
|
+
updateMarketConfirmTitle: "通过插件市场安装版本 {version}?",
|
|
210
|
+
updateMarketConfirmHint: "DSH 插件市场会安装本插件的新版本,并在安装后核对实际装上的版本,不一致时恢复原版本。安装期间请勿关闭 DSH;完成后可能需要重启 DSH 或刷新页面。",
|
|
211
|
+
updateMarketConfirmBtn: "确认安装",
|
|
212
|
+
updateMarketAutomaticPrefix: "自动更新:",
|
|
213
|
+
updateMarketInstalled: "已安装版本 {version}。",
|
|
214
|
+
updateMarketVersionLower: "插件市场安装的是 {installed},低于检查到的 {target}:插件市场按其自身的通道与镜像解析版本。",
|
|
215
|
+
updateMarketRestartHint: "重启 DSH 后新版本生效。",
|
|
216
|
+
updateMarketRestartDesktopHint: "请完全退出 DSH 桌面版并重新打开,新版本才会生效。",
|
|
217
|
+
updateMarketRestartManualHint: "请重启 DSH 服务,新版本才会生效。",
|
|
218
|
+
updateMarketRefreshHint: "刷新页面后新版本界面生效。",
|
|
219
|
+
updateMarketAppliedHint: "新版本已生效;若界面仍是旧版,请刷新页面。",
|
|
220
|
+
updateMarketRestartBtn: "重启 DSH",
|
|
221
|
+
updateMarketRestarting: "正在请求重启…",
|
|
222
|
+
updateMarketRestartRequested: "已请求重启 DSH,页面将在 DSH 重新启动后恢复。",
|
|
223
|
+
updateMarketRefreshBtn: "刷新页面",
|
|
224
|
+
updateMarketUnchanged: "插件市场报告完成,但插件版本没有变化。",
|
|
225
|
+
updateMarketCancelled: "插件市场的安装任务已取消。",
|
|
226
|
+
updateMarketRolledBack: "插件市场已回滚此次更新,插件保持原版本。",
|
|
227
|
+
updateMarketForceBtn: "仍然安装",
|
|
228
|
+
updateMarketBusy: "插件市场正在执行另一项更新,请稍后重试。",
|
|
229
|
+
updateMarketAgentsRunning: "有智能体正在运行,插件市场暂不安装。请等待任务结束后重试。",
|
|
230
|
+
updateMarketTooFresh: "新版本刚发布,仍在插件市场的发布冷却期内。可稍后重试,或选择「仍然安装」。",
|
|
231
|
+
updateMarketVersionUnchanged: "插件市场没有解析到更新的版本(镜像可能尚未同步)。可稍后重试,或选择「仍然安装」。",
|
|
232
|
+
updateMarketDowngrade: "插件市场解析到的版本低于当前版本,已自动恢复,未做更改。",
|
|
233
|
+
updateMarketMismatch: "插件市场安装的版本与目标版本不一致,已自动恢复。请等待镜像同步后重试。",
|
|
234
|
+
updateMarketTimeout: "插件市场安装超时,请稍后重试。",
|
|
235
|
+
updateMarketForbidden: "插件市场拒绝了此次更新请求,请从 DSH 打开的页面重试。",
|
|
236
|
+
updateMarketLost: "无法继续跟踪插件市场的安装任务(DSH 可能已重启)。请重新检查更新以确认当前版本。",
|
|
237
|
+
updateMarketUnreachable: "无法连接 DSH 插件市场的更新接口,请确认 dshmarket 已启用后重试。",
|
|
238
|
+
updateMarketRestartFailed: "重启请求失败,请手动重启 DSH。",
|
|
239
|
+
updateMarketFailed: "插件市场安装失败。可在插件市场中查看详情,稍后重试,或使用离线包下载。",
|
|
240
|
+
updateAutoInstallLabel: "自动安装更新",
|
|
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: "其他",
|
|
199
353
|
};
|
|
200
354
|
|
|
201
355
|
const en = {
|
|
202
356
|
nav: "CLIProxyAPI",
|
|
203
|
-
title: "CLIProxyAPI Auto-Discovery & Model Filter",
|
|
204
|
-
subtitle:
|
|
205
|
-
"Connect to CLIProxyAPI and check the exact models you want displayed in the DSH conversation model picker.",
|
|
206
357
|
statusConnected: "Connected & Synced",
|
|
207
358
|
statusProbing: "Probing…",
|
|
208
359
|
statusError: "Connection Error",
|
|
209
360
|
statusDisabled: "Disabled",
|
|
210
361
|
statusIdle: "Idle",
|
|
211
|
-
endpointLabel: "Active Endpoint",
|
|
212
|
-
endpointNone: "Not connected (tries configured URL then localhost:8317)",
|
|
213
|
-
modelsCountLabel: "Models in Picker",
|
|
214
|
-
refreshedAtLabel: "Last Synced",
|
|
215
362
|
refreshBtn: "Probe & Refresh",
|
|
216
363
|
refreshingBtn: "Probing…",
|
|
217
364
|
saveBtn: "Save & Apply",
|
|
218
365
|
savingBtn: "Applying…",
|
|
219
|
-
|
|
220
|
-
unsavedChanges: "Unsaved selection changes",
|
|
366
|
+
unsavedChanges: "Unsaved changes",
|
|
221
367
|
savedToast: "Saved and synchronized to the DSH model picker",
|
|
222
|
-
sectionConnection: "Connection & Authentication",
|
|
223
368
|
accountsLabel: "Accounts",
|
|
224
369
|
addAccountBtn: "+ Add Account",
|
|
225
|
-
addingAccountBtn: "Adding…",
|
|
226
370
|
removeAccountBtn: "Delete Account",
|
|
227
371
|
removeConfirm: "Delete account \"{name}\"? Its model routes are removed from the model picker too (the stored key is kept).",
|
|
228
|
-
|
|
229
|
-
accountOffBadge: "Off",
|
|
230
|
-
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.",
|
|
231
373
|
removedToast: "Account deleted and its models removed from the picker",
|
|
232
|
-
disabledHint:
|
|
233
|
-
"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.",
|
|
234
375
|
enabledLabel: "Enable This Account",
|
|
235
|
-
enabledHint: "
|
|
236
|
-
baseUrlLabel: "
|
|
237
|
-
baseUrlPlaceholder: "
|
|
238
|
-
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",
|
|
239
379
|
apiKeyLabel: "API Key",
|
|
240
380
|
apiKeySetPlaceholder: "Key is stored (leave blank to keep, or type a new key)",
|
|
241
381
|
apiKeyUnsetPlaceholder: "Enter CLIProxyAPI Bearer key (optional for local no-auth)",
|
|
242
|
-
apiKeyHint: "Stored
|
|
243
|
-
apiKeyConfiguredBadge: "Configured",
|
|
244
|
-
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.",
|
|
245
383
|
clearKeyBtn: "Clear Key",
|
|
246
384
|
protocolLabel: "Wire Protocol",
|
|
247
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.",
|
|
@@ -254,22 +392,19 @@ window.__ModuleLoader__.load({
|
|
|
254
392
|
modelProtocolDefault: "Follow global setting",
|
|
255
393
|
modelProtocolHint: "Native protocol: ",
|
|
256
394
|
sectionAdvanced: "Advanced Routing & Keyword Rules",
|
|
257
|
-
providerIdLabel: "
|
|
395
|
+
providerIdLabel: "Route ID",
|
|
258
396
|
providerIdHint: "Provider key under llm-pi-ai; must differ per account (first account: cli-proxy).",
|
|
259
397
|
apiKeyEnvLabel: "Credential Ref",
|
|
260
398
|
apiKeyEnvHint: "Name of the key in the DSH credentials service; use a distinct one per account.",
|
|
261
399
|
displayNameLabel: "Account Name",
|
|
262
400
|
displayNameHint: "Tells accounts apart and titles their group in the DSH model selector.",
|
|
263
|
-
includePrefixLabel: "Include
|
|
401
|
+
includePrefixLabel: "Include Keywords",
|
|
264
402
|
includePrefixPlaceholder: "e.g. gpt-6, claude-opus, gemini-4 (comma-separated)",
|
|
265
403
|
includePrefixHint: "Additional rule: only sync models matching these substrings.",
|
|
266
|
-
excludePrefixLabel: "Exclude
|
|
404
|
+
excludePrefixLabel: "Exclude Keywords",
|
|
267
405
|
excludePrefixPlaceholder: "e.g. tts, image, video, zj/ (comma-separated)",
|
|
268
406
|
excludePrefixHint: "Additional rule: hide models matching any of these substrings.",
|
|
269
|
-
refreshIntervalLabel: "Auto Refresh
|
|
270
|
-
sectionModels: "Model Picker Filter (Only checked models appear in DSH)",
|
|
271
|
-
sectionModelsHint:
|
|
272
|
-
"Click any model row to toggle selection. Only checked models will be shown in the conversation model selector.",
|
|
407
|
+
refreshIntervalLabel: "Auto Refresh",
|
|
273
408
|
searchModelsPlaceholder: "Search model ID or name (e.g. claude-opus, gpt-6, gemini-4)…",
|
|
274
409
|
filterAll: "All Discovered",
|
|
275
410
|
filterSelected: "Checked",
|
|
@@ -278,7 +413,7 @@ window.__ModuleLoader__.load({
|
|
|
278
413
|
selectAllVisibleBtn: "Check Visible",
|
|
279
414
|
deselectAllVisibleBtn: "Uncheck Visible",
|
|
280
415
|
selectRecommendedBtn: "Reset to Recommended",
|
|
281
|
-
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.",
|
|
282
417
|
badgeVision: "Image",
|
|
283
418
|
badgeReasoning: "Reasoning",
|
|
284
419
|
badgeSelected: "Active",
|
|
@@ -292,9 +427,10 @@ window.__ModuleLoader__.load({
|
|
|
292
427
|
imageInputLabel: "Image Input",
|
|
293
428
|
reasoningLevelsLabel: "Thinking Levels",
|
|
294
429
|
resetParamsBtn: "Reset to Discovered",
|
|
295
|
-
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.",
|
|
296
431
|
discoveredValue: "Discovered",
|
|
297
432
|
updateTitle: "Plugin Updates",
|
|
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.",
|
|
298
434
|
updateToggleDetails: "Expand or collapse plugin update settings",
|
|
299
435
|
updateCompactCurrent: "Current {version}",
|
|
300
436
|
updateCompactAvailable: "Update {version}",
|
|
@@ -305,6 +441,7 @@ window.__ModuleLoader__.load({
|
|
|
305
441
|
updateCompactErrorChannel: "Channel unpublished",
|
|
306
442
|
updateStatusUnavailableHint: "Could not reach the update service; the running host plugin code may still be the previous version. Restart DSH (refreshing the page is not enough), or check that the update endpoint is registered.",
|
|
307
443
|
updateServiceUnavailableError: "The update service is not ready: the running host plugin code is still the previous version, so DSH may not have been restarted. Restart DSH and retry.",
|
|
444
|
+
updateOriginError: "The request origin was rejected, so the update action was refused. Retry from the page DSH opened, and reach it through the same address you see (relevant behind a reverse proxy).",
|
|
308
445
|
updateSubtitle: "Updates this CLIProxyAPI plugin only, not the CPA server or DSH. Changes to update settings are saved immediately.",
|
|
309
446
|
updateNewVersion: "New Version",
|
|
310
447
|
updateVersionSummary: "Current {current} · Latest {latest}",
|
|
@@ -367,7 +504,162 @@ window.__ModuleLoader__.load({
|
|
|
367
504
|
updateDownloadPathLabel: "Verified Offline Package",
|
|
368
505
|
updateUpgradeCommandLabel: "Upgrade Command — Run After Closing DSH",
|
|
369
506
|
updateCommandUnavailable: "No upgrade command was generated. First verify the offline-package upgrade method supported by your DSH version. Close DSH before upgrading, then start it again.",
|
|
370
|
-
updateOfflineHint: "
|
|
507
|
+
updateOfflineHint: "Downloading an offline package never installs it. This plugin itself never runs a package manager, closes or restarts DSH; one-click and automatic installation are handed to the DSH plugin market (dshmarket).",
|
|
508
|
+
updateCompactInstalled: "{version} installed",
|
|
509
|
+
updateMarketLabel: "Update Through Plugin Market",
|
|
510
|
+
updateMarketDetecting: "Detecting the DSH plugin market…",
|
|
511
|
+
updateMarketReadyHint: "The DSH plugin market (dshmarket) was detected, so new versions can be installed in one click. The market installs and resolves the version; a DSH restart or page refresh may be needed for it to take effect.",
|
|
512
|
+
updateMarketUnavailableHint: "The DSH plugin market (dshmarket) update API was not detected. Install the plugin market to enable one-click and automatic updates here; until then, use the offline package download below.",
|
|
513
|
+
updateMarketLinkedHint: "This is a linked local installation, so it is not installed through the plugin market, which would replace the source link.",
|
|
514
|
+
updateMarketInstallBtn: "Update Now",
|
|
515
|
+
updateMarketInstalledBtn: "Installed",
|
|
516
|
+
updateMarketInstalling: "Installing through the plugin market…",
|
|
517
|
+
updateMarketProgress: "Installing through the plugin market… {percent}%",
|
|
518
|
+
updateMarketRedetectBtn: "Detect Again",
|
|
519
|
+
updateMarketConfirmTitle: "Install version {version} through the plugin market?",
|
|
520
|
+
updateMarketConfirmHint: "The DSH plugin market installs the new version of this plugin, then verifies the version it actually placed and restores the previous one on a mismatch. Keep DSH open while it runs; a DSH restart or page refresh may be needed afterwards.",
|
|
521
|
+
updateMarketConfirmBtn: "Confirm Install",
|
|
522
|
+
updateMarketAutomaticPrefix: "Automatic update:",
|
|
523
|
+
updateMarketInstalled: "Version {version} is installed.",
|
|
524
|
+
updateMarketVersionLower: "The plugin market installed {installed}, older than the checked {target}: the market resolves versions with its own channel and mirror.",
|
|
525
|
+
updateMarketRestartHint: "Restart DSH for the new version to take effect.",
|
|
526
|
+
updateMarketRestartDesktopHint: "Quit the DSH desktop app completely and reopen it for the new version to take effect.",
|
|
527
|
+
updateMarketRestartManualHint: "Restart the DSH service for the new version to take effect.",
|
|
528
|
+
updateMarketRefreshHint: "Refresh the page to load the new interface.",
|
|
529
|
+
updateMarketAppliedHint: "The new version is active; refresh the page if the interface still looks old.",
|
|
530
|
+
updateMarketRestartBtn: "Restart DSH",
|
|
531
|
+
updateMarketRestarting: "Requesting restart…",
|
|
532
|
+
updateMarketRestartRequested: "A DSH restart was requested. The page recovers once DSH is back.",
|
|
533
|
+
updateMarketRefreshBtn: "Refresh Page",
|
|
534
|
+
updateMarketUnchanged: "The plugin market reported success, but the plugin version did not change.",
|
|
535
|
+
updateMarketCancelled: "The plugin market cancelled the installation.",
|
|
536
|
+
updateMarketRolledBack: "The plugin market rolled this update back; the previous version is kept.",
|
|
537
|
+
updateMarketForceBtn: "Install Anyway",
|
|
538
|
+
updateMarketBusy: "The plugin market is running another update. Retry shortly.",
|
|
539
|
+
updateMarketAgentsRunning: "Agents are running, so the plugin market will not install now. Retry when they finish.",
|
|
540
|
+
updateMarketTooFresh: "This release is still inside the plugin market's release-age wait. Retry later, or choose Install Anyway.",
|
|
541
|
+
updateMarketVersionUnchanged: "The plugin market did not resolve a newer version (its mirror may still be syncing). Retry later, or choose Install Anyway.",
|
|
542
|
+
updateMarketDowngrade: "The plugin market resolved a version older than the current one and restored it; nothing changed.",
|
|
543
|
+
updateMarketMismatch: "The plugin market installed a version other than the target and restored the previous one. Retry after the mirror catches up.",
|
|
544
|
+
updateMarketTimeout: "The plugin market installation timed out. Retry later.",
|
|
545
|
+
updateMarketForbidden: "The plugin market refused this update request. Retry from the page DSH opened.",
|
|
546
|
+
updateMarketLost: "The plugin market operation can no longer be followed (DSH may have restarted). Check for updates again to confirm the current version.",
|
|
547
|
+
updateMarketUnreachable: "Could not reach the DSH plugin market update API. Make sure dshmarket is enabled and retry.",
|
|
548
|
+
updateMarketRestartFailed: "The restart request failed. Restart DSH manually.",
|
|
549
|
+
updateMarketFailed: "The plugin market installation failed. See the plugin market for details, retry later, or use the offline package download.",
|
|
550
|
+
updateAutoInstallLabel: "Automatically Install Updates",
|
|
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",
|
|
371
663
|
};
|
|
372
664
|
|
|
373
665
|
const CSS = `
|
|
@@ -858,7 +1150,9 @@ window.__ModuleLoader__.load({
|
|
|
858
1150
|
.dcpa-update-summary-current { flex: 0 1 auto; }
|
|
859
1151
|
.dcpa-update-summary-status { flex: 1; text-align: right; }
|
|
860
1152
|
.dcpa-update-summary-status[data-state="available"],
|
|
861
|
-
.dcpa-update-summary-status[data-state="downloaded"]
|
|
1153
|
+
.dcpa-update-summary-status[data-state="downloaded"],
|
|
1154
|
+
.dcpa-update-summary-status[data-state="installing"],
|
|
1155
|
+
.dcpa-update-summary-status[data-state="installed"] { color: var(--dsw-alias-brand-primary); }
|
|
862
1156
|
.dcpa-update-summary-status[data-state="error"] { color: var(--dsw-alias-state-error-primary); }
|
|
863
1157
|
.dcpa-update-summary::after {
|
|
864
1158
|
content: "";
|
|
@@ -932,74 +1226,706 @@ window.__ModuleLoader__.load({
|
|
|
932
1226
|
flex-wrap: wrap;
|
|
933
1227
|
}
|
|
934
1228
|
}
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
if (!res.ok || payload.ok === false) {
|
|
975
|
-
throw new Error(payload.error || `HTTP ${res.status}`);
|
|
976
|
-
}
|
|
977
|
-
return payload;
|
|
978
|
-
}
|
|
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
|
+
}
|
|
979
1268
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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; }
|
|
989
1597
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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",
|
|
1003
1929
|
disposed: "updateDisposedError",
|
|
1004
1930
|
"linked-install": "updateLinkedHint",
|
|
1005
1931
|
"unsupported-host": "updateUnsupportedHint",
|
|
@@ -1062,8 +1988,303 @@ window.__ModuleLoader__.load({
|
|
|
1062
1988
|
return body.update;
|
|
1063
1989
|
}
|
|
1064
1990
|
|
|
1991
|
+
// #region dshmarket installation (Update API v1)
|
|
1992
|
+
|
|
1993
|
+
const MARKET_TERMINAL_STATES = new Set(["succeeded", "failed", "cancelled", "rolled-back"]);
|
|
1994
|
+
const MARKET_POLL_MS = 1500;
|
|
1995
|
+
const MARKET_MAX_POLL_FAILURES = 20;
|
|
1996
|
+
const MARKET_FORCEABLE = new Set(["RELEASE_TOO_FRESH", "VERSION_UNCHANGED"]);
|
|
1997
|
+
const AUTO_INSTALL_FIRST_MS = 15000;
|
|
1998
|
+
const AUTO_INSTALL_INTERVAL_MS = 5 * 60 * 1000;
|
|
1999
|
+
const AUTO_INSTALL_RETRY_MS = 6 * 60 * 60 * 1000;
|
|
2000
|
+
const AUTO_INSTALL_MEMORY_KEY = `${PACKAGE_NAME}:market-auto-install`;
|
|
2001
|
+
const MARKET_ERROR_KEYS = {
|
|
2002
|
+
OPERATION_BUSY: "updateMarketBusy",
|
|
2003
|
+
AGENTS_RUNNING: "updateMarketAgentsRunning",
|
|
2004
|
+
RELEASE_TOO_FRESH: "updateMarketTooFresh",
|
|
2005
|
+
VERSION_UNCHANGED: "updateMarketVersionUnchanged",
|
|
2006
|
+
DOWNGRADE_DETECTED: "updateMarketDowngrade",
|
|
2007
|
+
RESOLVED_VERSION_MISMATCH: "updateMarketMismatch",
|
|
2008
|
+
UPDATE_TIMEOUT: "updateMarketTimeout",
|
|
2009
|
+
UPDATE_FORBIDDEN: "updateMarketForbidden",
|
|
2010
|
+
PLUGIN_NOT_INSTALLED: "updateNotInstalledHint",
|
|
2011
|
+
OPERATION_LOST: "updateMarketLost",
|
|
2012
|
+
MARKET_UNREACHABLE: "updateMarketUnreachable",
|
|
2013
|
+
RESTART_FAILED: "updateMarketRestartFailed",
|
|
2014
|
+
};
|
|
2015
|
+
|
|
2016
|
+
function parseVersion(value) {
|
|
2017
|
+
const match = typeof value === "string"
|
|
2018
|
+
? /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value)
|
|
2019
|
+
: null;
|
|
2020
|
+
return match ? { parts: match.slice(1, 4).map(Number), pre: match[4] ? match[4].split(".") : [] } : null;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
/** SemVer precedence, or null when either side is not a version. */
|
|
2024
|
+
function compareVersions(a, b) {
|
|
2025
|
+
const x = parseVersion(a), y = parseVersion(b);
|
|
2026
|
+
if (!x || !y) return null;
|
|
2027
|
+
for (let i = 0; i < 3; i++) if (x.parts[i] !== y.parts[i]) return x.parts[i] > y.parts[i] ? 1 : -1;
|
|
2028
|
+
if (!x.pre.length || !y.pre.length) return x.pre.length ? -1 : y.pre.length ? 1 : 0;
|
|
2029
|
+
for (let i = 0; i < Math.max(x.pre.length, y.pre.length); i++) {
|
|
2030
|
+
const p = x.pre[i], q = y.pre[i];
|
|
2031
|
+
if (p === undefined || q === undefined) return p === undefined ? -1 : 1;
|
|
2032
|
+
if (p === q) continue;
|
|
2033
|
+
const pn = /^\d+$/.test(p), qn = /^\d+$/.test(q);
|
|
2034
|
+
if (pn && qn) return Number(p) > Number(q) ? 1 : -1;
|
|
2035
|
+
if (pn !== qn) return pn ? -1 : 1;
|
|
2036
|
+
return p > q ? 1 : -1;
|
|
2037
|
+
}
|
|
2038
|
+
return 0;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
function validMarketOperation(value) {
|
|
2042
|
+
return !!value && typeof value === "object" && value.schema === MARKET_SCHEMA &&
|
|
2043
|
+
typeof value.operationId === "string" && value.operationId !== "" &&
|
|
2044
|
+
value.packageName === PACKAGE_NAME && typeof value.state === "string";
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
const marketFailureCode = (operation) =>
|
|
2048
|
+
typeof operation?.failure?.code === "string" && operation.failure.code ? operation.failure.code : "UPDATE_FAILED";
|
|
2049
|
+
|
|
2050
|
+
function marketErrorText(t, code) {
|
|
2051
|
+
return t(Object.prototype.hasOwnProperty.call(MARKET_ERROR_KEYS, code) ? MARKET_ERROR_KEYS[code] : "updateMarketFailed");
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
/**
|
|
2055
|
+
* Shared, page-wide client of the dshmarket Update API. One instance lives
|
|
2056
|
+
* per client activation so the settings card and the background
|
|
2057
|
+
* auto-install runner observe the same operation.
|
|
2058
|
+
*/
|
|
2059
|
+
function createMarketUpdater(io = {}) {
|
|
2060
|
+
const doFetch = io.fetch ?? ((url, init) => fetch(url, init));
|
|
2061
|
+
const later = io.setTimeout ?? ((fn, ms) => setTimeout(fn, ms));
|
|
2062
|
+
const cancelLater = io.clearTimeout ?? ((id) => clearTimeout(id));
|
|
2063
|
+
let state = {
|
|
2064
|
+
phase: "unknown", // unknown | discovering | ready | unavailable
|
|
2065
|
+
runtime: null,
|
|
2066
|
+
restartSupported: false,
|
|
2067
|
+
installing: false,
|
|
2068
|
+
automatic: false,
|
|
2069
|
+
targetVersion: null,
|
|
2070
|
+
operation: null,
|
|
2071
|
+
errorCode: null,
|
|
2072
|
+
restarting: false,
|
|
2073
|
+
restartRequested: false,
|
|
2074
|
+
};
|
|
2075
|
+
const listeners = new Set();
|
|
2076
|
+
const controllers = new Set();
|
|
2077
|
+
let disposed = false;
|
|
2078
|
+
let discovering = null;
|
|
2079
|
+
let pollTimer = null;
|
|
2080
|
+
let pollFailures = 0;
|
|
2081
|
+
|
|
2082
|
+
const set = (patch) => {
|
|
2083
|
+
if (disposed) return;
|
|
2084
|
+
state = { ...state, ...patch };
|
|
2085
|
+
for (const listener of [...listeners]) listener(state);
|
|
2086
|
+
};
|
|
2087
|
+
const request = async (url, init = {}) => {
|
|
2088
|
+
const controller = new AbortController();
|
|
2089
|
+
controllers.add(controller);
|
|
2090
|
+
try {
|
|
2091
|
+
const res = await doFetch(url, { cache: "no-store", ...init, signal: controller.signal });
|
|
2092
|
+
let body = null;
|
|
2093
|
+
try { body = await res.json(); } catch {}
|
|
2094
|
+
return { status: res.status, ok: res.ok, body };
|
|
2095
|
+
} finally {
|
|
2096
|
+
controllers.delete(controller);
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2099
|
+
const post = (url, payload) => request(url, {
|
|
2100
|
+
method: "POST",
|
|
2101
|
+
headers: { "content-type": "application/json" },
|
|
2102
|
+
body: JSON.stringify(payload),
|
|
2103
|
+
});
|
|
2104
|
+
const applyOperation = (operation) => {
|
|
2105
|
+
const done = MARKET_TERMINAL_STATES.has(operation.state);
|
|
2106
|
+
set({
|
|
2107
|
+
operation,
|
|
2108
|
+
installing: !done,
|
|
2109
|
+
errorCode: done && operation.state === "failed" ? marketFailureCode(operation) : null,
|
|
2110
|
+
});
|
|
2111
|
+
if (!done) schedulePoll();
|
|
2112
|
+
};
|
|
2113
|
+
const schedulePoll = () => {
|
|
2114
|
+
if (pollTimer !== null) cancelLater(pollTimer);
|
|
2115
|
+
pollTimer = later(poll, MARKET_POLL_MS);
|
|
2116
|
+
};
|
|
2117
|
+
const poll = async () => {
|
|
2118
|
+
pollTimer = null;
|
|
2119
|
+
const id = state.operation?.operationId;
|
|
2120
|
+
if (disposed || !id) return;
|
|
2121
|
+
try {
|
|
2122
|
+
const { status, body } = await request(`${MARKET_API.operations}?operationId=${encodeURIComponent(id)}`);
|
|
2123
|
+
if (disposed || state.operation?.operationId !== id) return;
|
|
2124
|
+
if (status === 404) {
|
|
2125
|
+
// Operations live in the current Host process only.
|
|
2126
|
+
set({ installing: false, errorCode: "OPERATION_LOST" });
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
if (status !== 200 || !validMarketOperation(body?.operation) || body.operation.operationId !== id) {
|
|
2130
|
+
throw new Error("Invalid market operation");
|
|
2131
|
+
}
|
|
2132
|
+
pollFailures = 0;
|
|
2133
|
+
applyOperation(body.operation);
|
|
2134
|
+
} catch (error) {
|
|
2135
|
+
if (disposed || error?.name === "AbortError") return;
|
|
2136
|
+
if (++pollFailures >= MARKET_MAX_POLL_FAILURES) set({ installing: false, errorCode: "OPERATION_LOST" });
|
|
2137
|
+
else schedulePoll();
|
|
2138
|
+
}
|
|
2139
|
+
};
|
|
2140
|
+
|
|
2141
|
+
return {
|
|
2142
|
+
getState: () => state,
|
|
2143
|
+
subscribe(listener) {
|
|
2144
|
+
listeners.add(listener);
|
|
2145
|
+
return () => listeners.delete(listener);
|
|
2146
|
+
},
|
|
2147
|
+
/** Reads only the local capability document; never contacts a registry. */
|
|
2148
|
+
discover() {
|
|
2149
|
+
if (disposed) return Promise.resolve(state);
|
|
2150
|
+
if (discovering) return discovering;
|
|
2151
|
+
if (state.phase !== "ready") set({ phase: "discovering" });
|
|
2152
|
+
discovering = request(MARKET_API.capabilities)
|
|
2153
|
+
.then(({ ok, body }) => {
|
|
2154
|
+
const usable = ok && body?.schema === MARKET_SCHEMA && body.apiVersion === 1 && body.features?.update === true;
|
|
2155
|
+
set({
|
|
2156
|
+
phase: usable ? "ready" : "unavailable",
|
|
2157
|
+
runtime: usable && typeof body.runtime === "string" ? body.runtime : null,
|
|
2158
|
+
restartSupported: usable && body.features?.restart === true && body.restart?.supported === true,
|
|
2159
|
+
});
|
|
2160
|
+
}, () => {
|
|
2161
|
+
set({ phase: "unavailable", runtime: null, restartSupported: false });
|
|
2162
|
+
})
|
|
2163
|
+
.finally(() => { discovering = null; })
|
|
2164
|
+
.then(() => state);
|
|
2165
|
+
return discovering;
|
|
2166
|
+
},
|
|
2167
|
+
/** Starts one market update operation, then follows it until it settles. */
|
|
2168
|
+
async install({ targetVersion = null, force = false, automatic = false } = {}) {
|
|
2169
|
+
if (disposed || state.phase !== "ready" || state.installing) return state;
|
|
2170
|
+
set({ installing: true, automatic, targetVersion, operation: null, errorCode: null, restartRequested: false });
|
|
2171
|
+
try {
|
|
2172
|
+
const { status, body } = await post(
|
|
2173
|
+
MARKET_API.updates,
|
|
2174
|
+
force ? { packageName: PACKAGE_NAME, force: true } : { packageName: PACKAGE_NAME }
|
|
2175
|
+
);
|
|
2176
|
+
if (disposed) return state;
|
|
2177
|
+
if (status === 202 && validMarketOperation(body?.operation)) {
|
|
2178
|
+
pollFailures = 0;
|
|
2179
|
+
applyOperation(body.operation);
|
|
2180
|
+
return state;
|
|
2181
|
+
}
|
|
2182
|
+
const code = typeof body?.failure?.code === "string" && body.failure.code ? body.failure.code
|
|
2183
|
+
: status === 409 ? "OPERATION_BUSY"
|
|
2184
|
+
: status === 403 ? "UPDATE_FORBIDDEN"
|
|
2185
|
+
: status === 400 ? "UPDATE_REJECTED"
|
|
2186
|
+
: status === 404 ? "MARKET_UNREACHABLE"
|
|
2187
|
+
: "UPDATE_FAILED";
|
|
2188
|
+
set({ installing: false, errorCode: code });
|
|
2189
|
+
} catch (error) {
|
|
2190
|
+
if (!disposed) set({ installing: false, errorCode: error?.name === "AbortError" ? null : "MARKET_UNREACHABLE" });
|
|
2191
|
+
}
|
|
2192
|
+
return state;
|
|
2193
|
+
},
|
|
2194
|
+
/** Only offered when the market reports that it owns process restarts. */
|
|
2195
|
+
async restart() {
|
|
2196
|
+
if (disposed || !state.restartSupported || state.restarting) return state;
|
|
2197
|
+
set({ restarting: true, errorCode: null });
|
|
2198
|
+
try {
|
|
2199
|
+
const { ok } = await post(MARKET_API.restart, {});
|
|
2200
|
+
set({ restarting: false, restartRequested: ok, errorCode: ok ? null : "RESTART_FAILED" });
|
|
2201
|
+
} catch (error) {
|
|
2202
|
+
set({ restarting: false, errorCode: error?.name === "AbortError" ? null : "RESTART_FAILED" });
|
|
2203
|
+
}
|
|
2204
|
+
return state;
|
|
2205
|
+
},
|
|
2206
|
+
dispose() {
|
|
2207
|
+
disposed = true;
|
|
2208
|
+
if (pollTimer !== null) cancelLater(pollTimer);
|
|
2209
|
+
pollTimer = null;
|
|
2210
|
+
for (const controller of controllers) controller.abort();
|
|
2211
|
+
controllers.clear();
|
|
2212
|
+
listeners.clear();
|
|
2213
|
+
},
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
function readAutoInstallMemory() {
|
|
2218
|
+
try {
|
|
2219
|
+
const raw = JSON.parse(globalThis.localStorage?.getItem(AUTO_INSTALL_MEMORY_KEY) ?? "null");
|
|
2220
|
+
return raw && typeof raw.version === "string" && Number.isFinite(raw.at) ? raw : null;
|
|
2221
|
+
} catch {
|
|
2222
|
+
return null;
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
function writeAutoInstallMemory(record) {
|
|
2227
|
+
try { globalThis.localStorage?.setItem(AUTO_INSTALL_MEMORY_KEY, JSON.stringify(record)); } catch {}
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
/**
|
|
2231
|
+
* Opt-in automatic installation. The Host's own automatic check finds the
|
|
2232
|
+
* version (channel/region aware); dshmarket installs it. Runs only while a
|
|
2233
|
+
* DSH page is open, reads the local status endpoint only, and tries each
|
|
2234
|
+
* version at most once per {@link AUTO_INSTALL_RETRY_MS} across reloads.
|
|
2235
|
+
*/
|
|
2236
|
+
function startAutoInstall(market, io = {}) {
|
|
2237
|
+
const later = io.setTimeout ?? ((fn, ms) => setTimeout(fn, ms));
|
|
2238
|
+
const cancelLater = io.clearTimeout ?? ((id) => clearTimeout(id));
|
|
2239
|
+
const now = io.now ?? (() => Date.now());
|
|
2240
|
+
const memory = io.memory ?? { read: readAutoInstallMemory, write: writeAutoInstallMemory };
|
|
2241
|
+
const controller = new AbortController();
|
|
2242
|
+
const readStatus = io.readStatus ?? ((signal) => requestPluginUpdate(API.updateStatus, undefined, signal));
|
|
2243
|
+
let stopped = false;
|
|
2244
|
+
let timer = null;
|
|
2245
|
+
const unsubscribe = market.subscribe((state) => {
|
|
2246
|
+
const operation = state.operation;
|
|
2247
|
+
if (state.automatic && operation?.state === "succeeded" && typeof state.targetVersion === "string" &&
|
|
2248
|
+
compareVersions(operation.installedVersion, operation.beforeVersion) === 1) {
|
|
2249
|
+
memory.write({ version: state.targetVersion, at: now(), done: true });
|
|
2250
|
+
}
|
|
2251
|
+
});
|
|
2252
|
+
const tick = async () => {
|
|
2253
|
+
timer = null;
|
|
2254
|
+
try {
|
|
2255
|
+
const status = await readStatus(controller.signal);
|
|
2256
|
+
if (stopped) return;
|
|
2257
|
+
const version = status?.latestVersion;
|
|
2258
|
+
if (status?.autoInstall !== true || status.autoCheck !== true || status.available !== true ||
|
|
2259
|
+
status.blockedReason || typeof version !== "string" || compareVersions(version, status.currentVersion) !== 1) return;
|
|
2260
|
+
const last = memory.read();
|
|
2261
|
+
if (last && last.version === version && (last.done === true || now() - last.at < AUTO_INSTALL_RETRY_MS)) return;
|
|
2262
|
+
if (market.getState().phase !== "ready") await market.discover();
|
|
2263
|
+
const snapshot = market.getState();
|
|
2264
|
+
if (stopped || snapshot.phase !== "ready" || snapshot.installing) return;
|
|
2265
|
+
memory.write({ version, at: now() });
|
|
2266
|
+
await market.install({ targetVersion: version, automatic: true });
|
|
2267
|
+
} catch {
|
|
2268
|
+
// Background work never surfaces errors; the card shows the market state.
|
|
2269
|
+
} finally {
|
|
2270
|
+
if (!stopped) timer = later(tick, AUTO_INSTALL_INTERVAL_MS);
|
|
2271
|
+
}
|
|
2272
|
+
};
|
|
2273
|
+
timer = later(tick, AUTO_INSTALL_FIRST_MS);
|
|
2274
|
+
return () => {
|
|
2275
|
+
stopped = true;
|
|
2276
|
+
if (timer !== null) cancelLater(timer);
|
|
2277
|
+
controller.abort();
|
|
2278
|
+
unsubscribe();
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
let sharedMarket = null;
|
|
2283
|
+
|
|
2284
|
+
// #endregion
|
|
2285
|
+
|
|
1065
2286
|
/** Update state is isolated: polling never reloads account data or drafts. */
|
|
1066
|
-
function PluginUpdatePanel({ t }) {
|
|
2287
|
+
function PluginUpdatePanel({ t, market = null, initiallyOpen = false }) {
|
|
1067
2288
|
const [update, setUpdate] = useState(null);
|
|
1068
2289
|
const [loading, setLoading] = useState(true);
|
|
1069
2290
|
const [busy, setBusy] = useState(null);
|
|
@@ -1169,6 +2390,21 @@ window.__ModuleLoader__.load({
|
|
|
1169
2390
|
}
|
|
1170
2391
|
}, []);
|
|
1171
2392
|
|
|
2393
|
+
// dshmarket state is shared with the background auto-install runner.
|
|
2394
|
+
const [marketState, setMarketState] = useState(() => market?.getState() ?? null);
|
|
2395
|
+
const [confirmInstall, setConfirmInstall] = useState(null);
|
|
2396
|
+
useEffect(() => {
|
|
2397
|
+
if (!market) return undefined;
|
|
2398
|
+
let live = true;
|
|
2399
|
+
const unsubscribe = market.subscribe((next) => { if (live) setMarketState(next); });
|
|
2400
|
+
setMarketState(market.getState());
|
|
2401
|
+
if (market.getState().phase === "unknown") void market.discover();
|
|
2402
|
+
return () => {
|
|
2403
|
+
live = false;
|
|
2404
|
+
unsubscribe();
|
|
2405
|
+
};
|
|
2406
|
+
}, [market]);
|
|
2407
|
+
|
|
1172
2408
|
const working = loading || busy !== null || update?.checking === true || update?.downloading === true;
|
|
1173
2409
|
const blockedReason = update?.blockedReason;
|
|
1174
2410
|
const downloadBlocked = !!blockedReason && blockedReason !== "linked-install";
|
|
@@ -1189,7 +2425,17 @@ window.__ModuleLoader__.load({
|
|
|
1189
2425
|
const statusError = update?.error || update?.errorCode
|
|
1190
2426
|
? { updateCode: update.errorCode }
|
|
1191
2427
|
: null;
|
|
1192
|
-
const
|
|
2428
|
+
const linkedInstall = blockedReason === "linked-install";
|
|
2429
|
+
const marketReady = marketState?.phase === "ready";
|
|
2430
|
+
const marketOperation = marketState?.operation ?? null;
|
|
2431
|
+
const marketInstalling = marketState?.installing === true;
|
|
2432
|
+
const marketInstalledVersion = marketOperation?.state === "succeeded" &&
|
|
2433
|
+
compareVersions(marketOperation.installedVersion, marketOperation.beforeVersion) === 1
|
|
2434
|
+
? marketOperation.installedVersion : null;
|
|
2435
|
+
const canMarketInstall = marketReady && !linkedInstall && !downloadBlocked && !marketInstalling &&
|
|
2436
|
+
!marketInstalledVersion && !!update?.available && typeof update.latestVersion === "string" && !!update.latestVersion;
|
|
2437
|
+
const compactState = loading ? "loading" : marketInstalling ? "installing" : downloading ? "downloading" : checking ? "checking" :
|
|
2438
|
+
marketInstalledVersion ? "installed" :
|
|
1193
2439
|
error || statusError ? "error" : update?.downloadedVersion && update.downloadedVersion === update.latestVersion ? "downloaded" :
|
|
1194
2440
|
update?.available ? "available" : "idle";
|
|
1195
2441
|
// Name the actual failure instead of a generic "update error": the
|
|
@@ -1202,7 +2448,9 @@ window.__ModuleLoader__.load({
|
|
|
1202
2448
|
: failureCode === "service-unavailable" ? "updateCompactStatusUnavailable"
|
|
1203
2449
|
: failureCode === "channel-unavailable" ? "updateCompactErrorChannel"
|
|
1204
2450
|
: "updateCompactError";
|
|
1205
|
-
const compactText = loading ? t("updateLoading") :
|
|
2451
|
+
const compactText = loading ? t("updateLoading") : marketInstalling ? t("updateMarketInstalling") :
|
|
2452
|
+
downloading ? t("updateDownloading") : checking ? t("updateChecking") :
|
|
2453
|
+
marketInstalledVersion ? t("updateCompactInstalled").replace("{version}", marketInstalledVersion) :
|
|
1206
2454
|
error || statusError ? t(compactErrorKey) : compactState === "downloaded"
|
|
1207
2455
|
? t("updateCompactDownloaded").replace("{version}", update.downloadedVersion)
|
|
1208
2456
|
: update?.available ? t("updateCompactAvailable").replace("{version}", availableVersion)
|
|
@@ -1229,7 +2477,7 @@ window.__ModuleLoader__.load({
|
|
|
1229
2477
|
}, t(key))
|
|
1230
2478
|
)
|
|
1231
2479
|
);
|
|
1232
|
-
const toggle = (field, label, hint, checked) =>
|
|
2480
|
+
const toggle = (field, label, hint, checked, unavailable = false) =>
|
|
1233
2481
|
h(
|
|
1234
2482
|
"div",
|
|
1235
2483
|
{ className: "dcpa-update-row dcpa-switch-row" },
|
|
@@ -1243,16 +2491,143 @@ window.__ModuleLoader__.load({
|
|
|
1243
2491
|
className: "dcpa-switch",
|
|
1244
2492
|
"aria-label": t(label),
|
|
1245
2493
|
"aria-checked": checked,
|
|
1246
|
-
|
|
2494
|
+
// An unavailable feature can still be switched off, never on.
|
|
2495
|
+
disabled: working || !update || unavailable && !checked,
|
|
1247
2496
|
onClick: () => { void runAction("save", { [field]: !checked }); },
|
|
1248
2497
|
}, h("span", { className: "dcpa-switch-thumb" }))
|
|
1249
2498
|
);
|
|
1250
2499
|
|
|
2500
|
+
// One-click/automatic installation through dshmarket. Hidden entirely
|
|
2501
|
+
// when the card is rendered without a market client (tests, previews).
|
|
2502
|
+
let marketSection = null;
|
|
2503
|
+
if (market) {
|
|
2504
|
+
const phase = marketState?.phase ?? "unknown";
|
|
2505
|
+
const hintKey = phase === "ready"
|
|
2506
|
+
? linkedInstall ? "updateMarketLinkedHint" : "updateMarketReadyHint"
|
|
2507
|
+
: phase === "unavailable" ? "updateMarketUnavailableHint" : "updateMarketDetecting";
|
|
2508
|
+
const operation = marketOperation;
|
|
2509
|
+
const progress = operation?.progress;
|
|
2510
|
+
const percent = typeof progress?.percent === "number" && Number.isFinite(progress.percent) ? Math.round(progress.percent) : null;
|
|
2511
|
+
const errorCode = marketState?.errorCode ?? null;
|
|
2512
|
+
const outcome = operation?.outcome ?? {};
|
|
2513
|
+
const target = marketState?.targetVersion;
|
|
2514
|
+
const prefix = marketState?.automatic ? `${t("updateMarketAutomaticPrefix")} ` : "";
|
|
2515
|
+
let result = null;
|
|
2516
|
+
if (marketInstalling) {
|
|
2517
|
+
result = h("div", { className: "dcpa-update-notice", role: "status" },
|
|
2518
|
+
h("p", { className: "dcpa-label" }, prefix + (percent === null
|
|
2519
|
+
? t("updateMarketInstalling")
|
|
2520
|
+
: t("updateMarketProgress").replace("{percent}", String(percent))))
|
|
2521
|
+
);
|
|
2522
|
+
} else if (marketInstalledVersion) {
|
|
2523
|
+
const restartHint = outcome.restartRequired
|
|
2524
|
+
? marketState?.restartSupported ? "updateMarketRestartHint"
|
|
2525
|
+
: marketState?.runtime === "desktop" ? "updateMarketRestartDesktopHint" : "updateMarketRestartManualHint"
|
|
2526
|
+
: outcome.refreshRequired ? "updateMarketRefreshHint" : "updateMarketAppliedHint";
|
|
2527
|
+
result = h("div", { className: "dcpa-update-notice", role: "status" },
|
|
2528
|
+
h("p", { className: "dcpa-success" }, prefix + t("updateMarketInstalled").replace("{version}", marketInstalledVersion)),
|
|
2529
|
+
typeof target === "string" && compareVersions(marketInstalledVersion, target) === -1
|
|
2530
|
+
? h("p", { className: "dcpa-hint" }, t("updateMarketVersionLower").replace("{installed}", marketInstalledVersion).replace("{target}", target))
|
|
2531
|
+
: null,
|
|
2532
|
+
h("p", { className: "dcpa-hint" }, t(marketState?.restartRequested ? "updateMarketRestartRequested" : restartHint)),
|
|
2533
|
+
outcome.restartRequired && marketState?.restartSupported && !marketState?.restartRequested
|
|
2534
|
+
? h("div", { className: "dcpa-actions" },
|
|
2535
|
+
h("button", {
|
|
2536
|
+
type: "button",
|
|
2537
|
+
className: "dcpa-btn dcpa-btn-primary",
|
|
2538
|
+
disabled: marketState?.restarting === true,
|
|
2539
|
+
onClick: () => { void market.restart(); },
|
|
2540
|
+
}, marketState?.restarting ? t("updateMarketRestarting") : t("updateMarketRestartBtn")))
|
|
2541
|
+
: !outcome.restartRequired && outcome.refreshRequired
|
|
2542
|
+
? h("div", { className: "dcpa-actions" },
|
|
2543
|
+
h("button", {
|
|
2544
|
+
type: "button",
|
|
2545
|
+
className: "dcpa-btn dcpa-btn-primary",
|
|
2546
|
+
onClick: () => { globalThis.location?.reload(); },
|
|
2547
|
+
}, t("updateMarketRefreshBtn")))
|
|
2548
|
+
: null
|
|
2549
|
+
);
|
|
2550
|
+
} else if (operation?.state === "succeeded") {
|
|
2551
|
+
result = h("div", { className: "dcpa-update-notice", role: "status" },
|
|
2552
|
+
h("p", { className: "dcpa-hint" }, prefix + t("updateMarketUnchanged")));
|
|
2553
|
+
} else if (operation?.state === "cancelled" || operation?.state === "rolled-back") {
|
|
2554
|
+
result = h("div", { className: "dcpa-update-notice", role: "status" },
|
|
2555
|
+
h("p", { className: "dcpa-hint" }, prefix + t(operation.state === "cancelled" ? "updateMarketCancelled" : "updateMarketRolledBack")));
|
|
2556
|
+
}
|
|
2557
|
+
const errorBlock = errorCode && !marketInstalling
|
|
2558
|
+
? h("div", { className: "dcpa-update-notice" },
|
|
2559
|
+
h("p", { className: "dcpa-error", role: "alert" }, prefix + marketErrorText(t, errorCode)),
|
|
2560
|
+
MARKET_FORCEABLE.has(errorCode) && canMarketInstall
|
|
2561
|
+
? h("div", { className: "dcpa-actions" },
|
|
2562
|
+
h("button", {
|
|
2563
|
+
type: "button",
|
|
2564
|
+
className: "dcpa-btn",
|
|
2565
|
+
disabled: working,
|
|
2566
|
+
onClick: () => { void market.install({ targetVersion: update.latestVersion, force: true }); },
|
|
2567
|
+
}, t("updateMarketForceBtn")))
|
|
2568
|
+
: null)
|
|
2569
|
+
: null;
|
|
2570
|
+
marketSection = h(React.Fragment, null,
|
|
2571
|
+
h("div", { className: "dcpa-update-row" },
|
|
2572
|
+
h("div", { className: "dcpa-field" },
|
|
2573
|
+
h("span", { className: "dcpa-label" }, t("updateMarketLabel")),
|
|
2574
|
+
h("p", { className: "dcpa-hint" }, t(hintKey))
|
|
2575
|
+
),
|
|
2576
|
+
phase === "ready" && !linkedInstall
|
|
2577
|
+
? h("div", { className: "dcpa-actions" },
|
|
2578
|
+
h("button", {
|
|
2579
|
+
type: "button",
|
|
2580
|
+
className: "dcpa-btn dcpa-btn-primary",
|
|
2581
|
+
disabled: working || !canMarketInstall,
|
|
2582
|
+
onClick: () => { setConfirmVersion(null); setConfirmInstall(update.latestVersion); },
|
|
2583
|
+
}, marketInstalling ? t("updateMarketInstalling")
|
|
2584
|
+
: marketInstalledVersion ? t("updateMarketInstalledBtn") : t("updateMarketInstallBtn")))
|
|
2585
|
+
: phase === "unavailable"
|
|
2586
|
+
? h("div", { className: "dcpa-actions" },
|
|
2587
|
+
h("button", { type: "button", className: "dcpa-btn", onClick: () => { void market.discover(); } }, t("updateMarketRedetectBtn")))
|
|
2588
|
+
: null
|
|
2589
|
+
),
|
|
2590
|
+
confirmInstall ? h("div", {
|
|
2591
|
+
className: "dcpa-update-notice",
|
|
2592
|
+
role: "alertdialog",
|
|
2593
|
+
"aria-modal": false,
|
|
2594
|
+
"aria-label": t("updateMarketConfirmTitle").replace("{version}", confirmInstall),
|
|
2595
|
+
},
|
|
2596
|
+
h("p", { className: "dcpa-label" }, t("updateMarketConfirmTitle").replace("{version}", confirmInstall)),
|
|
2597
|
+
h("p", { className: "dcpa-hint" }, t("updateMarketConfirmHint")),
|
|
2598
|
+
h("div", { className: "dcpa-actions" },
|
|
2599
|
+
h("button", {
|
|
2600
|
+
type: "button",
|
|
2601
|
+
className: "dcpa-btn dcpa-btn-primary",
|
|
2602
|
+
disabled: working || !canMarketInstall || confirmInstall !== update?.latestVersion,
|
|
2603
|
+
onClick: () => {
|
|
2604
|
+
const version = confirmInstall;
|
|
2605
|
+
setConfirmInstall(null);
|
|
2606
|
+
void market.install({ targetVersion: version });
|
|
2607
|
+
},
|
|
2608
|
+
}, t("updateMarketConfirmBtn")),
|
|
2609
|
+
h("button", { type: "button", className: "dcpa-btn", onClick: () => setConfirmInstall(null) }, t("updateCancelBtn"))
|
|
2610
|
+
),
|
|
2611
|
+
confirmInstall !== update?.latestVersion ? h("p", { className: "dcpa-hint" }, t("updateStaleVersion")) : null
|
|
2612
|
+
) : null,
|
|
2613
|
+
result,
|
|
2614
|
+
errorBlock
|
|
2615
|
+
);
|
|
2616
|
+
}
|
|
2617
|
+
|
|
1251
2618
|
// Native details starts closed. Do not control `open`: polling and saves
|
|
1252
2619
|
// must preserve the user's expanded/collapsed choice without remounting.
|
|
2620
|
+
// `initiallyOpen` (the update-only plugin page) passes a constant `true`
|
|
2621
|
+
// that never changes, so React never rewrites it after mount and the
|
|
2622
|
+
// user can still collapse the card.
|
|
1253
2623
|
return h(
|
|
1254
2624
|
"details",
|
|
1255
|
-
{
|
|
2625
|
+
{
|
|
2626
|
+
className: "dcpa-card dcpa-update-card",
|
|
2627
|
+
"aria-label": t("updateTitle"),
|
|
2628
|
+
"aria-busy": working,
|
|
2629
|
+
...(initiallyOpen ? { open: true } : {}),
|
|
2630
|
+
},
|
|
1256
2631
|
h("summary", { className: "dcpa-update-summary", title: t("updateToggleDetails") },
|
|
1257
2632
|
h("span", { className: "dcpa-update-summary-title" }, t("updateTitle")),
|
|
1258
2633
|
h("span", { className: "dcpa-update-summary-current" },
|
|
@@ -1280,9 +2655,9 @@ window.__ModuleLoader__.load({
|
|
|
1280
2655
|
}, checking ? t("updateChecking") : !update && !loading ? t("updateRetryBtn") : t("updateCheckBtn")),
|
|
1281
2656
|
h("button", {
|
|
1282
2657
|
type: "button",
|
|
1283
|
-
className: "dcpa-btn dcpa-btn-primary",
|
|
2658
|
+
className: marketReady && !linkedInstall ? "dcpa-btn" : "dcpa-btn dcpa-btn-primary",
|
|
1284
2659
|
disabled: working || !canDownload,
|
|
1285
|
-
onClick: () => setConfirmVersion(update.latestVersion),
|
|
2660
|
+
onClick: () => { setConfirmInstall(null); setConfirmVersion(update.latestVersion); },
|
|
1286
2661
|
}, downloading ? t("updateDownloading") : update?.downloadedVersion === update?.latestVersion && update?.downloadedVersion
|
|
1287
2662
|
? t("updateDownloadedBtn") : t("updateDownloadBtn"))
|
|
1288
2663
|
)
|
|
@@ -1306,6 +2681,7 @@ window.__ModuleLoader__.load({
|
|
|
1306
2681
|
),
|
|
1307
2682
|
confirmVersion !== update?.latestVersion ? h("p", { className: "dcpa-hint" }, t("updateStaleVersion")) : null
|
|
1308
2683
|
) : null,
|
|
2684
|
+
marketSection,
|
|
1309
2685
|
h("div", { className: "dcpa-update-row" },
|
|
1310
2686
|
h("div", { className: "dcpa-field" },
|
|
1311
2687
|
h("span", { className: "dcpa-label" }, t("updateChannelLabel")),
|
|
@@ -1327,6 +2703,8 @@ window.__ModuleLoader__.load({
|
|
|
1327
2703
|
),
|
|
1328
2704
|
toggle("autoCheck", "updateAutoCheckLabel", "updateAutoCheckHint", update?.autoCheck !== false),
|
|
1329
2705
|
toggle("autoDownload", "updateAutoDownloadLabel", "updateAutoDownloadHint", update?.autoDownload === true),
|
|
2706
|
+
toggle("autoInstall", "updateAutoInstallLabel", "updateAutoInstallHint", update?.autoInstall === true,
|
|
2707
|
+
!marketReady || linkedInstall || downloadBlocked),
|
|
1330
2708
|
blockedReason ? h("div", { className: "dcpa-update-notice" }, h("p", { className: "dcpa-hint" }, t(blockedKey))) : null,
|
|
1331
2709
|
update?.downloadedVersion && update?.downloadPath ? h("div", { className: "dcpa-update-notice", role: "status" },
|
|
1332
2710
|
h("p", { className: "dcpa-label" }, t(blockedReason === "linked-install" ? "updateLinkedDownloadedHint" : "updateDownloadedHint").replace("{version}", update.downloadedVersion)),
|
|
@@ -1351,24 +2729,133 @@ window.__ModuleLoader__.load({
|
|
|
1351
2729
|
}
|
|
1352
2730
|
|
|
1353
2731
|
/**
|
|
1354
|
-
*
|
|
1355
|
-
*
|
|
2732
|
+
* The plugin's own page under Plugins (bundle page and Plugins tab): only
|
|
2733
|
+
* plugin updates. Account and model settings live in their own settings
|
|
2734
|
+
* section ({@link CpaSettingsPanel}).
|
|
1356
2735
|
*/
|
|
1357
|
-
function
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
const [rev, setRev] = useState(0);
|
|
1366
|
-
|
|
1367
|
-
const accounts = Array.isArray(data?.accounts) ? data.accounts : [];
|
|
1368
|
-
const active = accounts.find((a) => a.id === activeId) ?? accounts[0];
|
|
2736
|
+
function PluginUpdatePage({ t }) {
|
|
2737
|
+
return h(
|
|
2738
|
+
"div",
|
|
2739
|
+
{ className: "dcpa-wrap", "data-dsh-plugin": MODULE_ID },
|
|
2740
|
+
h("p", { className: "dcpa-hint" }, t("updatePageHint")),
|
|
2741
|
+
h(PluginUpdatePanel, { t, market: sharedMarket, initiallyOpen: true })
|
|
2742
|
+
);
|
|
2743
|
+
}
|
|
1369
2744
|
|
|
1370
|
-
|
|
1371
|
-
|
|
2745
|
+
/**
|
|
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.
|
|
2841
|
+
*/
|
|
2842
|
+
function CpaSettingsPanel({ t }) {
|
|
2843
|
+
const [loading, setLoading] = useState(true);
|
|
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 }
|
|
2847
|
+
const [data, setData] = useState(null);
|
|
2848
|
+
const [activeId, setActiveId] = useState(null);
|
|
2849
|
+
// Bumped whenever the server's saved config should replace the drafts.
|
|
2850
|
+
const [rev, setRev] = useState(0);
|
|
2851
|
+
const [adding, setAdding] = useState(false);
|
|
2852
|
+
const dirtyRef = useRef(false);
|
|
2853
|
+
|
|
2854
|
+
const accounts = Array.isArray(data?.accounts) ? data.accounts : [];
|
|
2855
|
+
const active = accounts.find((a) => a.id === activeId) ?? accounts[0];
|
|
2856
|
+
|
|
2857
|
+
const fetchStatus = useCallback(async () => {
|
|
2858
|
+
try {
|
|
1372
2859
|
const res = await fetch(API.status, { cache: "no-store" });
|
|
1373
2860
|
const body = await res.json();
|
|
1374
2861
|
if (!res.ok || body.ok === false) {
|
|
@@ -1376,9 +2863,9 @@ window.__ModuleLoader__.load({
|
|
|
1376
2863
|
}
|
|
1377
2864
|
setData(body);
|
|
1378
2865
|
setRev((r) => r + 1);
|
|
1379
|
-
|
|
2866
|
+
setLoadError(null);
|
|
1380
2867
|
} catch (err) {
|
|
1381
|
-
|
|
2868
|
+
setLoadError(err instanceof Error ? err.message : String(err));
|
|
1382
2869
|
} finally {
|
|
1383
2870
|
setLoading(false);
|
|
1384
2871
|
}
|
|
@@ -1388,124 +2875,153 @@ window.__ModuleLoader__.load({
|
|
|
1388
2875
|
void fetchStatus();
|
|
1389
2876
|
}, [fetchStatus]);
|
|
1390
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
|
+
|
|
1391
2891
|
const run = useCallback(async (kind, fn) => {
|
|
1392
2892
|
setBusy(kind);
|
|
1393
|
-
|
|
1394
|
-
setToast(null);
|
|
2893
|
+
setNotice(null);
|
|
1395
2894
|
try {
|
|
1396
2895
|
return await fn();
|
|
1397
2896
|
} catch (err) {
|
|
1398
|
-
|
|
2897
|
+
setNotice({ tone: "error", text: err instanceof Error ? err.message : String(err) });
|
|
1399
2898
|
return undefined;
|
|
1400
2899
|
} finally {
|
|
1401
2900
|
setBusy(null);
|
|
1402
2901
|
}
|
|
1403
2902
|
}, []);
|
|
1404
2903
|
|
|
1405
|
-
const
|
|
1406
|
-
(
|
|
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) =>
|
|
1407
2916
|
run("refresh", async () => {
|
|
1408
2917
|
setData(await postJson(API.refresh, { accountId }));
|
|
1409
2918
|
}),
|
|
1410
|
-
|
|
1411
|
-
);
|
|
1412
|
-
|
|
1413
|
-
const onSave = useCallback(
|
|
1414
|
-
(accountId, payload) =>
|
|
2919
|
+
save: (accountId, payload) =>
|
|
1415
2920
|
run("save", async () => {
|
|
1416
2921
|
setData(await postJson(API.save, { ...payload, accountId }));
|
|
2922
|
+
dirtyRef.current = false;
|
|
1417
2923
|
setRev((r) => r + 1);
|
|
1418
|
-
|
|
2924
|
+
setNotice({ tone: "ok", text: t("savedToast") });
|
|
1419
2925
|
}),
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
run("add", async () => {
|
|
1426
|
-
const body = await postJson(API.addAccount, {});
|
|
1427
|
-
setData(body);
|
|
1428
|
-
if (body.createdId) setActiveId(body.createdId);
|
|
1429
|
-
setRev((r) => r + 1);
|
|
1430
|
-
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 });
|
|
1431
2931
|
}),
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
const onRemove = useCallback(
|
|
1436
|
-
(account) => {
|
|
2932
|
+
test: (accountId, draft) =>
|
|
2933
|
+
run("test", async () => (await postJson(API.testAccount, { ...(accountId ? { accountId } : {}), ...draft })).test),
|
|
2934
|
+
remove: (account) => {
|
|
1437
2935
|
const name = account.config?.displayName || account.id;
|
|
1438
|
-
if (typeof window !== "undefined" && !window.confirm(t
|
|
1439
|
-
return;
|
|
1440
|
-
}
|
|
2936
|
+
if (typeof window !== "undefined" && typeof window.confirm === "function" && !window.confirm(fmt(t, "removeConfirm", { name }))) return;
|
|
1441
2937
|
void run("remove", async () => {
|
|
1442
2938
|
const body = await postJson(API.removeAccount, { accountId: account.id });
|
|
2939
|
+
dirtyRef.current = false;
|
|
1443
2940
|
setData(body);
|
|
1444
2941
|
setActiveId(body.accounts?.[0]?.id ?? null);
|
|
1445
2942
|
setRev((r) => r + 1);
|
|
1446
|
-
|
|
2943
|
+
setNotice({ tone: "ok", text: t("removedToast") });
|
|
1447
2944
|
});
|
|
1448
2945
|
},
|
|
1449
|
-
|
|
1450
|
-
|
|
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);
|
|
1451
2964
|
|
|
1452
2965
|
return h(
|
|
1453
2966
|
"div",
|
|
1454
|
-
{ className: "dcpa-wrap", "data-dsh-plugin": MODULE_ID },
|
|
2967
|
+
{ className: "dcpa-wrap dcpa-q", "data-dsh-plugin": MODULE_ID },
|
|
1455
2968
|
h(
|
|
1456
2969
|
"div",
|
|
1457
|
-
{ className: "dcpa-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
className: "dcpa-account-chip",
|
|
1479
|
-
"aria-selected": active?.id === a.id,
|
|
1480
|
-
"data-active": active?.id === a.id ? "true" : "false",
|
|
1481
|
-
"data-enabled": a.config?.enabled === false ? "false" : "true",
|
|
1482
|
-
title: a.config?.provider,
|
|
1483
|
-
onClick: () => setActiveId(a.id),
|
|
1484
|
-
},
|
|
1485
|
-
h("span", { className: stateDotClass(a.status?.state) }),
|
|
1486
|
-
a.config?.displayName || a.id,
|
|
1487
|
-
a.config?.enabled === false
|
|
1488
|
-
? h("span", { className: "dcpa-badge" }, t("accountOffBadge"))
|
|
1489
|
-
: 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
|
+
)
|
|
1490
2991
|
)
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
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);
|
|
1499
3005
|
},
|
|
1500
|
-
|
|
1501
|
-
)
|
|
1502
|
-
)
|
|
1503
|
-
!loading && accounts.length === 0 ? h("p", { className: "dcpa-hint" }, t("noAccounts")) : null,
|
|
1504
|
-
error ? h("p", { className: "dcpa-error", role: "alert" }, error) : null,
|
|
1505
|
-
toast ? h("p", { className: "dcpa-success", role: "status" }, toast) : null
|
|
3006
|
+
},
|
|
3007
|
+
t("addAccountBtn")
|
|
3008
|
+
)
|
|
1506
3009
|
),
|
|
1507
|
-
h(
|
|
1508
|
-
|
|
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
|
|
1509
3025
|
? h(AccountPanel, {
|
|
1510
3026
|
key: active.id,
|
|
1511
3027
|
t,
|
|
@@ -1513,18 +3029,105 @@ window.__ModuleLoader__.load({
|
|
|
1513
3029
|
global: data?.global ?? {},
|
|
1514
3030
|
rev,
|
|
1515
3031
|
busy,
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
3032
|
+
notice,
|
|
3033
|
+
actions,
|
|
3034
|
+
onDirtyChange: (dirty) => {
|
|
3035
|
+
dirtyRef.current = dirty;
|
|
3036
|
+
},
|
|
1519
3037
|
})
|
|
1520
3038
|
: null
|
|
1521
3039
|
);
|
|
1522
3040
|
}
|
|
1523
3041
|
|
|
1524
|
-
|
|
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 }) {
|
|
1525
3124
|
const syncing = busy === "refresh";
|
|
1526
3125
|
const saving = busy === "save";
|
|
1527
|
-
|
|
3126
|
+
|
|
3127
|
+
const [tab, setTab] = useState("connection");
|
|
3128
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
3129
|
+
const menuRef = useRef(null);
|
|
3130
|
+
const [testResult, setTestResult] = useState(null);
|
|
1528
3131
|
|
|
1529
3132
|
// Model filter UI state
|
|
1530
3133
|
const [search, setSearch] = useState("");
|
|
@@ -1537,7 +3140,6 @@ window.__ModuleLoader__.load({
|
|
|
1537
3140
|
const [editingId, setEditingId] = useState(null);
|
|
1538
3141
|
|
|
1539
3142
|
// Form draft state
|
|
1540
|
-
const [enabled, setEnabled] = useState(true);
|
|
1541
3143
|
const [baseURL, setBaseURL] = useState("");
|
|
1542
3144
|
const [apiKeyDraft, setApiKeyDraft] = useState("");
|
|
1543
3145
|
// "auto" or the protocol every model is fixed to.
|
|
@@ -1550,20 +3152,32 @@ window.__ModuleLoader__.load({
|
|
|
1550
3152
|
const [refreshIntervalSeconds, setRefreshIntervalSeconds] = useState("300");
|
|
1551
3153
|
const [probeTimeoutMs, setProbeTimeoutMs] = useState("5000");
|
|
1552
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
|
+
|
|
1553
3166
|
// Load the drafts from the saved config on mount and after every save.
|
|
1554
3167
|
useEffect(() => {
|
|
1555
3168
|
const c = account.config ?? {};
|
|
1556
|
-
|
|
1557
|
-
setBaseURL(
|
|
1558
|
-
setProtocol(
|
|
1559
|
-
setProvider(
|
|
1560
|
-
setDisplayName(
|
|
1561
|
-
setApiKeyEnv(
|
|
1562
|
-
setIncludePrefix(
|
|
1563
|
-
setExcludePrefix(
|
|
1564
|
-
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);
|
|
1565
3178
|
setProbeTimeoutMs(String(global?.probeTimeoutMs ?? 5000));
|
|
1566
3179
|
setApiKeyDraft("");
|
|
3180
|
+
setTestResult(null);
|
|
1567
3181
|
|
|
1568
3182
|
const sel = Array.isArray(c.selectedModels)
|
|
1569
3183
|
? c.selectedModels
|
|
@@ -1579,53 +3193,46 @@ window.__ModuleLoader__.load({
|
|
|
1579
3193
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1580
3194
|
}, [rev]);
|
|
1581
3195
|
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
selectedIds
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
apiKeyEnv,
|
|
1618
|
-
includePrefix,
|
|
1619
|
-
excludePrefix,
|
|
1620
|
-
refreshIntervalSeconds,
|
|
1621
|
-
probeTimeoutMs,
|
|
1622
|
-
apiKeyDraft,
|
|
1623
|
-
]
|
|
1624
|
-
);
|
|
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
|
+
});
|
|
1625
3231
|
|
|
1626
3232
|
const st = account.status ?? { state: "idle", modelCount: 0, models: [], allModels: [] };
|
|
1627
3233
|
const cred = account.credential ?? { configured: false, ref: apiKeyEnv };
|
|
1628
3234
|
const effectiveUrl = account.config?.effectiveBaseURL || "";
|
|
3235
|
+
const enabled = account.config?.enabled !== false;
|
|
1629
3236
|
|
|
1630
3237
|
const allModels = useMemo(() => {
|
|
1631
3238
|
if (Array.isArray(st.allModels) && st.allModels.length > 0) return st.allModels;
|
|
@@ -1660,9 +3267,25 @@ window.__ModuleLoader__.load({
|
|
|
1660
3267
|
|
|
1661
3268
|
const currentIdsKey = useMemo(() => [...selectedIds].sort().join("\0"), [selectedIds]);
|
|
1662
3269
|
const currentOverridesKey = useMemo(() => overridesKey(modelOverrides), [modelOverrides]);
|
|
1663
|
-
const
|
|
1664
|
-
|
|
1665
|
-
|
|
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]);
|
|
1666
3289
|
|
|
1667
3290
|
const toggleModel = useCallback((id) => {
|
|
1668
3291
|
setSelectedIds((prev) => {
|
|
@@ -1708,18 +3331,10 @@ window.__ModuleLoader__.load({
|
|
|
1708
3331
|
};
|
|
1709
3332
|
const merged = { ...current, ...patch };
|
|
1710
3333
|
const normalized = {};
|
|
1711
|
-
if (
|
|
1712
|
-
Number.isInteger(merged.contextWindow) &&
|
|
1713
|
-
merged.contextWindow > 0 &&
|
|
1714
|
-
merged.contextWindow !== discovered.contextWindow
|
|
1715
|
-
) {
|
|
3334
|
+
if (Number.isInteger(merged.contextWindow) && merged.contextWindow > 0 && merged.contextWindow !== discovered.contextWindow) {
|
|
1716
3335
|
normalized.contextWindow = merged.contextWindow;
|
|
1717
3336
|
}
|
|
1718
|
-
if (
|
|
1719
|
-
Number.isInteger(merged.maxTokens) &&
|
|
1720
|
-
merged.maxTokens > 0 &&
|
|
1721
|
-
merged.maxTokens !== discovered.maxTokens
|
|
1722
|
-
) {
|
|
3337
|
+
if (Number.isInteger(merged.maxTokens) && merged.maxTokens > 0 && merged.maxTokens !== discovered.maxTokens) {
|
|
1723
3338
|
normalized.maxTokens = merged.maxTokens;
|
|
1724
3339
|
}
|
|
1725
3340
|
if (merged.image !== discovered.image) normalized.image = merged.image === true;
|
|
@@ -1735,9 +3350,7 @@ window.__ModuleLoader__.load({
|
|
|
1735
3350
|
|
|
1736
3351
|
// The protocol a model would use with the panel's current draft settings.
|
|
1737
3352
|
const effectiveProtocol = useCallback(
|
|
1738
|
-
(m) =>
|
|
1739
|
-
modelOverrides[m.id]?.protocol ??
|
|
1740
|
-
(protocol === "auto" ? m.nativeProtocol ?? "openai-completions" : protocol),
|
|
3353
|
+
(m) => modelOverrides[m.id]?.protocol ?? (protocol === "auto" ? m.nativeProtocol ?? "openai-completions" : protocol),
|
|
1741
3354
|
[modelOverrides, protocol]
|
|
1742
3355
|
);
|
|
1743
3356
|
const protocolName = (p) => t(PROTOCOL_NAME_KEYS[p] ?? "protocolNameCompletions");
|
|
@@ -1750,170 +3363,224 @@ window.__ModuleLoader__.load({
|
|
|
1750
3363
|
});
|
|
1751
3364
|
}, []);
|
|
1752
3365
|
|
|
1753
|
-
const
|
|
3366
|
+
const quotaSnap = useQuota(sharedQuota);
|
|
3367
|
+
const quotaFloor = accountQuotaFloor(quotaSnap.data, account.id);
|
|
1754
3368
|
|
|
1755
|
-
const
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
? t("statusProbing")
|
|
1760
|
-
: st.state === "error"
|
|
1761
|
-
? t("statusError")
|
|
1762
|
-
: st.state === "disabled"
|
|
1763
|
-
? t("statusDisabled")
|
|
1764
|
-
: t("statusIdle");
|
|
3369
|
+
const discard = () => {
|
|
3370
|
+
setTestResult(null);
|
|
3371
|
+
actions.discard();
|
|
3372
|
+
};
|
|
1765
3373
|
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
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" },
|
|
1770
3395
|
h(
|
|
1771
|
-
|
|
1772
|
-
{
|
|
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}` },
|
|
1773
3408
|
h(
|
|
1774
3409
|
"div",
|
|
1775
|
-
{ className: "dcpa-
|
|
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
|
+
}),
|
|
1776
3421
|
h(
|
|
1777
|
-
"
|
|
1778
|
-
null,
|
|
1779
|
-
|
|
1780
|
-
|
|
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")
|
|
3425
|
+
)
|
|
3426
|
+
),
|
|
3427
|
+
testResult
|
|
3428
|
+
? h(
|
|
1781
3429
|
"p",
|
|
1782
|
-
{ className: "dcpa-
|
|
1783
|
-
|
|
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 })
|
|
1784
3434
|
)
|
|
1785
|
-
|
|
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}` },
|
|
3440
|
+
h(
|
|
3441
|
+
"div",
|
|
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) })
|
|
3496
|
+
),
|
|
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
|
+
)
|
|
3524
|
+
),
|
|
3525
|
+
h(
|
|
3526
|
+
FieldRow,
|
|
3527
|
+
{ t, label: t("refreshIntervalLabel"), htmlFor: `dcpa-interval-${account.id}` },
|
|
3528
|
+
h(
|
|
3529
|
+
"div",
|
|
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
|
+
);
|
|
3543
|
+
|
|
3544
|
+
const modelsTab = h(
|
|
3545
|
+
"div",
|
|
3546
|
+
{ className: "dcpa-tabbody dcpa-models" },
|
|
3547
|
+
h("p", { className: "dcpa-q-sub" }, t("modelsEmptyRule")),
|
|
3548
|
+
// Search + Segmented Filter + Batch Actions
|
|
3549
|
+
h(
|
|
3550
|
+
"div",
|
|
3551
|
+
{ className: "dcpa-toolbar" },
|
|
1786
3552
|
h(
|
|
1787
3553
|
"div",
|
|
1788
|
-
{ className: "dcpa-
|
|
3554
|
+
{ className: "dcpa-seg" },
|
|
1789
3555
|
h(
|
|
1790
3556
|
"button",
|
|
1791
3557
|
{
|
|
1792
3558
|
type: "button",
|
|
1793
|
-
className: "dcpa-btn",
|
|
1794
|
-
|
|
1795
|
-
onClick:
|
|
3559
|
+
className: "dcpa-seg-btn",
|
|
3560
|
+
"data-active": tabFilter === "all" ? "true" : "false",
|
|
3561
|
+
onClick: () => setTabFilter("all"),
|
|
1796
3562
|
},
|
|
1797
|
-
|
|
3563
|
+
`${t("filterAll")} (${allModels.length})`
|
|
1798
3564
|
),
|
|
1799
3565
|
h(
|
|
1800
3566
|
"button",
|
|
1801
3567
|
{
|
|
1802
3568
|
type: "button",
|
|
1803
|
-
className: "dcpa-
|
|
1804
|
-
|
|
1805
|
-
onClick: () =>
|
|
3569
|
+
className: "dcpa-seg-btn",
|
|
3570
|
+
"data-active": tabFilter === "selected" ? "true" : "false",
|
|
3571
|
+
onClick: () => setTabFilter("selected"),
|
|
1806
3572
|
},
|
|
1807
|
-
|
|
1808
|
-
)
|
|
1809
|
-
)
|
|
1810
|
-
),
|
|
1811
|
-
h(
|
|
1812
|
-
"div",
|
|
1813
|
-
{ className: "dcpa-status-bar" },
|
|
1814
|
-
h(
|
|
1815
|
-
"span",
|
|
1816
|
-
{ className: "dcpa-status-pill" },
|
|
1817
|
-
h("span", { className: dotClass }),
|
|
1818
|
-
stateLabel
|
|
1819
|
-
),
|
|
1820
|
-
h(
|
|
1821
|
-
"span",
|
|
1822
|
-
{ className: "dcpa-meta-item" },
|
|
1823
|
-
`${t("endpointLabel")}:`,
|
|
3573
|
+
`${t("filterSelected")} (${selectedIds.size})`
|
|
3574
|
+
),
|
|
1824
3575
|
h(
|
|
1825
|
-
"
|
|
1826
|
-
{
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
`${t("modelsCountLabel")}:`,
|
|
1834
|
-
h(
|
|
1835
|
-
"span",
|
|
1836
|
-
{ className: "dcpa-meta-val" },
|
|
1837
|
-
`${st.modelCount ?? 0} / ${st.totalDiscovered ?? allModels.length}`
|
|
1838
|
-
)
|
|
1839
|
-
)
|
|
1840
|
-
),
|
|
1841
|
-
account.config?.enabled === false ? h("p", { className: "dcpa-hint" }, t("disabledHint")) : null,
|
|
1842
|
-
st.error ? h("p", { className: "dcpa-error", role: "alert" }, st.error) : null
|
|
1843
|
-
),
|
|
1844
|
-
|
|
1845
|
-
// 2. Interactive Model Selection Filter Card
|
|
1846
|
-
h(
|
|
1847
|
-
"div",
|
|
1848
|
-
{ className: "dcpa-card" },
|
|
1849
|
-
h(
|
|
1850
|
-
"div",
|
|
1851
|
-
{ className: "dcpa-head" },
|
|
1852
|
-
h(
|
|
1853
|
-
"div",
|
|
1854
|
-
null,
|
|
1855
|
-
h(
|
|
1856
|
-
"h4",
|
|
1857
|
-
{ className: "dcpa-section-title" },
|
|
1858
|
-
`${t("sectionModels")} (${selectedIds.size} / ${allModels.length})`
|
|
1859
|
-
),
|
|
1860
|
-
h("p", { className: "dcpa-hint" }, t("sectionModelsHint"))
|
|
1861
|
-
),
|
|
1862
|
-
h(
|
|
1863
|
-
"div",
|
|
1864
|
-
{ className: "dcpa-actions" },
|
|
1865
|
-
selectionDirty
|
|
1866
|
-
? h("span", { className: "dcpa-badge dcpa-badge-warn" }, t("unsavedChanges"))
|
|
1867
|
-
: null,
|
|
1868
|
-
h(
|
|
1869
|
-
"button",
|
|
1870
|
-
{
|
|
1871
|
-
type: "button",
|
|
1872
|
-
className: "dcpa-btn dcpa-btn-primary dcpa-btn-sm",
|
|
1873
|
-
disabled: saving || syncing,
|
|
1874
|
-
onClick: () => onSave(false),
|
|
1875
|
-
},
|
|
1876
|
-
saving ? t("savingBtn") : t("saveSelectionBtn")
|
|
1877
|
-
)
|
|
1878
|
-
)
|
|
1879
|
-
),
|
|
1880
|
-
|
|
1881
|
-
// Search + Segmented Filter + Batch Actions
|
|
1882
|
-
h(
|
|
1883
|
-
"div",
|
|
1884
|
-
{ className: "dcpa-toolbar" },
|
|
1885
|
-
h(
|
|
1886
|
-
"div",
|
|
1887
|
-
{ className: "dcpa-seg" },
|
|
1888
|
-
h(
|
|
1889
|
-
"button",
|
|
1890
|
-
{
|
|
1891
|
-
type: "button",
|
|
1892
|
-
className: "dcpa-seg-btn",
|
|
1893
|
-
"data-active": tabFilter === "all" ? "true" : "false",
|
|
1894
|
-
onClick: () => setTabFilter("all"),
|
|
1895
|
-
},
|
|
1896
|
-
`${t("filterAll")} (${allModels.length})`
|
|
1897
|
-
),
|
|
1898
|
-
h(
|
|
1899
|
-
"button",
|
|
1900
|
-
{
|
|
1901
|
-
type: "button",
|
|
1902
|
-
className: "dcpa-seg-btn",
|
|
1903
|
-
"data-active": tabFilter === "selected" ? "true" : "false",
|
|
1904
|
-
onClick: () => setTabFilter("selected"),
|
|
1905
|
-
},
|
|
1906
|
-
`${t("filterSelected")} (${selectedIds.size})`
|
|
1907
|
-
),
|
|
1908
|
-
h(
|
|
1909
|
-
"button",
|
|
1910
|
-
{
|
|
1911
|
-
type: "button",
|
|
1912
|
-
className: "dcpa-seg-btn",
|
|
1913
|
-
"data-active": tabFilter === "unselected" ? "true" : "false",
|
|
1914
|
-
onClick: () => setTabFilter("unselected"),
|
|
1915
|
-
},
|
|
1916
|
-
`${t("filterUnselected")} (${Math.max(0, allModels.length - selectedIds.size)})`
|
|
3576
|
+
"button",
|
|
3577
|
+
{
|
|
3578
|
+
type: "button",
|
|
3579
|
+
className: "dcpa-seg-btn",
|
|
3580
|
+
"data-active": tabFilter === "unselected" ? "true" : "false",
|
|
3581
|
+
onClick: () => setTabFilter("unselected"),
|
|
3582
|
+
},
|
|
3583
|
+
`${t("filterUnselected")} (${Math.max(0, allModels.length - selectedIds.size)})`
|
|
1917
3584
|
)
|
|
1918
3585
|
),
|
|
1919
3586
|
h(
|
|
@@ -1943,11 +3610,11 @@ window.__ModuleLoader__.load({
|
|
|
1943
3610
|
type: "button",
|
|
1944
3611
|
className: "dcpa-btn dcpa-btn-sm",
|
|
1945
3612
|
disabled: recommended.length === 0,
|
|
1946
|
-
title: t("selectRecommendedHint"),
|
|
1947
3613
|
onClick: resetRecommended,
|
|
1948
3614
|
},
|
|
1949
3615
|
`${t("selectRecommendedBtn")} (${recommended.length})`
|
|
1950
|
-
)
|
|
3616
|
+
),
|
|
3617
|
+
h(InfoTip, { t, text: t("selectRecommendedHint") })
|
|
1951
3618
|
)
|
|
1952
3619
|
),
|
|
1953
3620
|
|
|
@@ -2205,7 +3872,7 @@ window.__ModuleLoader__.load({
|
|
|
2205
3872
|
`${t("modelProtocolHint")}${protocolName(m.nativeProtocol ?? "openai-completions")}`
|
|
2206
3873
|
)
|
|
2207
3874
|
),
|
|
2208
|
-
h("
|
|
3875
|
+
h("div", { className: "dcpa-q-sub" }, t("paramsHintShort"), h(InfoTip, { t, text: t("paramsHint") })),
|
|
2209
3876
|
hasOverride
|
|
2210
3877
|
? h(
|
|
2211
3878
|
"div",
|
|
@@ -2226,22 +3893,30 @@ window.__ModuleLoader__.load({
|
|
|
2226
3893
|
);
|
|
2227
3894
|
})
|
|
2228
3895
|
)
|
|
2229
|
-
|
|
3896
|
+
);
|
|
2230
3897
|
|
|
2231
|
-
|
|
3898
|
+
return h(
|
|
3899
|
+
"div",
|
|
3900
|
+
{ className: "dcpa-account" },
|
|
2232
3901
|
h(
|
|
2233
3902
|
"div",
|
|
2234
|
-
{ className: "dcpa-
|
|
2235
|
-
h("h4", { className: "dcpa-section-title" }, t("sectionConnection")),
|
|
3903
|
+
{ className: "dcpa-acct-head" },
|
|
2236
3904
|
h(
|
|
2237
3905
|
"div",
|
|
2238
|
-
{ className: "dcpa-
|
|
3906
|
+
{ className: "dcpa-q-row-main" },
|
|
3907
|
+
h("h3", { className: "dcpa-q-h" }, account.config?.displayName || account.id),
|
|
2239
3908
|
h(
|
|
2240
3909
|
"div",
|
|
2241
|
-
|
|
2242
|
-
h("
|
|
2243
|
-
|
|
2244
|
-
)
|
|
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") }),
|
|
2245
3920
|
h(
|
|
2246
3921
|
"button",
|
|
2247
3922
|
{
|
|
@@ -2250,249 +3925,1292 @@ window.__ModuleLoader__.load({
|
|
|
2250
3925
|
className: "dcpa-switch",
|
|
2251
3926
|
"aria-checked": enabled,
|
|
2252
3927
|
"aria-label": t("enabledLabel"),
|
|
2253
|
-
|
|
3928
|
+
disabled: busy !== null,
|
|
3929
|
+
onClick: () => void actions.patch(account.id, { enabled: !enabled }, enabled ? t("disabledToast") : t("enabledToast")),
|
|
2254
3930
|
},
|
|
2255
3931
|
h("span", { className: "dcpa-switch-thumb" })
|
|
2256
|
-
)
|
|
2257
|
-
),
|
|
2258
|
-
h(
|
|
2259
|
-
"div",
|
|
2260
|
-
{ className: "dcpa-field" },
|
|
2261
|
-
h("label", { className: "dcpa-label" }, t("baseUrlLabel")),
|
|
2262
|
-
h("input", {
|
|
2263
|
-
type: "text",
|
|
2264
|
-
className: "dcpa-input",
|
|
2265
|
-
value: baseURL,
|
|
2266
|
-
placeholder: effectiveUrl || t("baseUrlPlaceholder"),
|
|
2267
|
-
onChange: (e) => setBaseURL(e.target.value),
|
|
2268
|
-
}),
|
|
2269
|
-
h("p", { className: "dcpa-hint" }, t("baseUrlHint"))
|
|
2270
|
-
),
|
|
2271
|
-
h(
|
|
2272
|
-
"div",
|
|
2273
|
-
{ className: "dcpa-field" },
|
|
2274
|
-
h(
|
|
2275
|
-
"div",
|
|
2276
|
-
{ className: "dcpa-label-row" },
|
|
2277
|
-
h("label", { className: "dcpa-label" }, `${t("apiKeyLabel")} (${cred.ref || apiKeyEnv})`),
|
|
2278
|
-
h(
|
|
2279
|
-
"span",
|
|
2280
|
-
{ className: cred.configured ? "dcpa-badge dcpa-badge-ok" : "dcpa-badge" },
|
|
2281
|
-
cred.configured ? t("apiKeyConfiguredBadge") : t("apiKeyUnsetBadge")
|
|
2282
|
-
)
|
|
2283
3932
|
),
|
|
2284
|
-
h("input", {
|
|
2285
|
-
type: "password",
|
|
2286
|
-
className: "dcpa-input",
|
|
2287
|
-
autoComplete: "new-password",
|
|
2288
|
-
value: apiKeyDraft,
|
|
2289
|
-
placeholder: cred.configured ? t("apiKeySetPlaceholder") : t("apiKeyUnsetPlaceholder"),
|
|
2290
|
-
onChange: (e) => setApiKeyDraft(e.target.value),
|
|
2291
|
-
}),
|
|
2292
|
-
h("p", { className: "dcpa-hint" }, t("apiKeyHint"))
|
|
2293
|
-
),
|
|
2294
|
-
h(
|
|
2295
|
-
"div",
|
|
2296
|
-
{ className: "dcpa-grid" },
|
|
2297
3933
|
h(
|
|
2298
|
-
"
|
|
2299
|
-
{ className: "dcpa-
|
|
2300
|
-
h("label", { className: "dcpa-label" }, t("protocolLabel")),
|
|
3934
|
+
"span",
|
|
3935
|
+
{ className: "dcpa-menu", ref: menuRef },
|
|
2301
3936
|
h(
|
|
2302
|
-
"
|
|
3937
|
+
"button",
|
|
2303
3938
|
{
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
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),
|
|
2307
3945
|
},
|
|
2308
|
-
|
|
2309
|
-
PROTOCOLS.map((p) =>
|
|
2310
|
-
h("option", { key: p, value: p }, `${t("protocolFixedPrefix")}${protocolName(p)}`)
|
|
2311
|
-
)
|
|
3946
|
+
"⋯"
|
|
2312
3947
|
),
|
|
2313
|
-
|
|
2314
|
-
|
|
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
|
|
3983
|
+
)
|
|
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]) =>
|
|
2315
3992
|
h(
|
|
3993
|
+
"button",
|
|
3994
|
+
{
|
|
3995
|
+
key: id,
|
|
3996
|
+
id: `dcpa-tab-${account.id}-${id}`,
|
|
3997
|
+
type: "button",
|
|
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),
|
|
4003
|
+
},
|
|
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(
|
|
2316
4015
|
"div",
|
|
2317
|
-
{ className: "dcpa-
|
|
2318
|
-
h(
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
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
|
|
2326
4049
|
)
|
|
2327
|
-
|
|
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),
|
|
2328
4563
|
h(
|
|
2329
|
-
"
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
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(
|
|
2337
4580
|
"div",
|
|
2338
|
-
{
|
|
2339
|
-
h(
|
|
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(
|
|
2340
4609
|
"div",
|
|
2341
|
-
{ className: "dcpa-
|
|
4610
|
+
{ key: `${m.route}:${m.id}`, className: "dcpa-q-map-row" },
|
|
4611
|
+
h("span", { className: "dcpa-q-map-model" }, m.id),
|
|
2342
4612
|
h(
|
|
2343
|
-
"
|
|
2344
|
-
{ className: "dcpa-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
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
|
|
2354
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"))
|
|
4742
|
+
),
|
|
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(
|
|
4750
|
+
"button",
|
|
4751
|
+
{
|
|
4752
|
+
key: a.accountId,
|
|
4753
|
+
type: "button",
|
|
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
|
+
},
|
|
4764
|
+
},
|
|
4765
|
+
a.displayName || a.accountId,
|
|
4766
|
+
a.keyConfigured ? null : h("span", { className: "dcpa-q-seg-note" }, t("qKeyStatusUnset"))
|
|
4767
|
+
)
|
|
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" },
|
|
2355
4779
|
h(
|
|
2356
4780
|
"div",
|
|
2357
|
-
{ className: "dcpa-
|
|
2358
|
-
h("label", {
|
|
2359
|
-
h("
|
|
2360
|
-
type: "text",
|
|
2361
|
-
className: "dcpa-input",
|
|
2362
|
-
value: excludePrefix,
|
|
2363
|
-
placeholder: t("excludePrefixPlaceholder"),
|
|
2364
|
-
onChange: (e) => setExcludePrefix(e.target.value),
|
|
2365
|
-
}),
|
|
2366
|
-
h("p", { className: "dcpa-hint" }, t("excludePrefixHint"))
|
|
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 ?? "" }) })
|
|
2367
4784
|
)
|
|
2368
4785
|
),
|
|
2369
4786
|
h(
|
|
2370
4787
|
"div",
|
|
2371
|
-
{ className: "dcpa-
|
|
4788
|
+
{ className: "dcpa-q-actions" },
|
|
2372
4789
|
h(
|
|
2373
|
-
"
|
|
2374
|
-
{ className: "dcpa-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
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")),
|
|
2384
4836
|
h(
|
|
2385
4837
|
"div",
|
|
2386
|
-
{ className: "dcpa-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
}),
|
|
2394
|
-
h("p", { className: "dcpa-hint" }, t("apiKeyEnvHint"))
|
|
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")}` : ""
|
|
2395
4845
|
)
|
|
2396
4846
|
),
|
|
2397
4847
|
h(
|
|
2398
4848
|
"div",
|
|
2399
|
-
{ className: "dcpa-
|
|
4849
|
+
{ className: "dcpa-q-actions" },
|
|
2400
4850
|
h(
|
|
2401
|
-
"
|
|
2402
|
-
{ className: "dcpa-
|
|
2403
|
-
|
|
2404
|
-
h("input", {
|
|
2405
|
-
type: "number",
|
|
2406
|
-
className: "dcpa-input",
|
|
2407
|
-
value: refreshIntervalSeconds,
|
|
2408
|
-
onChange: (e) => setRefreshIntervalSeconds(e.target.value),
|
|
2409
|
-
})
|
|
4851
|
+
"button",
|
|
4852
|
+
{ type: "button", className: "dcpa-q-btn", "data-variant": "outline", disabled: busy || refreshing, onClick: () => void store.refresh(account.accountId) },
|
|
4853
|
+
t("qRefresh")
|
|
2410
4854
|
)
|
|
2411
4855
|
)
|
|
2412
4856
|
)
|
|
2413
|
-
|
|
2414
|
-
|
|
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(
|
|
2415
4999
|
"div",
|
|
2416
|
-
{
|
|
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
|
+
},
|
|
2417
5006
|
h(
|
|
2418
|
-
"
|
|
2419
|
-
{
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
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
|
+
)
|
|
2426
5031
|
),
|
|
2427
|
-
cred.configured
|
|
2428
|
-
? h(
|
|
2429
|
-
"button",
|
|
2430
|
-
{
|
|
2431
|
-
type: "button",
|
|
2432
|
-
className: "dcpa-btn",
|
|
2433
|
-
disabled: saving || syncing,
|
|
2434
|
-
onClick: () => onSave(true),
|
|
2435
|
-
},
|
|
2436
|
-
t("clearKeyBtn")
|
|
2437
|
-
)
|
|
2438
|
-
: null,
|
|
2439
|
-
h("span", { style: { flex: 1 } }),
|
|
2440
5032
|
h(
|
|
2441
|
-
"
|
|
2442
|
-
{
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
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
|
+
)
|
|
2449
5046
|
)
|
|
2450
5047
|
)
|
|
2451
|
-
|
|
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
|
|
2452
5070
|
);
|
|
2453
5071
|
}
|
|
2454
5072
|
|
|
5073
|
+
// #endregion
|
|
5074
|
+
|
|
2455
5075
|
const inject = ["slots", "locale"];
|
|
2456
5076
|
|
|
2457
5077
|
function apply(ctx) {
|
|
2458
5078
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-cliproxyapi: dictionaries");
|
|
2459
5079
|
const t = ctx.locale.bind(NS);
|
|
2460
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.
|
|
2461
5084
|
if (typeof document !== "undefined") {
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
if (!styleEl) {
|
|
2465
|
-
styleEl = document.createElement("style");
|
|
2466
|
-
styleEl.id = styleId;
|
|
5085
|
+
ctx.effect(() => {
|
|
5086
|
+
const styleEl = document.createElement("style");
|
|
2467
5087
|
styleEl.dataset.plugin = MODULE_ID;
|
|
2468
5088
|
styleEl.textContent = CSS;
|
|
2469
5089
|
document.head.appendChild(styleEl);
|
|
2470
|
-
|
|
2471
|
-
|
|
5090
|
+
return () => styleEl.remove();
|
|
5091
|
+
}, "dsh-cliproxyapi: styles");
|
|
5092
|
+
}
|
|
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
|
+
}
|
|
2472
5108
|
}
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
5109
|
+
};
|
|
5110
|
+
|
|
5111
|
+
// dshmarket installation client, shared by the card and the opt-in
|
|
5112
|
+
// background auto-install runner. No DOM writes; timers and requests
|
|
5113
|
+
// are disposed with this activation (HMR/unmount).
|
|
5114
|
+
ctx.effect(() => {
|
|
5115
|
+
const market = createMarketUpdater();
|
|
5116
|
+
sharedMarket = market;
|
|
5117
|
+
const stopAutoInstall = startAutoInstall(market);
|
|
5118
|
+
return () => {
|
|
5119
|
+
stopAutoInstall();
|
|
5120
|
+
market.dispose();
|
|
5121
|
+
if (sharedMarket === market) sharedMarket = null;
|
|
5122
|
+
};
|
|
5123
|
+
}, "dsh-cliproxyapi: market updates");
|
|
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
|
+
});
|
|
2479
5178
|
}
|
|
2480
5179
|
|
|
2481
|
-
// 1.
|
|
5180
|
+
// 1. Accounts and models: a page of its own in the Settings dialog's left
|
|
5181
|
+
// navigation (like dshmarket's "Market" section). The slot owner gives
|
|
5182
|
+
// unregistered ids the default nav glyph; no DOM is touched here.
|
|
5183
|
+
ctx.slots.inject("settings.section", () =>
|
|
5184
|
+
registerListEntry(
|
|
5185
|
+
ctx.slots,
|
|
5186
|
+
{
|
|
5187
|
+
name: "settings.section",
|
|
5188
|
+
id: SETTINGS_SECTION_ID,
|
|
5189
|
+
order: 45,
|
|
5190
|
+
label: () => t("nav"),
|
|
5191
|
+
locale: NS,
|
|
5192
|
+
},
|
|
5193
|
+
() => h(CpaSectionPage, { t })
|
|
5194
|
+
)
|
|
5195
|
+
);
|
|
5196
|
+
|
|
5197
|
+
// 2. Plugin updates only, on the bundle page in Sidebar -> Plugins.
|
|
2482
5198
|
ctx.slots.inject("plugins.bundle.config", () =>
|
|
2483
|
-
|
|
5199
|
+
registerListEntry(
|
|
5200
|
+
ctx.slots,
|
|
2484
5201
|
{
|
|
2485
5202
|
name: "plugins.bundle.config",
|
|
2486
5203
|
key: MODULE_ID,
|
|
2487
5204
|
locale: NS,
|
|
2488
5205
|
},
|
|
2489
|
-
(ownerProps = {}) => (ownerProps.view === "summary" ? null : h(
|
|
5206
|
+
(ownerProps = {}) => (ownerProps.view === "summary" ? null : h(PluginUpdatePage, { t }))
|
|
2490
5207
|
)
|
|
2491
5208
|
);
|
|
2492
5209
|
|
|
2493
|
-
//
|
|
5210
|
+
// 3. Plugin updates only, as a tab in Settings -> Plugins.
|
|
2494
5211
|
ctx.slots.inject("settings.plugins.tab", () =>
|
|
2495
|
-
|
|
5212
|
+
registerListEntry(
|
|
5213
|
+
ctx.slots,
|
|
2496
5214
|
{
|
|
2497
5215
|
name: "settings.plugins.tab",
|
|
2498
5216
|
id: NS,
|
|
@@ -2500,7 +5218,7 @@ window.__ModuleLoader__.load({
|
|
|
2500
5218
|
label: () => t("nav"),
|
|
2501
5219
|
locale: NS,
|
|
2502
5220
|
},
|
|
2503
|
-
() => h(
|
|
5221
|
+
() => h(PluginUpdatePage, { t })
|
|
2504
5222
|
)
|
|
2505
5223
|
);
|
|
2506
5224
|
}
|