dsh-m 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -12,14 +12,20 @@ A DeepSeek Harness (DSH) plugin marketplace with a customizable registry: **brow
12
12
 
13
13
  ## Quick start
14
14
 
15
- ```sh
16
- npm install -g dsh-m # or: dsh plugin --profile web add dsh-m@<version>
15
+ **Paste this whole block to your agent to install:**
16
+
17
+ ```text
18
+ Install and enable the DSH plugin dsh-m:
19
+ 1. Run `dsh plugin --profile web add dsh-m`
20
+ 2. Restart DSH Web to load the plugin (on this machine: `systemctl --user restart dsh-web.service`; or restart it your usual way)
21
+ 3. Poll `curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3080` until it returns 200 again
22
+ 4. Run `curl -s -X POST http://127.0.0.1:3080/dshm -H 'content-type: application/json' -d '{"method":"ping"}'` and confirm the reply has `plugin: dsh-m`
23
+ 5. Then remind me to refresh the page and click "Plugin Marketplace" at the bottom of the sidebar
17
24
  ```
18
25
 
19
- 1. **Restart DSH Web** after installing (one-click restart works too).
20
- 2. Refresh the page and click "**Plugin Marketplace**" at the bottom of the sidebar.
26
+ > `npm install -g dsh-m` installs only the `dshm` CLI (terminal management) it does **not** register the plugin into the web profile; it can coexist with the plugin install above.
21
27
 
22
- The default registry includes DSH Skins, DSH Web Search, the Lark / QQ / Weixin / WeCom / DingTalk channel bridges, and dsh-m itself.
28
+ The default registry includes DSH Skins, ModSearch, the Lark / QQ / Weixin / WeCom / DingTalk channel bridges, and dsh-m itself.
23
29
 
24
30
  ## Panel
25
31
 
@@ -58,7 +64,7 @@ When the registry is unavailable, `registry` / `search` / `outdated` print the c
58
64
 
59
65
  ## Registry
60
66
 
61
- `registry.json` is hand-curated and served at runtime via **raw.githubusercontent / jsDelivr `@main`** (local 60-min TTL cache + bundled snapshot fallback) — listing updates are decoupled from plugin releases. To add or amend a listing, edit `registry.json` and open a PR; CI validates the strict schema, npm/GitHub existence, duplicate ids and URL reachability.
67
+ `registry.json` is hand-curated and fetched at runtime in order: **GitHub raw (`@main`) → GitHub mirror (jsDelivr CDN, backup line) → local 60-min TTL cache bundled snapshot** — listing updates are decoupled from plugin releases. To add or amend a listing, edit `registry.json` and open a PR; CI validates the strict schema, npm/GitHub existence, duplicate ids and URL reachability.
62
68
 
63
69
  **Custom registry (overrides the official one)**: the Settings tab supports a single custom registry address that **replaces** the default registry (no merging):
64
70
 
package/README.md CHANGED
@@ -12,14 +12,20 @@
12
12
 
13
13
  ## 30 秒上手
14
14
 
15
- ```sh
16
- npm install -g dsh-m # 或 dsh plugin --profile web add dsh-m@<version>
15
+ **把下面整段贴给 agent 即可完成安装**:
16
+
17
+ ```text
18
+ 安装并启用 DSH 插件 dsh-m:
19
+ 1. 执行 `dsh plugin --profile web add dsh-m`
20
+ 2. 重启 DSH Web 使插件加载(本机:`systemctl --user restart dsh-web.service`;或按你的部署方式重启)
21
+ 3. 轮询 `curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3080`,直到恢复 200
22
+ 4. 执行 `curl -s -X POST http://127.0.0.1:3080/dshm -H 'content-type: application/json' -d '{"method":"ping"}'`,确认返回 `plugin: dsh-m`
23
+ 5. 完成后提醒我刷新页面,点击侧栏底部的「插件市场」
17
24
  ```
18
25
 
19
- 1. 安装后**重启 DSH Web**(一键重启亦可)。
20
- 2. 刷新页面,点击侧栏底部的「**插件市场**」。
26
+ > `npm install -g dsh-m` 只安装 `dshm` CLI(终端管理用),**不会**把插件注册进 web profile;与上面插件安装不冲突,可并存。
21
27
 
22
- 默认收录清单里的插件包括:DSH Skins、DSH Web Search、Lark / QQ / 微信 / 企业微信 / 钉钉通道桥,以及 dsh-m 自身。
28
+ 默认收录清单里的插件包括:DSH Skins、ModSearch、Lark / QQ / 微信 / 企业微信 / 钉钉通道桥,以及 dsh-m 自身。
23
29
 
24
30
  ## 界面(侧栏「插件市场」)
25
31
 
@@ -58,7 +64,7 @@ dshm restart --yes
58
64
 
59
65
  ## 收录清单(registry)
60
66
 
61
- `registry.json` 手工 curated,运行时经 **raw.githubusercontent / jsDelivr `@main`** 分发(本地 60 分钟 TTL 缓存 + 包内快照兜底)——收录更新与插件发版**解耦**。收录 / 修订直接改 `registry.json` 发 PR,CI 自动校验:严格 schema、npm 包与 GitHub 仓库存在性、重复 id、URL 可达性。
67
+ `registry.json` 手工 curated,运行时按 **GitHub 原始文件(raw @main)→ GitHub 镜像(jsDelivr CDN,备用线路)→ 本地 60 分钟 TTL 缓存 包内快照** 的顺序获取——收录更新与插件发版**解耦**,push 后最多等一个缓存周期(可在设置页强制刷新)。收录 / 修订直接改 `registry.json` 发 PR,CI 自动校验:严格 schema、npm 包与 GitHub 仓库存在性、重复 id、URL 可达性。
62
68
 
63
69
  **自定义收录清单(可覆盖官方清单)**:设置页支持单一自定义 registry 地址,**整体覆盖**默认清单(不合并):
64
70
 
package/docs/DESIGN.md CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  ### 2.1 载体与分发(v1.1 可自定义 registry 定稿)
17
17
  - repo 内**单文件 `registry.json`**,手工 curated,版本号**不写死**(运行时实查 npm/GitHub)。
18
- - **默认源**获取顺序:raw.githubusercontent `@main` → jsDelivr `@main` → 默认 TTL 缓存 → npm 包内快照兜底。收录更新与插件发版**解耦**。
18
+ - **默认源**获取顺序:raw.githubusercontent `@main` → jsDelivr `@main` → 默认 TTL 缓存 → npm 包内快照兜底。jsDelivr 是 GitHub 内容的免费 CDN 镜像,仅作 raw 拉取失败时的**备用线路**(覆盖大陆可达性与 GitHub 故障;CDN 缓存可能滞后数小时,可用 purge.jsdelivr.net 手动清理)。收录更新与插件发版**解耦**。
19
19
  - **自定义覆盖源(单一地址,整体覆盖,不合并)**:`registryUrl` 为空 = 官方默认清单;非空 = 一个 HTTPS URL(或 loopback HTTP,仅本机管理员信任边界,不承诺 DNS rebinding 防护)或 DSH Web 主机上的本地普通文件(绝对路径 / `file://`,`realpath` + `O_NOFOLLOW` 同 fd 读取与复核,严格 UTF-8,2 MiB 原始字节上限)。自定义源失败只回退**该源自己的缓存**,绝不静默改用官方清单;无可缓存数据时返回空清单 + 不可用状态。
