dsh-web
zhu1090093659
DeepSeek Harness (DSH) Web 插件聚合生态 · 万物皆插件,通过创意工坊分发||DeepSeek Harness (DSH) Web Plugin Aggregation Ecosystem · Everything is a plugin, distributed via the Creative Workshop
PROJECT TOPICS
INSTALL REFERENCE
dsh plugin --profile web add github:luoxunhao/dsh-codex-project
该命令指向仓库当前默认分支;尚无绑定当前 commit 的完整验证结果。
PROJECT README
DSH 工作区共享子目录插件 —— 源自 Codex 的项目处理思想:一个工作区可以挂载任意数量的共享子目录(可跨盘符),该工作区的 DSH 会话可以像对待自己的工作区一样读写这些目录——全程保持 workspace-write 权限,永远不需要 danger-full-access。
Codex 处理项目时,一个"项目"往往横跨多个目录:主代码库、共享库、文档、数据目录。传统做法要么给模型 full access(危险),要么逐个授权(繁琐)。
本插件的模型是 Codex 式的项目共享:
一个共享配置(记录)= 一个主根(该工作区自身的 path)+ 任意数量的共享子目录(跨盘符)
命中该配置的会话 → 可读写该配置的全部可写根(主根 ∪ 现存共享子目录)
path 即命中该条记录(锚定);共享子目录里的会话不会仅因该目录出现在别人配置里而命中;workspace-write 权限级内,只是把可写集合从单根扩展为多根(Windows ACL 受限令牌 + 空间级 SID)。| 能力 | 说明 |
|---|---|
| 共享子目录配置 | 每个工作区可配置任意数量共享子目录(跨盘符、可裸目录) |
| 「管理工作区」弹窗 | 原生工作区「…」菜单注入入口:添加/移除共享子目录 |
| 「项目文件夹」tab | 侧边栏注册的项目多根目录树:主根 + 共享子目录(跨盘符),按层懒加载;仿 Files tab 布局(顶部搜索框 + 刷新/上传按钮 + 文件树,点击文件在插件自有的「文件预览」page 内打开),右键目录用文件管理器打开。承载走 DSH 原生右侧栏(ctx.sidebarRightTabs + sidebar.right.pane.tab 键控 seat,0.1.6 的 web 组合默认自带) |
| 「打开本地目录」 | 原生「…」菜单注入入口:用系统文件管理器打开该工作区文件夹(插件自有路由 spawn explorer.exe——不走 workspaces.openPath,避免被 better-sidebar 等插件劫持到侧边栏编辑器) |
| 多根沙箱 runner | 命中配置的会话,shell/subprocess 自动走多根受限令牌(lib/runner.js) |
| 多根 fs fence | 进程内 fs 工具(read/write/edit)同样按配置可写根放行(lib/fs.js) |
| 会话上下文提醒 | 第一条 user 消息后折叠 <system-reminder> 目录清单(注明附加目录与主目录权限一致) |
| 配置 CRUD + 持久化 | /codex-project/api JSON 路由,SQLite 存于 ~/.dsh-codex-project/dirs.db |
add_dir 模型工具 |
模型可通过工具请求添加目录(用户确认后生效)——工具名下划线,与 dsh 模型工具命名一致 |
/adddir 指令 |
人类在 composer 输入 /adddir → 弹系统目录选择框,把选中目录加入当前会话工作区(对应模型工具 add_dir) |
┌─────────────────────────── DSH web ───────────────────────────────────┐
│ (client half) │
│ 侧边栏工作区「…」菜单 ──注入「打开本地目录」+「管理工作区」──▶ 本地动作/弹窗 │
│ 「项目文件夹」tab ──注册进 DSH 原生右侧栏(sidebarRightTabs + seat) │
│ │ └ 文件在插件自有的「文件预览」page 内打开 │
│ │ fetch() │
│ ▼ │
│ /codex-project/api (CRUD + 项目目录树,loopback 守卫) │
└───────────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────┐
│ (host half) 四个独立钩子,共用命中判定 matchingWorkspace │
│ │
│ ① ctx.webServer.register ─── HTTP 路由(CRUD / 文件读写) │
│ ② ctx.on('agent/pre-step') ─── 折叠 <system-reminder> 目录清单 │
│ ③ ctx.tools.register ─── add_dir 模型工具 │
│ ④ sandbox.confine wrap ─── 多根 runner(lib/runner.js) │
│ │
│ ⑤ ctx.fs 提供者(bundle patch 替换核心 fs-sandbox) │
│ └── lib/fs.js 多根 fence(与 ④ 共用命中判定) │
│ │
│ ⑥ ctx.commands.register ─── /adddir 指令(原生目录选择器) │
│ │
│ 配置外 / 单根 / 无 cwd 的会话 → 四个钩子全部纯透传(零影响) │
└───────────────────────────────────────────────────────────────────────┘
本插件通过 Cordis(DSH 的 IoC 框架)提供的四个钩子切入 DSH 的运行循环,不侵入 DSH 源码。
ctx.webServer.register({ kind: 'prefix', path: '/codex-project/api', handler: ... })
DSH 启动时,插件把 /codex-project/api/* 路由挂到宿主 HTTP 服务器上。client 的 api.ts 通过 fetch() 调用这些路由,host 侧处理后返回 JSON。配置 CRUD、文件读写、目录列表全部走这条路。
agent/pre-step 事件 — 折叠上下文提醒ctx.on('agent/pre-step', async ({ agent, messages }, next) => {
const decision = await next()
return foldWorkspaceContext(decision, messages, agent.session)
})
DSH 的 agent 循环是:收消息 → pre-step 事件链 → 模型推理 → 工具执行 → 输出。agent/pre-step 是推理前的最后一道关卡。
插件在这里计算当前会话的共享目录清单,生成 <system-reminder> 消息插入模型上下文。提醒只在附加目录集合变化时重新折叠:新会话首次播种一次,add_dir 工具 / /adddir 指令 / 管理弹窗改动目录后才在下一轮重新注入,目录未变时跨轮不重复叠加。提醒列出主目录与附加目录,并注明附加目录与主目录权限一致——它们处在同一个 workspace-write 边界下(插件在每个存活根上授予工作区级 SID,附加目录绝不超出主根权限),模型可统一对待。
ctx.tools.register() — 注册 add_dir 模型工具ctx.tools.register(defineAddDirTool(deps))
注册后模型在推理时可以看到 add_dir 工具的 schema,并主动调用它添加新目录。工具名下划线 add_dir 与 dsh 模型工具命名一致。执行流程:
模型调用 add_dir(path)
→ 插件通过 ctx.approval.request() 请求用户确认
→ 用户批准 → 写入 SQLite(`dirs.db`)
→ 目录立即进入可写集合(无状态重校验)
/adddir 指令(用户侧)模型工具 add_dir 的用户侧镜像:在 composer 里输入 /adddir 并回车,会弹出一个系统原生目录选择框(宿主 ctx.directoryPicker 的 native 能力)。选中某文件夹后,它会直接加入当前会话所属工作区的附加可写目录——因为是用户主动明确选择,所以不再弹确认框。命令只在宿主组合了 native directory picker 时注册;未组合(如纯 browse/远程后端)时命令不注册,插件其余功能不受影响。
用户输入 /adddir
→ 宿主弹原生目录选择框
→ 选中目录 → 写入当前会话工作区的 SQLite(`dirs.db`)
→ 目录立即进入可写集合
sandbox.confine 包装 — 劫持进程沙箱const original = sandbox.confine
sandbox.confine = (argv, policy) => {
// 非 Windows / 非 workspace-write / 无匹配记录 / 单根 → 透传原逻辑
// 有多根共享目录 → 返回插件 runner.js 的 ConfinedArgv
return { argv: [node, runner.js, --bind root1, --bind root2, --, ...原命令], ... }
}
DSH 在执行 shell 命令时调用 sandbox.confine(argv, policy) 决定如何隔离进程。插件 wrap 了这个方法:无共享目录时行为与 DSH 原生完全一致;有多根时路由到 runner.js,由它在 Windows 上创建受限令牌、给每个存活根授予 Write ACE、在该令牌下 spawn 子进程。
用户在 GUI 输入命令
↓
DSH agent 循环启动
↓
pre-step 事件触发 → 插件折叠 <system-reminder>(钩子 2)
↓
模型推理(已看到共享目录清单)
↓
模型决定执行 shell 命令
↓
sandbox.confine → 插件 wrap 决定路由(钩子 4)
↓
├─ 无共享目录 → 核心 runner(单根 workspace-write)
└─ 有多根共享目录 → 插件 runner.js(多根受限令牌 + SID)
↓
子进程执行,fs 操作经过 ctx.fs(插件的 CodexProjectFileSystem)
↓
fs fence 按可写根集合放行/拒绝(与 runner 共用同一命中判定)
workspace-write 受限令牌(拒绝列表 + 空间级 SID 写授权),只是 Write ACE 覆盖配置的可写根;config 目录 + workspace id 摘要)——核心单根会话的 SID 无法沿着共享目录的 ACE 进入其他根,空间会话的令牌也无法使用别的根的 ACE;codex-project-run: <detail> 并以 127 退出,绝不以非受限方式 spawn 子进程;(⚠ directory missing) 标注,host 日志对每个空间首次 warn 一次、之后 debug;root 目录恢复后无需重启或改配置即可重新进入可写集合(无状态重校验);workspace-write 边界,绝不暗示高于该边界(如 full access)。[sandbox: …] 拒绝标记不变;实际读写仍以沙箱实测为准。path 时,cwd 落在该目录的会话匹配配置文件中靠前的那条(可写集合随配置顺序漂移)。建议共享子目录互不重叠;默认存于 SQLite ~/.dsh-codex-project/dirs.db(环境变量 DSH_CODEX_PROJECT_CONFIG 可覆盖),由 Node 内置 node:sqlite 驱动——无需额外原生依赖,写操作为单事务整表替换,不依赖「临时文件 + rename」原子写(后者在 Windows 目标被占用/查杀时会报 EPERM)。一张 workspaces 表,一行一个工作区,逻辑模型:
{
"workspaces": {
"<workspaceId>": {
"path": "主根(该工作区自身的路径)",
"dirs": ["共享子目录1", "共享子目录2"]
}
}
}
path 恒为该工作区自己的主根(锚定,记录键即工作区 id);dirs 为额外可写目录;/codex-project/api 前缀(loopback Host 守卫),全部 JSON:
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /ping |
挂载冒烟 |
| GET | /dirs?workspaceId=<id> |
某工作区的共享子目录列表(无记录返回空数组;只有未知 id 才 404) |
| PUT | /dirs |
替换某工作区的共享子目录({ workspaceId, dirs });首次添加自动锚定该工作区 |
| GET | /project?cwd=<path> |
cwd 命中的项目:{ path, dirs, missingDirs } 或 null(无项目配置) |
| GET | /list?cwd=<path>&path=<abs> |
列一个项目根目录层级(fence 到项目可写根,越界 403) |
| GET | /read?cwd=<path>&path=<abs> |
文本读取,上限 4MB,超出返回 truncated: true |
| POST | /write |
保存文本({ cwd, path, content }),fence,自动建父目录 |
| GET | /file?cwd=<path>&path=<abs> |
原始字节(图片/PDF/二进制);&download=1 → attachment disposition |
| POST | /open-directory |
用系统文件管理器打开一个文件夹(插件自有路由) |
错误:400 非法输入、403 项目根之外(fence)、404 未知工作区、405 未知方法/路由。
前置:已装好 DSH(dsh web 能正常运行),Node.js ≥ 22.5、pnpm ≥ 10。
本插件基线 DSH 0.1.6-alpha.2(peer 范围 ^0.1.6-alpha.2,已在发布版 0.1.6-alpha.2 上完成真机挂载验证:「项目文件夹」「文件预览」两个 tab 在原生右侧栏内正常打开)。0.1.5-rc.x 与 0.1.2-alpha.x 不再支持——原生侧边栏那条承载线要 0.1.6,旧线的宿主服务面也已变,请升级 DSH 运行时。
dsh plugin --profile web add @luoxunhao/dsh-codex-project
装完硬刷新浏览器(Cmd/Ctrl+Shift+R)即可看到「项目文件夹」tab 和管理工作区入口。client 改动无需重启 DSH;host 改动需重启。
git clone <repo-url> && cd dsh-codex-project
pnpm install && pnpm build
dsh plugin --profile web add <本仓库绝对路径>
也可用 —— 0.1.6 上不可用:该文件用两条 dev.patch.yml 挂载(无需 install 进 profile)file:// 行加载同一个包,client-modules 现在按包名归并 Loader 源,会直接报 resolves from multiple active Loader sources; remove one entry,插件 client 进不了模块图(侧边栏不出现)。用上面 dsh plugin --profile web add <路径> 挂载(实测可用)。
client 改动浏览器硬刷新即可;host 改动(路由、seam、fs、runner)需重启 dsh web。
| 现象 | 原因与解决 |
|---|---|
报 Ignored build scripts |
pnpm 拦截构建脚本。在 profile 目录下跑 pnpm approve-builds --all。 |
报 minimum release age |
版本发布不足 24 小时。等 24h 或重跑一次。 |
| 报「找不到 profile 目录」 | 先跑一次 dsh web,让它初始化 profile。 |
| 「项目文件夹」tab 不出现 | 多半是 client 根本没进模块图:看 dsh web 启动日志有没有 client-modules: ... resolves from multiple active Loader sources(用 dev.patch.yml 的两条 file:// 行挂载就会触发,见上方「本地开发」)。改用 dsh plugin --profile web add 挂载。原生侧边栏(ui-sidebar-right)是 0.1.6 web 组合的默认装配,缺失即宿主版本不对。 |
| Windows 下终端/runner 异常 | 确认 @deepseek-ai/dsh-sandbox-windows-acl 已正确安装(koffi 需构建脚本)。 |
pnpm typecheck # 类型检查(tsc --noEmit)
pnpm test # 单元测试(vitest,18 个文件 / 225 用例)
pnpm build # 构建 lib/(tsc types + tsdown:host ESM + client CJS + runner + fs)
pnpm proto:verify # 多根 runner 原型实证(Windows ACL,需先 build)
架构约束:
node:path——路径运算放 src/client/paths.ts;ctx.sidebarRightTabs + 键控 sidebar.right.pane.tab / .title seat,见 src/client/native-sidebar.tsx),只走公开面,client 侧用结构化再声明消费(src/client/context.ts)。所需服务经 ctx.inject(['sidebarRightTabs','slots','sessions'], …) 等待而非一次性探测;该承载包因此不写进 dsh.client.inject(那是加载/组合边,也不是 PLATFORM_MODULES)。往下传给组件的 runtime ctx 是合成出来的 { get, sessions }——插件自己的 ctx 是 cordis 代理,ctx.sessions 读出来就是 undefined,直接传下去会让「引用到对话」静默失效。better-sidebar ≥0.19 会把 tab 转发进同一原生面,所以不再往 betterSidebar.registerTab 注册(注册了就会出两个「项目文件夹」);codex-project / codex-project-file)都是 page 类型(不声明 patterns),不与产品自带的 dsh-resource://file/** viewer 抢地址;文件预览走插件自有多根路由,跨盘共享目录才可预览;dirs-api.ts 的 fenceFor,不要另写一份 roots 推导。| 项 | 值 |
|---|---|
| peer 范围 | @deepseek-ai/dsh-* 一律 ^0.1.6-alpha.2 |
| 验证基线 | DSH 0.1.6-alpha.2(dsh plugin --profile web add <本仓库> 装进 web profile → dsh web)。2026-09-20 真机重验通过,挂载面那两条改动(ctx.inject 依赖表加 sessions、dsh.client.inject 摘掉承载包)不再是欠账:原生右侧栏 tab 条出现「项目文件夹」并可打开;根行展开的目录列表走插件自有多根路由;点文件开 codex-project-file 预览 tab(markdown 正常渲染);切「编辑」CodeMirror 正常挂载,未脏时「保存」为 disabled;右键「引用到对话」真的把引用插进了输入框——即合成 { get, sessions } 那条在真宿主上生效,不是只被 jsdom fake 建模。pnpm typecheck / pnpm test 225 用例 / pnpm build 全绿,控制台零报错(唯一警告来自无关的 dsh-dream-skin,外观类)。/adddir 的 native 门控也在真机上复验过:/ 菜单里 /adddir 正常在册(54 项)。两点保留:本次是 link: 指向本地构建(npm 上只有 0.11.0,非发布版路径);「重复点击同一文件会重读」一项本轮未测 |
@deepseek-ai/cordis |
^4.0.2(与 DSH vendor/cordis 同版) |
三个必须知道的坑(升级时踩过,别再踩):
semver.satisfies('0.1.6-alpha.2', '^0.1.5-rc.1') 严格模式返回 false——peer 范围必须显式写成目标预发布线(^0.1.6-alpha.2),否则装不上。dsh-client-ui-primitives 不声明 dependencies,其 bundle 裸 import shiki / @shikijs/langs/* / anser / clsx / katex / mdast-util-* / micromark-*,0.1.6 起又加了 diff 与 simple-icons(DiffBlock / 品牌图标)。这些必须由消费者提升进 devDependencies(本仓库已补齐),否则 vitest 的 browser 用例在 resolve 阶段报 Failed to resolve import。这组 devDependencies 不得回退;升 primitives 后先把它 bundle 的裸 import 全列一遍再对。client-modules 按包名归并 Loader 源,lib/index.js + lib/fs.js 两条 file:// 行会被判为 resolves from multiple active Loader sources 并让插件 client 掉出模块图(见「本地开发」)。client bundle 的注册 id 与侧边栏承载无关:插件只调用公开面(
ctx.sidebarRightTabs+ctx.slots)。better-sidebar v0.19.0 起把 tab 转发到 DSH 原生右侧栏——正因如此,两者同时注册会出两个「项目文件夹」tab,插件只走原生那一条。
tests/(vitest,browser 组件用 jsdom):
dirs-api.spec.ts — CRUD + 锚定 + 失效根 + 项目解析 + 目录列表(排序/fence 403/跨盘根)+ 读/写/文件字节与下载 dispositionproject-tab.spec.tsx — 无配置回退单根、根行(主/共享/缺失)、懒加载、点击把文件交给 openPreview、右键菜单file-reference.spec.ts — @ 引用源注册 / 注入 / 序列化client-apply.spec.tsx / client-components.spec.tsx — 插件形态、菜单注入、管理弹窗native-sidebar.spec.tsx — 原生右侧栏两阶段注册(type/body/title 的 id 与 key)、page body 经 tab.actions.openTab 打开自有预览、navigation.params 与 chip 标题回退、useTabInfo 抛错时的等待态、晚到的 carrier、unload 回收、合成 runtime ctx 真能拿到 sessions.scope/conversation、同一文件再次导航(revision 变)会重读native-sidebar-composition.spec.ts — 用真实 SlotCore 验证键控 seat 的声明/落位/回收(未声明 seat 不抛,正是走 slots.inject 的理由)fs-fence.spec.ts / seam-wiring.spec.ts — 多根 fence 收窄/隔离/自愈、runner 接线context-injection.spec.ts — 上下文提醒(文本组成/折叠位置/去重/缺失标注)add-dir.spec.ts / adddir-command.spec.ts — add_dir 模型工具(校验/审批/持久化)与 /adddir 指令,含未 anchor 的新工作区首次加目录dirs-store-write.spec.ts — SQLite 单事务整表替换 + DirsStore.addDir(自动 anchor、追加不改 path、重复即幂等)open-directory.spec.ts / pick-browse.spec.ts — 打开本地目录路由、跨盘符目录选择器search-upload.spec.ts — 目录树搜索与上传client-api.spec.ts / plugin-shape.spec.ts — client API 面、插件导出形态新增 API 面(如 SpacesApi 加方法)时,记得同步更新各测试里的 fake,否则 typecheck 会因缺方法失败。
CLASSIFICATION EVIDENCE
系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: dsh-better-sidebar。