DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

lpf0404 /

lpf0404/dsh-compat-market

Verified

This plugin has no description yet.

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@d948325e

dsh-compat-market

为 DeepSeek Harness(DSH)桌面端打造的插件市场:在侧栏浏览官方与社区插件,装之前就看到兼容性结论,一键交给官方安装管线安装。


为什么再造一个市场

DSH 官方没有插件市场,但已经有一整套插件管理能力(@deepseek-ai/dsh-plugin-manager 服务 + 侧栏「插件」面板)。官方面板自己写明了一条限制:

「添加插件」接受包名(可带版本)、Git 地址、压缩包或本地绝对路径 —— 包名就是 README 里 dsh plugin add 后面的那一段

也就是说:官方面板只会「管理」,不会「发现」 —— 你得先知道包名才能装。而社区已有的几个市场全都绕开了官方安装管线,自己 clone 仓库、自己写 cordis.patch.yml。

本项目补的正是这条缝,并且只补这条缝:

能力 官方面板 其它社区市场 本插件
发现社区插件 ✗ 需先知道包名 ✓ ✓
走官方安装管线(注册表回退 / 失败回滚 / 构建脚本授权) ✓ ✗ 自行 clone ✓
装之前给出兼容性结论 ✗ 装完才报 incompatible-version ✗ 不检查 ✓
把社区信息注入原生插件详情页 — ✗ ✓
官方与社区同屏 只看随装的官方包 只看社区 ✓

它不做什么

不自建安装管线。安装、启用、卸载全部委托给 pluginManager,因此自动获得注册表按序回退、失败时回滚 package.json 与 pnpm-lock.yaml、pnpm 构建脚本授权,以及与 DSH 完全一致的兼容性预检。


安装

在 DSH 的「插件」面板里添加,或直接用 plugin_manager 工具:

plugin_manager { action: "install_bundle", target: "dsh-compat-market" }

安装会走官方管线:注册表按序回退、失败时回滚 package.json 与 pnpm-lock.yaml、需要构建脚本时向你请示。

安装后即时生效,无需重启 —— 当前 profile 的 hmr 行处于活动状态,官方管理器返回 application: "applied"。

从本地目录安装(开发用)

把 spec 传成本包的绝对路径:

plugin_manager { action: "install_bundle", target: "D:\\path\\to\\dsh-compat-market" }

本地路径会被装成 link: 依赖(Windows 上是 junction),因此改动本目录即刻生效。

注意(踩过的坑):pnpm 在 Windows 上建立的 node_modules 链接用的是绝对路径。所以移动或重命名这个检出目录之后,链接会全部悬空,插件会以一个非常费解的错误加载失败:ERR_MODULE_NOT_FOUND: Cannot find package 'semver' —— 看起来像依赖没装,实际上是路径变了。修复只需在这个目录里重跑一次 pnpm install(会从本地 store 复用,约 1 秒)。

顺带一条:清理 node_modules 时不要直接 Remove-Item -Recurse。pnpm 的 store 位于 D:\.pnpm-store,而树里有上千个指向它的硬链接和 151 个 junction,PowerShell 可能顺着 junction 递归进去。先逐个 cmd /c rmdir 摘掉 junction 再删,才是安全的。

边界的诚实说明:浏览器半边(lib/client.js)由 dsh-client-hmr 每 500 ms 轮询,改动会热替换,连页面刷新都不需要。Host 半边(lib/index.js)不会热重载(当前 profile 的 hmr.config.root: []),改动需重启 DSH 才生效。


兼容性门禁(核心价值)

市场在点击安装之前就给出结论。判据是 DSH 自己的预检函数 evaluatePluginCompatibility(@deepseek-ai/dsh-app-boot)的忠实移植:

  • 只检查名为 @deepseek-ai/dsh 或以 @deepseek-ai/dsh- 开头的 peer;@deepseek-ai/cordis 从不检查
  • workspace:^ / workspace:~ / workspace:* 视为「当前运行时」,恒满足
  • 空 range 视为不兼容
  • 其余用 semver.satisfies(runtime, range, { includePrerelease: true })

必须用真正的 semver 语义,不能用简化匹配:生态里出现过 278 种不同的 range 写法,包括 || 链和 >=x <y 复合区间。

