Files
dsh_ai1net_server/.workbuddy/memory/2026-09-下24.md
T
admin ce8e6ceed9 chore(工作区): 纳入版本控制基线(回收 411 MB 过程产物)
回收 411 MB(470 M → 58.8 M),全部经回收站,可恢复:
- 待清理/(146.2 M,含 relay 分片 128 M 与 42 项过程目录)
- tmp/(32.4 M,按接续棒命名的过程临时区)
- .workbuddy/tmp/(39.5 M)
- 4 份 workbuddy.db 冗余副本(101 M,09-23 事故的坏副本 / 抢救产物)
- tmp/im16/gw/centrifugo 二进制(63.9 M,可重下)+ 缓存残留

入库范围:常驻规则(CODEBUDDY.md / README.md / state.py)、在途接续入口与
接续包、docs/、交付物/、交接单/、归档/、scripts/、.codebuddy/、
.workbuddy/memory/;共 398 件,其中 >60 KB 的 26 件全为文档。

排除(.gitignore):tmp/、待清理/、运行态日志与缓存、*.db 与 DB 备份整目录、
打包二进制(*.tar.gz / *.tgz)、记忆修复前备份。
2026-09-24 07:51:03 +08:00

382 lines
34 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 工作日志 · 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\AI技能\aliyun-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\AI技能\aliyun-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 平台时不要清理或白名单化环境变量**,否则"零改动切多人"会被自己堵死)。