← Back to home@luoxunhao

dsh-codex-project

codex风格的项目管理,可以给dsh项目增加附加目录,访问附加目录不需要full-access权限,方便多项目开发,可配合dsh-better-sidebar插件使用。

Stars
3
Language
TypeScript
Created
Sep 2, 2026
Updated
Sep 25, 2026
GitHub repo

Introduction

dsh-codex-project

DSH 工作区共享子目录插件 —— 源自 Codex 的项目处理思想:一个工作区可以挂载任意数量的共享子目录(可跨盘符),该工作区的 DSH 会话可以像对待自己的工作区一样读写这些目录——全程保持 workspace-write 权限,永远不需要 danger-full-access。


设计思想

Codex 处理项目时,一个"项目"往往横跨多个目录:主代码库、共享库、文档、数据目录。传统做法要么给模型 full access(危险),要么逐个授权(繁琐)。

本插件的模型是 Codex 式的项目共享:

一个共享配置(记录)= 一个主根(该工作区自身的 path)+ 任意数量的共享子目录(跨盘符)
命中该配置的会话 → 可读写该配置的全部可写根(主根 ∪ 现存共享子目录)
  • 单配置模型:会话的可写集合 = 其 cwd 命中的那一条配置的可写根(无并集、无叠加),行为可预期;
  • 单边命中:会话 cwd(规范路径)== 某条记录的 path 即命中该条记录(锚定);共享子目录里的会话不会仅因该目录出现在别人配置里而命中;
  • 不强制注册:共享子目录可以是裸目录,不必是 DSH 注册的工作区;
  • 不升级权限:仍在 workspace-write 权限级内,只是把可写集合从单根扩展为多根(Windows ACL 受限令牌 + 空间级 SID)。

功能特性

能力说明
共享子目录配置每个工作区可配置任意数量共享子目录(跨盘符、可裸目录)
「编辑工作区」弹窗原生工作区「…」菜单注入入口:源文件夹列表 + 添加/移除/设为主要
「项目文件夹」tab侧边栏注册的项目多根目录树:主根 + 共享子目录(跨盘符),按层懒加载;仿 Files tab 布局(顶部搜索框 + 刷新/上传按钮 + 文件树)。点击文件交给 DSH 自带的文件 viewer(openResource,工作区外/跨盘一样能读);右键「编辑」在插件自有的「文件预览」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)  │
│        │              └ 点击→宿主自带 viewer;「编辑」→自有 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 源码。

1. HTTP 路由 — client/host 之间的桥梁

ctx.webServer.register({ kind: 'prefix', path: '/codex-project/api', handler: ... })