一个容易反直觉的点:<0.2.0 这个上界会匹配 0.2.0-rc.2 —— 因为预发布版本排序在正式版之前,而 includePrerelease 让这个比较成立。

实测数据(运行时 0.2.0-rc.2):

指标 数量
检索到的候选包 1 571
验证为真实 DSH 插件 1 224
社区插件 1 140
官方包 84
可作为 bundle 安装 1 136
与当前运行时不兼容 218
仅客户端(官方管线会以 not-bundle 拒绝) 88

约 19% 的社区插件在当前版本上会被 DSH 拒绝。官方面板只在你点下安装之后才告诉你,本插件在你点之前就标出来,并逐条列出是哪个 peer 的哪个 range 拦住了。


架构

dsh-compat-market
├── cordis.patch.yml      # 一行 insert;host 与 client 由这一行共同装配
├── package.json          # dsh.bundle.patch + dsh.client(platform:web)
└── lib
    ├── index.js          # Host 半边:Cordis 插件(name / inject / apply)
    ├── client.js         # Browser 半边:classic script + __ModuleLoader__.load
    └── host
        ├── catalog.js    # 发现与验证(多 seed 检索 + manifest 校验)
        ├── compat.js     # 兼容性门禁(DSH 预检的移植)
        ├── market.js     # 服务核心:分页 / 筛选 / 排序 / 安装委托
        ├── registry.js   # 注册表可达性探测
        ├── route.js      # 只读 HTTP 接口
        ├── cache.js      # 快照 TTL + manifest 缓存
        └── store.js      # profile 内的原子 JSON 存储

目录数据源

纯自建聚合,不依赖任何第三方目录站(dsh-plugin.org 与 dshbase.com 都没有公开 JSON API,只能抓 HTML)。有两个来源,主次分明。

主来源:npm 注册表

  1. 对 dsh-plugin / deepseek-harness / dsh 三个种子做全文检索 (npmmirror 的检索不支持 npm 的 keywords: / scope: 限定语法,两者都返回 total=0)
  2. 按包名模式粗筛后,逐个读取 /<pkg>/latest,以 manifest 是否声明 dsh 字段为权威判据
    • dsh.bundle.patch → 可安装的 bundle 层
    • dsh.client → 带浏览器半边
    • 官方核心包(dsh-base、dsh-plugin-manager、dsh-tool-todo…)没有 dsh 字段 —— 它们是 bundle patch 声明的行,不是可安装单元,因此不会被当作安装候选
  3. 热门度用 npm 下载量(检索响应里自带 downloads),而不是 GitHub star

次来源:GitHub topic(滴灌)

注册表看不见从未发布过的插件,而这个缺口可以量化:topic:dsh-plugin 里最近更新的 100 个仓库,抽样 14 个有 10 个是带 dsh.bundle.patch 的真插件,注册表一个都不知道。复跑 node tools/github-delta.mjs 可以重新检验这个判断,而不是凭它当真。

只看 updated,不看 star。 实测按 star 取前 100 名,抽样 12 个没有一个是插件 —— 前排全是误挂该 topic 的无关知名项目(PicGo、NocoBase、Tencent/WeKnora),真插件几乎都停在 ★0–2。按 star 扫等于花配额进口噪音。

滴灌而非全扫,因为一个仓库不等于一个可安装单元:可安装单元是 package.json 里 dsh.bundle.patch 声明的 bundle,所以每个候选都要花一次 API 请求才能判断它到底是不是插件。未认证配额只有 60 次/小时,而搜索 API 单次查询最多返回 1000 条、该 topic 有 1.7 万+ 仓库 —— 全扫在数学上不可能。于是:

参数 值 理由
单轮分类预算 40 次请求(启用 token 后 250) 配额与机器上其他工具共用,必须留余量
提前停止 剩余 ≤ 5 即停(启用 token 后 ≤ 200) 宁可少扫,也不把共用配额抽干
每仓库复核周期 24h 付过一次就不再重复付
发现窗口 搜索 API 前 10 页(≤1000 条) API 硬上限
每轮取页 游标 1→10 循环 第 1 页最新,循环才能发现重新活跃的仓库
凭据 默认无(可选开启,见下) 插件默认不存在任何秘密;代价就是"滴灌"

