dsh-cliproxyapi-pro 1.5.0 → 1.6.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 +17 -0
- package/README.en.md +32 -17
- package/README.md +32 -17
- package/lib/client.js +634 -22
- package/lib/index.js +24 -9
- package/lib/updater.js +6 -3
- package/package.json +1 -1
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 = {
|
|
@@ -26,6 +28,17 @@ window.__ModuleLoader__.load({
|
|
|
26
28
|
updateDownload: "api/dsh-cliproxyapi/updates/download",
|
|
27
29
|
};
|
|
28
30
|
|
|
31
|
+
// Public, versioned dshmarket Update API (UPDATE-API-V1.md in dshmarket).
|
|
32
|
+
// Installation is delegated to the market; this plugin never runs a
|
|
33
|
+
// package manager. Paths are document-relative like API above.
|
|
34
|
+
const MARKET_API = {
|
|
35
|
+
capabilities: "dsh-market/api/v1/capabilities",
|
|
36
|
+
updates: "dsh-market/api/v1/updates",
|
|
37
|
+
operations: "dsh-market/api/v1/operations",
|
|
38
|
+
restart: "dsh-market/api/v1/restart",
|
|
39
|
+
};
|
|
40
|
+
const MARKET_SCHEMA = "dsh-market/update-api/v1";
|
|
41
|
+
|
|
29
42
|
const zh = {
|
|
30
43
|
nav: "CLIProxyAPI",
|
|
31
44
|
title: "CLIProxyAPI 自动发现与模型筛选",
|
|
@@ -123,6 +136,7 @@ window.__ModuleLoader__.load({
|
|
|
123
136
|
paramsHint: "留空并保存即恢复 CLIProxyAPI 自动探测值。修改后请点击「保存模型勾选」才会写入模型选择器。",
|
|
124
137
|
discoveredValue: "探测值",
|
|
125
138
|
updateTitle: "插件更新",
|
|
139
|
+
updatePageHint: "这里只管理插件自身的更新。CLIProxyAPI 的账号、模型和协议设置位于「设置」左侧导航的「CLIProxyAPI」页。",
|
|
126
140
|
updateToggleDetails: "展开或收起插件更新设置",
|
|
127
141
|
updateCompactCurrent: "当前 {version}",
|
|
128
142
|
updateCompactAvailable: "有新版本 {version}",
|
|
@@ -131,7 +145,9 @@ window.__ModuleLoader__.load({
|
|
|
131
145
|
updateCompactStatusUnavailable: "更新服务未就绪",
|
|
132
146
|
updateCompactErrorNetwork: "网络异常",
|
|
133
147
|
updateCompactErrorChannel: "通道未发布",
|
|
134
|
-
updateStatusUnavailableHint: "
|
|
148
|
+
updateStatusUnavailableHint: "无法读取更新服务,可能是插件宿主代码尚未重载:重启 DSH(仅刷新页面不够),或检查更新接口是否可用。",
|
|
149
|
+
updateServiceUnavailableError: "更新服务未就绪:当前运行的插件宿主代码仍是旧版,可能尚未重启 DSH。重启 DSH 后重试。",
|
|
150
|
+
updateOriginError: "请求来源校验未通过,更新操作已拒绝。请从 DSH 打开的页面重试;使用反向代理时请保持与页面一致的地址访问。",
|
|
135
151
|
updateSubtitle: "仅更新 CLIProxyAPI 插件自身,不更新 CPA 服务或 DSH 宿主。设置修改后立即保存。",
|
|
136
152
|
updateNewVersion: "新版本",
|
|
137
153
|
updateVersionSummary: "当前版本 {current} · 最新版本 {latest}",
|
|
@@ -194,7 +210,51 @@ window.__ModuleLoader__.load({
|
|
|
194
210
|
updateDownloadPathLabel: "已校验的离线包",
|
|
195
211
|
updateUpgradeCommandLabel: "关闭 DSH 后执行升级命令",
|
|
196
212
|
updateCommandUnavailable: "未生成升级命令。请先确认 DSH 支持的离线包升级方式;升级前关闭 DSH,完成后重新启动。",
|
|
197
|
-
updateOfflineHint: "
|
|
213
|
+
updateOfflineHint: "离线包下载不会自动安装。本插件自身不会运行包管理器、关闭或重启 DSH;一键更新和自动安装都交给 DSH 插件市场(dshmarket)完成。",
|
|
214
|
+
updateCompactInstalled: "{version} 已安装",
|
|
215
|
+
updateMarketLabel: "通过插件市场更新",
|
|
216
|
+
updateMarketDetecting: "正在检测 DSH 插件市场…",
|
|
217
|
+
updateMarketReadyHint: "已检测到 DSH 插件市场(dshmarket),可一键安装新版本。安装和版本解析都由插件市场完成,可能需要重启 DSH 或刷新页面才能生效。",
|
|
218
|
+
updateMarketUnavailableHint: "未检测到 DSH 插件市场(dshmarket)的更新接口。安装插件市场后即可在此一键更新或自动更新;目前可使用下方的离线包下载。",
|
|
219
|
+
updateMarketLinkedHint: "当前为本地链接安装,不通过插件市场安装,以免替换源码链接。",
|
|
220
|
+
updateMarketInstallBtn: "立即更新",
|
|
221
|
+
updateMarketInstalledBtn: "已安装",
|
|
222
|
+
updateMarketInstalling: "正在通过插件市场安装…",
|
|
223
|
+
updateMarketProgress: "正在通过插件市场安装… {percent}%",
|
|
224
|
+
updateMarketRedetectBtn: "重新检测",
|
|
225
|
+
updateMarketConfirmTitle: "通过插件市场安装版本 {version}?",
|
|
226
|
+
updateMarketConfirmHint: "DSH 插件市场会安装本插件的新版本,并在安装后核对实际装上的版本,不一致时恢复原版本。安装期间请勿关闭 DSH;完成后可能需要重启 DSH 或刷新页面。",
|
|
227
|
+
updateMarketConfirmBtn: "确认安装",
|
|
228
|
+
updateMarketAutomaticPrefix: "自动更新:",
|
|
229
|
+
updateMarketInstalled: "已安装版本 {version}。",
|
|
230
|
+
updateMarketVersionLower: "插件市场安装的是 {installed},低于检查到的 {target}:插件市场按其自身的通道与镜像解析版本。",
|
|
231
|
+
updateMarketRestartHint: "重启 DSH 后新版本生效。",
|
|
232
|
+
updateMarketRestartDesktopHint: "请完全退出 DSH 桌面版并重新打开,新版本才会生效。",
|
|
233
|
+
updateMarketRestartManualHint: "请重启 DSH 服务,新版本才会生效。",
|
|
234
|
+
updateMarketRefreshHint: "刷新页面后新版本界面生效。",
|
|
235
|
+
updateMarketAppliedHint: "新版本已生效;若界面仍是旧版,请刷新页面。",
|
|
236
|
+
updateMarketRestartBtn: "重启 DSH",
|
|
237
|
+
updateMarketRestarting: "正在请求重启…",
|
|
238
|
+
updateMarketRestartRequested: "已请求重启 DSH,页面将在 DSH 重新启动后恢复。",
|
|
239
|
+
updateMarketRefreshBtn: "刷新页面",
|
|
240
|
+
updateMarketUnchanged: "插件市场报告完成,但插件版本没有变化。",
|
|
241
|
+
updateMarketCancelled: "插件市场的安装任务已取消。",
|
|
242
|
+
updateMarketRolledBack: "插件市场已回滚此次更新,插件保持原版本。",
|
|
243
|
+
updateMarketForceBtn: "仍然安装",
|
|
244
|
+
updateMarketBusy: "插件市场正在执行另一项更新,请稍后重试。",
|
|
245
|
+
updateMarketAgentsRunning: "有智能体正在运行,插件市场暂不安装。请等待任务结束后重试。",
|
|
246
|
+
updateMarketTooFresh: "新版本刚发布,仍在插件市场的发布冷却期内。可稍后重试,或选择「仍然安装」。",
|
|
247
|
+
updateMarketVersionUnchanged: "插件市场没有解析到更新的版本(镜像可能尚未同步)。可稍后重试,或选择「仍然安装」。",
|
|
248
|
+
updateMarketDowngrade: "插件市场解析到的版本低于当前版本,已自动恢复,未做更改。",
|
|
249
|
+
updateMarketMismatch: "插件市场安装的版本与目标版本不一致,已自动恢复。请等待镜像同步后重试。",
|
|
250
|
+
updateMarketTimeout: "插件市场安装超时,请稍后重试。",
|
|
251
|
+
updateMarketForbidden: "插件市场拒绝了此次更新请求,请从 DSH 打开的页面重试。",
|
|
252
|
+
updateMarketLost: "无法继续跟踪插件市场的安装任务(DSH 可能已重启)。请重新检查更新以确认当前版本。",
|
|
253
|
+
updateMarketUnreachable: "无法连接 DSH 插件市场的更新接口,请确认 dshmarket 已启用后重试。",
|
|
254
|
+
updateMarketRestartFailed: "重启请求失败,请手动重启 DSH。",
|
|
255
|
+
updateMarketFailed: "插件市场安装失败。可在插件市场中查看详情,稍后重试,或使用离线包下载。",
|
|
256
|
+
updateAutoInstallLabel: "自动安装更新",
|
|
257
|
+
updateAutoInstallHint: "自动检查发现新版本后,通过 DSH 插件市场自动安装(需开启自动检查并安装 dshmarket,仅在 DSH 页面打开时进行;同一版本 6 小时内最多尝试一次)。生效可能需要重启 DSH 或刷新页面。",
|
|
198
258
|
};
|
|
199
259
|
|
|
200
260
|
const en = {
|
|
@@ -294,6 +354,7 @@ window.__ModuleLoader__.load({
|
|
|
294
354
|
paramsHint: "Clear a value and save to restore the value discovered from CLIProxyAPI. Click Apply Selected Models to write changes to the picker.",
|
|
295
355
|
discoveredValue: "Discovered",
|
|
296
356
|
updateTitle: "Plugin Updates",
|
|
357
|
+
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.",
|
|
297
358
|
updateToggleDetails: "Expand or collapse plugin update settings",
|
|
298
359
|
updateCompactCurrent: "Current {version}",
|
|
299
360
|
updateCompactAvailable: "Update {version}",
|
|
@@ -302,7 +363,9 @@ window.__ModuleLoader__.load({
|
|
|
302
363
|
updateCompactStatusUnavailable: "Update service unavailable",
|
|
303
364
|
updateCompactErrorNetwork: "Network error",
|
|
304
365
|
updateCompactErrorChannel: "Channel unpublished",
|
|
305
|
-
updateStatusUnavailableHint: "Could not reach the update service; the host plugin code may
|
|
366
|
+
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.",
|
|
367
|
+
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.",
|
|
368
|
+
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).",
|
|
306
369
|
updateSubtitle: "Updates this CLIProxyAPI plugin only, not the CPA server or DSH. Changes to update settings are saved immediately.",
|
|
307
370
|
updateNewVersion: "New Version",
|
|
308
371
|
updateVersionSummary: "Current {current} · Latest {latest}",
|
|
@@ -365,7 +428,51 @@ window.__ModuleLoader__.load({
|
|
|
365
428
|
updateDownloadPathLabel: "Verified Offline Package",
|
|
366
429
|
updateUpgradeCommandLabel: "Upgrade Command — Run After Closing DSH",
|
|
367
430
|
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.",
|
|
368
|
-
updateOfflineHint: "
|
|
431
|
+
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).",
|
|
432
|
+
updateCompactInstalled: "{version} installed",
|
|
433
|
+
updateMarketLabel: "Update Through Plugin Market",
|
|
434
|
+
updateMarketDetecting: "Detecting the DSH plugin market…",
|
|
435
|
+
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.",
|
|
436
|
+
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.",
|
|
437
|
+
updateMarketLinkedHint: "This is a linked local installation, so it is not installed through the plugin market, which would replace the source link.",
|
|
438
|
+
updateMarketInstallBtn: "Update Now",
|
|
439
|
+
updateMarketInstalledBtn: "Installed",
|
|
440
|
+
updateMarketInstalling: "Installing through the plugin market…",
|
|
441
|
+
updateMarketProgress: "Installing through the plugin market… {percent}%",
|
|
442
|
+
updateMarketRedetectBtn: "Detect Again",
|
|
443
|
+
updateMarketConfirmTitle: "Install version {version} through the plugin market?",
|
|
444
|
+
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.",
|
|
445
|
+
updateMarketConfirmBtn: "Confirm Install",
|
|
446
|
+
updateMarketAutomaticPrefix: "Automatic update:",
|
|
447
|
+
updateMarketInstalled: "Version {version} is installed.",
|
|
448
|
+
updateMarketVersionLower: "The plugin market installed {installed}, older than the checked {target}: the market resolves versions with its own channel and mirror.",
|
|
449
|
+
updateMarketRestartHint: "Restart DSH for the new version to take effect.",
|
|
450
|
+
updateMarketRestartDesktopHint: "Quit the DSH desktop app completely and reopen it for the new version to take effect.",
|
|
451
|
+
updateMarketRestartManualHint: "Restart the DSH service for the new version to take effect.",
|
|
452
|
+
updateMarketRefreshHint: "Refresh the page to load the new interface.",
|
|
453
|
+
updateMarketAppliedHint: "The new version is active; refresh the page if the interface still looks old.",
|
|
454
|
+
updateMarketRestartBtn: "Restart DSH",
|
|
455
|
+
updateMarketRestarting: "Requesting restart…",
|
|
456
|
+
updateMarketRestartRequested: "A DSH restart was requested. The page recovers once DSH is back.",
|
|
457
|
+
updateMarketRefreshBtn: "Refresh Page",
|
|
458
|
+
updateMarketUnchanged: "The plugin market reported success, but the plugin version did not change.",
|
|
459
|
+
updateMarketCancelled: "The plugin market cancelled the installation.",
|
|
460
|
+
updateMarketRolledBack: "The plugin market rolled this update back; the previous version is kept.",
|
|
461
|
+
updateMarketForceBtn: "Install Anyway",
|
|
462
|
+
updateMarketBusy: "The plugin market is running another update. Retry shortly.",
|
|
463
|
+
updateMarketAgentsRunning: "Agents are running, so the plugin market will not install now. Retry when they finish.",
|
|
464
|
+
updateMarketTooFresh: "This release is still inside the plugin market's release-age wait. Retry later, or choose Install Anyway.",
|
|
465
|
+
updateMarketVersionUnchanged: "The plugin market did not resolve a newer version (its mirror may still be syncing). Retry later, or choose Install Anyway.",
|
|
466
|
+
updateMarketDowngrade: "The plugin market resolved a version older than the current one and restored it; nothing changed.",
|
|
467
|
+
updateMarketMismatch: "The plugin market installed a version other than the target and restored the previous one. Retry after the mirror catches up.",
|
|
468
|
+
updateMarketTimeout: "The plugin market installation timed out. Retry later.",
|
|
469
|
+
updateMarketForbidden: "The plugin market refused this update request. Retry from the page DSH opened.",
|
|
470
|
+
updateMarketLost: "The plugin market operation can no longer be followed (DSH may have restarted). Check for updates again to confirm the current version.",
|
|
471
|
+
updateMarketUnreachable: "Could not reach the DSH plugin market update API. Make sure dshmarket is enabled and retry.",
|
|
472
|
+
updateMarketRestartFailed: "The restart request failed. Restart DSH manually.",
|
|
473
|
+
updateMarketFailed: "The plugin market installation failed. See the plugin market for details, retry later, or use the offline package download.",
|
|
474
|
+
updateAutoInstallLabel: "Automatically Install Updates",
|
|
475
|
+
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.",
|
|
369
476
|
};
|
|
370
477
|
|
|
371
478
|
const CSS = `
|
|
@@ -856,7 +963,9 @@ window.__ModuleLoader__.load({
|
|
|
856
963
|
.dcpa-update-summary-current { flex: 0 1 auto; }
|
|
857
964
|
.dcpa-update-summary-status { flex: 1; text-align: right; }
|
|
858
965
|
.dcpa-update-summary-status[data-state="available"],
|
|
859
|
-
.dcpa-update-summary-status[data-state="downloaded"]
|
|
966
|
+
.dcpa-update-summary-status[data-state="downloaded"],
|
|
967
|
+
.dcpa-update-summary-status[data-state="installing"],
|
|
968
|
+
.dcpa-update-summary-status[data-state="installed"] { color: var(--dsw-alias-brand-primary); }
|
|
860
969
|
.dcpa-update-summary-status[data-state="error"] { color: var(--dsw-alias-state-error-primary); }
|
|
861
970
|
.dcpa-update-summary::after {
|
|
862
971
|
content: "";
|
|
@@ -985,10 +1094,13 @@ window.__ModuleLoader__.load({
|
|
|
985
1094
|
: "dcpa-dot dcpa-dot-idle";
|
|
986
1095
|
}
|
|
987
1096
|
|
|
1097
|
+
const SERVICE_UNAVAILABLE_STATUS = new Set([404, 405, 501]);
|
|
988
1098
|
const UPDATE_ERROR_KEYS = {
|
|
989
1099
|
"network-error": "updateNetworkError",
|
|
990
1100
|
"invalid-metadata": "updateMetadataError",
|
|
991
1101
|
"invalid-response": "updateMetadataError",
|
|
1102
|
+
"service-unavailable": "updateServiceUnavailableError",
|
|
1103
|
+
"forbidden-origin": "updateOriginError",
|
|
992
1104
|
"channel-unavailable": "updateChannelUnavailable",
|
|
993
1105
|
"unavailable-channel": "updateChannelUnavailable",
|
|
994
1106
|
"integrity-error": "updateIntegrityError",
|
|
@@ -1039,7 +1151,9 @@ window.__ModuleLoader__.load({
|
|
|
1039
1151
|
body = await res.json();
|
|
1040
1152
|
} catch {
|
|
1041
1153
|
const failure = new Error("Invalid update response");
|
|
1042
|
-
|
|
1154
|
+
// A missing route (host plugin not reloaded) answers 404/405 with no
|
|
1155
|
+
// update payload; that is "service unavailable", not bad metadata.
|
|
1156
|
+
failure.updateCode = !res.ok && SERVICE_UNAVAILABLE_STATUS.has(res.status) ? "service-unavailable" : "invalid-response";
|
|
1043
1157
|
if (!res.ok) failure.httpStatus = res.status;
|
|
1044
1158
|
throw failure;
|
|
1045
1159
|
}
|
|
@@ -1047,7 +1161,8 @@ window.__ModuleLoader__.load({
|
|
|
1047
1161
|
if (!res.ok || body?.ok === false || !validState ||
|
|
1048
1162
|
payload !== undefined && (body.update.error || body.update.errorCode)) {
|
|
1049
1163
|
const failure = new Error("Update request failed");
|
|
1050
|
-
failure.updateCode = body?.errorCode || body?.update?.errorCode ||
|
|
1164
|
+
failure.updateCode = body?.errorCode || body?.update?.errorCode ||
|
|
1165
|
+
(!validState ? (!res.ok && SERVICE_UNAVAILABLE_STATUS.has(res.status) ? "service-unavailable" : "invalid-response") : null);
|
|
1051
1166
|
failure.httpStatus = !res.ok ? res.status : null;
|
|
1052
1167
|
failure.updateState = validState ? body.update : null;
|
|
1053
1168
|
throw failure;
|
|
@@ -1055,8 +1170,303 @@ window.__ModuleLoader__.load({
|
|
|
1055
1170
|
return body.update;
|
|
1056
1171
|
}
|
|
1057
1172
|
|
|
1173
|
+
// #region dshmarket installation (Update API v1)
|
|
1174
|
+
|
|
1175
|
+
const MARKET_TERMINAL_STATES = new Set(["succeeded", "failed", "cancelled", "rolled-back"]);
|
|
1176
|
+
const MARKET_POLL_MS = 1500;
|
|
1177
|
+
const MARKET_MAX_POLL_FAILURES = 20;
|
|
1178
|
+
const MARKET_FORCEABLE = new Set(["RELEASE_TOO_FRESH", "VERSION_UNCHANGED"]);
|
|
1179
|
+
const AUTO_INSTALL_FIRST_MS = 15000;
|
|
1180
|
+
const AUTO_INSTALL_INTERVAL_MS = 5 * 60 * 1000;
|
|
1181
|
+
const AUTO_INSTALL_RETRY_MS = 6 * 60 * 60 * 1000;
|
|
1182
|
+
const AUTO_INSTALL_MEMORY_KEY = `${PACKAGE_NAME}:market-auto-install`;
|
|
1183
|
+
const MARKET_ERROR_KEYS = {
|
|
1184
|
+
OPERATION_BUSY: "updateMarketBusy",
|
|
1185
|
+
AGENTS_RUNNING: "updateMarketAgentsRunning",
|
|
1186
|
+
RELEASE_TOO_FRESH: "updateMarketTooFresh",
|
|
1187
|
+
VERSION_UNCHANGED: "updateMarketVersionUnchanged",
|
|
1188
|
+
DOWNGRADE_DETECTED: "updateMarketDowngrade",
|
|
1189
|
+
RESOLVED_VERSION_MISMATCH: "updateMarketMismatch",
|
|
1190
|
+
UPDATE_TIMEOUT: "updateMarketTimeout",
|
|
1191
|
+
UPDATE_FORBIDDEN: "updateMarketForbidden",
|
|
1192
|
+
PLUGIN_NOT_INSTALLED: "updateNotInstalledHint",
|
|
1193
|
+
OPERATION_LOST: "updateMarketLost",
|
|
1194
|
+
MARKET_UNREACHABLE: "updateMarketUnreachable",
|
|
1195
|
+
RESTART_FAILED: "updateMarketRestartFailed",
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
function parseVersion(value) {
|
|
1199
|
+
const match = typeof value === "string"
|
|
1200
|
+
? /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value)
|
|
1201
|
+
: null;
|
|
1202
|
+
return match ? { parts: match.slice(1, 4).map(Number), pre: match[4] ? match[4].split(".") : [] } : null;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/** SemVer precedence, or null when either side is not a version. */
|
|
1206
|
+
function compareVersions(a, b) {
|
|
1207
|
+
const x = parseVersion(a), y = parseVersion(b);
|
|
1208
|
+
if (!x || !y) return null;
|
|
1209
|
+
for (let i = 0; i < 3; i++) if (x.parts[i] !== y.parts[i]) return x.parts[i] > y.parts[i] ? 1 : -1;
|
|
1210
|
+
if (!x.pre.length || !y.pre.length) return x.pre.length ? -1 : y.pre.length ? 1 : 0;
|
|
1211
|
+
for (let i = 0; i < Math.max(x.pre.length, y.pre.length); i++) {
|
|
1212
|
+
const p = x.pre[i], q = y.pre[i];
|
|
1213
|
+
if (p === undefined || q === undefined) return p === undefined ? -1 : 1;
|
|
1214
|
+
if (p === q) continue;
|
|
1215
|
+
const pn = /^\d+$/.test(p), qn = /^\d+$/.test(q);
|
|
1216
|
+
if (pn && qn) return Number(p) > Number(q) ? 1 : -1;
|
|
1217
|
+
if (pn !== qn) return pn ? -1 : 1;
|
|
1218
|
+
return p > q ? 1 : -1;
|
|
1219
|
+
}
|
|
1220
|
+
return 0;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
function validMarketOperation(value) {
|
|
1224
|
+
return !!value && typeof value === "object" && value.schema === MARKET_SCHEMA &&
|
|
1225
|
+
typeof value.operationId === "string" && value.operationId !== "" &&
|
|
1226
|
+
value.packageName === PACKAGE_NAME && typeof value.state === "string";
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const marketFailureCode = (operation) =>
|
|
1230
|
+
typeof operation?.failure?.code === "string" && operation.failure.code ? operation.failure.code : "UPDATE_FAILED";
|
|
1231
|
+
|
|
1232
|
+
function marketErrorText(t, code) {
|
|
1233
|
+
return t(Object.prototype.hasOwnProperty.call(MARKET_ERROR_KEYS, code) ? MARKET_ERROR_KEYS[code] : "updateMarketFailed");
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/**
|
|
1237
|
+
* Shared, page-wide client of the dshmarket Update API. One instance lives
|
|
1238
|
+
* per client activation so the settings card and the background
|
|
1239
|
+
* auto-install runner observe the same operation.
|
|
1240
|
+
*/
|
|
1241
|
+
function createMarketUpdater(io = {}) {
|
|
1242
|
+
const doFetch = io.fetch ?? ((url, init) => fetch(url, init));
|
|
1243
|
+
const later = io.setTimeout ?? ((fn, ms) => setTimeout(fn, ms));
|
|
1244
|
+
const cancelLater = io.clearTimeout ?? ((id) => clearTimeout(id));
|
|
1245
|
+
let state = {
|
|
1246
|
+
phase: "unknown", // unknown | discovering | ready | unavailable
|
|
1247
|
+
runtime: null,
|
|
1248
|
+
restartSupported: false,
|
|
1249
|
+
installing: false,
|
|
1250
|
+
automatic: false,
|
|
1251
|
+
targetVersion: null,
|
|
1252
|
+
operation: null,
|
|
1253
|
+
errorCode: null,
|
|
1254
|
+
restarting: false,
|
|
1255
|
+
restartRequested: false,
|
|
1256
|
+
};
|
|
1257
|
+
const listeners = new Set();
|
|
1258
|
+
const controllers = new Set();
|
|
1259
|
+
let disposed = false;
|
|
1260
|
+
let discovering = null;
|
|
1261
|
+
let pollTimer = null;
|
|
1262
|
+
let pollFailures = 0;
|
|
1263
|
+
|
|
1264
|
+
const set = (patch) => {
|
|
1265
|
+
if (disposed) return;
|
|
1266
|
+
state = { ...state, ...patch };
|
|
1267
|
+
for (const listener of [...listeners]) listener(state);
|
|
1268
|
+
};
|
|
1269
|
+
const request = async (url, init = {}) => {
|
|
1270
|
+
const controller = new AbortController();
|
|
1271
|
+
controllers.add(controller);
|
|
1272
|
+
try {
|
|
1273
|
+
const res = await doFetch(url, { cache: "no-store", ...init, signal: controller.signal });
|
|
1274
|
+
let body = null;
|
|
1275
|
+
try { body = await res.json(); } catch {}
|
|
1276
|
+
return { status: res.status, ok: res.ok, body };
|
|
1277
|
+
} finally {
|
|
1278
|
+
controllers.delete(controller);
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
const post = (url, payload) => request(url, {
|
|
1282
|
+
method: "POST",
|
|
1283
|
+
headers: { "content-type": "application/json" },
|
|
1284
|
+
body: JSON.stringify(payload),
|
|
1285
|
+
});
|
|
1286
|
+
const applyOperation = (operation) => {
|
|
1287
|
+
const done = MARKET_TERMINAL_STATES.has(operation.state);
|
|
1288
|
+
set({
|
|
1289
|
+
operation,
|
|
1290
|
+
installing: !done,
|
|
1291
|
+
errorCode: done && operation.state === "failed" ? marketFailureCode(operation) : null,
|
|
1292
|
+
});
|
|
1293
|
+
if (!done) schedulePoll();
|
|
1294
|
+
};
|
|
1295
|
+
const schedulePoll = () => {
|
|
1296
|
+
if (pollTimer !== null) cancelLater(pollTimer);
|
|
1297
|
+
pollTimer = later(poll, MARKET_POLL_MS);
|
|
1298
|
+
};
|
|
1299
|
+
const poll = async () => {
|
|
1300
|
+
pollTimer = null;
|
|
1301
|
+
const id = state.operation?.operationId;
|
|
1302
|
+
if (disposed || !id) return;
|
|
1303
|
+
try {
|
|
1304
|
+
const { status, body } = await request(`${MARKET_API.operations}?operationId=${encodeURIComponent(id)}`);
|
|
1305
|
+
if (disposed || state.operation?.operationId !== id) return;
|
|
1306
|
+
if (status === 404) {
|
|
1307
|
+
// Operations live in the current Host process only.
|
|
1308
|
+
set({ installing: false, errorCode: "OPERATION_LOST" });
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
if (status !== 200 || !validMarketOperation(body?.operation) || body.operation.operationId !== id) {
|
|
1312
|
+
throw new Error("Invalid market operation");
|
|
1313
|
+
}
|
|
1314
|
+
pollFailures = 0;
|
|
1315
|
+
applyOperation(body.operation);
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
if (disposed || error?.name === "AbortError") return;
|
|
1318
|
+
if (++pollFailures >= MARKET_MAX_POLL_FAILURES) set({ installing: false, errorCode: "OPERATION_LOST" });
|
|
1319
|
+
else schedulePoll();
|
|
1320
|
+
}
|
|
1321
|
+
};
|
|
1322
|
+
|
|
1323
|
+
return {
|
|
1324
|
+
getState: () => state,
|
|
1325
|
+
subscribe(listener) {
|
|
1326
|
+
listeners.add(listener);
|
|
1327
|
+
return () => listeners.delete(listener);
|
|
1328
|
+
},
|
|
1329
|
+
/** Reads only the local capability document; never contacts a registry. */
|
|
1330
|
+
discover() {
|
|
1331
|
+
if (disposed) return Promise.resolve(state);
|
|
1332
|
+
if (discovering) return discovering;
|
|
1333
|
+
if (state.phase !== "ready") set({ phase: "discovering" });
|
|
1334
|
+
discovering = request(MARKET_API.capabilities)
|
|
1335
|
+
.then(({ ok, body }) => {
|
|
1336
|
+
const usable = ok && body?.schema === MARKET_SCHEMA && body.apiVersion === 1 && body.features?.update === true;
|
|
1337
|
+
set({
|
|
1338
|
+
phase: usable ? "ready" : "unavailable",
|
|
1339
|
+
runtime: usable && typeof body.runtime === "string" ? body.runtime : null,
|
|
1340
|
+
restartSupported: usable && body.features?.restart === true && body.restart?.supported === true,
|
|
1341
|
+
});
|
|
1342
|
+
}, () => {
|
|
1343
|
+
set({ phase: "unavailable", runtime: null, restartSupported: false });
|
|
1344
|
+
})
|
|
1345
|
+
.finally(() => { discovering = null; })
|
|
1346
|
+
.then(() => state);
|
|
1347
|
+
return discovering;
|
|
1348
|
+
},
|
|
1349
|
+
/** Starts one market update operation, then follows it until it settles. */
|
|
1350
|
+
async install({ targetVersion = null, force = false, automatic = false } = {}) {
|
|
1351
|
+
if (disposed || state.phase !== "ready" || state.installing) return state;
|
|
1352
|
+
set({ installing: true, automatic, targetVersion, operation: null, errorCode: null, restartRequested: false });
|
|
1353
|
+
try {
|
|
1354
|
+
const { status, body } = await post(
|
|
1355
|
+
MARKET_API.updates,
|
|
1356
|
+
force ? { packageName: PACKAGE_NAME, force: true } : { packageName: PACKAGE_NAME }
|
|
1357
|
+
);
|
|
1358
|
+
if (disposed) return state;
|
|
1359
|
+
if (status === 202 && validMarketOperation(body?.operation)) {
|
|
1360
|
+
pollFailures = 0;
|
|
1361
|
+
applyOperation(body.operation);
|
|
1362
|
+
return state;
|
|
1363
|
+
}
|
|
1364
|
+
const code = typeof body?.failure?.code === "string" && body.failure.code ? body.failure.code
|
|
1365
|
+
: status === 409 ? "OPERATION_BUSY"
|
|
1366
|
+
: status === 403 ? "UPDATE_FORBIDDEN"
|
|
1367
|
+
: status === 400 ? "UPDATE_REJECTED"
|
|
1368
|
+
: status === 404 ? "MARKET_UNREACHABLE"
|
|
1369
|
+
: "UPDATE_FAILED";
|
|
1370
|
+
set({ installing: false, errorCode: code });
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
if (!disposed) set({ installing: false, errorCode: error?.name === "AbortError" ? null : "MARKET_UNREACHABLE" });
|
|
1373
|
+
}
|
|
1374
|
+
return state;
|
|
1375
|
+
},
|
|
1376
|
+
/** Only offered when the market reports that it owns process restarts. */
|
|
1377
|
+
async restart() {
|
|
1378
|
+
if (disposed || !state.restartSupported || state.restarting) return state;
|
|
1379
|
+
set({ restarting: true, errorCode: null });
|
|
1380
|
+
try {
|
|
1381
|
+
const { ok } = await post(MARKET_API.restart, {});
|
|
1382
|
+
set({ restarting: false, restartRequested: ok, errorCode: ok ? null : "RESTART_FAILED" });
|
|
1383
|
+
} catch (error) {
|
|
1384
|
+
set({ restarting: false, errorCode: error?.name === "AbortError" ? null : "RESTART_FAILED" });
|
|
1385
|
+
}
|
|
1386
|
+
return state;
|
|
1387
|
+
},
|
|
1388
|
+
dispose() {
|
|
1389
|
+
disposed = true;
|
|
1390
|
+
if (pollTimer !== null) cancelLater(pollTimer);
|
|
1391
|
+
pollTimer = null;
|
|
1392
|
+
for (const controller of controllers) controller.abort();
|
|
1393
|
+
controllers.clear();
|
|
1394
|
+
listeners.clear();
|
|
1395
|
+
},
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function readAutoInstallMemory() {
|
|
1400
|
+
try {
|
|
1401
|
+
const raw = JSON.parse(globalThis.localStorage?.getItem(AUTO_INSTALL_MEMORY_KEY) ?? "null");
|
|
1402
|
+
return raw && typeof raw.version === "string" && Number.isFinite(raw.at) ? raw : null;
|
|
1403
|
+
} catch {
|
|
1404
|
+
return null;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function writeAutoInstallMemory(record) {
|
|
1409
|
+
try { globalThis.localStorage?.setItem(AUTO_INSTALL_MEMORY_KEY, JSON.stringify(record)); } catch {}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/**
|
|
1413
|
+
* Opt-in automatic installation. The Host's own automatic check finds the
|
|
1414
|
+
* version (channel/region aware); dshmarket installs it. Runs only while a
|
|
1415
|
+
* DSH page is open, reads the local status endpoint only, and tries each
|
|
1416
|
+
* version at most once per {@link AUTO_INSTALL_RETRY_MS} across reloads.
|
|
1417
|
+
*/
|
|
1418
|
+
function startAutoInstall(market, io = {}) {
|
|
1419
|
+
const later = io.setTimeout ?? ((fn, ms) => setTimeout(fn, ms));
|
|
1420
|
+
const cancelLater = io.clearTimeout ?? ((id) => clearTimeout(id));
|
|
1421
|
+
const now = io.now ?? (() => Date.now());
|
|
1422
|
+
const memory = io.memory ?? { read: readAutoInstallMemory, write: writeAutoInstallMemory };
|
|
1423
|
+
const controller = new AbortController();
|
|
1424
|
+
const readStatus = io.readStatus ?? ((signal) => requestPluginUpdate(API.updateStatus, undefined, signal));
|
|
1425
|
+
let stopped = false;
|
|
1426
|
+
let timer = null;
|
|
1427
|
+
const unsubscribe = market.subscribe((state) => {
|
|
1428
|
+
const operation = state.operation;
|
|
1429
|
+
if (state.automatic && operation?.state === "succeeded" && typeof state.targetVersion === "string" &&
|
|
1430
|
+
compareVersions(operation.installedVersion, operation.beforeVersion) === 1) {
|
|
1431
|
+
memory.write({ version: state.targetVersion, at: now(), done: true });
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1434
|
+
const tick = async () => {
|
|
1435
|
+
timer = null;
|
|
1436
|
+
try {
|
|
1437
|
+
const status = await readStatus(controller.signal);
|
|
1438
|
+
if (stopped) return;
|
|
1439
|
+
const version = status?.latestVersion;
|
|
1440
|
+
if (status?.autoInstall !== true || status.autoCheck !== true || status.available !== true ||
|
|
1441
|
+
status.blockedReason || typeof version !== "string" || compareVersions(version, status.currentVersion) !== 1) return;
|
|
1442
|
+
const last = memory.read();
|
|
1443
|
+
if (last && last.version === version && (last.done === true || now() - last.at < AUTO_INSTALL_RETRY_MS)) return;
|
|
1444
|
+
if (market.getState().phase !== "ready") await market.discover();
|
|
1445
|
+
const snapshot = market.getState();
|
|
1446
|
+
if (stopped || snapshot.phase !== "ready" || snapshot.installing) return;
|
|
1447
|
+
memory.write({ version, at: now() });
|
|
1448
|
+
await market.install({ targetVersion: version, automatic: true });
|
|
1449
|
+
} catch {
|
|
1450
|
+
// Background work never surfaces errors; the card shows the market state.
|
|
1451
|
+
} finally {
|
|
1452
|
+
if (!stopped) timer = later(tick, AUTO_INSTALL_INTERVAL_MS);
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
timer = later(tick, AUTO_INSTALL_FIRST_MS);
|
|
1456
|
+
return () => {
|
|
1457
|
+
stopped = true;
|
|
1458
|
+
if (timer !== null) cancelLater(timer);
|
|
1459
|
+
controller.abort();
|
|
1460
|
+
unsubscribe();
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
let sharedMarket = null;
|
|
1465
|
+
|
|
1466
|
+
// #endregion
|
|
1467
|
+
|
|
1058
1468
|
/** Update state is isolated: polling never reloads account data or drafts. */
|
|
1059
|
-
function PluginUpdatePanel({ t }) {
|
|
1469
|
+
function PluginUpdatePanel({ t, market = null, initiallyOpen = false }) {
|
|
1060
1470
|
const [update, setUpdate] = useState(null);
|
|
1061
1471
|
const [loading, setLoading] = useState(true);
|
|
1062
1472
|
const [busy, setBusy] = useState(null);
|
|
@@ -1162,6 +1572,21 @@ window.__ModuleLoader__.load({
|
|
|
1162
1572
|
}
|
|
1163
1573
|
}, []);
|
|
1164
1574
|
|
|
1575
|
+
// dshmarket state is shared with the background auto-install runner.
|
|
1576
|
+
const [marketState, setMarketState] = useState(() => market?.getState() ?? null);
|
|
1577
|
+
const [confirmInstall, setConfirmInstall] = useState(null);
|
|
1578
|
+
useEffect(() => {
|
|
1579
|
+
if (!market) return undefined;
|
|
1580
|
+
let live = true;
|
|
1581
|
+
const unsubscribe = market.subscribe((next) => { if (live) setMarketState(next); });
|
|
1582
|
+
setMarketState(market.getState());
|
|
1583
|
+
if (market.getState().phase === "unknown") void market.discover();
|
|
1584
|
+
return () => {
|
|
1585
|
+
live = false;
|
|
1586
|
+
unsubscribe();
|
|
1587
|
+
};
|
|
1588
|
+
}, [market]);
|
|
1589
|
+
|
|
1165
1590
|
const working = loading || busy !== null || update?.checking === true || update?.downloading === true;
|
|
1166
1591
|
const blockedReason = update?.blockedReason;
|
|
1167
1592
|
const downloadBlocked = !!blockedReason && blockedReason !== "linked-install";
|
|
@@ -1182,7 +1607,17 @@ window.__ModuleLoader__.load({
|
|
|
1182
1607
|
const statusError = update?.error || update?.errorCode
|
|
1183
1608
|
? { updateCode: update.errorCode }
|
|
1184
1609
|
: null;
|
|
1185
|
-
const
|
|
1610
|
+
const linkedInstall = blockedReason === "linked-install";
|
|
1611
|
+
const marketReady = marketState?.phase === "ready";
|
|
1612
|
+
const marketOperation = marketState?.operation ?? null;
|
|
1613
|
+
const marketInstalling = marketState?.installing === true;
|
|
1614
|
+
const marketInstalledVersion = marketOperation?.state === "succeeded" &&
|
|
1615
|
+
compareVersions(marketOperation.installedVersion, marketOperation.beforeVersion) === 1
|
|
1616
|
+
? marketOperation.installedVersion : null;
|
|
1617
|
+
const canMarketInstall = marketReady && !linkedInstall && !downloadBlocked && !marketInstalling &&
|
|
1618
|
+
!marketInstalledVersion && !!update?.available && typeof update.latestVersion === "string" && !!update.latestVersion;
|
|
1619
|
+
const compactState = loading ? "loading" : marketInstalling ? "installing" : downloading ? "downloading" : checking ? "checking" :
|
|
1620
|
+
marketInstalledVersion ? "installed" :
|
|
1186
1621
|
error || statusError ? "error" : update?.downloadedVersion && update.downloadedVersion === update.latestVersion ? "downloaded" :
|
|
1187
1622
|
update?.available ? "available" : "idle";
|
|
1188
1623
|
// Name the actual failure instead of a generic "update error": the
|
|
@@ -1192,9 +1627,12 @@ window.__ModuleLoader__.load({
|
|
|
1192
1627
|
const compactErrorKey = error
|
|
1193
1628
|
? error.source === "status" ? "updateCompactStatusUnavailable" : "updateCompactError"
|
|
1194
1629
|
: failureCode === "network-error" ? "updateCompactErrorNetwork"
|
|
1630
|
+
: failureCode === "service-unavailable" ? "updateCompactStatusUnavailable"
|
|
1195
1631
|
: failureCode === "channel-unavailable" ? "updateCompactErrorChannel"
|
|
1196
1632
|
: "updateCompactError";
|
|
1197
|
-
const compactText = loading ? t("updateLoading") :
|
|
1633
|
+
const compactText = loading ? t("updateLoading") : marketInstalling ? t("updateMarketInstalling") :
|
|
1634
|
+
downloading ? t("updateDownloading") : checking ? t("updateChecking") :
|
|
1635
|
+
marketInstalledVersion ? t("updateCompactInstalled").replace("{version}", marketInstalledVersion) :
|
|
1198
1636
|
error || statusError ? t(compactErrorKey) : compactState === "downloaded"
|
|
1199
1637
|
? t("updateCompactDownloaded").replace("{version}", update.downloadedVersion)
|
|
1200
1638
|
: update?.available ? t("updateCompactAvailable").replace("{version}", availableVersion)
|
|
@@ -1221,7 +1659,7 @@ window.__ModuleLoader__.load({
|
|
|
1221
1659
|
}, t(key))
|
|
1222
1660
|
)
|
|
1223
1661
|
);
|
|
1224
|
-
const toggle = (field, label, hint, checked) =>
|
|
1662
|
+
const toggle = (field, label, hint, checked, unavailable = false) =>
|
|
1225
1663
|
h(
|
|
1226
1664
|
"div",
|
|
1227
1665
|
{ className: "dcpa-update-row dcpa-switch-row" },
|
|
@@ -1235,16 +1673,143 @@ window.__ModuleLoader__.load({
|
|
|
1235
1673
|
className: "dcpa-switch",
|
|
1236
1674
|
"aria-label": t(label),
|
|
1237
1675
|
"aria-checked": checked,
|
|
1238
|
-
|
|
1676
|
+
// An unavailable feature can still be switched off, never on.
|
|
1677
|
+
disabled: working || !update || unavailable && !checked,
|
|
1239
1678
|
onClick: () => { void runAction("save", { [field]: !checked }); },
|
|
1240
1679
|
}, h("span", { className: "dcpa-switch-thumb" }))
|
|
1241
1680
|
);
|
|
1242
1681
|
|
|
1682
|
+
// One-click/automatic installation through dshmarket. Hidden entirely
|
|
1683
|
+
// when the card is rendered without a market client (tests, previews).
|
|
1684
|
+
let marketSection = null;
|
|
1685
|
+
if (market) {
|
|
1686
|
+
const phase = marketState?.phase ?? "unknown";
|
|
1687
|
+
const hintKey = phase === "ready"
|
|
1688
|
+
? linkedInstall ? "updateMarketLinkedHint" : "updateMarketReadyHint"
|
|
1689
|
+
: phase === "unavailable" ? "updateMarketUnavailableHint" : "updateMarketDetecting";
|
|
1690
|
+
const operation = marketOperation;
|
|
1691
|
+
const progress = operation?.progress;
|
|
1692
|
+
const percent = typeof progress?.percent === "number" && Number.isFinite(progress.percent) ? Math.round(progress.percent) : null;
|
|
1693
|
+
const errorCode = marketState?.errorCode ?? null;
|
|
1694
|
+
const outcome = operation?.outcome ?? {};
|
|
1695
|
+
const target = marketState?.targetVersion;
|
|
1696
|
+
const prefix = marketState?.automatic ? `${t("updateMarketAutomaticPrefix")} ` : "";
|
|
1697
|
+
let result = null;
|
|
1698
|
+
if (marketInstalling) {
|
|
1699
|
+
result = h("div", { className: "dcpa-update-notice", role: "status" },
|
|
1700
|
+
h("p", { className: "dcpa-label" }, prefix + (percent === null
|
|
1701
|
+
? t("updateMarketInstalling")
|
|
1702
|
+
: t("updateMarketProgress").replace("{percent}", String(percent))))
|
|
1703
|
+
);
|
|
1704
|
+
} else if (marketInstalledVersion) {
|
|
1705
|
+
const restartHint = outcome.restartRequired
|
|
1706
|
+
? marketState?.restartSupported ? "updateMarketRestartHint"
|
|
1707
|
+
: marketState?.runtime === "desktop" ? "updateMarketRestartDesktopHint" : "updateMarketRestartManualHint"
|
|
1708
|
+
: outcome.refreshRequired ? "updateMarketRefreshHint" : "updateMarketAppliedHint";
|
|
1709
|
+
result = h("div", { className: "dcpa-update-notice", role: "status" },
|
|
1710
|
+
h("p", { className: "dcpa-success" }, prefix + t("updateMarketInstalled").replace("{version}", marketInstalledVersion)),
|
|
1711
|
+
typeof target === "string" && compareVersions(marketInstalledVersion, target) === -1
|
|
1712
|
+
? h("p", { className: "dcpa-hint" }, t("updateMarketVersionLower").replace("{installed}", marketInstalledVersion).replace("{target}", target))
|
|
1713
|
+
: null,
|
|
1714
|
+
h("p", { className: "dcpa-hint" }, t(marketState?.restartRequested ? "updateMarketRestartRequested" : restartHint)),
|
|
1715
|
+
outcome.restartRequired && marketState?.restartSupported && !marketState?.restartRequested
|
|
1716
|
+
? h("div", { className: "dcpa-actions" },
|
|
1717
|
+
h("button", {
|
|
1718
|
+
type: "button",
|
|
1719
|
+
className: "dcpa-btn dcpa-btn-primary",
|
|
1720
|
+
disabled: marketState?.restarting === true,
|
|
1721
|
+
onClick: () => { void market.restart(); },
|
|
1722
|
+
}, marketState?.restarting ? t("updateMarketRestarting") : t("updateMarketRestartBtn")))
|
|
1723
|
+
: !outcome.restartRequired && outcome.refreshRequired
|
|
1724
|
+
? h("div", { className: "dcpa-actions" },
|
|
1725
|
+
h("button", {
|
|
1726
|
+
type: "button",
|
|
1727
|
+
className: "dcpa-btn dcpa-btn-primary",
|
|
1728
|
+
onClick: () => { globalThis.location?.reload(); },
|
|
1729
|
+
}, t("updateMarketRefreshBtn")))
|
|
1730
|
+
: null
|
|
1731
|
+
);
|
|
1732
|
+
} else if (operation?.state === "succeeded") {
|
|
1733
|
+
result = h("div", { className: "dcpa-update-notice", role: "status" },
|
|
1734
|
+
h("p", { className: "dcpa-hint" }, prefix + t("updateMarketUnchanged")));
|
|
1735
|
+
} else if (operation?.state === "cancelled" || operation?.state === "rolled-back") {
|
|
1736
|
+
result = h("div", { className: "dcpa-update-notice", role: "status" },
|
|
1737
|
+
h("p", { className: "dcpa-hint" }, prefix + t(operation.state === "cancelled" ? "updateMarketCancelled" : "updateMarketRolledBack")));
|
|
1738
|
+
}
|
|
1739
|
+
const errorBlock = errorCode && !marketInstalling
|
|
1740
|
+
? h("div", { className: "dcpa-update-notice" },
|
|
1741
|
+
h("p", { className: "dcpa-error", role: "alert" }, prefix + marketErrorText(t, errorCode)),
|
|
1742
|
+
MARKET_FORCEABLE.has(errorCode) && canMarketInstall
|
|
1743
|
+
? h("div", { className: "dcpa-actions" },
|
|
1744
|
+
h("button", {
|
|
1745
|
+
type: "button",
|
|
1746
|
+
className: "dcpa-btn",
|
|
1747
|
+
disabled: working,
|
|
1748
|
+
onClick: () => { void market.install({ targetVersion: update.latestVersion, force: true }); },
|
|
1749
|
+
}, t("updateMarketForceBtn")))
|
|
1750
|
+
: null)
|
|
1751
|
+
: null;
|
|
1752
|
+
marketSection = h(React.Fragment, null,
|
|
1753
|
+
h("div", { className: "dcpa-update-row" },
|
|
1754
|
+
h("div", { className: "dcpa-field" },
|
|
1755
|
+
h("span", { className: "dcpa-label" }, t("updateMarketLabel")),
|
|
1756
|
+
h("p", { className: "dcpa-hint" }, t(hintKey))
|
|
1757
|
+
),
|
|
1758
|
+
phase === "ready" && !linkedInstall
|
|
1759
|
+
? h("div", { className: "dcpa-actions" },
|
|
1760
|
+
h("button", {
|
|
1761
|
+
type: "button",
|
|
1762
|
+
className: "dcpa-btn dcpa-btn-primary",
|
|
1763
|
+
disabled: working || !canMarketInstall,
|
|
1764
|
+
onClick: () => { setConfirmVersion(null); setConfirmInstall(update.latestVersion); },
|
|
1765
|
+
}, marketInstalling ? t("updateMarketInstalling")
|
|
1766
|
+
: marketInstalledVersion ? t("updateMarketInstalledBtn") : t("updateMarketInstallBtn")))
|
|
1767
|
+
: phase === "unavailable"
|
|
1768
|
+
? h("div", { className: "dcpa-actions" },
|
|
1769
|
+
h("button", { type: "button", className: "dcpa-btn", onClick: () => { void market.discover(); } }, t("updateMarketRedetectBtn")))
|
|
1770
|
+
: null
|
|
1771
|
+
),
|
|
1772
|
+
confirmInstall ? h("div", {
|
|
1773
|
+
className: "dcpa-update-notice",
|
|
1774
|
+
role: "alertdialog",
|
|
1775
|
+
"aria-modal": false,
|
|
1776
|
+
"aria-label": t("updateMarketConfirmTitle").replace("{version}", confirmInstall),
|
|
1777
|
+
},
|
|
1778
|
+
h("p", { className: "dcpa-label" }, t("updateMarketConfirmTitle").replace("{version}", confirmInstall)),
|
|
1779
|
+
h("p", { className: "dcpa-hint" }, t("updateMarketConfirmHint")),
|
|
1780
|
+
h("div", { className: "dcpa-actions" },
|
|
1781
|
+
h("button", {
|
|
1782
|
+
type: "button",
|
|
1783
|
+
className: "dcpa-btn dcpa-btn-primary",
|
|
1784
|
+
disabled: working || !canMarketInstall || confirmInstall !== update?.latestVersion,
|
|
1785
|
+
onClick: () => {
|
|
1786
|
+
const version = confirmInstall;
|
|
1787
|
+
setConfirmInstall(null);
|
|
1788
|
+
void market.install({ targetVersion: version });
|
|
1789
|
+
},
|
|
1790
|
+
}, t("updateMarketConfirmBtn")),
|
|
1791
|
+
h("button", { type: "button", className: "dcpa-btn", onClick: () => setConfirmInstall(null) }, t("updateCancelBtn"))
|
|
1792
|
+
),
|
|
1793
|
+
confirmInstall !== update?.latestVersion ? h("p", { className: "dcpa-hint" }, t("updateStaleVersion")) : null
|
|
1794
|
+
) : null,
|
|
1795
|
+
result,
|
|
1796
|
+
errorBlock
|
|
1797
|
+
);
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1243
1800
|
// Native details starts closed. Do not control `open`: polling and saves
|
|
1244
1801
|
// must preserve the user's expanded/collapsed choice without remounting.
|
|
1802
|
+
// `initiallyOpen` (the update-only plugin page) passes a constant `true`
|
|
1803
|
+
// that never changes, so React never rewrites it after mount and the
|
|
1804
|
+
// user can still collapse the card.
|
|
1245
1805
|
return h(
|
|
1246
1806
|
"details",
|
|
1247
|
-
{
|
|
1807
|
+
{
|
|
1808
|
+
className: "dcpa-card dcpa-update-card",
|
|
1809
|
+
"aria-label": t("updateTitle"),
|
|
1810
|
+
"aria-busy": working,
|
|
1811
|
+
...(initiallyOpen ? { open: true } : {}),
|
|
1812
|
+
},
|
|
1248
1813
|
h("summary", { className: "dcpa-update-summary", title: t("updateToggleDetails") },
|
|
1249
1814
|
h("span", { className: "dcpa-update-summary-title" }, t("updateTitle")),
|
|
1250
1815
|
h("span", { className: "dcpa-update-summary-current" },
|
|
@@ -1272,9 +1837,9 @@ window.__ModuleLoader__.load({
|
|
|
1272
1837
|
}, checking ? t("updateChecking") : !update && !loading ? t("updateRetryBtn") : t("updateCheckBtn")),
|
|
1273
1838
|
h("button", {
|
|
1274
1839
|
type: "button",
|
|
1275
|
-
className: "dcpa-btn dcpa-btn-primary",
|
|
1840
|
+
className: marketReady && !linkedInstall ? "dcpa-btn" : "dcpa-btn dcpa-btn-primary",
|
|
1276
1841
|
disabled: working || !canDownload,
|
|
1277
|
-
onClick: () => setConfirmVersion(update.latestVersion),
|
|
1842
|
+
onClick: () => { setConfirmInstall(null); setConfirmVersion(update.latestVersion); },
|
|
1278
1843
|
}, downloading ? t("updateDownloading") : update?.downloadedVersion === update?.latestVersion && update?.downloadedVersion
|
|
1279
1844
|
? t("updateDownloadedBtn") : t("updateDownloadBtn"))
|
|
1280
1845
|
)
|
|
@@ -1298,6 +1863,7 @@ window.__ModuleLoader__.load({
|
|
|
1298
1863
|
),
|
|
1299
1864
|
confirmVersion !== update?.latestVersion ? h("p", { className: "dcpa-hint" }, t("updateStaleVersion")) : null
|
|
1300
1865
|
) : null,
|
|
1866
|
+
marketSection,
|
|
1301
1867
|
h("div", { className: "dcpa-update-row" },
|
|
1302
1868
|
h("div", { className: "dcpa-field" },
|
|
1303
1869
|
h("span", { className: "dcpa-label" }, t("updateChannelLabel")),
|
|
@@ -1319,6 +1885,8 @@ window.__ModuleLoader__.load({
|
|
|
1319
1885
|
),
|
|
1320
1886
|
toggle("autoCheck", "updateAutoCheckLabel", "updateAutoCheckHint", update?.autoCheck !== false),
|
|
1321
1887
|
toggle("autoDownload", "updateAutoDownloadLabel", "updateAutoDownloadHint", update?.autoDownload === true),
|
|
1888
|
+
toggle("autoInstall", "updateAutoInstallLabel", "updateAutoInstallHint", update?.autoInstall === true,
|
|
1889
|
+
!marketReady || linkedInstall || downloadBlocked),
|
|
1322
1890
|
blockedReason ? h("div", { className: "dcpa-update-notice" }, h("p", { className: "dcpa-hint" }, t(blockedKey))) : null,
|
|
1323
1891
|
update?.downloadedVersion && update?.downloadPath ? h("div", { className: "dcpa-update-notice", role: "status" },
|
|
1324
1892
|
h("p", { className: "dcpa-label" }, t(blockedReason === "linked-install" ? "updateLinkedDownloadedHint" : "updateDownloadedHint").replace("{version}", update.downloadedVersion)),
|
|
@@ -1343,8 +1911,23 @@ window.__ModuleLoader__.load({
|
|
|
1343
1911
|
}
|
|
1344
1912
|
|
|
1345
1913
|
/**
|
|
1346
|
-
*
|
|
1347
|
-
*
|
|
1914
|
+
* The plugin's own page under Plugins (bundle page and Plugins tab): only
|
|
1915
|
+
* plugin updates. Account and model settings live in their own settings
|
|
1916
|
+
* section ({@link CpaSettingsPanel}).
|
|
1917
|
+
*/
|
|
1918
|
+
function PluginUpdatePage({ t }) {
|
|
1919
|
+
return h(
|
|
1920
|
+
"div",
|
|
1921
|
+
{ className: "dcpa-wrap", "data-dsh-plugin": MODULE_ID },
|
|
1922
|
+
h("p", { className: "dcpa-hint" }, t("updatePageHint")),
|
|
1923
|
+
h(PluginUpdatePanel, { t, market: sharedMarket, initiallyOpen: true })
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
/**
|
|
1928
|
+
* Account list + switcher (the "CLIProxyAPI" settings section). Each
|
|
1929
|
+
* account is edited in its own {@link AccountPanel}, remounted when the
|
|
1930
|
+
* selection changes.
|
|
1348
1931
|
*/
|
|
1349
1932
|
function CpaSettingsPanel({ t }) {
|
|
1350
1933
|
const [loading, setLoading] = useState(true);
|
|
@@ -1496,7 +2079,6 @@ window.__ModuleLoader__.load({
|
|
|
1496
2079
|
error ? h("p", { className: "dcpa-error", role: "alert" }, error) : null,
|
|
1497
2080
|
toast ? h("p", { className: "dcpa-success", role: "status" }, toast) : null
|
|
1498
2081
|
),
|
|
1499
|
-
h(PluginUpdatePanel, { t }),
|
|
1500
2082
|
active
|
|
1501
2083
|
? h(AccountPanel, {
|
|
1502
2084
|
key: active.id,
|
|
@@ -2470,7 +3052,37 @@ window.__ModuleLoader__.load({
|
|
|
2470
3052
|
);
|
|
2471
3053
|
}
|
|
2472
3054
|
|
|
2473
|
-
//
|
|
3055
|
+
// dshmarket installation client, shared by the card and the opt-in
|
|
3056
|
+
// background auto-install runner. No DOM writes; timers and requests
|
|
3057
|
+
// are disposed with this activation (HMR/unmount).
|
|
3058
|
+
ctx.effect(() => {
|
|
3059
|
+
const market = createMarketUpdater();
|
|
3060
|
+
sharedMarket = market;
|
|
3061
|
+
const stopAutoInstall = startAutoInstall(market);
|
|
3062
|
+
return () => {
|
|
3063
|
+
stopAutoInstall();
|
|
3064
|
+
market.dispose();
|
|
3065
|
+
if (sharedMarket === market) sharedMarket = null;
|
|
3066
|
+
};
|
|
3067
|
+
}, "dsh-cliproxyapi: market updates");
|
|
3068
|
+
|
|
3069
|
+
// 1. Accounts and models: a page of its own in the Settings dialog's left
|
|
3070
|
+
// navigation (like dshmarket's "Market" section). The slot owner gives
|
|
3071
|
+
// unregistered ids the default nav glyph; no DOM is touched here.
|
|
3072
|
+
ctx.slots.inject("settings.section", () =>
|
|
3073
|
+
ctx.slots.register(
|
|
3074
|
+
{
|
|
3075
|
+
name: "settings.section",
|
|
3076
|
+
id: SETTINGS_SECTION_ID,
|
|
3077
|
+
order: 45,
|
|
3078
|
+
label: () => t("nav"),
|
|
3079
|
+
locale: NS,
|
|
3080
|
+
},
|
|
3081
|
+
() => h(CpaSettingsPanel, { t })
|
|
3082
|
+
)
|
|
3083
|
+
);
|
|
3084
|
+
|
|
3085
|
+
// 2. Plugin updates only, on the bundle page in Sidebar -> Plugins.
|
|
2474
3086
|
ctx.slots.inject("plugins.bundle.config", () =>
|
|
2475
3087
|
ctx.slots.register(
|
|
2476
3088
|
{
|
|
@@ -2478,11 +3090,11 @@ window.__ModuleLoader__.load({
|
|
|
2478
3090
|
key: MODULE_ID,
|
|
2479
3091
|
locale: NS,
|
|
2480
3092
|
},
|
|
2481
|
-
(ownerProps = {}) => (ownerProps.view === "summary" ? null : h(
|
|
3093
|
+
(ownerProps = {}) => (ownerProps.view === "summary" ? null : h(PluginUpdatePage, { t }))
|
|
2482
3094
|
)
|
|
2483
3095
|
);
|
|
2484
3096
|
|
|
2485
|
-
//
|
|
3097
|
+
// 3. Plugin updates only, as a tab in Settings -> Plugins.
|
|
2486
3098
|
ctx.slots.inject("settings.plugins.tab", () =>
|
|
2487
3099
|
ctx.slots.register(
|
|
2488
3100
|
{
|
|
@@ -2492,7 +3104,7 @@ window.__ModuleLoader__.load({
|
|
|
2492
3104
|
label: () => t("nav"),
|
|
2493
3105
|
locale: NS,
|
|
2494
3106
|
},
|
|
2495
|
-
() => h(
|
|
3107
|
+
() => h(PluginUpdatePage, { t })
|
|
2496
3108
|
)
|
|
2497
3109
|
);
|
|
2498
3110
|
}
|