@nimoxie/dsh-ui-tweaks 0.2.1 → 0.2.2
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/DEVELOPMENT.md +28 -6
- package/LICENSE +21 -21
- package/README.md +126 -119
- package/cordis.patch.yml +7 -7
- package/lib/client.js +617 -59
- package/lib/index.js +400 -19
- package/lib/window-helper.cs +195 -195
- package/lib/window-helper.ps1 +276 -276
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -17,7 +17,7 @@ window.__ModuleLoader__.load({
|
|
|
17
17
|
const module = { exports: {} };
|
|
18
18
|
const exports = module.exports;
|
|
19
19
|
|
|
20
|
-
const BUILD = "0.2.
|
|
20
|
+
const BUILD = "0.2.2";
|
|
21
21
|
|
|
22
22
|
/* 每个模块的注册各套一层 guard:失败只记 warn(座位/模块失败模式是「静默消失」,
|
|
23
23
|
必须主动吭声;但不能让一个模块的异常打断同文件里其它模块的 apply)。 */
|
|
@@ -725,27 +725,62 @@ window.__ModuleLoader__.load({
|
|
|
725
725
|
(预检 + 二次确认 + host 已写生效豁免)才会带上它。用完即删、终态也清。 */
|
|
726
726
|
var forcedNames = {}; // name → true
|
|
727
727
|
|
|
728
|
-
|
|
728
|
+
/* 调 host 半区的本机回环路由。**必须带超时**:回包丢了不能让面板永远停在
|
|
729
|
+
「正在写入豁免表…」—— 真机遇到过响应没回来、阶段无限停留(写盘其实已成功)。
|
|
730
|
+
AbortController 到点 abort,结果统一收敛成 {ok:false, code:"timeout"}。 */
|
|
731
|
+
function hostPost(route, body, timeoutMs) {
|
|
732
|
+
var ms = Math.max(1000, Number(timeoutMs) || 15000);
|
|
733
|
+
var ctl = null, timer = null;
|
|
734
|
+
try { if (typeof AbortController === "function") { ctl = new AbortController(); } } catch (e0) {}
|
|
735
|
+
function stop() { if (timer) { clearTimeout(timer); timer = null; } }
|
|
736
|
+
var opts = {
|
|
737
|
+
method: "POST", cache: "no-store",
|
|
738
|
+
headers: { "Content-Type": "application/json" },
|
|
739
|
+
body: JSON.stringify(body || {}),
|
|
740
|
+
};
|
|
741
|
+
if (ctl) {
|
|
742
|
+
opts.signal = ctl.signal;
|
|
743
|
+
timer = setTimeout(function () { try { ctl.abort(); } catch (e2) {} }, ms);
|
|
744
|
+
}
|
|
729
745
|
try {
|
|
730
|
-
return fetch("/api/ui-tweaks/" + route, {
|
|
731
|
-
|
|
732
|
-
headers: { "Content-Type": "application/json" },
|
|
733
|
-
body: JSON.stringify(body || {}),
|
|
734
|
-
}).then(function (r) {
|
|
746
|
+
return fetch("/api/ui-tweaks/" + route, opts).then(function (r) {
|
|
747
|
+
stop();
|
|
735
748
|
return r.json().then(function (j) { return j; }, function () { return { ok: false, code: "HTTP " + r.status }; });
|
|
736
|
-
}).catch(function (e) {
|
|
749
|
+
}).catch(function (e) {
|
|
750
|
+
stop();
|
|
751
|
+
var aborted = false;
|
|
752
|
+
try { aborted = !!(ctl && ctl.signal && ctl.signal.aborted); } catch (e3) {}
|
|
753
|
+
return { ok: false, code: aborted ? "timeout" : String((e && e.message) || e) };
|
|
754
|
+
});
|
|
737
755
|
} catch (e) {
|
|
756
|
+
stop();
|
|
738
757
|
return Promise.resolve({ ok: false, code: String((e && e.message) || e) });
|
|
739
758
|
}
|
|
740
759
|
}
|
|
741
|
-
function hostGet(route) {
|
|
760
|
+
function hostGet(route, timeoutMs) {
|
|
761
|
+
var ms = Math.max(1000, Number(timeoutMs) || 10000);
|
|
762
|
+
var ctl = null, timer = null;
|
|
763
|
+
try { if (typeof AbortController === "function") { ctl = new AbortController(); } } catch (e0) {}
|
|
764
|
+
function stop() { if (timer) { clearTimeout(timer); timer = null; } }
|
|
765
|
+
var opts = { cache: "no-store" };
|
|
766
|
+
if (ctl) {
|
|
767
|
+
opts.signal = ctl.signal;
|
|
768
|
+
timer = setTimeout(function () { try { ctl.abort(); } catch (e2) {} }, ms);
|
|
769
|
+
}
|
|
742
770
|
try {
|
|
743
|
-
return fetch("/api/ui-tweaks/" + route,
|
|
771
|
+
return fetch("/api/ui-tweaks/" + route, opts)
|
|
744
772
|
.then(function (r) {
|
|
773
|
+
stop();
|
|
745
774
|
return r.json().then(function (j) { return j; }, function () { return { ok: false, code: "HTTP " + r.status }; });
|
|
746
775
|
})
|
|
747
|
-
.catch(function (e) {
|
|
776
|
+
.catch(function (e) {
|
|
777
|
+
stop();
|
|
778
|
+
var aborted = false;
|
|
779
|
+
try { aborted = !!(ctl && ctl.signal && ctl.signal.aborted); } catch (e3) {}
|
|
780
|
+
return { ok: false, code: aborted ? "timeout" : String((e && e.message) || e) };
|
|
781
|
+
});
|
|
748
782
|
} catch (e) {
|
|
783
|
+
stop();
|
|
749
784
|
return Promise.resolve({ ok: false, code: String((e && e.message) || e) });
|
|
750
785
|
}
|
|
751
786
|
}
|
|
@@ -1030,7 +1065,7 @@ window.__ModuleLoader__.load({
|
|
|
1030
1065
|
rstStage = 'confirm';
|
|
1031
1066
|
rstRender();
|
|
1032
1067
|
}
|
|
1033
|
-
hostGet('restart').then(function (j) { plan = j || { ok: false, code: 'empty' }; decide(); });
|
|
1068
|
+
hostGet('restart', 10000).then(function (j) { plan = j || { ok: false, code: 'empty' }; decide(); });
|
|
1034
1069
|
runningSessions(function (n, names) { running = n; rstNames = names || []; decide(); });
|
|
1035
1070
|
}
|
|
1036
1071
|
|
|
@@ -1040,7 +1075,7 @@ window.__ModuleLoader__.load({
|
|
|
1040
1075
|
showScrim(true);
|
|
1041
1076
|
armKillTimer();
|
|
1042
1077
|
rstRender();
|
|
1043
|
-
hostPost('restart', {}).then(function (j) {
|
|
1078
|
+
hostPost('restart', {}, 16000).then(function (j) {
|
|
1044
1079
|
if (!j || !j.ok) {
|
|
1045
1080
|
/* 回包中断多半就是宿主已经退出(连接先断是正常的),但"多半"不等于一定:
|
|
1046
1081
|
如实报成失败并给可点的出口(重试 / 刷新页面看看),别留一个死状态。 */
|
|
@@ -1079,10 +1114,16 @@ window.__ModuleLoader__.load({
|
|
|
1079
1114
|
var row = findRow(name);
|
|
1080
1115
|
if (!row || row.state !== "cooling" || !row.latest) { return; }
|
|
1081
1116
|
forceSet(name, "checking");
|
|
1082
|
-
|
|
1117
|
+
/* 预检要逐包查 registry 发布时长,可能十几秒;给足 30 秒但仍要有上限 */
|
|
1118
|
+
hostPost("precheck-exempt", { name: name, version: row.latest, windowMs: policyWindowMs }, 30000).then(function (r) {
|
|
1083
1119
|
var cur = findRow(name);
|
|
1084
1120
|
if (!cur || cur.state !== "cooling") { forceReset(name); return; }
|
|
1085
|
-
if (!r || !r.ok) {
|
|
1121
|
+
if (!r || !r.ok) {
|
|
1122
|
+
forceSet(name, "error", (r && r.code) === "timeout"
|
|
1123
|
+
? "预检超时(registry 响应慢),稍后重试。"
|
|
1124
|
+
: "预检没能完成:" + String((r && r.code) || "未知原因"));
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1086
1127
|
var b = (r.blocked || []).filter(function (x) { return x && x.name && x.name !== name; });
|
|
1087
1128
|
if (b.length) { forceBlocked[name] = b; forceSet(name, "blocked"); return; }
|
|
1088
1129
|
forceSet(name, "confirm");
|
|
@@ -1094,10 +1135,33 @@ window.__ModuleLoader__.load({
|
|
|
1094
1135
|
if (!row || !row.latest) { return; }
|
|
1095
1136
|
var ver = row.latest;
|
|
1096
1137
|
forceSet(name, "writing");
|
|
1097
|
-
|
|
1098
|
-
|
|
1138
|
+
/* 写豁免表是幂等的(同值重复写返回 changed:false),所以超时可以安全重试一次:
|
|
1139
|
+
回包丢失时上一次很可能已经写成功,重试只会立刻返回 changed:false。 */
|
|
1140
|
+
function attempt(left) {
|
|
1141
|
+
return hostPost("force-exempt", { name: name, version: ver }, 15000).then(function (r) {
|
|
1142
|
+
if (r && r.code === "timeout" && left > 0) { return attempt(left - 1); }
|
|
1143
|
+
return r;
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
attempt(1).then(function (r) {
|
|
1147
|
+
if (!r || !r.ok) {
|
|
1148
|
+
forceSet(name, "error", (r && r.code) === "timeout"
|
|
1149
|
+
? "写入豁免表没有等到回应(超时)。这一步是幂等的:很可能已经写进去了 —— 点「重试」或先在官方插件页看一下豁免表都安全。"
|
|
1150
|
+
: "写入豁免表失败:" + String((r && r.code) || "未知原因"));
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1099
1153
|
forceReset(name);
|
|
1100
1154
|
forcedNames[name] = true; // 硬闸放行凭据(一次性,终态清理)
|
|
1155
|
+
/* 豁免不能「装完立刻删」:pnpm 每次安装都拿整个 lockfile 校验供应链策略,
|
|
1156
|
+
被强制装上的版本还没满 24h 时删掉豁免,接下来几小时里所有 pnpm 操作都会被
|
|
1157
|
+
整体拒绝。登记成熟时刻(+60s 缓冲),由 host 到点自动删掉这一条。 */
|
|
1158
|
+
try {
|
|
1159
|
+
var mature = isFinite(row.matureAt) ? Number(row.matureAt) : NaN;
|
|
1160
|
+
if (!isFinite(mature) && isFinite(row.publishedAt)) { mature = Number(row.publishedAt) + policyWindowMs; }
|
|
1161
|
+
if (isFinite(mature) && mature > 0) {
|
|
1162
|
+
hostPost("schedule-exempt-cleanup", { name: name, version: ver, dropAt: mature + 60000 }, 8000);
|
|
1163
|
+
}
|
|
1164
|
+
} catch (e1) { /* 登记失败只是这条豁免会多留一阵,不影响安装 */ }
|
|
1101
1165
|
enqueue(name, { force: true });
|
|
1102
1166
|
});
|
|
1103
1167
|
}
|
|
@@ -1173,9 +1237,40 @@ window.__ModuleLoader__.load({
|
|
|
1173
1237
|
return null;
|
|
1174
1238
|
}
|
|
1175
1239
|
|
|
1240
|
+
/* 安装进度心跳:pnpm 一次重链几十个包时可能好几分钟零输出,看门狗不能只按总时长
|
|
1241
|
+
判死。官方 client 用 remote.$on 订阅安装事件(plugin-manager/install-log 等),
|
|
1242
|
+
我们拿同一组事件当「还在动」的心跳;拿不到就退回按总时长兜底。 */
|
|
1243
|
+
var pmEventsBound = false;
|
|
1244
|
+
var lastPmProgressAt = 0;
|
|
1245
|
+
function bindPmEvents() {
|
|
1246
|
+
if (pmEventsBound) { return; }
|
|
1247
|
+
var root = null;
|
|
1248
|
+
try { root = (ctx && ctx.remote) || null; } catch (e0) { root = null; }
|
|
1249
|
+
if (!root) { try { root = pickService(null, "remote"); } catch (e1) { root = null; } }
|
|
1250
|
+
if (!root || typeof root.$on !== "function") { return; }
|
|
1251
|
+
pmEventsBound = true;
|
|
1252
|
+
function note() { lastPmProgressAt = Date.now(); }
|
|
1253
|
+
try {
|
|
1254
|
+
root.$on("plugin-manager/install-log", note);
|
|
1255
|
+
root.$on("plugin-manager/install-state", note);
|
|
1256
|
+
root.$on("plugin-manager/changed", note);
|
|
1257
|
+
} catch (e2) {}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/* /latest 必须带超时:比对是**串行**的,一个请求挂住会让后面所有包永远停在
|
|
1261
|
+
「比对中 …」(fetch 默认没有超时,网络半死/代理吞包时最容易撞上)。 */
|
|
1262
|
+
var REGISTRY_TIMEOUT_MS = 12000;
|
|
1176
1263
|
function fetchLatest(name) {
|
|
1177
1264
|
var path = String(name).split("/").map(encodeURIComponent).join("/");
|
|
1178
|
-
|
|
1265
|
+
var ctl = null, timer = null;
|
|
1266
|
+
try { if (typeof AbortController === "function") { ctl = new AbortController(); } } catch (e0) {}
|
|
1267
|
+
function stop() { if (timer) { clearTimeout(timer); timer = null; } }
|
|
1268
|
+
var opts = { cache: "no-store" };
|
|
1269
|
+
if (ctl) {
|
|
1270
|
+
opts.signal = ctl.signal;
|
|
1271
|
+
timer = setTimeout(function () { try { ctl.abort(); } catch (e2) {} }, REGISTRY_TIMEOUT_MS);
|
|
1272
|
+
}
|
|
1273
|
+
return fetch("https://registry.npmjs.org/" + path + "/latest", opts)
|
|
1179
1274
|
.then(function (r) {
|
|
1180
1275
|
if (r.status === 404) {
|
|
1181
1276
|
/* npm 上没有这个包:本地直装(link:/file:)或尚未发布 —— 不是「检查失败」。
|
|
@@ -1189,6 +1284,13 @@ window.__ModuleLoader__.load({
|
|
|
1189
1284
|
})
|
|
1190
1285
|
.then(function (d) {
|
|
1191
1286
|
return d && d.version ? { version: String(d.version), requiresDsh: dshRequirementOf(d) } : null;
|
|
1287
|
+
})
|
|
1288
|
+
.then(function (v) { stop(); return v; }, function (e) {
|
|
1289
|
+
stop();
|
|
1290
|
+
if (e && (e.name === "AbortError" || /abort/i.test(String(e.message || "")))) {
|
|
1291
|
+
throw new Error("registry 请求超时(" + Math.round(REGISTRY_TIMEOUT_MS / 1000) + " 秒)");
|
|
1292
|
+
}
|
|
1293
|
+
throw e;
|
|
1192
1294
|
});
|
|
1193
1295
|
}
|
|
1194
1296
|
|
|
@@ -1226,17 +1328,29 @@ window.__ModuleLoader__.load({
|
|
|
1226
1328
|
* 来源判定主路径是 host 半区的 /api/ui-tweaks/local-plugins(读 profile
|
|
1227
1329
|
* 依赖声明):本地路径(link:/file:)与仓库/URL 直装(git/github/http)分列,
|
|
1228
1330
|
* host 未重启/路由不可用时退回 registry 404 签名。 */
|
|
1331
|
+
/* host 半区的来源名单:它**卡住整条比对链**(回调不回来,所有行都停在「比对中 …」),
|
|
1332
|
+
所以同样要超时;拿不到就当没有,退回 404 签名判定。 */
|
|
1333
|
+
var LOCAL_NAMES_TIMEOUT_MS = 8000;
|
|
1229
1334
|
function fetchLocalNames(cb) {
|
|
1230
|
-
|
|
1335
|
+
var ctl = null, timer = null;
|
|
1336
|
+
try { if (typeof AbortController === "function") { ctl = new AbortController(); } } catch (e0) {}
|
|
1337
|
+
function stop() { if (timer) { clearTimeout(timer); timer = null; } }
|
|
1338
|
+
var opts = { cache: "no-store" };
|
|
1339
|
+
if (ctl) {
|
|
1340
|
+
opts.signal = ctl.signal;
|
|
1341
|
+
timer = setTimeout(function () { try { ctl.abort(); } catch (e2) {} }, LOCAL_NAMES_TIMEOUT_MS);
|
|
1342
|
+
}
|
|
1343
|
+
fetch("/api/ui-tweaks/local-plugins", opts)
|
|
1231
1344
|
.then(function (r) { if (!r.ok) { throw new Error("HTTP " + r.status); } return r.json(); })
|
|
1232
1345
|
.then(function (d) {
|
|
1346
|
+
stop();
|
|
1233
1347
|
cb({
|
|
1234
1348
|
local: d && d.ok && Array.isArray(d.local) ? d.local : [],
|
|
1235
1349
|
remote: d && d.ok && Array.isArray(d.remote) ? d.remote : [],
|
|
1236
1350
|
dsh: d && d.ok && typeof d.dsh === "string" ? d.dsh : ""
|
|
1237
1351
|
});
|
|
1238
1352
|
})
|
|
1239
|
-
.catch(function () { cb({ local: [], remote: [] }); });
|
|
1353
|
+
.catch(function () { stop(); cb({ local: [], remote: [] }); });
|
|
1240
1354
|
}
|
|
1241
1355
|
function isLocalSrc(src) { return /^(link:|file:)/.test(String(src || "")); }
|
|
1242
1356
|
function isRemoteSrc(src) { return /^(git[+:@]|github:|git:|http)/.test(String(src || "")); }
|
|
@@ -1257,8 +1371,25 @@ window.__ModuleLoader__.load({
|
|
|
1257
1371
|
var busyBefore = ""; // 本次安装前该行的版本(回读比对基准,不受中途重查影响)
|
|
1258
1372
|
var busyLatest = ""; // 本次安装解析出的目标版本(取消后退回「有新版本」要用)
|
|
1259
1373
|
var cancelling = false;
|
|
1374
|
+
/* 队列放行闸:底层请求真的收尾(busyChainDone)+ 终态已判定(busyFinished)才释放
|
|
1375
|
+
busy 并 pump 下一项。真机上「看门狗到点先放行、旧请求还在跑」会让两条 pnpm
|
|
1376
|
+
叠在一起抢 profile 锁,表现就是全都卡住。 */
|
|
1377
|
+
var busyChainDone = true;
|
|
1378
|
+
var busyFinished = true;
|
|
1379
|
+
var busyReleased = true;
|
|
1380
|
+
var busyWatchdog = 0;
|
|
1381
|
+
var busyStartedAt = 0;
|
|
1260
1382
|
var checking = false;
|
|
1261
1383
|
var updatedCount = 0; // 本次页面会话里成功更新的插件数(>0 时头部挂「重启生效」提示条)
|
|
1384
|
+
/* 本次会话里成功更新、但装完仍是「关闭」的插件:重启提示栏里给一枚「启用」按钮。
|
|
1385
|
+
官方安装默认不启用(installBundle 的 enabled 默认 false),我们更新时又刻意保留
|
|
1386
|
+
行内原状态,所以「原本关着的插件」更新完仍然关着。按钮逐个调
|
|
1387
|
+
pluginManager.setBundleEnabled(name, true)(官方插件页那个总开关用的同一 API),
|
|
1388
|
+
只动**这次更新过**的包,不碰用户没更新的插件。 */
|
|
1389
|
+
var updatedNames = []; // 更新成功的插件名(按顺序,去重)
|
|
1390
|
+
var updatedOff = {}; // name → true:更新后回读清单发现仍是关闭
|
|
1391
|
+
var enableStage = ""; // "" | "running" | "error"
|
|
1392
|
+
var enableNote = ""; // 启用失败的说明
|
|
1262
1393
|
var isDesktop = /Electron/i.test(String(navigator.userAgent || ""));
|
|
1263
1394
|
var listeners = []; // 面板重绘订阅(面板未开时也维护状态)
|
|
1264
1395
|
|
|
@@ -1309,14 +1440,29 @@ window.__ModuleLoader__.load({
|
|
|
1309
1440
|
var name = b.name || b.id || "";
|
|
1310
1441
|
var ver = b.version != null ? String(b.version) : (b.installedVersion != null ? String(b.installedVersion) : "");
|
|
1311
1442
|
var src = typeof b.installed === "string" ? b.installed : (typeof b.source === "string" ? b.source : "");
|
|
1443
|
+
var errCode = b.error && b.error.code ? String(b.error.code) : "";
|
|
1444
|
+
var errDiag = b.error && b.error.diagnostic ? String(b.error.diagnostic) : "";
|
|
1445
|
+
/* listBundles 对「装了但读不到」的包仍会返回一行(带 error、没有 version)。
|
|
1446
|
+
旧实现只显示「无法比对」,看不出是文件缺失还是清单坏了;把原因带到
|
|
1447
|
+
标签悬停里(见 tagHtml 的 unknown 分支)。 */
|
|
1448
|
+
var why = "";
|
|
1449
|
+
if (!ver) {
|
|
1450
|
+
if (errCode) {
|
|
1451
|
+
why = "插件清单报错 " + errCode + (errDiag ? "(" + errDiag.slice(0, 160) + ")" : "") + ":读不到已装版本";
|
|
1452
|
+
} else if (b.availability === "missing") {
|
|
1453
|
+
why = "安装文件缺失(package.json 读不到)";
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1312
1456
|
return {
|
|
1313
1457
|
name: name, version: ver, latest: "",
|
|
1314
1458
|
state: isLocalSrc(src) ? "local" : (isRemoteSrc(src) ? "remote" : (ver ? "checking" : "unknown")),
|
|
1315
|
-
src: src, enabled: b.enabled !== false, why:
|
|
1459
|
+
src: src, enabled: b.enabled !== false, why: why
|
|
1316
1460
|
};
|
|
1317
1461
|
}).filter(function (x) { return x.name; })
|
|
1318
1462
|
/* 官方 @deepseek-ai 插件由 dsh 本体更新流程负责,直接从检查列表里剔除 */
|
|
1319
1463
|
.filter(function (x) { return !/^@deepseek-ai\//.test(x.name); });
|
|
1464
|
+
/* 重查顺便校正「更新过但仍关闭」的标记(比如刚在官方插件页手动打开了) */
|
|
1465
|
+
rows.forEach(function (x) { if (x.enabled !== false) { delete updatedOff[x.name]; } });
|
|
1320
1466
|
/* 与在飞的安装对齐(见 reconcileInstallStates):重查不得抹掉「更新中/等待更新」 */
|
|
1321
1467
|
reconcileInstallStates();
|
|
1322
1468
|
checking = false;
|
|
@@ -1447,6 +1593,21 @@ window.__ModuleLoader__.load({
|
|
|
1447
1593
|
queue.splice(i, 1);
|
|
1448
1594
|
setRow(name, { state: "update", why: "" });
|
|
1449
1595
|
}
|
|
1596
|
+
/* 放行队列(幂等):busy 状态、看门狗、强制凭据一起收掉,再 pump 下一项。
|
|
1597
|
+
只有「底层请求已收尾 + 终态已判定」才该调用它(见 startOne / finishOne 的 done)。
|
|
1598
|
+
expectedName:旧一项的迟到回调(先前超时被放行的链)不得放行**当前**这一项。 */
|
|
1599
|
+
function releaseBusy(expectedName) {
|
|
1600
|
+
if (busyReleased) { return; }
|
|
1601
|
+
if (expectedName && busyName && expectedName !== busyName) { return; }
|
|
1602
|
+
busyReleased = true;
|
|
1603
|
+
if (busyWatchdog) { clearInterval(busyWatchdog); busyWatchdog = 0; }
|
|
1604
|
+
var nm = busyName;
|
|
1605
|
+
busyName = ""; busyReq = ""; busyBefore = ""; busyLatest = "";
|
|
1606
|
+
cancelling = false;
|
|
1607
|
+
if (nm) { delete forcedNames[nm]; } // 放行凭据只对一次安装生效,终态一律清掉
|
|
1608
|
+
emit();
|
|
1609
|
+
pump();
|
|
1610
|
+
}
|
|
1450
1611
|
|
|
1451
1612
|
/* ---- 更新 = 「先卸载、再安装」(DSH 安装接口的硬约束)--------------------
|
|
1452
1613
|
官方 installBundle 在**开着 HMR(热重载)的运行时**里会直接拒绝:
|
|
@@ -1484,6 +1645,12 @@ window.__ModuleLoader__.load({
|
|
|
1484
1645
|
var REMOVE_POLL_MS = 400; // 轮询间隔
|
|
1485
1646
|
var INSTALL_RETRY_TIMES = 3; // 被 already-installed 拒后的重试次数
|
|
1486
1647
|
var INSTALL_RETRY_DELAY_MS = 1500; // 每次重试前等多久
|
|
1648
|
+
/* 看门狗(真机教训:一次重链 44 个包的卸载跑了 5 分 08 秒,旧的固定 240 秒把它
|
|
1649
|
+
判成超时,队列提前放行,旧请求还在跑 → 两条 pnpm 抢锁,全都卡住): */
|
|
1650
|
+
var WATCHDOG_TICK_MS = 5000; // 巡检间隔
|
|
1651
|
+
var SLOW_NOTE_MS = 90000; // 无进度 90s:只在行内提示「还在跑」
|
|
1652
|
+
var HARD_CAP_MS = 15 * 60000; // 单项总时长上限:到点才判超时
|
|
1653
|
+
var HARD_CAP_GRACE_MS = 5 * 60000; // 判超时后再等底层收尾 5 分钟才放行队列
|
|
1487
1654
|
function bundleStillListed(pm, nm) {
|
|
1488
1655
|
return Promise.resolve(pm.listBundles()).then(function (r) {
|
|
1489
1656
|
var list = r && r.ok === false ? null : (r && r.value) || [];
|
|
@@ -1521,8 +1688,23 @@ window.__ModuleLoader__.load({
|
|
|
1521
1688
|
}
|
|
1522
1689
|
return Promise.resolve(pm.removeBundle(nm)).then(function (rm) {
|
|
1523
1690
|
var rmFail = resultFailure(rm);
|
|
1524
|
-
/* 卸载被拒:把原始拒绝交回去,同时把"卸载失败"
|
|
1525
|
-
|
|
1691
|
+
/* 卸载被拒:把原始拒绝交回去,同时把"卸载失败"这个真实原因一起带出,别再吞掉。
|
|
1692
|
+
光一个 operation-error 没法排查 —— 把卸载那次的 diagnostic / pnpm 输出首行
|
|
1693
|
+
一并带上(例如 pnpm 读不了 pnpm-workspace.yaml 时的 YAML 报错)。 */
|
|
1694
|
+
if (rmFail) {
|
|
1695
|
+
var diag = "";
|
|
1696
|
+
try {
|
|
1697
|
+
var st = rm && rm.value;
|
|
1698
|
+
diag = String((st && st.error && st.error.diagnostic) || (st && st.packageResult && st.packageResult.output) || "");
|
|
1699
|
+
} catch (e0) { diag = ""; }
|
|
1700
|
+
diag = stripAnsi(diag).split("\n").filter(function (ln) { return String(ln).trim(); })[0] || "";
|
|
1701
|
+
if (diag.length > 160) { diag = diag.slice(0, 160) + "…"; }
|
|
1702
|
+
try {
|
|
1703
|
+
var pr = rm && rm.value && rm.value.packageResult;
|
|
1704
|
+
if (pr && pr.logPath) { diag += (diag ? ";" : "") + "日志 " + pr.logPath; }
|
|
1705
|
+
} catch (e1) { /* 字段缺失就算了 */ }
|
|
1706
|
+
return { result: res, removed: false, removeFailure: rmFail + (diag ? ":" + diag : "") };
|
|
1707
|
+
}
|
|
1526
1708
|
if (onRemoved) { onRemoved(); }
|
|
1527
1709
|
return waitBundlesGone(pm, nm)
|
|
1528
1710
|
.then(function () { return installRetrying(pm, spec, opts); })
|
|
@@ -1549,28 +1731,54 @@ window.__ModuleLoader__.load({
|
|
|
1549
1731
|
|
|
1550
1732
|
function startOne(name) {
|
|
1551
1733
|
var row = findRow(name);
|
|
1734
|
+
var pm = pickService(null, "remote.pluginManager");
|
|
1552
1735
|
busyName = name;
|
|
1553
1736
|
busyBefore = row ? row.version : "";
|
|
1554
1737
|
busyLatest = (row && row.latest) || "";
|
|
1555
1738
|
busyReq = uuid();
|
|
1739
|
+
busyChainDone = false;
|
|
1740
|
+
busyFinished = false;
|
|
1741
|
+
busyReleased = false;
|
|
1742
|
+
busyStartedAt = Date.now();
|
|
1556
1743
|
setRow(name, { state: "busy", why: "" });
|
|
1557
1744
|
emit();
|
|
1558
|
-
|
|
1559
|
-
if (!pm) { finishOne(name, "failed", "remote.pluginManager 不可用"); return; }
|
|
1745
|
+
if (!pm) { busyChainDone = true; finishOne(name, "failed", "remote.pluginManager 不可用"); return; }
|
|
1560
1746
|
var settled = false;
|
|
1561
1747
|
var busyRemoved = false; // 本次更新走没走「先卸载再安装」:卸载过 → 失败必须回滚
|
|
1562
|
-
|
|
1563
|
-
var
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1748
|
+
var lastProgress = busyStartedAt;
|
|
1749
|
+
var slowNote = "";
|
|
1750
|
+
bindPmEvents();
|
|
1751
|
+
/* 看门狗改成「有进度就不算卡」:①无输出 90s 只在行内提示仍在跑;②单项 15 分钟
|
|
1752
|
+
才判超时;③判超时后仍等底层请求收尾(最多再 5 分钟)才放行队列 —— 否则旧请求
|
|
1753
|
+
没结束、下一项又开始,两条 pnpm 会在 profile 锁上互相等,表现就是全都卡住。 */
|
|
1754
|
+
busyWatchdog = setInterval(function () {
|
|
1755
|
+
if (settled) { return; }
|
|
1756
|
+
var now = Date.now();
|
|
1757
|
+
if (lastPmProgressAt > lastProgress) { lastProgress = lastPmProgressAt; }
|
|
1758
|
+
var total = now - busyStartedAt;
|
|
1759
|
+
var idle = now - lastProgress;
|
|
1760
|
+
if (total >= HARD_CAP_MS) {
|
|
1761
|
+
clearInterval(busyWatchdog); busyWatchdog = 0;
|
|
1762
|
+
finish("unknown", "更新超时(" + Math.round(total / 60000) + " 分钟未结束);结果未知,请点「重新检查」确认");
|
|
1763
|
+
try { var pm2 = pickService(null, "remote.pluginManager"); if (pm2 && pm2.cancelInstall && busyReq) { pm2.cancelInstall(busyReq); } } catch (e) {}
|
|
1764
|
+
/* 判超时后底层请求可能还在跑:再给 5 分钟收尾,才放行队列 */
|
|
1765
|
+
setTimeout(function () { if (!busyReleased && busyName === name) { releaseBusy(name); } }, HARD_CAP_GRACE_MS);
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
if (idle >= SLOW_NOTE_MS && !settled) {
|
|
1769
|
+
var note = "pnpm 仍在运行(已 " + Math.max(1, Math.round(total / 60000)) +
|
|
1770
|
+
" 分钟,最近一次输出在 " + Math.round(idle / 1000) + " 秒前);要重链几十个包时可能超过 5 分钟,请再等等";
|
|
1771
|
+
if (note !== slowNote) { slowNote = note; setRow(name, { state: "busy", why: note }); emit(); }
|
|
1772
|
+
}
|
|
1773
|
+
}, WATCHDOG_TICK_MS);
|
|
1567
1774
|
function finish(kind, why, meta) {
|
|
1568
1775
|
if (settled) { return; }
|
|
1569
1776
|
settled = true;
|
|
1570
|
-
|
|
1777
|
+
busyFinished = true;
|
|
1778
|
+
if (busyWatchdog) { clearInterval(busyWatchdog); busyWatchdog = 0; }
|
|
1571
1779
|
/* 卸载成功但重装没成(失败/被取消)→ 把旧版本装回去,绝不把插件留在"没了"的状态。
|
|
1572
1780
|
只有 failed / cancelled 自动回滚:unknown 时安装可能仍在飞,硬回滚会打架,
|
|
1573
|
-
|
|
1781
|
+
改为交给 recoverAfterUnknown 复查(见下)。 */
|
|
1574
1782
|
if (busyRemoved && busyBefore && (kind === "failed" || kind === "cancelled")) {
|
|
1575
1783
|
var pmR = pickService(null, "remote.pluginManager");
|
|
1576
1784
|
if (pmR && typeof pmR.installBundle === "function") {
|
|
@@ -1589,11 +1797,40 @@ window.__ModuleLoader__.load({
|
|
|
1589
1797
|
return;
|
|
1590
1798
|
}
|
|
1591
1799
|
if (busyRemoved && kind === "unknown") {
|
|
1592
|
-
|
|
1800
|
+
recoverAfterUnknown(name, busyBefore, why || "", meta);
|
|
1593
1801
|
return;
|
|
1594
1802
|
}
|
|
1595
1803
|
finishOne(name, kind, why || "", meta);
|
|
1596
1804
|
}
|
|
1805
|
+
/* 超时(unknown)且本次中途卸载过:不能只写一句「请确认」就放走 —— 真机上
|
|
1806
|
+
@linxin666/dsh-session-archive 就是这样被卸掉后没装回来(后续「装回」步骤的
|
|
1807
|
+
结果还会因为 settled 被丢弃)。这里主动复查清单:不在就自动装回旧版本。 */
|
|
1808
|
+
function recoverAfterUnknown(nm, ver, why, meta) {
|
|
1809
|
+
var pmR = pickService(null, "remote.pluginManager");
|
|
1810
|
+
if (!ver || !pmR || typeof pmR.listBundles !== "function") {
|
|
1811
|
+
finishOne(nm, "unknown", why + (ver ? ";本次中途卸载过该插件,请手动重装 " + nm + "@" + ver : ""), meta);
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
setRow(nm, { state: "busy", why: "更新超时,正在确认 " + nm + " 还在不在并自动回滚 …" });
|
|
1815
|
+
emit();
|
|
1816
|
+
Promise.resolve(pmR.listBundles()).then(function (r) {
|
|
1817
|
+
var list = r && r.ok !== false ? (r.value || []) : [];
|
|
1818
|
+
var present = false;
|
|
1819
|
+
for (var i = 0; i < list.length; i++) { if (list[i] && list[i].name === nm) { present = true; break; } }
|
|
1820
|
+
if (present) {
|
|
1821
|
+
finishOne(nm, "unknown", why + ";该插件仍在清单中,版本请点「重新检查」确认", meta);
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
return Promise.resolve(pmR.installBundle(nm + "@" + ver, { enabled: true })).then(function (rr) {
|
|
1825
|
+
var rf = resultFailure(rr);
|
|
1826
|
+
finishOne(nm, "unknown", why + (rf
|
|
1827
|
+
? ";自动回滚失败(" + rf + "),请手动重装 " + nm + "@" + ver
|
|
1828
|
+
: ";已自动装回 " + nm + "@" + ver), meta);
|
|
1829
|
+
});
|
|
1830
|
+
}).catch(function () {
|
|
1831
|
+
finishOne(nm, "unknown", why + ";复查/回滚失败,请手动确认并重装 " + nm + "@" + ver, meta);
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1597
1834
|
function settle(st) {
|
|
1598
1835
|
if (!st || typeof st !== "object") { finish("unknown", "安装未返回可判定的结果"); return; }
|
|
1599
1836
|
var app = st.application;
|
|
@@ -1635,17 +1872,17 @@ window.__ModuleLoader__.load({
|
|
|
1635
1872
|
function abortForCooling(version, publishedAt, matureAt) {
|
|
1636
1873
|
if (settled) { return; }
|
|
1637
1874
|
settled = true;
|
|
1638
|
-
|
|
1875
|
+
busyFinished = true;
|
|
1876
|
+
if (busyWatchdog) { clearInterval(busyWatchdog); busyWatchdog = 0; }
|
|
1639
1877
|
setRow(name, {
|
|
1640
1878
|
state: "cooling", latest: version, why: "", publishedAt: publishedAt, matureAt: matureAt,
|
|
1641
1879
|
detail: "安装前校验:registry 记录 " + version + " 发布于 " + new Date(publishedAt).toISOString() +
|
|
1642
1880
|
",仍在 minimumReleaseAge 窗口内(" + fmtWindow(policyWindowMs) + "),本次安装已拦下(未安装)。"
|
|
1643
1881
|
});
|
|
1644
1882
|
scheduleCoolingRefresh(name, matureAt);
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
pump();
|
|
1883
|
+
/* 还没开始装就被拦下:底层链不会再跑,直接放行队列 */
|
|
1884
|
+
busyChainDone = true;
|
|
1885
|
+
releaseBusy(name);
|
|
1649
1886
|
}
|
|
1650
1887
|
function guardMaturity(nm, version, go) {
|
|
1651
1888
|
if (!version) { go(); return; } // 版本未知(@latest 兜底路径)不拦
|
|
@@ -1702,7 +1939,15 @@ window.__ModuleLoader__.load({
|
|
|
1702
1939
|
finish("failed", text); return;
|
|
1703
1940
|
}
|
|
1704
1941
|
settle(res.value);
|
|
1705
|
-
}).catch(function (e) { finish("failed", "安装请求失败:" + msgOf(e)); })
|
|
1942
|
+
}).catch(function (e) { finish("failed", "安装请求失败:" + msgOf(e)); })
|
|
1943
|
+
.then(chainSettled, chainSettled);
|
|
1944
|
+
/* 底层链真正收尾:只有「终态已判定 + 链已收尾」才放行队列 —— 旧实现看门狗到点就
|
|
1945
|
+
done()→pump(),旧请求还在跑,下一项又发 pnpm,两条操作叠在 profile 锁上。 */
|
|
1946
|
+
function chainSettled() {
|
|
1947
|
+
if (busyName !== name) { return; } // 这一项早已收尾/被放行:别动当前项
|
|
1948
|
+
busyChainDone = true;
|
|
1949
|
+
if (busyFinished) { releaseBusy(name); }
|
|
1950
|
+
}
|
|
1706
1951
|
}
|
|
1707
1952
|
|
|
1708
1953
|
/* 终态落定:回读清单核对真实版本(registry 解析与 pnpm 实际安装可能不一致,
|
|
@@ -1714,16 +1959,27 @@ window.__ModuleLoader__.load({
|
|
|
1714
1959
|
var before = busyBefore;
|
|
1715
1960
|
var latest = busyLatest;
|
|
1716
1961
|
var pm = pickService(null, "remote.pluginManager");
|
|
1962
|
+
var doneOnce = false;
|
|
1963
|
+
var doneGuard = 0;
|
|
1717
1964
|
function done() {
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1965
|
+
if (doneOnce) { return; }
|
|
1966
|
+
doneOnce = true;
|
|
1967
|
+
if (doneGuard) { clearTimeout(doneGuard); doneGuard = 0; }
|
|
1968
|
+
if (busyName !== name) { return; } // 迟到回调(旧一项已被放行):不改当前项
|
|
1969
|
+
busyFinished = true;
|
|
1970
|
+
/* 链已收尾就直接放行;否则等 chainSettled(例如 lostReply 还在等 waitForInstall) */
|
|
1971
|
+
if (busyChainDone) { releaseBusy(name); }
|
|
1972
|
+
}
|
|
1973
|
+
/* 回读清单本身也可能挂住(RPC 无超时):60 秒还没结论就先收尾,别让队列陪着它卡死 */
|
|
1974
|
+
doneGuard = setTimeout(done, 60000);
|
|
1975
|
+
function judge(ver, info) {
|
|
1725
1976
|
if (ver && ver !== before) {
|
|
1726
1977
|
updatedCount++; // 版本真的变了:装到盘上的已是新代码,运行中的进程还是旧的
|
|
1978
|
+
if (updatedNames.indexOf(name) < 0) { updatedNames.push(name); }
|
|
1979
|
+
/* 装完回读的 enabled 才是真的:官方安装默认不启用,我们传的是行内原状态,
|
|
1980
|
+
所以「原本关着的插件」更新完仍然关着 —— 记下来给重启提示栏的启用按钮用。 */
|
|
1981
|
+
var onNow = info ? info.enabled !== false : ((findRow(name) || {}).enabled !== false);
|
|
1982
|
+
if (onNow) { delete updatedOff[name]; } else { updatedOff[name] = true; }
|
|
1727
1983
|
setRow(name, { version: ver, state: "current", latest: ver, why: "", publishedAt: NaN, matureAt: NaN, detail: "" });
|
|
1728
1984
|
/* 安装真的落地了:顺手让 host 半边归并豁免表,免得陈旧条遮蔽下一次安装 */
|
|
1729
1985
|
normalizeExcludes();
|
|
@@ -1750,23 +2006,85 @@ window.__ModuleLoader__.load({
|
|
|
1750
2006
|
done();
|
|
1751
2007
|
}
|
|
1752
2008
|
function attempt(n) {
|
|
1753
|
-
if (!pm || !pm.listBundles) { judge(""); return; }
|
|
2009
|
+
if (!pm || !pm.listBundles) { judge("", null); return; }
|
|
1754
2010
|
Promise.resolve(pm.listBundles()).then(function (r) {
|
|
1755
2011
|
var bundles = (r && r.ok && r.value) || [];
|
|
1756
2012
|
if (!bundles.length && n < 3) { setTimeout(function () { attempt(n + 1); }, 1500); return; }
|
|
1757
|
-
var
|
|
2013
|
+
var found = null;
|
|
1758
2014
|
for (var i = 0; i < bundles.length; i++) {
|
|
1759
|
-
if (bundles[i].name === name) {
|
|
2015
|
+
if (bundles[i].name === name) { found = bundles[i]; break; }
|
|
1760
2016
|
}
|
|
1761
|
-
judge(
|
|
2017
|
+
judge(found && found.version != null ? String(found.version) : "", found || null);
|
|
1762
2018
|
}).catch(function () {
|
|
1763
2019
|
if (n < 3) { setTimeout(function () { attempt(n + 1); }, 1500); return; }
|
|
1764
|
-
judge("");
|
|
2020
|
+
judge("", null);
|
|
1765
2021
|
});
|
|
1766
2022
|
}
|
|
1767
2023
|
attempt(0);
|
|
1768
2024
|
}
|
|
1769
2025
|
|
|
2026
|
+
/* ---- 重启提示栏里的「启用已更新的插件」 ---------------------------------
|
|
2027
|
+
* 官方安装/更新的默认结果是**不启用**(installBundle 的 enabled 默认 false),
|
|
2028
|
+
* 面板更新又刻意保留行内原状态,于是「原本关着的插件」更新完仍然关着。
|
|
2029
|
+
* 这里给一个显式按钮:逐个调 setBundleEnabled(name, true)(官方插件页总开关同款),
|
|
2030
|
+
* 串行执行(host 每次启用都会写 manifest + reload,别并发),只动本次更新过的包。
|
|
2031
|
+
* 成功就从标记里删掉;失败留在按钮里可重试,原因写在提示条下方。 */
|
|
2032
|
+
function pendingEnableNames() {
|
|
2033
|
+
var out = [];
|
|
2034
|
+
for (var i = 0; i < updatedNames.length; i++) {
|
|
2035
|
+
if (updatedOff[updatedNames[i]]) { out.push(updatedNames[i]); }
|
|
2036
|
+
}
|
|
2037
|
+
return out;
|
|
2038
|
+
}
|
|
2039
|
+
function enableUpdated() {
|
|
2040
|
+
if (enableStage === "running") { return; }
|
|
2041
|
+
var todo = pendingEnableNames();
|
|
2042
|
+
if (!todo.length) { return; }
|
|
2043
|
+
var pm = pickService(null, "remote.pluginManager");
|
|
2044
|
+
if (!pm || typeof pm.setBundleEnabled !== "function") {
|
|
2045
|
+
enableStage = "error";
|
|
2046
|
+
enableNote = "这个 dsh 版本的插件管理器没有 setBundleEnabled,请到官方插件页手动打开这几个插件。";
|
|
2047
|
+
emit();
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
enableStage = "running";
|
|
2051
|
+
enableNote = "";
|
|
2052
|
+
emit();
|
|
2053
|
+
var failed = [];
|
|
2054
|
+
function step(i) {
|
|
2055
|
+
if (i >= todo.length) {
|
|
2056
|
+
enableStage = failed.length ? "error" : "";
|
|
2057
|
+
enableNote = failed.length ? "有 " + failed.length + " 个没启用成功:" + failed.join("、") + ";可以再点一次。" : "";
|
|
2058
|
+
emit();
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2061
|
+
var nm = todo[i];
|
|
2062
|
+
var p;
|
|
2063
|
+
try { p = Promise.resolve(pm.setBundleEnabled(nm, true)); }
|
|
2064
|
+
catch (e0) { failed.push(nm + "(" + msgOf(e0) + ")"); step(i + 1); return; }
|
|
2065
|
+
p.then(function (res) {
|
|
2066
|
+
var code = "";
|
|
2067
|
+
if (!res || res.ok === false) {
|
|
2068
|
+
code = String((res && res.error && res.error.code) || "unknown-error");
|
|
2069
|
+
} else {
|
|
2070
|
+
var st = res.value;
|
|
2071
|
+
if (st && st.application === "failed") { code = String((st.error && st.error.code) || "unknown-error"); }
|
|
2072
|
+
}
|
|
2073
|
+
if (code) {
|
|
2074
|
+
failed.push(nm + "(" + code + ")");
|
|
2075
|
+
} else {
|
|
2076
|
+
delete updatedOff[nm];
|
|
2077
|
+
setRow(nm, { enabled: true });
|
|
2078
|
+
}
|
|
2079
|
+
step(i + 1);
|
|
2080
|
+
}, function (e) {
|
|
2081
|
+
failed.push(nm + "(" + msgOf(e) + ")");
|
|
2082
|
+
step(i + 1);
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
step(0);
|
|
2086
|
+
}
|
|
2087
|
+
|
|
1770
2088
|
/* 取消:排队项只摘自己(cancelQueued);在装项请求取消**本次**,队列保留,
|
|
1771
2089
|
等它落定后 done() → pump() 自然接着装下一项。
|
|
1772
2090
|
旧实现把整个 queue 清空,于是「等待更新」的项全部退回「有新版本」——
|
|
@@ -1879,7 +2197,7 @@ window.__ModuleLoader__.load({
|
|
|
1879
2197
|
var rw = liveWrap(RST_PANEL_ID, '.rwrap');
|
|
1880
2198
|
if (rw) { placeNear(rstBtn, rw, RST_WIDTH, 180); }
|
|
1881
2199
|
var pw = liveWrap(PANEL_ID, '.wrap');
|
|
1882
|
-
if (pw) { placeNear(document.getElementById(BTN_ID), pw, PANEL_WIDTH, 220); }
|
|
2200
|
+
if (pw) { placeNear(document.getElementById(BTN_ID), pw, PANEL_WIDTH, 220); syncSb(pw); }
|
|
1883
2201
|
};
|
|
1884
2202
|
try { if (typeof requestAnimationFrame === 'function') { requestAnimationFrame(run); return; } } catch (e) {}
|
|
1885
2203
|
setTimeout(run, 0);
|
|
@@ -1923,7 +2241,20 @@ window.__ModuleLoader__.load({
|
|
|
1923
2241
|
".hd b{flex:1;font-size:13px;font-weight:600}" +
|
|
1924
2242
|
".x{cursor:pointer;opacity:.55;font-size:17px;line-height:1;padding:0 3px}" +
|
|
1925
2243
|
".x:hover{opacity:1}" +
|
|
1926
|
-
".
|
|
2244
|
+
".rowswrap{flex:1 1 auto;min-height:0;position:relative;display:flex;flex-direction:column}" +
|
|
2245
|
+
/* 列表容器:隐藏原生滚动条(Windows 上会吃掉 17px 行内宽),改用浮层拇指 */
|
|
2246
|
+
".rows{flex:1 1 auto;min-height:0;overflow-y:auto;overflow-x:hidden;scrollbar-width:none;-ms-overflow-style:none}" +
|
|
2247
|
+
".rows::-webkit-scrollbar{width:0;height:0}" +
|
|
2248
|
+
/* 浮层滚动条:与对话区那条自绘滚动条同一套观感(currentColor、7px→9px、可拖动) */
|
|
2249
|
+
".sbbar{position:absolute;top:0;right:0;width:12px;height:100%;z-index:2;" +
|
|
2250
|
+
"user-select:none;-webkit-user-select:none;touch-action:none}" +
|
|
2251
|
+
".sbbar[hidden]{display:none}" +
|
|
2252
|
+
".sbthumb{position:absolute;right:0;top:0;width:100%;background:transparent}" +
|
|
2253
|
+
".sbthumb::after{content:\"\";position:absolute;top:0;bottom:0;right:2px;width:7px;border-radius:4px;" +
|
|
2254
|
+
"background:rgba(128,128,128,.45);background:color-mix(in srgb,currentColor 36%,transparent);" +
|
|
2255
|
+
"transition:width .12s,right .12s,background .12s}" +
|
|
2256
|
+
".sbbar:hover .sbthumb::after,.sbthumb.drag::after{right:1px;width:9px;" +
|
|
2257
|
+
"background:rgba(128,128,128,.68);background:color-mix(in srgb,currentColor 60%,transparent)}" +
|
|
1927
2258
|
".rowwrap{border-bottom:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.12))}" +
|
|
1928
2259
|
".rowwrap:last-child{border-bottom:0}" +
|
|
1929
2260
|
".row{display:flex;align-items:center;gap:8px;padding:7px 12px}" +
|
|
@@ -1972,6 +2303,9 @@ window.__ModuleLoader__.load({
|
|
|
1972
2303
|
".restart{flex:0 0 auto;padding:6px 12px;font-size:12px;line-height:1.5;" +
|
|
1973
2304
|
"background:rgba(62,207,142,.12);color:#4fbf8b;" +
|
|
1974
2305
|
"border-bottom:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.18))}" +
|
|
2306
|
+
/* 提示栏里的「启用已更新的插件」:与冷静期详情里的强制按钮同一套 .fbtn.fgo 样式 */
|
|
2307
|
+
".restart .fbtn{margin-left:8px;vertical-align:1px}" +
|
|
2308
|
+
".rnote{margin-top:4px;font-size:11px;line-height:1.45;color:#e07070;opacity:.9}" +
|
|
1975
2309
|
".foot{padding:8px 12px;font-size:11px;line-height:1.5;opacity:.6;" +
|
|
1976
2310
|
"border-top:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.12))}" +
|
|
1977
2311
|
"</style><div class=\"wrap\" role=\"dialog\" aria-label=\"插件更新\" tabindex=\"-1\" hidden></div>";
|
|
@@ -2009,6 +2343,8 @@ window.__ModuleLoader__.load({
|
|
|
2009
2343
|
rows.forEach(function (r) { if (r.state === "update" && queue.indexOf(r.name) < 0 && r.name !== busyName) { enqueue(r.name); } });
|
|
2010
2344
|
return;
|
|
2011
2345
|
}
|
|
2346
|
+
/* 重启提示栏里的「启用已更新的插件」(橙色按钮,样式同强制更新) */
|
|
2347
|
+
if (t.classList.contains("renable")) { enableUpdated(); return; }
|
|
2012
2348
|
if (t.classList.contains("cooling")) {
|
|
2013
2349
|
/* 冷静期标签:只展开 / 收起详情,绝不触发安装 */
|
|
2014
2350
|
var nm0 = t.getAttribute("data-n");
|
|
@@ -2141,15 +2477,127 @@ window.__ModuleLoader__.load({
|
|
|
2141
2477
|
return "<div class=\"warn\">⚠ 有会话在用「自动审查」:装/更新插件容易失败,工具调用也可能被审查拦掉。建议先切「工作区内修改」,装完再切回。</div>";
|
|
2142
2478
|
}
|
|
2143
2479
|
|
|
2480
|
+
/* ---- 浮层滚动条(更新面板)-----------------------------------------------
|
|
2481
|
+
* 原生滚动条会在 Windows 上占掉列表 17px 的宽,把每一行往里挤;这里隐藏原生条,
|
|
2482
|
+
* 只在内容右缘画一条拇指(绝对定位、不参与布局)。
|
|
2483
|
+
* **抄对话区那条自绘滚动条的观感与手感**(模块 2,A/B 实测过的实现):
|
|
2484
|
+
* · 视觉:默认 `color-mix(currentColor 32%)`、悬停/拖动时加宽到 9px、55%;
|
|
2485
|
+
* 不自动隐藏(常驻半透明,一眼看得出还能滚)。
|
|
2486
|
+
* · 拖动:PointerCapture + 按下点偏移(不是「拇指中心跳过来」),
|
|
2487
|
+
* 点轨道时先把拇指中心移到点击处再顺势进入拖动;pointermove 同步写
|
|
2488
|
+
* scrollTop,scroll 事件回纠几何;轨道上滚轮转发给真容器。
|
|
2489
|
+
* 面板是 Shadow DOM 浮层:事件只挂在面板内部节点,不监听 document。 */
|
|
2490
|
+
var SB_THUMB_MIN = 28;
|
|
2491
|
+
function sbParts(wrap) {
|
|
2492
|
+
var sc = wrap && wrap.querySelector(".rows");
|
|
2493
|
+
var bar = wrap && wrap.querySelector(".sbbar");
|
|
2494
|
+
var th = bar && bar.querySelector(".sbthumb");
|
|
2495
|
+
return sc && bar && th ? { sc: sc, bar: bar, th: th } : null;
|
|
2496
|
+
}
|
|
2497
|
+
/* 几何:高 = 轨道 × 可见比,位置 = scrollTop / 可滚动范围。
|
|
2498
|
+
注意别用 bar.clientHeight 量轨道高:`hidden` 时它是 0,会把自己永久锁在隐藏。
|
|
2499
|
+
轨道与滚动容器同高(都填满 .rowswrap),用视口高即可。 */
|
|
2500
|
+
function syncSb(wrap) {
|
|
2501
|
+
var p = sbParts(wrap);
|
|
2502
|
+
if (!p) { return; }
|
|
2503
|
+
var viewH = p.sc.clientHeight;
|
|
2504
|
+
var range = p.sc.scrollHeight - viewH;
|
|
2505
|
+
if (range <= 0 || viewH <= 0) { p.bar.hidden = true; return; }
|
|
2506
|
+
p.bar.hidden = false;
|
|
2507
|
+
var barH = viewH;
|
|
2508
|
+
var h = Math.round(barH * viewH / p.sc.scrollHeight);
|
|
2509
|
+
if (h < SB_THUMB_MIN) { h = SB_THUMB_MIN; }
|
|
2510
|
+
if (h > barH) { h = barH; }
|
|
2511
|
+
var maxTop = barH - h;
|
|
2512
|
+
var top = maxTop > 0 ? Math.round(maxTop * p.sc.scrollTop / range) : 0;
|
|
2513
|
+
if (top < 0) { top = 0; }
|
|
2514
|
+
if (top > maxTop) { top = maxTop; }
|
|
2515
|
+
if (p.th.style.height !== h + "px") { p.th.style.height = h + "px"; }
|
|
2516
|
+
if (p.th.style.top !== top + "px") { p.th.style.top = top + "px"; }
|
|
2517
|
+
}
|
|
2518
|
+
function bindSb(wrap) {
|
|
2519
|
+
var p = sbParts(wrap);
|
|
2520
|
+
if (!p || p.bar.__sbBound) { return; }
|
|
2521
|
+
p.bar.__sbBound = true;
|
|
2522
|
+
var sc = p.sc, bar = p.bar, th = p.th;
|
|
2523
|
+
var drag = null;
|
|
2524
|
+
sc.addEventListener("scroll", function () { syncSb(wrap); }, { passive: true });
|
|
2525
|
+
bar.addEventListener("pointerdown", function (e) {
|
|
2526
|
+
if (e.button !== undefined && e.button !== 0) { return; }
|
|
2527
|
+
var range = sc.scrollHeight - sc.clientHeight;
|
|
2528
|
+
var barH = sc.clientHeight;
|
|
2529
|
+
if (range <= 0 || barH <= 0) { return; }
|
|
2530
|
+
syncSb(wrap);
|
|
2531
|
+
var barRect = bar.getBoundingClientRect();
|
|
2532
|
+
var thRect = th.getBoundingClientRect();
|
|
2533
|
+
if (e.clientY >= thRect.top && e.clientY <= thRect.bottom) {
|
|
2534
|
+
/* 按在拇指上:保持按下点与拇指顶部的偏移 */
|
|
2535
|
+
drag = { offsetY: e.clientY - thRect.top, barRect: barRect };
|
|
2536
|
+
} else {
|
|
2537
|
+
/* 按在轨道上:先把拇指中心挪到点击处,再顺势进入拖动 */
|
|
2538
|
+
var h = th.offsetHeight || SB_THUMB_MIN;
|
|
2539
|
+
var maxTop = Math.max(0, barH - h);
|
|
2540
|
+
var top0 = Math.round(e.clientY - barRect.top - h / 2);
|
|
2541
|
+
if (top0 < 0) { top0 = 0; }
|
|
2542
|
+
if (top0 > maxTop) { top0 = maxTop; }
|
|
2543
|
+
sc.scrollTop = maxTop > 0 ? top0 / maxTop * range : 0;
|
|
2544
|
+
syncSb(wrap);
|
|
2545
|
+
thRect = th.getBoundingClientRect();
|
|
2546
|
+
drag = { offsetY: e.clientY - thRect.top, barRect: barRect };
|
|
2547
|
+
}
|
|
2548
|
+
th.classList.add("drag");
|
|
2549
|
+
try { bar.setPointerCapture(e.pointerId); } catch (e0) {}
|
|
2550
|
+
e.preventDefault();
|
|
2551
|
+
});
|
|
2552
|
+
bar.addEventListener("pointermove", function (e) {
|
|
2553
|
+
if (!drag) { return; }
|
|
2554
|
+
var range = sc.scrollHeight - sc.clientHeight;
|
|
2555
|
+
var barH = sc.clientHeight;
|
|
2556
|
+
var h = th.offsetHeight || SB_THUMB_MIN;
|
|
2557
|
+
var maxTop = Math.max(0, barH - h);
|
|
2558
|
+
var top = e.clientY - drag.barRect.top - drag.offsetY;
|
|
2559
|
+
if (top < 0) { top = 0; }
|
|
2560
|
+
if (top > maxTop) { top = maxTop; }
|
|
2561
|
+
sc.scrollTop = maxTop > 0 ? top / maxTop * range : 0;
|
|
2562
|
+
syncSb(wrap);
|
|
2563
|
+
e.preventDefault();
|
|
2564
|
+
});
|
|
2565
|
+
function release(e) {
|
|
2566
|
+
if (!drag) { return; }
|
|
2567
|
+
drag = null;
|
|
2568
|
+
th.classList.remove("drag");
|
|
2569
|
+
try { bar.releasePointerCapture(e.pointerId); } catch (e0) {}
|
|
2570
|
+
}
|
|
2571
|
+
bar.addEventListener("pointerup", release);
|
|
2572
|
+
bar.addEventListener("pointercancel", release);
|
|
2573
|
+
/* 指针压在条上时,滚轮转发给真容器(条是覆盖层,不是滚动容器本身) */
|
|
2574
|
+
bar.addEventListener("wheel", function (e) {
|
|
2575
|
+
e.preventDefault();
|
|
2576
|
+
sc.scrollTop += e.deltaY;
|
|
2577
|
+
syncSb(wrap);
|
|
2578
|
+
}, { passive: false });
|
|
2579
|
+
syncSb(wrap);
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2144
2582
|
function paint() {
|
|
2145
2583
|
var wrap = panelWrap();
|
|
2146
2584
|
var upd = 0;
|
|
2147
2585
|
/* 有成功更新就常驻提示:装到盘上的是新代码,运行中的还是旧的,
|
|
2148
2586
|
不重启(桌面端)/不重启 dsh 并强刷(web 端)就不会生效。 */
|
|
2149
2587
|
function restartBar() {
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2588
|
+
var pending = pendingEnableNames();
|
|
2589
|
+
if (!updatedCount && !pending.length) { return ""; }
|
|
2590
|
+
var head = updatedCount
|
|
2591
|
+
? "已更新 " + updatedCount + " 个插件" +
|
|
2592
|
+
(pending.length ? ",其中 " + pending.length + " 个仍是关闭状态" : "") +
|
|
2593
|
+
"," + (isDesktop ? "重启客户端后生效" : "重启 dsh 服务并刷新页面后生效")
|
|
2594
|
+
: "";
|
|
2595
|
+
var btn = pending.length
|
|
2596
|
+
? "<span class=\"fbtn fgo renable\"" + KBD + " title=\"调用插件管理器的 setBundleEnabled,逐个打开这次更新过、但仍是关闭的插件\">" +
|
|
2597
|
+
(enableStage === "running" ? "正在启用 …" : "启用已更新的 " + pending.length + " 个插件") + "</span>"
|
|
2598
|
+
: "";
|
|
2599
|
+
var note = enableNote ? "<div class=\"rnote\">" + esc(enableNote) + "</div>" : "";
|
|
2600
|
+
return "<div class=\"restart\">" + head + btn + note + "</div>";
|
|
2153
2601
|
}
|
|
2154
2602
|
rows.forEach(function (r) { if (r.state === "update") { upd++; } });
|
|
2155
2603
|
var allBtn = upd ? "<span class=\"allbtn\"" + KBD + ">全部更新(" + upd + ")</span>" : "<span class=\"allbtn\" hidden></span>";
|
|
@@ -2177,7 +2625,9 @@ window.__ModuleLoader__.load({
|
|
|
2177
2625
|
}
|
|
2178
2626
|
var html = "<div class=\"hd\"><b>插件更新 · " + (busyName ? "正在更新 " + esc(busyName) + " …" : (checking ? "检查中…" : (rows.length ? upd + " 个插件有新版本" : "插件更新检查"))) + "</b>" +
|
|
2179
2627
|
"<span class=\"recheck\"" + KBD + " title=\"重新读取插件清单并比对 registry\">重新检查</span>" + allBtn +
|
|
2180
|
-
"<span class=\"x\"" + KBD + " title=\"关闭\" aria-label=\"关闭\">×</span></div>" + restartBar() + accessModeBar() +
|
|
2628
|
+
"<span class=\"x\"" + KBD + " title=\"关闭\" aria-label=\"关闭\">×</span></div>" + restartBar() + accessModeBar() +
|
|
2629
|
+
"<div class=\"rowswrap\"><div class=\"rows\">" + body + "</div>" +
|
|
2630
|
+
"<div class=\"sbbar\" aria-hidden=\"true\"><div class=\"sbthumb\"></div></div></div>" +
|
|
2181
2631
|
/* 面板脚注:把「只查第三方」这条口径写在明处(plugin-manager 同款一句) */
|
|
2182
2632
|
"<div class=\"foot\">只检查第三方插件;官方 @deepseek-ai/ 包随 DSH 本体升级。</div>";
|
|
2183
2633
|
if (wrap.innerHTML !== html) {
|
|
@@ -2192,6 +2642,9 @@ window.__ModuleLoader__.load({
|
|
|
2192
2642
|
wrap.hidden = false;
|
|
2193
2643
|
/* 位置只有一个来源:那枚「检查更新」按钮(不可拖动) */
|
|
2194
2644
|
placeNear(document.getElementById(BTN_ID), wrap, PANEL_WIDTH, 220);
|
|
2645
|
+
/* 浮层滚动条:先绑事件(innerHTML 换过就重绑),再按当前高度画拇指 */
|
|
2646
|
+
bindSb(wrap);
|
|
2647
|
+
syncSb(wrap);
|
|
2195
2648
|
}
|
|
2196
2649
|
}
|
|
2197
2650
|
|
|
@@ -2867,6 +3320,7 @@ window.__ModuleLoader__.load({
|
|
|
2867
3320
|
* ====================================================================== */
|
|
2868
3321
|
var ADAPTER_SUMMARY_SUFFIX = "/api/changes.summary";
|
|
2869
3322
|
var ADAPTER_DIFF_SUFFIX = "/api/changes.diff";
|
|
3323
|
+
var ADAPTER_OPEN_SUFFIX = "/api/changes.open";
|
|
2870
3324
|
var ADAPTER_CONTEXT_LINES = 3;
|
|
2871
3325
|
var ADAPTER_MAX_HUNK_LINES = 2000;
|
|
2872
3326
|
|
|
@@ -2953,7 +3407,7 @@ window.__ModuleLoader__.load({
|
|
|
2953
3407
|
});
|
|
2954
3408
|
}
|
|
2955
3409
|
out.sort(function (left, right) { return left.display < right.display ? -1 : (left.display > right.display ? 1 : 0); });
|
|
2956
|
-
return { sessionId: String(sessionId), seq: String(seq), turn: turn, files: out, stateFiles: files };
|
|
3410
|
+
return { sessionId: String(sessionId), seq: String(seq), turn: turn, files: out, stateFiles: files, cwd: cwd || null };
|
|
2957
3411
|
}
|
|
2958
3412
|
|
|
2959
3413
|
/* 会话 cwd:官方 display 的基准。取不到就退化为绝对路径(不影响校验,只是显示不同)。 */
|
|
@@ -3013,6 +3467,54 @@ window.__ModuleLoader__.load({
|
|
|
3013
3467
|
} catch (e) { return null; }
|
|
3014
3468
|
}
|
|
3015
3469
|
|
|
3470
|
+
/* ---------- 动作路由补位(会话释放后官方 /api/changes.open 只回 404) ----------
|
|
3471
|
+
官方那条路由要先查 Host 内存里的改动摘要才能把 file.path 解析成绝对路径,
|
|
3472
|
+
会话一释放就 404 —— 文件却还在盘上。这里补两件事:
|
|
3473
|
+
GET → 由 host 枚举系统关联,返回真实应用列表(官方控件的主按钮=第一个应用,
|
|
3474
|
+
菜单列全部;枚举不出来就给空数组,控件退化成只有「显示文件位置」);
|
|
3475
|
+
POST → 用自己 host 半区的 /api/ui-tweaks/open-path 打开 / 定位 / 用指定应用打开。
|
|
3476
|
+
只对索引里已有的坐标生效,活会话照旧走官方原路(先试官方,404 才补位)。 */
|
|
3477
|
+
function changesAbsPath(entry, p) {
|
|
3478
|
+
var s = String(p == null ? "" : p);
|
|
3479
|
+
if (!s) { return null; }
|
|
3480
|
+
var isWin = /^[A-Za-z]:[\\/]/.test(s) || s.slice(0, 2) === "\\\\";
|
|
3481
|
+
if (isWin || s.charAt(0) === "/") { return s; }
|
|
3482
|
+
var cwd = entry && entry.cwd ? String(entry.cwd) : "";
|
|
3483
|
+
if (!cwd) { return null; }
|
|
3484
|
+
var sep = (/^[A-Za-z]:[\\/]/.test(cwd) || cwd.slice(0, 2) === "\\\\") ? "\\" : "/";
|
|
3485
|
+
return cwd.replace(/[\\/]+$/, "") + sep + s.replace(/^[\\/]+/, "").split("/").join(sep);
|
|
3486
|
+
}
|
|
3487
|
+
/* 调 host 半区回环路由(模块 5b 自身作用域;带超时,回包丢失不会吊死动作) */
|
|
3488
|
+
function adapterHostPost(route, body, timeoutMs) {
|
|
3489
|
+
var ms = Math.max(1000, Number(timeoutMs) || 8000);
|
|
3490
|
+
var ctl = null, timer = null;
|
|
3491
|
+
try { if (typeof AbortController === "function") { ctl = new AbortController(); } } catch (e0) {}
|
|
3492
|
+
function stop() { if (timer) { clearTimeout(timer); timer = null; } }
|
|
3493
|
+
var opts = {
|
|
3494
|
+
method: "POST", cache: "no-store",
|
|
3495
|
+
headers: { "Content-Type": "application/json" },
|
|
3496
|
+
body: JSON.stringify(body || {}),
|
|
3497
|
+
};
|
|
3498
|
+
if (ctl) {
|
|
3499
|
+
opts.signal = ctl.signal;
|
|
3500
|
+
timer = setTimeout(function () { try { ctl.abort(); } catch (e2) {} }, ms);
|
|
3501
|
+
}
|
|
3502
|
+
try {
|
|
3503
|
+
return fetch("/api/ui-tweaks/" + route, opts).then(function (r) {
|
|
3504
|
+
stop();
|
|
3505
|
+
return r.json().then(function (j) { return j; }, function () { return { ok: false, code: "HTTP " + r.status }; });
|
|
3506
|
+
}).catch(function (e) {
|
|
3507
|
+
stop();
|
|
3508
|
+
var aborted = false;
|
|
3509
|
+
try { aborted = !!(ctl && ctl.signal && ctl.signal.aborted); } catch (e3) {}
|
|
3510
|
+
return { ok: false, code: aborted ? "timeout" : String((e && e.message) || e) };
|
|
3511
|
+
});
|
|
3512
|
+
} catch (e) {
|
|
3513
|
+
stop();
|
|
3514
|
+
return Promise.resolve({ ok: false, code: String((e && e.message) || e) });
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3016
3518
|
function installChangesFetchAdapter() {
|
|
3017
3519
|
if (adapterInstalled) { return; }
|
|
3018
3520
|
if (typeof window === "undefined" || typeof window.fetch !== "function") { return; }
|
|
@@ -3026,11 +3528,67 @@ window.__ModuleLoader__.load({
|
|
|
3026
3528
|
var path = url.pathname || "";
|
|
3027
3529
|
var isSummary = path.length >= ADAPTER_SUMMARY_SUFFIX.length && path.slice(-ADAPTER_SUMMARY_SUFFIX.length) === ADAPTER_SUMMARY_SUFFIX;
|
|
3028
3530
|
var isDiff = path.length >= ADAPTER_DIFF_SUFFIX.length && path.slice(-ADAPTER_DIFF_SUFFIX.length) === ADAPTER_DIFF_SUFFIX;
|
|
3029
|
-
|
|
3531
|
+
var isOpen = path.length >= ADAPTER_OPEN_SUFFIX.length && path.slice(-ADAPTER_OPEN_SUFFIX.length) === ADAPTER_OPEN_SUFFIX;
|
|
3532
|
+
if (!isSummary && !isDiff && !isOpen) { return original.apply(this, arguments); }
|
|
3030
3533
|
var sessionId = url.searchParams.get("sessionId");
|
|
3031
3534
|
var seq = url.searchParams.get("seq");
|
|
3032
3535
|
var entry = changesEntryFor(sessionId, seq);
|
|
3033
3536
|
if (!entry) { return original.apply(this, arguments); }
|
|
3537
|
+
if (isOpen) {
|
|
3538
|
+
/* 先走官方原路:活会话它本来就能干活(有真应用列表)。只有它答 404
|
|
3539
|
+
(会话释放:Host 内存里没有改动摘要了)才由我们补位。 */
|
|
3540
|
+
var self = this, args = arguments;
|
|
3541
|
+
var fallbackOpen = function () {
|
|
3542
|
+
var method = "GET";
|
|
3543
|
+
try { method = String((init && init.method) || "GET").toUpperCase(); } catch (eM) { method = "GET"; }
|
|
3544
|
+
var openIndex = Number(url.searchParams.get("index"));
|
|
3545
|
+
var openFile = entry.files[openIndex] || null;
|
|
3546
|
+
var openTarget = changesAbsPath(entry, openFile ? openFile.path : "");
|
|
3547
|
+
if (method === "GET") {
|
|
3548
|
+
/* 应用列表交给 host(复用官方 native-command 枚举系统关联,只回真实应用);
|
|
3549
|
+
host 不可用或枚举失败就给空数组 —— 官方控件的退化形态就是「显示文件位置」。 */
|
|
3550
|
+
return adapterHostPost("open-apps", { path: openTarget || "" }, 20000).then(function (r) {
|
|
3551
|
+
var apps = (r && r.ok && Array.isArray(r.apps)) ? r.apps : [];
|
|
3552
|
+
var res = changesJsonResponse(apps);
|
|
3553
|
+
if (!res) { return new Response("unavailable", { status: 500 }); }
|
|
3554
|
+
try {
|
|
3555
|
+
console.info("[dsh-ui-tweaks:" + BUILD + "] 动作补位 · 应用列表 " + apps.length + " 项(" +
|
|
3556
|
+
((r && r.source) || "fallback") + (r && r.code ? " · " + r.code : "") + ")");
|
|
3557
|
+
} catch (e2) {}
|
|
3558
|
+
return res;
|
|
3559
|
+
});
|
|
3560
|
+
}
|
|
3561
|
+
if (!openTarget) {
|
|
3562
|
+
try { console.warn("[dsh-ui-tweaks:" + BUILD + "] 动作补位失败:拿不到绝对路径", openFile); } catch (e3) {}
|
|
3563
|
+
return Promise.resolve(new Response("no-path", { status: 404 }));
|
|
3564
|
+
}
|
|
3565
|
+
var openAction = url.searchParams.get("action") === "reveal" ? "reveal" : "open";
|
|
3566
|
+
/* 菜单里选中的应用 id 原样交给 host(它按官方 openNativeFileApplication 启动);
|
|
3567
|
+
host 不认的 id 由它按默认关联打开,不会把点击变成失败。 */
|
|
3568
|
+
var openApp = url.searchParams.get("application") || "";
|
|
3569
|
+
return adapterHostPost("open-path", { path: openTarget, action: openAction, application: openApp }, 20000).then(function (r) {
|
|
3570
|
+
if (r && r.ok) { return new Response(null, { status: 204 }); }
|
|
3571
|
+
var code = r && r.code ? String(r.code) : "failed";
|
|
3572
|
+
var status = code === "path-not-found" ? 404 : (code === "timeout" ? 504 : 500);
|
|
3573
|
+
try {
|
|
3574
|
+
console.warn("[dsh-ui-tweaks:" + BUILD + "] 动作补位失败:" + code +
|
|
3575
|
+
(r && r.source ? "(" + r.source + ")" : "") + " · " + openTarget);
|
|
3576
|
+
} catch (e4) {}
|
|
3577
|
+
return new Response(code, { status: status });
|
|
3578
|
+
});
|
|
3579
|
+
};
|
|
3580
|
+
var attempt;
|
|
3581
|
+
try { attempt = original.apply(self, args); }
|
|
3582
|
+
catch (eA) { return fallbackOpen(); }
|
|
3583
|
+
if (!attempt || typeof attempt.then !== "function") {
|
|
3584
|
+
/* 不是 Promise(被别的包装改了签名):原样交回 */
|
|
3585
|
+
return attempt;
|
|
3586
|
+
}
|
|
3587
|
+
return attempt.then(function (res) {
|
|
3588
|
+
if (res && res.status === 404) { return fallbackOpen(); }
|
|
3589
|
+
return res;
|
|
3590
|
+
}, function () { return fallbackOpen(); });
|
|
3591
|
+
}
|
|
3034
3592
|
var payload = isSummary ? changesSummaryPayload(entry) : changesDiffPayload(entry, Number(url.searchParams.get("index")));
|
|
3035
3593
|
if (payload === null) { return original.apply(this, arguments); }
|
|
3036
3594
|
var response = changesJsonResponse(payload);
|