Files
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

34 KiB
Raw Permalink Blame History

工作日志 · 2026-09(第 25 片)

⚠️ 本目录日志已按【月】分片(2026-09-15 用户定,单文件 ≤50 KB):2026-09.md / 2026-09-下.md / … / 2026-09-下24.md 写入约定:一律 append 到当月最后一片;该片超 50 KB ⇒ 新建 2026-09-下N.md;⛔ 不要再按日新建 2026-09-DD.md。 覆盖来源:2026-09-15.md(续) 上一片:2026-09-下23.md 下一片:(无,本片为末片)


17:00–17:20 日志改「按月分片」+ 钩子「从未生效」真因闭环

用户两条指令

  1. 日志改按月分片(在我给的 A 维持 / B 按 50 KB 切 / C 按月分片 里选了 C)
  2. 已重启 ⇒ 做 UserPromptSubmit 探针定论

① 日志按月分片(8 个日文件 → 24 片,逐字节无损)

  • 迁移脚本:.workbuddy/tmp/shard-memory-by-month.py(幂等;先备份 + 校验 ⇒ 只在 ## 章节边界切 ⇒ 回读逐字节比对)
  • 结果:2026-09-08..15.md(1,101,184 B)→ 2026-09.md + 2026-09-下.md … -下23.md,24 片,各片 ≤49.9 KB
  • 独立复核:各片去头后按真实片序拼接,sha256 = d162d194… 与原件拼接完全一致(1,101,184 B,零丢失)
  • 每片头部带:系列指针(上一片 / 下一片) + 覆盖来源(原日文件)⇒ 旧引用 2026-09-12.md:1110 这类可按"覆盖来源"行定位
  • 备份:.workbuddy/tmp/mem-bak-20260915_171005/(8 个原日文件)
  • ⚠️ 踩到两个自己的校验 bug(都是排序):① sorted(glob) 是字典序 ⇒ -下10 排在 -下2 前;② 我的 key() 把 -下.md 排到了首片前。同一晚两次栽在"按名字排序"上 —— 片序必须显式定义(首片=0 / -下=1 / -下N=N)
  • ⚠️ 首版迁移脚本有个真会丢字节的 bug:各片 "\n".join() 会丢掉片与片之间那个换行 ⇒ 已补(末片外每片补一个 \n)

② 钩子 UserPromptSubmit「从未被调用」—— 真因是 -E,不是宿主不支持

排查链路(有两次误判,都靠更硬的判据纠正)

步 观察 判定
1 .workbuddy/stop-dialog-guard.log 不存在,全盘也搜不到 疑似"宿主不调用该事件"
2 但 lock-hook.log 正常 ⇒ 钩子机制本身能落盘 推翻上一步的笼统结论
3 对照两者安装形态:lock-guard-hook.py 无 flag;stop-dialog-guard.py 是 -S -E 差异点锁定
4 实测 -S -E ⇒ stdin.encoding = **gbk**、utf8_mode=0;-S(无 -E)⇒ utf-8、utf8_mode=1 -E 屏蔽了 PYTHONUTF8=1 / PYTHONIOENCODING=utf-8
5 中文 payload 在 -E 下:json.loads 抛 ValueError ⇒ 原代码 except ValueError: return 静默空转 —— 钩子一直在被调用,只是每次都悄悄退出

修法(3e170f9):脚本加固到与 flag / locale 解耦

  • _read_stdin_text():sys.stdin.buffer.read().decode('utf-8','replace')
  • _emit():sys.stdout.buffer.write(json.dumps(…, ensure_ascii=False).encode('utf-8'))(原 sys.stdout.write 在 cp936 下遇中文/⛔ 会 UnicodeEncodeError)
  • _entry_log():入口即留痕(parse-fail / 未进作用域 / 空 stdin 三种静默情形都记)—— 根治"日志缺失时无法区分『没被调用』与『被静默 return』"
  • WS_FALLBACK:host 未给 cwd 时按「脚本位置上溯三级」兜底
  • docstring 安装处加 ⛔ 不要给本脚本加 -E

实证:修复前 entry|event=(parse-fail) → 修复后 entry|event=UserPromptSubmit|cwd=E:\ProgramData\AIProject\ai1net-dsh-server|in_scope=True + invoked(user-prompt)|mode=probe;4 个用例均能区分并留痕

同款风险(未改,只报告):lock-guard-hook.py 当前可用(无 -E),但同样依赖环境里的 PYTHONUTF8=1 ⇒ 该变量一旦消失会静默 fail-open(守卫失效)

③ 记忆整编(含一次"整编差点丢内容"的教训)

  • MEMORY.md 已涨到 13,170 B(另发现有别的写入者加过一节),超 12 KB 额度
  • 改法:先抽 142 个"事实 token"作回扫基线 ⇒ 整编 ⇒ 回扫(缺 1 个,且只是排版片段)
  • 结构性去重:把它与根 CODEBUDDY.md §2 重复的「去查表」迁到 PLAYBOOK §11,MEMORY 只留指针
  • 新增两条铁律:日志按月分片约定 + Python 编码铁律(详版 PLAYBOOK §10)
  • 终值 12,255 B ≤ 12 KB ✓(备份:.workbuddy/tmp/MEMORY.md.bak-pre-shard-20260915)
  • 💡 本晚最有价值的元教训:整编记忆必须"先抽 token 再改写,改完回扫" —— 纯靠记忆判断"我没删东西"是不可信的(本晚 pkill 例证就被我压掉过,是回扫抓出来的)

提交 / 锁

  • 提交 3e170f9(脚本修复,1 文件)|镜像已推(md5 双端一致,600 root:root,备份 TS 20260915_170920)
  • 锁:--claim-exec "钩子留痕-1703" → 全程 → --release-exec(均按硬判据校验:不接管道查退出码 + 复读 OWNER 断言)

17:17–17:25 上抛「取舍筛」(用户明令)+ 钩子定论切 inject + 锁守卫加固

用户原话

「需要我确认的方案需要说明优点和缺点,现在没法判断,假如只有优点或只有缺点那不需要我判断」

⇒ 给的是一个上抛筛选器 + 呈现要求(此前只写"可感知差别",用户据此没法判断):

  • 筛选器:候选各写「优点 / 缺点」—— 某个只有优点(明显更优)或只有缺点 ⇒ 不需要用户判断,自己拍掉再陈述; 只有各有优有劣、客观标准分不出高下(真取舍)才上抛
  • 呈现:上抛时逐项列出优点与缺点(只写"差别在哪"不算)

全载体同步(严格按 dsh-change-workflow §5 的「跨载体规则必做全载体扫描」清单执行)

grep 命中 8 处,全部改齐(这是该 SOP 立起来的当天第一次真用,命中即全改,没漏):

载体 改动
dsh-feature-first 1.6.0 §5.1 骨架改为「每个候选写 优点|缺点 + 我的倾向」;§5.3 四条→五条铁律(新增铁律 5 = 取舍筛);§5.4 硬约束 七→八条、提问骨架加"必须写优缺点"、反模式 十一→十二条;§6 自检加第 13 条
项目根 CODEBUDDY.md §1 判据行加「上抛门槛 = 真取舍」;回话前自检第 ③ 问改为「候选之间是真取舍吗」;📌 口径行;排版指针行(八条 / 十二条)
用户级 ~/.workbuddy/MEMORY.md 跨项目指针同步
scripts/stop-dialog-guard.py REASON / CONTEXT 加「候选只有优点/只有缺点 ⇒ 自己拍掉、不要问」
dsh-decision-method 2.7.3 references/素材库-U 追加「修正 2」,写清筛选器 + 呈现两条
dsh-env-bootstrap 1.0.3 references/常驻规则-快照 由 CODEBUDDY.md 重生成(--check ✅ 关键规则齐备)
README.md 模块表 3 行版本号

钩子「探针定论」—— 结论:机制是通的,之前是我的编码 bug

用户这条消息触发了钩子,日志(.workbuddy/stop-dialog-guard.log)首次出现:

17:17:00  entry|event=UserPromptSubmit|cwd=e:\ProgramData\AI技能liyun-dsh-server|in_scope=True|stdin_len=501
17:17:00  invoked(user-prompt)|mode=probe|上轮收尾=征询句:False|…
  • 宿主确实在调用 UserPromptSubmit ⇒ 之前"全盘无日志"纯粹是 -E → cp936 → json.loads 失败 → 静默 return
  • in_scope=True(transcript 路径形如 \e-ProgramData-AI技能-aliyun-dsh-server\<uuid>.jsonl,含 aliyun-dsh-server)⇒ 作用域判据有效
  • 上轮末行是陈述句 ⇒ 正确判为"非征询" ⇒ 已按计划切 inject(echo inject > .workbuddy/stop-guard-mode),此后命中时会真注入自检上下文

锁守卫加固(按新规则自检 = 只有优点 ⇒ 自决,未上抛)

lock-guard-hook.py:走 sys.stdin.buffer 显式 UTF-8 + 解析失败留痕。 原来 except: return 是静默 fail-open —— 锁守卫会不再拦人却没有任何痕迹(比"拦错"更危险)。 实测:垃圾 payload ⇒ 退出码 0(仍放行,方向正确)且留下 payload-unparsable JSONDecodeError: Expecting value…;加 -S -E 也能解析(隐患消除)。

验收 / 提交 / 锁

  • 4 个技能 9 个文件两副本 md5 全一致(CR=0);镜像 8 个文件就位后 md5 双端一致(600 root:root,备份 TS 20260915_172214)
  • 七件套全绿;grep 优点/缺点 回扫只命中已改处;resident-rules.py --check ✅
  • ⚠️ shrink-guard 曾报 MEMORY.md 90 → 59 行(-34%) ⇒ 是我今晚的整编(系统要求 + 收编方向)。 已用备份 + 142-token 回扫确认无损;按脚本提示刷新基线,但刻意不加 --allow-shrink 白名单 —— 这文件正是当初被并行会话抹行才催生这条守卫,不能给它免检
  • 提交 0c88b4b(8 文件)|锁已释放(--release-exec + 目录断言)

本晚第三次踩同一个坑(必须记)

bash heredoc / -c 内联里的 \\ 会被 MSYS 改成 / 或吞掉 ⇒ 本轮版本 bump 脚本因此在 print 处崩掉、 只跑完第 1 个文件(env-bootstrap 漏 bump,靠核对才发现)。 ⇒ 凡含 Windows 路径 / 反斜杠的 Python 一律用 Write 落成 .py 再跑,别走 heredoc。

17:25–17:32 待确认项的候选必须「竖排成段」

用户原话

「每个需要我决策的问题的潜在解决方案 A B C 也按照段落式排版,别横着排列」

触发它的正是我自己的输出 —— 前两轮我把三个候选写成一行并列: 选项:A 维持现状 · B 按 50 KB 切分 · C 改成按月分片 ⇒ 这就是"横着排列"。

规则(补进 §5.3 铁律 5 的 ④)

  • 候选 A / B / C 各占一行(各自成段)
  • ⛔ 不许 A:… · B:… 一行横排
  • ⛔ 不许把候选做成表格的列(表格是横向对比,与"段落式"正相反)
  • 段内「优点…;缺点…」连写即可,不必每个字段再拆行 —— 否则 3 候选 × 3 行 = 9 行,会撞 §5.4 硬约束 3「每节 ≤7 行」

全载体同步(grep 命中 9 行 / 4 文件,全改齐)

载体 改动
dsh-feature-first 1.7.0 §5.1 骨架改竖排段落式(**A** —— 优点:…;缺点:… 各占一行)+ 加 📐 段;§5.3 铁律 5 加 ④;§5.4 硬约束 八→九条;提问骨架注明"候选各占一段";反模式 十二→十三条;§6 自检加第 14 条
项目根 CODEBUDDY.md §1 📌 行 + 排版指针行(九条 / 十三条)
用户级 ~/.workbuddy/MEMORY.md 跨项目指针
scripts/stop-dialog-guard.py REASON / CONTEXT 加"竖排成段"判据
dsh-decision-method 2.7.4 素材库-U 追加「修正 3」
dsh-env-bootstrap 1.0.4 常驻规则快照重生成(--check ✅)
README.md 模块表三行版本号

⚠️ 我犯并自查出的一处内容损失(值得单记)

编辑 素材库-U 时,我把「修正 2」整段当作 old_string,而 new_string 只写了「修正 3」 ⇒ 修正 2 被整段替换掉(差一点就静默丢失一条用户明令)。 发现靠的是收尾核对(grep -c "^- \*\*修正 2" 只有 1 条、且与 git HEAD 逐行比对)。 已从 git show HEAD:… 恢复,并逐字节 diff 确认与已提交版一致。

教训:Edit 是"替换"不是"追加" —— 要在既有条目之后追一条新条目时, old_string 必须包含旧条目、new_string 必须同时含新旧两条。 (同族:本晚已踩 3 次的「bash heredoc 吞反斜杠」。两者的共同点 = 工具语义与直觉不符时, 必须用"改完取证"兜住,而不是靠记住。)

验收 / 提交 / 锁

  • 4 技能 9 文件两副本 md5 全一致(CR=0);镜像 7 文件 md5 双端一致(600 root:root,备份 TS 20260915_172924)
  • 七件套全绿;grep 竖排成段 只命中已改的 4 个文件;resident-rules.py --check ✅
  • 提交 bb9ebe9(7 文件)|锁已释放(--release-exec + 目录断言)

调研:dsh 单机运行的官方开源构成(23:4x · 只读,未动任何文件)

触发:用户问「dsh 单机运行有哪些开源项目」+「有没有官方的开源项目」。

官方(deepseek-ai 组织) —— 结论:官方就是主力。

  • 主仓 deepseek-ai/deepseek-harness(TypeScript monorepo,MIT,"Everything is a Plugin"); 顶层 = apps/ packages/ native/ python/(Python SDK) docs/ benchmarks/ scripts/ patches/ snapshots/;版本已到 0.1.6-alpha.1(线上我们跑 0.1.5-rc.1)。
  • npm 上 @deepseek-ai/* 共 294 个包(分页枚举到 from=500 归零 ⇒ 该数完整)。 分布:client-ui-* 45 | session-* 25 | tool-* 24 | 沙箱/执行器 12 | client-* 12 | MCP/ACP/SDK 11 | web* 9 | 存储·设置 9 | host-* 9 | node-addon-* 8 | subagent* 8 | cordis+schemastery 8 | llm* 6 | api-* 6 | command* 4 | util-* 4 | skill* 3 | 其他 91 @deepseek-ai/dsh 自身直接依赖里就有 69 个官方包。
  • 官方维护的指南仓(非工具本体):deepseek-ai/awesome-deepseek-agent(22 篇接入指南)。
  • ⚠️ 许可证不一致:@deepseek-ai/dsh = MIT,但 @deepseek-ai/dsh-base = BSD-3-Clause ⇒ 开源导出对账时⛔ 别一律当 MIT。

官方七种单机形态(profile bundle):dsh(CLI)|dsh-web-app(浏览器界面)|dsh-headless(无宿主一次性)|dsh-sdk-minimal(最小独立 SDK)|dsh-sdk-app(stdio JSON-RPC)|dsh-acp-app(ACP 自动化)|dsh-base(所有 profile 的第一 patch 层)。

官方单机跑法:git clone …/deepseek-harness.git → pnpm install → pnpm run build → pnpm dsh web → 127.0.0.1:3080。

第三方生态(非官方,GitHub topic dsh-plugin 头部): nexu-io/open-design 96.3k | ruvnet/ruflo 72.5k | volcengine/OpenViking 37.4k | esengine/DeepSeek-Reasonix 35.6k | anywhere-labs/dsh-desktop 26.8k | awesome-dsh-plugin/awesome-dsh-plugin 15.8k(收录 3,627 条插件) | walkinglabs/learn-harness-engineering 15.2k | EverMind-AI/EverOS 13k | MemTensor/MemOS 11.3k | YaoApp/yao 8k | zhu1090093659/dsh-web 7.6k。

判定:官方只出单机形态;多租户托管(门户 + bwrap + 独立 uid + scope + Manager/Worker)没有官方开源对应物 ⇒ 那部分是我们自造的增量。 可复用取证命令:registry.npmjs.org/-/v1/search?text=@deepseek-ai/&size=250&from=N 分页 + 前缀过滤(⚠️ npm 的 scope: 语法不被支持,会退化成全文搜 ⇒ 必须自己按前缀过滤);registry.npmjs.org/@deepseek-ai%2Fdsh/latest 取官方依赖全集。


评估:能不能「部署在客户端 / 局域网多人访问」(00:0x · 只读代码,未改动)

结论:⚠️ 取决于客户机是不是 Linux。 Linux ⇒ 可行(隔离层原样复用);Windows ⇒ 平台能起但租户隔离整套失效,多人访问不可接受。

🔴 以上判定有误,已被用户当场纠正两次,结论以本节末尾「§ 更正」为准(错误原因:只看了我们平台自己的代码,没查 dsh 本体的 Windows 支持)。

代码级事实(本轮实测,后续复用):

  • DSHS_ISOLATION_MODE 只有 soft / account 两档,默认 soft(src/config.ts:180);cli.ts:39 自己写着 "account" … (Linux, needs root)。
  • soft 分支 = 裸 spawn(command, args)(orchestrator.ts:663)⇒ 零 Linux 依赖、任何系统能跑,但零隔离。
  • account 分支 = systemd-run --scope -p Memory… -- bwrap … -- setpriv --reuid …(orchestrator.ts:853-861)⇒ 三件套全是 Linux 专属。
  • portGuard(nft 防火墙)默认 false(DSHS_PORT_GUARD,config.ts:323);非 Linux 上启用会 fail loud(firewall.ts:42)⇒ 不启用即可跨平台。
  • dataRoot 默认 ~/.dshs(不是硬编码 /var/lib);dbPath = SQLite,dbUrl = PG 可选 ⇒ 存储层跨平台。
  • 子域路由 = DSHS_BASE_DOMAIN + Host 头(subdomainForUser,routes/dsh.ts:59)⇒ 域名不硬编码,可换内网域名。
  • --secure-cookies 是可选开关(默认关)⇒ 局域网 http 也能登录。
  • ⚠️ 平台侧 Windows 隔离代码 = 0 行(全仓 grep win32|windows 只命中 fs/workspace.ts 一句注释)。
  • ✅ 上游 dsh 本体本身支持 Windows:dsh-pwsh-local / dsh-pwsh-sandbox / dsh-tool-pwsh / dsh-sandbox-windows-acl(restricted-token 写限制)/ dsh-win32-process(Job Object 资源控制)⇒ "Windows 上做隔离"有原料,但要新写后端(独立账户 + 文件 ACL + Job Object),替代 bwrap/uid/cgroup。

网络拓扑(好消息):实例只监听 127.0.0.1,局域网只需暴露平台一个端口,由平台按 Host 头分流 ⇒ 不需要给每个实例开端口。

要落地的三块:① 入口(内网 DNS 泛解析 + 泛域名证书,替代 CF/LE)② 安装(现在是一堆 .sh + 宝塔 + 手工 provisioning ⇒ 要收敛成一键包)③ 隔离(Linux 复用;Windows 需新写后端)。


§ 更正:Windows 上 dsh 原生可用,连 Linux 都不用套(2026-09-16 00:4x)

触发:用户连纠两次 —— ①「软件里面套个 linux 环境不就行了,官方原生的 dsh 也能在客户端部署运行」②「linux 环境都不用套,根本不需要」。用户是对的,上面那份判定作废。

官方证据(全部 Status: implemented,取自 deepseek-ai/deepseek-harness 的 master 分支 — 注意该仓默认分支是 master 不是 main):

  1. Windows 默认走 PowerShell 栈(.agents/notes/archived/feature/2026-08-01-windows-pwsh-default.zh.md)原文:

    启动交付 profile(dsh web、dsh --profile headless、一次性任务)的 Windows 主机默认获得 PowerShell 栈;POSIX 主机不变。 受限 pwsh 栈运行在 ACL 受限令牌 runner 之上,权限面与 POSIX 完全一致。 机制:同一份 base patch 按 process.platform === 'win32' 门控两套 shell 栈(bash-sandbox/tool-bash 在 win32 disabled,孪生行 pwsh-sandbox/tool-pwsh 仅在 win32 挂载)⇒ 每个宿主恰好挂一套。

  2. TUI 三平台对等支持,且官方明确否决 WSL/Cygwin(2026-07-20-windows-tui-support.zh.md)原文:

    DeepSeek Harness 不增加平台拒绝逻辑,也不采用功能受限的 Windows 模式。 其「曾考虑的替代方案」里写死:「通过 MSYS、Cygwin 或 WSL 运行 POSIX 驱动:不予采纳,因为这会测试兼容环境,而不是用户实际运行的原生 Windows 控制台路径。」

  3. 原生 Windows CI 是阻断性门禁(2026-08-08-native-windows-pull-request-ci.zh.md):每个 PR 在组织自有 dsh-windows-2025-16core 运行器跑 4 个原生作业,其中 windows-build 与 windows-native-tests 是 all checks passed 的依赖项。

Windows 原生能力面(与 POSIX 对等):

面 Windows 实现
shell 执行器 dsh-pwsh-local + dsh-pwsh-sandbox;工具 dsh-tool-pwsh / -persistent
沙箱 dsh-sandbox-windows-acl —— 受限令牌 + 写限制 SID,任何 Win32 失败都阻止子进程在不受限下启动(fail closed)
进程/资源 dsh-win32-process —— Win32 进程、stdio、Job Object 原语
终端 / 文件 ConPTY(测试用 node-pty);dsh-fs-local/src/win32.ts
其他 Python SDK 有 Windows x64 运行时;桌面端有 Windows 签名 + 冒烟脚本

⚠️ 官方自认的限制(要记住):sandbox-windows-acl 的 README 原文说保证是**「部分强制(partial)」**——

进程启动会保留 Everyone 访问权限,NTFS 硬链接也可以通过其他路径暴露同一文件 WRITE_RESTRICTED 只交叉检查写访问,需要读侧隔离或网络限制时,请把此后端与读侧策略或 AppContainer 能力令牌配对。

⇒ Windows 沙箱是写限制;跨租户读隔离要另配(AppContainer / 独立账户 + NTFS ACL)。但这与 Linux 无关 —— Windows 自带对应机制,不需要 WSL/容器。

⚠️ 本机安全策略(未来复用别踩):wsl.exe 在 Program Blacklist 里 —— bash 调用直接 Permission denied + 「PROGRAM BLOCKED BY SECURITY POLICY」,且明确禁止换 shell/脚本绕行。⇒ 验证 Windows 原生能力不要走 wsl.exe。

修正后的判定:dsh 本体在 Windows 原生可用、与 Linux 对等(官方原话"权限面与 POSIX 完全一致"),不需要套 Linux。 我们平台 soft 模式纯 Node 也能起;多租户隔离在 Windows 上走 Windows 自己的机制,平台侧这块目前 0 行 —— 是「要做」,不是「做不到」。


📄 产出:dsh客户端化部署方案_20260916.md(00:5x · 规划稿 · 工作区根)

路径:E:\ProgramData\AIProject\ai1net-dsh-server\dsh客户端化部署方案_20260916.md(与 集群化改造方案_Manager-Worker_20260914.md 同级) 性质:只做规划,未动任何代码 / 未改任何服务器(方案与执行分离)。⛔ 未写进文档库 —— 规划会话对 04-调整方案/ 只读,且这样可避开抢全局执行锁。

本轮新增的关键事实(前两轮没有的):

  • ✅ 平台入口支持"子路径"分流:src/supervisor/proxy.ts:546 有 legacy 代理 /u/:slug/dsh/*(已认证);DEFAULT_BASE_DOMAIN = '' 时 parseSubdomain/subdomainForUser 返回 null ⇒ 纯 IP 访问形态不需要改任何路由代码(✅ 修正了上一轮"换 IP 要动核心路由"的判断)。
  • ✅ 平台控制面天然跨平台:dataRoot 默认 ~/.dshs(非硬编码)、SQLite 默认、--secure-cookies 可选、portGuard 默认 false ⇒ 客户端化的代码改动量 ≈ 0,工作量全在「入口配置 + 安装打包 + (可选)Windows 隔离开发」。
  • ✅ 已有容器资产:Dockerfile(控制面 非 root uid 65532 + drop ALL + restricted PSA 友好 ⇒ Docker Desktop 能跑)、Dockerfile.dsh(每用户镜像)、deploy/*.yaml ×11。⚠️ K8s 后端代码 2026-09-15 已从仓库下线(备份 D:\github\_dsh_shenxian_K8s后端备份_20260915\)。

方案空间(4 案 + 对比表):A Linux 宿主(零改动)|B1 Windows+WSL2(隔离零改动)|B2 Windows 原生(要开发隔离后端,交付体感最好)|C 容器(要恢复 K8s 后端)。 待决三项(缺客户环境信息,非技术优劣):D1 客户机器系统与配置 | D2 隔离要"互不可见"还是"各有工作区" | D3 入口用域名还是纯 IP。

⚠️ 一条必须说清的区分(易被误读):官方否决 WSL,指的是"不让 dsh 的 TUI 走 WSL 的 POSIX 路径";不等于禁止把整个平台部署在 WSL 里 —— 在 WSL 里平台看到的就是一台正常 Linux 机器。

⚠️ 写作坑(本轮踩,与上一轮同族):Edit 的 old_string = 旧内容、new_string = 旧+新(追加时)。本轮把两者写反 ⇒ 报 String to replace not found。追加前一律先 Read 末尾取准锚点。


✅ 客户端化选型定稿 + B2 路实测(01:0x)

用户拍板:1、B2 2、乙 3、乙 ⇒ 宿主 = Windows 原生 | 隔离 = 不启用(各有工作区,小团队互信)| 入口 = 纯内网 IP + 子路径。 文档已从 v1 规划稿改写为 v2 定稿(同一路径),核心从"4 案对比"收敛为"B2 单一路径 + 落地清单"。

🔴 本机实测到的唯一硬阻塞(可复现,Windows + Node 22.22.2):

尝试 结果
spawn('npm', ['--version']) ← 平台当前就是这么做(orchestrator.ts:663-665) ❌ ENOENT
spawn('…\\npm.cmd', […]) 显式 .cmd ❌ THROW EINVAL(Node 安全限制:.cmd 必须带 shell)
spawn('cmd.exe', ['/c','npm','--version']) ✅ exit=0
spawn(p, { shell: true }) ✅ exit=0

⇒ B2 路唯一必须改代码的地方:Windows 上实例启动要走 cmd.exe /c 或 shell: true。

✅ 实测确认"不用改"的部分:

  • typeof process.getuid === 'undefined'(Windows)⇒ local-user-fs.ts:40 的 chown/chmod 整块自动跳过。
  • --host 只从命令行读、无 env(config.ts:269)⇒ 服务化时把 --host 0.0.0.0 写进启动参数即可。
  • orchestrator.ts:503 的 chownSync 是无条件调用,但在 try/catch 内 ⇒ 只刷日志、不阻塞(建议顺手加平台判断)。

交付口径:B2 路 ≈ 1 处小改(几行) + 配置 + 打包;最大风险是 R1「用户之间可互读文件」(选"乙"的必然代价,须书面告知客户,仅适合互信小团队)。


🔄 范围再次收窄 → v3:单机自用(07:0x)

用户两条指令(连续):

  1. 「客户端部署考虑用户自己使用就可以了,不用考虑当作服务器、其他人访问什么的」
  2. 「机制都保留,只是不使用,因为多租户没有域名跑不起来」

⇒ v2 的"内网多用户"整块作废;文档已改写为 v3 单机自用(同一路径,v1/v2 要点存进附录)。

v3 的核心(两条原则):

  • ① 使用者只有他自己 ⇒ 监听回环(默认就是 127.0.0.1,零配置)、单账号、不做分流、不做隔离、不做服务化。
  • ② 平台机制全部保留、只是不启用 —— ⛔ 不为客户端化删改多租户相关代码(多租户/门户/账号/隔离档位/配额/集群 一行不动)。用户的依据 =「多租户需要域名才能跑起来,客户端没有域名」。

⚠️ 一条应记下的事实(与用户判断有出入,但不影响结论**):平台内置了无域名降级路径** —— src/web/routes/dsh.ts:58-62 dshUrl():baseDomain 为空时 subdomainForUser 返回 null ⇒ 入口自动回落 /u/<userId>/dsh/ 子路径。 即"没域名跑不起来"在代码层面有兜底;但客户端单人场景不需要依赖它,故不在方案里改变结论,只作为事实记入文档附录 B。

v3 的改造清单(只剩两条必做):

  • C1:Windows 子进程启动适配(唯一必须改代码处,实测见上一节)
  • C2:安装包 / 一键安装脚本 (该做:启动器 / 容量参数重算 / 备份清理 Windows 实现;可选:开机自启 / 免登录 / 修两处小点)

v3 消失的风险:用户互读文件、无 HTTPS、局域网暴露。新增须知悉:Windows 上实例沙箱强度低于 Linux(官方标注 partial)。


🔍 调研:能否沿用官方 Electron 桌面版「把项目装进去」(07:1x)→ 结论写进文档 §13 附录 D

用户问:「看能否沿用官方桌面客户端方案,把这个项目装进去」。

官方桌面版事实(apps/desktop + apps/desktop-host,均 private: true,版本 0.1.6-alpha.1):

  • @deepseek-ai/dsh-desktop = Electron 壳;@deepseek-ai/dsh-desktop-host = "Private Node-mode host process"(依赖里含 dsh-host-webserver)。
  • 不开监听端口,用"分帧字节管道"承载 Fetch 与流式响应;dsh-app:// 服务客户端资源。
  • Electron 独占 $DSH_HOME/profiles/desktop;CLI 无法启动或修改该 profile。
  • profile 的 dependencies 只放外部插件;有独立插件管理窗口(增删改查);用自带 pnpm。
  • 与 CLI 共享 $DSH_HOME 的 会话/设置/凭据/工作区/存储。
  • ⚠️ GitHub Releases 有 5 个 tag,但 assets 全为 0 ⇒ 官方未发布可下载安装包(要自己用 package:win:x64 打)。

❌ 装不进去的三条硬理由:

  1. 载体只接受 dsh 插件 —— 我们平台是独立 HTTP 服务(路由 + DB + 子进程编排),无"作为 profile 插件加载"的形态。
  2. 桌面版无 web server —— 官方 Known limitations 原文:「The Web "Open In..." action is disabled in Desktop because its host plugin requires HTTP routes; Desktop does not provide a webServer」。而我们的 business-plugins 整个是平台的前端:数据全来自 portalHost() + /api/...(/api/plugins/mine、/api/dsh/status、/api/skills/mine、/api/me/keys,见 poc/business-plugins/lib/client.js)⇒ 装进去就是空壳。
  3. 两套编排者争同一份 $DSH_HOME —— 桌面版自己在跑 dsh,平台还要每用户再起实例。 (另注:business-plugins 的 host 侧 apply(_ctx) 是空实现,能力全在 client 侧 ⇒ 进一步坐实"它是平台前端"。)

✅ 可以沿用的:形态(Electron 桌面应用)+ 打包链(官方 apps/desktop/scripts/ 的 electron-builder / NSIS / Windows 签名 / 冒烟脚本)+ 数据互通(指向同一 $DSH_HOME)。 可行做法 = 自建薄 Electron 壳(不复用官方壳代码):启动平台 → 等 127.0.0.1:3080 就绪 → 窗口加载 → 退出收子进程。 三档形态:① 快捷方式 + 浏览器(最小)② 薄 Electron 壳(中)③ Fork 官方桌面版(大,⛔ 不建议 —— 官方设计方向与"承载平台服务"相反)。


📄 产出:dsh桌面客户端_开发方案_20260916.md(08:0x · 规划稿 · 工作区根)

用户拍板:「肯定是要基于官方的壳进行迭代,规划一套方案进行开发,看是否需要单独一个代码仓库」⇒ 选了第三档(fork 官方壳),与上一轮我的建议相反 —— 按用户决定执行规划,不再争辩,但把代价写清。

用户问的"是否要独立仓库" → 判定:✅ 需要(命名建议 dsh-desktop)。五条理由:发布物与节奏不同 | 上游要持续同步 | R2 边界(桌面壳是官方源码衍生品,混进平台仓库会让归属与合规边界模糊)| 依赖形态差异大(拖慢平台 CI)| 安全信任模型不同(用户机 vs 服务器)。

🔑 可行性硬证据(决定"能不能独立"):

  • apps/desktop 的 dependencies 只有 2 个且全是公开包(electron-updater、semver)。
  • devDependencies 16 个中只有 1 个 monorepo 内部引用:@deepseek-ai/dsh-home-paths: workspace:^,而它已在 npm 发布(0.0.1-rc.3)。
  • ⇒ 独立仓库只需把那 1 处 workspace:^ 换成 npm 版本,其余全公开包。
  • ⚠️ 但 @deepseek-ai/dsh-desktop / dsh-desktop-host 是 private: true、未发 npm ⇒ 必须 fork 源码,不能直接依赖。

官方壳结构(已摸清):apps/desktop = 100 文件(src/ 主进程 + renderer/ 启动页与插件管理 + scripts/ 打包链 + 大量 tests);apps/desktop-host = 仅 6 文件。 改造策略 = 取骨架与打包链,换内核:

  • ✅ 保留:single-instance / locale / ipc / paths / preload* / update-coordinator / startup-document / startup-error / renderer/startup.* / 全套 Windows 打包链(electron-builder.config.mjs + package-target.ts + windows-sign.mjs + installer.nsh + smoke-windows.ps1 等)
  • 🔧 改:main.ts(改加载目标与启动对象)、backend-controller.ts(换内核 → 管理平台进程)
  • ❌ 删:host-process / host-protocol(走 HTTP 不用管道)、project-manager / profile-packages / runtime-tree / core-package-set / owned-directory / release、renderer/plugin-manager.*、scripts/prepare-*.ts、整个 apps/desktop-host/**
  • 🟡 重写:针对内置运行时的 tests

其他关键事实:

  • Windows 签名是外部依赖,从环境变量读(DSH_DESKTOP_WINDOWS_CER_FILE / SIGNTOOL / TOKEN_PIN / KEY_CONTAINER);✅ 官方支持 package:win:x64:unsigned ⇒ 开发内测不需要证书。
  • appId / productName 由 resolveDesktopAppId(env) 解析 ⇒ 配置项,不用改代码。
  • 同步机制建议 = upstream-baseline/ 快照 + patches/ 补丁清单(改动尽量放新增文件,减少与官方的冲突面);官方自称 developer preview、明写会有破坏性变更。
  • 交付含 平台侧配套 3 项(P1 Windows 子进程启动适配 ← 必做;P2 打包形态;P3 端口可配置)。
  • 里程碑 M0→M4,M0(最薄链路:壳 + spawn 平台 + 窗口加载)同时验证桌面改造与 P1。
  • 未决 3 项:D1 关闭窗口行为(退出 vs 托盘)|D2 与官方桌面版关系(独立目录 vs 共享)|D3 是否保留插件管理界面(删 vs 留)。

✅ 确认:多人形态保留,靠"配域名"启用,零改动(08:0x)

用户指令:「保留客户端部署 可以当作服务器 多人访问的机制,只要他配置域名就行,这个没问题把(对当前项目最小改动 能不动的就不动)」。

判定:没问题,而且一行代码都不用改 —— 平台现有配置项本身就是这个开关:

配置 形态 地址
域名留空(默认) 单机自用 http://127.0.0.1:3080
配了域名 多人访问 http://<用户名>.<域名>
  • 开关 = DSHS_BASE_DOMAIN + DSHS_COOKIE_DOMAIN(有 HTTPS 再加 DSHS_SECURE_COOKIES)—— 全是环境变量,src/config.ts:320-321 ✓
  • 配套(用户侧)= 内网 DNS 泛解析 *.<域名> → 本机
  • ✅ 不需要 HTTPS 也能跑:门户域与用户子域属于同一 site,Cookie 的 SameSite=Lax 足以支撑跨子域跳转(src/web/auth.ts:55-57 正是按"有没有 HTTPS"分这两档)
  • 隔离档位仍默认 soft ⇒ 多人时用户之间无隔离(与既定取舍一致;且 account 在 Windows 上本来不可用)

已写进两份文档:部署方案 §1.3 | 开发方案 §4.7(含一条防自堵的硬要求:⛔ 客户端 spawn 平台时不要清理或白名单化环境变量,否则"零改动切多人"会被自己堵死)。