DSH 启动时,插件把 /codex-project/api/* 路由挂到宿主 HTTP 服务器上。client 的 api.ts 通过 fetch() 调用这些路由,host 侧处理后返回 JSON。配置 CRUD、文件读写、目录列表全部走这条路。

2. 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 指令 / 编辑弹窗增删目录 / 改「主要」后才在下一轮重新注入,清单未变时跨轮不重复叠加。清单顺序是主要根领头(标 (primary workspace))、随后是会话工作区(标 (current session workspace))与其余目录;结尾声明列出的目录与会话工作区权限相同——它们处在同一个 workspace-write 边界下(插件在每个存活根上授予工作区级 SID,附加目录绝不超出会话根权限),模型可统一对待。

3. 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`)
  → 目录立即进入可写集合

4. 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 共用同一命中判定)

安全模型

  • 权限不升级:多根 runner 仍是 workspace-write 受限令牌(拒绝列表 + 空间级 SID 写授权),只是 Write ACE 覆盖配置的可写根;
  • 空间级 SID:每条配置一个专属 SID(config 目录 + workspace id 摘要)——核心单根会话的 SID 无法沿着共享目录的 ACE 进入其他根,空间会话的令牌也无法使用别的根的 ACE;
  • 失败契约:runner 任何失败输出 codex-project-run: <detail> 并以 127 退出,绝不以非受限方式 spawn 子进程;
  • 失效根收窄(narrowing):配置里某个共享子目录事后被删除(被动失效)时,可写集合收窄到现存根——死根本身物理上已不可写,其存在性失败不阻塞其余根,也不影响任何无关会话;上下文提醒给死根加 (⚠ directory missing) 标注,host 日志对每个空间首次 warn 一次、之后 debug;root 目录恢复后无需重启或改配置即可重新进入可写集合(无状态重校验);
  • 权限提示不升级:上下文提醒说明附加目录与主目录权限一致——即所有根共享同一个 workspace-write 边界,绝不暗示高于该边界(如 full access)。[sandbox: …] 拒绝标记不变;实际读写仍以沙箱实测为准。

已知边界(设计取舍)

  • 重叠根歧义:同一目录同时是两条记录的 path 时,cwd 落在该目录的会话匹配配置文件中靠前的那条(可写集合随配置顺序漂移)。建议共享子目录互不重叠;
  • standing ACE 常驻:runner 在每个根上物化的空间 SID Write ACE 是常驻的(跨会话复用缓存,dispose 只回收私有 temp)。删除配置不会回收已打上的 ACE(孤立 ACE 无令牌携带,无害但会累积);
  • 无只读共享:所有根都授予读写——想"只读共享"(如只允许读 DSH 源码树)需要后续特性;
  • 提醒不重注入:每会话一次性;恢复的会话若已有相同提醒不再折叠(带着旧文案恢复的会话会在下一条 user 消息后折叠进当前文案);
  • 增删仍校验存在性:通过 API 保存的共享子目录要求存在(主动操作 fail loud);只有目录事后消失的被动失效会被收窄,不阻塞其余根。

配置文件

默认存于 SQLite ~/.dsh-codex-project/dirs.db(环境变量 DSH_CODEX_PROJECT_CONFIG 可覆盖),由 Node 内置 node:sqlite 驱动——无需额外原生依赖,写操作为单事务整表替换,不依赖「临时文件 + rename」原子写(后者在 Windows 目标被占用/查杀时会报 EPERM)。一张 workspaces 表,一行一个工作区,逻辑模型:

{
  "workspaces": {
    "<workspaceId>": {
      "path": "主根(该工作区自身的路径)",
      "dirs": ["共享子目录1", "共享子目录2"],
      "primary": "展示层主要根(dirs 之一,可缺省)"
    }
  }
}
  • path 恒为该工作区自己的主根(锚定,记录键即工作区 id);dirs 为额外可写目录;
  • primary 只影响"谁领头、谁被称作主":「编辑工作区」弹窗的源文件夹列表、「项目文件夹」tab 根行、以及注入给模型的上下文提醒都由它领头并带标注(改主要会让提醒文本变化并在下一轮重新折叠)。锚定、fence、@ 相对基准一律忽略它,所以设主要既不会放宽也不会收窄任何目录的权限;该目录消失时自动回落成 path 领头;
  • 缺省/空库 = 无配置 = 纯透传;
  • 失效根不自动清理:目录消失不会改写配置(对齐 DSH 核心"被动失效保留记录、降级显示"策略);通过「编辑工作区」弹窗或 API 显式移除。

HTTP API

/codex-project/api 前缀(loopback Host 守卫),全部 JSON:

方法路径说明
GET/ping挂载冒烟
GET/dirs?workspaceId=<id>某工作区的共享子目录与主要根 { dirs, primary? }(无记录返回空数组;只有未知 id 才 404)
PUT/dirs替换某工作区的子目录与主要根({ workspaceId, dirs, primary? });首次添加自动锚定该工作区,省略 primary 即清除;primary 不是 dirs 之一 → 400
GET/project?cwd=<path>cwd 命中的项目:{ path, dirs, missingDirs, primary? } 或 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.7-rc.2(peer 范围 ^0.1.7-rc.2,已在真机完成挂载验证:「项目文件夹」「文件预览」两个 tab 在原生右侧栏内正常打开,/codex-project/api/* 路由与 client bundle 均由宿主实际供给)。0.1.6-alpha.x 及更早不再支持——0.1.7 起消息来源(MessageSource)改成按生产者各自声明的可合并联合类型(旧的通用 'plugin' kind 已被移除),侧边栏承载线也随之上移,旧线的宿主服务面已变,请升级 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 <本仓库绝对路径>

也可用 dev.patch.yml 挂载(无需 install 进 profile) —— 0.1.6 起不可用(0.1.7 同样):该文件用两条 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 scriptspnpm 拦截构建脚本。在 profile 目录下跑 pnpm approve-builds --all。
报 minimum release age版本发布不足 24 小时。等 24h 或重跑一次。
报「找不到 profile 目录」先跑一次 dsh web,让它初始化 profile。
装上之后 dsh 直接起不来报错一定是 plugin tree failed to load: failed to import loader entry codex-project…(或 codex-project-fs),后面紧跟原因 —— 通常是宿主版本不在 ^0.1.7-rc.2 这条线上(缺我们 import 的导出),或 Node < 22.5(没有 node:sqlite)。一条命令即可恢复启动:dsh plugin --profile web remove @luoxunhao/dsh-codex-project,然后对齐版本再装。这类失败发生在启动期、不会留下半装状态。(本插件不用条件挂载来"降级绕过",就是为了让这种不兼容报出来而不是被藏起来。)
「项目文件夹」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 个文件 / 246 用例)
pnpm build              # 构建 lib/(tsc types + tsdown:host ESM + client CJS + runner + fs)
pnpm proto:verify       # 多根 runner 原型实证(Windows ACL,需先 build)

架构约束:

  • client bundle 禁止 value-import 其他插件的运行时符号(纯度门);与 DSH 源码的集成只走公开/只读 API;
  • browser bundle 无 node:path——路径运算放 src/client/paths.ts;
  • 「项目文件夹」「文件预览」注册进 DSH 原生右侧栏(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 注册(注册了就会出两个「项目文件夹」);
  • 原生两个 type(codex-project / codex-project-file)都是 page 类型(不声明 patterns),不与产品自带的 dsh-resource://file/** viewer 抢地址。读文件反而主动交给那个 viewer:openResource('dsh-resource://file/session/<id>/<绝对路径>'),宿主 workspaceFiles 的 read/readBytes/readAll/stat 不 confine 到会话根(只有 list confine),所以工作区外与跨盘文件都能原生预览;插件自有的 codex-project-file page 只承担宿主做不到的两件事——编辑写回(走插件 /write 的多根 fence)与无扩展名文件的下载;
  • fence 只改一处:复用 dirs-api.ts 的 fenceFor,不要另写一份 roots 推导。

宿主版本基线(0.1.7-rc.2)

项值
peer 范围@deepseek-ai/dsh-* 一律 ^0.1.7-rc.2
验证基线DSH 0.1.7-rc.2(2026-09-21 升级并真机验证通过)。pnpm install:packaged --profile web 走完整 10 项校验(含真启动探针)后,dsh web --no-open --port 0 实起一台服务:/codex-project/api/ping 返回 {"ok":true,"plugin":"dsh-codex-project"}、/dirs 返回本机真实多根工作区数据;插件 client bundle 经宿主实际供给(plugins/??@luoxunhao/dsh-codex-project/client.js 取回 1,615,527 字节,与本次构建产物逐字节同尺寸),且确认服务出去的那份里已是新图标名、无残留 …16;__DSH_BOOT__ 清单里 @luoxunhao/dsh-codex-project 在册、rev 有效、inject: ["@deepseek-ai/dsh-client-ui-slots"] 边完好;--dump-config 里补丁层正确合成(codex-project + codex-project-fs 两行插入、核心 fs-sandbox 被禁)。pnpm typecheck / pnpm test 246 用例 / pnpm build(含 client 纯度门)全绿。本轮未覆盖:浏览器面板未开,故「项目文件夹」tab 的视觉呈现与点击交互、自有 page 编辑写回、「引用到对话」、/adddir、add_dir 工具、从 npm registry 下载那条安装路径(registry 上仍是 0.11.0),均由既有 jsdom 用例守着而非本轮真机复验
0.1.7 升级改动两处 API 断裂,均为语义变化而非简单改名:① 消息来源不再有通用 'plugin' kind——MessageSource 改成按生产者各自声明 kind 的可合并联合类型(MessageSourceMap 模块增强),插件改为在 src/context-injection.ts 里 declare module '@deepseek-ai/dsh-llm' 声明自有 kind 'codex-project',并带 form: 'catalog'(目录随变更重发布,正是该提醒的契约);去重判断同步改用 SOURCE_KIND,若只改类型不改判断会静默失去去重、每步重复注入。② dsh-client-ui-primitives 的 16px 图标改名:IconXxx16 → IconXxxRegular / IconXxxMedium,两者尺寸同为 16px,只差描边(Regular=1、Medium=1.3);旧名未显式传 strokeWidth,走 SVG 默认 1,故按 Regular 对应(取 Medium 会让全线图标变粗)。另:primitives 0.1.7 的 bundle 新增 @deepseek-ai/dsh-client-store / dsh-util-code-language / dsh-util-workspace-path 三个裸 import,dsh-client-store 又需要 zustand / immer——按"消费者提升进 devDependencies"的既有规则补齐,否则 browser 用例在 resolve 阶段就挂
「编辑工作区」弹窗 + primary2026-09-21 在 0.1.6-alpha.2 真机验证通过(dsh web --no-open --port 0 + link 本地构建,用真实 pigo 工作区):菜单注入项显示「编辑工作区」;弹窗截图核对过视觉(源文件夹分组、灰底圆角行、蓝色「主要」药丸、行尾「设为主要」/「×」);真点击设主要 → PUT → dirs.db 落库 → 重读行序翻转,交回锚点即清除标记;「项目文件夹」tab 根行刷新后跟随 primary;GET /project 带出 primary、非 dirs 成员的 primary → 400 且不改写记录;用户既有 dirs.db 被 ALTER TABLE 真实升级(测后与快照逐字节一致)。本轮真机还揪出两个静态渲染看不到的坑:① 宿主浅色主题下 --dsw-alias-bg-layer-1/2/base 全是 #fff,靠 layer token 做的行底色/选择器框是白底白块,改成 --dsh-cxp-fill(按 label 色 color-mix);② 弹窗 portal 在宿主 border-box reset 之外,无按钮的「主要」行比别的行矮 6px,补了 min-height。未覆盖:深色/皮肤主题下的观感;真机上「走选择器添加一个新目录」的完整写入(会改用户配置,只验到选择器打开与返回);发布版安装路径
打包产物安装(0.14.0)2026-09-21 覆盖:pnpm pack 出 luoxunhao-dsh-codex-project-0.14.0.tgz → dsh plugin --profile web add <绝对路径>.tgz → dsh web --no-open --port 0。profile 依赖由 link: 变成 file:…tgz,node_modules/@luoxunhao/dsh-codex-project 是解出来的真实目录(lib/client.js 与本次构建产物同尺寸、且不带 client.js.map),bundles 行被 add 自动补回。真机验到:宿主正常 boot、右侧栏「项目文件夹」tab 在册并可开、5 个工作区根行来自本机既有配置、展开根走插件自有多根路由拿到目录(含中文文件名)、点文件交给宿主自带 viewer 渲染 markdown(预览/编辑切换在位)、控制台零报错。未覆盖:这一轮的自有 page 编辑写回、「引用到对话」、/adddir、add_dir 工具;从 npm registry 下载(registry 上只有 0.11.0);桌面 profile 的 vendor tarball 路径。两个坑见 AGENTS §3 第 7 条(link 残留导致「装了等于没装」与 ERR_PNPM_EPERM)
0.1.7 适配(0.15.1)2026-09-21 完成:把 peer/dev 依赖从 ^0.1.6-alpha.2 提到 ^0.1.7-rc.2,按上面「0.1.7 升级改动」修掉两处 API 断裂,并补齐 primitives 0.1.7 新增裸 import 的 devDependencies。真机验证:pnpm install:packaged --profile web 10 项校验全过(含真启动探针)→ dsh web --no-open --port 0,/codex-project/api/ping 200、插件 client bundle 经宿主供给 1,615,527 字节且内容是新的(有 …Regular、无残留 …16)。过程中顺带修掉脚本自身一个 Windows 缺陷(preflight 因 tar 输出 CRLF 恒判"缺文件",见上表 tar 解析一行)。未覆盖:浏览器面板未开,tab 视觉与点击交互、编辑写回、「引用到对话」、/adddir、add_dir、npm registry 下载路径
浏览器自动化验收(0.15.1)2026-09-21 真机通过:23/23 项。用 Playwright 驱动真实 Chrome(channel="chrome",系统已装,无需下载浏览器)跑 scripts/browser_probe.py <url> <outDir>,针对实起的 dsh web 逐项断言,脚本已接成 pnpm browser:verify。这也补齐了上一行"未开浏览器面板"的欠账:① 插件 client 进 __DSH_BOOT__,inject 边只有 ui-slots;② 原生右侧栏出现「项目文件夹」tab 并可打开;③ 多根树渲染出 dsh-codex-project (主) + deepseek-harness 两个根,走插件自有 /project 路由;④ 展开主根走插件自有 /list 路由,列出 26 行真实目录;⑤ 点文件经 openResource 交给宿主自带 viewer(新开 tab、DOM 里出现 dsh-resource:// 地址);⑥ 右键菜单按目标类型区分——目录 ['引用到对话','上传到此处','用文件管理器打开','复制相对路径','复制绝对路径']、文件 ['引用到对话','下载','编辑','复制相对路径','复制绝对路径'];⑦ 「编辑」开插件自有 page、CodeMirror 挂载且 hidden 为 false(60 行)、干净时「保存」disabled、改动后转可点、保存真的 POST /codex-project/api/write(写回落到磁盘:探针追加的文本随后确实出现在该文件里);⑧ 「引用到对话」真的把 chip 注进会话输入框(AGENTS.md)——即合成 { get, sessions } 那条在 0.1.7 真宿主上生效。全程 0 个 page exception、0 条 console error。仓库同时保留 scripts/browser_probe.py 与其依赖的 DOM 事实(右侧栏默认折叠、工作区"新建会话"按钮需 hover 才出现、.dsh-cxp-tree-row 行、菜单 [role="menu"])。仍未覆盖:/adddir 与 add_dir 工具、npm registry 下载安装路径、深色/皮肤主题观感
安装脚本 tar 解析(Windows)2026-09-21 升级时发现并修复:scripts/install-packaged.mjs 的 preflight 直接 split('\n') 比较 tar 清单,而 Windows 的 tar 输出是 CRLF,每个条目尾部带 \r,于是 listing.includes('package/cordis.patch.yml') 恒为 false —— 一个完全正常的 tgz 会被判成"缺 manifest/patch/bundle"而拒绝安装。已在 tarOut() 统一把 \r\n 归一为 \n。这条与 DSH 版本无关,是脚本自身的 Windows 缺陷
@deepseek-ai/cordis^4.0.2(与 DSH vendor/cordis 同版)
依赖面修复(0.14.1)2026-09-21 真机定位并修复。0.14.0 打包产物装上后,该 profile 里每条对话都在第一次工具调用时失败:Cannot read properties of undefined (reading 'prepare'),UI 显示「本轮运行失败」、skill 调用 interrupted —— 而宿主启动、右侧栏 tab、文件预览全部正常,所以第一反应会以为是插件市场坏了。二分过程:装上有错 → 卸载即正常 → 用 --patch 只禁主行 codex-project 仍有错(锁定在 fs 提供者那一层,与主行无关)→ 手工删掉 profile 里那 6 份重复的 @deepseek-ai/dsh-* 副本立刻恢复。根因是把宿主自带的包挂在了 dependencies 上(跨实例模块级 Symbol 对不上),规则见 AGENTS §2。修后复验:dir 命令真的执行并贴回输出(1 轮 2 步、无报错,顺带证明 spawn 出去的 runner 仍能解析 windows-acl)、/codex-project/api/list 多根列表正常、越界路径仍被 fence 拒(is outside the project roots)、profile 里只剩 dsh-sandbox-windows-acl 一份是我们带的;pnpm typecheck / 242 用例 / install:packaged 8 项校验全过。仍未覆盖:自有 page 编辑写回、「引用到对话」、/adddir、add_dir 工具、npm registry 下载路径

三个必须知道的坑(升级时踩过,别再踩):

  1. semver 普通范围不匹配预发布版本。semver.satisfies('0.1.7-rc.2', '^0.1.6-alpha.2') 严格模式返回 false——peer 范围必须显式写成目标预发布线(现为 ^0.1.7-rc.2),否则装不上。
  2. dsh-client-ui-primitives(以及 dsh-client-store)不声明 dependencies,其 bundle 裸 import shiki / @shikijs/langs/* / anser / clsx / katex / mdast-util-* / micromark-* / diff / simple-icons,0.1.7 起再加 @deepseek-ai/dsh-client-store、dsh-util-code-language、dsh-util-workspace-path,而 dsh-client-store 自己又要 zustand(~4.4.7) 与 immer(^10.1.1)。这些必须由消费者提升进 devDependencies(本仓库已补齐),否则 vitest 的 browser 用例在 resolve 阶段直接 Failed to resolve import / Cannot find package。这组 devDependencies 不得回退;升 primitives 后先把它 bundle 的裸 import 全列一遍再对。
  3. 一个包只能有一条 Loader 行。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 + 锚定 + 失效根 + 项目解析(含 primary 存活/回落)+ 目录列表(排序/fence 403/跨盘根)+ 读/写/文件字节与下载 disposition
  • project-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)、树点击可读文件经 tab.actions.openResource 交给宿主 viewer(含盘符与中文/空格/# 的地址构造)、无扩展名文件与「编辑」经 tab.actions.openTab 走自有 page、navigation.params 的 path/mode 与 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 会因缺方法失败。