代价说清楚:首轮只覆盖几十个仓库,之后靠累计,几天才接近完整。这是刻意的取舍 —— 默认不引入凭据比"立刻扫完"更重要。未配置 token 时该模块从不发 Authorization 头,这一点由单测与实况校验双重断言。

这个来源可以在界面里关掉,也可以不动代码用环境变量关掉。 侧栏「插件市场」→ 右上角「设置」→ 把「来源开关」切到「已关闭」即可:不会发出任何请求,/health 会报 github.disabled("关掉"与"失败"在响应里是两个不同的值,不会混淆)。环境变量 DSH_MARKET_GITHUB_TOPIC 优先级更高:设为 off 即完全停用,设为别的值则改扫另一个 topic,不设则用内置的 dsh-plugin(此时以界面开关为准)。关掉之后 npm 目录照常工作 —— 主来源从来不依赖它。所以"要不要保留 GitHub 扫描"是一个可以在运行时反悔的决定。

可选:启用 GitHub token(默认关闭)

滴灌之所以慢,唯一的原因是没有凭据。GitHub 未认证配额是 60 次/小时,带 token 是 5000 次/小时 —— 83 倍。所以这是一个用速度换"更少的秘密"的取舍,而取舍权在你。

方式一:在界面里填(不用重启)。侧栏「插件市场」→ 右上角「设置」→ 在「GitHub token(可选)」里粘贴 token → 「保存」。保存后输入框会被清空 —— 服务端没有回传明文的接口(/settings 只回答"有没有配",不回答"是什么"),所以界面永远不回显已保存的值。想撤销就点「清除」。

方式二:用环境变量(优先级更高;适合 CI,或你要求"绝不落盘"):

# 启用(显式指名给本插件的 token)
DSH_MARKET_GITHUB_TOKEN=<your-token>

# 关闭 / 恢复匿名(默认就是这个状态)
DSH_MARKET_GITHUB_TOKEN=off

⚠️ 开启意味着什么,请先读完再决定

  • 这个 token 会被发送给 GitHub,每个请求都带 Authorization: Bearer <token>,GitHub 端能看到调用者是这个 token 对应的账号。
  • 消耗的是这个 token 的配额,不是 token 本身。 token 不会被"用掉",被消耗的是它名下每小时 5000 次的调用额度;额度用尽后该 token 的所有调用(包括你自己在用的其他工具、CI)都会一起被限流到下一个整点。这也是为什么启用后仍保留 200 次的余量:这份配额很可能不只你在用。
  • 额度是按 token 计的,不随插件卸载而恢复;卸载插件不会撤销这个 token。
  • 在界面里保存的 token 会以明文落盘。 位置是 <profile>/.plugin-market/settings.json(DSH_HOME 下;Windows 上通常是 C:\Users\<你>\.dsh\profiles\desktop\.plugin-market\settings.json),写入时请求权限 0600。在 Windows 上 0600 基本是装饰性的 —— NTFS 仍按父目录继承 ACL,同机其他账号、以及任何以你身份运行的程序都能读到它。要"绝不落盘",就用方式二的环境变量,并点「清除」确保文件里不留副本。这是本次新增界面开关带来的真实代价,不是可以忽略的细节。

开启后自动生效的变化,不需要改任何配置:单轮预算 40 → 250,余量 5 → 200,扫描进度按配额自动推进,窗口 1000 条约 4 次重建走完(匿名模式约 6 天)。面板顶部会显示一行提示「已启用 GitHub token:请求会消耗该 token 的配额」——只要它在发 token,就不会让你看不见。

