dsh-m 0.2.2 → 0.2.4

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/docs/DESIGN.md CHANGED
@@ -63,7 +63,7 @@
63
63
 
64
64
  底层原语(本机实证):`dsh plugin --profile web add|remove|update`(转发 pnpm,作用于 `$DSH_HOME/profiles/web`)。**profile 的 `package.json` 就是唯一事实源**——不引入任何额外状态文件。
65
65
 
66
- - **安装(npm 源)**:装最新版并以**精确版本锁定**(不用 `^` 范围;用户指定版本必须为精确 semver,经该精确版本 endpoint 查询)。安装前对 profile 的 `package.json` / `pnpm-lock.yaml` / `pnpm-workspace.yaml` 做字节快照;安装后核验 importer 依赖为该精确版本,并在 lockfile `packages` 条目中比对与 npm dist 一致的 `resolution.integrity`——缺失或不一致 **fail closed** 并执行 **best-effort dependency rollback**(原子恢复快照 + `pnpm install --frozen-lockfile`;恢复失败同时报告两类错误并提示人工修复)。不声称 node_modules 与间接依赖已字节级回滚。
66
+ - **安装(npm 源)**:装最新版并以**精确版本锁定**(不用 `^` 范围;用户指定版本必须为精确 semver,经该精确版本 endpoint 查询)。安装前对 profile 的 `package.json` / `pnpm-lock.yaml` / `pnpm-workspace.yaml` 做**字节快照**;安装后核验 importer 依赖为该精确版本,并在 lockfile `packages` 条目中比对与 npm dist 一致的 `resolution.integrity`——缺失或不一致 **fail closed** 并执行 **best-effort dependency rollback**:原子恢复快照字节 + frozen 自愈阶梯(`pnpm install --frozen-lockfile` → 仍报 `CONFIG_MISMATCH` 时把 lockfile 记录的 overrides 对齐回 `package.json#pnpm.overrides` 再复验 → `CONFIG_MISMATCH` / `OUTDATED_LOCKFILE` 顽固失配才降级 `--no-frozen-lockfile` 重建并明确告知「lockfile 已重建」;全阶梯失败才报「可能需要人工修复」)。成功路径若发现安装链丢失了 manifest 顶层未知键(如 `pnpm.overrides`,2026-09-05 升级回滚事故),从快照找回并复验 frozen 一致性,输出带 `[dsh-m 自愈]` 报告。刚发布 ~1 分钟内的 `ERR_PNPM_NO_MATCHING_VERSION` 多为 packument CDN 滞后:退避重试 2 次(5s/15s),每次重试前拉一次完整 packument 预热。不声称 node_modules 与间接依赖已字节级回滚。
67
67
  - **安装(GitHub 源)**:解析并**锁定 commit SHA**(`github:owner/repo#sha`),skillhub 同款。
68
68
  - **已装识别**:读 profile `package.json` dependencies,与 registry 匹配 → 标注「市场安装」;不匹配的也列出,标注「非市场安装 / 来源未知」。卸载/升级对两类都可用。
69
69
  - **卸载**:live-disable(先让 client bundle 下线,避免 404)→ 摘除该包在 profile 的补丁条目(`pnpm-workspace.yaml` 顶层 `patchedDependencies` 与 `package.json#pnpm.patchedDependencies`;依赖移除后残留条目会令 pnpm 以 `ERR_PNPM_UNUSED_PATCH` 整单失败,只精确匹配 `pkg` / `pkg@ver`,补丁文件本体保留并计入残留报告)→ `dsh plugin remove`。**不清理插件产生的数据/配置**,但把检测到的疑似残留路径(如 `~/.dsh/<plugin>.json`)列出报告。