20
20
  - **「新增插件」流程**:设置页下载默认 `registry.json` → 用户自行编辑副本 → 填入副本地址「校验并应用」。副本是独立快照,不自动同步官方新条目。
21
21
  - **严格 v1 schema**:顶层只允许 `version/plugins`,条目只允许 `id/name/description/category/tags/source/npm/github/homepage/icon`;未知字段、非法 ID/npm/GitHub/URL、重复 ID/tag、字段超限、`plugins` 超过 1,000 条均拒绝整份清单(不截断、不部分加载);超过 200 条提示性能边界。
@@ -66,7 +66,7 @@
66
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 与间接依赖已字节级回滚。
67
67
  - **安装(GitHub 源)**:解析并**锁定 commit SHA**(`github:owner/repo#sha`),skillhub 同款。
68
68
  - **已装识别**:读 profile `package.json` dependencies,与 registry 匹配 → 标注「市场安装」;不匹配的也列出,标注「非市场安装 / 来源未知」。卸载/升级对两类都可用。
69
- - **卸载**:live-disable(先让 client bundle 下线,避免 404)→ `dsh plugin remove`。**不清理插件产生的数据/配置**,但把检测到的疑似残留路径(如 `~/.dsh/<plugin>.json`)列出报告。
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`)列出报告。
70
70
  - **升级**:**按需检查**(`dshm_outdated` / `dshm_list` 时实时比对本地版本 vs npm latest / GitHub main),半自动——展示升级计划,确认后执行。**不做后台定时器**。
71
71
  - **自更新**:dsh-m 对自己同样做版本比对 + 提示升级(设置页呈现)。
72
72
  - **重启**:内置**一键重启**,复用 skillhub 验证过的重启路径(本机 `dsh-web.service` 是转发 shim,不新建 systemd 单元、不监听 3080)。安装/卸载/升级完成后 GUI 弹「需重启生效 [一键重启]」横幅,工具返回重启提示。
package/lib/cli.js CHANGED
@@ -51,6 +51,19 @@ function requireYes(flags, action) {
51
51
  function sourceLabel(s) {
52
52
  return { npm: 'npm', github: 'github', link: '本地 link', file: '本地 file', unknown: '未知' }[s] || s;
53
53
  }
54
+ /** registry 来源的用户友好名称(CLI 本地终端可见)。 */
55
+ function registrySourceLabel(s) {
56
+ return {
57
+ 'default-raw': 'GitHub 原始文件(@main)',
58
+ 'default-jsdelivr': 'GitHub 镜像(备用)',
59
+ 'default-cache': '默认清单缓存',
60
+ bundled: '包内快照(兜底)',
61
+ 'custom-url': '自定义 URL 源',
62
+ 'custom-file': '本地文件源',
63
+ 'custom-cache': '自定义源(缓存)',
64
+ 'custom-unavailable': '自定义源(不可用)',
65
+ }[s] || s;
66
+ }
54
67
  function addressLines(state) {
55
68
  return [
56
69
  `配置地址:${state.configuredAddress === '' ? '(默认官方清单)' : state.configuredAddress}`,
@@ -138,7 +151,7 @@ async function runCliDispatch(argv, deps, io) {
138
151
  out(` ${it.description}`);
139
152
  }
140
153
  const s = result.registryState;
141
- out(`\n来源:${s.source}${s.stale ? '(缓存)' : ''} · 更新:${s.fetchedAt ?? '—'} · 共 ${result.total} 条`);
154
+ out(`\n来源:${registrySourceLabel(s.source)}${s.stale ? '(缓存)' : ''} · 更新:${s.fetchedAt ?? '—'} · 共 ${result.total} 条`);
142
155
  return 0;
143
156
  }
144
157
  case 'list': {
@@ -188,7 +201,7 @@ async function runCliDispatch(argv, deps, io) {
188
201
  err(line);
189
202
  return 1;
190
203
  }
191
- out(`来源:${loaded.source} · 更新时间:${loaded.fetchedAt ?? '—'} · 条目:${loaded.count}`);
204
+ out(`来源:${registrySourceLabel(loaded.source)} · 更新时间:${loaded.fetchedAt ?? '—'} · 条目:${loaded.count}`);
192
205
  for (const line of addressLines(loaded))
193
206
  out(line);
194
207
  for (const e of loaded.registry.plugins) {
package/lib/client.js CHANGED
@@ -196,12 +196,12 @@ var ZH = {
196
196
  "settings.about": "\u5173\u4E8E",
197
197
  "settings.about.text": "\u4E2A\u4EBA DSH \u63D2\u4EF6\u5E02\u573A\uFF1A\u6536\u5F55\u3001\u5B89\u88C5\u3001\u5378\u8F7D\u3001\u5347\u7EA7\u5168\u90E8\u672C\u673A\u5B8C\u6210\uFF1B\u6536\u5F55\u6E05\u5355\u652F\u6301\u81EA\u5B9A\u4E49\u8986\u76D6\uFF0C\u5E94\u7528\u5373\u65F6\u751F\u6548\u3002",
198
198
  "src.override": "\u81EA\u5B9A\u4E49\u6E90",
199
- "src.jsdelivr": "jsDelivr\uFF08@main\uFF09",
199
+ "src.jsdelivr": "GitHub \u955C\u50CF\uFF08\u5907\u7528\uFF09",
200
200
  "src.raw": "raw.githubusercontent\uFF08@main\uFF09",
201
201
  "src.cache": "\u672C\u5730\u7F13\u5B58",
202
202
  "src.bundled": "\u5305\u5185\u5FEB\u7167\uFF08\u515C\u5E95\uFF09",
203
- "src.default.raw": "raw.githubusercontent\uFF08@main\uFF09",
204
- "src.default.jsdelivr": "jsDelivr\uFF08@main\uFF09",
203
+ "src.default.raw": "GitHub \u539F\u59CB\u6587\u4EF6\uFF08@main\uFF09",
204
+ "src.default.jsdelivr": "GitHub \u955C\u50CF\uFF08\u5907\u7528\uFF09",
205
205
  "src.default.cache": "\u9ED8\u8BA4\u6E05\u5355\u7F13\u5B58",
206
206
  "src.custom.url": "\u81EA\u5B9A\u4E49 URL \u6E90",
207
207
  "src.custom.file": "\u672C\u5730\u6587\u4EF6\u6E90",
@@ -365,12 +365,12 @@ var EN = {
365
365
  "settings.about": "About",
366
366
  "settings.about.text": "A personal DSH plugin marketplace \u2014 browse, install, uninstall and upgrade, all local; registry overrides apply live.",
367
367
  "src.override": "Custom source",
368
- "src.jsdelivr": "jsDelivr (@main)",
368
+ "src.jsdelivr": "GitHub mirror (backup)",
369
369
  "src.raw": "raw.githubusercontent (@main)",
370
370
  "src.cache": "Local cache",
371
371
  "src.bundled": "Bundled snapshot (fallback)",
372
- "src.default.raw": "raw.githubusercontent (@main)",
373
- "src.default.jsdelivr": "jsDelivr (@main)",
372
+ "src.default.raw": "GitHub raw (@main)",
373
+ "src.default.jsdelivr": "GitHub mirror (backup)",
374
374
  "src.default.cache": "Default registry cache",
375
375
  "src.custom.url": "Custom URL source",
376
376
  "src.custom.file": "Local file source",
@@ -514,7 +514,7 @@ var CSS = `
514
514
  .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)}