安全边界(都有测试钉住):

  • 绝不自动读取环境里的 GITHUB_TOKEN/GH_TOKEN/GITHUB_PAT。 只有点名给本插件的 DSH_MARKET_GITHUB_TOKEN 才会被使用 —— 一个"顺手把环境里那个 token 转发出去"的插件,会让一次例行重装变成凭据泄露。社区里确实有插件从本机 gh 凭据里捞 token(dsh-plugin-radar)或直接 shell 出 gh auth token(dsw-workshop-plugin),本插件不这么做。
  • 只发给 api.github.com。 认证头按解析后的主机名判断,其它主机(含 api.github.com.evil.test 这类仿冒域名)一律不发。
  • 永不回传给浏览器。 token 不进入缓存状态、不进入 /list 摘要、不进入 /health,对外只暴露「有没有配置」这个布尔量。/settings 的响应体被断言过:整段响应文本里不允许出现 token,也不允许出现任何 ghp_ 形状的字符串。
  • /settings 只接受本机调用。 这个 HTTP 载体不做任何认证,所以设置接口的读和写都限制在 loopback —— 远程调用者既不能探出"这台机器上配没配 token",也不能替换或清除它。写设置必须是 POST,请求体上限 4KB(超了直接断开,不缓冲),字段类型不对一律 400 而不是被强转。
  • 环境变量永远压过界面开关。 面板会显示当前生效的是哪一层(env / settings / default);当环境变量说了算时,开关显示为禁用并注明原因。这样就不会出现"点了开关看着生效了、其实一直是环境变量在管事"。
  • 设置改动在下次重建目录时生效(快照 TTL 6h),点「刷新」可以立即生效。这里刻意不做"改一下立刻重建":那会让一次界面点击触发约 1500 次注册表请求。
  • DSH_MARKET_GITHUB_TOKEN=off(或 none/false/空)一律视为匿名,绝不会把字面量 off 当成凭据发出去。存进设置文件的 token 走同一道归一化,所以粘贴一个 off 也不会变成凭据。

GitHub 条目的结论是「临时」的,描述的是默认分支当前状态而非固定发布,因此:

  • verdict 带 provisional: true,界面上明确标注,不伪装成和 npm 条目同级
  • 没有版本可固定,所以界面不显示「版本」,改显示源码仓库与 star
  • 安装 spec 就是这个仓库地址,仍由官方 pluginManager 执行、仍跑官方预检
  • 不做提交固定:pnpm 取的是默认分支当下的状态,两次安装可能装到不同提交。安全上没有缺口(官方预检在实际取到的产物上再跑一次),但可复现性弱于 npm 条目 —— 见「已知限制」

数据通道

  • 目录:Host 侧构建并缓存在 <profile>/.plugin-market/catalog.json,经同源 HTTP 前缀路由 /api/plugin-market 暴露只读接口。 之所以用普通路由而不是 Typert Remote:安装已经走预生成的 pluginManager remote 命名空间,本插件唯一需要暴露给自己浏览器半边的只是只读目录 JSON,普通路由不需要 schema 代码生成,也不会因为 Typert 反射格式变动而加载失败。
  • 安装:浏览器半边直接调用 ctx.remote.pluginManager.installBundle(...),不经过本插件的 Host 代码。

接口

方法 路径 说明
GET /api/plugin-market/list search tone kind official sort offset limit
GET /api/plugin-market/detail?name= 完整条目(含 peer 明细)
GET /api/plugin-market/official 随 DSH 提供的官方组合包及其启用状态
POST /api/plugin-market/refresh 强制重建(仅回环地址可用)
GET /api/plugin-market/health 运行时版本 / 注册表 / 缓存状态

性能(实测,真实 DSH 进程内)

场景 耗时
冷构建(1 571 候选 → 1 224 插件) 约 22 s(受注册表限流影响),缓存命中后不再发生
面板打开(快照 TTL 6 小时内) 754 ms
单页 6 条响应体 4.2 KB

manifest 缓存以 name@version 为键并只保留所需字段(原始 manifest 全程缓存约 27 MB,投影后约 1.5 MB)。注册表返回 429/503 时会做一次退避重试;全部注册表都失败时不会用空目录覆盖已有快照,而是继续提供略旧的快照。


安装后的验收

实测于 desktop profile、DSH 0.2.0-rc.2:

验收项 结果
官方管理器安装 changed: true,application: "applied",退出码 0
加载器行 include:plugin-market → enabled: true,fiberPhase: "active"
Host 路由(进程内) GET /api/plugin-market/health → 200,runtimeVersion: "0.2.0-rc.2",注册表探测选中 registry.npmmirror.com
实时目录 GET /list → 200,1 140 个社区插件,218 个不兼容
侧栏面板 sidebar.panellist 占用者:原生 plugins(order 0) + 本插件 plugin-market(order 20),均 active: true
主面板 main 键 plugin-market,active: true
原生详情页注入 plugins.detail.badge → plugin-market.verdict;plugins.detail.section → plugin-market.section,均 active: true

前四项用官方 plugin_manager 工具与 HTTP 请求验证,后三项用实时客户端的 cordis_inspect_query(client / Slots / listSubTree)读取真实 Slot 树验证 —— 这直接证明浏览器半边被加载并完成了注册,而不只是「文件存在」。


开发

# 单元测试:兼容性门禁 + 服务核心 + 存储
node --test test/compat.test.mjs test/market.test.mjs test/store.test.mjs

# 插件装配与路由(不需要 DSH 运行)
node tools/test-plugin.mjs

# 浏览器半边:真实 React 19 + jsdom,跑 effect、点安装按钮、断言 DOM
node tools/test-client.mjs

# Host 半边端到端:真实注册表 + 真实 HTTP 服务
node tools/test-host.mjs

# GitHub 来源实况校验:真实 api.github.com,受预算约束(默认 25 次分类)
node tools/test-github.mjs

# 复核「加 GitHub 扫描到底值不值」这个判断本身
node tools/github-delta.mjs

# 双语字典键一致性(少一个键,界面就会把原始 code 显示给用户)
node tools/check-locale.mjs

# 离线构建目录快照用于分析
node tools/build-catalog.mjs .reference/catalog.json
node tools/analyze-compat.mjs

# 用真实 host 命令行的 argv 验证 profile 目录解析
node tools/verify-profile-resolution.mjs @argv.json

验证「插件不消耗额外 token」

会话日志 session.v4.jsonl.zstd(append-only 的 zstd 帧序列,一次性解压只能拿到第一条记录)里有两类可用记录:

  • request/header 存着逐字的 data.header.tools —— 真正发给模型的工具 schema 数组,可以直接 diff
  • assistant/message 存着每请求的 usage:inputTokens / cacheReadTokens / totalTokens / outputTokens
# 每个会话的工具面 + 首个请求的真实 token
node tools/token-report.mjs "%USERPROFILE%\.dsh\sessions"

# 两个会话直接 diff:工具增删 + 首请求 token 变化
node tools/token-report.mjs --diff <日志A> <日志B>

# 每个工具 schema 各占多少 token
node tools/measure-tool-cost.mjs "%USERPROFILE%\.dsh\sessions"

# 找出是什么在改变工具面
node tools/probe-tool-surface.mjs "%USERPROFILE%\.dsh\sessions"

实测:工具面本身约 26,558 字符 ≈ 7,588 tokens/请求。本插件注册 0 个工具,因此加成是 0。反过来,官方 plugin_manager 工具本身约 699 tokens/请求,cordis_inspect_query 约 320,cordis_inspect_list 约 160。

tools/test-client.mjs 不是打桩渲染:它在 jsdom 里用真实 React 渲染真实注册的组件、驱动 effect(因此真的去打 HTTP 接口)、真的点安装按钮,并断言产生的 DOM。

反向得到的、写代码时依赖的契约