package/lib/cli.js CHANGED
@@ -82,7 +82,7 @@ const HELP = `dshm — DSH Marketplace(个人自用 DSH 插件市场)
82
82
  用法:dshm <命令> [参数]
83
83
 
84
84
  只读命令:
85
- dshm search [--query 关键词] [--category market|tools|ui|search|media|other] [--limit N]
85
+ dshm search [--query 关键词] [--category market|tools|ui|search|other] [--limit N]
86
86
  dshm list 列出 web profile 已装插件(含市场标注/可升级)
87
87
  dshm outdated 检查已装插件的最新版本
88
88
  dshm registry 查看收录清单来源与条目
package/lib/client.js CHANGED
@@ -97,7 +97,7 @@ var init_market_state = __esm({
97
97
  "src/client/market-state.js"() {
98
98
  "use strict";
99
99
  MARKET_PAGE_SIZE = 50;
100
- CATEGORIES = ["market", "tools", "ui", "search", "media", "other"];
100
+ CATEGORIES = ["market", "tools", "ui", "search", "other"];
101
101
  FALLBACK_REGISTRY_STATE = {
102
102
  configuredAddress: "",
103
103
  activeAddress: null,
@@ -130,7 +130,6 @@ var ZH = {
130
130
  "cat.tools": "\u5DE5\u5177",
131
131
  "cat.ui": "\u754C\u9762",
132
132
  "cat.search": "\u641C\u7D22",
133
- "cat.media": "\u591A\u5A92\u4F53",
134
133
  "cat.other": "\u5176\u4ED6",
135
134
  "search.ph": "\u641C\u7D22\u540D\u79F0 / \u63CF\u8FF0 / \u6807\u7B7E\u2026",
136
135
  "common.refresh": "\u5237\u65B0",
@@ -299,7 +298,6 @@ var EN = {
299
298
  "cat.tools": "Tools",
300
299
  "cat.ui": "UI",
301
300
  "cat.search": "Search",
302
- "cat.media": "Media",
303
301
  "cat.other": "Other",
304
302
  "search.ph": "Search name, description, tags\u2026",
305
303
  "common.refresh": "Refresh",
@@ -468,7 +466,7 @@ function lookup(key, params) {
468
466
  const dict = browserLang() === "en" ? EN : ZH;
469
467
  return interpolate(dict[key] ?? ZH[key] ?? key, params);
470
468
  }
471
- var CATEGORIES2 = ["market", "tools", "ui", "search", "media", "other"];
469
+ var CATEGORIES2 = ["market", "tools", "ui", "search", "other"];
472
470
  var CSS = `
473
471
  .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:24px 16px;box-sizing:border-box}