515
515
  .dshm-actions{display:flex;gap:6px;flex-wrap:wrap;margin-top:4px}
516
516
  .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}
517
- .dshm-banner .dshm-banner-text{flex:1}
517
+ .dshm-banner .dshm-banner-text{flex:1;max-height:140px;overflow:auto;overscroll-behavior:contain;white-space:pre-wrap;word-break:break-word;line-height:18px}
518
518
  .dshm-row{display:flex;align-items:center;gap:8px}
519
519
  .dshm-kv{display:grid;grid-template-columns:96px 1fr;gap:6px 10px;font-size:12px;align-items:baseline}
520
520
  .dshm-kv .k{color:var(--dsw-alias-label-caption,#6b7280);font-size:11px}
@@ -10,6 +10,119 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
10
10
  import { dirname, isAbsolute, join, resolve } from 'node:path';
11
11
  import { installTimeoutMs, WEB_PROFILE, webProfileDir } from './env.js';
12
12
  import { createProgressTracker } from './progress.js';
13
+ /** pnpm patchedDependencies 条目键与目标包匹配:`pkg` 或 `pkg@任意版本/区间`。 */
14
+ function matchesPatchedKey(key, pkg) {
15
+ return key === pkg || key.startsWith(`${pkg}@`);
16
+ }
17
+ /**
18
+ * 摘除 profile 里目标包的 pnpm 补丁条目:pnpm-workspace.yaml 顶层
19
+ * `patchedDependencies` 与 package.json 的 `pnpm.patchedDependencies` 两处。
20
+ * 卸载场景下依赖被移除后,残留补丁条目会让 pnpm 以 ERR_PNPM_UNUSED_PATCH
21
+ * 拒绝整个 remove/install。只精确匹配目标包的键(`pkg` / `pkg@ver`),
22
+ * 其他包的补丁不动;补丁文件本体保留在磁盘(删包不删数据,DESIGN.md §3)。
23
+ */
24
+ export function removePatchedDependencyEntries(profileDirectory, pkg, deps = {}) {
25
+ const target = String(pkg || '').trim();
26
+ if (!target || !isSafePluginTarget(target))
27
+ return { changed: false, orphanedPatchFiles: [] };
28
+ const exists = deps.existsSync ?? existsSync;
29
+ const read = deps.readFileSync ?? readFileSync;
30
+ const write = deps.writeFileSync ?? writeFileSync;
31
+ const orphanedPatchFiles = [];
32
+ let changed = false;
33
+ // --- pnpm-workspace.yaml(pnpm≥10 补丁配置落点;行级手术,只摘匹配键) ---
34
+ const wsFile = join(profileDirectory, 'pnpm-workspace.yaml');
35
+ let ws = '';
36
+ try {
37
+ ws = read(wsFile, 'utf8');
38
+ }
39
+ catch {
40
+ /* 无文件则跳过 */
41
+ }
42
+ if (ws) {
43
+ const srcLines = ws.split('\n');
44
+ let start = -1;
45
+ for (let i = 0; i < srcLines.length; i++) {
46
+ if (/^patchedDependencies:\s*$/.test(srcLines[i])) {
47
+ start = i;
48
+ break;
49
+ }
50
+ }
51
+ if (start >= 0) {
52
+ // 块边界:下一个顶层键(无缩进行)或文件尾
53
+ let end = srcLines.length;
54
+ for (let i = start + 1; i < srcLines.length; i++) {
55
+ if (/^\S/.test(srcLines[i])) {
56
+ end = i;
57
+ break;
58
+ }
59
+ }
60
+ const keptBlock = [];
61
+ for (const line of srcLines.slice(start + 1, end)) {
62
+ // 条目行:缩进键 + `:` + 补丁文件路径(键可带引号)
63
+ const m = /^\s+(["']?)([^"':]+?)\1\s*:\s*(.+?)\s*$/.exec(line);
64
+ if (m && matchesPatchedKey(m[2].trim(), target)) {
65
+ changed = true;
66
+ const file = resolve(profileDirectory, m[3].trim());
67
+ if (exists(file))
68
+ orphanedPatchFiles.push(file);
69
+ }
70
+ else {
71
+ keptBlock.push(line);
72
+ }
73
+ }
74
+ if (changed) {
75
+ // 块内条目被摘空 → 连 `patchedDependencies:` 头一起移除,避免留下空映射
76
+ const stillHasEntry = keptBlock.some((l) => /^\s+\S/.test(l));
77
+ const next = stillHasEntry
78
+ ? [...srcLines.slice(0, start + 1), ...keptBlock, ...srcLines.slice(end)]
79
+ : [...srcLines.slice(0, start), ...srcLines.slice(end)];
80
+ write(wsFile, next.join('\n'));
81
+ }
82
+ }
83
+ }
84
+ // --- package.json#pnpm.patchedDependencies(pnpm<10 落点,兼容清理) ---
85
+ const pkgJsonFile = join(profileDirectory, 'package.json');
86
+ let raw = '';
87
+ try {
88
+ raw = read(pkgJsonFile, 'utf8');
89
+ }
90
+ catch {
91
+ /* 无文件则跳过 */
92
+ }
93
+ if (raw) {
94
+ try {
95
+ const doc = JSON.parse(raw);
96
+ const patched = doc?.pnpm?.patchedDependencies;
97
+ if (patched && typeof patched === 'object') {
98
+ let touched = false;
99
+ for (const [key, file] of Object.entries(patched)) {
100
+ if (!matchesPatchedKey(key, target))
101
+ continue;
102
+ delete patched[key];
103
+ touched = true;
104
+ changed = true;
105
+ if (typeof file === 'string') {
106
+ const abs = resolve(profileDirectory, file);
107
+ if (exists(abs))
108
+ orphanedPatchFiles.push(abs);
109
+ }
110
+ }
111
+ if (touched) {
112
+ if (Object.keys(patched).length === 0)
113
+ delete doc.pnpm.patchedDependencies;
114
+ if (Object.keys(doc.pnpm).length === 0)
115
+ delete doc.pnpm;
116
+ write(pkgJsonFile, `${JSON.stringify(doc, null, 2)}\n`);
117
+ }
118
+ }
119
+ }
120
+ catch {
121
+ /* package.json 不是合法 JSON:不动 */
122
+ }
123
+ }
124
+ return { changed, orphanedPatchFiles };
125
+ }
13
126
  const TARGET_RE = /^[A-Za-z0-9@:./_#+-]+$/;
14
127
  const NDJSON_COMMANDS = new Set(['add', 'remove', 'install']);
15
128
  export const BOOT_ID = `${String(process.pid)}-${String(Date.now())}`;
@@ -163,6 +276,9 @@ function writeDangerouslyAllowAllBuilds(profileDirectory) {
163
276
  }
164
277
  export function rewritePnpmError(err) {
165
278
  const text = err instanceof Error ? err.message : String(err);
279
+ if (/ERR_PNPM_UNUSED_PATCH/.test(text)) {
280
+ return new Error('profile 的补丁配置(patchedDependencies)里存在不再使用的条目,pnpm 拒绝执行。卸载时 dsh-m 会自动摘除目标包自己的补丁条目;仍报此错通常是其他包留有失效补丁,请手工清理 profile 的 pnpm-workspace.yaml。');
281
+ }
166
282
  if (isPrepareBlocked(text)) {
167
283
  return new Error('该插件需要执行构建脚本(prepare),pnpm 默认拦截。dsh-m 已写入 profile 的 dangerouslyAllowAllBuilds 并重试;若仍失败请检查 web profile 是否可写。');
168
284
  }
@@ -7,7 +7,7 @@
7
7
  import { existsSync } from 'node:fs';
8
8
  import { readFile } from 'node:fs/promises';
9
9
  import { join } from 'node:path';
10
- import { addDshPlugin, removeDshPlugin, runCommand } from './dsh-cli.js';
10
+ import { addDshPlugin, removeDshPlugin, removePatchedDependencyEntries, runCommand } from './dsh-cli.js';
11
11
  import { dshHome, installTimeoutMs, webProfileDir } from './env.js';
12
12
  import { assertNpmIntegrity, readPnpmLockIntegrity, restoreSnapshots, snapshotFiles } from './npm-integrity.js';
13
13
  import { listInstalledPlugins as defaultListInstalledPlugins, readProfileDeps, removeInstalledPlugin, } from './installed.js';
@@ -528,11 +528,14 @@ export async function installEntry(entry, cfg = {}, opts = {}, deps) {
528
528
  }
529
529
  throw new Error(`条目 ${entry.id} 缺少可安装来源`);
530
530
  }
531
- /** 卸载:live-disable → pnpm remove → 报告疑似残留(DESIGN.md §3:删包不删数据)。 */
532
- export async function uninstallPlugin(pkg, _cfg = {}, _opts = {}) {
531
+ /** 卸载:live-disable → 摘除该包补丁条目 → pnpm remove → 报告疑似残留(DESIGN.md §3:删包不删数据)。 */
532
+ export async function uninstallPlugin(pkg, _cfg = {}, _opts = {}, deps = {}) {
533
533
  const liveDisabled = await setLivePluginDisabled(pkg, true);
534
- await removeInstalledPlugin(pkg);
535
- return { pkg, liveDisabled, needsRestart: true, leftovers: leftoverCandidates(pkg) };
534
+ // 依赖移除后残留的 patchedDependencies 条目会让 pnpm 以 ERR_PNPM_UNUSED_PATCH 整单失败,先摘掉
535
+ const patchCleanup = (deps.removePatchedEntries ?? removePatchedDependencyEntries)(webProfileDir(), pkg);
536
+ await (deps.removeInstalled ?? removeInstalledPlugin)(pkg);
537
+ const leftovers = [...new Set([...leftoverCandidates(pkg), ...patchCleanup.orphanedPatchFiles])];
538
+ return { pkg, liveDisabled, needsRestart: true, leftovers };
536
539
  }
537
540
  /** 升级 = 按最新重新安装(npm 拉最新精确版;github 重新锁 HEAD)。 */
538
541
  export async function upgradePlugin(pkg, cfg = {}, opts = {}, deps) {
@@ -479,8 +479,11 @@ function buildState(params) {
479
479
  };
480
480
  }
481
481
  const REPO = 'iasiv5/dsh-m';
482
- // raw 优先:jsDelivr 对 @main 有 CDN 缓存(可 stale 数小时),raw 始终反映 main 最新内容;
483
- // jsDelivr 降为备用线路(可通过 purge.jsdelivr.net 手动清缓存)。
482
+ // 默认源两条网络线路(用户可见名称为「GitHub 镜像(备用)」,无需理解 jsDelivr 是什么):
483
+ // - raw.githubusercontent:GitHub 原始文件,准实时,永远优先;
484
+ // - jsDelivr(cdn.jsdelivr.net,GitHub 内容的免费 CDN 镜像):仅当 raw 拉不到时启用——
485
+ // 覆盖大陆可达性与 GitHub 故障两类场景,代价是 CDN 缓存可能滞后数小时
486
+ // (可用 purge.jsdelivr.net 手动清缓存)。再往下还有本地缓存与包内快照兜底。
484
487
  const DEFAULT_URLS = [
485
488
  { source: 'default-raw', url: `https://raw.githubusercontent.com/${REPO}/main/registry.json` },
486
489
  { source: 'default-jsdelivr', url: `https://cdn.jsdelivr.net/gh/${REPO}@main/registry.json` },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-m",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "DSH Marketplace — 个人自用的 DeepSeek Harness 插件市场:收录、安装、卸载、升级 DSH 插件",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/registry.json CHANGED
@@ -7,9 +7,10 @@
7
7
  "description": "DeepSeek Harness Web 界面主题/皮肤管理插件,可切换多种视觉风格。",
8
8
  "category": "ui",
9
9
  "tags": ["主题", "美化"],
10
- "source": "github",
11
- "github": "iasiv5/skins",
12
- "homepage": "https://github.com/iasiv5/skins"
10
+ "source": "npm",
11
+ "npm": "@iasiv5/dsh-skins",
12
+ "github": "iasiv5/dsh-skins",
13
+ "homepage": "https://github.com/iasiv5/dsh-skins"
13
14
  },
14
15
  {
15
16
  "id": "modsearch",