这些不是猜的,是读官方源码与 TypeScript 声明得到的,直接决定了实现:

  • 浏览器半边是 classic script(document.createElement('script'),无 type=module),因此不能用顶层 import/export/await,也不能用 JSX。

  • require() 只解析 9 个平台种子(react、react-dom、@deepseek-ai/dsh-client-ui-primitives 等)+ 已注册的行;其它裸包名必须打进产物。所以本插件不依赖任何第三方前端库。

  • window.__ModuleLoader__.load({id}) 的 id 必须等于包名;exports["./client"] 必须存在且为字符串,否则客户端加载器抛错。

  • sidebar.panellist 是 list slot(需要 id),main 是按 key 索引的 keyed slot,且 main 的 key 必须等于 panellist 的 id。

  • plugins.detail.badge / .section 的 owner props 只有 {subject}(没有 view);subject.pkg 只有 {name, version?, installed, enabled, rows},不含 title/description/icon。

  • ManagementError 是 {code, diagnostic?, incompatible?} —— 没有 message 字段;incompatible[].peers 是「包名 → range」的记录。

  • ChangeResult.application ∈ applied | restart-required | overridden | failed | cancelled,且 pendingBuilds 在 ChangeResult 顶层。安装失败可以是「已 resolve 的成功响应」(ok: true 但 application: "failed"),因此不能只看 ok。

  • Host 进程里取不到 profile 目录,而每一个「显然」的答案都是错的:

    • DSH_PROFILE_DIR / DSH_PROFILE 在 host 进程里不存在 —— DSH 只把它们注入工具子进程的环境(dshEnvironment.collect),所以在终端里能看到、在 host 里看不到。这是本项目踩过的真实坑:第一版把缓存写到了 .dsh\.plugin-market\。
    • hmr.baseDir 是真实公开字段,但只有在服务就绪后才可读;在 apply() 期间读到 undefined 是合法的。
    • 真正的权威信号是 process.argv:桌面端启动形式是 … dsh-desktop-host/lib/index.js <asar>/dsh <profileDir>,CLI 形式是 dsh --profile <name>,而命令行从第一刻起就可用。

    因为每个来源在证明之前都只是猜测,所以每个候选都要结构化验证:真 profile 目录的 package.json 里必须有 dsh.profile。这条判据能排除启动器自己的路径(asar 子路径在 Electron 里看起来也像可读目录),因此猜错时只会退化到 DSH home,而不会写进无关目录树。/health 回报最终选中项与全部被拒候选,让错误答案可诊断。


安全模型

这个插件向浏览器暴露一个 HTTP 接口,并消费 npm 注册表返回的数据。两件事都不该被默认为可信,所以边界写在下面,并各有回归测试(test/security.test.mjs)。

前提:载体不提供鉴权

读官方源码可以确认:webServer.register 注册的路由,handler 是裸调用的,DSH 不做任何鉴权(webserver.index.js),而 bind host 的 schema 允许 0.0.0.0。也就是说,凡是这里提供的东西,能连到这个端口的人都能读。