474
472
  .dshm-panel{width:min(920px,100%);height:min(680px,86vh);display:flex;flex-direction:column;background:var(--dsw-alias-bg-overlay,var(--dsw-alias-bg-layer-3,#fff));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)}
@@ -560,6 +558,10 @@ var CSS = `
560
558
  .dshm-entry:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06))}
561
559
  .dshm-entry svg{flex:none;width:16px;height:16px}
562
560
  .dshm-entry span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
561
+ /* \u6536\u8D77\uFF08rail\uFF09\u6001\uFF1A\u4E0E\u5BBF\u4E3B\u8BBE\u7F6E\u5165\u53E3 / dsh-skins \u540C\u51E0\u4F55\u2014\u201436\xD736 \u5706\u5F62\u3001margin 8/10\u3001
562
+ \u56FE\u6807 18\u3001\u5185\u5BB9\u6C34\u5E73\u5C45\u4E2D\u3002\u7F3A\u8FD9\u5957\u4F1A\u5BFC\u81F4\u6536\u8D77\u65F6\u56FE\u6807\u504F\u79BB\u8F74\u7EBF\u4E14\u884C\u8DDD\u4E0D\u9F50\uFF08\u5B9E\u6D4B\u5DE6\u504F ~4px\uFF09\u3002 */
563
+ .dshm-entry.rail{width:36px;height:36px;margin:8px 0 10px;padding:0;justify-content:center;border-radius:50%;gap:0}
564
+ .dshm-entry.rail svg{width:18px;height:18px}
563
565
  [data-slot="sidebar.footer.action"]{display:flex!important;flex-direction:column;width:100%;min-width:0}
564
566
  [data-slot="sidebar.footer.action"]>*{flex:none;min-width:0}
565
567
  .dshm-empty{text-align:center;color:var(--dsw-alias-label-caption,#6b7280);font-size:13px;padding:32px 0}
@@ -1619,10 +1621,11 @@ function MarketIcon() {
1619
1621
  }
1620
1622
  function MarketEntry(props) {
1621
1623
  useEffect(() => ensureCss(), []);
1624
+ const rail = !(props && props.wide);
1622
1625
  return h(
1623
1626
  "button",
1624
1627
  {
1625
- className: "dshm-entry",
1628
+ className: rail ? "dshm-entry rail" : "dshm-entry",
1626
1629
  onClick: () => {
1627
1630
  try {
1628
1631
  mountPanel();
@@ -3,17 +3,21 @@
3
3
  * 安装 / 升级。安装语义见 DESIGN.md §3(npm 精确锁定、GitHub 锁 SHA)。
4
4
  * Task 3:服务端 query/category/offset/limit 过滤;只对当前页查 latest(并发 ≤8、
5
5
  * TTL cache、共享全局 deadline);unavailable 返回结构化空页;host/cli namespace 贯穿。
6
+ * 2026-09-05 回滚缺陷加固:B1 成功路径保留 manifest 顶层未知键(如 pnpm.overrides)、
7
+ * 失败路径字节级回滚后 frozen 自愈阶梯(overrides 对齐 → no-frozen 重建,B2)、
8
+ * 新发布后 NO_MATCHING_VERSION 的退避重试 + packument 预热(B3)。
6
9
  */
7
10
  import { existsSync } from 'node:fs';
11
+ import { Buffer } from 'node:buffer';
8
12
  import { readFile } from 'node:fs/promises';
9
13
  import { join } from 'node:path';
10
14
  import { addDshPlugin, removeDshPlugin, removePatchedDependencyEntries, runCommand } from './dsh-cli.js';
11
15
  import { dshHome, installTimeoutMs, webProfileDir } from './env.js';
12
- import { assertNpmIntegrity, readPnpmLockIntegrity, restoreSnapshots, snapshotFiles } from './npm-integrity.js';
16
+ import { assertNpmIntegrity, atomicWriteFile, readPnpmLockIntegrity, readPnpmLockOverrides, restoreSnapshots, snapshotFiles, } from './npm-integrity.js';
13
17
  import { listInstalledPlugins as defaultListInstalledPlugins, readProfileDeps, removeInstalledPlugin, } from './installed.js';
14
18
  import { setLivePluginDisabled } from './live-plugin.js';
15
19
  import { loadRegistry as defaultLoadRegistry, } from './registry.js';
16
- import { githubLatestTag as defaultGithubLatestTag, isNewerVersion, npmLatest as defaultNpmLatest, npmVersion } from './versions.js';
20
+ import { githubLatestTag as defaultGithubLatestTag, isNewerVersion, npmLatest as defaultNpmLatest, npmPackument as defaultNpmPackument, npmVersion, } from './versions.js';
17
21
  // ---------- 通用工具 ----------
18
22
  const DEFAULT_DEADLINE_MS = 60_000;
19
23
  const WITH_LATEST_MAX = 50;
@@ -42,7 +46,7 @@ export async function mapWithConcurrency(items, limit, worker) {
42
46
  return results;
43
47
  }
44
48
  function zeroCounts() {
45
- return { market: 0, tools: 0, ui: 0, search: 0, media: 0, other: 0 };
49
+ return { market: 0, tools: 0, ui: 0, search: 0, other: 0 };
46
50
  }
47
51
  function clampLimit(raw, max) {
48
52
  const n = typeof raw === 'number' && Number.isFinite(raw) ? Math.floor(raw) : max;
@@ -400,6 +404,157 @@ export async function listInstalledWithMeta(cfg = {}, opts = {}, deps = {}) {
400
404
  async function defaultRestoreInstall(profileDir) {
401
405
  return runCommand('pnpm', ['--dir', profileDir, 'install', '--frozen-lockfile'], { timeoutMs: installTimeoutMs() });
402
406
  }
407
+ async function defaultRebuildInstall(profileDir) {
408
+ return runCommand('pnpm', ['--dir', profileDir, 'install', '--no-frozen-lockfile'], { timeoutMs: installTimeoutMs() });
409
+ }
410
+ /** B3 默认退避:新发布 ~1 分钟内的升级失败多为 packument CDN 滞后(2026-09-05 实证)。 */
411
+ const NO_MATCHING_VERSION_RETRY_DELAYS_MS = [5_000, 15_000];
412
+ // ---------- B1/B2:manifest 保留与 frozen 自愈(2026-09-05 事故加固) ----------
413
+ const LOCKFILE_CONFIG_MISMATCH_RE = /ERR_PNPM_LOCKFILE_CONFIG_MISMATCH/;
414
+ const OUTDATED_LOCKFILE_RE = /ERR_PNPM_OUTDATED_LOCKFILE/;
415
+ const NO_MATCHING_VERSION_RE = /ERR_PNPM_NO_MATCHING_VERSION/;
416
+ function errText(err) {
417
+ return err instanceof Error ? err.message : String(err);
418
+ }
419
+ function sleep(ms) {
420
+ return new Promise((resolve) => setTimeout(resolve, ms));
421
+ }
422
+ async function readManifestDoc(profileDir) {
423
+ try {
424
+ const parsed = JSON.parse(await readFile(join(profileDir, 'package.json'), 'utf8'));
425
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
426
+ return null;
427
+ return parsed;
428
+ }
429
+ catch {
430
+ return null;
431
+ }
432
+ }
433
+ /**
434
+ * B1(成功路径):安装链(pnpm / 宿主 CLI)若把升级前 manifest 的顶层键丢掉
435
+ * (如 `pnpm.overrides` 事故前态),从安装前字节快照里找回并原子写回。
436
+ * 只补「快照有、现在无」的键,绝不覆盖安装刚写入的 dependencies/dsh 变更。
437
+ * 返回找回的键名列表;无需修复返回 null。
438
+ */
439
+ async function restoreManifestKeys(profileDir, snapshot) {
440
+ if (!snapshot?.existed || snapshot.bytes === null)
441
+ return null;
442
+ let prev;
443
+ try {
444
+ const parsed = JSON.parse(snapshot.bytes.toString('utf8'));
445
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
446
+ return null;
447
+ prev = parsed;
448
+ }
449
+ catch {
450
+ return null;
451
+ }
452
+ const cur = await readManifestDoc(profileDir);
453
+ if (!cur)
454
+ return null;
455
+ const missing = Object.keys(prev).filter((k) => !(k in cur));
456
+ if (missing.length === 0)
457
+ return null;
458
+ for (const key of missing)
459
+ cur[key] = prev[key];
460
+ const bytes = Buffer.from(`${JSON.stringify(cur, null, 2)}\n`, 'utf8');
461
+ await atomicWriteFile(join(profileDir, 'package.json'), bytes);
462
+ return missing;
463
+ }
464
+ /**
465
+ * B2 第一层自愈:frozen 校验报 overrides 失配时,把 lockfile 记录的 overrides
466
+ * 并入 manifest 的 `pnpm.overrides`(manifest 已有条目优先,绝不删用户手写的键)。
467
+ * lockfile 的 overrides 是上一次解析实际生效的钉版,以此为准可让 frozen 直接通过
468
+ * 而无需重建依赖树(与 2026-09-05 人工修复路径等价)。未做任何修改返回 null。
469
+ */
470
+ async function restoreOverridesFromLock(profileDir) {
471
+ let lockText;
472
+ try {
473
+ lockText = await readFile(join(profileDir, 'pnpm-lock.yaml'), 'utf8');
474
+ }
475
+ catch {
476
+ return null;
477
+ }
478
+ const lockOverrides = readPnpmLockOverrides(lockText);
479
+ const doc = await readManifestDoc(profileDir);
480
+ if (!doc)
481
+ return null;
482
+ const pnpm = doc.pnpm !== null && typeof doc.pnpm === 'object' && !Array.isArray(doc.pnpm)
483
+ ? doc.pnpm
484
+ : {};
485
+ const current = pnpm.overrides !== null && typeof pnpm.overrides === 'object' && !Array.isArray(pnpm.overrides)
486
+ ? pnpm.overrides
487
+ : {};
488
+ const merged = { ...lockOverrides, ...current };
489
+ if (JSON.stringify(merged) === JSON.stringify(current))
490
+ return null;
491
+ doc.pnpm = { ...pnpm, overrides: merged };
492
+ const bytes = Buffer.from(`${JSON.stringify(doc, null, 2)}\n`, 'utf8');
493
+ await atomicWriteFile(join(profileDir, 'package.json'), bytes);
494
+ const restored = Object.keys(lockOverrides).filter((k) => !(k in current));
495
+ return restored.length > 0 ? `(还原自 lockfile:${restored.join(', ')})` : '(与 lockfile overrides 对齐)';
496
+ }
497
+ /**
498
+ * B2 frozen 自愈阶梯:frozen install → CONFIG_MISMATCH 时先 overrides 对齐再重试 →
499
+ * 仍失配(或 OUTDATED_LOCKFILE specifier 漂移,实机实证:override 钉直接依赖时回滚
500
+ * 快照本身即 specifier 不一致)则降级 `--no-frozen-lockfile` 重建一致性。自愈动作按序
501
+ * 记入 notes(最终呈现给用户);阶梯走完仍失败时抛最后一个错误。
502
+ */
503
+ async function frozenInstallWithHeal(profileDir, d, notes) {
504
+ try {
505
+ await d.restoreInstall(profileDir);
506
+ return;
507
+ }
508
+ catch (frozenErr) {
509
+ const text = errText(frozenErr);
510
+ const isConfigMismatch = LOCKFILE_CONFIG_MISMATCH_RE.test(text);
511
+ if (!isConfigMismatch && !OUTDATED_LOCKFILE_RE.test(text))
512
+ throw frozenErr;
513
+ if (isConfigMismatch) {
514
+ const merged = await restoreOverridesFromLock(profileDir);
515
+ if (merged !== null) {
516
+ notes.push(`已把 lockfile overrides 还原进 manifest ${merged}`);
517
+ try {
518
+ await d.restoreInstall(profileDir);
519
+ notes.push('frozen 校验通过');
520
+ return;
521
+ }
522
+ catch {
523
+ /* 对齐后仍失配 → 走重建降级 */
524
+ }
525
+ }
526
+ }
527
+ try {
528
+ await d.rebuildInstall(profileDir);
529
+ }
530
+ catch (rebuildErr) {
531
+ throw new Error(`${text};lockfile 重建(--no-frozen-lockfile)也失败:${errText(rebuildErr)}`);
532
+ }
533
+ notes.push('lockfile 已重建(--no-frozen-lockfile 完成一致性安装)');
534
+ }
535
+ }
536
+ /**
537
+ * B3:ERR_PNPM_NO_MATCHING_VERSION 在刚发布的窗口内几乎都是 packument CDN 滞后
538
+ * (2026-09-05 实证:/latest 已新、完整 packument 仍旧)。按 retryDelaysMs 退避重试,
539
+ * 每次重试前拉一次完整 packument 预热/校验;其他错误与重试耗尽后原样抛出。
540
+ */
541
+ async function addDshPluginWithRetry(spec, pkg, d, retryDelaysMs, timeoutMs, signal) {
542
+ let attempt = 0;
543
+ for (;;) {
544
+ try {
545
+ return await d.addDshPlugin(spec);
546
+ }
547
+ catch (err) {
548
+ if (!NO_MATCHING_VERSION_RE.test(errText(err)) || attempt >= retryDelaysMs.length)
549
+ throw err;
550
+ const delay = retryDelaysMs[attempt] ?? 0;
551
+ attempt += 1;
552
+ if (delay > 0)
553
+ await sleep(delay);
554
+ await d.npmPackument(pkg, timeoutMs, signal).catch(() => undefined);
555
+ }
556
+ }
557
+ }
403
558
  /** 从 registry 收录条目安装(npm → 精确锁定最新版;github → 锁 HEAD SHA)。 */
404
559
  export async function installFromRegistry(id, cfg = {}, opts = {}, deps) {
405
560
  const loaded = await (deps?.loadRegistry ?? defaultLoadRegistry)(cfg, { namespace: opts.namespace ?? 'host' });
@@ -416,12 +571,15 @@ export async function installEntry(entry, cfg = {}, opts = {}, deps) {
416
571
  const d = {
417
572
  npmLatest: deps?.npmLatest ?? defaultNpmLatest,
418
573
  npmVersion: deps?.npmVersion ?? npmVersion,
574
+ npmPackument: deps?.npmPackument ?? defaultNpmPackument,
419
575
  addDshPlugin: deps?.addDshPlugin ?? addDshPlugin,
420
576
  removeDshPlugin: deps?.removeDshPlugin ?? removeDshPlugin,
421
577
  readProfileDeps: deps?.readProfileDeps ?? readProfileDeps,
422
578
  readLockIntegrity: deps?.readLockIntegrity ?? readPnpmLockIntegrity,
423
579
  restoreInstall: deps?.restoreInstall ?? defaultRestoreInstall,
580
+ rebuildInstall: deps?.rebuildInstall ?? defaultRebuildInstall,
424
581
  };
582
+ const retryDelaysMs = deps?.retryDelaysMs ?? NO_MATCHING_VERSION_RETRY_DELAYS_MS;
425
583
  const profileDir = deps?.profileDir ?? webProfileDir();
426
584
  if (entry.source === 'npm' && entry.npm) {
427
585
  const pkg = entry.npm;
@@ -441,14 +599,18 @@ export async function installEntry(entry, cfg = {}, opts = {}, deps) {
441
599
  if (!expectedIntegrity)
442
600
  throw new Error(`npm metadata 缺少 dist integrity:${pkg}@${version},拒绝安装`);
443
601
  const spec = `${pkg}@${version}`;
444
- // 安装前快照:失败时 best-effort 依赖回滚的依据
602
+ // 安装前快照:失败时 best-effort 依赖回滚与 B1 键找回的依据
445
603
  const snapshots = await snapshotFiles([
446
604
  join(profileDir, 'package.json'),
447
605
  join(profileDir, 'pnpm-lock.yaml'),
448
606
  join(profileDir, 'pnpm-workspace.yaml'),
449
607
  ]);
608
+ const healNotes = [];
609
+ // 区分 add 阶段与校验阶段失败:回滚报错不再把 pnpm 安装失败误标成「integrity 校验失败」
610
+ let phase = 'install';
450
611
  try {
451
- const res = await d.addDshPlugin(spec);
612
+ const res = await addDshPluginWithRetry(spec, pkg, d, retryDelaysMs, timeoutMs, opts.signal);
613
+ phase = 'verify';
452
614
  const depsNow = await d.readProfileDeps(profileDir);
453
615
  if (depsNow[pkg] === undefined)
454
616
  throw new Error(`安装后未在 profile 依赖中找到 ${pkg}`);
@@ -463,6 +625,17 @@ export async function installEntry(entry, cfg = {}, opts = {}, deps) {
463
625
  }
464
626
  const actual = d.readLockIntegrity(lockText, pkg, version);
465
627
  assertNpmIntegrity(expectedIntegrity, actual, pkg, version);
628
+ // B1(成功路径):安装链若丢了 manifest 顶层键(如 pnpm.overrides),从快照找回并复验 frozen 一致性
629
+ const restoredKeys = await restoreManifestKeys(profileDir, snapshots[0]);
630
+ if (restoredKeys) {
631
+ healNotes.push(`安装链丢失了 manifest 顶层键(${restoredKeys.join(', ')}),已从安装前快照找回`);
632
+ try {
633
+ await frozenInstallWithHeal(profileDir, d, healNotes);
634
+ }
635
+ catch (healErr) {
636
+ healNotes.push(`frozen 一致性自愈未完成,profile 可能需要人工检查:${errText(healErr)}`);
637
+ }
638
+ }
466
639
  return {
467
640
  id: entry.id,
468
641
  pkg,
@@ -470,18 +643,19 @@ export async function installEntry(entry, cfg = {}, opts = {}, deps) {
470
643
  version,
471
644
  usedAllowAllBuilds: res.usedAllowAllBuilds,
472
645
  needsRestart: true,
473
- output: res.output.slice(-800),
646
+ output: res.output.slice(-800) + (healNotes.length > 0 ? `\n[dsh-m 自愈] ${healNotes.join(';')}` : ''),
474
647
  };
475
648
  }
476
649
  catch (err) {
477
- // best-effort rollback:原子恢复 manifest/lock/workspace 快照,再 frozen-lockfile 重装
650
+ // best-effort rollback:原子恢复 manifest/lock/workspace 快照字节,再按 frozen 自愈阶梯收敛
478
651
  let rollbackError = null;
652
+ const rollbackNotes = [];
479
653
  try {
480
654
  await restoreSnapshots(snapshots);
481
- await d.restoreInstall(profileDir);
655
+ await frozenInstallWithHeal(profileDir, d, rollbackNotes);
482
656
  }
483
657
  catch (rerr) {
484
- rollbackError = rerr instanceof Error ? rerr.message : String(rerr);
658
+ rollbackError = errText(rerr);
485
659
  // 原先不存在该依赖且恢复安装失败:尝试移除
486
660
  const originallyAbsent = snapshots[0] && snapshots[0].existed && (() => {
487
661
  try {
@@ -497,14 +671,16 @@ export async function installEntry(entry, cfg = {}, opts = {}, deps) {
497
671
  rollbackError = null;
498
672
  }
499
673
  catch (rmErr) {
500
- rollbackError = `${rollbackError};移除 ${pkg} 也失败(${rmErr instanceof Error ? rmErr.message : String(rmErr)})`;
674
+ rollbackError = `${rollbackError};移除 ${pkg} 也失败(${errText(rmErr)})`;
501
675
  }
502
676
  }
503
677
  }
678
+ const prefix = phase === 'install' ? '安装失败' : 'integrity 校验失败';
504
679
  if (rollbackError) {
505
- throw new Error(`integrity 校验失败(${err instanceof Error ? err.message : String(err)});依赖回滚也失败(${rollbackError}),profile 可能需要人工修复`);
680
+ throw new Error(`${prefix}(${errText(err)});依赖回滚也失败(${rollbackError}),profile 可能需要人工修复`);
506
681
  }
507
- throw new Error(`integrity 校验失败,已回滚到安装前状态:${err instanceof Error ? err.message : String(err)}`);
682
+ const suffix = rollbackNotes.length > 0 ? `(${rollbackNotes.join(';')})` : '';
683
+ throw new Error(`${prefix},已回滚到安装前状态${suffix}:${errText(err)}`);
508
684
  }
509
685
  }
510
686
  if (entry.github) {
@@ -99,6 +99,33 @@ export function assertNpmIntegrity(expected, actual, pkg, version) {
99
99
  throw new Error(`integrity 不一致:${pkg}@${version} 期望 ${expected},lockfile 实际 ${actual}`);
100
100
  }
101
101
  }
102
+ /**
103
+ * 解析 pnpm lockfile v9 顶层的 `overrides:` 映射(键可带引号,值为版本串)。
104
+ * 无该区块返回 {}。只认 pnpm 自己生成的两空格顶层缩进;单条目解析失败即停(宁可少配,
105
+ * 不可错配)。frozen 自愈用它把 lockfile 记录的 overrides 还原进 manifest(2026-09-05 事故)。
106
+ */
107
+ export function readPnpmLockOverrides(lockText) {
108
+ const lines = lockText.split(/\r?\n/);
109
+ const start = lines.findIndex((l) => l.trim() === 'overrides:');
110
+ const result = {};
111
+ if (start === -1)
112
+ return result;
113
+ for (let i = start + 1; i < lines.length; i++) {
114
+ const line = lines[i];
115
+ if (line.trim() === '')
116
+ continue;
117
+ if (!/^\s{2}\S/.test(line))
118
+ break;
119
+ const m = /^\s{2}(?:'([^']+)'|"([^"]+)"|([^\s:][^:]*?))\s*:\s*(.+?)\s*$/.exec(line);
120
+ if (!m)
121
+ break;
122
+ const key = m[1] ?? m[2] ?? m[3];
123
+ if (key === undefined)
124
+ break;
125
+ result[key] = m[4];
126
+ }
127
+ return result;
128
+ }
102
129
  async function atomicWriteFile(path, bytes) {
103
130
  await mkdir(dirname(path), { recursive: true });
104
131
  const tmp = `${path}.restore-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
@@ -113,6 +140,7 @@ async function atomicWriteFile(path, bytes) {
113
140
  await rm(path, { force: true });
114
141
  await rename(tmp, path);
115
142
  }
143
+ export { atomicWriteFile };
116
144
  /** 记录 profile 关键文件的字节快照(package.json / pnpm-lock.yaml / pnpm-workspace.yaml)。 */
117
145
  export async function snapshotFiles(paths) {
118
146
  return Promise.all(paths.map(async (path) => {
@@ -10,7 +10,7 @@ import { constants as fsConstants, mkdirSync } from 'node:fs';
10
10
  import { lstat, mkdir, open, readFile, readdir, realpath, rename, rm } from 'node:fs/promises';
11
11
  import { cacheDir } from './env.js';
12
12
  import { decodeUtf8Fatal, fetchJsonLimitedMeta } from './httpx.js';
13
- export const CATEGORIES = ['market', 'tools', 'ui', 'search', 'media', 'other'];
13
+ export const CATEGORIES = ['market', 'tools', 'ui', 'search', 'other'];
14
14
  /** 容量上限:2 MiB 是原始 UTF-8 bytes(不是字符数);条目超限拒绝整份清单。 */
15
15
  export const MAX_REGISTRY_BYTES = 2 * 1024 * 1024;
16
16
  export const MAX_PLUGINS = 1000;
@@ -39,6 +39,17 @@ export async function npmVersion(pkg, version, timeoutMs = 20_000, signal) {
39
39
  tarball: typeof data.dist?.tarball === 'string' ? data.dist.tarball : undefined,
40
40
  };
41
41
  }
42
+ /**
43
+ * 拉取完整 packument(NO_MATCHING_VERSION 退避重试前的预热/校验原语)。
44
+ * 返回该包已知的全部版本号;解析不出 versions 时返回空列表(不抛)。
45
+ */
46
+ export async function npmPackument(pkg, timeoutMs = 20_000, signal) {
47
+ if (!/^@?[A-Za-z0-9-._~]+(\/[A-Za-z0-9-._~]+)?$/.test(pkg))
48
+ throw new Error(`无效 npm 包名: ${pkg}`);
49
+ const data = await fetchJsonLimited(`https://registry.npmjs.org/${encodeURIComponent(pkg)}`, { timeoutMs, signal, maxBytes: 8 * 1024 * 1024 });
50
+ const versions = data?.versions !== null && typeof data?.versions === 'object' ? Object.keys(data.versions) : [];
51
+ return { versions };
52
+ }
42
53
  export async function npmLatest(pkg, timeoutMs = 20_000, signal) {
43
54
  // 允许 scoped 包名:@scope/name(isSafePkgName 同款字符集)
44
55
  if (!/^@?[A-Za-z0-9-._~]+(\/[A-Za-z0-9-._~]+)?$/.test(pkg))
package/lib/tools.js CHANGED
@@ -7,7 +7,6 @@ export const CATEGORY_LABELS = {
7
7
  tools: '工具',
8
8
  ui: '界面',
9
9
  search: '搜索',
10
- media: '多媒体',
11
10
  other: '其他',
12
11
  };
13
12
  function cloneJson(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-m",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "DSH Marketplace — 个人自用的 DeepSeek Harness 插件市场:收录、安装、卸载、升级 DSH 插件",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/registry.json CHANGED
@@ -1,6 +1,17 @@
1
1
  {
2
2
  "version": 1,
3
3
  "plugins": [
4
+ {
5
+ "id": "dsh-m",
6
+ "name": "DSH Marketplace",
7
+ "description": "可自定义 Registry 内容的 DeepSeek Harness 插件市场:收录、安装、卸载、升级 DSH 插件,一站搞定。",
8
+ "category": "market",
9
+ "tags": ["市场", "插件管理"],
10
+ "source": "npm",
11
+ "npm": "dsh-m",
12
+ "github": "iasiv5/dsh-m",
13
+ "homepage": "https://github.com/iasiv5/dsh-m"
14
+ },
4
15
  {
5
16
  "id": "dsh-skins",
6
17
  "name": "DSH Skins",
@@ -12,6 +23,28 @@
12
23
  "github": "iasiv5/dsh-skins",
13
24
  "homepage": "https://github.com/iasiv5/dsh-skins"
14
25
  },
26
+ {
27
+ "id": "dsh-copilot-auth",
28
+ "name": "DSH Copilot Auth",
29
+ "description": "为 DSH 内置 GitHub Copilot 提供方补上设备码(device flow)登录/注销设置页,并预置开箱即用的 Copilot 路由;凭据托管 DSH 内置凭据库,模型目录自动同步。",
30
+ "category": "tools",
31
+ "tags": ["Copilot", "认证", "模型接入"],
32
+ "source": "npm",
33
+ "npm": "@inventec/dsh-copilot-auth",
34
+ "github": "iasiv5/dsh-copilot-auth",
35
+ "homepage": "https://github.com/iasiv5/dsh-copilot-auth"
36
+ },
37
+ {
38
+ "id": "dsh-skip-browser-auth",
39
+ "name": "DSH Skip Browser Auth",
40
+ "description": "在已适配版本上替换官方 Connection 提供方,跳过 BrowserAuth,不再需要输入烦人的Token。(当前已适配:0.1.2-rc.1)",
41
+ "category": "tools",
42
+ "tags": ["BrowserAuth", "连接", "安全"],
43
+ "source": "npm",
44
+ "npm": "@iasiv5/dsh-skip-browser-auth",
45
+ "github": "iasiv5/dsh-skip-browser-auth",
46
+ "homepage": "https://github.com/iasiv5/dsh-skip-browser-auth"
47
+ },
15
48
  {
16
49
  "id": "modsearch",
17
50
  "name": "ModSearch",
@@ -23,6 +56,17 @@
23
56
  "github": "liustack/modsearch",
24
57
  "homepage": "https://github.com/liustack/modsearch"
25
58
  },
59
+ {
60
+ "id": "dsh-auth",
61
+ "name": "DSH Auth",
62
+ "description": "DSH Web 管理员认证插件:Argon2id 口令、可撤销会话、双语界面,经 Caddy forward_auth 边缘保护页面/API/下载/SSE/WebSocket,Harness 保持仅回环监听。",
63
+ "category": "tools",
64
+ "tags": ["认证", "安全"],
65
+ "source": "npm",
66
+ "npm": "dsh-auth",
67
+ "github": "hxy91819/dsh-auth",
68
+ "homepage": "https://github.com/hxy91819/dsh-auth"
69
+ },
26
70
  {
27
71
  "id": "deepseek-harness-lark",
28
72
  "name": "DeepSeek Harness Lark",
@@ -77,39 +121,6 @@
77
121
  "npm": "deepseek-harness-dingtalk",
78
122
  "github": "sliverp/DeepSeek-harness-dingtalk",
79
123
  "homepage": "https://github.com/sliverp/DeepSeek-harness-dingtalk"
80
- },
81
- {
82
- "id": "dsh-auth",
83
- "name": "DSH Auth",
84
- "description": "DSH Web 管理员认证插件:Argon2id 口令、可撤销会话、双语界面,经 Caddy forward_auth 边缘保护页面/API/下载/SSE/WebSocket,Harness 保持仅回环监听。",
85
- "category": "tools",
86
- "tags": ["认证", "安全"],
87
- "source": "npm",
88
- "npm": "dsh-auth",
89
- "github": "hxy91819/dsh-auth",
90
- "homepage": "https://github.com/hxy91819/dsh-auth"
91
- },
92
- {
93
- "id": "dsh-copilot-auth",
94
- "name": "DSH Copilot Auth",
95
- "description": "为 DSH 内置 GitHub Copilot 提供方补上设备码(device flow)登录/注销设置页,并预置开箱即用的 Copilot 路由;凭据托管 DSH 内置凭据库,模型目录自动同步,中英双语。",
96
- "category": "tools",
97
- "tags": ["Copilot", "认证", "模型接入"],
98
- "source": "npm",
99
- "npm": "@inventec/dsh-copilot-auth",
100
- "github": "iasiv5/dsh-copilot-auth",
101
- "homepage": "https://github.com/iasiv5/dsh-copilot-auth"
102
- },
103
- {
104
- "id": "dsh-m",
105
- "name": "DSH Marketplace",
106
- "description": "个人自用的 DeepSeek Harness 插件市场:收录、安装、卸载、升级 DSH 插件,含 Web 界面、agent 工具与 CLI。",
107
- "category": "market",
108
- "tags": ["市场", "插件管理"],
109
- "source": "npm",
110
- "npm": "dsh-m",
111
- "github": "iasiv5/dsh-m",
112
- "homepage": "https://github.com/iasiv5/dsh-m"
113
124
  }
114
125
  ]
115
126
  }