@1e0zj/dsh-plugin-mall 0.1.9 → 0.1.11

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.md CHANGED
@@ -1,76 +1,123 @@
1
- # dsh-plugin-mall
2
-
3
- **dsh 插件市场** 搜索 GitHub `dsh-plugin` 话题下的 DeepSeek Harness 插件仓库,并一键安装到本地 dsh profile。
4
-
5
- 两个入口:
6
-
7
- 1. **设置插件插件市场 tab**(浏览器 UI,dsh web 模式):搜索、看详情、点安装/卸载、看进度与已装列表
8
- 2. **会话内 5 个 agent 工具**(所有模式可用)
9
-
10
- | 工具 | 作用 |
11
- |---|---|
12
- | `market_search` | 搜索 GitHub 上带 `topic:dsh-plugin` 标签的仓库(按 star 排序,可按关键词过滤) |
13
- | `market_info` | 查看单个仓库详情:star、license、package.json、是否声明 `dsh.bundle.patch` / `dsh.client` |
14
- | `market_install` | 把插件装进某个 profile(后台任务,可用 `job_output` 查看进度) |
15
- | `market_uninstall` | 把插件从某个 profile 卸载(后台任务:`pnpm remove` + 剔除 bundle + 删除客户端加载行) |
16
- | `market_installed` | 列出某 profile 已装的插件及其 bundle 状态 |
17
-
18
- ## 安装
19
-
20
- ```powershell
21
- # 从 npm(发布后)
22
- dsh plugin --profile web add @1e0zj/dsh-plugin-mall
23
-
24
- # 从 GitHub
25
- dsh plugin --profile web add github:1e0zj/dsh-plugin-mall
26
-
27
- # 本地开发(软链,改代码后重启 dsh 即生效)
28
- dsh plugin --profile web add link:C:\path\to\dsh-plugin-mall
29
- ```
30
-
31
- 装完**重启 dsh**(`dsh web` 进程)后生效。
32
-
33
- > Windows 下用 `link:` 开发本插件时,Node 会从项目的真实路径加载模块,
34
- > 因此项目目录里必须先装一次依赖(`npm install`),裸导入才能解析;
35
- > 通过 npm/GitHub 安装时无此要求(pnpm 会把真实拷贝装进 profile 的 node_modules)。
36
-
37
- ## 工作原理
38
-
39
- - 双面包(dual-face)插件:`dsh.bundle` 半边挂在 **host 平面**(profile bundle 层),
40
- 注册 5 agent 工具(进 global 层,所有会话可见,与 MCP 工具同理);
41
- `dsh.client` 半边是浏览器插件,往设置页插件区注册「插件市场」tab
42
- (`settings.plugins.tab` slot;手写无构建,经 `window.__ModuleLoader__` 加载)。
43
- - 浏览器 → 服务端走 Connection 服务的独立 RPC 通道 `/market`
44
- (loopback-only,与 `/api` 通道互不干扰);页面发起的安装任务用进程内
45
- tracker 跟踪 —— web host 层没有 job 控制器,`ctx.jobs` 无法在会话外起任务。
46
- - `market_install` 复刻官方 `dsh plugin add` 的流程:在 profile 目录跑
47
- `pnpm add <spec>`,成功后把声明了 `dsh.bundle.patch` 的依赖登记进
48
- `dsh.profile.bundles`(layer 列表),声明了 `dsh.client` 的依赖自动在
49
- profile 的 `cordis.patch.yml` 注册加载行,与官方 reconcile 逻辑一致。
50
- - `market_uninstall` 复刻官方 `dsh plugin remove` 的流程:在 profile 目录跑
51
- `pnpm remove <package>`,成功后从 `dsh.profile.bundles` 剔除该依赖的
52
- bundle 条目,并删掉 `cordis.patch.yml` 里由安装流程注册的客户端加载行
53
- (文本级精准移除,用户手写的行不受影响)。
54
- - GitHub 源的插件安装时要跑 prepare 构建脚本,pnpm 默认拦截;本插件检测到
55
- 拦截后会把包名自动合并进 profile `pnpm-workspace.yaml` 的 `allowBuilds`
56
- 并自动重试一次。
57
- - 配置(`cordis.patch.yml` 中可改):`defaultProfile`(默认装进哪个 profile,
58
- 默认 `web`)、`apiBase`(GitHub API 地址)、`perPageMax`(搜索单页上限)。
59
-
60
- ## 发布
61
-
62
- ```bash
63
- npm publish
64
- # 或在 GitHub 建仓库并打上 topic:dsh-plugin
65
- ```
66
-
67
- ## 开发说明
68
-
69
- - 插件形态:`package.json` 里 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`,
70
- patch 文件把 `dsh-plugin-mall` 这一行 insert 进装配树;**同一行同时是
71
- client 插件行**(`dsh.client` 声明让 client-modules 扫描并服务
72
- `/plugins/<id>/client.js`)。
73
- - node 半边导出具名成员 `{ name, inject, Config, apply }`,**不要** `export default`
74
- (cordis loader 会做 `exports.default ?? exports` 解包,default 会吞掉 inject/Config);
75
- client 半边是 `window.__ModuleLoader__.load({id, factory})`,导出 `{ apply, inject }`。
76
- - 单测 `src/github.js`(无 harness 依赖):`node src/github.js --self-test`。
1
+ # dsh-plugin-mall
2
+
3
+ **An open plugin marketplace for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh): search every GitHub repo tagged `topic:dsh-plugin`, automatically verify which ones are real dsh plugins, install and update with one click.**
4
+
5
+ [中文说明](#中文说明) · [Install](#install) · [Why another marketplace](#why-another-marketplace)
6
+
7
+ Two surfaces: a **Settings PluginsMarketplace** tab in the dsh web UI, and five agent tools usable from any session.
8
+
9
+ ## Why another marketplace?
10
+
11
+ Curated lists only show what has been reviewed and merged. This marketplace is open by construction: **any repo tagged `topic:dsh-plugin` is discoverable the moment it is pushed** — no submission, no approval queue. To keep that openness usable:
12
+
13
+ - **Automatic verification** — every search result's `package.json` is fetched (jsDelivr/raw dual-source CDN, no API quota) and checked for the official `dsh.bundle` / `dsh.client` manifest. Verified plugins get a green badge; the default "verified only" view filters out ~73% of topic noise (empty repos and unrelated projects riding the tag).
14
+ - **Anti-squatting** — an install prefers the npm tarball only when the registry entry's `repository` URL points back to the same GitHub repo; anything else falls back to the explicit `github:` spec.
15
+ - **npm-first installs** registry tarballs are smaller than whole-repo GitHub downloads and come with integrity checks.
16
+ - **Update management** installed plugins are compared against the registry `latest`; one-click update per plugin.
17
+ - **Resilience** — rate-limit circuit breaker, GitHub's 1000-result search window handled gracefully, `corepack enable pnpm` self-heal when pnpm is missing, one-click dsh restart (loopback-only, `allowRestart: false` to disable).
18
+
19
+ ## Install
20
+
21
+ ```powershell
22
+ # from npm
23
+ dsh plugin --profile web add @1e0zj/dsh-plugin-mall
24
+
25
+ # from GitHub
26
+ dsh plugin --profile web add github:1e0zj/dsh-plugin-mall
27
+
28
+ # local development (symlink; restart dsh after edits)
29
+ dsh plugin --profile web add link:C:\path\to\dsh-plugin-mall
30
+ ```
31
+
32
+ Restart dsh after installing.
33
+
34
+ ## Agent tools
35
+
36
+ | Tool | What it does |
37
+ |---|---|
38
+ | `market_search` | Search GitHub repos tagged `topic:dsh-plugin` (star-ranked, keyword filter, server-side `stars:>=1` noise floor) |
39
+ | `market_info` | Inspect one repo: stars, license, package.json, whether it declares `dsh.bundle.patch` / `dsh.client` |
40
+ | `market_install` | Install a plugin into a profile as a background job (npm-first spec resolution) |
41
+ | `market_uninstall` | Remove a plugin: `pnpm remove` + bundle-layer reconcile + client-row cleanup |
42
+ | `market_installed` | List a profile's installed plugins and their bundle status |
43
+
44
+ ---
45
+
46
+ # 中文说明
47
+
48
+ **dsh 插件市场** — 搜索 GitHub `dsh-plugin` 话题下的 DeepSeek Harness 插件仓库,自动验证哪些是真 dsh 插件,一键安装与更新。
49
+
50
+ 与策展列表不同:**任何打上 `topic:dsh-plugin` 的仓库推送后立即可被发现**——无需投稿、无需审批。为保证开放性可用,做了这些事:
51
+
52
+ - **自动验证**:逐仓库拉取 `package.json`(jsDelivr/raw 双源 CDN,不占 API 配额),按官方 `dsh.bundle` / `dsh.client` 声明打徽章;默认"只看已验证"视图过滤约 73% 的话题噪音
53
+ - **防抢注**:仅当 npm registry 条目的 `repository` 指回同一 GitHub 仓库时才用 npm 安装,否则回退 `github:` 源
54
+ - **npm 优先安装**:registry tarball 比整仓库下载更小且带完整性校验
55
+ - **更新管理**:已装插件与 registry `latest` 比对,逐个一键更新
56
+ - **工程韧性**:限流熔断、GitHub 1000 条搜索上限优雅处理、pnpm 缺失时 `corepack` 自愈、一键重启 dsh(仅 loopback,可 `allowRestart: false` 关闭)
57
+
58
+ ## 安装
59
+
60
+ ```powershell
61
+ # 从 npm
62
+ dsh plugin --profile web add @1e0zj/dsh-plugin-mall
63
+
64
+ # GitHub
65
+ dsh plugin --profile web add github:1e0zj/dsh-plugin-mall
66
+
67
+ # 本地开发(软链,改代码后重启 dsh 即生效)
68
+ dsh plugin --profile web add link:C:\path\to\dsh-plugin-mall
69
+ ```
70
+
71
+ 装完**重启 dsh**(`dsh web` 进程)后生效。
72
+
73
+ > Windows 下用 `link:` 开发本插件时,Node 会从项目的真实路径加载模块,
74
+ > 因此项目目录里必须先装一次依赖(`npm install`),裸导入才能解析;
75
+ > 通过 npm/GitHub 安装时无此要求(pnpm 会把真实拷贝装进 profile node_modules)。
76
+
77
+ ## 工作原理
78
+
79
+ - 双面包(dual-face)插件:`dsh.bundle` 半边挂在 **host 平面**(profile bundle 层),
80
+ 注册 5 个 agent 工具(进 global 层,所有会话可见,与 MCP 工具同理);
81
+ `dsh.client` 半边是浏览器插件,往设置页插件区注册「插件市场」tab
82
+ (`settings.plugins.tab` slot;手写无构建,经 `window.__ModuleLoader__` 加载)。
83
+ - 浏览器 → 服务端走 Connection 服务的独立 RPC 通道 `/market`
84
+ (loopback-only,与 `/api` 通道互不干扰);页面发起的安装任务用进程内
85
+ tracker 跟踪 —— web host 层没有 job 控制器,`ctx.jobs` 无法在会话外起任务。
86
+ - `market_install` 复刻官方 `dsh plugin add` 的流程:在 profile 目录跑
87
+ `pnpm add <spec>`,成功后把声明了 `dsh.bundle.patch` 的依赖登记进
88
+ `dsh.profile.bundles`(layer 列表),声明了 `dsh.client` 的依赖自动在
89
+ profile 的 `cordis.patch.yml` 注册加载行,与官方 reconcile 逻辑一致。
90
+ - `market_uninstall` 复刻官方 `dsh plugin remove` 的流程:在 profile 目录跑
91
+ `pnpm remove <package>`,成功后从 `dsh.profile.bundles` 剔除该依赖的
92
+ bundle 条目,并删掉 `cordis.patch.yml` 里由安装流程注册的客户端加载行
93
+ (文本级精准移除,用户手写的行不受影响)。
94
+ - 安装源解析:`github:owner/repo` 优先改写为同名 npm 包(仅当 registry
95
+ 条目的 repository 指回该仓库,防止抢注),否则用 GitHub 全仓库 spec。
96
+ - GitHub 源的插件安装时要跑 prepare 构建脚本,pnpm 默认拦截;本插件检测到
97
+ 拦截后会把包名自动合并进 profile 的 `pnpm-workspace.yaml` 的 `allowBuilds`
98
+ 并自动重试一次(解析只认合法 npm 包名,写入一律加引号,不会写坏 YAML)。
99
+ - 配置(`cordis.patch.yml` 中可改):`defaultProfile`(默认装进哪个 profile,
100
+ 默认 `web`)、`apiBase`(GitHub API 地址)、`perPageMax`(搜索单页上限)、
101
+ `allowRestart`(是否允许一键重启,默认 `true`)。
102
+
103
+ ## 发布
104
+
105
+ ```bash
106
+ npm publish --access public
107
+ # 或在 GitHub 建仓库并打上 topic:dsh-plugin
108
+ ```
109
+
110
+ ## 开发说明
111
+
112
+ - 插件形态:`package.json` 里 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`,
113
+ patch 文件把 `dsh-plugin-mall` 这一行 insert 进装配树;**同一行同时是
114
+ client 插件行**(`dsh.client` 声明让 client-modules 扫描并服务
115
+ `/plugins/<id>/client.js`)。
116
+ - **`@deepseek-ai/*` 框架包必须声明为 `peerDependencies`**:装成 dependencies
117
+ 会把宿主模块副本 hoist 进 profile,cordis loader 双副本加载、Symbol 身份
118
+ 分裂,宿主的工具调度全线崩溃。宿主经 `profiles/node_modules` fallback
119
+ 提供框架包。
120
+ - node 半边导出具名成员 `{ name, inject, Config, apply }`,**不要** `export default`
121
+ (cordis loader 会做 `exports.default ?? exports` 解包,default 会吞掉 inject/Config);
122
+ client 半边是 `window.__ModuleLoader__.load({id, factory})`,导出 `{ apply, inject }`。
123
+ - 单测 `src/github.js`(无 harness 依赖):`node src/github.js --self-test`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1e0zj/dsh-plugin-mall",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "dsh 插件市场:搜索 GitHub dsh-plugin 话题下的插件仓库,一键安装到本地 dsh profile(agent 工具 + 设置页插件市场 tab)",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/client.js CHANGED
@@ -43,10 +43,13 @@ window.__ModuleLoader__.load({
43
43
  ".mkt_cardActions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:auto;padding-top:4px}",
44
44
  ".mkt_cardActions .mkt_btn{text-decoration:none}",
45
45
  ".mkt_panelTitle{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin:0}",
46
+ ".mkt_panelRow{display:flex;align-items:center;gap:8px}",
47
+ ".mkt_panelRow .mkt_link{margin-left:auto}",
46
48
  ".mkt_pre{font-family:Consolas,Monaco,monospace;font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-secondary,#f6f7f8);border-radius:6px;padding:8px;max-height:220px;overflow:auto;white-space:pre-wrap;word-break:break-all}",
47
49
  ".mkt_ok{color:var(--dsw-alias-state-success-primary,#2f855a)}",
48
50
  ".mkt_badge{display:inline-block;border-radius:999px;padding:1px 8px;font-size:11px;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary)}",
49
51
  ".mkt_badgeOk{border-color:var(--dsw-alias-state-success-primary,#2f855a);color:var(--dsw-alias-state-success-primary,#2f855a)}",
52
+ ".mkt_badgeBad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}",
50
53
  ".mkt_check{display:flex;align-items:center;gap:4px;font-size:12.5px;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap}",
51
54
  ".mkt_link{color:var(--dsw-alias-state-business-primary);font-size:12px;text-decoration:none;cursor:pointer}",
52
55
  ".mkt_installedHead{display:flex;align-items:center;gap:8px;width:100%;background:none;border:0;padding:0;margin:0;cursor:pointer;font:inherit;text-align:left}",
@@ -127,7 +130,11 @@ window.__ModuleLoader__.load({
127
130
  jobsRef.current = Object.assign({}, jobsRef.current, { [id]: { status: "running", spec: spec, output: "" } });
128
131
  setJobs(Object.assign({}, jobsRef.current));
129
132
  }, []);
130
- return { jobs: jobs, track: track };
133
+ var clear = useCallback(function () {
134
+ jobsRef.current = {};
135
+ setJobs({});
136
+ }, []);
137
+ return { jobs: jobs, track: track, clear: clear };
131
138
  }
132
139
 
133
140
  // ── plugin verification badge ───────────────────────────────────────────
@@ -135,6 +142,9 @@ window.__ModuleLoader__.load({
135
142
  // dsh.bundle/dsh.client 声明,进程内缓存)。unknown 不显示徽章。
136
143
  function verifyBadge(verified) {
137
144
  if (verified === undefined || verified === null) return null;
145
+ if (verified.hostDeps !== undefined && verified.hostDeps.length > 0) {
146
+ return h("span", { className: "mkt_badge mkt_badgeBad", title: verified.hostDeps.join(", ") }, "宿主依赖风险");
147
+ }
138
148
  if (verified.kind === "bundle") return h("span", { className: "mkt_badge mkt_badgeOk" }, "宿主插件");
139
149
  if (verified.kind === "client") return h("span", { className: "mkt_badge mkt_badgeOk" }, "UI插件");
140
150
  if (verified.kind === "plain") return h("span", { className: "mkt_badge" }, "未声明");
@@ -147,8 +157,8 @@ window.__ModuleLoader__.load({
147
157
  var item = props.item;
148
158
  var installing = props.installing === true;
149
159
  var job = props.installJob;
150
- var installLabel = "安装";
151
- var installDisabled = installing;
160
+ var installLabel = props.alreadyInstalled === true ? "已装" : "安装";
161
+ var installDisabled = installing || props.alreadyInstalled === true;
152
162
  if (!installing && job) {
153
163
  if (job.status === "running") { installLabel = "安装中…"; installDisabled = true; }
154
164
  else if (job.status === "completed") { installLabel = "已装 · 重启生效"; installDisabled = true; }
@@ -181,7 +191,10 @@ window.__ModuleLoader__.load({
181
191
  var ids = Object.keys(props.jobs);
182
192
  if (ids.length === 0) return null;
183
193
  return h("div", { className: "mkt_card" },
184
- h("p", { className: "mkt_panelTitle" }, "任务"),
194
+ h("div", { className: "mkt_panelRow" },
195
+ h("p", { className: "mkt_panelTitle" }, "任务"),
196
+ props.onClear ? h("span", { className: "mkt_link", onClick: props.onClear }, "清空") : null
197
+ ),
185
198
  ids.map(function (id) {
186
199
  var job = props.jobs[id];
187
200
  var done = job.status === "completed" || job.status === "failed" || job.status === "killed";
@@ -393,6 +406,7 @@ window.__ModuleLoader__.load({
393
406
  var polling = useJobPolling(call, refreshInstalled);
394
407
  var track = polling.track;
395
408
  var jobs = polling.jobs;
409
+ var clearJobs = polling.clear;
396
410
 
397
411
  useEffect(function () {
398
412
  doSearch();
@@ -472,16 +486,15 @@ window.__ModuleLoader__.load({
472
486
  refreshInstalled();
473
487
  }, [refreshInstalled]);
474
488
 
475
- // 活动任务置顶(紧跟已装面板,点安装立刻看到进度);全部静止后归档到底部当日志。
476
- var jobsActive = false;
477
- for (var jid in jobs) { if (jobs[jid] && jobs[jid].status === "running") { jobsActive = true; break; } }
478
-
489
+ // 任务面板位置固定(已装面板之后):不随任务状态在顶部/底部之间
490
+ // 跳——npm 安装几秒即完成,"完成即落底"曾让结果凭空消失。
479
491
  // "只看已验证"开关下的可见项:verify 判定 bundle/client 的才算插件。
480
492
  // verifyPending:当前加载的仓库里还有未验证完的(verified map 尚未覆盖)。
481
493
  var visibleItems = results === null ? [] : results.items.filter(function (it) {
482
494
  if (verifiedOnly !== true) return true;
483
495
  var v = verified[it.fullName];
484
- return v !== undefined && (v.kind === "bundle" || v.kind === "client");
496
+ return v !== undefined && (v.kind === "bundle" || v.kind === "client")
497
+ && !(v.hostDeps !== undefined && v.hostDeps.length > 0);
485
498
  });
486
499
  var verifyPending = results !== null && results.items.some(function (it) { return verified[it.fullName] === undefined; });
487
500
 
@@ -516,7 +529,7 @@ window.__ModuleLoader__.load({
516
529
  ),
517
530
  error ? h("div", { className: "mkt_error" }, error) : null,
518
531
  h(InstalledPanel, { installed: installed, removing: removing, updates: updates, onUninstall: doUninstall, onInstallSpec: doInstallSpec }),
519
- jobsActive ? h(JobsPanel, { jobs: jobs }) : null,
532
+ h(JobsPanel, { jobs: jobs, onClear: clearJobs }),
520
533
  h("div", { className: "mkt_list" },
521
534
  results == null
522
535
  ? h("div", { className: "mkt_meta mkt_listHead" }, loading ? "正在加载最热插件…" : "—")
@@ -534,12 +547,17 @@ window.__ModuleLoader__.load({
534
547
  for (var jobId in jobs) {
535
548
  if (jobs[jobId] && jobs[jobId].spec === "github:" + item.fullName) { installJob = jobs[jobId]; break; }
536
549
  }
550
+ var vInfo = verified[item.fullName];
551
+ var alreadyInstalled = vInfo !== undefined && typeof vInfo.name === "string"
552
+ && installed !== null && installed.error === undefined
553
+ && (installed.deps || []).some(function (dep) { return dep.name === vInfo.name; });
537
554
  return h(RepoCard, {
538
555
  key: item.fullName,
539
556
  item: item,
540
557
  installing: installing[item.fullName] === true || installing["github:" + item.fullName] === true,
541
558
  installJob: installJob,
542
- verified: verified[item.fullName],
559
+ verified: vInfo,
560
+ alreadyInstalled: alreadyInstalled,
543
561
  onInstall: doInstall,
544
562
  });
545
563
  }),
@@ -547,8 +565,7 @@ window.__ModuleLoader__.load({
547
565
  ? h("div", { className: "mkt_loadMore", ref: sentinelRef }, loadingMore ? "加载中…" : Date.now() < retryAt ? "GitHub 限流中,稍后再下滑加载" : "下滑加载更多")
548
566
  : h("div", { className: "mkt_loadMore" }, reachedLimit ? "已达 GitHub 搜索上限(前 1000 个结果)" : "已显示全部 " + results.items.length + " 个")
549
567
  )
550
- ),
551
- jobsActive ? null : h(JobsPanel, { jobs: jobs })
568
+ )
552
569
  );
553
570
  }
554
571
 
package/src/github.js CHANGED
@@ -137,7 +137,7 @@ export async function npmPackageInfo(name) {
137
137
  if (typeof latest === "string") {
138
138
  const rawRepository = body?.repository;
139
139
  const repositoryUrl = typeof rawRepository === "string" ? rawRepository : rawRepository?.url;
140
- info = { latest, repositoryUrl: typeof repositoryUrl === "string" ? repositoryUrl : undefined };
140
+ info = { latest, repositoryUrl: typeof repositoryUrl === "string" ? repositoryUrl : undefined, hostDeps: hostShadowDependencies(body) };
141
141
  }
142
142
  }
143
143
  } catch {
@@ -166,6 +166,32 @@ export async function preferNpmSpec({ spec }) {
166
166
  return pointsBack ? declaredName : raw;
167
167
  }
168
168
 
169
+ /**
170
+ * Refuse to install a package whose `dependencies` shadow host framework
171
+ * packages. Host copies inside a profile split module identities and crash
172
+ * all tool scheduling — the dsh contract is peerDependencies, but the host
173
+ * enforces nothing, so the marketplace is the last line of defense.
174
+ * Sources: registry abbreviated metadata for npm names, the verify cache
175
+ * (raw package.json) for github: specs.
176
+ */
177
+ export async function assertSafeToInstall({ spec }) {
178
+ const raw = String(spec ?? "");
179
+ let hostDeps;
180
+ if (/^(?:@[\w.-]+\/)?[\w.-]+$/.test(raw)) {
181
+ const info = await npmPackageInfo(raw);
182
+ hostDeps = info?.hostDeps;
183
+ } else {
184
+ const match = /^github:([^/\s]+\/[^/\s]+?)(?:\.git)?$/i.exec(raw);
185
+ if (match !== null) {
186
+ const { results } = await verifyPlugins({ repos: [match[1]] });
187
+ hostDeps = results[match[1]]?.hostDeps;
188
+ }
189
+ }
190
+ if (hostDeps !== undefined && hostDeps.length > 0) {
191
+ throw new Error(`${raw} declares ${hostDeps.length} host framework package(s) as dependencies (${hostDeps.slice(0, 3).join(", ")}${hostDeps.length > 3 ? ", …" : ""}) — installing it would duplicate host modules and crash dsh tool scheduling. Ask the plugin author to move @deepseek-ai/* to peerDependencies.`);
192
+ }
193
+ }
194
+
169
195
  /** Loose semver-ish comparison: "0.2.10" vs "0.2.9" → 1. Non-numeric parts read as 0. */
170
196
  export function compareVersions(a, b) {
171
197
  const pa = String(a ?? "").split(".");
@@ -222,10 +248,27 @@ async function fetchRawPackageJson(repo, signal) {
222
248
  throw lastError ?? new Error("no raw source reachable");
223
249
  }
224
250
 
251
+ /**
252
+ * Framework packages the host provides via the profiles/node_modules fallback.
253
+ * A plugin declaring any of these as `dependencies` (instead of
254
+ * peerDependencies) installs real copies into the profile — the loader then
255
+ * holds two module instances, symbol identities split, and every tool call
256
+ * crashes with an undiagnosable "reading 'prepare'". See installer docs.
257
+ */
258
+ const HOST_PACKAGES = /^(@deepseek-ai\/|cosmokit$|schemastery$)/;
259
+
260
+ /** The @deepseek-ai/* (and cordis runtime) entries inside `dependencies`. */
261
+ function hostShadowDependencies(pkg) {
262
+ if (pkg === undefined || typeof pkg !== "object") return undefined;
263
+ const deps = Object.keys(pkg.dependencies ?? {});
264
+ const host = deps.filter((name) => HOST_PACKAGES.test(name));
265
+ return host.length > 0 ? host : undefined;
266
+ }
267
+
225
268
  /**
226
269
  * Verify repositories as real dsh plugins by their package.json declaration.
227
270
  * @param {{repos: string[], signal?: AbortSignal}} options - "owner/name" list.
228
- * @returns the {results} map: fullName -> {kind: "bundle"|"client"|"plain"|"no-manifest"|"unknown", name?, version?}.
271
+ * @returns the {results} map: fullName -> {kind: "bundle"|"client"|"plain"|"no-manifest"|"unknown", name?, version?, hostDeps?}.
229
272
  */
230
273
  export async function verifyPlugins({ repos, signal }) {
231
274
  const wanted = [...new Set((Array.isArray(repos) ? repos : []).map(String)
@@ -241,7 +284,7 @@ export async function verifyPlugins({ repos, signal }) {
241
284
  : typeof pkg.dsh?.bundle?.patch === "string" ? "bundle"
242
285
  : pkg.dsh?.client !== undefined ? "client"
243
286
  : "plain";
244
- verifyCache.set(repo, { kind, name: pkg?.name, version: pkg?.version });
287
+ verifyCache.set(repo, { kind, name: pkg?.name, version: pkg?.version, hostDeps: hostShadowDependencies(pkg) });
245
288
  } catch (error) {
246
289
  if (error?.name === "AbortError") throw error;
247
290
  verifyCache.set(repo, { kind: "unknown" });
package/src/index.js CHANGED
@@ -19,7 +19,7 @@ import { existsSync, readFileSync } from "node:fs";
19
19
  import { join } from "node:path";
20
20
  import { spawn } from "node:child_process";
21
21
  import { resolveProfileDir } from "@deepseek-ai/dsh-app-boot";
22
- import { repoInfo, searchPlugins, verifyPlugins, preferNpmSpec, npmPackageInfo, compareVersions } from "./github.js";
22
+ import { repoInfo, searchPlugins, verifyPlugins, preferNpmSpec, npmPackageInfo, compareVersions, assertSafeToInstall } from "./github.js";
23
23
  import { ensureProfile, listInstalled, normalizeSpec, runInstall, runRemove, createJobTracker } from "./installer.js";
24
24
 
25
25
  export const name = "@1e0zj/dsh-plugin-mall";
@@ -212,6 +212,13 @@ async function rpcDispatch(ctx, endpoint, payload, config, token, tracker) {
212
212
  // npm tarball 优先(小而快、带 integrity);registry 条目不同源的包名
213
213
  // 视为抢注,回退 github: 全仓库 spec。
214
214
  spec = await preferNpmSpec({ spec });
215
+ // 宿主依赖硬拦:dependencies 里拖着 @deepseek-ai/* 的包装进 profile
216
+ // 就是双模块实例 + 全工具调度崩溃(宿主无任何护栏,市场是最后防线)。
217
+ try {
218
+ await assertSafeToInstall({ spec });
219
+ } catch (error) {
220
+ return rpcFail(error);
221
+ }
215
222
  try {
216
223
  const profileDir = resolveProfileDir(profile);
217
224
  if (!existsSync(join(profileDir, "package.json"))) ensureProfile(profile);
@@ -419,6 +426,7 @@ export function apply(ctx, config = {}) {
419
426
  async execute(args, exec) {
420
427
  const profile = String(args.profile ?? defaultProfile).trim();
421
428
  const spec = await preferNpmSpec({ spec: normalizeSpec(args.spec) });
429
+ await assertSafeToInstall({ spec });
422
430
  let profileDir;
423
431
  try {
424
432
  profileDir = resolveProfileDir(profile);