dsh-m 0.9.78 → 0.9.80
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 +28 -0
- package/lib/cli.js +4 -2
- package/lib/client.js +154 -39
- package/lib/core/external-journal.js +137 -0
- package/lib/core/host-api.js +25 -5
- package/lib/core/market.js +82 -24
- package/lib/core/profile-ops.js +99 -28
- package/lib/core/toggle.js +18 -1
- package/lib/tools.js +8 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,20 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
8
8
|
|
|
9
9
|
## 中文
|
|
10
10
|
|
|
11
|
+
### 0.9.80 修复:已装页开关抖动根治 + 社区开关定位恒定 + 「已实测」徽章退役(三项 UX 修复,client-only)
|
|
12
|
+
|
|
13
|
+
- **① 已装页开关抖动根治(主人实拍:点开关整页抖、开关原地失踪、鼠标不再命中)**:CDP 实测每次点击开关发生 47px×2 卡片栅格位移 + 0.6–1.3s 命中丢失窗口(原点落到上方卡片 `.dshm-links` 外链行,误触即开浏览器标签)。根因三件:`ProgressLine`(install 专用 pnpm 进度条)对 toggle 也挂载(35px 行 + body 12px gap,插入/拔出即位移);toast/重启横幅是面板 column flex 的流内子元素(出现/消失压缩 body 39px×2);开关视觉翻转要等 5 个刷新请求落地(1.5–2.3s「点了没反应」真空期,且期间开关 `locked` 灰态)。修复:新增 `progressTarget`(仅 install/upgrade/self-upgrade 挂进度条;`activeMutateTarget` 语义不动,仍管逐行禁用);toast/横幅包进 `.dshm-banner-stack`(absolute 底部浮层,pointer-events 穿透);`pendingToggles` 乐观翻转(点击 42ms 即变,`finally` 摘除=成功收敛/失败回滚);toggle 只重取已装视图(installed+market×2+updates+external-operations → installed×1,`view-refresh.js` 未动、单测原样);`.dshm-switch::before` 热区 34×19 → 46×31(视觉不变)。CDP 回测四断言全过:位移 0、命中零丢失、翻转 42ms、body 底缘 0 位移。
|
|
14
|
+
- **② 社区目录开关钉进标题行(主人实拍:开/关后开关位置高低不同)**:开关行原是卡片 body 末行,关态 5 行元信息消失即被上提约 5 行高。移入 `.dshm-section-title` 右侧(flex 现成 + 开关自带 `margin-left:auto`;与已装卡片右上角开关同一交互语言),CDP 实测 ON/OFF 两态开关 y 恒等(135.2px,漂移 0.0);冗余「社区目录开关」行退役,语义并入开关 tooltip。
|
|
15
|
+
- **③ 「已实测」徽章退役(B 方案:byline 灰字承接)**:verified 仅覆盖精选 4/21 条且全为作者自家插件——徽章实为署名代理、区分度趋零,真载荷(实测过的 DSH 运行时清单)藏在 hover tooltip 里触屏不可达;按「卡片徽章只放例外信号(弃用/自用/可升级)」原则退役。市场卡/收藏快照卡/弹窗头三处渲染退役;市场卡 tag 行尾新增灰字 `实测 ≤{最新复验版本}`(tooltip 全量清单);详情弹窗「实测版本」行保留为唯一权威呈现;`badge.verified` 键按 0.9.76「版本无关徽章退役」先例退役;registry 数据、snapshotOf 透传与安装期 peer 兼容弹窗零触碰。
|
|
16
|
+
- **验证**:1425 项测试全绿(0.9.79 基线 1423 + 退役断言 2;U12 收藏卡契约随 ③ 改写为新契约)、typecheck 零错误;三项均 client-only,刷新页面即生效;①② 经 CDP 全程量化回测(位移/命中/翻转延迟/几何恒定)。
|
|
17
|
+
|
|
18
|
+
### 0.9.79 功能:外部操作记录——非面板通道(CLI / Agent 工具)变更直落审计日志进面板「已结束」(ADR-0018)
|
|
19
|
+
|
|
20
|
+
- **问题(审计面脱节实证)**:全局操作记录的口径 = 面板发起——quota-watch 0.1.28 经 `dshm_upgrade`(Agent 工具)升级装机成功后,面板「已结束」列表零记录,操作审计面与真实系统状态脱节。CLI 与 Agent 工具是独立进程,写不进浏览器 localStorage 的操作泵。
|
|
21
|
+
- **方案(ADR-0018:机器本地直落日志,与操作泵分离)**:新增 `$DSH_HOME/dshm/external-operations.json`(`DSHM_EXTERNAL_JOURNAL_FILE` 可覆盖;机器本地、不跨机同步)——九个公共变更入口增 `channel` 参数(`'panel'|'cli'|'host-tool'`,缺省 `'external'` 兜底),非面板通道的操作终态后直落一条记录:**成功与失败都记**、滚动 200 条、tmp+rename 原子写、损坏文件改名隔离、append 任何失败被钩子吞掉(至多 stderr 一行,绝不反噬操作终态,R1-1);reason 截断 ≤300。desktop 口径(R1-2):desktopToggle 透传 togglePlugin(唯一嵌套对,单笔事务一条记录);desktopInstall/Uninstall/Upgrade 自记(call-graph audit 证实不经已插桩入口);服务缺席拒绝自记 `desktop-toggle` failed。
|
|
22
|
+
- **面板消费(内存 overlay 并读,不写 localStorage)**:host-api 新增 `external-operations`(list)与 `external-operations-clear` 两 method(鉴权沿既有宿主信任检查);「已结束」列表 = 本地操作泵(localStorage,现状不动)+ 外部记录 overlay(服务端为唯一事实源,persist 路径零触碰——防服务端淘汰后被本地副本复活,R1-3);渠道徽标 **Agent 工具** / **命令行** / 兜底**外部**(Q2b/R1-4);失败记录点亮红点(Q4);外部行不渲染单条 ✕(R2-3);「清除已结束」连带清除服务端日志 + overlay 同步清空;fetch 时机 = 面板初始化 + 每次操作完成后 + 打开操作记录卡(不轮询,R1-6);「已结束」计数改用合并后全量,最近 50 条渲染截断照旧(R1-7)。
|
|
23
|
+
- **验证**:1423 项测试全绿(0.9.78 基线 1389 + 新增 34:journal 存储 8(滚动/损坏容错/panel 拒绝/无凭据/路径上抛/截断)、通道插桩 8(panel 零记录/cli、host-tool 各一条/嵌套单条/失败 failed/append 注入抛错不反噬)、host-api 4(list/clear/鉴权/注入位)、client 模型+视图 14(overlay 升序并读/persist 纯净/幂等/徽标/✕ 出口/计数/接线锚))、typecheck 零错误;测试全程零触碰真实 `~/.dsh`(10 个既有测试文件补 journal 隔离,另新增 1 个通道插桩测试文件,沿用 cacheRoot 纪律)。实施计划经独立评审 Agent 三轮评审收敛;npm 发版与 dsh-m 自升级另行经用户确认。
|
|
24
|
+
|
|
11
25
|
### 0.9.78 性能:收藏 stale 检查零网络 + 市场页零增量提交守卫(钢人 C3)
|
|
12
26
|
|
|
13
27
|
- **问题(钢人报告 C3 节)**:收藏 stale 判定只需要「id 还在不在目录」的成员布尔,却穿透富 market interface 的探测段——每次收藏区访问/切换(800ms debounce 合并后)仍是 N 条收藏 × full 查询,含每命中条 npm/github latest **网络扇出**且产物全部丢弃;叠加 toggle 后 refreshViews 四连响应无条件 `setData`——数据层精心维持的引用稳定性一次 setState 即蒸发。
|
|
@@ -565,6 +579,20 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
565
579
|
|
|
566
580
|
## English
|
|
567
581
|
|
|
582
|
+
### Fixes in 0.9.80 — installed-tab toggle jitter eliminated + community switch pinned to its title row + 已实测 badge retired (three UX fixes, client-only)
|
|
583
|
+
|
|
584
|
+
- **① Installed-tab toggle jitter eliminated (user report: clicking the switch shook the whole page, the switch vanished in place, and the cursor no longer hit it)**: CDP measurement showed a 47px×2 card-grid shift per click plus a 0.6–1.3s hit-test loss window (the cursor's fixed point landed on the card above's `.dshm-links` external-links row — a mis-click opens a browser tab). Three root causes: `ProgressLine` (the install-only pnpm progress bar) mounted for toggles too (35px row + 12px body gap — inserting/unmounting it is the shift itself); the toast/restart banners were in-flow flex children of the panel column (mount/dismiss compressed the body by 39px×2); the switch visually flipped only after five refresh requests landed (a 1.5–2.3s "nothing happened" dead window, with the switch greyed to `locked`). Fixes: a new `progressTarget` gates ProgressLine to install/upgrade/self-upgrade (`activeMutateTarget` semantics untouched — it still drives per-row disabling); toasts/banners are wrapped in `.dshm-banner-stack` (absolute bottom overlay, pointer-events pass-through); optimistic `pendingToggles` flips the switch in 42ms (cleared in `finally` — converges on success, reverts to server state on failure); toggle refreshes the installed view only (installed+market×2+updates+external-operations → installed×1; `view-refresh.js` untouched, its unit tests unchanged); `.dshm-switch::before` grows the hit area 34×19 → 46×31 (visuals unchanged). CDP re-probe passes all four assertions: zero drift, zero hit-loss, 42ms flip, stable body bottom edge.
|
|
585
|
+
- **② Community catalog switch pinned to its section title row (user report: the switch sat at different heights when on vs off)**: the switch row used to be the last child of the card body — toggling off dropped five metadata rows and yanked the switch up ~5 row heights. It now lives at the right edge of `.dshm-section-title` (flex already in place + the switch's own `margin-left:auto`; same interaction language as the installed-page card switches). CDP-measured identical Y in both states (135.2px, 0.0 drift); the redundant "社区目录开关" row is retired into the switch tooltip.
|
|
586
|
+
- **③ 已实测 (verified) badge retired (plan B: byline gray text)**: verified covers only 4/21 curated entries — all the author's own plugins — making the badge an authorship proxy with near-zero discriminating power, while its real payload (the tested DSH runtime list) lived in a hover-only tooltip. Per the "card badges only carry exceptions (deprecated / internal / outdated)" principle it is retired from the market card, the favorite snapshot card, and the modal header; the market card tag line gains a low-emphasis `verified ≤{latest re-verified runtime}` suffix (full list on hover); the detail modal's "Verified runtimes" row remains the single authoritative presentation; the `badge.verified` i18n key is retired following the 0.9.76 version-independent-badge precedent. registry data, snapshotOf passthrough, and the install-time peer-compat dialog are untouched.
|
|
587
|
+
- **Verification**: 1425 tests green (0.9.79 baseline 1423 + 2 retirement assertions; the U12 favorites-card contract rewritten for ③), typecheck clean; all three fixes are client-only — a page refresh applies them; ① and ② were quantified end-to-end with CDP probes (drift / hit-testing / flip latency / geometry stability).
|
|
588
|
+
|
|
589
|
+
### Feature in 0.9.79 — external operation records: non-panel channels (CLI / agent tools) land terminal-state audit entries into the panel's Ended list (ADR-0018)
|
|
590
|
+
|
|
591
|
+
- **Problem (audit-surface disconnect, real incident)**: the global operation records only covered panel-initiated changes — the quota-watch 0.1.28 upgrade via `dshm_upgrade` (an agent tool) installed successfully yet left zero rows in the panel's Ended list. CLI and agent tools are separate processes and cannot write the browser-localStorage operations pump.
|
|
592
|
+
- **Solution (ADR-0018: machine-local direct-write journal, decoupled from the pump)**: a new `$DSH_HOME/dshm/external-operations.json` (overridable via `DSHM_EXTERNAL_JOURNAL_FILE`; machine-local, never synced across machines) — the nine public mutation entries gain a `channel` parameter (`'panel'|'cli'|'host-tool'`, defaulting to `'external'` as the audit-safe fallback). Non-panel operations append one terminal-state record after completion: **both success and failure are recorded**, rolling cap of 200, tmp+rename atomic writes, corrupted files quarantined by rename, and any append failure swallowed by the hook (at most one stderr line — it never back-fires onto the operation result, R1-1); `reason` truncated to 300 chars. Desktop accounting (R1-2): desktopToggle passes through to togglePlugin (the only nested pair — one transaction, one record); desktopInstall/Uninstall/Upgrade self-record (call-graph audit confirms they bypass instrumented entries); the service-absent structured refusal self-records `desktop-toggle` failed.
|
|
593
|
+
- **Panel consumption (in-memory overlay, never localStorage)**: host-api gains `external-operations` (list) and `external-operations-clear` methods (auth rides the existing host trust check); the Ended list = local operations pump (localStorage, untouched) + external-records overlay (the server side is the single source of truth, persist path untouched — prevents local resurrection after server-side eviction, R1-3); channel badges **Agent tool** / **CLI** / fallback **External** (Q2b/R1-4); failed external entries light the red dot (Q4); external rows render no per-row ✕ (R2-3); Clear ended also clears the server journal plus the overlay; fetch timing = panel init + after each operation + opening the operations card (no polling, R1-6); the Ended count now uses the merged total while the last-50 render cap stays (R1-7).
|
|
594
|
+
- **Verification**: 1423 tests green (0.9.78 baseline 1389 + 34 added: journal storage 8 (rolling / corruption quarantine / panel rejection / no-credentials / path error propagation / truncation), channel wiring 8 (panel zero-record / cli and host-tool one each / nested single record / failure recorded as failed / injected append failure never back-fires), host-api 4 (list / clear / auth / override slots), client model+view 14 (overlay ascending merge / persist purity / idempotence / badge / ✕ exit / count / wiring anchors)), typecheck clean; tests never touch the real `~/.dsh` (10 existing test files gained journal isolation, plus 1 new channel-wiring test file, following the cacheRoot discipline). The implementation plan was closed over three independent review rounds; npm publish and dsh-m self-upgrade remain subject to separate user confirmation.
|
|
595
|
+
|
|
568
596
|
### Performance in 0.9.78 — favorites stale checks go cache-only: zero-network favorites access + zero-increment commit guard (steelman C3)
|
|
569
597
|
|
|
570
598
|
- **Problem (steelman report, card C3)**: the favorites stale verdict only needs a "is this id still in the catalog" membership boolean, yet it pierces the rich market interface's probe segment - every favorites visit/toggle (after the 800ms debounce merge) still fires N favorites × full queries including a per-hit npm/github latest **network fan-out** whose products are entirely discarded. On top of that, the unconditional `setData` after refreshViews' four-in-a-row responses (the post-toggle norm) evaporates the reference stability the data layer carefully maintains.
|
package/lib/cli.js
CHANGED
|
@@ -337,6 +337,7 @@ async function runCliDispatch(argv, deps, io) {
|
|
|
337
337
|
version,
|
|
338
338
|
forceIncompatible: flags.force === true,
|
|
339
339
|
namespace: 'cli',
|
|
340
|
+
channel: 'cli',
|
|
340
341
|
});
|
|
341
342
|
}
|
|
342
343
|
catch (e) {
|
|
@@ -361,6 +362,7 @@ async function runCliDispatch(argv, deps, io) {
|
|
|
361
362
|
res = await d.upgradePlugin(target, cfg, {
|
|
362
363
|
forceIncompatible: flags.force === true,
|
|
363
364
|
namespace: 'cli',
|
|
365
|
+
channel: 'cli',
|
|
364
366
|
});
|
|
365
367
|
}
|
|
366
368
|
catch (e) {
|
|
@@ -386,7 +388,7 @@ async function runCliDispatch(argv, deps, io) {
|
|
|
386
388
|
if (onFlag === offFlag)
|
|
387
389
|
throw new Error('开关命令需要恰好一个 --on 或 --off');
|
|
388
390
|
requireYes(flags, '切换插件运行状态');
|
|
389
|
-
const res = await d.togglePlugin(target, onFlag);
|
|
391
|
+
const res = await d.togglePlugin(target, onFlag, { channel: 'cli' });
|
|
390
392
|
out(`✅ 已${onFlag ? '启用' : '停用'} ${res.pkg}(profile 文件已更新)`);
|
|
391
393
|
if (res.applied === 'live')
|
|
392
394
|
out('本次进程内已即时生效;重启后状态保持。');
|
|
@@ -397,7 +399,7 @@ async function runCliDispatch(argv, deps, io) {
|
|
|
397
399
|
case 'uninstall': {
|
|
398
400
|
const target = needFlag(flags, 'pkg');
|
|
399
401
|
requireYes(flags, '卸载');
|
|
400
|
-
const res = await d.uninstallPlugin(target, cfg, { namespace: 'cli' });
|
|
402
|
+
const res = await d.uninstallPlugin(target, cfg, { namespace: 'cli', channel: 'cli' });
|
|
401
403
|
out(`✅ 已卸载 ${res.pkg}${res.liveDisabled ? '(运行中的界面已先下线)' : ''}`);
|
|
402
404
|
if (res.leftovers.length)
|
|
403
405
|
out(`ℹ️ 疑似残留数据(未删除):${res.leftovers.join('、')}`);
|
package/lib/client.js
CHANGED
|
@@ -1344,10 +1344,28 @@ __export(operations_exports, {
|
|
|
1344
1344
|
createOperationsStore: () => createOperationsStore,
|
|
1345
1345
|
createOpsPump: () => createOpsPump,
|
|
1346
1346
|
drainRestored: () => drainRestored,
|
|
1347
|
+
externalToRecord: () => externalToRecord,
|
|
1347
1348
|
opAppliesTo: () => opAppliesTo,
|
|
1348
1349
|
restoreRecords: () => restoreRecords,
|
|
1349
1350
|
upgradeNotify: () => upgradeNotify
|
|
1350
1351
|
});
|
|
1352
|
+
function externalToRecord(er) {
|
|
1353
|
+
if (!er || typeof er !== "object") return null;
|
|
1354
|
+
const meta = {};
|
|
1355
|
+
if (typeof er.fromVersion === "string" && er.fromVersion !== "") meta.from = er.fromVersion;
|
|
1356
|
+
if (typeof er.toVersion === "string" && er.toVersion !== "") meta.to = er.toVersion;
|
|
1357
|
+
return {
|
|
1358
|
+
id: typeof er.id === "string" && er.id !== "" ? er.id : `ext-${er.at ?? "unknown"}`,
|
|
1359
|
+
kind: er.kind,
|
|
1360
|
+
target: typeof er.target === "string" ? er.target : "",
|
|
1361
|
+
status: er.result === "failed" ? "failed" : "done",
|
|
1362
|
+
createdAt: Number.isFinite(er.at) ? er.at : 0,
|
|
1363
|
+
updatedAt: Number.isFinite(er.at) ? er.at : 0,
|
|
1364
|
+
channel: typeof er.channel === "string" && er.channel !== "" ? er.channel : "external",
|
|
1365
|
+
...er.reason ? { error: String(er.reason) } : {},
|
|
1366
|
+
...Object.keys(meta).length > 0 ? { meta } : {}
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1351
1369
|
function upgradeNotify(activation) {
|
|
1352
1370
|
if (activation === "client-only") return { needsRestart: false, suffixKey: "notify.upgraded.clientonly" };
|
|
1353
1371
|
if (activation === "unknown") return { needsRestart: true, suffixKey: "notify.upgraded.activationUnknown" };
|
|
@@ -1385,6 +1403,7 @@ function createOperationsStore(storage) {
|
|
|
1385
1403
|
const s = storageOf(storage);
|
|
1386
1404
|
let records = safeParse(s.read());
|
|
1387
1405
|
let persistDegraded = false;
|
|
1406
|
+
let externalOverlay = [];
|
|
1388
1407
|
const persist = () => {
|
|
1389
1408
|
if (persistDegraded) return;
|
|
1390
1409
|
try {
|
|
@@ -1398,6 +1417,28 @@ function createOperationsStore(storage) {
|
|
|
1398
1417
|
list() {
|
|
1399
1418
|
return records.map((r) => ({ ...r }));
|
|
1400
1419
|
},
|
|
1420
|
+
/** 外部记录 overlay(内存):id 幂等合并(后到值覆盖),返回合并视图。 */
|
|
1421
|
+
setExternal(next) {
|
|
1422
|
+
const incoming = (Array.isArray(next) ? next : []).filter((r) => r && typeof r === "object" && typeof r.id === "string");
|
|
1423
|
+
const byId = new Map(externalOverlay.map((r) => [r.id, r]));
|
|
1424
|
+
for (const r of incoming) byId.set(r.id, { ...byId.get(r.id) || {}, ...r });
|
|
1425
|
+
externalOverlay = [...byId.values()];
|
|
1426
|
+
return this.listMerged();
|
|
1427
|
+
},
|
|
1428
|
+
/** 清空外部 overlay(「清除已结束」联动 external-operations-clear 成功后调用)。 */
|
|
1429
|
+
clearExternal() {
|
|
1430
|
+
externalOverlay = [];
|
|
1431
|
+
return this.listMerged();
|
|
1432
|
+
},
|
|
1433
|
+
/** @returns {OperationRecord[]} 外部 overlay 当前快照(原始形态,浅拷贝)。 */
|
|
1434
|
+
externalList() {
|
|
1435
|
+
return externalOverlay.map((r) => ({ ...r }));
|
|
1436
|
+
},
|
|
1437
|
+
/** 本地 + 外部合并视图:外部映射为渲染记录后与本地按 createdAt 升序(groupOpsByDay 假定升序输入)。 */
|
|
1438
|
+
listMerged() {
|
|
1439
|
+
const external = externalOverlay.map(externalToRecord).filter(Boolean);
|
|
1440
|
+
return [...records.map((r) => ({ ...r })), ...external].sort((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0));
|
|
1441
|
+
},
|
|
1401
1442
|
get persistDegraded() {
|
|
1402
1443
|
return persistDegraded;
|
|
1403
1444
|
},
|
|
@@ -1616,6 +1657,8 @@ var init_operations = __esm({
|
|
|
1616
1657
|
// src/client/operations-view.js
|
|
1617
1658
|
var operations_view_exports = {};
|
|
1618
1659
|
__export(operations_view_exports, {
|
|
1660
|
+
canRemoveRow: () => canRemoveRow,
|
|
1661
|
+
channelLabelKey: () => channelLabelKey,
|
|
1619
1662
|
fmtOpClock: () => fmtOpClock,
|
|
1620
1663
|
fmtOpDuration: () => fmtOpDuration,
|
|
1621
1664
|
fmtOpFull: () => fmtOpFull,
|
|
@@ -1686,9 +1729,19 @@ function opRowVm(r) {
|
|
|
1686
1729
|
durText: durMs === null ? "" : fmtOpDuration(durMs),
|
|
1687
1730
|
dir: isToggle && typeof meta.on === "boolean" ? meta.on ? "on" : "off" : null,
|
|
1688
1731
|
version,
|
|
1689
|
-
target: typeof rec.target === "string" ? rec.target : ""
|
|
1732
|
+
target: typeof rec.target === "string" ? rec.target : "",
|
|
1733
|
+
// 外部记录渠道标记(0.9.79 ADR-0018):非空 = 渠道徽标渲染 + 无单条 ✕ 出口
|
|
1734
|
+
channel: typeof rec.channel === "string" && rec.channel !== "" ? rec.channel : null
|
|
1690
1735
|
};
|
|
1691
1736
|
}
|
|
1737
|
+
function canRemoveRow(r) {
|
|
1738
|
+
if (!r || typeof r !== "object") return false;
|
|
1739
|
+
if (r.channel) return false;
|
|
1740
|
+
return r.status === "input" || r.status === "failed" || r.status === "superseded";
|
|
1741
|
+
}
|
|
1742
|
+
function channelLabelKey(channel) {
|
|
1743
|
+
return `op.channel.${channel === "host-tool" || channel === "cli" ? channel : "external"}`;
|
|
1744
|
+
}
|
|
1692
1745
|
var init_operations_view = __esm({
|
|
1693
1746
|
"src/client/operations-view.js"() {
|
|
1694
1747
|
"use strict";
|
|
@@ -1902,7 +1955,7 @@ var { RESTART_POLL_MS: RESTART_POLL_MS2, RESTART_DEADLINE_MS: RESTART_DEADLINE_M
|
|
|
1902
1955
|
var { refreshAfterMutation: refreshAfterMutation2 } = (init_view_refresh(), __toCommonJS(view_refresh_exports));
|
|
1903
1956
|
var { applyInstalledUpdates: applyInstalledUpdates2, installedUpdateStats: installedUpdateStats2 } = (init_installed_updates(), __toCommonJS(installed_updates_exports));
|
|
1904
1957
|
var { createOperationsStore: createOperationsStore2, restoreRecords: restoreRecords2, createOpsPump: createOpsPump2, opAppliesTo: opAppliesTo2, TERMINAL_CLEARABLE: TERMINAL_CLEARABLE2, upgradeNotify: upgradeNotify2, REGISTRY_DEFAULT_TARGET: REGISTRY_DEFAULT_TARGET2 } = (init_operations(), __toCommonJS(operations_exports));
|
|
1905
|
-
var { groupOpsByDay: groupOpsByDay2, opRowVm: opRowVm2, opUpgradeMeta: opUpgradeMeta2 } = (init_operations_view(), __toCommonJS(operations_view_exports));
|
|
1958
|
+
var { groupOpsByDay: groupOpsByDay2, opRowVm: opRowVm2, opUpgradeMeta: opUpgradeMeta2, canRemoveRow: canRemoveRow2, channelLabelKey: channelLabelKey2 } = (init_operations_view(), __toCommonJS(operations_view_exports));
|
|
1906
1959
|
var { createFavoritesStore: createFavoritesStore2, partitionStale: partitionStale2 } = (init_favorites(), __toCommonJS(favorites_exports));
|
|
1907
1960
|
var { readSelfCheckCache: readSelfCheckCache2, writeSelfCheckCache: writeSelfCheckCache2, clearSelfCheckCache: clearSelfCheckCache2, deriveChipState: deriveChipState2 } = (init_self_check(), __toCommonJS(self_check_exports));
|
|
1908
1961
|
var { shouldShowInstallCmd: shouldShowInstallCmd2 } = (init_install_cmd(), __toCommonJS(install_cmd_exports));
|
|
@@ -1922,8 +1975,8 @@ var ZH = {
|
|
|
1922
1975
|
"zone.favorites": "\u6536\u85CF",
|
|
1923
1976
|
"badge.deprecated": "\u5DF2\u5F03\u7528",
|
|
1924
1977
|
"sub.snapshot": "v{v}\uFF08\u76EE\u5F55\u5FEB\u7167\uFF09",
|
|
1925
|
-
"badge.verified": "\u5DF2\u5B9E\u6D4B",
|
|
1926
1978
|
"badge.internal": "\u4F5C\u8005\u81EA\u7528",
|
|
1979
|
+
"byline.verified": "\u5B9E\u6D4B \u2264{v}",
|
|
1927
1980
|
"modal.category": "\u5206\u7C7B",
|
|
1928
1981
|
"modal.added": "\u6536\u5F55\u65E5\u671F",
|
|
1929
1982
|
"modal.dlwindow": "\u4E0B\u8F7D\u91CF\uFF0830 \u5929\u7A97\u53E3\uFF09",
|
|
@@ -1951,6 +2004,14 @@ var ZH = {
|
|
|
1951
2004
|
"op.kind.toggle": "\u5F00\u5173",
|
|
1952
2005
|
"op.kind.community-toggle": "\u793E\u533A\u76EE\u5F55\u5F00\u5173",
|
|
1953
2006
|
"op.kind.registry-url": "\u6E05\u5355\u6E90\u53D8\u66F4",
|
|
2007
|
+
"op.kind.self-upgrade": "\u81EA\u5347\u7EA7",
|
|
2008
|
+
"op.kind.desktop-install": "\u684C\u9762\u5B89\u88C5",
|
|
2009
|
+
"op.kind.desktop-uninstall": "\u684C\u9762\u5378\u8F7D",
|
|
2010
|
+
"op.kind.desktop-upgrade": "\u684C\u9762\u5347\u7EA7",
|
|
2011
|
+
"op.kind.desktop-toggle": "\u684C\u9762\u5F00\u5173",
|
|
2012
|
+
"op.channel.host-tool": "Agent \u5DE5\u5177",
|
|
2013
|
+
"op.channel.cli": "\u547D\u4EE4\u884C",
|
|
2014
|
+
"op.channel.external": "\u5916\u90E8",
|
|
1954
2015
|
"op.state.on": "\u5F00",
|
|
1955
2016
|
"op.state.off": "\u5173",
|
|
1956
2017
|
"op.status.queued": "\u6392\u961F\u4E2D",
|
|
@@ -2213,8 +2274,8 @@ var EN = {
|
|
|
2213
2274
|
"zone.favorites": "Favorites",
|
|
2214
2275
|
"badge.deprecated": "Deprecated",
|
|
2215
2276
|
"sub.snapshot": "v{v} (catalog snapshot)",
|
|
2216
|
-
"badge.verified": "Verified",
|
|
2217
2277
|
"badge.internal": "Author's own",
|
|
2278
|
+
"byline.verified": "verified \u2264{v}",
|
|
2218
2279
|
"modal.category": "Category",
|
|
2219
2280
|
"modal.added": "Added",
|
|
2220
2281
|
"modal.dlwindow": "Downloads (30-day window)",
|
|
@@ -2242,6 +2303,14 @@ var EN = {
|
|
|
2242
2303
|
"op.kind.toggle": "Toggle",
|
|
2243
2304
|
"op.kind.community-toggle": "Community catalog toggle",
|
|
2244
2305
|
"op.kind.registry-url": "Registry source change",
|
|
2306
|
+
"op.kind.self-upgrade": "Self-upgrade",
|
|
2307
|
+
"op.kind.desktop-install": "Desktop install",
|
|
2308
|
+
"op.kind.desktop-uninstall": "Desktop uninstall",
|
|
2309
|
+
"op.kind.desktop-upgrade": "Desktop upgrade",
|
|
2310
|
+
"op.kind.desktop-toggle": "Desktop toggle",
|
|
2311
|
+
"op.channel.host-tool": "Agent tool",
|
|
2312
|
+
"op.channel.cli": "CLI",
|
|
2313
|
+
"op.channel.external": "External",
|
|
2245
2314
|
"op.state.on": "On",
|
|
2246
2315
|
"op.state.off": "Off",
|
|
2247
2316
|
"op.status.queued": "Queued",
|
|
@@ -2509,7 +2578,7 @@ function communityLabels(data) {
|
|
|
2509
2578
|
}
|
|
2510
2579
|
var CSS = `
|
|
2511
2580
|
.dshm-overlay{position:fixed;inset:0;z-index:2147483000;background:var(--dsw-alias-bg-mask-3,rgba(15,23,42,.48));display:flex;align-items:center;justify-content:center;padding:max(24px,var(--dsh-windows-titlebar-height,0px)) 16px 24px;box-sizing:border-box}
|
|
2512
|
-
.dshm-panel{width:min(920px,100%);height:min(680px,86vh);display:flex;flex-direction:column;background:var(--dsw-alias-bg-elevated,var(--dsw-alias-bg-base,#fff));background:color-mix(in srgb,var(--dsw-alias-bg-base,#fff) 86%,transparent);backdrop-filter:blur(14px) saturate(1.3);-webkit-backdrop-filter:blur(14px) saturate(1.3);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:14px;box-shadow:0 18px 48px rgba(2,6,23,.25);overflow:hidden;font-family:inherit;color:var(--dsw-alias-label-primary,inherit)}
|
|
2581
|
+
.dshm-panel{position:relative;width:min(920px,100%);height:min(680px,86vh);display:flex;flex-direction:column;background:var(--dsw-alias-bg-elevated,var(--dsw-alias-bg-base,#fff));background:color-mix(in srgb,var(--dsw-alias-bg-base,#fff) 86%,transparent);backdrop-filter:blur(14px) saturate(1.3);-webkit-backdrop-filter:blur(14px) saturate(1.3);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:14px;box-shadow:0 18px 48px rgba(2,6,23,.25);overflow:hidden;font-family:inherit;color:var(--dsw-alias-label-primary,inherit)}
|
|
2513
2582
|
/* 0.9.70 \u8FD8\u539F\u6001\u62D6\u62FD\uFF1A.abs = \u6709\u4F4D\u7F6E\u8BB0\u5FC6\u65F6\u7684\u7EDD\u5BF9\u5B9A\u4F4D\u5F62\u6001\uFF08overlay \u4E3A inset:0 fixed\uFF0C
|
|
2514
2583
|
\u7EDD\u5BF9\u5B9A\u4F4D\u5750\u6807\u5373\u89C6\u53E3\u5750\u6807\uFF09\uFF1B\u65E0\u8BB0\u5F55\u65F6\u9762\u677F\u4ECD\u8D70 overlay flex \u5C45\u4E2D\uFF0C\u6E32\u67D3\u8DEF\u5F84\u4E0E\u65E7\u7248\u9010\u5B57\u8282\u4E00\u81F4\u3002
|
|
2515
2584
|
\u62D6\u62FD\u89C2\u611F\u4E0D\u53D8\u91CF\uFF080.9.70 \u4FEE\u8BA2\uFF0CADR-0017 v2\uFF09\uFF1A\u62D6\u62FD\u6001\u4E0E\u9759\u6B62\u6001\u540C\u73BB\u7483\u540C blur\uFF0C\u5916\u89C2\u9010\u50CF\u7D20\u4E00\u81F4
|
|
@@ -2733,6 +2802,8 @@ var CSS = `
|
|
|
2733
2802
|
.dsvm-opdir{font-size:11px;line-height:16px;padding:0 6px;border-radius:8px;border:1px solid currentColor}
|
|
2734
2803
|
.dsvm-opdir.on{color:#15803d}
|
|
2735
2804
|
.dsvm-opdir.off{color:var(--dsw-alias-label-secondary,#4b5563)}
|
|
2805
|
+
/* 0.9.79 \u5916\u90E8\u8BB0\u5F55\u6E20\u9053\u5FBD\u6807\uFF08Agent \u5DE5\u5177/\u547D\u4EE4\u884C/\u5916\u90E8\uFF09\uFF1A\u4E2D\u6027\u63CF\u8FB9\u80F6\u56CA\uFF0C\u4E0E\u65B9\u5411\u5FBD\u7AE0\u540C\u5F62\u4E0D\u540C\u8272 */
|
|
2806
|
+
.dsvm-opchan{font-size:11px;line-height:16px;padding:0 6px;border-radius:8px;border:1px solid currentColor;color:var(--dsw-alias-label-caption,#9ca3af)}
|
|
2736
2807
|
.dsvm-opver{font-size:11px;color:var(--dsw-alias-label-secondary,#4b5563);font-variant-numeric:tabular-nums;overflow-wrap:anywhere}
|
|
2737
2808
|
.dsvm-favbtn{appearance:none;border:0;background:transparent;color:var(--dsw-alias-label-caption,#9ca3af);font-size:15px;line-height:1;cursor:pointer;padding:0 2px;margin-left:auto}
|
|
2738
2809
|
.dsvm-favbtn:hover{color:var(--dsw-alias-state-business-primary,#4d6bfe)}
|
|
@@ -2760,6 +2831,8 @@ button.dshm-badge:hover{filter:brightness(.95)}
|
|
|
2760
2831
|
.dshm-sub{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--dsw-alias-label-caption,#6b7280)}
|
|
2761
2832
|
.dshm-detail{margin-top:8px;border-top:1px dashed var(--dsw-alias-border-l2,#e5e7eb);padding-top:8px;display:flex;flex-direction:column;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary,#4b5563)}
|
|
2762
2833
|
.dshm-actions{display:flex;gap:6px;flex-wrap:wrap;margin-top:4px}
|
|
2834
|
+
.dshm-banner-stack{position:absolute;left:0;right:0;bottom:0;z-index:30;display:flex;flex-direction:column;pointer-events:none}
|
|
2835
|
+
.dshm-banner-stack .dshm-banner{pointer-events:auto}
|
|
2763
2836
|
.dshm-banner{display:flex;align-items:center;gap:10px;padding:10px 14px;border-top:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:var(--dsw-alias-state-warn-tertiary,#fffbeb);color:var(--dsw-alias-state-warn-primary,#b45309);font-size:12px}
|
|
2764
2837
|
.dshm-banner.err{background:rgba(239,68,68,.13);color:var(--dsw-alias-state-error-primary,#b91c1c)}
|
|
2765
2838
|
/* 0.9.72 \u540C\u6B3E\u6EDA\u8F6E\u6CBB\u7406\uFF08\u89C1 dshm-ops-scroll \u6CE8\uFF09\uFF1A\u6A2A\u5E45\u6587\u672C\u4E0D\u6EE1 140px \u65F6\u4E0D\u5F97\u541E\u6EDA\u8F6E */
|
|
@@ -2857,6 +2930,9 @@ button.dshm-dshchip:disabled{cursor:default;opacity:.75}
|
|
|
2857
2930
|
.dshm-dot.idle{color:#9ca3af}
|
|
2858
2931
|
.dshm-dot.busy{color:#f59e0b}
|
|
2859
2932
|
.dshm-switch{position:relative;display:inline-flex;align-items:center;width:34px;height:19px;border-radius:10px;background:var(--dsw-alias-fill-secondary,#d1d5db);cursor:pointer;transition:background .15s;flex:none;margin-left:auto}
|
|
2933
|
+
/* \u70ED\u533A\u6269\u5C55\uFF082026-10-13 UX \u4FEE\u590D\uFF09\uFF1A\u89C6\u89C9 34\xD719 \u4E0D\u53D8\uFF0C\u53EF\u70B9\u9762\u79EF\u7ECF\u4F2A\u5143\u7D20\u6269\u5230 46\xD731\u2014\u2014
|
|
2934
|
+
\u5C0F\u76EE\u6807 + \u6574\u5361\u5927\u76EE\u6807\u76F8\u90BB\u7684\u8BEF\u89E6\u534A\u5F84\u6536\u655B\uFF1B\u5F00\u5173\u8DDD\u5361\u8FB9 12px\uFF0C-6px \u5916\u6EA2\u4E0D\u8D8A\u51FA\u81EA\u8EAB\u5361\u7247 */
|
|
2935
|
+
.dshm-switch::before{content:"";position:absolute;inset:-6px;border-radius:14px}
|
|
2860
2936
|
.dshm-switch input{display:none}
|
|
2861
2937
|
.dshm-switch.on{background:#22c55e}
|
|
2862
2938
|
.dshm-switch.locked{opacity:.45;cursor:not-allowed}
|
|
@@ -3675,7 +3751,6 @@ function DetailModal({ it, labels, busy, onClose, onInstall, onUpgrade, upgradeB
|
|
|
3675
3751
|
h(Icon, { entry: it }),
|
|
3676
3752
|
h("span", { className: "dshm-name" }, it.name),
|
|
3677
3753
|
it.deprecated === true ? h("span", { className: "dshm-badge warn" }, lookup("badge.deprecated")) : null,
|
|
3678
|
-
it.community !== true && Array.isArray(it.verified) && it.verified.length ? h("span", { className: "dshm-badge", title: it.verified.join("\u3001") }, lookup("badge.verified")) : null,
|
|
3679
3754
|
it.audience === "internal" ? h("span", { className: "dshm-badge" }, lookup("badge.internal")) : null,
|
|
3680
3755
|
it.installed ? h("span", { className: "dshm-badge" }, lookup("badge.installed")) : null,
|
|
3681
3756
|
it.community === true ? h("span", { className: "dshm-badge info" }, lookup("badge.community")) : null,
|
|
@@ -3772,6 +3847,7 @@ var ZONE_TABS = [
|
|
|
3772
3847
|
{ id: "primary", labelKey: "zone.primary" },
|
|
3773
3848
|
{ id: "favorites", labelKey: "zone.favorites" }
|
|
3774
3849
|
];
|
|
3850
|
+
var verifiedByline = (it) => it.community !== true && Array.isArray(it.verified) && it.verified.length ? { text: lookup("byline.verified", { v: it.verified[it.verified.length - 1] }), title: it.verified.join("\u3001") } : null;
|
|
3775
3851
|
function MarketTab({ notify, markets, onMutation, ops, favorites, profileKind, onOpenOps }) {
|
|
3776
3852
|
const [zone, setZone] = useState("community");
|
|
3777
3853
|
const market = zone === "favorites" ? null : markets[zone];
|
|
@@ -4072,7 +4148,6 @@ function MarketTab({ notify, markets, onMutation, ops, favorites, profileKind, o
|
|
|
4072
4148
|
lookup("op.status.running")
|
|
4073
4149
|
) : null,
|
|
4074
4150
|
it.deprecated === true ? h("span", { className: "dshm-badge warn", key: "dep" }, lookup("badge.deprecated")) : null,
|
|
4075
|
-
it.community !== true && Array.isArray(it.verified) && it.verified.length ? h("span", { className: "dshm-badge", key: "v", title: it.verified.join("\u3001") }, lookup("badge.verified")) : null,
|
|
4076
4151
|
it.audience === "internal" ? h("span", { className: "dshm-badge dshm-badge-internal", key: "internal" }, lookup("badge.internal")) : null,
|
|
4077
4152
|
it.outdated ? h("span", { className: "dshm-badge warn", key: "u" }, lookup("badge.update")) : null,
|
|
4078
4153
|
it.installed ? h("span", { className: "dshm-badge", key: "i" }, lookup("badge.installed")) : null,
|
|
@@ -4084,7 +4159,10 @@ function MarketTab({ notify, markets, onMutation, ops, favorites, profileKind, o
|
|
|
4084
4159
|
it.owner ? { text: `by ${it.owner}` } : null,
|
|
4085
4160
|
typeof it.downloads === "number" ? { text: `${compactCount(it.downloads)} \u2193`, title: String(it.downloads) } : null,
|
|
4086
4161
|
typeof it.stars === "number" ? { text: `${compactCount(it.stars)} \u2605`, title: String(it.stars) } : null
|
|
4087
|
-
].filter(Boolean) :
|
|
4162
|
+
].filter(Boolean) : [
|
|
4163
|
+
...(it.tags || []).map((t) => ({ text: `#${t}` })),
|
|
4164
|
+
verifiedByline(it)
|
|
4165
|
+
].filter(Boolean),
|
|
4088
4166
|
desc: browserLang() === "en" && typeof it.descriptionEn === "string" && it.descriptionEn !== "" ? it.descriptionEn : it.description,
|
|
4089
4167
|
clampLines: it.community === true ? 5 : 2,
|
|
4090
4168
|
sub: [
|
|
@@ -4254,8 +4332,11 @@ function InstalledTab({ notify, installed, updates, onMutation, ops }) {
|
|
|
4254
4332
|
const updateRows = updates && updates.data && Array.isArray(updates.data.updates) ? updates.data.updates : [];
|
|
4255
4333
|
const [openPkg, setOpenPkg] = useState(null);
|
|
4256
4334
|
const [readmePkg, setReadmePkg] = useState(null);
|
|
4335
|
+
const [pendingToggles, setPendingToggles] = useState({});
|
|
4257
4336
|
const activeMutateTarget = (ops.records.find((r) => r.kind !== "install" && (r.status === "running" || r.status === "queued" || r.status === "input")) || {}).target || null;
|
|
4337
|
+
const progressTarget = (ops.records.find((r) => (r.kind === "install" || r.kind === "upgrade" || r.kind === "self-upgrade") && (r.status === "running" || r.status === "queued" || r.status === "input")) || {}).target || null;
|
|
4258
4338
|
const doToggle = async (it, enabled) => {
|
|
4339
|
+
setPendingToggles((m) => ({ ...m, [it.pkg]: enabled }));
|
|
4259
4340
|
const call = () => api("set-enabled", { pkg: it.pkg, enabled });
|
|
4260
4341
|
try {
|
|
4261
4342
|
let res;
|
|
@@ -4269,13 +4350,19 @@ function InstalledTab({ notify, installed, updates, onMutation, ops }) {
|
|
|
4269
4350
|
const note = toggleNoticeKeys2(res);
|
|
4270
4351
|
const extra = res.warnings && res.warnings.length ? `\uFF08${res.warnings.join("\uFF1B")}\uFF09` : "";
|
|
4271
4352
|
notify({ kind: "ok", needsRestart: note.needsRestart, text: lookup(note.textKey, note.params) + extra });
|
|
4272
|
-
await
|
|
4353
|
+
await reload();
|
|
4273
4354
|
} catch (e) {
|
|
4274
4355
|
if (e && e.opSuperseded) {
|
|
4275
4356
|
notify({ kind: "ok", text: lookup("op.superseded.note", { target: it.pkg }) });
|
|
4276
4357
|
} else {
|
|
4277
4358
|
notify({ kind: "err", text: lookup("toggle.failed", { err: e && e.message || e }) });
|
|
4278
4359
|
}
|
|
4360
|
+
} finally {
|
|
4361
|
+
setPendingToggles((m) => {
|
|
4362
|
+
if (!(it.pkg in m)) return m;
|
|
4363
|
+
const { [it.pkg]: _drop, ...rest } = m;
|
|
4364
|
+
return rest;
|
|
4365
|
+
});
|
|
4279
4366
|
}
|
|
4280
4367
|
};
|
|
4281
4368
|
const doUninstall = async (it) => {
|
|
@@ -4356,13 +4443,15 @@ function InstalledTab({ notify, installed, updates, onMutation, ops }) {
|
|
|
4356
4443
|
lookup("installed.upgradeAll", { n: items.filter((x) => x && x.outdated).length })
|
|
4357
4444
|
)
|
|
4358
4445
|
) : null,
|
|
4359
|
-
|
|
4446
|
+
progressTarget ? h(ProgressLine, { key: "prog" }) : null,
|
|
4360
4447
|
h(
|
|
4361
4448
|
"div",
|
|
4362
4449
|
{ className: "dshm-cards" },
|
|
4363
4450
|
items.map((it) => {
|
|
4364
4451
|
const vm = installedViewModel2(it);
|
|
4365
|
-
const
|
|
4452
|
+
const pendingOn = pendingToggles[it.pkg];
|
|
4453
|
+
const tvm0 = toggleViewModel2(it);
|
|
4454
|
+
const tvm = pendingOn === void 0 ? tvm0 : { ...tvm0, switchOn: pendingOn, ...pendingOn === false ? { phaseDotClass: "", phaseLabelKey: null } : {} };
|
|
4366
4455
|
return Card({
|
|
4367
4456
|
key: it.pkg,
|
|
4368
4457
|
icon: h(Icon, { entry: { name: it.name, github: vm.githubRepo, icon: null } }),
|
|
@@ -4379,7 +4468,7 @@ function InstalledTab({ notify, installed, updates, onMutation, ops }) {
|
|
|
4379
4468
|
desc: it.description || "\uFF08\u65E0\u63CF\u8FF0\uFF09",
|
|
4380
4469
|
sub: [
|
|
4381
4470
|
h(PhaseDot, { key: "dot", vm: tvm }),
|
|
4382
|
-
vm.phaseKey ? lookup(vm.phaseKey) : lookup(vm.enabledLabelKey),
|
|
4471
|
+
pendingOn === void 0 ? vm.phaseKey ? lookup(vm.phaseKey) : lookup(vm.enabledLabelKey) : lookup(pendingOn ? "toggle.state.on" : "toggle.state.off"),
|
|
4383
4472
|
`v${it.version || "?"}`,
|
|
4384
4473
|
vm.sourceLabelKey ? lookup(vm.sourceLabelKey) : it.source,
|
|
4385
4474
|
vm.latestIssue ? lookup("installed.check.incomplete") : null
|
|
@@ -4557,17 +4646,10 @@ function SettingsTab({ notify, onRegistryChanged, onForceMarket, opsRecords, onO
|
|
|
4557
4646
|
}),
|
|
4558
4647
|
h("span", { className: "dshm-switch-slider" })
|
|
4559
4648
|
);
|
|
4560
|
-
const communitySwitchRow = h(
|
|
4561
|
-
"div",
|
|
4562
|
-
{ className: "dshm-kvline-row" },
|
|
4563
|
-
h("span", { className: "rl" }, lookup("settings.community.toggle")),
|
|
4564
|
-
communitySwitch
|
|
4565
|
-
);
|
|
4566
4649
|
const communityBody = communityOn === false ? h(
|
|
4567
4650
|
React.Fragment,
|
|
4568
4651
|
null,
|
|
4569
|
-
h("div", { className: "dshm-hint" }, lookup("settings.community.off"))
|
|
4570
|
-
communitySwitchRow
|
|
4652
|
+
h("div", { className: "dshm-hint" }, lookup("settings.community.off"))
|
|
4571
4653
|
) : h(
|
|
4572
4654
|
"div",
|
|
4573
4655
|
{ className: "dshm-kvlines" },
|
|
@@ -4580,17 +4662,16 @@ function SettingsTab({ notify, onRegistryChanged, onForceMarket, opsRecords, onO
|
|
|
4580
4662
|
kvlineRow("settings.community.version", communityLive ? communitySummary.version || "\u2014" : "\u2014"),
|
|
4581
4663
|
kvlineRow("settings.community.route", communityLive ? communitySummary.route || "\u2014" : "\u2014"),
|
|
4582
4664
|
kvlineRow("settings.community.count", communityLive ? lookup("settings.community.count.v", { n: communitySummary.acceptedCount ?? 0, up: communitySummary.upstreamCount ?? "\u2014" }) : "\u2014"),
|
|
4583
|
-
kvlineRow("settings.community.displaced", communityLive ? lookup("settings.community.displaced.v", { n: communitySummary.displaced ?? 0 }) : "\u2014")
|
|
4584
|
-
communitySwitchRow
|
|
4665
|
+
kvlineRow("settings.community.displaced", communityLive ? lookup("settings.community.displaced.v", { n: communitySummary.displaced ?? 0 }) : "\u2014")
|
|
4585
4666
|
);
|
|
4586
4667
|
return h(
|
|
4587
4668
|
"div",
|
|
4588
4669
|
{ className: "dshm-settings-grid" },
|
|
4589
|
-
// ① 社区清单(awesome-dsh-plugin
|
|
4670
|
+
// ① 社区清单(awesome-dsh-plugin):左卡;开关在标题行右侧(位置恒定,2026-10-13 方案A)
|
|
4590
4671
|
h(
|
|
4591
4672
|
"div",
|
|
4592
4673
|
{ className: "dshm-section" },
|
|
4593
|
-
h("div", { className: "dshm-section-title" }, lookup("settings.community")),
|
|
4674
|
+
h("div", { className: "dshm-section-title" }, lookup("settings.community"), communitySwitch),
|
|
4594
4675
|
communityBody,
|
|
4595
4676
|
communitySummary && Array.isArray(communitySummary.errors) && communitySummary.errors.length && communityOn !== false ? h("div", { className: "dshm-err" }, communitySummary.errors.join("\uFF1B")) : null
|
|
4596
4677
|
),
|
|
@@ -4794,10 +4875,10 @@ function FavoriteZone({ favorites, onOpen }) {
|
|
|
4794
4875
|
name: s.name,
|
|
4795
4876
|
badges: [
|
|
4796
4877
|
s.deprecated === true ? h("span", { className: "dshm-badge warn", key: "dep" }, lookup("badge.deprecated")) : null,
|
|
4797
|
-
// 0.9.45 U12(R7 收窄):无 owner = 精选条目——补「精选」身份徽标;
|
|
4878
|
+
// 0.9.45 U12(R7 收窄):无 owner = 精选条目——补「精选」身份徽标;
|
|
4879
|
+
// verified 徽标已移除(2026-10-13 B 方案:卡面只留例外信号,实测信息在详情弹窗「实测版本」行);
|
|
4798
4880
|
// 受众/解耦徽标不进收藏区(DESIGN §2.7 裁决⑤「已装视图与收藏页不打标」),快照仅存字段
|
|
4799
4881
|
s.owner ? h("span", { className: "dshm-badge info", key: "c" }, lookup("badge.community")) : h("span", { className: "dshm-badge", key: "cz" }, lookup("zone.primary")),
|
|
4800
|
-
Array.isArray(s.verified) && s.verified.length && s.community !== true ? h("span", { className: "dshm-badge", key: "v", title: s.verified.join("\u3001") }, lookup("badge.verified")) : null,
|
|
4801
4882
|
h("span", { className: "dshm-badge info", key: "s" }, s.source === "npm" ? "npm" : "github"),
|
|
4802
4883
|
isStale ? h("span", { className: "dshm-badge warn", key: "st" }, lookup("favorites.stalebadge")) : null
|
|
4803
4884
|
],
|
|
@@ -4860,7 +4941,8 @@ function useScrollableContain(dep) {
|
|
|
4860
4941
|
}
|
|
4861
4942
|
function OperationsCard({ records, onClearFinished, onRemove }) {
|
|
4862
4943
|
const active = records.filter((r) => r.status === "queued" || r.status === "running" || r.status === "input");
|
|
4863
|
-
const
|
|
4944
|
+
const finishedAll = records.filter((r) => active.indexOf(r) < 0);
|
|
4945
|
+
const finished = finishedAll.slice(-50);
|
|
4864
4946
|
const clearable = records.some((r) => TERMINAL_CLEARABLE2.has(r.status));
|
|
4865
4947
|
const [scrollRef, scrollCls] = useScrollableContain(finished.length);
|
|
4866
4948
|
const dayGroups = useMemo(() => groupOpsByDay2(finished), [records]);
|
|
@@ -4873,6 +4955,7 @@ function OperationsCard({ records, onClearFinished, onRemove }) {
|
|
|
4873
4955
|
vm.clock ? h("span", { className: "dsvm-optime", title: vm.full }, vm.clock) : null,
|
|
4874
4956
|
h("span", { className: "dsvm-opstatus" }, lookup("op.status." + r.status)),
|
|
4875
4957
|
h("span", { className: "dsvm-opkind" }, lookup("op.kind." + r.kind)),
|
|
4958
|
+
vm.channel ? h("span", { className: "dsvm-opchan", title: lookup(channelLabelKey2(vm.channel)) }, lookup(channelLabelKey2(vm.channel))) : null,
|
|
4876
4959
|
vm.dir ? h("span", { className: `dsvm-opdir ${vm.dir}` }, lookup(vm.dir === "on" ? "op.state.on" : "op.state.off")) : null,
|
|
4877
4960
|
h("span", { className: "dsvm-optarget", title: tip || void 0 }, vm.target),
|
|
4878
4961
|
vm.version ? h("span", { className: "dsvm-opver", title: tip || void 0 }, vm.version) : null,
|
|
@@ -4880,8 +4963,10 @@ function OperationsCard({ records, onClearFinished, onRemove }) {
|
|
|
4880
4963
|
r.status === "running" ? Spin() : null,
|
|
4881
4964
|
r.error ? h("span", { className: "dsvm-opnote" }, r.error) : null,
|
|
4882
4965
|
r.warning ? h("span", { className: "dsvm-opnote" }, r.warning) : null,
|
|
4883
|
-
// 单条移除(终审·新伤2
|
|
4884
|
-
|
|
4966
|
+
// 单条移除(终审·新伤2 → 0.9.79 R3-1/R2-3):出口判定下沉 canRemoveRow——
|
|
4967
|
+
// 本地 input/failed/superseded 保留 ✕;外部记录(channel 非空)不给单条 ✕(防按 id 复活),
|
|
4968
|
+
// 移除出口仅「清除已结束」。
|
|
4969
|
+
canRemoveRow2(r) && onRemove ? h("button", { className: "dshm-xbtn", title: lookup("op.remove"), onClick: () => onRemove(r.id) }, h(XIcon)) : null
|
|
4885
4970
|
);
|
|
4886
4971
|
};
|
|
4887
4972
|
return h(
|
|
@@ -4893,7 +4978,7 @@ function OperationsCard({ records, onClearFinished, onRemove }) {
|
|
|
4893
4978
|
finished.length ? h(
|
|
4894
4979
|
"div",
|
|
4895
4980
|
{ className: "dsvm-ops-caption", style: { marginTop: active.length ? "7px" : "0" } },
|
|
4896
|
-
lookup("settings.ops.group.done.n", { n:
|
|
4981
|
+
lookup("settings.ops.group.done.n", { n: finishedAll.length })
|
|
4897
4982
|
) : null,
|
|
4898
4983
|
finished.length ? h(
|
|
4899
4984
|
"div",
|
|
@@ -5211,8 +5296,24 @@ function MarketPanel({ onClose }) {
|
|
|
5211
5296
|
const markets = { community: marketCommunity, primary: marketPrimary };
|
|
5212
5297
|
const installed = useAsync(() => api("installed", { probe: false }), []);
|
|
5213
5298
|
const updates = useAsync(() => api("installedUpdates"), []);
|
|
5214
|
-
const [opRecords, setOpRecords] = useState(() => opsStore.
|
|
5215
|
-
const syncOps = useCallback(() => setOpRecords(opsStore.
|
|
5299
|
+
const [opRecords, setOpRecords] = useState(() => opsStore.listMerged().map((r) => ({ ...r })));
|
|
5300
|
+
const syncOps = useCallback(() => setOpRecords(opsStore.listMerged().map((r) => ({ ...r }))), []);
|
|
5301
|
+
const fetchExternalOps = useCallback(async () => {
|
|
5302
|
+
try {
|
|
5303
|
+
const r = await api("external-operations");
|
|
5304
|
+
if (r && Array.isArray(r.records)) {
|
|
5305
|
+
opsStore.setExternal(r.records);
|
|
5306
|
+
syncOps();
|
|
5307
|
+
}
|
|
5308
|
+
} catch {
|
|
5309
|
+
}
|
|
5310
|
+
}, [syncOps]);
|
|
5311
|
+
useEffect(() => {
|
|
5312
|
+
void fetchExternalOps();
|
|
5313
|
+
}, [fetchExternalOps]);
|
|
5314
|
+
useEffect(() => {
|
|
5315
|
+
if (tab === "settings") void fetchExternalOps();
|
|
5316
|
+
}, [tab, fetchExternalOps]);
|
|
5216
5317
|
const openOps = useCallback(() => {
|
|
5217
5318
|
setTab("settings");
|
|
5218
5319
|
if (typeof document !== "undefined") {
|
|
@@ -5227,9 +5328,13 @@ function MarketPanel({ onClose }) {
|
|
|
5227
5328
|
(kind, target, exec, meta = {}, reuseId) => (
|
|
5228
5329
|
// session:true = 活会话标记(终审·新伤1):重挂载的 restore 跳过(泵仍拥有);persist 时剥离。
|
|
5229
5330
|
// 入队与 waiters/原始错误保真都在 createOpsPump(评审 P4:实现与测试同源)。
|
|
5230
|
-
|
|
5331
|
+
// 0.9.79(R1-6):操作终态后连带拉取一次外部记录——他终端(CLI/Agent 工具)的并发变更
|
|
5332
|
+
// 尽快可见;拉取失败静默。
|
|
5333
|
+
opsPump.enqueue({ ...reuseId ? { id: reuseId } : {}, kind, target, status: "queued", meta: { ...meta, session: true } }, exec).finally(() => {
|
|
5334
|
+
void fetchExternalOps();
|
|
5335
|
+
})
|
|
5231
5336
|
),
|
|
5232
|
-
[]
|
|
5337
|
+
[fetchExternalOps]
|
|
5233
5338
|
);
|
|
5234
5339
|
const disposeOp = useCallback((id, note) => {
|
|
5235
5340
|
opsStore.upsert({ id, status: "superseded", error: note });
|
|
@@ -5443,7 +5548,10 @@ function MarketPanel({ onClose }) {
|
|
|
5443
5548
|
opsRecords: opRecords,
|
|
5444
5549
|
onOpsClearFinished: () => {
|
|
5445
5550
|
opsStore.clearFinished();
|
|
5551
|
+
opsStore.clearExternal();
|
|
5446
5552
|
syncOps();
|
|
5553
|
+
void api("external-operations-clear").catch(() => {
|
|
5554
|
+
});
|
|
5447
5555
|
},
|
|
5448
5556
|
onOpsRemove: (id) => {
|
|
5449
5557
|
opsStore.remove(id);
|
|
@@ -5452,12 +5560,19 @@ function MarketPanel({ onClose }) {
|
|
|
5452
5560
|
}) : null
|
|
5453
5561
|
// 操作记录已整体搬迁至设置 tab 末位通宽卡(0.9.x 布局重构·第二批):底部不再常驻
|
|
5454
5562
|
),
|
|
5455
|
-
toast
|
|
5563
|
+
// toast/横幅 overlay 化(2026-10-13 UX 修复):原先是面板 column flex 的流内子元素,
|
|
5564
|
+
// 出现/消失会压缩 body(flex:1) 造成二次抖动;现挂 absolute 底部浮层(.dshm-banner-stack),
|
|
5565
|
+
// 零布局参与。stack 用 pointer-events:none 放行下层点击,banner 自身恢复 auto。
|
|
5566
|
+
toast || banner ? h(
|
|
5456
5567
|
"div",
|
|
5457
|
-
{ className:
|
|
5458
|
-
h(
|
|
5459
|
-
|
|
5460
|
-
|
|
5568
|
+
{ className: "dshm-banner-stack" },
|
|
5569
|
+
toast ? h(
|
|
5570
|
+
"div",
|
|
5571
|
+
{ className: toast.kind === "err" ? "dshm-banner err" : "dshm-banner" },
|
|
5572
|
+
h("span", { className: `dshm-banner-text${toastScrollCls}`, ref: toastTextRef }, toast.text)
|
|
5573
|
+
) : null,
|
|
5574
|
+
banner ? h(RestartBanner, { note: banner.text, onDone: () => setBanner(null), desktop: banner.desktop === true, onRestarted: reloadPing }) : null
|
|
5575
|
+
) : null
|
|
5461
5576
|
)
|
|
5462
5577
|
);
|
|
5463
5578
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 机器本地的外部操作日志(0.9.79,ADR-0018):外部通道(dshm CLI / Agent 工具)发起的
|
|
3
|
+
* Profile 变更以终态直落记录进入本文件——不入操作泵、永不 queued/running、不参与恢复重放、
|
|
4
|
+
* 宿主重启后原样保留;记录机器本地、不跨机同步;不含凭据与原始命令输出。
|
|
5
|
+
*
|
|
6
|
+
* - 存储文件:`$DSH_HOME/dshm/external-operations.json`(`DSHM_EXTERNAL_JOURNAL_FILE` 可整文件
|
|
7
|
+
* 覆盖供测试;与 dshm/cache 同基址、不带 profile 维度、不被缓存清理波及)。
|
|
8
|
+
* - append:mkdir recursive 先行(R2-4:全新机器上 `dshm/` 目录可能尚不存在,首条审计记录
|
|
9
|
+
* 恰是最有价值的一条,不能静默丢失)→ tmp+rename 原子替换(community.ts 同款惯例)→
|
|
10
|
+
* 滚动保留最新 200 条。channel='panel' 拒绝(防御性不变量:面板通道不走外部日志,
|
|
11
|
+
* 面板调用点应在钩子层被跳过)。错误一律上抛——吞错是上层钩子的职责(R1-1)。
|
|
12
|
+
* - list:按 `at` 降序(新→旧);文件缺失 = 空列表;损坏文件不抛——改名
|
|
13
|
+
* `<file>.corrupt-<ts>` 隔离留证后从空开始(best-effort,隔离失败同样不抛)。
|
|
14
|
+
* - clear:写空数组,文件保留(面板「清除已结束」联动出口)。
|
|
15
|
+
*/
|
|
16
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
17
|
+
import { dirname, join } from 'node:path';
|
|
18
|
+
import { dshHome } from './env.js';
|
|
19
|
+
const MAX_RECORDS = 200;
|
|
20
|
+
const REASON_MAX = 300;
|
|
21
|
+
/** 日志文件路径:env 整文件覆盖优先(测试隔离),缺省 $DSH_HOME/dshm/external-operations.json。 */
|
|
22
|
+
export function journalFile() {
|
|
23
|
+
return process.env.DSHM_EXTERNAL_JOURNAL_FILE || join(dshHome(), 'dshm', 'external-operations.json');
|
|
24
|
+
}
|
|
25
|
+
/** 显式字段白名单重组(超集字段一律不落盘——「记录不含凭据」的形状级保证);reason 截断 ≤300。 */
|
|
26
|
+
function normalizeRecord(record) {
|
|
27
|
+
const reason = typeof record.reason === 'string' ? record.reason.slice(0, REASON_MAX) : undefined;
|
|
28
|
+
return {
|
|
29
|
+
id: record.id,
|
|
30
|
+
at: record.at,
|
|
31
|
+
channel: record.channel,
|
|
32
|
+
kind: record.kind,
|
|
33
|
+
target: record.target,
|
|
34
|
+
...(record.fromVersion !== undefined ? { fromVersion: record.fromVersion } : {}),
|
|
35
|
+
...(record.toVersion !== undefined ? { toVersion: record.toVersion } : {}),
|
|
36
|
+
result: record.result,
|
|
37
|
+
...(reason !== undefined && reason !== '' ? { reason } : {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async function atomicWrite(path, text) {
|
|
41
|
+
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
42
|
+
await writeFile(tmp, text);
|
|
43
|
+
await rename(tmp, path);
|
|
44
|
+
}
|
|
45
|
+
/** 读全量记录(插入序 = 时间升序);损坏 → 隔离改名后按空处理;文件缺失 → []。 */
|
|
46
|
+
async function readAll(file) {
|
|
47
|
+
let text;
|
|
48
|
+
try {
|
|
49
|
+
text = await readFile(file, 'utf8');
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
if (err.code === 'ENOENT')
|
|
53
|
+
return [];
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(text);
|
|
58
|
+
return Array.isArray(parsed) ? parsed.filter((r) => r && typeof r === 'object') : [];
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// 损坏容错:整文件改名隔离留证(best-effort),从空开始;不抛——读路径永不炸
|
|
62
|
+
const quarantined = `${file}.corrupt-${Date.now()}`;
|
|
63
|
+
try {
|
|
64
|
+
await rename(file, quarantined);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
try {
|
|
68
|
+
// 同毫秒重入等极小概率命名冲突:换一个带随机尾缀的名字再试一次;仍败则放弃隔离
|
|
69
|
+
const fallback = `${file}.corrupt-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
70
|
+
await rename(file, fallback);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
/* 放弃隔离:按空继续 */
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 追加一条终态记录(成功与失败都记)。channel='panel' 抛错(防御性不变量);
|
|
81
|
+
* 不可创建路径/磁盘错误等存储失败原样上抛,由上层钩子吞(R1-1:绝不反噬操作终态)。
|
|
82
|
+
*/
|
|
83
|
+
export async function appendExternalOperation(record) {
|
|
84
|
+
if (record.channel === 'panel') {
|
|
85
|
+
throw new Error('panel 通道不入外部操作日志(防御性不变量:面板调用点应在钩子层跳过 append)');
|
|
86
|
+
}
|
|
87
|
+
const file = journalFile();
|
|
88
|
+
await mkdir(dirname(file), { recursive: true });
|
|
89
|
+
const records = await readAll(file);
|
|
90
|
+
records.push(normalizeRecord(record));
|
|
91
|
+
const kept = records.slice(-MAX_RECORDS);
|
|
92
|
+
await atomicWrite(file, `${JSON.stringify(kept, null, 2)}\n`);
|
|
93
|
+
}
|
|
94
|
+
/** 全量外部记录,按 `at` 降序(新→旧)。 */
|
|
95
|
+
export async function listExternalOperations() {
|
|
96
|
+
const records = await readAll(journalFile());
|
|
97
|
+
return records
|
|
98
|
+
.slice()
|
|
99
|
+
.sort((a, b) => (b.at ?? 0) - (a.at ?? 0))
|
|
100
|
+
.map((r) => ({ ...r }));
|
|
101
|
+
}
|
|
102
|
+
/** 清空外部记录:文件保留(空数组),面板「清除已结束」联动出口。 */
|
|
103
|
+
export async function clearExternalOperations() {
|
|
104
|
+
const file = journalFile();
|
|
105
|
+
await mkdir(dirname(file), { recursive: true });
|
|
106
|
+
await atomicWrite(file, '[]\n');
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 九个公共变更入口共用的终态直落钩子(0.9.79,ADR-0018):
|
|
110
|
+
* - channel 缺省 'external'(审计上漏报更糟,未知调用方落兜底而非丢弃);
|
|
111
|
+
* - channel='panel' 在钩子层**跳过**(面板通道零记录;append 侧的 panel 抛错仅为防御性不变量);
|
|
112
|
+
* - append 任何失败被吞(至多 stderr 一行)——**绝不反噬操作终态与返回值**(R1-1);
|
|
113
|
+
* - reason 截断 ≤300 字符(R1-8)。
|
|
114
|
+
* 调用时机 = 操作终态之后(成功与失败都记)。
|
|
115
|
+
*/
|
|
116
|
+
export async function journalExternalOperation(channel, record, result, deps = {}) {
|
|
117
|
+
const ch = channel ?? 'external';
|
|
118
|
+
if (ch === 'panel')
|
|
119
|
+
return;
|
|
120
|
+
try {
|
|
121
|
+
const reason = typeof deps.reason === 'string' ? deps.reason.slice(0, REASON_MAX) : undefined;
|
|
122
|
+
await (deps.appendExternalOperation ?? appendExternalOperation)({
|
|
123
|
+
id: `ext-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
124
|
+
at: Date.now(),
|
|
125
|
+
channel: ch,
|
|
126
|
+
kind: record.kind,
|
|
127
|
+
target: record.target,
|
|
128
|
+
...(record.fromVersion !== undefined ? { fromVersion: record.fromVersion } : {}),
|
|
129
|
+
...(record.toVersion !== undefined ? { toVersion: record.toVersion } : {}),
|
|
130
|
+
result,
|
|
131
|
+
...(reason !== undefined && reason !== '' ? { reason } : {}),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
console.error(`[dsh-m] 外部操作记录落盘失败(不影响操作结果): ${err instanceof Error ? err.message : String(err)}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
package/lib/core/host-api.js
CHANGED
|
@@ -12,6 +12,7 @@ import { resolveDshVersion, readLauncherPackageVersion } from './dsh-version.js'
|
|
|
12
12
|
import { runDoctor } from './doctor.js';
|
|
13
13
|
import { listInstalledWithMeta, listMarket, installFromRegistry, selfUpgrade, uninstallPlugin, upgradePlugin, InstallGuardError, } from './market.js';
|
|
14
14
|
import { runProfileTransaction, TransactionError } from './profile-transaction.js';
|
|
15
|
+
import { listExternalOperations, clearExternalOperations } from './external-journal.js';
|
|
15
16
|
import { readInstalledPluginReadme } from './installed.js';
|
|
16
17
|
import { loadPrimaryScreenshots } from './primary-screenshots.js';
|
|
17
18
|
import { isNewerVersion, npmLatest, npmPackumentReadme } from './versions.js';
|
|
@@ -231,6 +232,8 @@ export function createApiDispatcher(ctx) {
|
|
|
231
232
|
desktopUninstall: ctx.deps?.desktopUninstall ?? desktopUninstall,
|
|
232
233
|
desktopUpgrade: ctx.deps?.desktopUpgrade ?? desktopUpgradeFromRegistry,
|
|
233
234
|
ensureService: ctx.deps?.ensureService,
|
|
235
|
+
listExternalOperations: ctx.deps?.listExternalOperations ?? listExternalOperations,
|
|
236
|
+
clearExternalOperations: ctx.deps?.clearExternalOperations ?? clearExternalOperations,
|
|
234
237
|
};
|
|
235
238
|
const cfg = () => ctx.controller.config;
|
|
236
239
|
// 0.9.0:宿主信任检查委派(含 ping 与未知 method,fail-closed);
|
|
@@ -284,6 +287,7 @@ export function createApiDispatcher(ctx) {
|
|
|
284
287
|
profile: profile.name,
|
|
285
288
|
profileDir: profile.dir,
|
|
286
289
|
signal,
|
|
290
|
+
channel: 'panel', // 0.9.79:面板自升级零外部记录(漏标 = 落 external,违反面板零记录不变量)
|
|
287
291
|
}, { getService: d.getService, ensureService: d.ensureService });
|
|
288
292
|
if ('needsBuildApproval' in managed) {
|
|
289
293
|
// build-blocked(自升级罕见):结构化透出,不冒充成功
|
|
@@ -300,7 +304,7 @@ export function createApiDispatcher(ctx) {
|
|
|
300
304
|
break;
|
|
301
305
|
}
|
|
302
306
|
// M2 Task 3:self-upgrade 收编 market.selfUpgrade(统一 mutation session + 守卫,直调事务旁路封死)
|
|
303
|
-
const result = await d.selfUpgrade(ctx.pkg.name, ctx.pkg.version, cfg(), { signal });
|
|
307
|
+
const result = await d.selfUpgrade(ctx.pkg.name, ctx.pkg.version, cfg(), { signal, channel: 'panel' });
|
|
304
308
|
payload = {
|
|
305
309
|
pkg: result.pkg,
|
|
306
310
|
version: result.version,
|
|
@@ -502,6 +506,7 @@ export function createApiDispatcher(ctx) {
|
|
|
502
506
|
profile: profile.name,
|
|
503
507
|
profileDir: profile.dir,
|
|
504
508
|
signal,
|
|
509
|
+
channel: 'panel',
|
|
505
510
|
}, { getService: d.getService, ensureService: d.ensureService })) };
|
|
506
511
|
break;
|
|
507
512
|
}
|
|
@@ -511,6 +516,7 @@ export function createApiDispatcher(ctx) {
|
|
|
511
516
|
forceIncompatible: boolArg(body.forceIncompatible),
|
|
512
517
|
namespace: 'host',
|
|
513
518
|
signal,
|
|
519
|
+
channel: 'panel',
|
|
514
520
|
});
|
|
515
521
|
payload = { ...result };
|
|
516
522
|
break;
|
|
@@ -522,10 +528,10 @@ export function createApiDispatcher(ctx) {
|
|
|
522
528
|
assertWriteAllowed(profile, 'uninstall');
|
|
523
529
|
// 0.9.8:desktop → 官方管理器 removeBundle(dsh-market 同策略;此前结构化拒绝)
|
|
524
530
|
if (profile.kind !== 'web') {
|
|
525
|
-
payload = { ...(await d.desktopUninstall(target, { getService: d.getService, ensureService: d.ensureService, profileDir: profile.dir })) };
|
|
531
|
+
payload = { ...(await d.desktopUninstall(target, { getService: d.getService, ensureService: d.ensureService, profileDir: profile.dir, channel: 'panel' })) };
|
|
526
532
|
break;
|
|
527
533
|
}
|
|
528
|
-
const result = await d.uninstallPlugin(target, cfg(), { namespace: 'host', signal });
|
|
534
|
+
const result = await d.uninstallPlugin(target, cfg(), { namespace: 'host', signal, channel: 'panel' });
|
|
529
535
|
payload = { ...result };
|
|
530
536
|
break;
|
|
531
537
|
}
|
|
@@ -538,10 +544,10 @@ export function createApiDispatcher(ctx) {
|
|
|
538
544
|
// 0.9.0:desktop → 官方管理器委派(服务缺席结构化拒绝,禁 fallback 文件写)
|
|
539
545
|
if (profile.kind !== 'web') {
|
|
540
546
|
assertWriteAllowed(profile, 'set-enabled');
|
|
541
|
-
payload = { ...(await d.desktopToggle(target, body.enabled, { getService: d.getService, profileDir: profile.dir })) };
|
|
547
|
+
payload = { ...(await d.desktopToggle(target, body.enabled, { getService: d.getService, profileDir: profile.dir, channel: 'panel' })) };
|
|
542
548
|
break;
|
|
543
549
|
}
|
|
544
|
-
const result = await d.toggle(target, body.enabled, { getService: d.getService });
|
|
550
|
+
const result = await d.toggle(target, body.enabled, { getService: d.getService, channel: 'panel' });
|
|
545
551
|
payload = { ...result };
|
|
546
552
|
break;
|
|
547
553
|
}
|
|
@@ -558,6 +564,7 @@ export function createApiDispatcher(ctx) {
|
|
|
558
564
|
profile: profile.name,
|
|
559
565
|
profileDir: profile.dir,
|
|
560
566
|
signal,
|
|
567
|
+
channel: 'panel',
|
|
561
568
|
}, { getService: d.getService, ensureService: d.ensureService })) };
|
|
562
569
|
break;
|
|
563
570
|
}
|
|
@@ -565,10 +572,23 @@ export function createApiDispatcher(ctx) {
|
|
|
565
572
|
forceIncompatible: boolArg(body.forceIncompatible),
|
|
566
573
|
namespace: 'host',
|
|
567
574
|
signal,
|
|
575
|
+
channel: 'panel',
|
|
568
576
|
});
|
|
569
577
|
payload = { ...result };
|
|
570
578
|
break;
|
|
571
579
|
}
|
|
580
|
+
case 'external-operations': {
|
|
581
|
+
// 0.9.79 外部操作记录(ADR-0018):面板「已结束」overlay 数据源;鉴权走方法分发前的
|
|
582
|
+
// 既有宿主信任检查(零新增特例)
|
|
583
|
+
const records = await d.listExternalOperations();
|
|
584
|
+
payload = { records };
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
case 'external-operations-clear': {
|
|
588
|
+
await d.clearExternalOperations();
|
|
589
|
+
payload = {};
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
572
592
|
case 'restart': {
|
|
573
593
|
// 0.9.0:Desktop 不触发旧 helper(Electron 生命周期归官方;杀子进程=红线)
|
|
574
594
|
assertWriteAllowed(profile, 'restart');
|
package/lib/core/market.js
CHANGED
|
@@ -30,6 +30,7 @@ import { classifyUpgradeActivation } from './activation.js';
|
|
|
30
30
|
import { HttpError } from './httpx.js';
|
|
31
31
|
import { ensureLatestCacheSwept, invalidateLatestCache, latestCacheKey, latestItemId, peekLatestCache, readLatestCache, writeLatestCache } from './latest-cache.js';
|
|
32
32
|
import { verifyInstalledAdditions } from './install-guard.js';
|
|
33
|
+
import { journalExternalOperation } from './external-journal.js';
|
|
33
34
|
import { fetchTextLimited } from './httpx.js';
|
|
34
35
|
import { githubLatestTag as defaultGithubLatestTag, isNewerVersion, npmLatest as defaultNpmLatest, npmVersion, } from './versions.js';
|
|
35
36
|
/** profile package.json 的 dsh.profile.bundles 数组(读不到 → [];Task 13)。 */
|
|
@@ -672,8 +673,17 @@ export async function resolveRegistryEntry(id, cfg = {}, opts = {}, deps) {
|
|
|
672
673
|
throw new Error(`registry 中没有该条目: ${id}`);
|
|
673
674
|
}
|
|
674
675
|
export async function installFromRegistry(id, cfg = {}, opts = {}, deps) {
|
|
675
|
-
|
|
676
|
-
|
|
676
|
+
// 0.9.79 外部操作直落(ADR-0018):终态后挂钩,成功与失败都记;append 失败被钩子吞(R1-1)
|
|
677
|
+
try {
|
|
678
|
+
const entry = await resolveRegistryEntry(id, cfg, opts, deps);
|
|
679
|
+
const result = await installEntry(entry, cfg, opts, deps);
|
|
680
|
+
await journalExternalOperation(opts.channel, { kind: 'install', target: id, ...(result.version ? { toVersion: result.version } : {}) }, 'ok', { appendExternalOperation: deps?.appendExternalOperation });
|
|
681
|
+
return result;
|
|
682
|
+
}
|
|
683
|
+
catch (err) {
|
|
684
|
+
await journalExternalOperation(opts.channel, { kind: 'install', target: id }, 'failed', { reason: errText(err), appendExternalOperation: deps?.appendExternalOperation });
|
|
685
|
+
throw err;
|
|
686
|
+
}
|
|
677
687
|
}
|
|
678
688
|
// ---------- M2 Task 3:统一 mutation session + 装后守卫 + 补偿接线 ----------
|
|
679
689
|
let sessionTail = Promise.resolve();
|
|
@@ -1196,7 +1206,23 @@ async function installEntryLocked(entry, cfg = {}, opts = {}, deps) {
|
|
|
1196
1206
|
}
|
|
1197
1207
|
/** 卸载:validate(严格读取)→ live-disable → 摘补丁 → pnpm remove → verify gone(DESIGN.md §3:删包不删数据)。 */
|
|
1198
1208
|
export function uninstallPlugin(pkg, cfg = {}, opts = {}, deps = {}) {
|
|
1199
|
-
|
|
1209
|
+
// 0.9.79 外部操作直落(ADR-0018):终态后挂钩,session 区间内保持单写者
|
|
1210
|
+
return withMutationSession(async () => {
|
|
1211
|
+
try {
|
|
1212
|
+
const result = await uninstallPluginLocked(pkg, cfg, opts, deps);
|
|
1213
|
+
await journalExternalOperation(opts.channel, { kind: 'uninstall', target: pkg }, 'ok', {
|
|
1214
|
+
appendExternalOperation: deps.appendExternalOperation,
|
|
1215
|
+
});
|
|
1216
|
+
return result;
|
|
1217
|
+
}
|
|
1218
|
+
catch (err) {
|
|
1219
|
+
await journalExternalOperation(opts.channel, { kind: 'uninstall', target: pkg }, 'failed', {
|
|
1220
|
+
reason: errText(err),
|
|
1221
|
+
appendExternalOperation: deps.appendExternalOperation,
|
|
1222
|
+
});
|
|
1223
|
+
throw err;
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1200
1226
|
}
|
|
1201
1227
|
async function uninstallPluginLocked(pkg, _cfg = {}, opts = {}, deps = {}) {
|
|
1202
1228
|
// 保护门(Task 12,grilling Q3):dsh-m 自身与官方宿主命脉拒绝卸载(升级不受影响)
|
|
@@ -1221,7 +1247,26 @@ async function uninstallPluginLocked(pkg, _cfg = {}, opts = {}, deps = {}) {
|
|
|
1221
1247
|
}
|
|
1222
1248
|
/** 升级 = 按最新重新安装(npm 拉最新精确版;github 重新锁 HEAD)。 */
|
|
1223
1249
|
export function upgradePlugin(pkg, cfg = {}, opts = {}, deps) {
|
|
1224
|
-
|
|
1250
|
+
// 0.9.79 外部操作直落(ADR-0018):终态后挂钩,session 区间内保持单写者
|
|
1251
|
+
return withMutationSession(async () => {
|
|
1252
|
+
try {
|
|
1253
|
+
const result = await upgradePluginLocked(pkg, cfg, opts, deps);
|
|
1254
|
+
await journalExternalOperation(opts.channel, {
|
|
1255
|
+
kind: 'upgrade',
|
|
1256
|
+
target: pkg,
|
|
1257
|
+
...(result.fromVersion ? { fromVersion: result.fromVersion } : {}),
|
|
1258
|
+
...(result.version ? { toVersion: result.version } : {}),
|
|
1259
|
+
}, 'ok', { appendExternalOperation: deps?.appendExternalOperation });
|
|
1260
|
+
return result;
|
|
1261
|
+
}
|
|
1262
|
+
catch (err) {
|
|
1263
|
+
await journalExternalOperation(opts.channel, { kind: 'upgrade', target: pkg }, 'failed', {
|
|
1264
|
+
reason: errText(err),
|
|
1265
|
+
appendExternalOperation: deps?.appendExternalOperation,
|
|
1266
|
+
});
|
|
1267
|
+
throw err;
|
|
1268
|
+
}
|
|
1269
|
+
});
|
|
1225
1270
|
}
|
|
1226
1271
|
async function upgradePluginLocked(pkg, cfg = {}, opts = {}, deps) {
|
|
1227
1272
|
const loaded = await (deps?.loadRegistry ?? defaultLoadRegistry)(cfg, { namespace: opts.namespace ?? 'host', profile: opts.profile });
|
|
@@ -1278,29 +1323,42 @@ async function upgradePluginLocked(pkg, cfg = {}, opts = {}, deps) {
|
|
|
1278
1323
|
* (owner--name 等),主清单 miss 时按同一 id 查社区目录。社区清单未启用/不可用/加载
|
|
1279
1324
|
* 异常/未命中 → undefined,由调用方统一报「registry 中没有该条目」。
|
|
1280
1325
|
*/
|
|
1281
|
-
/** 自升级(0.5.0 收编:host-api 直调事务的旁路封死):npmLatest + integrity fail-closed
|
|
1326
|
+
/** 自升级(0.5.0 收编:host-api 直调事务的旁路封死):npmLatest + integrity fail-closed;函数体直调事务(call-graph audit:不经 installEntry,无嵌套双记)。 */
|
|
1282
1327
|
export async function selfUpgrade(pkgName, currentVersion, cfg = {}, opts = {}, deps) {
|
|
1283
1328
|
return withMutationSession(async () => {
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1329
|
+
try {
|
|
1330
|
+
const timeoutMs = cfg.timeoutMs ?? 20_000;
|
|
1331
|
+
const latest = await (deps?.npmLatest ?? defaultNpmLatest)(pkgName, timeoutMs, opts.signal);
|
|
1332
|
+
if (!latest.integrity) {
|
|
1333
|
+
throw new Error(`npm metadata 缺少 dist integrity:${pkgName}@${latest.version},拒绝升级`);
|
|
1334
|
+
}
|
|
1335
|
+
const result = await runProfileTransaction({ kind: 'install-npm', pkg: pkgName, version: latest.version, integrity: latest.integrity, signal: opts.signal }, { ...(deps?.transaction ?? {}), warmPackument: deps?.transaction?.warmPackument ?? makeNpmWarmPackument(timeoutMs) });
|
|
1336
|
+
if (!result.ok)
|
|
1337
|
+
throw new TransactionError(result);
|
|
1338
|
+
const payload = {
|
|
1339
|
+
id: pkgName,
|
|
1340
|
+
pkg: result.pkg ?? pkgName,
|
|
1341
|
+
spec: result.spec ?? `${pkgName}@${latest.version}`,
|
|
1342
|
+
version: result.version ?? latest.version,
|
|
1343
|
+
buildApprovals: result.buildApprovals ?? [],
|
|
1344
|
+
fallbackAllBuilds: result.fallbackAllBuilds === true,
|
|
1345
|
+
...(result.bundleWarning ? { bundleWarning: result.bundleWarning } : {}),
|
|
1346
|
+
needsRestart: true,
|
|
1347
|
+
output: result.output,
|
|
1348
|
+
healActions: result.healActions,
|
|
1349
|
+
};
|
|
1350
|
+
await journalExternalOperation(opts.channel, {
|
|
1351
|
+
kind: 'self-upgrade',
|
|
1352
|
+
target: pkgName,
|
|
1353
|
+
...(currentVersion ? { fromVersion: currentVersion } : {}),
|
|
1354
|
+
...(payload.version ? { toVersion: payload.version } : {}),
|
|
1355
|
+
}, 'ok', { appendExternalOperation: deps?.appendExternalOperation });
|
|
1356
|
+
return payload;
|
|
1357
|
+
}
|
|
1358
|
+
catch (err) {
|
|
1359
|
+
await journalExternalOperation(opts.channel, { kind: 'self-upgrade', target: pkgName, ...(currentVersion ? { fromVersion: currentVersion } : {}) }, 'failed', { reason: errText(err), appendExternalOperation: deps?.appendExternalOperation });
|
|
1360
|
+
throw err;
|
|
1288
1361
|
}
|
|
1289
|
-
const result = await runProfileTransaction({ kind: 'install-npm', pkg: pkgName, version: latest.version, integrity: latest.integrity, signal: opts.signal }, { ...(deps?.transaction ?? {}), warmPackument: deps?.transaction?.warmPackument ?? makeNpmWarmPackument(timeoutMs) });
|
|
1290
|
-
if (!result.ok)
|
|
1291
|
-
throw new TransactionError(result);
|
|
1292
|
-
return {
|
|
1293
|
-
id: pkgName,
|
|
1294
|
-
pkg: result.pkg ?? pkgName,
|
|
1295
|
-
spec: result.spec ?? `${pkgName}@${latest.version}`,
|
|
1296
|
-
version: result.version ?? latest.version,
|
|
1297
|
-
buildApprovals: result.buildApprovals ?? [],
|
|
1298
|
-
fallbackAllBuilds: result.fallbackAllBuilds === true,
|
|
1299
|
-
...(result.bundleWarning ? { bundleWarning: result.bundleWarning } : {}),
|
|
1300
|
-
needsRestart: true,
|
|
1301
|
-
output: result.output,
|
|
1302
|
-
healActions: result.healActions,
|
|
1303
|
-
};
|
|
1304
1362
|
});
|
|
1305
1363
|
}
|
|
1306
1364
|
/** 疑似残留路径(存在才列出):删包不删数据,只报告。 */
|
package/lib/core/profile-ops.js
CHANGED
|
@@ -33,6 +33,7 @@ import { classifyUpgradeActivation } from './activation.js';
|
|
|
33
33
|
import { togglePlugin, ToggleError } from './toggle.js';
|
|
34
34
|
import { describeReleaseAgeFailure, releaseAgePrecheck } from './release-age.js';
|
|
35
35
|
import { governExcludeBlock, registerExclusion } from './exclude-governance.js';
|
|
36
|
+
import { journalExternalOperation } from './external-journal.js';
|
|
36
37
|
/** desktop 升级社区兜底的等待上限(对齐 market.ts DEFAULT_DEADLINE_MS 量级;仅本路径使用)。 */
|
|
37
38
|
const DEFAULT_COMMUNITY_DEADLINE_MS = 60_000;
|
|
38
39
|
// 0.9.19:等待期失败翻译移入 release-age.ts(describeReleaseAgeFailure)——全量违规解析 +
|
|
@@ -77,7 +78,27 @@ function changeOf(raw) {
|
|
|
77
78
|
* 全程 within mutation session(与 web 五入口同一 FIFO)。
|
|
78
79
|
*/
|
|
79
80
|
export async function desktopInstallFromRegistry(id, cfg = {}, opts = {}, deps = {}) {
|
|
80
|
-
|
|
81
|
+
// 0.9.79 外部操作直落(ADR-0018):自记(call-graph audit:内部走 resolveEntry + service.installBundle,
|
|
82
|
+
// 不经 market.ts 已插桩入口);成功/失败/needsBuildApproval(结构化未遂)都记;append 失败被钩子吞
|
|
83
|
+
try {
|
|
84
|
+
const result = await withMutationSession(() => desktopInstallLocked(id, cfg, opts, deps));
|
|
85
|
+
if ('needsBuildApproval' in result) {
|
|
86
|
+
await journalExternalOperation(opts.channel, { kind: 'desktop-install', target: id }, 'failed', {
|
|
87
|
+
reason: result.message,
|
|
88
|
+
appendExternalOperation: deps.appendExternalOperation,
|
|
89
|
+
});
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
await journalExternalOperation(opts.channel, { kind: 'desktop-install', target: id, ...(result.version ? { toVersion: result.version } : {}) }, 'ok', { appendExternalOperation: deps.appendExternalOperation });
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
await journalExternalOperation(opts.channel, { kind: 'desktop-install', target: id }, 'failed', {
|
|
97
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
98
|
+
appendExternalOperation: deps.appendExternalOperation,
|
|
99
|
+
});
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
81
102
|
}
|
|
82
103
|
async function desktopInstallLocked(id, cfg, opts, deps) {
|
|
83
104
|
const service = await resolveManager(deps);
|
|
@@ -197,15 +218,27 @@ async function desktopInstallLocked(id, cfg, opts, deps) {
|
|
|
197
218
|
};
|
|
198
219
|
}
|
|
199
220
|
/**
|
|
200
|
-
* Desktop 开关:服务缺席 → 结构化拒绝(ToggleError/unaddressable,禁 fallback
|
|
201
|
-
*
|
|
221
|
+
* Desktop 开关:服务缺席 → 结构化拒绝(ToggleError/unaddressable,禁 fallback 文件写;
|
|
222
|
+
* 该拒绝发生在透传前,由本入口自记 desktop-toggle failed——类型契约中该 kind 的唯一可达路径);
|
|
223
|
+
* 服务在用 → 既有 togglePlugin 委派路径(installed 校验按 profileDir,与 web 委派同代码;
|
|
224
|
+
* channel/钩子透传,单笔事务只产生一条 toggle 记录,不双记——R1-2)。
|
|
202
225
|
*/
|
|
203
226
|
export async function desktopToggle(pkg, enabled, deps = {}) {
|
|
204
227
|
const service = await resolveManager(deps);
|
|
205
228
|
if (!service) {
|
|
206
|
-
|
|
229
|
+
const message = '官方 pluginManager 服务不可用:desktop profile 的开关必须走官方管理器(dsh-m 不做文件级 fallback);请在官方 Desktop 插件页操作';
|
|
230
|
+
await journalExternalOperation(deps.channel, { kind: 'desktop-toggle', target: pkg }, 'failed', {
|
|
231
|
+
reason: message,
|
|
232
|
+
appendExternalOperation: deps.appendExternalOperation,
|
|
233
|
+
});
|
|
234
|
+
throw new ToggleError('unaddressable', message);
|
|
207
235
|
}
|
|
208
|
-
return togglePlugin(pkg, enabled, {
|
|
236
|
+
return togglePlugin(pkg, enabled, {
|
|
237
|
+
getService: () => service,
|
|
238
|
+
profileDir: deps.profileDir,
|
|
239
|
+
channel: deps.channel,
|
|
240
|
+
appendExternalOperation: deps.appendExternalOperation,
|
|
241
|
+
});
|
|
209
242
|
}
|
|
210
243
|
/**
|
|
211
244
|
* 管理器安装共享核心(0.9.8):installBundle + ChangeResult 判定 + listBundles 复读。
|
|
@@ -363,40 +396,78 @@ async function runManagedInstall(service, ctx, deps = {}) {
|
|
|
363
396
|
/**
|
|
364
397
|
* Desktop 卸载(0.9.8,dsh-market 同策略):removeBundle 委派 + ChangeResult 判定 +
|
|
365
398
|
* listBundles 复读(可用时必须不见目标在装)。服务缺席 → 结构化拒绝,零 CLI、零文件级 fallback。
|
|
399
|
+
* 0.9.79:自记外部操作(call-graph audit:内部直接 service.removeBundle,不经 market.ts 插桩入口)。
|
|
366
400
|
*/
|
|
367
401
|
export async function desktopUninstall(pkg, deps = {}) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
402
|
+
try {
|
|
403
|
+
const service = await resolveManager(deps);
|
|
404
|
+
if (!service || typeof service.removeBundle !== 'function') {
|
|
405
|
+
throw new DesktopOpsError('no-manager', '官方 pluginManager 服务不可用(desktop profile 的卸载必须由官方管理器执行);拒绝卸载(fail-closed)');
|
|
406
|
+
}
|
|
407
|
+
const raw = await service.removeBundle(pkg);
|
|
408
|
+
const change = changeOf(raw);
|
|
409
|
+
if (change.application === 'failed' || change.error) {
|
|
410
|
+
throw new DesktopOpsError('remove-failed', `官方管理器卸载失败(${pkg};application=${change.application ?? 'failed'}${change.error?.code ? `、error=${change.error.code}` : ''}${change.error?.diagnostic ? `:${change.error.diagnostic}` : ''})`);
|
|
411
|
+
}
|
|
412
|
+
if (typeof service.listBundles === 'function') {
|
|
413
|
+
const bundles = await service.listBundles();
|
|
414
|
+
const found = (bundles ?? []).find((b) => b && b.name === (change.bundle ?? pkg));
|
|
415
|
+
if (found && found.installed === true) {
|
|
416
|
+
throw new DesktopOpsError('verify-failed', `卸载复读失败(${pkg}):官方结果为 ${change.application ?? 'unknown'} 但 listBundles 仍见目标在装——不冒充卸载成功`);
|
|
417
|
+
}
|
|
382
418
|
}
|
|
419
|
+
await journalExternalOperation(deps.channel, { kind: 'desktop-uninstall', target: pkg }, 'ok', {
|
|
420
|
+
appendExternalOperation: deps.appendExternalOperation,
|
|
421
|
+
});
|
|
422
|
+
return {
|
|
423
|
+
pkg,
|
|
424
|
+
liveDisabled: false,
|
|
425
|
+
needsRestart: change.application === 'restart-required',
|
|
426
|
+
leftovers: [],
|
|
427
|
+
via: 'desktop-manager',
|
|
428
|
+
output: `official pluginManager: application=${change.application ?? 'applied'}`,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
await journalExternalOperation(deps.channel, { kind: 'desktop-uninstall', target: pkg }, 'failed', {
|
|
433
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
434
|
+
appendExternalOperation: deps.appendExternalOperation,
|
|
435
|
+
});
|
|
436
|
+
throw err;
|
|
383
437
|
}
|
|
384
|
-
return {
|
|
385
|
-
pkg,
|
|
386
|
-
liveDisabled: false,
|
|
387
|
-
needsRestart: change.application === 'restart-required',
|
|
388
|
-
leftovers: [],
|
|
389
|
-
via: 'desktop-manager',
|
|
390
|
-
output: `official pluginManager: application=${change.application ?? 'applied'}`,
|
|
391
|
-
};
|
|
392
438
|
}
|
|
393
439
|
/**
|
|
394
440
|
* Desktop 升级(0.9.8,dsh-market 同策略——本机 0.9.3→0.9.4/0.9.5 即此路径实证):
|
|
395
441
|
* 覆盖安装 = installBundle(latest 精确 spec)。收录匹配(主清单 → 社区目录)与
|
|
396
442
|
* upgradePluginLocked 同语义;非 dsh-m 收录的插件结构化拒绝(官方插件页负责)。
|
|
443
|
+
* 0.9.79:自记外部操作(call-graph audit:内部走 loadRegistry/mergedOutcome + service.installBundle,
|
|
444
|
+
* 不经 market.ts 已插桩入口);needsBuildApproval(结构化未遂)记 failed。
|
|
397
445
|
*/
|
|
398
446
|
export async function desktopUpgradeFromRegistry(pkg, cfg = {}, opts = {}, deps = {}) {
|
|
399
|
-
|
|
447
|
+
try {
|
|
448
|
+
const result = await withMutationSession(() => desktopUpgradeLocked(pkg, cfg, opts, deps));
|
|
449
|
+
if ('needsBuildApproval' in result) {
|
|
450
|
+
await journalExternalOperation(opts.channel, { kind: 'desktop-upgrade', target: pkg }, 'failed', {
|
|
451
|
+
reason: result.message,
|
|
452
|
+
appendExternalOperation: deps.appendExternalOperation,
|
|
453
|
+
});
|
|
454
|
+
return result;
|
|
455
|
+
}
|
|
456
|
+
await journalExternalOperation(opts.channel, {
|
|
457
|
+
kind: 'desktop-upgrade',
|
|
458
|
+
target: pkg,
|
|
459
|
+
...(result.fromVersion ? { fromVersion: result.fromVersion } : {}),
|
|
460
|
+
...(result.version ? { toVersion: result.version } : {}),
|
|
461
|
+
}, 'ok', { appendExternalOperation: deps.appendExternalOperation });
|
|
462
|
+
return result;
|
|
463
|
+
}
|
|
464
|
+
catch (err) {
|
|
465
|
+
await journalExternalOperation(opts.channel, { kind: 'desktop-upgrade', target: pkg }, 'failed', {
|
|
466
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
467
|
+
appendExternalOperation: deps.appendExternalOperation,
|
|
468
|
+
});
|
|
469
|
+
throw err;
|
|
470
|
+
}
|
|
400
471
|
}
|
|
401
472
|
async function desktopUpgradeLocked(pkg, cfg, opts, deps) {
|
|
402
473
|
const service = await resolveManager(deps);
|
package/lib/core/toggle.js
CHANGED
|
@@ -24,6 +24,7 @@ import { isSingleRowPlugin, planRowOverride } from './patch-yaml.js';
|
|
|
24
24
|
import { PROTECTED_MODULES } from './enablement.js';
|
|
25
25
|
import { withMutationSession } from './market.js';
|
|
26
26
|
import { setLivePluginDisabled, loaderHost } from './live-plugin.js';
|
|
27
|
+
import { journalExternalOperation } from './external-journal.js';
|
|
27
28
|
import { webProfileDir } from './env.js';
|
|
28
29
|
export class ToggleError extends Error {
|
|
29
30
|
code;
|
|
@@ -55,7 +56,23 @@ async function readPatchText(io, path) {
|
|
|
55
56
|
}
|
|
56
57
|
export function togglePlugin(pkg, enabled, deps = {}) {
|
|
57
58
|
// M2 Task 3:toggle 纳入统一 mutation session(本体单点获取;自身 file lock 在 session 内层)
|
|
58
|
-
|
|
59
|
+
// 0.9.79 外部操作直落(ADR-0018):终态后挂钩,成功与失败都记;append 失败被钩子吞(R1-1)
|
|
60
|
+
return withMutationSession(async () => {
|
|
61
|
+
try {
|
|
62
|
+
const result = await togglePluginLocked(pkg, enabled, deps);
|
|
63
|
+
await journalExternalOperation(deps.channel, { kind: 'toggle', target: pkg }, 'ok', {
|
|
64
|
+
appendExternalOperation: deps.appendExternalOperation,
|
|
65
|
+
});
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
await journalExternalOperation(deps.channel, { kind: 'toggle', target: pkg }, 'failed', {
|
|
70
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
71
|
+
appendExternalOperation: deps.appendExternalOperation,
|
|
72
|
+
});
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
59
76
|
}
|
|
60
77
|
async function togglePluginLocked(pkg, enabled, deps = {}) {
|
|
61
78
|
const key = String(pkg || '').trim();
|
package/lib/tools.js
CHANGED
|
@@ -257,10 +257,10 @@ export function registerTools(ctx, cfg, deps = {}, profile = resolveActiveProfil
|
|
|
257
257
|
if (profile.kind !== 'web') {
|
|
258
258
|
assertWriteAllowed(profile, 'install');
|
|
259
259
|
// 0.9.8:补传服务解析 deps(此前漏传 → desktop 安装恒 no-manager,实机 2026-10-01)
|
|
260
|
-
return cloneJson(await m.desktopInstall(id, cfg, { version, forceIncompatible: force, namespace: 'host', profile: profile.name, profileDir: profile.dir }, { getService: getService, ensureService: ensureService }));
|
|
260
|
+
return cloneJson(await m.desktopInstall(id, cfg, { version, forceIncompatible: force, namespace: 'host', profile: profile.name, profileDir: profile.dir, channel: 'host-tool' }, { getService: getService, ensureService: ensureService }));
|
|
261
261
|
}
|
|
262
262
|
try {
|
|
263
|
-
return cloneJson(await m.installFromRegistry(id, cfg, { version, forceIncompatible: force, namespace: 'host' }));
|
|
263
|
+
return cloneJson(await m.installFromRegistry(id, cfg, { version, forceIncompatible: force, namespace: 'host', channel: 'host-tool' }));
|
|
264
264
|
}
|
|
265
265
|
catch (err) {
|
|
266
266
|
if (err instanceof InstallGuardError) {
|
|
@@ -303,9 +303,9 @@ export function registerTools(ctx, cfg, deps = {}, profile = resolveActiveProfil
|
|
|
303
303
|
// 0.9.0:desktop → 官方管理器委派(服务缺席结构化拒绝);web → 既有委派/降级
|
|
304
304
|
if (profile.kind !== 'web') {
|
|
305
305
|
assertWriteAllowed(profile, 'set-enabled');
|
|
306
|
-
return cloneJson(await m.desktopToggle(target, args.enabled, { getService: getService, ensureService: ensureService, profileDir: profile.dir }));
|
|
306
|
+
return cloneJson(await m.desktopToggle(target, args.enabled, { getService: getService, ensureService: ensureService, profileDir: profile.dir, channel: 'host-tool' }));
|
|
307
307
|
}
|
|
308
|
-
return cloneJson(await toggle(target, args.enabled, { getService: getService }));
|
|
308
|
+
return cloneJson(await toggle(target, args.enabled, { getService: getService, channel: 'host-tool' }));
|
|
309
309
|
},
|
|
310
310
|
}));
|
|
311
311
|
ctx.tools.register(defineTool({
|
|
@@ -333,9 +333,9 @@ export function registerTools(ctx, cfg, deps = {}, profile = resolveActiveProfil
|
|
|
333
333
|
assertWriteAllowed(profile, 'uninstall');
|
|
334
334
|
// 0.9.8:desktop → 官方管理器 removeBundle(dsh-market 同策略;此前该工具在 desktop 走 web 事务必然错位)
|
|
335
335
|
if (profile.kind !== 'web') {
|
|
336
|
-
return cloneJson(await m.desktopUninstall(target, { getService: getService, ensureService: ensureService, profileDir: profile.dir }));
|
|
336
|
+
return cloneJson(await m.desktopUninstall(target, { getService: getService, ensureService: ensureService, profileDir: profile.dir, channel: 'host-tool' }));
|
|
337
337
|
}
|
|
338
|
-
return cloneJson(await m.uninstallPlugin(target, cfg, { namespace: 'host' }));
|
|
338
|
+
return cloneJson(await m.uninstallPlugin(target, cfg, { namespace: 'host', channel: 'host-tool' }));
|
|
339
339
|
},
|
|
340
340
|
}));
|
|
341
341
|
ctx.tools.register(defineTool({
|
|
@@ -420,10 +420,10 @@ export function registerTools(ctx, cfg, deps = {}, profile = resolveActiveProfil
|
|
|
420
420
|
assertWriteAllowed(profile, 'upgrade');
|
|
421
421
|
// 0.9.8:desktop → 官方管理器覆盖安装(dsh-market 同策略;此前该工具在 desktop 走 web 事务必然错位)
|
|
422
422
|
if (profile.kind !== 'web') {
|
|
423
|
-
return cloneJson(await m.desktopUpgrade(target, cfg, { forceIncompatible: force, namespace: 'host', profile: profile.name, profileDir: profile.dir }, { getService: getService, ensureService: ensureService }));
|
|
423
|
+
return cloneJson(await m.desktopUpgrade(target, cfg, { forceIncompatible: force, namespace: 'host', profile: profile.name, profileDir: profile.dir, channel: 'host-tool' }, { getService: getService, ensureService: ensureService }));
|
|
424
424
|
}
|
|
425
425
|
try {
|
|
426
|
-
return cloneJson(await m.upgradePlugin(target, cfg, { forceIncompatible: force, namespace: 'host' }));
|
|
426
|
+
return cloneJson(await m.upgradePlugin(target, cfg, { forceIncompatible: force, namespace: 'host', channel: 'host-tool' }));
|
|
427
427
|
}
|
|
428
428
|
catch (err) {
|
|
429
429
|
if (err instanceof InstallGuardError) {
|