文档库:目录改为编号制(01-规范/02-架构设计/03-数据库/04-调整方案/
05-交接单/06-ops/07-scripts/08-skills/09-archive),顶层散文件归入 01-规范/;
INDEX.md 与 docs-manifest.json 重刷(档案 146 篇);旧目录名引用全量对齐。
IM 线:src/im/**(SDK / hub / store / presence / ws / gateway-token)、
src/web/routes/im.ts、src/db/plugin-data/**、src/supervisor/plugin-assembly.ts
及对应 test/**。
插件线:poc/{im-agent-bridge,im-connection-gateway,im-conversation-tabs,
business-plugins-im,carbon-mcp-probe}、src/web/routes/{sessions,overlay-device}.ts、
src/net/relay/{device-grant,instance-credential}.ts。
仓库卫生:清出 40 个历史误入库 / 已改名文件(34 个交接单归档 + 6 个旧结构,
本地均有副本);dsh-server-docs/.gitignore 补 tmp/;交接单不入库(政策)。
28 KiB
name: dsh-desktop-dev-shell
description: 在 Windows 本机把官方 dsh 跑起来并完成端到端取证 —— 覆盖两个形态:① web 实例(apps/cli 的 dsh CLI,起实例 / 查回环监听 / token+cookie jar 取 200 / 探测 Windows 沙箱后端)② 桌面壳(apps/desktop,需源码构建,且能挂自研 client 插件做真跑验证,含离线可用验证:黑洞服务造"挂起"、有界超时、如实状态)。⚠️ 作用域 = 本机源码/开发态(与 dsh-instance-diagnose 的边界:服务器上的用户实例属对方)。当出现「本机 dsh 实例起不来」「壳起不来」「plugin tree failed to load」「atomic-write: timed out waiting for the writer lock」「curl 实例页 404 / 401 / 303」「怀疑实例内工具被整体拒绝 / refusing to run the command unconfined」「Electron 一出窗口就 FATAL」「pnpm 卡在 added 一百多不动」「pnpm run 每次都重装」「dev:desktop 起不来」「装了插件但界面没变化」「启动器日志停在 [devkit] release= 不动」、**或「跑起来不是登录注册页 / 跟服务器版本不像一套」**时使用;也用于判断"壳能不能离线用 / 启动链有没有网络依赖",在有 safe-delete 护栏的机器上删除 node_modules 这类巨型树,或需要绕开 pnpm 直接构建这个 monorepo 的场景。
version: 1.0.0
updated_at: 2026-09-22
last_change: 【2026-09-22 按要求统一版本号】frontmatter version → 1.0.0(原 v2.0.1);正文与历史中的版本号为当时记录,未改动。此前 v2.0.1(2026-09-22):触发词加「本机」限定并写明与 dsh-instance-diagnose 的作用域边界 —— 实测「实例起不来」原本会撞(两技能都像命中,且偏向本技能),用户其实指服务器实例时不该加载这里。此前 v2.0.0(2026-09-21):并入原 dsh-windows-run-web-instance(该技能已随之删除,其内容现为 §11)—— 两形态共用一条判据链(Node 22 / 监听≠就绪 / token 栅栏 / 陈旧写锁)。
agent_created: true
dsh-desktop-dev-shell — 在 Windows 上跑起官方 dsh 并做真跑验证
0. 为什么存在
@deepseek-ai/dsh 的运行时可以 npx 直跑,但桌面壳(apps/desktop + apps/desktop-host)是 private: true、未发布 npm ⇒ 只能克隆源码构建。这条路在 Windows 上有一串看起来像"方案不行"、其实是环境的坑,逐条定位一次要花几小时。本技能把这些坑与处置固化下来(含 web 实例形态,见 §10)。
1. 两种形态:先选对入口
官方 dsh 在 Windows 本机有两种跑法,本技能两条都管。先判定你要哪一种,再跳到对应章节:
| 形态 | 入口 | 有没有 HTTP 端口 | 适用章节 |
|---|---|---|---|
| web 实例 | apps/cli 的 dsh --profile web |
✅ 有(回环 + token 栅栏) | §11 |
| 桌面壳 | apps/desktop(源码构建) |
❌ 不开监听端口(Electron 管道 + dsh-app://) |
§2–§10 |
两形态共用的判据链(无论走哪条都先过一遍):
- 必须 managed Node 22 —— 原生模块按 Node 22 编译,用 24 会
ERR_DLOPEN_FAILED。 - 监听 ≠ 就绪 —— 插件层晚于端口挂载,起听后立刻访问会拿到 404 / 拒连 ⇒ 轮询到成功响应再判。
- 陈旧写锁会让实例"永久起不来" —— 实例被中途掐断留下
<DSH_HOME>/.credentials.yaml.lock⇒ 之后同一DSH_HOME每次启动都exit 1(报plugin tree failed to load+atomic-write: timed out waiting for the writer lock)。 ✅ 处置 = 换全新DSH_HOME(立刻恢复)或启动前清掉该.lock。别改官方源码。 ⇒ 探针 / 守护脚本一律「每次全新 DSH_HOME + 就绪前不掐断」。 ELECTRON_RUN_AS_NODE=1可能就在环境里 ⇒ 不删掉的话 Electron 退化成纯 Node、不出 GUI(像"壳起不来")。
⚠️ 形态选错的典型症状:去
curl桌面壳的 HTTP 端口找页面 —— 壳压根不开端口(见 §2 第 1 条)。
2. 桌面壳:三条必须先知道的判断
- 壳不开监听端口。 官方 README 原文:"It opens no listening port" ⇒ Electron 主进程与内置 Node 子进程走带版本的定长帧管道,渲染层用
dsh-app://取资源。 ⇒ ⛔ 别去curl壳的 HTTP 端口找页面(没有);要验渲染层请用--remote-debugging-port。 - 开发态里
DSH_DESKTOP_DEV_PROJECT_DIR直接充当 profile。main.ts里activeProject = development ?? paths.profile⇒ 跳过 project-manager 的 staging / healthCheck / activate 事务。 ⇒ 想挂插件,自己造 profile 目录,别指望走事务。 - ⛔ 别用
grep -nE "https?://" <源码>判"有没有网络依赖" —— 它会结构性漏掉electron-updater: 它的 feed 地址在打包产物的app-update.yml里,源码里一个 URL 字面量都没有(本项目因此错下过一次结论)。 ✅ 正确口径(本线实测,逐处读码得来):壳的启动链只有三类碰网 —— ①main.ts启动后 10 s 自动checkAndPrompt(false)→electron-updater:三重可容忍 (enabled()要求app.isPackaged且resourcesPath/app-update.yml存在 ⇒ 开发态/未配 publish 的包零网络; 用void发起不阻塞窗口;全程 try/catch +manual=false⇒ 静默失败不弹窗) ②project-manager.ts的DESKTOP_REGISTRY='https://registry.npmjs.org/'⇒ 只在装/更新插件时(离线装不了插件) ③host-process.ts的async fetch(request)是进程内 fd 转发(壳↔host),⛔ 不走网络 ⇒ 结论:"启动 → 出窗口 → 实例页"这一段不依赖网络,可以做成离线可用。 ⚠️ 但装成正式包并配了app-update.yml之后,10 s 那次检查会真的发请求 ⇒ 届时应复测。 dev.ts每次启动都重写 profile 的package.json。 所以手工挂进去的插件会被抹掉。 ⇒ 要么每次重新挂,要么别用dev.ts(见 §4)。
3. 依赖安装:pnpm 在本机"卡在 added ~100"的真根因
症状:pnpm run <任意脚本> 都先跑一遍安装,然后卡在 added ~104 十几分钟不动(downloaded 0,包全在 store 里)。
根因(三条叠加,缺一不可):
- pnpm 的 before-run 安装钩子默认开启 ⇒ 每次
pnpm run都先做依赖校验,判定"需装"就真的装。 - 该次安装要按虚拟 store 重建已有的
node_modules;若.pnpm是上一轮失败安装留下的半成品,这一步在本机退化成每包数秒。 ⚠️ 不在网络、不在磁盘、不在 pnpm 版本(11.7 与 11.21 一样中招)—— 别在这三个方向浪费时间。 - 若上一轮用过非默认
virtual-store-dir,顶层符号链接会全指向那棵树,并留下.pnpm-workspace-state-v1.json记录该配置 ⇒ 任何不带该配置的pnpm run都被判"配置变了要重建" ⇒ 死循环。 🔴 附带后果:官方dev.ts的dependencyDir写死node_modules/.pnpm/node_modules⇒ 非默认虚拟 store 会让官方 dev 流程直接失效。
处置(三步):
# ① 把损坏/陈旧的树【改名移走】——⛔ 不是删除
mv node_modules/.pnpm <外部目录>/pnpm-old
mv node_modules/.pnpm-new <外部目录>/pnpm-new
mv node_modules <外部目录>/node_modules-old
# ② 空目录上做默认布局安装
pnpm install --frozen-lockfile --config.minimumReleaseAge=0
# ③ 此后所有 pnpm 调用都带这一条(⭐ 能透传到嵌套 pnpm run,无需 shim)
pnpm --config.verify-deps-before-run=false run <script>
- ⚠️ 两处必须解释清楚:
minimumReleaseAge(供应链校验)在 npmmirror 上会超时并报… entries failed verification⇒ 不是真实版本违规,必须--config.minimumReleaseAge=0。- 非标准
virtual-store-dir会让 pnpm 判"要 purge",无人值守下ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY。⛔ 别用CI=true绕过 —— 那会真的 purge。
- ⚠️
rm -rf node_modules会被环境的 safe-delete 护栏拦下(报SAFE_DELETE_BULK_CONFIRM_REQUIRED)⇒ "删除完成"是假象,你还在同一堆半成品上重试。⇒ 这就是为什么要用mv改名而不是删。 护栏机制全文 ⇒ §9(阈值、正规批量通道、Windows 上删 pnpm 树必失败的坑)。 - ✅
build:native-system在 Windows 上是空操作(native/system/07-scripts/build.ts:平台非 linux/darwin 且带--host-addon-only⇒ 直接exit(0))⇒ 不需要 MSVC / Python / Windows SDK。别被"打包前置要求"吓住。 - ⚠️ Electron 二进制不在 npm 包里:全新安装后
node_modules/.pnpm/electron@*/node_modules/electron/下没有dist/与path.txt(该包未列入allowBuilds⇒ pnpm 不跑它的脚本)⇒ 会看到 "Downloading Electron binary..." 然后卡死/失败。手动补齐:从 npmmirror 拉electron-v<ver>-win32-x64.zip⇒ 解压进dist/⇒ 写path.txt(内容就是electron.exe)。
4. 启动:不要用 pnpm run dev:desktop
两个理由:
dev:desktop=pnpm --filter … run dev⇒ 两层pnpm run= 两次 before-run 安装钩子。dev.ts每次重写 profile ⇒ 挂的插件被抹掉。
做法:自建一个启动器(放在工作区外或本工作区的 .workbuddy/ 下,⛔ 不进官方源码树),步骤:
- 复用官方
apps/desktop/07-scripts/development-project.ts的prepareDevelopmentProject(),但把projectDir指到自己的目录(如…/.desktop-build/development/project-adapt),与dev.ts的project/互不干扰。release需要:{ schemaVersion:1, version:<apps/desktop 版本>, hostProtocolVersion:<从 src/host-protocol.ts 读>, nodeVersion:process.versions.node, pnpmVersion:<apps/desktop/node_modules/pnpm 的版本> }- ⚠️ 该模块只依赖 node 内置 + 本地 ts,不牵连 electron,可以安全 import。
- 改 profile 的
package.json,把插件名 push 进dsh.profile.bundles(basis 的两项@deepseek-ai/dsh-base、@deepseek-ai/dsh-web-app必须在前)。 - 把插件包 junction 进 profile 的
node_modules/<scope>/<name>。 - 直接
spawn(electron, [...]),参数见 §5。require('electron')要用createRequire(<apps/desktop/package.json>)才解析得到。
4.1 🔴 prepareDevelopmentProject() 会偶发卡死在删目录(⛔ 不报错)
- 症状:启动器日志停在
[devkit] release=…不动,下一条[devkit] profile=…永不出现, 窗口起不来,且没有任何错误输出 ⇒ 极易被误判成"壳坏了 / 方案不行"。 - 判定(30 秒内可确认):
<projectDir>/node_modules已空,但同层元数据文件 (package.json/pnpm-workspace.yaml/desktop-release.json/desktop.cordis.yml)的 mtime 仍是上一次的 ⇒ 卡在removeOwnedPath()的rmSync(recursive)(development-project.ts里removeOwnedPath(options.projectDir)那一步),还没走到重建。 ⚠️ 这几个文件的 mtime 是最好用的判据:重建会刷新它们。 - 先排除"junction 遍历误删"(别慌着重装):看
apps/cli/apps/desktop-host的 mtime 有没有变新、node_modules/.pnpm/node_modules条目数有没有掉(正常 ≈ 820)⇒ 都没变 ⇒ 官方树未受损,只是删不动。 - ✅ 处置 = 移走,不删除:
陈旧树留给用户处置(Windows 上删这种树会卡,理由与 §9 同源)。
taskkill /F /PID <启动器 PID> # 先停掉卡住的那个 mv "<projectDir>" "<projectDir>.stale-<时间戳>" # mv 在同卷上近乎瞬时 # 重新启动 ⇒ prepareDevelopmentProject 走 ENOENT 快路径,秒级过 - ⚠️ 非必现:同条件下另一次重启(
projectDir存在)顺利通过。⇒ 按"偶发环境坑"记, ⛔ 不要因此去改官方development-project.ts(那是官方源码,本项目红线 R2)。
4.2 ⚠️ 改插件后必须重启壳
readClientBoot() 之类的读取与模块级常量都在加载时求值 ⇒ 没有热更新。
⛔ 杀壳只能按端口定位 PID:netstat -ano | grep LISTENING | grep 9333 → taskkill /F /T /PID <pid>。
⛔⛔ 绝不用 taskkill /IM electron.exe —— WorkBuddy 自己就是 Electron,会把自己杀掉。
5. Electron 启动参数(Windows 本机实测)
// ⛔ 必须先从 env 里删掉;本机环境常带着它
delete env.ELECTRON_RUN_AS_NODE
const args = [
`--inspect=127.0.0.1:${mainPort}`, // 主进程调试(默认 9229,本机可能被占)
`--remote-debugging-port=${rendererPort}`, // 渲染层 CDP(默认 9222,⛔ 本机常被别的进程占用)
'--no-sandbox', // 🔴 Windows 上以管理员身份运行时必需
`--user-data-dir=${userDataDir}`,
appRoot,
]
// env 还要给:DSH_HOME(默认 .desktop-build/development/home)
// DSH_DESKTOP_DEV_PROJECT_DIR(= 你的 profile 目录)
// DSH_DESKTOP_HOST_INSPECT_PORT、DSH_DESKTOP_NODE_BINARY
--no-sandbox 的判据(别误判成 GPU 问题):症状是 GPU 进程连崩 6 次后
FATAL: … gpu_data_manager_impl_private.cc: GPU process isn't usable. Goodbye.(退出码 2147483651)。
实测:单独 --disable-gpu 无效;加 --no-sandbox 后 GPU 报错 0 条 ⇒ GPU 本身没问题,sandbox 是单一根因。
⛔ 这是开发态在管理员会话里跑的权宜,不得带进正式分发包。
定位这类问题的通用手法:写一个最小 Electron 探针(package.json + 一个只创建 BrowserWindow 并 setTimeout 打印存活标志的 main.js),用 timeout 跑多组参数扫描,按"是否打印存活标志"判定。⛔ 别一上来就跑整个壳 —— 一轮几秒 vs 一轮几十秒。
6. 真跑验证(怎么拿到硬证据)
| 想验什么 | 怎么做 |
|---|---|
| 渲染层加载了什么 | curl http://127.0.0.1:<rendererPort>/json/list ⇒ 看 url / title |
| 插件是否真被装载 | CDP Runtime.evaluate 读 DOM:插件注入的 <style id> / data-* 属性是否在;typeof window.__ModuleLoader__ |
| 窗口能否窄到某个宽度 | 走主进程调试端口:Runtime.evaluate 执行 process.mainModule.require('electron').BrowserWindow.getAllWindows()[0].setSize(w,h) —— 这是真·窗口缩放,受 minWidth 约束 |
| 断点/响应式是否真生效 | 缩放前后各读一次 window.innerWidth + 你的断点属性;再 Page.captureScreenshot 抓两张图对比 |
三个"换路走"的坑:
- ⛔
Browser.getWindowForTarget在 Chromium 152 / Electron 44 里不存在(-32601)⇒ CDP 的 Browser 域改窗口走不通。 - ⛔ 主进程调试上下文里
require未定义 · 动态import()报ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING⇒process.mainModule.require('electron')是唯一入口。 - ⛔ 若本机安全策略拦
Add-Type("compiles and loads .NET code at runtime")或拦"从 Bash 调 PowerShell",Windows APIMoveWindow这条路就没了 ⇒ 只能走上面主进程调试器那条。
6.1 ⚠️ 选 CDP 目标:DevTools 自己也是 type:'page'
/json/list 里会有多个 page。⛔ 别取第一个 —— 按 t.url.startsWith('dsh-app:') 选,
否则你量的是 DevTools 而不是壳。(同理:壳的 scheme 在你的项目里可能是别的自定义协议。)
6.2 🔴 判据写窄 = 假红(会让你去改本来正确的产品行为)
实测两例:① 假定起始态干净 ⇒ 上一脚本留在打开状态的视图被当成产品缺陷
(修法 = 连接后先归一化前置态,并把归一化动作写进报告);
② 断言实例页"有 textarea/contenteditable" ⇒ 而实例页用的是 role="textbox"
(buttons=15 明明说明页面画出来了)。
✅ 规矩:判据写语义层("页面画出来了且不是空壳"),信号取全再判
(body.children.length / #root.innerHTML.length / #root.textContent / textarea / [contenteditable] / [role=textbox] / input / button)。
6.3 「离线可用」怎么验(⛔ 别用"没人监听的端口")
真断网的形态是连接挂起(黑洞地址 / 丢包),不是 ECONNREFUSED。
拿"连不上就秒拒"的端口去验离线是假验证 —— 它只证明了快速失败,而真正要防的是"永远不返回"。
✅ 做法:起一个黑洞服务(接受 TCP 连接、永不回话),把客户端指向它:
// blackhole-api.mjs —— 只有 net.createServer,连上来什么都不写
const server = createServer((socket) => { socket.on('error', () => {}) }) // ⛔ 不 write、不 end
server.listen(9414, '127.0.0.1')
判据三条(缺一不可):
- 有界:每一次对外调用都要在预算内给出结论(本线:探针 1 200 ms / 代理 6 000 ms / 客户端兜底 8 000 ms)。
- 如实:够不着就显示"离线/单机",⛔ 不许谎称"未登录"(那会诱导用户去点必然失败的入口)。
- 佐证:黑洞侧打印连接数与
still_open峰值 —— 峰值就是"挂起本身", 之后回落则证明超时真的在释放 socket(不是泄漏)。 ⭐ 这条比"壳里看到单机模式"更强:它同时证明壳真的去连了。
⚠️ 全盘端到端验证之外,先写一层"宿主半单测"(造最小 ctx 直接调路由 handler): 秒级、确定、能直接量到毫秒,且能把"离线"拆成几条独立事实分别定位。
6.4 ⚠️ 造假的 ctx 有个必踩的语义坑
Cordis 的 ctx.effect(fn) 是「立即执行 fn 并把返回的销毁函数登记起来」。
插件一般把 register(...) 放在 effect 回调里面 ⇒ 把 effect 写成 () => {}
会导致路由一条都没挂上,而插件日志照报 mounted=9(因为它数的是循环次数)。
✅ 正确的最小实现:
effect: (fn) => { const d = fn(); return typeof d === 'function' ? d : () => {} }
7. 其他本机环境坑(会误导判断)
- 前台 Bash 命令有 120s 上限,且
sleep不会被自动转后台 ⇒sleep 150会被直接杀掉、无任何输出(看起来像命令失败了,其实是超时)⇒ 长等待拆段或走后台。 - node 不认 MSYS 路径:
node /e/foo会解析成E:\e\foo⇒ 传参一律写E:/foo。 - 中文路径必须整体加引号。
- Windows git 不认 MSYS 路径:
git -C /d/repo报 "cannot change to" ⇒ 写D:/repo。 ELECTRON_RUN_AS_NODE=1可能就在环境里 ⇒ 不删掉的话 Electron 退化成纯 Node、不出 GUI(像"壳起不来")。- Bash 命令正文里出现某些敏感关键词会被直接拦掉(如正文含 "PowerShell" 触发"Invoking PowerShell from Bash")⇒ 想写含这类词的文档时,改用文件编辑工具落盘,别用
cat <<EOF拼。
8. 「壳里打开不是登录注册页」——先说清它不是 bug
如果你挂的是官方 desktop 壳 + 官方 bundle(@deepseek-ai/dsh-base + @deepseek-ai/dsh-web-app),首屏就是官方「内测声明」弹窗 + 空工作区。这不是装错了:
- 🔴 官方 dsh 单机版没有账号体系:官方
packages/client/src与apps/web/src对「登录 / 注册」0 命中。 - 🔴 登录 / 注册属于「控制面」那一层,不是实例层。以本机的
dshs平台为例:它是一个dsh:{plugin:true, kind:"server"}的独立 Fastify 服务(bin: dshs),页面在它自己的web/login.html·register.html·portal.html,路由在src/web/routes/auth.ts;而注入每个用户实例的只是零依赖的dshs/runtime(orchestrator 用dsh --patch注入,见其Dockerfile.dsh/cordis.patch.yml)。 - ⇒ 桌面壳对应的是"实例"那一层 ⇒ 只装官方 bundle 时,壳里天生不会有登录注册页。
- ✅ 判据:要确认"壳里装了什么",直接读 profile 的
package.json→dsh.profile.bundles(开发态 =DSH_DESKTOP_DEV_PROJECT_DIR指向的那个目录)。bundles 里没有控制面包 ⇒ 就别期待控制面页面。 - ⚠️ 想让它出现,只有三条路:① 把控制面包当 bundle 装进 profile(壳里跑"单机版平台")② 壳改成加载线上域名的浏览器外壳(要改官方源码)③ 接受现状。这三条都要用户拍板,⛔ 不要代为决定。
9. 环境 safe-delete 护栏(删 node_modules 前必读)
A. 机制
- 实现 = CLI 的 shim 链
safe-bin/{rm,unlink,rmdir}→safe-delete-common.sh→safe-delete-bulk-guard.cjs(bash 里rm已被函数覆盖)。 - ⭐ 阈值 = 环境变量
CODEBUDDY_SAFE_DELETE_BULK_THRESHOLD(代码默认 20,本机实测 50),且按"轮次"累积(key =CODEBUDDY_CONVERSATION_REQUEST_ID)⇒ 同一轮内多次rm会累加,很容易触顶。 - ⭐⭐ 正规批量通道 = 让 host 弹确认:
rm→ guard 打印[safe-delete][SAFE_DELETE_BULK_CONFIRM_REQUIRED] {…}退出码 2 → host 拦截并弹确认 → 同意后 host 调approveSafeDeleteBulkGuard(toolCallId)并重试同一条命令。 🔴 自己调…/safe-delete-bulk-guard.cjs approve= 自我批准 = 绕过护栏 ⇒ 禁止。 - ✅ 怎么确认"真被批准了":读
$CODEBUDDY_SAFE_DELETE_BULK_STATE_DIR/<sha256(sessionId)>/state.json,看toolApprovals[<toolCallId>].approved === true;命令行侧还会附⚠️ Sandbox bypassed (escalation-approved)—— 那句提示就是批准本身,不是被绕过。 - ✅ 豁免:目标在 OS 临时目录(
$TMPDIR/$TMP/$TEMP、/tmp)⇒ 跳过 guard,走真rm。 - ✅ 去向 = Windows 回收站(
genie-trash);可用$CODEBUDDY_SAFE_DELETE_REPORT_PATH逐条核对。
B. 🔴 Windows 上「回收站失败 = 不删」,且 pnpm 树必失败
- 症状:
genie-trash报Error during a 'trash' operation: Unknown { description: "Some operations were aborted" }→ shim 打印[SAFE_DELETE_FAIL_CLOSED] {"reason":"trash-failed"}→ 文件原样保留(退出码 1)。 - ⭐ 规模相关,已实测定位:42 文件 / 1 符号链接的小目录 → 成功进回收站;66k 文件 / 3778 符号链接的 pnpm store → 必中止(复现 3 次)。不是 MAX_PATH(最长 227 < 260)、不是磁盘满。
- 🔴🔴 ⛔ 不要"绕过":直接用真
rm(SAFE_DELETE_REAL_RM)·cmd /c rd /s /q· Shell COM 删 —— 三者都跳过回收站,等于手动关掉一个刻意 fail-closed 的控制(shim 源码明写:Windows 下 native helper 存在但失败必须 fail-closed,不得降级)。 - ⚠️
mv到$TEMP再rm只是"把硬删换个地方",一样不可恢复 ⇒ 不构成更安全的替代。 - ✅ 正确动作 = 停手 + 报告用户(给对象 / 大小 / 失败原因 / 可用命令),⛔ 不自行强删。
10. 纪律(本项目硬约束)
- ⛔ 不改官方源码:官方源码树 = 只读参照;自研代码与启动器放独立目录/独立仓,两者永不混树。
- ⛔ 不 commit / 不 push,除非用户明确要求。
- ⛔ 不碰用户正在用的库/工作台:开发态务必用独立的
DSH_HOME(默认.desktop-build/development/home已隔离);起第二个 host 时用新端口 + 隔离 profile。 - ⛔ 删锁/接管锁的处置权只属于用户:遇到陈旧锁(如
dsh-lefthook-install.lock)只报告、不删。 - ⛔ 改名优于删除:清理
node_modules这类大目录时用mv而不是rm,既绕开护栏也留后路。
11. web 实例形态(apps/cli 的 dsh CLI)
与桌面壳的区别:这一形态有 HTTP 端口,且实例页受 token 栅栏保护。共用判据见 §1 的判据链。
11.1 适用
官方源码工作树(如 E:\github\dsh-desktop-0.1.5rc1,版本 0.1.5-rc.1)内,用 apps/cli 的 dsh 把 web 档实例跑起来,并给出可被第三方复现的读数。
11.2 基线命令
# 必须用 managed Node(项目原生模块按 Node 22 编译;用 24 会 ERR_DLOPEN_FAILED)
NODE="E:/ProgramData/.workbuddy/binaries/node/versions/22.22.2-3/node.exe"
cd <开发树根>
DSH_HOME=<每次全新目录> "$NODE" apps/cli/lib/bin.js --profile web \
--host 127.0.0.1 --port <空闲口> --no-open
web是随包发的 profile(自动落到$DSH_HOME/profiles/web/,bundles: [dsh-base, dsh-web-app])。 ⛔ 别用--from-default-profile web—— 会被拒:profile "web" is shipped and cannot be a custom profile target。--port 0合法(OS 选空闲口),但端口值得从 stdout 解析。- 不要用
pnpm run dev:*(两层安装钩子 + 每次重写 profile 的package.json,理由见 §4)。
11.3 三条必踩的坑(顺序即排错顺序)
- 监听 ≠ 就绪:HTTP 端口先 listen,插件层随后才挂载 ⇒ 起听后立刻 curl 会拿到 404 / 连接被拒。
✅ 正确姿势 = 轮询到 200(
--host 127.0.0.1时 3~4 s 起听,路由就绪略晚)。 - 实例页不是裸 200,受 token 栅栏保护:
GET /无 token → 401,正文dsh web authentication required; reopen the URL printed by dsh web.- token 在启动 stdout 里:
dsh web: http://127.0.0.1:<port>/?token=<43 字符> GET /?token=不跟随 → 303(要 cookie jar)⇒ 必须:三条(curl -s -L --compressed -c jar -b jar -H "Accept: text/html" \ -o page.html -w "%{http_code}" "http://127.0.0.1:<port>/?token=<TOKEN>"-L/--compressed/ cookie jar)缺一会被挡。/index.html/app/login本就 404(实例侧无账号体系;登录页在平台控制面,不在实例 —— 理由见 §8)。
- 判据「实例内 bash 工具可用」不能靠启动日志判断:
confine()是每次工具调用才判定,不是启动探针;启动期无sandbox告警 ≠ 运行时可用。
11.4 沙箱后端的组件级探针(确定性、零凭据、零外发)
默认组成就挂了受限 bash(packages/bundle/base/cordis.patch.yml L205–247:sandbox=dsh-sandbox-local · sandbox-policy · bash-sandbox · pwsh-sandbox · tool-bash,档位 read-only(默认)/workspace-write/danger-full-access)。Windows 上挂 sandbox-local 即自动选 @deepseek-ai/dsh-sandbox-windows-acl(WRITE_RESTRICTED 受限令牌,依赖 koffi FFI)。
// 探针脚本放在开发树之外时,裸包名按「导入方所在目录」解析会失败 ⇒ 用绝对 file:// URL
const m = await import('file:///E:/github/<dev-tree>/packages/sandbox/sandbox-windows-acl/lib/index.js')
const s = new m.AclSandbox({ writableDirs:[ws], tempDir,
writeSid:m.workspaceWriteSid(ws), tempWriteSid:m.tempWriteSid(tempDir), mode:'workspace-write' })
await s.init() // 抛错 = runner 起不来(最坏分支:工具被整体拒绝)
// 期望:init OK;受限子进程 echo → exit 0;写 ws 内 → exit 0;写 ws 外 → EPERM
- ⚠️ 测写权限别用
cmd.exe拼重定向(argv 二次解析会假失败,报 GBK「文件名、目录名或卷标语法不正确。」);直接让受限子进程跑node -e "require('fs').writeFileSync(process.argv[1],'x')" <path>。 - 组件级通过只排除"后端起不来";实例级真跑仍需一次模型调用,取证时如实标注级别。
11.5 安全清理
- 回收实例用端口占有者 PID(
netstat -ano | grep ":<port> " | grep -i listening | awk '{print $NF}'再taskkill //PID <pid> //T //F);直接记后台 job 的$!在VAR=x cmd &形式下拿到的是子 shell PID,会漏杀。 - 探针用的 scratch
DSH_HOME是临时产物,收尾删掉(⚠️ 但先确认它不在node_modules那类巨型树里 —— 见 §9 护栏)。