2026-09-24 07:51:03 +08:00
# 工作日志 · 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`**
2026-10-10 23:13:22 +08:00
**实证** :修复前 `entry| event=(parse-fail)` → 修复后 `entry| event=UserPromptSubmit| cwd=E:\ProgramData\AIProject\ai1net-dsh-server| in_scope=True` + `invoked(user-prompt)| mode=probe` ; 4 个用例均能区分并留痕
2026-09-24 07:51:03 +08:00
**同款风险(未改,只报告)** : `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 · 规划稿 · 工作区根)
2026-10-10 23:13:22 +08:00
**路径** : `E:\ProgramData\AIProject\ai1net-dsh-server\dsh客户端化部署方案_20260916.md` (与 `集群化改造方案_Manager-Worker_20260914.md` 同级)
2026-09-24 07:51:03 +08:00
**性质** :**只做规划,未动任何代码 / 未改任何服务器**(方案与执行分离)。⛔ **未写进文档库** —— 规划会话对 `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 平台时不要清理或白名单化环境变量** ,否则"零改动切多人"会被自己堵死)。