由此推出三条硬约束:

  1. 这里不许输出任何凭据。 私有注册表的 URL 可能内嵌凭据(https://user:token@npm.internal/)。它不止出现在一个地方:/health 响应、目录 summary(既发给浏览器、又落盘到缓存文件)、日志行、pluginMarket.health() 服务方法,以及失败时的错误消息(那句消息会被记录、被重新抛出、还被拼进别的错误里)。因此脱敏发生在错误消息构造的那一刻,而不是在某个输出口 —— 否则总有一个出口会被漏掉。请求本身仍然用带凭据的真实 URL。
  2. 内部错误不回显给调用者。 500 响应体只有 {error:"internal"},完整错误只进日志。原因很直接:错误消息里可能带注册表凭据,也可能带文件系统错误里的绝对路径,而这个端点没有任何鉴权。
  3. 暴露本机状态的端点只对本机开放。 /official 报告的是本机装了哪些插件、哪些是启用的 —— 它不是用户的密码,但它是本机状态,因此仅限回环地址访问。公共目录仍然可读,所以从别的设备浏览插件依然可用,只是看不到本机安装列表(前端会给出明确原因,而不是一个裸状态码)。

任意网页都不能驱动本插件

回环判定本身是弱的,这一点必须说清楚:当 DSH 前面挂了本机反向代理时,所有请求的 remoteAddress 都是 127.0.0.1。

更普遍的问题是,用户浏览器发出的跨站请求也来自回环地址。所以 <img src="http://127.0.0.1:19387/api/plugin-market/refresh"> 这种写法能天然通过回环检查 —— 一次全量扫描就这样被任意网页触发了。

因此每个请求都必须带一个自定义头 x-plugin-market:

  • 跨站的 <img>、<script>、<form> 无法设置请求头,请求直接被拒(403)
  • 跨站的 fetch 想带头就会触发 CORS 预检,而本插件不返回任何 CORS 头,预检必然失败,真实请求根本不会发出
  • /refresh 另外还要求 POST,所以链接、预取、浏览器导航都不会构成变更

这个头是 CSRF 防护,不是鉴权:它证明调用方是跑在 DSH 同源页面上的脚本,而不是别处的页面。它挡不住能直接连到这个端口的本机程序,所以下面那些上界依然必要。

会做工作的端点

/refresh 会全量扫描注册表,因此限回环 + 要求 POST + 60 秒冷却。

冷却不是多余的:既然回环判定可被绕过,冷却就让无论谁在问,扫描次数都有上界。

/list 和 /detail 在冷启动时也会走到构建路径,所以扫描的代价有三个上界:

上界 值 防的是什么
单次候选数 2000 种子查询最多能点出 3000 个包名,恶意注册表可以把每一页填满
单次扫描时长 5 分钟 一个卡住 manifest 的注册表能把扫描拖到几十分钟
失败后退避 60 秒 注册表坏掉时,每个 list 请求都重新发起一次扫描

超时的扫描算失败,而不是返回「已经验证完的那部分」:把残缺目录当成完整目录缓存和排序,比继续用旧目录更糟。

不信任注册表返回的数据

搜索响应里的包名会被插进请求路径,版本会被拼进安装 spec。于是一份恶意或仅仅是坏掉的注册表响应,理论上能做路径穿越(URL 解析器会规范化 ..)。因此:

  • 包名在进入 URL 之前先过 npm 自己的字符合法性检查(只允许 [a-z0-9-._~] + 一个可选 scope,拒绝 ..、空白、控制字符、?、#、%,以及会产出 - 开头 spec 的前导 -)
  • 版本号同样校验字符集
  • 前端在把一行变成 ${name}@${version} 之前再校验一次 —— 目录数据来自注册表,属于不可信输入,即使官方管理器自己也会解析这个 spec

版本必须是确切版本,不能是 dist-tag 或范围。 这是一条独立的要求:注册表完全可以对 /pkg/latest 回一个 "version": "latest",那种 manifest 的 peer 依赖会被照常评估、徽章显示「兼容」,而安装 spec 会变成 pkg@latest —— pnpm 装的就不是刚才审阅的那个版本。所以 semver.valid() 是安装资格的一部分,拿不到确切版本就标记为不可安装(unpinned-version)并说明原因。同理,写不出确切运行时版本时判定为未知(runtime-unknown)而不是「兼容」:猜「兼容」是撒谎,判「不兼容」会冤枉整个目录。

资源上界

响应体积有上限(搜索 8 MB,单个 manifest 1 MB)。没有这个上限,一份超大的响应就足以让 host 进程 OOM。上限是流式执行的,所以谎报的 Content-Length 也拦得住,不是只看头部。/-/ping 探测更彻底:body 直接 cancel,根本不读。

光有单响应上限还不够 —— 缓存会永久保留每个包的投影,所以还需要聚合上界:

  • projectManifest 截断 description(500 字符)、keywords(20 个 × 40 字符)、homepage(300 字符)、maintainers(10 个)
  • manifest 缓存有条数上界 2000 与字节上界 8 MB,超出按插入顺序淘汰最旧的
  • 非插件不入缓存:一次扫描最多能点出几千个包名,为每个非插件长期保留一份投影是纯内存开销
  • 载入时也会裁剪,因此旧版本写下的超大缓存文件会被自动收敛

注入面

  • 客户端不用 innerHTML / dangerouslySetInnerHTML / eval / new Function,没有字符串拼 HTML
  • 没有任何 href —— 注册表提供的 repository / homepage 是纯文本渲染。这一点是刻意的:如果渲染成链接,一份恶意 manifest 只要填 javascript: 就能在 DSH 自己的页面里执行脚本,而那个页面能调用 remote.pluginManager.installBundle
  • 原型污染不可达:唯一一处「不可信键赋值」由 peer 名校验把关;缓存键必然含 @;投影只复制固定字段

不增加模型上下文

本插件注册 0 个工具,也不注册 systemPrompt section/context/variable。实测每个请求的工具面约 26,558 字符 ≈ 7,588 tokens,本插件的贡献是 0。目录数据走同源 HTTP 直接进浏览器,不进模型上下文。

(对照:官方 plugin_manager 工具本身约 699 tokens/请求,cordis_inspect_query 约 320,cordis_inspect_list 约 160。)

它不做的事

  • 不自己安装任何东西 —— 安装、启用、卸载全部委托给官方 pluginManager,因此注册表回退、package.json / pnpm-lock.yaml 回滚、构建脚本授权、兼容性预检都用官方的实现
  • 默认不碰任何凭据。 不自动读取环境里的 GITHUB_TOKEN/GH_TOKEN/GITHUB_PAT,不 shell 出 gh auth token,未配置时一个 Authorization 头都不发。唯一的例外是你主动在面板里填的那个 token —— 它只存在本机设置文件里、只发给 api.github.com、且永远不回传给浏览器(细节见上文「可选:启用 GitHub token」)
  • 不执行注册表数据里的任何内容

你可以自己验的

node --test test/*.mjs                  # 133 项,7 个文件,全绿
node --test test/security.test.mjs      # 24 项:脱敏、包名/版本校验、体积与缓存上界、CSRF 头、端点鉴权、冷却
node --test test/encoding.test.mjs      #  5 项:编码损坏守卫(U+FFFD / NUL / BOM)
node --test test/compat.test.mjs        # 18 项:兼容性门禁与安装资格
node --test test/github.test.mjs        # 34 项:滴灌预算、配额退避、TTL、路径穿越、体积上限、开关、凭据边界
node --test test/settings.test.mjs      # 20 项:环境变量优先级、落盘、/settings 只允许本机、token 永不出现在响应里
node --test test/store.test.mjs         # 18 项:profile 定位、缓存与设置分离、0600 权限、损坏文件读作缺失
node tools/check-locale.mjs             # 双语字典键一致(93 键 ×2)、无死键、不渲染原始 code
node tools/test-client.mjs              # 浏览器半:面板会真的切换开关,且从不回显 token
node tools/test-plugin.mjs              # 装配:bundle patch、client 注入、exports 可达
node tools/test-github.mjs              # 实况:真实 GitHub topic,断言从不发 Authorization 头

已知限制

  • GitHub 来源是滴灌的,不是即时全量。 匿名时单轮最多分类 40 个仓库、剩余配额 ≤5 就停、每仓库 24h 内不重复读;启用 token 后是 250 / 余量 200。搜索 API 单次查询最多 1000 条而该 topic 有 1.7 万+ 仓库,因此永远覆盖不全。这是配额硬上限,不是可以调参绕开的:只有开启 token(83 倍配额)能实质改变覆盖率,而那要以交出凭据为代价。
  • 滴灌只在目录重建时推进(快照过期 6h,或手动点「重建目录」)。所以匿名模式下约 6 天才走完发现窗口;启用 token 后约 4 次重建即可。想更快推进就多点几次重建 —— 配额守卫会在余量耗尽前替你停手,不会失控。
  • GitHub 条目不做提交固定。 安装 spec 就是仓库地址,pnpm 取默认分支当下的状态;界面上的结论也只是那次扫描时的默认分支状态,所以标为「临时」。安全上没有缺口(官方预检会在实际取到的产物上再跑一次),但可复现性弱于 npm 条目:同一行两次安装可能装到不同提交。要固定,需要在安装前解析 HEAD sha 并拼成 #<sha>;DSH 的 spec 解析器接受这个形式(HOSTED_REPOSITORY_URL 尾部有 (?:#.*)?),这是明确的下一步。
  • 不做版本选择/升级:与官方面板一致,升级靠卸载后重装(官方当前也不支持就地升级)。
  • 不按 star 排序:star 只是 github 条目的展示字段。实测该 topic 按 star 排序前排全是误挂 tag 的无关项目,拿它排序等于花配额进口噪音。
  • 官方分组取自随装的组合包,不使用注册表上的官方包 —— 注册表上的官方包常有比随装版本更旧的发布(实测 84 个官方包中 57 个与当前运行时不一致)。
  • 缓存按 profile 隔离,因此多 profile 会各存一份约 1.5 MB 的 manifest 缓存。

许可

MIT

—/ 5

No ratings yet

Verified DSH bundle

Commit d948325e9d32

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout