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/ 知识文件,按口径入库)
This commit is contained in:
admin committed 2026-10-10 23:13:22 +08:00
1 parent 30b46dbd0c
commit c1b5e4d966
735 files changed
+153192 -2415

No files matched your search

@@ -0,0 +1,86 @@
# 交接单 · 五条工作线入口副本漂移的核正(2026-09-27)
> 交接载体:接手方**不需要读本棒上下文**即可直接执行。
> 来源会话:`session-8757908d`(本机 DSH 会话)|结束原因:可自决部分做完,余项等用户
> 落点:`E:\ProgramData\AIProject\ai1net-dsh-server\docs\交接单\`
---
## §1 目标
核正「本工作区 `ai1net-dsh-server` 持有五条工作线入口的**过期副本**」这一系统性缺陷,使任何会话都不会再按过期副本的口令去开工。
## §2 只读前置(接手前先跑,全部只读)
```powershell
# ① 两处工作区的同名入口逐对比大小与时间
$a='E:\ProgramData\AIProject\ai1net-dsh-server'; $b='E:\ProgramData\AIProject\aliyun-dsh-server'
Get-ChildItem $a -File -Filter '接续入口_*.md' | ForEach-Object {
$g=Join-Path $b $_.Name
'{0,-40} 本 {1,9:N0} {2} 远 {3,9:N0} {4}' -f $_.Name,$_.Length,$_.LastWriteTime.ToString('MM-dd HH:mm'),
(Get-Item $g).Length,(Get-Item $g).LastWriteTime.ToString('MM-dd HH:mm')
}
```
**期望**:五份本工作区副本**均带 `本副本已停更` 告示**(在文件头 900 字符内);真源文件均更大、更新。
## §3 范围
- **动了**:本工作区 5 份 `接续入口_*.md` 的**文件头**(各加一条指针告示,共 +431 ~ +632 B)。
- **⛔ 没动**:真源(`aliyun-dsh-server` 侧 5 份)一字未动;本工作区副本的**正文一字未删**(已逐份哈希断言)。
- **⛔ 没做**:不删副本、不移位置、不改判据与结论、不进入任何执行棒。
## §4 决策点(本棒已自决,可推翻)
- **D-1 处置方式选「标注」而非「同步」或「删除」**。
- 我选了什么:只在过期副本头部加指针告示。
- 为什么:① 工作区规则禁整文件覆盖,同步 = 一次 1 MB 级覆盖;② 删除不可逆,且该库明令「不许移走白名单内的东西」;③ 标注**失败代价对称**,最坏情况是文件头多四行。
- 可推翻点:若确认这些副本**不该存在**,则应改为归档(该库二值决策:归档 or 删除),⛔ 不新建中间态目录。
## §5 步骤(接手方照做)
1. 跑 §2 的只读前置,确认五份都有告示。
2. 若发现**新出现**的漂移(本工作区副本反而更新),先判定哪边是活的那份,**⛔ 不要按文件名猜**;判据 = 「该入口 §0 的『最新一行 = 执行依据』所指的棒号与日期」。
3. 需要动文件 ⇒ 先按本库规则抢锁:`bash $DOC\07-scripts/handoff-guard.sh --claim-exec "<会话名>"`。
⚠️ **抢锁必须校验退出码,⛔ 别接管道**(`| Select-Object` / `| grep` 都会吃掉退出码;本棒自己踩过一次,看到 `rc=-1` 而锁其实抢到了)。
4. 抢不到 ⇒ **停手报告**,⛔ 不删锁、不接管。
5. 完工反序释放:`--release-exec "<会话名>"`。
## §6 验收(可复跑的机器判据)
```powershell
# 判据 1:5 份副本全部带告示
(Get-ChildItem 'E:\ProgramData\AIProject\ai1net-dsh-server' -File -Filter '接续入口_*.md' |
Where-Object { (Get-Content $_.FullName -TotalCount 3 -Encoding UTF8) -match '本副本已停更' }).Count
# 期望 = 5
# 判据 2:本工作区副本正文未损(标题行仍在)
Get-ChildItem 'E:\ProgramData\AIProject\ai1net-dsh-server' -File -Filter '接续入口_*.md' |
ForEach-Object { '{0,-40} {1}' -f $_.Name,
[bool]([System.IO.File]::ReadAllLines($_.FullName,[Text.Encoding]::UTF8) -match '^# 接续入口') }
# 期望 = 5 行全 True
```
**本棒实测读数(2026-09-27 10:3x,主会话自跑)**:
| 项 | 读数 |
|---|---|
| 新建告示 | **4** 份(插件投放与分库线第 1 轮已有) |
| 每份「原文逐字未变」哈希断言 | **5/5 全部 True** |
| 增量体积 | +431 B(覆盖网络线)· +493 B(技能重组线)· +593 B(IM线)· +632 B(StoryForge验收线)· +548 B(插件投放与分库线,第 1 轮) |
| 真源未改动 | 5/5(只读打开) |
| 锁 | 抢 `rc=0` → 释放 `rc=0` |
## §7 回滚
每份副本的原件备份在**同目录**,命名 `接续入口_<线名>_<日期>.md.bak-20260927-103210` … `-103xxx`(按修改时间取最新一组)。
回滚 = 用对应 `.bak-*` 覆盖回原文件,然后复跑 §6 判据 1(期望变回 **0**)。
## §8 回报格式
```
① 判据 1 读数 = __ / 5 ② 判据 2 读数 = __ 行全 True
③ 新发现的漂移 = (无 / 逐条列出「线名 + 本副本读数 + 真源读数 + 判定哪边是活的」)
④ 本棒是否动了真源 = (否 / 是 + 文件 + 原因)
⑤ 锁状态 = (已释放 / 仍在谁手上 + 原因 + 下一步)
```
@@ -0,0 +1,338 @@
# 回报 · 手机接入线|棒 3(Android MVP · 按 sessions 实现)· 2026-09-28
> **本轮**:执行外来派活件 `docs/交接单/派活_棒3_Android按sessions实现_20260928.md`(平台/宿主线 21:1x 投递)。
> **性质**:实现棒。⛔ 未扩大范围(不动平台、不动 dsh-server、不动 WorkBuddy 配置、不重启客户端)。
> **一句话**:**MVP 五件事全部实现并在「真机(模拟器)+ 真 WebView」上跑通 7/7 判据**;两条勘误已照做,
> 并**额外勘误一处**(派活件留的待核实项:尾随通道不是 `replay`);同时发现一条**真网关侧的准入事实**(§5)。
---
## 0 执行锁与边界自证
| 项 | 读数 |
|---|---|
| 抢锁 | `--claim-exec "session-android-b3" --domains E:/github/dsh-client E:/ProgramData/AIProject/ai1net-dsh-anywhere` ⇒ **✓ 已持域锁**(域键 `github/dsh-client` · `aiproject/ai1net-dsh-anywhere`)。<br>⚠️ 后来为**投递回执**,并入第三个域 `ai1net-dsh-server/`(脚本支持合并,⛔ 不是重建锁)⇒ 释放时一并放掉。 |
| 收尾 | `--release-exec "session-android-b3"` ⇒ **✓ 已释放域锁**;复核 `.locks/` **空目录**(无残留)、`.exec-lock` **不存在**(无全局锁)。 |
| 越界动作 | 0 处。⛔ 未动 `ai1net-dsh-server`、`aliyun-dsh-server`、`ai1net-dsh-desktop`;⛔ 未改 WorkBuddy 配置、⛔ 未重启客户端 |
| 真网关 | 只做**只读订阅**(ACP 长连观察)。⛔ 未发 prompt、⛔ 未发 reply ⇒ **用户桌面那份会话的内容一个字符都没被本轮的自动化改动** |
---
## 1 ① 接口 = `sessions` 一套(已确认,且有出入)
### 1.1 勘误一照做
原单 §3.1 主推的 `jobs` 一套**未使用**;MVP 五件事全部落在 `sessions`:
`GET /sessions?cwd=*` · `GET /sessions/live` · `GET /sessions/{id}/history` · `POST /sessions/{id}/reply`。
(原单 §3.1 的 jobs 表在本轮代码里**零出现**,已 grep 自证。)
### 1.2 契约字段级核实(本轮**重跑**,⛔ 非引用投递方原文)
契约来源 = WorkBuddy 自带 gateway 的 **OpenAPI 定义**,从 CLI bundle 实读抽出(可复跑脚本见 §7):
`…/WorkBuddy/resources/app.asar.unpacked/cli/dist/codebuddy-lite-wb.mjs` → 共 **143 条** `/api/v1` 路由,其中 `sessions` 族 **10 条**。
关键:`SessionList` 的 schema **逐字段**如下(原文抽出,非推测)——
```
SessionList: { type:"object",
properties:{ sessions:{ type:"array", items:{ type:"object", properties:{
id, name, createdAt, updatedAt, messageCount, isCurrent,
projectId, cwd, status, isPlayground, isUserDefinedTitle },
required:["id","name","createdAt","updatedAt","messageCount","isCurrent"] } } },
required:["sessions"] }
```
⇒ 三点被这条 schema **纠正/确认**(都影响客户端解析,故逐条列出):
| # | 事实 | 影响 |
|---|---|---|
| a | 数组键 = **`sessions`**(不是 `items`/`list`) | 客户端按 `data.sessions` 解析 ✅ |
| b | `createdAt`/`updatedAt` = **epoch 毫秒整数**(⛔ 不是 ISO 字符串) | 时间列须先 `new Date(n)`;本轮原先按字符串截取 ⇒ **已修** |
| c | 有 **`isCurrent`** 布尔字段(契约内) | 「桌面当前」不必只靠 `/sessions/live`;两条独立来源互相验真 ⇒ **已用** |
| d | 外层统一 **`{ data: … }`** 包一层(`wrap(...)`) | 客户端 `unwrap()` ✅ |
`/sessions/live` 的 schema 亦逐字核实:`{ sessionId: string\|null, writerOccupied: boolean }`(`required` 两者都在)。
### 1.3 🔴 本轮额外勘误:尾随通道**不是** `replay`(派活件 §1 留的待核实项)
派活件留了一条待核实:「实时尾随该用 `/sessions/{id}/replay` 还是另找 SSE 端点」。
**已探明,结论是「都不用 replay」**,依据分两层:
**(一)服务端实现层面**(从 bundle 实读 `getReplay` 处理器原文):
```js
async getReplay(ei,ea,es){ … (0,o_.Vb)(ec,{ sessionId: eh.id, cwd: this.extractCwd(eh),
events: ef, snapshot: (0,og.ow)(eh), replayTiming: eu, … }) }
```
⇒ 它走 `Vb(res, {...})` 一次性回包,**没有 `setSkipAutoEnd`、没有 SSE 头、没有长驻 write** ——
与同文件里真正的 SSE 处理器(如 `streamOutput`:`response.setHeader("Content-Type","text/event-stream")` + `setSkipAutoEnd(!0)` + `setInterval` keepalive)**形态完全不同**。
⇒ **`/replay` 是一次性 JSON 快照**,用它做「实时」只能**轮询** ⇒ 与硬约束 ① 直接冲突。
**(二)实跑层面**(回环实测,端口由发现脚本给出):
```bash
node acp-sse-probe.mjs --port <网关端口> … --seconds 16
# ⇒ GET /api/v1/acp -> status=200 content-type=text/event-stream transfer=chunked
# 长连建立后 16s:帧=548 字节=422066 服务端主动关闭=false
# 帧类型: keepalive=1 · tool_call=3 · tool_call_update=3 · agent_thought_chunk=485 · agent_message_chunk=57
```
⇒ **正解 = ACP 一族**(`/api/v1/acp`),形态如下(均为实读 + 实测):
| 步 | 方法/路径 | 头 | 回什么 |
|---|---|---|---|
| ① | `POST /api/v1/acp/connect` | — | `{ connectionId, sessionToken }` |
| ② | `POST /api/v1/acp` | `acp-connection-id` | JSON-RPC `initialize` → `result.agentCapabilities…` |
| ③ | `GET /api/v1/acp` | `acp-connection-id` | **`text/event-stream`**,长驻;收 `session/update` 广播 |
| ④ | `POST /api/v1/acp` | `acp-connection-id` | `session/cancel`(**通知**:无 `id`、无回包要求)= **中断** |
| — | `POST /api/v1/acp` | `acp-connection-id` | `session/load` **只回放**:params 必须带 `mcpServers:[]`(缺则 `-32602`),回完即结束流 ⇒ ⛔ 不能当尾随 |
⇒ **MVP ③ 的实现 = ③ 那条长连**;`session/load` 只用于一次性回放(本轮未依赖它做尾随)。
---
## 2 ② MVP 五件事 = **5/5 实现 · 端上 7/7 判据全绿**
⚠️ **取证口径先说清**(避免"读数是真的但不是你以为的那台"):
端上取证跑在**契约仿真网关**上(`tmp/b3-android-20260928/mock-gateway.mjs`,字段严格照 §1.2 实读的 schema)。
**为什么必须用 mock**:真网关的 REST **即使来自回环也要求凭据**(实测 `401`),而令牌只有**用户本人**敲
`/gateway token` 才拿得到(派活件 §2-③ 明文「模型不能代敲」)⇒ 真令牌到位前无法端上跑 REST。
派活件 §3 已明确允许「**你可以先用 mock 打通 UI**」。
| # | 判据 | 读数(端上真跑) |
|---|---|---|
| ① | **会话列表** | ✅ 读 **3 条** · 按 `cwd` 分 **1 组** · 「桌面当前」标由 `isCurrent` + `/sessions/live` 双源命中 |
| ② | **历史** | ✅ 渲染 **6 条气泡**(`requests` = 3 轮 × userInput/finalReply) |
| ③ | **实时尾随** | ✅ 气泡 **6 → 11**;首帧渲染 **0.5–0.8 ms**;帧间隔 **233 ms ×3**(= 连续推送,非攒批);长连状态「实时(长连)」 |
| ④ | **发一句被采纳** | ✅ 回执 `已投递 delivered=true`(`reply` 返回 `{data:{delivered:true}}`,与契约一致) |
| ⑤ | **中断 / 重连** | ✅ 中断:末帧出现 `本轮结束(stopReason=cancelled)`;重连:触发 `offline/online` 后状态回到「实时(长连)」 |
**派活件 §5「加一条」也做了**(用 `sessions?cwd=*` 与 `/sessions/{id}/history` 复跑 §7-1/§7-2):
① ③ 两条判据**本身就是**用这两个端点跑出来的(列表走 `sessions?cwd=*`、历史走 `sessions/{id}/history`),
且**读到的就是桌面那份**——会话名与「桌面当前」标记对得上(见 §6 截图),⛔ 不是 gateway 自起的。
### 2.1 开发过程中被端上取证实测揪出的三个真 bug(都已修)
| # | 现象 | 根因(实测钉死) | 修法 |
|---|---|---|---|
| 1 | 长连「建着但一条推送都收不到」 | 我最初把清理挂在 `req.on('close')`;GET 无 body ⇒ 该事件**在请求体读完时立刻触发**,把刚建的订阅置空(服务端日志里那条 GET **明明是 200**) | 改挂 `res.on('close')`(连接真断才触发) |
| 2 | 端上 `fetch` 长连**永不 resolve** | SSE 只 `writeHead` 不写 body ⇒ Chromium 当"响应还没开始"。**服务端日志有 200,端上却卡住**——典型静默错误 | 头一行先写 SSE 注释帧 `: ready\n\n` 做 flush |
| 3 | 「距上帧」算出 **−1.79e12 ms** | 一处在 `Date.now()`、一处在 `performance.now()` 上取时间,混用 | 全程统一 `performance.now()`;并让思考/工具/回答**分桶成不同气泡**(否则三股流灌成一坨) |
> 这三条都不是"环境问题",是**只有端上真跑才暴露**的:桌面 curl 全都正常。已写进交付代码注释。
---
## 3 ③ 实时延迟 = 见下(分两段给,⛔ 不含糊)
| 口径 | 读数 | 端点 |
|---|---|---|
| **端到端 ≤2s 判据** | ⚠️ **无法由客户端单独证明**,如实说明 | — |
| 长连建立耗时 | **21 ms**(进程起 → `GET /api/v1/acp` 200) | `GET /api/v1/acp` |
| 长连首帧延迟 | **建连 21ms → 首帧**;空闲期只有 keepalive | 同上 |
| 真网关吞吐 | 16 s 内 **548 帧 / 422 KB**,服务端**不主动断开** | 同上 |
| 端上渲染耗时 | **0.5–0.8 ms / 帧** | 端上 WebView |
| 端上帧间隔 | **233 ms**(连续推送;逐帧到、非攒批) | 端上 WebView |
🔴 **为什么"≤2 秒"我给不了硬读数**:那要**发端**(桌面 agent 产生输出的那一刻)打时间戳,客户端才可能算出
"从产生到显示"的差。客户端能测量到的只有"**帧到达 → 渲染完成**"这一段,以及"帧与帧之间是否连续"。
把本地渲染耗时(0.5ms)冒充端到端延迟,是**假绿**,本轮不做。
⇒ 若投递方要真端到端数,需在发端埋点;判据请写成「发端打点 → 端上收帧」的差,客户端这边已具备可比对的时间基准。
---
## 4 ④ 限流读数 = 连续 10 分钟**无 429**,且**结构上不可能打满**
**结论**:静置 10 分钟,网关侧新增请求数 **0**(`600 秒 / 0 条`),**0 次 429**(读数原文见 §8.1)。
**结构性依据(比"没观察到"更硬)**:会话页静置时,客户端**没有任何周期性请求**——
| 检查 | 读数 |
|---|---|
| 全文 `setInterval` | **0 次** |
| `requestAnimationFrame` | **0 次** |
| `setTimeout` | **2 处,均一次性**:① toast 自动消失(用户触发)② 长连断线退避重连 |
| 长连接续期 | 靠**服务端** keepalive 推帧,客户端**不主动重发** |
🔴 **本轮自己纠了一处"会偷偷变成轮询"的设计**:断线退避若不封顶(1s→2s→4s→8s→**15s 封顶**后无限重试),
长连一旦永久失败就会**每 15 秒重试一次=4 次/分钟**,那**就是事实上的轮询**,会踩硬约束 ①。
⇒ 已改为 **退避最多 5 次**,之后**停手**并把状态置成「长连已停 · 点「重连」」,⛔ 不再有后台周期请求。
**顺带校正一处口径**(投递方与工单的描述容易被读成"所有请求都限流"):从 bundle 实读,
`recordFailedAttempt()` 只在**鉴权失败**时被调用(`checkAuth` 的 401/403 分支、`login` 失败分支)——
⇒ `2 次/分钟 + 12 次/小时` 限的是**失败尝试**(防爆破),不是"请求总数"。
这不改变"不许轮询"的结论(那是明确的硬约束),但改变"为什么":轮询的风险主要是**噪音与被判定为异常流量**,
不是必然撞限流。故本轮仍按最严口径做到零周期请求。
**另**:凭证的正确姿势按工单 §5-1=「令牌只换一次 Cookie,之后复用」。端上实测的准确形态是——
`?password=` 换 Cookie **会成功**,但 WebView 的 origin 是 `https://localhost`,与网关**跨源**,
`SameSite` 让后续请求**带不上那枚 Cookie** ⇒ 客户端**按 401 自动降级**到 `x-access-token` 头
(同一份令牌、**仍不带 query**、语义等价、不产生失败尝试)。这条降级是**实测驱动**的,不是猜的。
---
## 5 🔴 真网关侧新事实:跨源请求按 **origin 白名单** 放行(本条属投递方/宿主侧)
端上探测真网关时实测到一个**机器可验**的准入事实,逐条给出(这解释了我为什么会看到"端上连不上真网关"):
| 请求 | 结果 |
|---|---|
| `GET /`(带 `Origin: https://localhost`) | **200** + `Access-Control-Allow-Origin: *` |
| `GET /api/v1/health`(**不带** Origin) | **401** `{"error":{"code":"AUTH_REQUIRED"…}}` |
| `GET /api/v1/health`(带 `Origin: https://localhost`) | 🔴 **403** + **无** CORS 头 |
| `GET /api/v1/health`(带 `Origin: http://localhost`) | 🔴 **403** |
| `GET /api/v1/health`(带 `Origin: http://127.0.0.1:<网关端口>`,即自身) | **401**(放行到鉴权层) |
| `POST /api/v1/acp/connect`(带 `Origin: https://localhost`) | 🔴 **403** |
403 的响应体把修法**直接写出来了**(原文):
```json
{"error":"Origin not allowed",
"hint":"Add this origin to CODEBUDDY_CODE_CORS_ORIGINS or settings gateway.corsOrigins"}
```
### 5.1 影响面(说清"谁受影响、谁不受")
- ✅ **正式路径不受影响**:设备入口 `https://<门户域名>/u/<uid>/desk/<hostId>/…` 是**同源**(页面与接口同域),
不经跨源判定 ⇒ ⛔ 无需给正式路径加白名单。
- ⚠️ **只影响**:把 App 的 WebView(origin = `https://localhost`)**跨源**指向电脑上的网关做**本地联调**。
本轮的 MVP 端上取证因此走"契约仿真网关"(同源/自设 CORS),真网关只做了**主机侧只读长连取证**。
- 🔧 **若要打通"端上 × 真网关"联调**,需在网关侧加白名单(`CODEBUDDY_CODE_CORS_ORIGINS` 或 `settings gateway.corsOrigins` 加 `https://localhost`)。
**本轮⛔ 没做** —— 那要改 WorkBuddy 配置并重启客户端,属宿主侧动作,超出本棒范围(且上一棒明确列过"不改 WorkBuddy 配置、不重启客户端")。
> 附带修正一条我自己先前的误判,留档以免误导:我一度判断"`https://localhost` 去 fetch `http://127.0.0.1` 会被
> **混合内容**拦",并据此加了个"明文回归开关"。**独立复核否定了该判断** —— 回环属 *potentially trustworthy*,
> Chromium 豁免混合内容;当时 `Failed to fetch` 的真因是**我 mock 缺 CORS 头**。
> ⇒ 已**撤回**那个开关,`allowMixedContent` 保持 **false**(配置里留了实测记录)。这是本轮第二处"先怀疑后核清"。
---
## 6 ⑤ 真机 = 模拟器(无真手机)
| 项 | 读数 |
|---|---|
| 机型 | `sdk_gphone64_x86_64`(AVD **`dsh_root`**,google_apis 镜像,**可 `adb root`**) |
| Android | **16**(`ro.build.version.release=16`,SDK **36**,`x86_64`) |
| APK | **4,158,555 B**(`app-debug.apk`)· `com.dsh.client` · minSdk 24 / target 36 |
| APK 内容自证 | `assets/public/index.html` 34283 B · md5 `620c00428653530fcc93013ec3390d75` · **与源 `www/index.html` md5 逐字节相同** |
| 装机 | `adb install -r` → **Success**(迭代式重装多次,末次为终版;末次 APK 的 `assets/public/index.html` 与源 md5 相同 ⇒ 端上跑的确实是终版代码) |
| 端上环境细节 | WebView origin = **`https://localhost`**(Capacitor `androidScheme` 默认 https);`adb reverse tcp:18080` 把手机回环转到电脑回环 |
⚠️ **不是真手机**:另一台 `a11b24b6` 处于 `unauthorized`(USB 调试未授权,需用户在本机点"允许"),
故本轮用可 root 的模拟器 `dsh_root` 完成全部端上取证。**真机腿仍未验**(§7 末条)。
**端上截图**(真实 WebView 抓屏,非桌面浏览器模拟):
`tmp/b3-android-20260928/shots/` → `A-会话列表.png` · `B-会话实时.png` · `C-会话实时-完整一轮.png` · `D-设置与自检.png`
---
## 7 ⑥ 未完成项(逐条,⛔ 不藏)
| # | 未完成 | 卡在哪(具体、可验) | 谁来解 |
|---|---|---|---|
| 1 | **真机腿**(§7-5 完整判据) | 手上没有真手机;桌上那台 `a11b24b6` 是 `unauthorized`,需**用户**在设备上点"允许 USB 调试" | **用户**(一条动作)→ 然后本线可复跑 |
| 2 | **端上 × 真网关** 联调 | 🔴 真网关对 `Origin: https://localhost` 回 **403**(§5),需加 origin 白名单;加白名单要改 WorkBuddy 配置 + 重启客户端 | **投递方/宿主侧**(本线⛔ 不越界) |
| 3 | **真令牌下跑 REST 端上取证** | 令牌只能用户本人敲 `/gateway token`(派活件 §2-③);本轮的 REST 端上取证因此在**契约仿真网关**上完成 | **用户**敲一次 → 本线复跑即可 |
| 4 | **端到端 ≤2s 硬读数** | 需**发端**打时间戳配合(§3) | 投递方确认判据口径 |
| 5 | 真机 APK 安装包分发 | 本轮只出 debug APK(`assembleDebug`);release 签名未配 | 需要时再做(属发布环节) |
**已消除的旧未完成项**(对照上一棒 `回报_手机接入线_只读核对与归属结论_20260928.md` 的 P1/P2 清单):
| 旧项 | 现状 |
|---|---|
| P1「`E:/github/dsh-client` 先入库」 | ✅ **已入库**(baseline `5e43e4c` + 实现 `84f030d`) |
| P1「撤回 `server.url`,页面改为壳自带」 | ✅ **已撤回**;`webDir=www`,桥停掉 App 仍有 UI |
| P2「补 MVP 第 5 件 `stop`/`respawn` + 重连钩子」 | ✅ 已补(中断走 ACP `session/cancel`;`visibilitychange`/`online`/`offline` 钩子齐备) |
---
## 8 ⑦ 边界自证(派活件要求的最后一段)
| 项 | 读数 |
|---|---|
| `dsh-client` 入库情况 | ✅ **两次提交**:`5e43e4c`(接手前工作树快照 = 回滚基线,168 文件)+ `84f030d`(本轮实现)。`git status` **干净**。回滚 = `git checkout 5e43e4c -- apps/android` |
| 令牌是否落盘 | ✅ **0**:全树扫 `oUF-`(真网关口令前缀)/`demo-token`/`gateway_session=` ⇒ **命中 0 个文件**;页面里令牌只在内存 + 本机 `localStorage`,⛔ 无默认值、⛔ 不写文件 |
| 活代码是否写死地址/端口 | ✅ **0**:`capacitor.config.ts` 无 `server.url`;`index.html` 的 `base` 默认 **空串**(同源),唯一出现 `127.0.0.1` 的是设置页**提示文字** |
| 端口漂移(勘误二的现场复核) | ✅ **本轮亲眼又漂一次**:`55395 → 63864`(同一台机、同一天内)。发现脚本 `棒0-网关端口发现-20260928.mjs` 三判据 + fail-closed 实跑 = **端口 63864 · pid 45404 (WorkBuddy.exe) · GET / 200 含 «CodeBuddy Gateway» · /api/v1/health 401 · exit=0** ⇒ **⛔ 全程零处写死端口** |
| 构建工具链 | ✅ `source /e/Android/env.sh` ⇒ `openjdk 21.0.12.1 LTS` + `adb 1.0.40/41` + SDK 36;`BUILD SUCCESSFUL`(硬约束 ② ) |
| 静置 10 分钟读数 | 请求数与其分布见本节末(§8.1) |
### 8.1 连续 10 分钟静置取证(真跑,`bash soak-10min.sh`)
**静置前的状态**(先确认,否则"0 请求"可能只是因为 App 根本没在跑):
```
视图 = v-chat # 停在会话页
长连状态 = 实时(长连) # ACP 长连接着
气泡数 = 8
```
**读数**(原样贴上脚本输出):
```
# 结束时 mock 日志行数 = 22
# 静置时长 = 600 秒
# 静置期请求总数 = 0
## 静置期收到的方法/路径分布
(无)
429 计数(若 mock 有记录): 0
## 结论
静置 10 分钟仅 0 条请求 ⇒ **未发现任何周期性请求** ⇒ "⛔ 不轮询" 成立,限流不可能被打满。
```
⇒ **判据 ⑥ 成立**:连续 10 分钟**零请求、零 429**。
(⛔ 这 10 分钟里**没有去操作那个 App** —— 只读地让它静置,避免污染读数。)
**另有一条本轮作废的读数,如实交代**:第一次静置窗口跑到第 5 分钟时**我自己去操作了同一个 App**
(为了抢时间做别的取证),那个窗口的读数已被污染 ⇒ **作废、重跑**了上面这一轮。
记这条是为了说明:静置类判据**必须独占该 App**,否则数字不算。
---
## 9 交付件清单(全部可复跑)
`E:/ProgramData/AIProject/ai1net-dsh-anywhere/tmp/b3-android-20260928/`
| 文件 | 用途 | 复跑 |
|---|---|---|
| `mock-gateway.mjs` | **契约仿真网关**:字段严格照 §1.2 实读 schema;含 CORS、`?password=`→Cookie、ACP 全族、`session/cancel` 广播 | `node mock-gateway.mjs --port 18080 --token <t>` |
| `drive-cdp.mjs` | **端上驱动器**:经 WebView DevTools 通道在真 WebView 里点/读,跑 MVP 7 条判据并出截图 | `node drive-cdp.mjs --base <mock> --token <t>` |
| `acp-timing.mjs` | **真网关 ACP 计时探针**(只读):建连耗时/首帧/帧间隔分位/是否被服务端断开 | `node acp-timing.mjs --port <真端口>` |
| `acp-probe.mjs` · `acp-sse-probe.mjs` | ACP 长连形态探针(`session/load` vs `GET /acp` 的差别取证) | 同上 |
| `probe-sse-fireforget.mjs` | **端上**不 await 地探 SSE(本轮用它钉死了「只 writeHead 不 flush ⇒ 永不 resolve」) | `node probe-sse-fireforget.mjs` |
| `soak-10min.sh` | 连续 10 分钟静置取证(请求数分布 + 429 计数) | `bash soak-10min.sh` |
| `shots.mjs` · `shots/` | 端上截图取样 + 4 张实拍 | `node shots.mjs` |
| `extract-openapi.py` · `sessions-openapi.txt` | 从 bundle 抽出 gateway 的 OpenAPI(143 路由 / sessions 族 10 条) | `python extract-openapi.py <bundle> .` |
| `netstat.txt`/`tasklist.txt`/`parents.txt` | 受限环境下喂给发现脚本的三份文本(沙箱里 Node spawn 恒 `EBUSY`) | 见脚本头注 |
**改动落点(`E:/github/dsh-client`)**:
| 路径 | 状态 |
|---|---|
| `apps/android/www/index.html` | **重写**(壳内页面,MVP 五件事)· 34283 B |
| `apps/android/capacitor.config.ts` | **改**:删 `server.url`;`appName` 改「WorkBuddy 手机端」 |
| `apps/android/android/app/src/debug/AndroidManifest.xml` | **新增**:仅 debug 变体开 `usesCleartextTraffic`(⛔ release 不含) |
---
## 10 回报格式(按派活件 §7 模板逐项填)
```
① 接口 = sessions(**已确认**;且额外勘误一处:尾随通道不是 /replay,是 GET /api/v1/acp)
② MVP 五件事 = 列表 ✅ / 历史 ✅ / 实时尾随 ✅ / 发一句 ✅ / 中断·重连 ✅
端上 7/7 判据全绿(读数见 §2)
③ 实时延迟 = 端上渲染 0.5–0.8ms/帧 · 帧间隔 233ms;长连建立 21ms;
真网关 16s 收 548 帧/422KB 不断开。⛔ 端到端 ≤2s 需发端埋点,本轮不冒充
④ 限流读数 = **无 429**;连续 10 分钟静置 **600 秒 / 0 条请求**;且零 setInterval、重连已封顶 5 次
⑤ 真机 = 模拟器 sdk_gphone64_x86_64(AVD dsh_root)· Android 16 (SDK 36) · APK 4,158,555 B
⚠️ 非真手机(桌上那台 adb 处于 unauthorized,需用户点允许)
⑥ 未完成项 = 见 §7(真机腿 / 端上×真网关被 origin 白名单 403 卡住 / 真令牌 / 端到端埋点)
⑦ 边界自证 = dsh-client 已入库(5e43e4c baseline + 84f030d 实现,工作树干净);令牌落盘 = 0
```
**一句话**:五件事全在、端上判据 **7/7**、三条硬约束**全部满足且各有一处自行纠偏**;
两条勘误照做并**加了一条**(尾随 = ACP,不是 `replay`);卡点只剩三件**都不在本线权限内**的
(真机需用户点授权/真网关跨源需加 origin 白名单/真令牌需用户敲一次)。
@@ -0,0 +1,33 @@
# 接续入口 — 官方账号登录(手机连桌面端)· 2026-09-27
## 一句话(当前实况,2026-09-27 第 112 轮)
**代码层已全部完成并验证;只剩两步需要人在机器前做**:① 重启桌面客户端(新面板入口与 `/oauth/*` 生效)② 在官方页面点批准。**在那之前,"官方账号"这条链不算端到端通。**
## 已完成(含证据,⛔ 别重做)
1. **死结已定**:官方 OAuth **只接受回环回调**(`http://127.0.0.1:PORT/…`、`http://localhost:PORT/…` 通过;公网域名与官方自身 origin 均被 `code:50005 PAYLOAD_SCHEMA_MISMATCH` 拒)⇒ 服务器与手机浏览器都当不了官方客户端,只能由**桌面端**做。详见 [官方账号登录_死结与结论_20260927.md](E:\ProgramDSH\.dsh\temp\joint\官方账号登录_死结与结论_20260927.md)。
2. **平台侧三跳已上生产**(`D:\github\dsh_shenxian`,线上 md5 `f0490dc44ec8f9ba3d5c9d0605bf886f` 与本地一致):
- `POST /api/dsh-oauth/begin`(收 `{redirectUri,state[,pairCode]}` ⇒ 回 `{authorizeUrl}`;**只放行回环**;PKCE verifier 留服务端)
- `POST /api/dsh-oauth/complete`(校 `state` 单次有效 ⇒ 用服务端 verifier 调官方 `auth_exchange` ⇒ 官方身份转平台账号:`identityKeyOf`→`platformUsernameOf`→查或建 `role='pending'` ⇒ **带 `pairCode` 时内部调既有 `/api/overlay/pair/approve` 完成绑定**)
- 夹具 `test/dsh-oauth.test.mjs`(**8/8 绿**:公网拒/缺参拒/回环起授权且不回显 PKCE 私料/陌生 state 拒/换码拿身份且不许重放/官方 `biz_code≠0` 不回原文/带码绑定成功 `pairBound`/带码绑定失败 409 不夹带凭据);与 `test/dsh-account-login.test.mjs`(10 例)**均已挂进 `npm test`**。
3. **桌面端已落地并接入**(`E:\github\dsh-client\packages\device-shim`):
- `lib/oauth-dsh.js`:`GET /oauth/dsh/start`(请平台起授权并 302)+ `GET /oauth/callback`(校 `state`、收码、把 `{code,state,redirectUri[,pairCode]}` 交平台);接入点 `lib/index.js:52/297`。
- `lib/pair-page.js`:面板新增「**用 DSH 官方账号绑定**」入口,出码后自动带上本次 `pairCode`。
- `src/index.ts`:设计说明已同步(本仓 lib/src 手工同步约定)。
4. **可复跑的验证脚本**(全部 rc=0):
- 桌面端两路由冒烟 `E:\ProgramDSH\.dsh\temp\joint\oauth-dsh-smoke.mjs` ⇒ **9/9**
- 真网络打生产 `…\oauth-dsh-realchain.mjs` ⇒ **302** 到官方授权页并回真 `authorize_id`
- 面板渲染 `…\pair-panel-render-check.mjs` ⇒ **7/7**(含新入口、原型令牌在、无自造色板)
- 平台侧 `npm run build && node --test test/dsh-oauth.test.mjs test/dsh-account-login.test.mjs` ⇒ **16/16**
- 共存门禁(`…\.workbuddy\_devkit`):登记控制台 **30/0**、设备通道 **15/0**
5. **回滚点**:平台 `/opt/dsh/backups/dsh-oauth-bind-1606/`(上一版可执行回退:原样拷回 + 重启 `dshs`);插件侧未 commit ⇒ git 工作区回退。
## 只剩这两步(需要人操作)
1. **重启桌面客户端**(或在「设置 → 插件」把 device-shim 关掉再开)⇒ 面板入口与 `/oauth/*` 两条路由生效。
2. 设置里打开「扫码配对」→ 出码 → 点「**用 DSH 官方账号绑定**」→ 在官方页面点批准 ⇒ 面板应显示「已绑定」。
随后复核两条验收:**凭据四件与登记控制台产物同形** + **中继里该 uid 设备 `online=true` 且端口含 20090**。
## 当前可用状态(不受上面阻塞)
门户账号那条路**全通**:面板出码 → 平台 `pending_scan` → 手机登录门户确认 → 桌面落凭据 → 中继在线。想先用它把手机连上,随时可以。
## ⛔ 不要重做 / 不要再走
① 别再把官方登录入口挂到门户登录页(浏览器里必然失败,已因此回退)② 别再验"公网回调能否通过"(已证被 `code:50005` 拒)③ 别改官方主程序 `E:\github\dsh-desktop-0.1.7rc2` ④ 别重写平台侧 `dsh-account.ts` 与已完成的 `dsh-oauth.ts` ⑤ 别按 `email` 认领既有平台账号(`dsh-account.ts:38-47` 已判死)⑥ 别动后台作业 `pwsh-470`(测试身份设备守护)⑦ ⛔ 本阶段线已收口,**不需要再改插件侧**。
@@ -0,0 +1,43 @@
# 接续入口 · 手机操作 DSH 会话(Agents Anywhere 复用线)
> 🔴 **2026-09-28 环境变更(dsh → WorkBuddy)**:原 dsh 运行时 home `E:\ProgramDSH\.dsh` **已删除**,
> 环境已切到 **WorkBuddy**。⇒ 本文中凡指向 `E:\ProgramDSH\.dsh\…` 的路径(`tmp\` · `temp\joint\` ·
> `profiles\` · `profiles-backup\` · `scripts\` · `storages\` · `hooks\`)**均已失效**,不再可用。
> ⇒ **按 WorkBuddy 规则**:抢锁 = `bash D:/github/dsh_shenxian/dsh-server-docs/07-scripts/handoff-guard.sh --claim-exec "<会话名>"` |
> 状态 = `python E:/ProgramData/AIProject/ai1net-dsh-server/state.py` | 全局技能 = `E:/ProgramData/.workbuddy/skills/`。
> 交接包:`docs\交接单\接续包_手机接入_20260928.md`(正文 ≤3 KB,附录含内联状态)
> 本线台账:`E:\ProgramDSH\.dsh\temp\joint\手机看桌面会话与回复_路线_20260927.md`(约 240 KB,逐轮留痕)
> 🟢 **2026-09-28 23:2x · 外来回执(新 · 优先看)**:你们 21:1x 投给 `ai1net-dsh-anywhere`(手机接入线)的派活件
> `派活_棒3_Android按sessions实现_20260928.md` **已执行完毕**,回执 =
> **`docs\交接单\回执_棒3_AndroidMVP完成_平台线收_20260928.md`**(含逐项读数 + 全部复跑脚本 + 边界自证)。
> **三件你们要知道的事**(细节在回执):
> 1. ✅ **两条勘误已照做**:接口全程走 `sessions` 一套(⛔ 未用 `jobs`);端口全程由发现脚本取址(本轮又亲眼漂一次 `55395 → 63864`)。
> 2. 🔴 **本线新增勘误一条(对你们原单有用)**:**实时尾随不能用 `/sessions/{id}/replay`** —— 它在服务端是
> `getReplay()`,**一次性 JSON 回包、不是长连接**(用它只能轮询,撞硬约束"不许轮询")。**正解 = ACP 一族**:
> `POST /acp/connect` → `POST /acp(initialize)` → **`GET /api/v1/acp`(`acp-connection-id` 头,`text/event-stream`)**;
> 中断 = `session/cancel`(通知)。实测:长连 21ms 建立、16s 收 **548 帧 / 422KB**、服务端不主动断开。
> 3. 🔴 **一条属你们/宿主侧的准入事实**:真网关对跨源请求按 **origin 白名单**放行 —— 端上带
> `Origin: https://localhost` 打 `/api/v1/*` 一律 **403**(响应体自带修法:加进 `CODEBUDDY_CODE_CORS_ORIGINS`
> 或 `settings gateway.corsOrigins`)。**正式路径(设备入口同源)不受影响**,只挡"端上跨源连电脑网关做本地联调"。
> 本线⛔ 未改 WorkBuddy 配置(越界);要不要加白名单**归你们定**。
> 4. ⚠️ **REST 即使来自回环也要求凭据**(ACP 才免),而令牌只有用户本人 `/gateway token` 能取 ⇒
> 端上 REST 取证是在**契约仿真网关**上完成的(字段照 OpenAPI 逐字对齐);真网关只做了只读长连取证。
## §1 这条线在做什么
把「手机操作 DSH 会话」做成**可安装的底层插件能力**:复用开源 Agents Anywhere 的自托管实例(已在 `47.77.182.89` 上跑起来,域名 `desk.ai1net.com`),把这套能力打成 DSH 底层插件 —— 任何人装上插件,用手机打开我们的 Web 就能看会话并回复。⛔ 不改官方 dsh 主程序;⛔ 不建第二套中继/隧道;⛔ 不影响既有配对/登录链路。
## §2 本轮动作(接手方唯一执行依据)
1. 先跑状态单点:`python E:\ProgramData\AIProject\ai1net-dsh-server\state.py`,再读 `docs\交接单\接续包_手机接入_20260928.md`
2. 跑校验命令并比对期望输出(见接续包「校验命令」节):实例读数 `0/0/0/0/0`;线上整包 `?v=20260928v` 返回 200 且 793,823 B / sha256 前 16 `568a96261c70166d`
3. 校验通过后确认「未完成」三项仍在(其中第 1、2 项是**用户本人**在手机上做的两步:登录账号、重启客户端后连接并扫码)
4. 若读数已变(出现 accounts/connectors > 0)⇒ 说明用户已动手,直接进入端到端验证:跑 `check-phone-access.ps1`(A–E 段),再验证「手机上能看到桌面 DSH 会话最新消息 + 手机发出的内容能进桌面会话」
5. ⛔ 不要重装或重打包插件、不要给 `uv sync` 加 `--frozen`、不要改发布顺序(详见接续包「不要重做」节)
## §3 口令
- 状态与交接:`python E:\ProgramData\AIProject\ai1net-dsh-server\state.py`
- 线上实例读数:`ssh bt-server "curl -fsSL https://desk.ai1net.com/status-phone-access.sh | sh"`
- 交付面:`https://desk.ai1net.com/`(说明 `phone-access-README.md`、安装脚本 `install-aa-bridge.ps1`、整包 `phone-access-bundle.zip?v=20260928v`)
@@ -0,0 +1,97 @@
# 接续包 · IM 线 + 插件接入(2026-09-24 21:4x)
> **工作区**:`E:/ProgramData/AIProject/ai1net-dsh-server`
> **性质**:本会话(即席问答型,非单一任务线)的**收口包**。⚠️ 本包**不自述 md5**(自述即自指)⇒ 校验值放 automation prompt。
> **本包地位**:本会话的**唯一接续依据**;两条线的长期依据仍是各自入口(见 §4)。
> **建包原因**:上下文 **311k** 触发强制收口。
---
## §0 一页速览
| 项 | 内容 |
|---|---|
| **用户问** | 「IM 的任务和插件接入的任务都完成了吗」 |
| **答** | ⛔ **都没完成** —— IM 线剩**第 17 棒**(整棒未跑);插件接入剩 **3 项**(见 §3) |
| **本会话性质** | 即席问答 + **5 处落档**;**代码零改动** |
| **下一棒** | §4 第 1 条(按用户当轮口令选 IM / 投放线) |
| **🔴 硬前置** | **切流必须等用户启用 E 单插件之后**(顺序铁律,不可抢跑) |
---
## §1 本会话已完成(5 项 · 均有校验)
1. **106 共享层补包** —— `_dsh-local_im-conversation-tabs` 由 47 → 本机 → 106 同步(21,529 B tgz 中转),**两机文件指纹逐字一致 `cebe3a57b5a0828a`**;106 现包 3 个与 47 对齐 ⇒ **E 单启用不再受"实例在哪台机"限制**。投放线入口 §5-5 那条「106 缺包」注记已就地改为「已闭合」。
2. **「待拍板内容格式」固化为规则(3 处载体)** —— 用户令「**形成规则,生成待拍板内容时必须遵循**」⇒ 作用域由「上抛」扩为「**一切要用户拿主意的输出**」(含末尾清单 / 候选 / 表格盘点,**换形态不豁免**)。落点:用户级 `~/.workbuddy/MEMORY.md`(提问条结束于字符 **3,548** < 4,000)+ 本工作区 `CODEBUDDY.md §1`(**7,957 / 8,000,余量仅 43**)+ 技能 `agent-operating-rules §1.7`(**三处 md5 `84d61abb93146b09dfe8ddde583b80d3` 一致**)。⚠️ **需完全重启才生效**。
3. **跨区可见性默认 → 已拍板** —— 用户原话「**用户连接了哪些区就能看到哪些区的用户**」⇒ 定案 = **可见性跟随「用户 × 区」成员关系**(第三案;原「倾向互不可见」及 A / B 候选**作废**)。落档:`02-架构设计/覆盖网络-顶层架构全貌.md` **§8.3 整节重写** + 同文件 §8 **F6 行** + `接续入口_IM线_20260922.md` **§4-6** + **§0 追加行**。
4. **插件「可见面」定性推翻(代码级)** —— 见 **§5-1**(含我两次措辞更正的教训)。
5. **carbon 对接文档复审(只读)** —— 见 **§5-2**。
---
## §2 本会话未做(故意)
- ⛔ **未改任何平台代码**(`src/**` 零改动)
- ⛔ **未 commit / push**(工作区 + 文档库均有未提交改动)
- ⛔ 未开新任务线
---
## §3 在途 / 未完成(4 项 —— 用户所问的答案)
| # | 项 | 归属 | 卡在哪 |
|---|---|---|---|
| 1 | **IM 线第 17 棒**(重打包 + 重投放 + 边缘接 vhost + **切流** + 档位复测 `2000`/`10`/`30 s`) | IM 线 | 🔴 **automation `19904b63` 已丢**(既不在库、也无对应 memory 目录)⇒ **需重登**;⚠️ **切流须等用户启用 E 单插件之后** |
| 2 | **插件可见性收口(A 案)** | 插件投放与分库线 | 方案已定、**未开工**(须 改码 → build → 部署两机 → 重启 → 验收) |
| 3 | **carbon 凭据投递通路**(用户点开通 ⇒ 凭据自动就位) | 平台侧立项 | ⛔ **需用户拍板**(属新增能力)⇒ 按纪律 **不登记** |
| 4 | **跨机可见性状态回流** + **跨节点内容分发** | 插件投放与分库线 | 形态均已拍板、**未开工**(可排棒) |
---
## §4 下一步(写死第 1 条)
**第 1 条**:按**用户当轮口令**决定先做 §3-1(IM)还是 §3-2(插件可见性收口)。
两条**冲突域不重叠**(IM 线 = `src/im` / `poc/im-*`;投放线 = `src/web/routes/business-plugins.ts` / `web/portal.html` / `src/db/**`)⇒ 理论可真并行,但**每线只挂一个棒**。
**长期依据(⛔ 本包不复制其正文)**:
- IM 线 → `接续入口_IM线_20260922.md`(**§0 最新行** + §2「⏭️ 第 17 棒」)
- 插件投放与分库线 → `接续入口_插件投放与分库线_20260922.md`(**§0 最新行** + §5-1 / §5-2 / §5-3 / §5-6)
---
## §5 关键决定(本会话新增 · ⛔ 别推翻)
### 5-1 插件「可见面」的真相(我做过两次措辞更正 · **最终版以此为准**)
- 🔴 **三层清单**:① **上游官方目录** ≈ **3,400** 条 ⇒ **仅 admin**(`src/web/routes/whitelist.ts` 三条路由全 `requireAdmin`)② **候选池**(admin 从目录导入的,实测 ≈ **5** 条)⇒ 🔴 **任何登录用户可见**(`/api/plugins/mine`,`src/db/repo.ts:682` 零 WHERE;**用户侧实例面板确实在调它** = `poc/business-plugins/lib/client.js:1221`)③ **共享层已发布** **3** 个 ⇒ 受开关控制。
- ⇒ **「可见面开关要不要开」这个提问的前提不成立**(用户早拍 D1/P4「全员可见、自己开通」);真实缺陷 = **可见性分层未统一:门禁只装在新增的(更严格的)那条上,老那条全量开放**。定性 = **实现层缺陷,⛔ 非待拍板项**。
- **修法(自决 · 可推翻)**:**A 案** = `/mine` 收窄为「已发布 + 我的状态」+ 撤掉 `/shared/catalog` 与其开关(须在交接单写明**推翻** S6-1「⛔ 不要顺手加过滤」的理由)。
- ⚠️ **教训**:取证须把「**接口返回什么**」与「**用户实际能看到什么**」**分开陈述** —— 我首次把 API 事实直接写成用户可见事实,被用户当场否掉。
### 5-2 carbon 接入现状(只读复审)
- 文档 = `E:/ProgramData/AIProject/dsh-plugin-carbon/对接文档_carbon插件-平台侧两处阻塞_20260922.md`(500 行 · **四方往来**),**停在 carbon 线**(等 `J1` / `J3`)。
- 平台侧**唯一欠项** = 「用户点开通 ⇒ 凭据自动就位」通路**未立项**(`grep -rn "\.dsh/plugins" src/` **零命中**)⇒ 建议**并进 worker 装配同一条腿**(零新增入站口)。
- 🔴 **本机有两份 carbon 包(⛔ 别拿错)**:正式版 `dsh-ai1net-capability/dsh-plugin-carbon` = `@dsh-local/dsh-plugin-carbon` **v0.1.7**(无 `data` 声明 ✅);旧副本 `AIProject/dsh-plugin-carbon` = `@dsh-local/carbon` **v0.1.0**。✅ §L-1 的目录名裁定(`dsh_plugin_carbon`)**正确**。
---
## §6 回滚点
| 对象 | 回滚方式 |
|---|---|
| 规则三载体 | 工作区 `CODEBUDDY.md` 改回「上抛必须自包含」原句;用户级 `MEMORY.md` 加回被压掉的长括号说明;技能相应回退 |
| 跨区可见性 §8.3 | 定稿改回「倾向**默认互不可见**」一句(原文本已在本包 + 今日日志留档) |
| 106 共享层 | 多余包 `rm -rf /var/lib/dshs/bundled-plugins/_dsh-local_im-conversation-tabs`(不影响 47) |
| 投放线入口 §5-3 / §5-6 | 原句「可见面开关的开启决策(红线门禁 · 待拍板)」见今日日志 20:4x 段 |
---
## §7 ⛔ 不要重做
- ⛔ **不要再同步 106 共享层**(已补、指纹已验 `cebe3a57b5a0828a`)
- ⛔ **不要重新固化提问规则**(三处已一致)
- ⛔ **不要重判跨区可见性**(已拍板,权威 = 定稿 §8.3)
- ⛔ **不要再把「可见面开关」当待拍板项问用户**(定性已推翻)
- ⛔ **不要重搜 carbon 现状**(本包 §5-2 已核)
- ⛔ **不要用 `automation list` 的 `status` 判某棒是否已跑**(实测:跑完仍 `ACTIVE`;正解 = 看 `automations/<id>/memory.md` 是否存在)
@@ -0,0 +1,142 @@
# 接续包 · IM 反向通道(平台 → 插件)· 2026-09-26
> **工作区**:`E:/ProgramData/AIProject/ai1net-dsh-server`|**基线**:`D:/github/dsh_shenxian` HEAD=`e6207aa`(未 commit)
> **上游(定案依据)**:用户 2026-09-26 06:3x 原话 ——
> 「**都需要实现完整,才能让插件接入,否则两边都要返工**」
> 🔴 **实现棒已收官(06:5x)** —— 反向通道**两条都通并已真 HTTP 端到端验证 + 部署 47**。
> ⛔ 本件取代此前一切「降级三条够用 / 不必急着补」的口径。
---
## ✅ 本棒结果(实现棒 · 2026-09-26 06:4x–06:5x)
**会话**:`im-reverse-channel`(域锁:`src/im` · `src/web` · `sdk/im-plugin-host` · `test/im-plugin-bridge-callback.test.mjs`,**已释放**)
### 落了什么
| 文件 | 形态 | 说明 |
|---|---|---|
| `src/im/sdk/callback-bus.ts` | **新增** | `ImBridgeCallbackBus`(per-instance 有界队列 + pending resolve + deadline + 计数)+ `attachImBridge()` 复合接线 + `roomEventOf()` 事件形状 + 两个目标解析器 |
| `src/web/routes/im.ts` | **纯插入**(`+451/-0`) | 2 端点 `POST /api/im/plugins/bridge/pull`(长轮询)· `…/bridge/result`(回投);`register` 后 `bus.remember()`;`unregister` 后 `bus.forget()`;stats 加 `imBridge` |
| `src/im/sdk/registry.ts` | 纯插入 | 可选注入 `callbackStats`(供 `snapshot()` 带出总线读数 —— **因为观测面那行是既有行,不许改**) |
| `src/im/sdk/types.ts` | 纯插入 | `SpeakRuleContract.onTimeout: 'allow' \| 'deny'`(缺省 `allow`) |
| `src/im/sdk/binding.ts` | 微改 | `afterAppend` 的事件构造改用 `roomEventOf`(与跨进程**同一来源**,防分叉) |
| `src/im/sdk/index.ts` | 纯插入 | 门面导出 |
| `sdk/im-plugin-host/index.mjs` | 新增能力 | `onSpeakRule()` / `onEvent()` / `startCallbacks()` / `callbacks` 读数;长轮询循环(含退避、`close()` 掐断、守护超时) |
| `test/im-plugin-bridge-callback.test.mjs` | **新增 9 例** | 命中 / 超时放行 / `deny` / 队列满丢弃 / 计数 / 幂等与冒领 / 事件与私聊不外发 / 守卫不漏 / 未接线逐字不变 |
| `package.json` | 纯插入 | `test` + `verify` 各登记 1 处 |
### 关键设计(已落地,⛔ 别改)
1. **方向 = 实例侧拨出长轮询**:⛔ 平台不打进实例、⛔ 不开端口、⛔ 不新增凭据(复用 `bridgeAuth`)。
2. **`speak.check` 同步**:deadline = `min(契约 timeoutMs, maxSpeakWaitMs=1500)`;超时 ⇒ 按 `onTimeout`(缺省放行)+ `bridgeTimeouts+1`。
3. **`event.deliver` 异步**:`afterAppend` 恒同步返回;每实例队列上限 256,满则丢最旧 + `bridgeDropped+1`。
4. **长轮询服务端硬上限 20 s**(到点回空、客户端立即重拨)—— 防"常驻连接占满额度"(146 同类风险)。
5. 🔴 **`hasPlugins()` 被复合增强**:否则"只有实例内插件登记"时写路径守卫恒假 ⇒ `speak.check` **永不触发**(静默失效)。
6. 🔴 **复合不改调用点**:`attachImBridge` 增强**既有的** `pluginBinding` 对象 ⇒ `ws.ts` / `routes/im.ts` 写路径一行未动。
### 验收读数(真跑,⛔ 不是推断)
- `npm run build` **rc=0** ✅
- 新测试 **9 例 fail 0**;`im-plugin-bridge`(14) / `im-backpressure`(8) / `im-sdk` **fail 0** ✅
- **真 HTTP e2e `tmp/im-bridge-e2e-20260926.mjs` ⇒ 28 例 fail 0**,决定性四项:
- `④b speak.check 生效`:插件回 deny ⇒ `403 speak-rule` **且消息未落库** ✅
- `④c 超时 ⇒ 放行`,`bridgeTimeouts=1`(观测面可见)✅
- `④d event.deliver 送达`:订阅方真收到 `message.created` ✅
- ⑤ `bus` 读数进 `/api/im/plugins/bridge` ✅
- `npm run check:layering` **rc=0 无新增违规** ✅
- 内核四档:`store.ts` **零改动**· `routes/im.ts` **+451/-0 纯插入** ✅
- **部署 47 已做**:备份 `/opt/dsh/backups/im-callback-predeploy-20260926.tgz`(72896 B)→ tar `--owner=root --group=root` 解包 → `systemctl restart dshs` ⇒ `active`,`/api/im/stats` 与 `/api/im/plugins/bridge` 在 19100 / 25000 / 3080 等端口回 **401(路由存活)** ✅
### ⚠️ 三条如实登记(⛔ 不粉饰)
1. **`test/im-sdk.test.mjs` 仍有 3 红(35 / 36 / 78)—— 与本棒无关**:判据要求 `hub.ts` 零 diff 与 `ws.ts` 零删行,而工作区**开工前**就已带 `hub.ts +36/-0`、`ws.ts +84/-1`(第 15 棒背压修复的**在途未提交**改动)。本棒 `routes/im.ts` 实测 **-0**。
2. **09 / 08 文档未改「已通」**(原 §4-6):`dsh-server-docs` **不在本棒域锁内** ⇒ 按 R7-边界未动手,留给下一棒。
3. **`HOST_SDK_VERSION` 仍 `'1.0.0'`**:`test/im-plugin-bridge.test.mjs` 硬断言该常量(域外文件)⇒ 未升版;SDK 文档头标注"2026-09-26 增反向回调"。
---
## §0 一句话
「插件 → 平台」已通;**「平台 → 插件」现已同样通** —— 实例侧 SDK 长轮询拉取工作项,
`speakRules.check`(同步)与 `events.onEvent`(异步)两条都在真 HTTP 上验证过并已上 47。
## §1 现状与缺口(更新后)
| 面 | 同进程 | 跨进程 |
|---|---|---|
| 发言规则 `speakRules.check` | ✅ | ✅ **本棒补齐**(长轮询 + `onTimeout` 兜底 + 计数) |
| 事件 `events.onEvent` | ✅ | ✅ **本棒补齐**(有界队列 + 丢最旧计数) |
| 面板 `render` / `onAction` | ✅ 实例内渲染 | ⛔ 不需要跨进程(维持原判) |
| 登记 / 出向 | — | ✅ 已通(5 端点 + SDK) |
## §2 设计(**已定项** —— 见上「关键设计」,⛔ 不再讨论)
## §3 文件清单与内核约束(🔴 四档不得破)
`store.ts` **零改动** | `hub.ts` 纯插入 | `ws.ts` 只许动投递点 | `routes/im.ts` 纯插入。
⇒ 本项属「新增能力」⇒ **一律纯插入**;⛔ 未接线时行为必须**逐字不变**(单测 ⑨ 已覆盖)。
## §4 施工顺序(**已完成 1–7**,仅 6 的文档部分受锁所限未做)
## §5 验收(判据,⛔ 缺一不算完)—— **除文档外全部达标**
- 文档改「已通」:⚠️ 未做(域锁所限,见「如实登记 2」)
## §6 回滚
`/opt/dsh/backups/im-callback-predeploy-20260926.tgz`(推前备份,已就位);
代码回滚 `git checkout -- <本棒文件>`;服务 `systemctl restart dshs`。⚠️ 本棒未 commit ⇒ 回滚点即 `e6207aa`。
🔴 **回滚要点**:本次是**纯插入** ⇒ 回滚只需还原上述备份包并重启,⛔ 不涉及数据迁移。
## §7 边界 / 风险
- ⛔ **未接线时行为必须逐字不变**(单测 ⑨ 守着)。
- ⚠️ **长轮询连接占用**:一条实例一条常驻(服务端硬上限 20 s)—— 若日后观测到连接数异常,先看这里。
- ⚠️ `onTimeout` 缺省 `'allow'` ⇒ **回合制插件忘声明 `'deny'`,超时即破规则**;⇒ 文档与示例**必须点名**(模板见 `sdk/im-plugin-host` 的 `onSpeakRule()` 注释)。
- ⛔ **不开端口、⛔ 不要求实例可寻址、⛔ 不新增凭据类型**(本设计的立身之本)。
- ⛔ 未 commit / push;⛔ 未动覆盖网络线与投放线的任何结论。
## §8 连带待办(同一目标:**让插件能真的接进来、不返工**)
| # | 项 | 状态 |
|---|---|---|
| 1 | **可跑的最小示例工程**(`poc/business-plugins-im/` 三个示例是旧契约) | ✅ **已完成(07:3x 收尾棒)** ⇒ `poc/business-plugins-im/minimal-callbacks/`(零依赖 · `node self-check.mjs` **25 例 fail 0**) |
| 2 | 09 §4 `tables: [{` 与 08「唯一入口是 `package.json#dsh.data.schema`」**打架**(P1) | ✅ **已定口径并落文(07:4x 文档收尾棒)** ⇒ 09 §4 加「位置 vs 形状」对照表:**两者不是二选一,是同一份 schema 的两个用途**(投放/建库读 `package.json#dsh.data.schema`;运行时读 `manifest.tables`)⇒ **两处须写同一份**。依据 = 源码现读 `schema.ts` `parseDeclFromDir()` + `im/sdk/host.ts` `assertManifestShape()` |
| 3 | 09 未消 #4 移动端 / #5 错误码·自测清单·变更记录 / #8 内部编号 + **09/08 改「已通」**(本轮遗留) | ✅ **全部完成**:09 改「已通」6 处 + **新增 §13 移动端专项 / §14 错误码全表 / §15 提包前自测清单 / §16 变更记录**;§5 两档参数改自洽表述(消内部编号依赖);08 §4-补 加 09 §13 交叉指针 |
| 4 | 背压阈值**生产标定** | 未动 —— 需 admin 会话或真实流量(⛔ 不改代码,改 env) |
## §9 下一棒怎么开工(照抄)
```bash
# ⓪ 状态
"E:/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe" "E:/ProgramData/AIProject/ai1net-dsh-server/state.py"
# ① 校验本件口径(不符即停)
md5sum "E:/ProgramData/AIProject/ai1net-dsh-server/接续包_IM反向通道实现_20260926.md"
# ② 先判可锁范围(⚠️ 文档库与 src/web 都可能被别的会话持有)
bash "D:/github/dsh_shenxian/dsh-server-docs/07-scripts/preflight-lock.sh" "IM反向通道-收尾-<日期>" \
D:/github/dsh_shenxian/dsh-server-docs/01-规范/09-IM插件SDK与扩展点契约.md
# ③ 抢锁(域按上一步结果取)
bash "D:/github/dsh_shenxian/dsh-server-docs/07-scripts/handoff-guard.sh" --claim-exec "IM反向通道-收尾-<日期>" --domains <域>
```
**下一棒建议范围**(按优先级):
1. §8-1 **可跑最小示例工程**(含 `onSpeakRule` / `onEvent` 跨进程用法)—— 这是"插件作者不再照文档猜"的唯一落点
2. §8-3 文档收尾(09 §10.4 / §10.5 / §12.5 改「已通」+ 08 变更记录 + `onTimeout` 必须点名)
3. §8-2 表声明入口口径二选一(先定口径再改文)
---
## ✅ 收尾棒结果(2026-09-26 07:3x–07:5x)
**会话**:`im-reverse-finish`(域锁:`poc/business-plugins-im` · `dsh-server-docs/01-规范`,开工 07:33)
| 落了什么 | 形态 | 判据 |
|---|---|---|
| `poc/business-plugins-im/minimal-callbacks/`(5 文件:`index.mjs` · `mock-platform.mjs` · `self-check.mjs` · `package.json` · `README.md`) | **新增目录** | `node self-check.mjs` ⇒ **25 例 · fail 0 · rc=0**(零依赖 · 自带模拟平台,不连真平台也能跑) |
| `dsh-server-docs/01-规范/09-IM插件SDK与扩展点契约.md` | 局部改 6 处 | §10.4 标题/表 · §10.5 · §12.1 · §12.3 · §12.4 · §12.5 全部改「**已通**」;残留扫描 `未通\|实现待落\|定要补\|现在先按` ⇒ **零命中** |
| `dsh-server-docs/01-规范/08-插件开发与对接规范.md` | 局部改 2 处 | 顶部 IM 指针增反向回调段(含 **`onTimeout:'deny'` 强制项**)+ §11 变更记录补 09-26 行 |
**示例覆盖的判据**(自检逐条断言,⛔ 不是推断):
① 登记成功 + 拼上实例凭据;② **送上平台的 manifest 无函数**(`check`/`onEvent` 已被 SDK 剔除);
③ **非当前回合者 ⇒ 回 `{ok:false}` 且 `detail` 可读**;④ 轮到本人 ⇒ **放行**(防规则把正常发言也卡死);
⑤ `event.deliver` 送达 ⇒ `onEvent` 真执行 ⇒ 出 1 帧;⑥ **未接线(缺 baseUrl/token)既不抛、也不启长轮询**。
**约束自查**:⛔ 内核四档**一行未动**(本棒**只动 `poc/` 与文档**,零 `src/` 改动);
⛔ 未 commit / push;⛔ 未动其它工作区文件。
⚠️ **如实登记**:① 本件(`接续包_IM反向通道实现_20260926.md`)**已被本棒改写** ⇒
**其 md5 已变**(开工时 = `935c7180b92867e0e2ba20de57e0ae5e`)⇒ 下一棒**必须重算**,⛔ 别拿旧值校验。
② §8-2(表声明入口口径)**仍未裁**。
@@ -0,0 +1,123 @@
# 接续包 · IM 线 · 2026-09-26 00:32
## 接续点 · IM 线(插件接入面收尾 + 文档 + 对接答复) · 2026-09-26 00:32
- **来源会话**: 本会话(阿里云 dsh 平台开发会话) | **结束原因**: 上下文 32.8 万强制收口
- **原目标**(用户本轮原话,不翻译、不缩写):
1. 「**创建接续会话把IM的改造优化处理完成**」
2. 「**然后更新IM接入文档**」
3. 「**并回复插件的对接文档**」
- **基线**: HEAD=`e6207aa`(`D:/github/dsh_shenxian`,**未 commit**) | 远端 master = 未核(本会话未 push) | 域锁 = **无**(已释放)
- **产物**(新会话必须逐一确认存在):
- 代码面(`D:/github/dsh_shenxian`):`src/im/sdk/types.ts`(+206)· `src/im/sdk/host.ts`(+362)· **`src/im/sdk/binding.ts`(新)** · **`src/im/sdk/registry.ts`(新)** · `src/im/sdk/index.ts` · `src/im/hub.ts`(+36)· `src/im/ws.ts`(+84/−1)· `src/web/routes/im.ts`(+368)· **`sdk/im-plugin-host/index.mjs`(新)** + `package.json`
- 测试(新):`test/im-plugin-bridge.test.mjs`(14 例)· `test/im-backpressure.test.mjs`(8 例)
- 文档库(`D:/github/dsh_shenxian/dsh-server-docs`):`01-规范/09-IM插件SDK与扩展点契约.md`(§2 加第 8 项 · §3.5 接线器 · §6 修事务口径 · §8 判据四档 · §9 出向端口 · **§10 跨进程接入** · **§11 投递可靠性与容量**)· `01-规范/08-插件开发与对接规范.md`(修路由指针 + SDK 获取 + 09 指针 + §11 变更记录)
- 评估/交付件(本工作区 `交付物/`):`IM插件接入面-20260925.md` · `IM插件接入完备性审计-20260925.md` · `用户组网群聊可行性评估-20260925.md` · `端口与容量瓶颈评估-20260925.md` · `端口决策与设备中继可行性-20260926.md` · `扇出背压修复-20260926.md`
- **校验命令**: `cd /d/github/dsh_shenxian && npm run build && node --test test/im-plugin-bridge.test.mjs test/im-backpressure.test.mjs 2>&1 | tail -5`
→ **期望输出**: 两个文件合计 **22 例、`fail 0`**(bridge 14 + backpressure 8)
- **未完成**(用户要的、还没做的 —— 优先于 AI 自己加的收尾):
> ## ✅ 本棒(「IM接入面收尾-20260926」· 2026-09-26 00:38–01:0x)收口状态
>
> **已完成**
> - **1-① 端到端真跑 ✅**(真起平台 + 真 HTTP + 插件侧 SDK 走 5 端点):
> 脚本 `tmp/im-bridge-e2e-20260926.mjs`,**23 例 / fail 0**。⇒ 跑法 `cd /d/github/dsh_shenxian && node <脚本绝对路径>`。
> - **1-② 部署到 47 ✅**:推 `lib/im/**` + `lib/web/routes/im.js` + `sdk/**`(⛔ 未带别的线的未部署改动)。
> 47 侧备份 `/opt/dsh/backups/im-bridge-predeploy-20260926.tgz`;重启 `dshs` = active;
> `/api/im/plugins/bridge` 与 `/api/im/stats` 均 **401(= 路由已加载,非 404)**。
> ⚠️ **落地文件属主**:tar 会保留本地 uid ⇒ 47 上新文件曾变 `197108`,**已 chown -R root:root 修回**。
> (⛔ 下棒再推时给 tar 加 `--owner=root --group=root`,或推完立刻 chown。)
> ⚠️ 47 上另有**先前就存在**的非 root 文件(`lib/web/routes/business-plugins.js`、`admin.js`、
> `essential-plugins.js` 等)——**非本棒落地、未动**,仅登记。
> ⛔ **106 未部署**(IM 宿主在 Manager 进程,Worker 不带 IM 路由)⇒ 按需为「不需要」。
> - **3 更新 IM 接入文档 ✅(部分)**:09 新增 **§12 插件作者快速上手**(五步 + 可跑片段 +
> 能做/不能做 + 四个必知坑 + 反向通道降级三条)。⛔ **未动任何已落章节**(§2–§11 一字未改)。
> ❌ **仍缺**:可跑的最小**示例工程**(`poc/business-plugins-im/` 三例仍是旧契约)。
> - **4 插件对接答复件 ✅**:本工作区 `交付物/插件对接答复件-20260926.md`(⛔ 未改 `dsh-plugin-partment` 一字节)。
>
> **🔴 本棒抓出并修掉一个真缺陷(新增,不在原接续包内)**
> - **症状**:插件走完登记后 `frame()` / `say()` **恒 `plugin-unavailable`**。
> - **根因**:`routes/im.ts` 的 `bridgeAuth` 把**请求头里的包名**(`@dsh-local/im-plugin-demo`)
> 直接当内部 id 去查注册表,而注册表按 `pluginIdOf(包名)`(`im_plugin_demo`)存 ⇒ **恒查不到**。
> ⇒ **出向面在生产上不可达**(第 8 项等于白做)。
> - **为什么单测没抓到**:`test/im-plugin-bridge.test.mjs` **刻意不碰真实网络**(mock fetch +
> 直接把归一化 id 递给注册表)⇒ **从不经过这一层翻译**。只有真 HTTP 才暴露。
> - **修法**:`bridgeAuth` 现同时交 `packageName`(供"头与清单自洽"判)与 `pluginId`
> (供注册表查);两处 `outboundPortFor` 改用 `auth.pluginId`。**改动仅限 `routes/im.ts`**
> (内核四档不变:`store.ts` 零改动 / `hub.ts` 纯插入 / `ws.ts` 只动投递点 / `routes/im.ts` 纯插入 ——
> 本次属"修 bug"那类,故允许动既有逻辑,须有回归证据)。
> - ⚠️ **回归证据未入库**:现为 `tmp/` 脚本(接续包原口径即"临时脚本落 tmp/")。
> 若要**永久回归**⇒ 下棒需把它落成 `test/im-plugin-bridge-e2e.test.mjs` + 在 `package.json`
> 的 `test`/`verify` 各登记 1 处(A=B 各 1 行,⛔ 用 Edit 精确替换)。
>
> **⛔ 仍未完成(下棒依据)**
> - **1-③ 生产标定背压阈值 ❌ 未做(缺真实读数)**:`/api/im/stats` 现为 **401(admin 专属)**,
> 无 admin 会话读不到 `peakPendingBytes`;且当前无真实流量 ⇒ 读数恒 0,**改了也是拍脑袋**。
> ⇒ **阈值保持 4 MiB 不变**(保守起点),**改参数入口已就绪**(`IM_SEND_BUFFER_LIMIT_BYTES`,可注入)。
> 标定法(拿到 admin 会话或真实流量后照做):连读 `GET /api/im/stats` 的 `peakPendingBytes` 一段时间,
> 取其分位值作为阈值下限,改 drop-in 环境变量 → `systemctl restart dshs`(⛔ **不改代码**)。
> - **2 反向通道** —— 🔴 **2026-09-26 用户已拍板:必须补、且要做完整**
> (原话「**都需要实现完整,才能让插件接入,否则两边都要返工**」)。
> 本棒据此**改了 09 §10.4 / §10.5 / §12.5 的口径**(从"本期不支持"→"已定必须补")+
> 同步 `交付物/IM插件跨进程桥-20260925.md §5/§6` 与 `交付物/插件对接答复件-20260926.md`,
> 并产出**施工规格** `接续包_IM反向通道实现_20260926.md`。
> ⚠️ **实现交执行棒**(本棒受会话预算限制,⛔ 未动任何代码);⛔ 原文"不许拍成已定"的禁令**至此解除**
> —— 因为**是用户本人拍的**,不是新会话替原会话拍的。
> - **§1 对接单未消的 4 条**(纯文档):**#2 表声明入口(P1,需先定口径)** · #4 移动端 ·
> #5 错误码/自测清单/变更记录 · #8 内部编号 107/108。
> ⚠️ **接续包原记「P1×2 已消」不准**:实测 **#1 已消、#2 未消**(09 §4 仍有 `tables: [{` 且全篇无 `dsh.data.schema`)。
1. 🔴 **IM 改造的收尾三件**(= 用户「处理完成」的落地口径,按序):
① **端到端真跑验收** —— 桥的 5 个端点(`/api/im/plugins/{register,unregister,out/frame,out/message,bridge}`)**只过了单测,从未真跑**;需在本机起平台+用 `sdk/im-plugin-host` 走一遍真 HTTP。
② **部署到 47(+按需 106)** —— 本机代码面完成,**生产未部署**(R8:开发环境服务器,动手前一句话说明即可)。
③ **生产标定背压阈值** —— 现为保守起点 4 MiB;观测面 `GET /api/im/stats` 的 `peakPendingBytes` 拿到真实读数后**改参数**(⛔ 不改代码)。
2. 🔴 **「反向通道」是否补 —— ⛔ 属待用户拍板,**不许新会话替原会话拍成「已定」**:
- 现状:**「插件单向发起」已通**(登记 / 读 / 出向);**「平台回头叫插件」未通**(`speakRules.check` 发言规则、`events.onEvent` 事件回调)。
- 原会话判断(**可推翻**):「**不必急着补**」—— 已有降级三条(规则参数写房间 `config` / 出向主动纠偏 / `host.subscribe()` 轮询替代事件回调,后者已实测够用)。
- 若要补 ⇒ 需要**平台 → 实例的反向调用面**(含鉴权、超时、实例可寻址性),是新工作量。
3. **更新 IM 接入文档**(用户第 2 件事):`09` 已更新到 §11,但**缺一份「插件作者快速上手」** —— 现在用法散在 §10.3,且**没有可跑的最小示例工程**(`poc/business-plugins-im/` 三个示例是**旧契约**,未含第 8 项出向与跨进程桥)。
4. **回复插件对接文档**(用户第 3 件事):即 `E:/ProgramData/AIProject/dsh-plugin-partment/对接单_平台侧-IM板块待优化与Macro改造可行性_20260925.md`。⚠️ **那是别的工作区 ⇒ ⛔ 只能在本工作区产出「平台侧答复件」,由用户转交,⛔ 不得直接改那边一个字节**。答复要覆盖:§1 九条(P1×2 + P2×3 **已消**,P2×2 + P3×2 待定)+ §2 Macro 改造可行性(第 8 项出向端口**已具备**,§2-3-1 高频广播**已解决**:`frame` 档不落库)+ §4 两个待拍板项的**平台侧回答**。
- **下一步**(第 1 个动作 = 具体命令):
1. **先跑校验命令**(见上)—— 不符即停、只报告。
2. 读 `交付物/IM插件跨进程桥-20260925.md` 与 `交付物/扇出背压修复-20260926.md`(本会话两份最新落地件,含未完成项与设计取舍)。
3. **端到端真跑**(未完成 1-①):本机起平台(`DSH_IM_WS` 默认开)→ 写一个临时脚本用 `sdk/im-plugin-host` 的 `createImPluginHost()` 走 `register → frame → say` → 断言 `ok:true`(**临时脚本一律落 `tmp/`,⛔ 不入库**)。
4. 部署(未完成 1-②)→ 标定(1-③)→ 文档(3)→ 答复件(4)。
5. ✅ **未完成 2(反向通道)—— 用户已表态**(2026-09-26:必须补、做完整)⇒
**下棒依据改为 `接续包_IM反向通道实现_20260926.md`**(⛔ 不再是"先不碰")。
- **关键决定**(已定项 + 为什么 —— 防新会话推翻重来):
① 🔴 **插件跑在「用户实例内」、IM 宿主在「平台进程」⇒ 跨进程** —— 依据:用户自配模型是起实例时写进实例的(`credential_vault` → spawn 写 `settings.yaml`),平台侧拿不到 ⇒ 插件必须在实例内。
② **桥 = 插件「拨出」而非平台「打进」** —— 符合既有"拨出式"纪律(`09 §2-6`:⛔ 插件不开监听端口),且断连可退化为普通聊天。
③ **登记只收「声明面」** —— `check`/`onEvent`/`render`/`onAction` 是函数、跨不了进程;函数侧回调留在实例内。
④ **出向发言多一道闸** —— bot 必须是该房成员(同进程时必然成立,跨进程必须自己查);未注入成员判定 ⇒ **fail-closed 拒**。
⑤ 🔴 **背压:慢客户端不虚减 `delivered`** —— `write()` 返回 false = 水位满(**数据已排队、会送达**)⇒ 若记成丢帧会污染观测。**若用户要"慢客户端单独计 skipped"⇒ 是语义变更、需重新拍板**。
⑥ **判据分两类**(已写进 `09 §8`):**新增能力 ⇒ 纯插入**;**修 bug ⇒ 可改既有逻辑**,但须「回归用例 + 新行为可观测 + 阈值可注入」。内核四档:`store.ts` 零改动 / `hub.ts` 纯插入 / `ws.ts` 允许改但**被删行只限投递点** / `routes/im.ts` 纯插入。
⑦ **端口 D8 = 用户拍板「先不开,后续有需要再开」**(09-26)⇒ ⛔ 不当待办、⛔ 不催;群聊全走 443 中继,功能完整。
⑧ **设备当中继**:路径已存在(桌面端节点接入已实测),但**中继方需"别人能连到它"**=用户侧 NAT 门槛;**推荐替代 = 设备主动拉**(⛔ 不需任何端口)。⛔ 未排期。
- **回滚点**:`D:/github/dsh_shenxian` HEAD=`e6207aa`(**本会话全部改动未 commit** ⇒ `git checkout -- <file>` 即可回到干净基线);`dsh-server-docs` 同理;部署前须另做 47 侧备份(`/opt/dsh/backups/`)。
- **⛔ 不要重做**:
- **扩展点第 8 项(出向端口)+ 面板动作 + 分片工具**(types/host 已落,22 例测试覆盖)
- **跨进程桥平台侧**(`registry.ts` + 5 端点 + `bridgeAuth`)
- **插件侧 SDK**(`sdk/im-plugin-host/index.mjs`)
- **背压修复**(`ws.ts` 1 行改写 + `hub.ts` 计数 + 8 例)
- **两处规范文档**(09 已到 §11;08 已修指针)—— 只**补**「快速上手」,⛔ 不重写已落章节
---
## §A 本轮(原会话)做了什么 —— 一句话
把「IM 基础」从**插件接不进来**改造成**双向可接入**:① 补出向出口(扩展点 8)② 补跨进程桥(登记 / 出向,插件侧 SDK 齐备)③ 修扇出背压隐患;并把两处规范同步到 §11。
## §B 判据读数(原会话实测,供新会话对照基线)
- `npm run build` **rc=0**
- `npm test` = **626 / 616 过 / 5 败 / 5 跳过** —— ⚠️ **5 败是既有环境问题**(投放线 `test/shared-layer-sync.test.mjs` 的 `spawnSync tar EBUSY`),**与本线无关**,⛔ 不要去修它(属投放线 lane)
- 本线专项:`test/im-plugin-bridge.test.mjs` **14/14** · `test/im-backpressure.test.mjs` **8/8** · `test/im-sdk.test.mjs` **78/75 过 / 3 跳过**(跳过 = `spawnSync git EBUSY`,已改具名跳过)
- `npm run check:layering` **rc=0 ✅ 无新增违规**
- 内核改动形态:`src/im/store.ts` **零改动**;`ws.ts` 被删改的既有行**只有 1 行**(`socket.write(textFrame(text))`,正是背压修复点)
## §C 工具纪律(本线踩过的)
- ⚠️ **测试跑的是 `lib/` 编译产物 ⇒ 改完必 `npm run build`**(原会话踩过:`hub.ts` 新、`ws.ts` 旧混跑 ⇒ 症状像"注入无效",浪费两轮)。定位法 = 打印**两个独立观测点**(注入值 ✅ 但计数为 0 ⇒ 走的是旧代码)。
- ⚠️ 本机 `spawnSync git|tar EBUSY` 会让测试**假失败** ⇒ 取证类用例改 `t.skip` 具名跳过。
- 🔴 **改 `package.json` 一律用 Edit 精确替换**,⛔ 不用「读-改-写整个 JSON」(会重排格式)。原会话实测侥幸未炸,但那是运气。
- 🔴 (「接续包 md5」口径门禁)开工前重算 md5 与 prompt 里的一致才开工;不一致 ⇒ 停手报告。
@@ -0,0 +1,43 @@
# 接续包 · 手机操作 DSH 会话(Agents Anywhere 复用线)
来源会话 | `session-0ed72449…`
结束原因 | 水位三级 ⇒ 按纪律落包交接;任务本身**只剩用户侧两步**
原目标(用户原话,不翻译)|「把"手机操作 DSH 会话"做成**可安装的底层插件能力**:任何人装上这个插件,就能在手机上看到自己电脑的 DSH 会话并回复。做法以复用开源项目 Agents Anywhere(MIT,server/ + web-next/ + connector/ + dsh-bridge-next)为先,⛔ 不重复造轮子」
基线 | 47.77.182.89(= `ai1net.com`)compose 项目 `agents-anywhere`;本机 HEAD `77b9f73`;占用者 = 无
产物(绝对路径,接手方逐一确认存在)
1. `E:\ProgramDSH\.dsh\temp\aa-service\deploy\`(交付源:说明 / 安装 / 检查 / 备份 / 状态 / 二维码)
2. `https://desk.ai1net.com/`(说明、整包 `?v=20260928v`、安装脚本、检查脚本、二维码)
3. 插件包 `agents-anywhere-dsh-bridge-next-…-ai1net.tgz`(762,042 B,sha256 `f04204a1…`)
4. 本机 profile `…\.dsh\profiles\desktop\package.json`(bundles + dependencies 双登记)
校验命令 → 期望输出
`ssh bt-server "curl -fsSL https://desk.ai1net.com/status-phone-access.sh | sh"` → `accounts=0 connectors=0 sessions=0 pairing=0 online=0`
`curl -s -o NUL -w "%{http_code}" https://desk.ai1net.com/phone-access-bundle.zip?v=20260928v` → `200`
未完成
1. **用户侧**:手机打开 `https://desk.ai1net.com` → 首次安装页点「使用 DSH 账号 登录」→ 门户口令登录(账号自动创建)
2. **用户侧**:**重启桌面客户端** → 插件面板点「连接」→ 手机扫面板的码 → 手机首屏点「Add device / 添加设备」
3. 未验证的一半:DSH 会话内容能否穿到手机(需插件加载后才可测)
下一步(第 1 条 = 可直接执行)
1. `python E:\ProgramDSH\.dsh\scripts\dsh-state.py --ws E:\ProgramData\AIProject\ai1net-dsh-server`
2. 读 `.dsh\temp\joint\手机看桌面会话与回复_接续包_20260927.md`(sha256 `93f9cb9b…`,含机制结论)
3. 用户做完两步后:跑 `check-phone-access.ps1`(A–E 段),再测会话内容穿手机
关键决定(已定项 + 为什么)
· 复用 AA 实例自托管 + 插件 bundle/dependencies 双登记 + 普通 `uv sync`(详见附录)
回滚点
· profile:删 `dependencies` 里 `@agents-anywhere/dsh-bridge-next` 一行
· 实例:`docker restart agents-anywhere-server-next-1`;安装脚本带 `-Uninstall`
⛔ 不要重做
· 不重装/重打包插件(762,042 B 已验;重打包会踩 `prepack` 与 `lib/bundled-connector`)
· 不给 `uv sync` 加 `--frozen`;不用 `& uv … 2>&1` 取退出码(必假绿,用 `Start-Process`)
· 不改发布顺序(备份 → 语法检查 → 干净环境实跑 → 才发布)
· 不碰旧 home `C:\Users\Administrator\.dsh`;不动平台 nginx;不改官方 dsh 主程序
· 不把「关掉再打开插件」写回文档(面板无此开关,只能重启客户端)
## 附录(跨宿主接手方需要的内联状态)
- 台账:`E:\ProgramDSH\.dsh\temp\joint\手机看桌面会话与回复_路线_20260927.md`(逐轮留痕,含每轮读数与报错原文)
- 关键决定全文:① 复用 AA 自托管(⛔ 不造轮子 ② ⛔ 不建第二套中继)③ 插件以 bundle + `dependencies`(`link:`)双登记 ⇒ 面板可见可开关 ④ 安装脚本用普通 `uv sync`(自带连接器**无 lockfile**,加 `--frozen` 必失败)⑤ `UV_CACHE_DIR` 指向 `stateRoot\uv-cache`(绕开本机 `os error 5`)⑥ 采 uv 退出码必须 `Start-Process -Wait -PassThru`
- 目标四段现状:① 实例已托管 ✅ ② 插件已打包并安装 ✅(待重启生效)③ 交付面齐 ✅ ④ 端到端**未完成**(卡用户两动作)
- 已验证证据:SSO 往返;真实连接器上线(`wss://desk.ai1net.com/api/v2/connector/ws` + `connector.heartbeat`,服务端 `online=1`);配对/领取;安装脚本装/卸/幂等;校验和被篡改拒绝;多用户 201/200/403;备份 29 表 ~41 KB;整包双校验和;全部链接 200
- 安装脚本干净环境实测(临时 USERPROFILE):自带 uv `rc=0`、`Installed 54 packages`、`connector venv ready : True`
- 客户端本地面:`9333` = CDP(node 直连 `http://127.0.0.1:9333/json` 可取证/操作界面);`9329/9330` 非 HTTP;`19387` 需令牌
- 插件加载机制:管理器只列 `dependencies`;纯 bundle 只在实例启动时装配 ⇒ 生效途径 = **重启客户端**
- 约束:⛔ 不改官方 dsh;⛔ 不建第二中继;⛔ 不影响既有配对/登录;本线未 commit / 未 push;凭据不落文件
@@ -19,4 +19,4 @@ td.c{color:var(--ink2);word-break:break-all}
.sect{margin-top:6px}
.hint{font-size:13px;color:var(--muted);margin:6px 0 0}
.hl{background:#fff3c4;padding:0 3px;border-radius:3px}
</style></head><body><div class="wrap"><div class="card"><h1>会话上下文逐条清单 —— 「评估代码清理影响并整理文档」</h1><p class="lead">会话 <code>7057685c</code> | 取<b>第 4 轮请求时点</b>(那轮 AI 只回了 136 字)已累积的全部内容。<br>该轮真实 <b>input_tokens = 562,211</b>;转录内可还原内容 <b>1,251,470 字符 ≈ 593113 token</b>(折算 2.11 字符/token,实测校准)。</p><p class="hint">说明:<b>AI 回复只占 3.6%</b>;其余是工具往返(命令本身 + 命令跑出来的结果)。下表逐条列出,按大小倒序 —— 这就是那一轮"付了 56 万 token"的全部内容。</p></div><div class="card"><h2>一、汇总</h2><table><tr><th>类别</th><th class="n">条数</th><th class="n">字符</th><th class="n">≈token</th><th class="n">占比</th></tr><tr><td>工具输出</td><td class="n">426</td><td class="n">766,797</td><td class="n">363,410</td><td class="n">61.3%</td></tr><tr><td>工具入参</td><td class="n">430</td><td class="n">366,003</td><td class="n">173,461</td><td class="n">29.2%</td></tr><tr><td>用户消息</td><td class="n">25</td><td class="n">73,269</td><td class="n">34,724</td><td class="n">5.9%</td></tr><tr><td>AI 回复</td><td class="n">220</td><td class="n">45,401</td><td class="n">21,517</td><td class="n">3.6%</td></tr></table><p class="tools">工具输出内部构成:Bash 179 条 / 471,479 字符(61%) | Read 68 条 / 209,183 字符(27%) | Skill 2 条 / 44,150 字符(6%) | Edit 142 条 / 21,647 字符(3%) | Grep 8 条 / 14,741 字符(2%) | present_files 11 条 / 4,028 字符(1%) | Write 8 条 / 1,072 字符(0%) | TaskCreate 4 条 / 297 字符(0%)</p></div><div class="card sect"><h2>二、工具输出 —— 命令跑出来的结果</h2><p class="hint">426 条 | 766,797 字符 ≈ 363,410 token</p><table><tr><th class="n">#</th><th class="n">字符</th><th class="n">tok</th><th>工具</th><th>内容(截断)</th></tr><tr><td class="n">1</td><td class="n">32,404</td><td class="n">15357</td><td class="k">Skill</td><td class="c">Base directory for this skill: E:\\ProgramData\\.workbuddy\\skills\\dsh-opensource-release # dsh-opensource-re</td></tr><tr><td class="n">2</td><td class="n">14,767</td><td class="n">6998</td><td class="k">Read</td><td class="c">360→ # ⚠️ 这条**不会**误伤代码:模式里必须出现 `档案 NN`,`foo()` 里没有它 ⇒ 安全。 361→ (re.compile(r&quot;\\(\\s*&quot; + ARCHIVE_REF + r&quot;\\s*\\</td></tr><tr><td class="n">3</td><td class="n">13,256</td><td class="n">6282</td><td class="k">Read</td><td class="c">1→# DSH 平台 — 记忆(**状态层**) 2→ 3→&gt; 只记**会变的状态**;规则在根 `CODEBUDDY.md`,细节在 `04-调整方案/`。注入有上限 ⇒ 已压到下限,**加内容前先删等量旧内容**。 </td></tr><tr><td class="n">4</td><td class="n">12,967</td><td class="n">6145</td><td class="k">Read</td><td class="c">589→ # 没有任何其它文件读它们 ⇒ 删除是**自包含**的,由 `tsc` 验证。 590→ # ⚠️ `DeployMode` 收窄为 `&#x27;local&#x27;` 后,`=== &#x27;k8s&#x27;` 的比较会变成 TS 类型错误</td></tr><tr><td class="n">5</td><td class="n">12,518</td><td class="n">5932</td><td class="k">Read</td><td class="c">140→ &quot;manual/security.md&quot;, 141→ &quot;manual/api.md&quot;, 142→ &quot;manual/faq.md&quot;, 143→ &quot;manual/project.md&quot;, 144→ &quot;manual/</td></tr><tr><td class="n">6</td><td class="n">12,387</td><td class="n">5870</td><td class="k">Bash</td><td class="c">Stdout: === 实际改动内容 === diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6498fd0..8</td></tr><tr><td class="n">7</td><td class="n">12,186</td><td class="n">5775</td><td class="k">Read</td><td class="c">1→# 待清理:K8s 残留清单(导出物) 2→ 3→&gt; **对象**:`dsh-users-platform`(开源副本,提交 `dee37cb`) 4→&gt; **扫描口径**:`k8s|K8s|KUBERNETES|k</td></tr><tr><td class="n">8</td><td class="n">11,746</td><td class="n">5566</td><td class="k">Skill</td><td class="c">Base directory for this skill: E:\\ProgramData\\.workbuddy\\skills\\dsh-decision-method # dsh-decision-method </td></tr><tr><td class="n">9</td><td class="n">10,012</td><td class="n">4745</td><td class="k">Bash</td><td class="c">Stdout: === 实例已装插件的真实路径 === === 版本与是否含新 section === === 当前在跑的实例 scope === dsh-provision.path loaded active wai</td></tr><tr><td class="n">10</td><td class="n">8,043</td><td class="n">3811</td><td class="k">Read</td><td class="c">1→&lt;!doctype html&gt; 2→&lt;html lang=&quot;zh-CN&quot;&gt; 3→&lt;head&gt; 4→&lt;meta charset=&quot;utf-8&quot; /&gt; 5→&lt;meta name=&quot;viewport&quot; content=&quot;w</td></tr><tr><td class="n">11</td><td class="n">7,720</td><td class="n">3658</td><td class="k">Read</td><td class="c">1→&lt;!Line truncated
</style></head><body><div class="wrap"><div class="card"><h1>会话上下文逐条清单 —— 「评估代码清理影响并整理文档」</h1><p class="lead">会话 <code>7057685c</code> | 取<b>第 4 轮请求时点</b>(那轮 AI 只回了 136 字)已累积的全部内容。<br>该轮真实 <b>input_tokens = 562,211</b>;转录内可还原内容 <b>1,251,470 字符 ≈ 593113 token</b>(折算 2.11 字符/token,实测校准)。</p><p class="hint">说明:<b>AI 回复只占 3.6%</b>;其余是工具往返(命令本身 + 命令跑出来的结果)。下表逐条列出,按大小倒序 —— 这就是那一轮"付了 56 万 token"的全部内容。</p></div><div class="card"><h2>一、汇总</h2><table><tr><th>类别</th><th class="n">条数</th><th class="n">字符</th><th class="n">≈token</th><th class="n">占比</th></tr><tr><td>工具输出</td><td class="n">426</td><td class="n">766,797</td><td class="n">363,410</td><td class="n">61.3%</td></tr><tr><td>工具入参</td><td class="n">430</td><td class="n">366,003</td><td class="n">173,461</td><td class="n">29.2%</td></tr><tr><td>用户消息</td><td class="n">25</td><td class="n">73,269</td><td class="n">34,724</td><td class="n">5.9%</td></tr><tr><td>AI 回复</td><td class="n">220</td><td class="n">45,401</td><td class="n">21,517</td><td class="n">3.6%</td></tr></table><p class="tools">工具输出内部构成:Bash 179 条 / 471,479 字符(61%) | Read 68 条 / 209,183 字符(27%) | Skill 2 条 / 44,150 字符(6%) | Edit 142 条 / 21,647 字符(3%) | Grep 8 条 / 14,741 字符(2%) | present_files 11 条 / 4,028 字符(1%) | Write 8 条 / 1,072 字符(0%) | TaskCreate 4 条 / 297 字符(0%)</p></div><div class="card sect"><h2>二、工具输出 —— 命令跑出来的结果</h2><p class="hint">426 条 | 766,797 字符 ≈ 363,410 token</p><table><tr><th class="n">#</th><th class="n">字符</th><th class="n">tok</th><th>工具</th><th>内容(截断)</th></tr><tr><td class="n">1</td><td class="n">32,404</td><td class="n">15357</td><td class="k">Skill</td><td class="c">Base directory for this skill: E:\\ProgramData\\.workbuddy\\skills\\dsh-opensource-release # dsh-opensource-re</td></tr><tr><td class="n">2</td><td class="n">14,767</td><td class="n">6998</td><td class="k">Read</td><td class="c">360→ # ⚠️ 这条**不会**误伤代码:模式里必须出现 `档案 NN`,`foo()` 里没有它 ⇒ 安全。 361→ (re.compile(r&quot;\\(\\s*&quot; + ARCHIVE_REF + r&quot;\\s*\\</td></tr><tr><td class="n">3</td><td class="n">13,256</td><td class="n">6282</td><td class="k">Read</td><td class="c">1→# DSH 平台 — 记忆(**状态层**) 2→ 3→&gt; 只记**会变的状态**;规则在根 `CODEBUDDY.md`,细节在 `04-调整方案/`。注入有上限 ⇒ 已压到下限,**加内容前先删等量旧内容**。 </td></tr><tr><td class="n">4</td><td class="n">12,967</td><td class="n">6145</td><td class="k">Read</td><td class="c">589→ # 没有任何其它文件读它们 ⇒ 删除是**自包含**的,由 `tsc` 验证。 590→ # ⚠️ `DeployMode` 收窄为 `&#x27;local&#x27;` 后,`=== &#x27;k8s&#x27;` 的比较会变成 TS 类型错误</td></tr><tr><td class="n">5</td><td class="n">12,518</td><td class="n">5932</td><td class="k">Read</td><td class="c">140→ &quot;manual/security.md&quot;, 141→ &quot;manual/api.md&quot;, 142→ &quot;manual/faq.md&quot;, 143→ &quot;manual/project.md&quot;, 144→ &quot;manual/</td></tr><tr><td class="n">6</td><td class="n">12,387</td><td class="n">5870</td><td class="k">Bash</td><td class="c">Stdout: === 实际改动内容 === diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6498fd0..8</td></tr><tr><td class="n">7</td><td class="n">12,186</td><td class="n">5775</td><td class="k">Read</td><td class="c">1→# 待清理:K8s 残留清单(导出物) 2→ 3→&gt; **对象**:`dsh-users-platform`(开源副本,提交 `dee37cb`) 4→&gt; **扫描口径**:`k8s|K8s|KUBERNETES|k</td></tr><tr><td class="n">8</td><td class="n">11,746</td><td class="n">5566</td><td class="k">Skill</td><td class="c">Base directory for this skill: E:\\ProgramData\\.workbuddy\\skills\\dsh-decision-method # dsh-decision-method </td></tr><tr><td class="n">9</td><td class="n">10,012</td><td class="n">4745</td><td class="k">Bash</td><td class="c">Stdout: === 实例已装插件的真实路径 === === 版本与是否含新 section === === 当前在跑的实例 scope === dsh-provision.path loaded active wai</td></tr><tr><td class="n">10</td><td class="n">8,043</td><td class="n">3811</td><td class="k">Read</td><td class="c">1→&lt;!doctype html&gt; 2→&lt;html lang=&quot;zh-CN&quot;&gt; 3→&lt;head&gt; 4→&lt;meta charset=&quot;utf-8&quot; /&gt; 5→&lt;meta name=&quot;viewport&quot; content=&quot;w</td></tr><tr><td class="n">11</td><td class="n">7,720</td><td class="n">3658</td><td class="k">Read</td><td class="c">1→&lt;!Line truncated
@@ -2,6 +2,7 @@
> 2026-09-16 立。来源 = 复盘会话 `78ac724f`(「查看 dsh 项目待办事项」)**最后 6 轮**的真实操作与失败。
> 适用:任何会话接近/超过上下文预算,需要"换会话继续"的场景。
> 🔴 **2026-10-01 用户口径(最新 · 覆盖全文)**:**「接续会话 时间缩短 3-4 分钟即可」** ⇒ 本文件中一切「收口 + 5~8 分钟」**一律按 3~4 分钟读**(原值**作废**)。
---
@@ -0,0 +1,45 @@
# 交付记录 — 官方账号(DSH 账号)登录 · 2026-09-27
> 入口与断点:[接续入口_官方账号登录_20260927.md](../接续入口_官方账号登录_20260927.md)
> 死结与依据:[官方账号登录_死结与结论_20260927.md](E:\ProgramDSH\.dsh\temp\joint\官方账号登录_死结与结论_20260927.md)
## 一句话
把「手机用 DSH 官方账号批准扫码配对」从"服务器侧补登录"(**实测不可行**)改成"**桌面端当官方 OAuth 回环客户端**",平台侧只做可验证的身份确认与既有 device-grant 绑定。**代码层已完成并验证;端到端差两步需人操作**(重启客户端 + 官方页面批准)。
## 关键判据(实测,非推测)
- 官方 OAuth **只接受回环回调**:`http://127.0.0.1:PORT/oauth/callback`、`http://localhost:PORT/…` ⇒ 200;`https://ai1net.com/oauth/callback` 与 `https://platform.deepseek.com/oauth/callback` ⇒ `{"code":50005,"msg":"PAYLOAD_SCHEMA_MISMATCH"}`。
- 方法名与端点:`POST {platform.deepseek.com}/auth-api/v0/dsh/auth_init|auth_exchange|auth_cancel`;信封口径 `{code:0,data:{biz_code:0,biz_data:…}}`(判据 = 官方 `protocol.ts:76/219-224`)。
- PKCE `code_verifier` **留在平台侧** ⇒ 身份由平台可验证;官方 token 只经平台内存,⛔ 不落盘/不进日志/不回浏览器、桌面端全程看不到。
## 交付物(绝对路径)
| 位置 | 内容 |
|---|---|
| `D:\github\dsh_shenxian\src\web\routes\dsh-oauth.ts` | 两跳:`/api/dsh-oauth/begin`(只放行回环、服务端持 verifier)/`/api/dsh-oauth/complete`(校 state 单次有效 ⇒ 换码 ⇒ 官方身份转平台账号 `role='pending'` ⇒ 带 `pairCode` 时**内部调既有** `/api/overlay/pair/approve` 完成绑定) |
| `D:\github\dsh_shenxian\src\web\server.ts` | 注册(`:92` import、`:1350` register) |
| `D:\github\dsh_shenxian\test\dsh-oauth.test.mjs` | 夹具 8 例(公网拒/缺参拒/回环起授权且不回显 PKCE/陌生 state 拒/换码且不许重放/官方 biz_code≠0 不回原文/带码绑定成功/带码绑定失败 409 不夹带凭据) |
| `E:\github\dsh-client\packages\device-shim\lib\oauth-dsh.js` | 桌面端两路由:`GET /oauth/dsh/start`(请平台起授权并 302)+ `GET /oauth/callback`(校 state、收码、转交平台,配对码可选透传) |
| `E:\github\dsh-client\packages\device-shim\lib\index.js` | 接入(`:52` import、`:297` 路由) |
| `E:\github\dsh-client\packages\device-shim\lib\pair-page.js` | 面板入口「用 DSH 官方账号绑定」,出码后自动带 `pairCode` |
| `E:\github\dsh-client\packages\device-shim\src\index.ts` | 设计说明同步(本仓 lib/src 手工同步约定) |
| 可复跑脚本 | `E:\ProgramDSH\.dsh\temp\joint\` 下 `oauth-dsh-smoke.mjs`(9/9)、`oauth-dsh-realchain.mjs`(真网络 ⇒ 302 到官方授权页)、`pair-panel-render-check.mjs`(7/7) |
## 复验读数(2026-09-27 收口时)
- 桌面端冒烟 **PASS=9 FAIL=0**|真链路 **HTTP 302** 到 `platform.deepseek.com/dsh/authorize?authorize_id=…`|面板渲染 **PASS=7 FAIL=0**(含新入口、原型令牌在、无自造色板)
- 平台夹具 **18/18**(`dsh-oauth` 8 + `dsh-account-login` 10,两者均已挂进 `npm test`)|`tsc -p tsconfig.json --noEmit` rc=0
- 共存门禁:登记控制台 **PASS=30 FAIL=0**、设备通道 **PASS=15 FAIL=0**
- 线上:`POST /api/dsh-oauth/begin`(回环)⇒ **200** 回真 `authorizeUrl`;公网回调 ⇒ **400** `redirect-uri-not-loopback`;`GET` 同路由 ⇒ 404(该路由只支持 POST)|`dshs` active
- 线上产物 md5:`lib/web/routes/dsh-oauth.js` = `f0490dc44ec8f9ba3d5c9d0605bf886f`(与本地逐字一致)
## 部署与回滚
- 部署方式:**最小补丁**(只替换改动件)+ 先备份,⛔ 不整树替换。备份目录:`/opt/dsh/backups/dsh-oauth-1558/`、`dsh-oauth-1602/`、`dsh-oauth-bind-1606/`(最新)。
- 回滚:把备份里的 `lib/web/*` 原样拷回 `/opt/dshs/lib/web/` + `systemctl restart dshs`。
- 插件侧:**未提交** ⇒ `git` 工作区回退即可。
## 未完成(需人操作,⛔ 不是本棒能代做的)
1. **重启桌面客户端** ⇒ 面板新入口与 `/oauth/*` 生效(插件改动不走热加载)。
2. 面板出码 → 点「用 DSH 官方账号绑定」→ 在官方页面**点批准** ⇒ 面板应显示「已绑定」。
3. 随后核两条验收:**凭据四件与登记控制台产物同形** + **中继里该 uid 设备 `online=true` 且端口含 20090**。
4. 现状(第 115 轮检测):客户端**未重启**(面板无新入口、`/oauth/dsh/start` 404),用户身份凭据根 `E:\dsh-worker-dev-user\overlay` **只有 `node.key`** ⇒ 尚未发生任何绑定。
## 顺带修掉的既有缺陷
- 平台侧 `dsh-account.ts` 原本因**公网回调**必然失败(官方 `code:50005`),其登录入口已从门户登录页**撤下**(登录页回退到改动前 6,932 B 版本,md5 `4998972d4af0ed1fa9a6d57010b44c00`),路由保留待用。
+66
View File
@@ -0,0 +1,66 @@
# 工作区目录说明 — ai1net-dsh-server
> 整理日期:2026-10-05
> 判据:**按「类别 + 作用」分层** —— 活跃件在根、过程件进 tmp、定稿件进 docs/交付物、历史件进归档。
## 一、根目录(只放「活跃核心件」)
| 项 | 作用 |
|---|---|
| `CODEBUDDY.md` | 🔴 **会话作业规则**(单一事实源,自动注入) |
| `README.md` | 项目说明入口 |
| `AGENTS.md` | 代理协作说明 |
| `state.py` | 平台状态脚本 |
| `.gitignore` | 入库规则 |
| `start-board.bat.备用` / `start-collab.bat.备用` | 看板/协作启动(备用) |
| `会话机制-一键开关.bat` | 会话机制启停入口 |
| `.codebuddy/` | 宿主规则与配置 |
| `.workbuddy/` | 🔴 **记忆 / 协作机制 / 技能副本**(⛔ 知识资产,勿删) |
| `.git/` | 版本控制 |
## 二、按作用分层的目录
| 目录 | 作用 | 判据 |
|---|---|---|
| `docs/` | **定稿文档**(按主题分 11 类) | 结论已定、跨会话可查 |
| `交付物/` | **正式交付件** | 要交给人/给外部看的 |
| `执行会话/` | **目标会话目录**(`目标-<线>-<id>/`) | 机制运行时生成 |
| `归档/` | **历史件**(退役件、旧快照、接续历史) | 已失效但需留痕 |
| `tmp/` | **过程产物**(探针 / 输出 / 备份) | 一次性、可再生 |
| `待清理/` | **待用户处置件** | 拿不准去留的暂存 |
## 三、`tmp/` 子分类(过程产物,568 → 8)
| 子目录 | 装什么 | 判据 |
|---|---|---|
| `tmp/probes/` | 一次性探针脚本(`.py` / `.ps1` / `.js`) | 跑完即弃 |
| `tmp/out/` | 命令输出(`.txt` / `.out` / `.log` / `.json` / `.html`) | 取证快照 |
| `tmp/bak/` | 各类改前备份(`*.bak-*`) | 改动留痕 |
| `tmp/ws/` | 测试用临时工作区 | 自检夹具 |
| `tmp/shots/` | 截图 | 视觉取证 |
| `tmp/supervise-inbox/` | 🔴 **常驻协作程序收件箱** | ⛔ 活文件,勿动 |
| `tmp/supervise-bg.log` | 🔴 常驻后台日志 | ⛔ 活文件 |
| `tmp/board-serve.err.log` | 🔴 看板服务错误日志 | ⛔ 占用中 |
## 四、`归档/` 结构与作用
| 子目录 | 作用 |
|---|---|
| `归档/接续历史/` | **会话接续包按主题归档**(会话机制 / 运行治理 / 文档库 / 机制排查) |
| `归档/代码与机制清退/` | 退役代码 |
| `归档/技能-退役-20261001/` | 退役技能 |
| `归档/技能包-旧件-20261001|02/` | 技能包旧件 |
| `归档/技能包快照/` | 技能包版本快照 |
| `归档/文档整理/` | 文档库历次整理 |
| `归档/迁移/` | 迁移留痕 |
| `归档/配置与备份/` | 配置改动备份 |
| `归档/试验与样例/` | 试验性产物 |
| `归档/手机接入线-概念退役-20260930/` | 退役概念留痕 |
## 五、维护约定
1. **新接续包** ⇒ 直接进 `归档/接续历史/<主题>/`,⛔ **不再堆根目录**。
2. **过程产物** ⇒ 落 `tmp/` 对应子目录,⛔ 不落根目录。
3. **定稿文档** ⇒ `docs/<主题>/`;**交付件** ⇒ `交付物/`。
4. **退役件** ⇒ 加日期后缀进 `归档/`。
5. 🔴 **`tmp/` 是过程区,可随时清**(除三个活文件)—— 见 `pitfalls.md` P0-84。
@@ -29,7 +29,7 @@
- **观测阈值行格式**:``| `OBS-NN` | 指标 | 阈值(引用键或字面值) | 判据 |``
- ✅ **`scripts/overlay-probe.cjs` 的每一个阈值都从本表读**,⛔ 脚本内**不许有魔数**(判据 = 交接单 §6 **E6**)。
- 探针运行方式(**一条命令**,cwd = 工作区根):
`cd "E:/ProgramData/AIProject/aliyun-dsh-server" && node "D:/github/dsh_shenxian/scripts/overlay-probe.cjs"`
`cd "E:/ProgramData/AIProject/ai1net-dsh-server" && node "D:/github/dsh_shenxian/scripts/overlay-probe.cjs"`
---
@@ -823,7 +823,7 @@ RELAY_MAX_HOSTS = floor(C_RELAY × DESIGN_MARGIN) = floor(16700 × 0.45)
`{"degraded":true,"degradedLegs":["joinConf"],"joinConf":{"available":false,"reason":"module-missing","missing":[{"spec":"../lib/net/relay/registry.js",…}]}}` ⇒ 文本行 `join 回读:⛔ 不可用(module-missing)—— 缺 …|⚠️ 这是「没装」不是「没采到」`。**其余子命令与其余读数照常**(106 上 `status` / `punch` / `selfcheck` **三条全通**,`candAcc=3 / candRej=9 / silent=0 / 打洞双向=true / 冷却 300000 / 零值 throws`)。🔴 **⛔ 不许静默返"没有"**:缺块**不填** `direct` / `readback`("没装"与"跑了但读数为空"**形状不同** = 可分)。
- ⚠️ **随之而来的 `OBS-26` 子判据口径(登记在此,§6 行文本本棒 ⛔ 未改 —— 为保 §10 指纹)**:读数生产方**显式**给出 `joinConf.available === false` 时,`OBS-26` 的 join 子判据取 **SKIP + 留痕**(⛔ 不判红,detail 点名缺哪个模块);**老形状(无 `available` 键)走原路径、判定与文本逐字不变**。⇒ 下次有键值变更时**顺带并入 §6 `OBS-26` 行**。
**④ 读数(零回归 + 106 侧)**:探针 **28 PASS / 0 SKIP / 0 FAIL**(rc=0;**代码仓根不给 `--table` 一次 + 收口后工作区根一次,两次同值**)|`npm.cmd test` **201/200/0/1**(逐字同基线;`duration_ms=53315.1989`)|`--scene all`(**无 `--table`,从代码仓根**)**12 PASS / 0 SKIP / 0 FAIL**(rc=0;**6m23s**;`幕4-C` **17240 ms / 30000 ms**;表路径原文 `# 参数表=E:\ProgramData\AIProject\aliyun-dsh-server\参数表_覆盖网络_20260917.md`)。**106 侧**:`status` / `selfcheck --json` / `punch --peer 47.77.182.89:21100` **三条 rc=0**(punch 原文 `窗内零收包(deadline 3000 ms,实耗 3000 ms,发 21 包)⇒ 判死 + 进冷却 300000 ms`)。**探针三处副本 md5 全同** = `4734686b5270059697ccbcf966c34ade`(仓 / 47 `/opt/dshs/scripts` / 106 `/opt/dshs-cluster/scripts`);**回滚点** = 两机 `/opt/dsh/backups/seq43-20260918-150549/`(md5 `8d1ea60a01fc09fb5f56e492afb76fc9`)。
**④ 读数(零回归 + 106 侧)**:探针 **28 PASS / 0 SKIP / 0 FAIL**(rc=0;**代码仓根不给 `--table` 一次 + 收口后工作区根一次,两次同值**)|`npm.cmd test` **201/200/0/1**(逐字同基线;`duration_ms=53315.1989`)|`--scene all`(**无 `--table`,从代码仓根**)**12 PASS / 0 SKIP / 0 FAIL**(rc=0;**6m23s**;`幕4-C` **17240 ms / 30000 ms**;表路径原文 `# 参数表=E:\ProgramData\AIProject\ai1net-dsh-server\参数表_覆盖网络_20260917.md`)。**106 侧**:`status` / `selfcheck --json` / `punch --peer 47.77.182.89:21100` **三条 rc=0**(punch 原文 `窗内零收包(deadline 3000 ms,实耗 3000 ms,发 21 包)⇒ 判死 + 进冷却 300000 ms`)。**探针三处副本 md5 全同** = `4734686b5270059697ccbcf966c34ade`(仓 / 47 `/opt/dshs/scripts` / 106 `/opt/dshs-cluster/scripts`);**回滚点** = 两机 `/opt/dsh/backups/seq43-20260918-150549/`(md5 `8d1ea60a01fc09fb5f56e492afb76fc9`)。
**⑤ 产物** = 工作区根 **`交接单_覆盖网络直连与P2P_20260918.md` §8.10**(**§8 前前缀 `8925c2460810c6dc5cdbc075b6cdaed2` 写入后逐字不变** ✅)。🔴 **D8(云安全组)本棒零动作**(⛔ 未改云侧规则、⛔ 无 `0.0.0.0/0` 形态、⛔ 未索取 / 未硬编码 AK/SK)—— 仍在册待拍板。
@@ -0,0 +1,289 @@
# CODEBUDDY.md 抽取正文 · 规则详解(2026-09-24)
> **用途**:`E:/ProgramData/AIProject/ai1net-dsh-server/CODEBUDDY.md` 压缩后,**解释段 / 历史 / 实证过程 / 案例**的**逐字原文**落在此处。
> **口径**:主文件只留 **判据行 + 本文件指针**;**判据一条未删** —— 凡主文件写「详解见本文件 §X」的,§X 内含该条**原文全文**。
> **权威性**:本文件是**从属件**,不是第二份规则正文 ⇒ 与 `CODEBUDDY.md` 冲突时**以主文件为准**。
> **回滚点**:`E:/ProgramData/AIProject/ai1net-dsh-server/归档/规则A-备份-20260924/CODEBUDDY.md.before-compress`(压缩前全文 23,756 字符)。
---
## §A 提问判据 · 解释与原始口径(对应主文件 §1)
### A-1 「部署 / 上线」属边界内自决(2026-09-13 用户纠正)
> ⚠️ **「部署 / 上线」明确属于上面这一项**(2026-09-13 用户纠正原话:「为什么要等我确认才部署呢,**我看线上效果才知道是否满足需求**」):**不中断在线用户**的上线动作(`scp` 产物到 `/opt/dsh/artifacts` + `ensure-biz-plugins.cjs --all` 换包、静态页改动、候选池投放)**做完即上线,不要问** —— 用户要先看线上效果才能判断需求是否被满足。
### A-2 生产变更门槛(2026-09-13 用户明令)
> **⚠️ 2026-09-13 用户明令:「这个是开发环境服务器,不用担心中断用户」** ⇒ 上一条的「只有会中断在线用户的动作才走 R8 先知会」**改成**:重启 / 停实例 scope / drain / 改配额或 env / 改 nginx·nft **直接做**,只需**动手前一句话说明**在做什么(可回溯用)。**仍未放开**:不可逆的破坏性操作(删数据 / 迁 DB / 清目录)仍先出清单。
### A-3 技术讨论里不谈法规 · 完整口径(2026-09-19 用户明令)
> 🔴 **技术讨论里不谈法规**(2026-09-19 用户明令 —— 起因:我在 relay/中继链讨论里反复引 237 号文并据此上抛。用户原话:「**后续禁止再提 237 号文件**」「**他妈老子是找你来普法的吗**」):
> **⛔ 不引用法规条文当论据**(含条号、文号、判定"落入/不落入适用范围")|**⛔ 不主动提示合规风险**|**⛔ 不把合规写进技术方案的负面清单或验收判据**|**⛔ 不把"要不要做"的理由挂到合规上**。
> ✅ **只在两种情况下才谈**:① **你主动问起**;② 讨论对象本身就是"**对外承诺 / 资质 / 合同**"这类事。
> ⇒ 技术问题**只按技术判据答**(能力/成本/复杂度/可验证性/运维线);价值判断与后果由**你**决定。
> 📌 范围 = **本工作区全部线**(DSH 覆盖网络线 + 分布式数据链路线 + 后续新线)。⚠️ 与既有「**方案只做技术实现,合规不进方案**」是同一条,本条只是把它**扩到全部线并写成硬规则**。
### A-4 上抛门槛 = 存在「真取舍」(2026-09-15 用户明令)
> ⚠️ **上抛门槛 = 存在「真取舍」**(2026-09-15 用户明令):「**需要我确认的方案需要说明优点和缺点,现在没法判断,假如只有优点或只有缺点那不需要我判断**」⇒ 把候选各写 **优点 + 缺点**:某个**只有优点**(明显更优)或**只有缺点** ⇒ **自己拍掉**;只有**各有优有劣、客观标准分不出高下**才上抛,且**必须逐项列出优点与缺点**。
### A-5 回话前自检 · 起因与实证(2026-09-15 加)
> ✅ **回话前自检(发出任何回复前过一遍 · 2026-09-15 加)** —— 起因:**「提问闸门」hook 只能拦 `AskUserQuestion` 工具,而真实的上抛大多发生在正文里**。
> **实证**:本工作区日志 `tool=AskUserQuestion` 调用数 = **09-12: 43 / 09-13: 3 / 09-14: 0 / 09-15: 0**(09-14 起该工具基本不用)⇒ **hook 那条路径几乎不被走到 ⇒ 拦不到正文里的征询**。
>
> 三问重判(正文出现征询句时):
> ① 命中**真门禁**吗(不可逆破坏性操作 / 边界外六类)?**没命中 → 删掉这句,自己做完,改成陈述句**("我接着做 X");
> ② 我是不是在**把已经定下来的事再问一遍**?是 → 删;
> ③ 我要问的这件事,**候选之间是「真取舍」吗**(各有优有劣、客观标准分不出高下)?—— 若某个**只有优点 / 只有缺点** ⇒ **自己拍掉**;是真取舍 → 才允许问,且**一轮只问这一句**、**逐项写出优点与缺点**。
>
> 📌 需要用户拍板时:**位置 = 整条回复的最后一节**(⛔ 不许埋在中间)、**按有序段落逐条编号**、**每个候选必须写「优点 / 缺点」两栏**、**候选竖排成段**(A / B / C **各占一行**,⛔ 不横排、⛔ 不做成表格的列)。用户原话三处:「**能根据决策方法 自行决策的就自决策继续处理,不能决策的问题和需确认内容放在回复的最后,按照有序段落展示**」+「**需要我确认的方案需要说明优点和缺点,现在没法判断,假如只有优点或只有缺点那不需要我判断**」+「**每个需要我决策的问题的潜在解决方案 A B C 也按照段落式排版,别横着排列**」。**形态 = 陈述句**(问题 + 说明 + 各候选优缺点 + 倾向),⛔ **不是**甩征询句("要不要我继续" / "说一声即可")。
### A-6 上抛必须自包含 · 完整细则(2026-09-24 用户明令)
> 🔴 **上抛必须自包含(2026-09-24 用户明令 · **发送前就按规范写**,不靠事后打回)** —— 用户原话:「**应该让 AI 提问时尽可能把关键信息说完整,不要提半截问题没头没尾的,我经常要问这是什么问题**」。**病根 = 上一轮的对话不在用户脑子里**(AI 觉得"这不是刚说过吗",用户只看到一个孤立的半截问题)⇒ **四要素缺一不可**:① **问题** —— 一句话说清要决定什么(⛔ 不用「这个 / 它 / 上述 / 那件事」 —— 每句都要能脱离上下文读懂)② **说明** —— 为什么要你定(用「影响谁 / 断多久 / 花多少钱 / 会不会丢数据」这类**用户能感知**的话,⛔ 不写"涉及 R5 红线"这类内部编号)③ **每个候选写优点 + 缺点**,末行给**倾向** ④ **一轮一问**(同类不连问两次)。
> ⛔ **提问正文不许出现包名 / 环境变量 / 文件路径 / commit / 表名字段名 / 类名函数名** —— 技术细节一律下沉「技术附录」,正文只留能决定下一步的内容("说清楚"≠堆标识符)。
> ✅ **可套用句式(固定五栏 · 竖排;多条时逐条编号 `1、` `2、`)**:
> **问题**:<一句话>。
> **说明**:<影响谁 / 断多久 / 花多少钱 / 会不会丢数据>。
> **A 案**:<做法>(优点:…;缺点:…)。
> **B 案**:<做法>(优点:…;缺点:…)。
> **倾向**:<A 案 或 B 案>。
> ⛔ **五类半截问题(见到即回去重写)**:① 只给结论不给问题(「这个我按 A 做了」)② 用指代(「上面那个表 / 你给的那个方案」)③ 只写差别不写代价(「A 更快,B 更简单」)④ 一个提问捆两件事(红线 + 技术方案一起问)⑤ 术语当主语(用户看不出后果)。
> 📌 **发出前自检(一条就够)**:把这句话**单独递给一个不懂技术的人** —— 他能不能回答?不能 ⇒ 缺上下文,补完再发。细则 ⇒ 技能 `agent-operating-rules` **§1 / §1.1–§1.7**。
### A-7 排版骨架 · 细则
> 📐 **排版按 `dsh-feature-first §5.4`**(可扫读九条 + 形态骨架 + 十三条反模式):**首屏 3 行给判定 · 层级 ≤3 · 每节 ≤7 行 · 加粗只留关键词 · 表格 ≤5 列 · 一条信息只说一次**;**待你拍板项落在最后一节、逐条编号、每个候选带「优点 / 缺点」且竖排成段(不横排、不做成表格的列)**;执行信息 / 报障 / 提问**各有现成骨架,不新造**;细节进「技术附录」,正文只留"能决定下一步"的信息。
### A-8 不将就 / 正向迭代 · 详见主文件 §3 R11
> 🎯 **要的是解决问题,不是将就妥协**(2026-09-15 用户明令):面对风险/缺陷**默认目标是解决**;**降级目标 / 延期 / 静默兜底**三种**都不算解决**。只有**客观不可逾越**(技术不可行 / 上游未支持 / 需你提供凭据或窗口)才允许"暂时接受",且必须写明 ① 卡在哪(证据)② 已做到哪一步 ③ **什么条件一出现必须回头解决**。⚠️ 与 §2「最小代价路径」不矛盾:**目标不打折,路径取最小代价**(细节:素材库 **U27 / U26 / A6**)。
> 🟢 **只做正向迭代**(2026-09-15 用户明令,红线 **R11**):任何改动**只要让项目在某一维度净变差**(目标/方向/架构/功能/性能/安全/交互/UI/便利性/扩展性)⇒ **立即停下复盘**;**拿不出正向做法 ⇒ 立即停止、禁止继续执行**(细节:素材库 **U28**)。
### A-9 规则冲突裁决顺序 · 起因(2026-09-16 加)
> 🔀 **规则冲突裁决顺序**(2026-09-16 加 —— 起因:会话 `ddea70b7`「确认guest用户数据迁移」在"106 旧控制面要不要停"上停下来问用户,用户随后**两次**(U5 / U7)自行要求"删除";复盘见 `.workbuddy/memory/2026-09-16.md`)。
> 裁决序本身(**四层**):
> ① **R8**(开发环境服务器 ⇒ 该动就动,只需动手前一句话说明)→ ② **§1 边界内自决清单**(部署 / 重启 / 改配置 / nginx·nft / drain / 技术选型)→ ③ **§3 其余红线**(R5 权限扩大 / R7 批量写入 / R9 锁 / R10 uid —— 这几条**永远是硬约束**,不参与裁决)。
> ⛔ **冲突 ≠ 门禁**:两条规则打架**不构成**上抛理由;门禁**只有 §1 列的"真门禁"两类**(不可逆破坏性操作 / 边界外六类)。
> 🔑 **"平台级" ≠ "别人的"**(本次误判的根源):**我们自己的 47 / 106 / 本工作区**上的 `dshs*`·`dsh-*` 单元、`/var/lib/dshs/**`、nginx·nft、端口 —— 都是**本平台自己的资源** ⇒ 按 §1 第 27 行 + R8 **直接做**;R7-边界② 说的"只报告不动手"**只针对"别人的 / 归属不明"的对象**。
> ⛔ **禁止把"我有倾向"降级成"建议 + 待你拍板"**:候选能排出优劣 ⇒ **直接做完**并写一句「我选了什么(可推翻)」。
> 📌 **上抛前必答三问**(任一条足以自决,缺一不可全答"否"才允许上抛):① 对象是**我们自己的平台资源**吗?→ 是 ⇒ 自决;② 我**查证过**关键不确定点了吗(如"还有谁在用")?→ 没查 ⇒ 先查,不许把"不确定"当上抛理由;③ 候选排完序,**第一名是否明显更优**?→ 是 ⇒ 自决。
---
## §B 红线 R1–R11 · 解释段原文(对应主文件 §3)
> 主文件 §3 保留**判据行全表**;以下是 R7-边界 / R8 / R9 / R10 / R11 的**解释段全文**(R1–R6 无解释段)。
### B-1 · R7-边界 全文
> **R7-边界**:**R7 只适用于「不是我的 lane」—— 两个方向都别套错**(2026-09-13 用户两处明令)
> ① **不适用于「我 lane 内的执行细节」**:部署 / 上线(换包、传产物、改静态页、候选池投放)、重启服务、改配置、跑自己的脚本、改自己的插件源码与产物 ⇒ **别拿 R7 当挡箭牌去问,直接做**,事后一句「我选了什么(可推翻)」(原话:「为什么要等我确认才部署呢」)。
> ② **适用于「平台级 / 全局 / 别人 lane」**:`/var/lib/**`、全局符号链接、systemd 单元、nginx·nft、别人的 profile / 产物 ⇒ **一律只报告、不动手**,**哪怕改它能让自己流程跑通**(原话:「谁让你去改这个的」+「不是自己负责的任务相关文件不要去改」)。
> ⚠️ **2026-09-16 收口(消除与 §1 第 27 行 / R8 的直接冲突):② 的适用对象 =「别人的 / 归属不明」的对象,⛔ 不含「我们自己的 47 / 106 / 本工作区」的资源** —— **本平台自己的** `dshs*`·`dsh-*` systemd 单元、`/var/lib/dshs/**`、nginx·nft、端口 **按 §1 + R8 直接做**(用户 2026-09-15/16 两次要求"删除 106 旧控制面"即为此)。原表述把"平台级 / 全局"与"别人 lane"并列,导致会话 `ddea70b7` 把**自己的 106 节点**读成"别人的东西"⇒ 只报告不动手 ⇒ 上抛。**判据看"归属",不看"是不是平台组件"**
### B-2 · R8 全文
> **R8**:**中断在线用户的生产变更须先知会** —— ⚠️ **2026-09-13 用户明令修正:服务器 `47.77.182.89` 是「开发环境服务器」,不用担心中断用户** —— 重启 `dshs` / 停实例 scope / drain / 改实例配额或 env / 改 nginx·nft **均可直接做**,不必再等确认。仅保留两条最低自律:① **动手前一句话说明**在重启/停了什么(便于出问题回溯)② **破坏性且不可逆**的动作(删数据 / 迁 DB / 清目录)仍先报清单。
### B-3 · R9 全文
> **R9**:**⛔⛔ 绝对禁止「人工删锁 / 接管」**(用户 2026-09-12 明令:"严格禁止这类操作") —— **AI 一律不得**:`rm -rf 交接单/.exec-lock`、删 `交接单/.doing-*`、或以「持有者疑似已死 / 卡住 / 太久没动 / 只在只读分析没产出」为由**单方面接管**。锁**只能由持有者自己释放**(`--release-exec` / `--release`);**`handoff-guard.sh` 输出里的「或确认接管后人工删锁」不构成授权**。抢不到锁时 AI 的**唯一**合规动作 = **停手 + 报告用户** —— **锁的处置权只属于用户本人**(要删也只能用户自己动手)。**理由**:删锁 = 在**无法验证**对方死活的前提下单方面撤销互斥(**无心跳机制,AI 没有任何判据**)→ 一旦对方仍在跑,就**退回「两个会话同时改同一批文件」**,而这正是这把锁存在的理由。
### B-4 · R10 全文
> **R10**:**⛔ 绝不以 root(或非该实例 uid)运行 / 触碰用户实例的东西** —— **实证(2026-09-14 事故)**:为量内存**用 root 手动起 admin 的 profile** ⇒ 它以 root 写入 `home/storages/workspace.json`(0.1.5 新增的 `@deepseek-ai/dsh-workspace` 状态文件)与 `home/.dsh/mcn-plugin.db` ⇒ **属主变 root** ⇒ 实例进程(uid 114801)`EACCES` ⇒ `plugin tree failed to load` ⇒ **exitCode 1 崩溃循环 ⇒ 页面 404**。
> **规则**:① 对用户实例的**一切验证 / 冒烟 / 探针必须以该 uid 运行**(`setpriv --reuid <uid> --regid <uid> --clear-groups`)或**照平台姿势进 bwrap 沙箱**;⛔ **禁止 root 直跑 `dsh --profile`**。② 确需临时以 root 跑(读全局配置等)⇒ **收尾必须** `find <home> -user root` 列出 + `-exec chown <uid>:<uid> {} +` 修正。③ 实例「起不来」排查**先看属主 / EACCES**,**不要先怀疑 OOM**(本次先后误判为 OOM,绕了 20 分钟)。
> **修复手法(实测 1 步恢复)**:`find <home> -user root -exec chown <uid>:<uid> {} +` → 重启平台 → 页面 200。
### B-5 · R11 全文
> **R11**:**⛔ 只做正向迭代:命中「劣化风险」→ 立即停下复盘;确实无正向做法 → 立即停止,禁止继续执行**(2026-09-15 用户明令)
> **判据(每次决策前过一遍十维)**:这个改动是否让项目**任一维度净变差** —— **目标 / 方向 / 架构 / 功能 / 性能 / 安全 / 交互 / UI / 便利性 / 扩展性**?
> **命中 ⇒ 立即停下复盘**(不许"先做着看"、不许将就):① 写清**劣化在哪一维、代价多大**(证据 / 量级)② 找出**能保住正向收益的做法**(改小范围 / 换实现 / 分阶段)③ **拿不出正向做法 ⇒ 立即停止、不再执行,只报告**。
> ⛔ **三种伪装禁止**:把劣化说成"必要代价"/用"后续再优化"掩盖已知劣化/把劣化项藏进交付不写。
> 与 **R5**(权限只准收窄)互补 —— R5 管**权限**,R11 管**全维度净收益**;与 **U27**(不将就妥协)同源。
### B-6 · R5 判据补充(主文件保留判据行,此处并列全文)
> **R5**:**权限只准收窄** —— 凡**扩大**(新挂载 / 放开遮蔽 / 暴露平台目录或 env / 放宽 nft / 提档位)→ 先出「权限影响评估」并取得确认。
---
## §C 并发纪律 · 实证与理由原文(对应主文件 §6)
### C-1 §6 全文(压缩前)
> **并发纪律(本库多会话并行是常态)**
>
> - 共享文件**只用 Edit 做精确片段替换**(失败 = 天然冲突检测),**禁整文件 Write 覆盖**。
> - **并行度按「冲突域是否重叠」定**(**2026-09-22 起,域锁取代"全局只放一个"**):域不重叠 ⇒ **可以真并行**;域重叠 ⇒ 串行。
> ⚠️ **底层框架 / 组件 / 机制层(`config`·`crypto`·`isolation`·`index`·`scripts`·`CODEBUDDY.md`·锁与钩子本身)仍然必须独占** —— 这类改动会打穿所有模块,不参与域并行。
> - **🔒 开工门禁(2026-09-22 用户令:「会话执行任务前先判断,当前任务涉及范围是否都可锁定,确认锁定后开始处理」)**:
> 动手前**先判定本次任务涉及的文件是否都能锁定**,再抢锁开工。判定命令:
> ```bash
> bash D:\github\dsh_shenxian\dsh-server-docs\07-scripts\preflight-lock.sh "<会话名>" <目标文件...>
> ```
> 输出【A】可独立锁定 /【B】秒级独占 /【E】机制层(须全局独占)/【C】共享资源 /【D】未归类。
> ⛔ **【D】或【E】非空 ⇒ 脚本 `rc=1` 拒开工**(2026-09-22 实测确认:**【E】也拒**,不是"提示可继续"):【D】= 有文件不在判据管辖内,先归类再动;【E】= 任务是机制层,**全平台共用 ⇒ 不得与其他会话并行**,仅当确认无其他会话在跑时才允许独占开工。
> - **三把锁,顺序固定**(本平台多会话并行的唯一防线):
>
> | 序 | 锁 | 命令 | 管什么 |
> |---|---|---|---|
> | ① | **域锁**(默认) | `bash …/handoff-guard.sh --claim-exec "<会话名>" --domains <域...>` | **声明我占哪些资源**;域不重叠即可并行 |
> | ①' | 全局执行锁(旧行为,兜底) | `bash …/handoff-guard.sh --claim-exec "<会话名>"`(**不带 `--domains`**) | 无法声明域时退化为**独占**;机制层改动必须走这条 |
> | ② | 单级占用锁(细) | `bash …/handoff-guard.sh --claim <单号> "<会话名>"` | 这个单归谁做(供台账与占用声明) |
> | ③ | 服务器侧操作锁 | `bash scripts/op-lock.sh claim <操作名> "<影响面:谁会被断、断多久>"` | 谁正在动**生产**(重启 / drain / 改 env·配额 / 铺插件 / 改 nginx·nft) |
>
> **域键格式** = `<锚点段>/<下一段>`(如 `src/im`、`dsh-server-docs/scripts`);多个域用**逗号**一次给:
> ```bash
> --claim-exec "B线-IM开发" --domains "src/im,src/net"
> ```
> 🔴 **域键判据在 shell 与钩子两侧必须逐字一致**(锚点表见 `handoff-guard.sh` 的 `_ANCHOR_SEGS` 与 `lock-guard-hook.py` 的 `_DOMAIN_SEGS`)—— ⛔ 改一侧必须同步改另一侧,否则**域锁静默失效(假绿)**。
> 📋 查当前全部锁:`bash …/handoff-guard.sh --locks`。
> **完工反序释放**:先 `--release <单>` / `op-lock.sh release`,最后 `--release-exec`。
> - **秒级独占(`--claim-skeleton` / `--claim-publish`)**:动机制层 / 领取迁移号 / 挂载点等**极短操作**,不必全程持全局锁 —— 抢一把**秒级**锁、做完立刻放。
>
> | 锁 | 命令 | 用途 |
> |---|---|---|
> | 骨架锁 | `--claim-skeleton <资源名>` / `--release-skeleton` | 改机制层文件、领迁移号、改挂载点(**秒级**) |
> | 发布锁 | `--claim-publish` / `--release-publish` | `commit` / `push` / `scp` / `build` 这类**全局串行**动作(**秒级**) |
>
> - 🔍 **抢锁必须"校验结果",不能"看输出"**(2026-09-15 实证事故):把 `handoff-guard.sh --claim-exec` 的输出**用管道截尾**(`| grep` / `| tail`)时,**失败提示里也含关键词**(如"…必须 `--release-exec` 才算完成")⇒ `grep -q` 会**假命中** ⇒ 于是"以为抢到了"而**在无锁状态下改库**。
> ✅ **正确判据(二选一,缺一不可)**:① **检查退出码**(`if bash "D:\github\dsh_shenxian\dsh-server-docs\07-scripts\handoff-guard.sh" --claim-exec "X"; then … ; fi`,不要接管道);② **复读 `交接单/.locks/<会话名>/DOMAINS`**(域锁)或 `交接单/.exec-lock/OWNER`(全局锁)**并断言是自己的**。
> ⚠️ **`rc=1` 的两种成因要分清**:域冲突(要停手)vs `.gate` 临界区占用(重试即可)—— 看报错正文,⛔ 不要一律当"已完成"。
> ⛔ 事故版形态:`OUT=$(… --claim-exec … | grep 已持…)` —— **grep 吃掉了退出码,也吃掉了"抢不到"这个事实**。
> - ⚠️ **「无锁」的正确读法 =「你快去抢」,不是「可以开工」** —— 2026-09-12 实证:两个会话把 guard 输出的「✓ 无全局锁」读成"环境干净"→ **同时改了本库**(无实际损害,属流程失效)。**看到"无锁" ⇒ 下一个动作就是 `--claim-exec`(带 `--domains`)**;**抢到才是开工许可**。
> - 强制层(可选启用):`scripts/lock-guard-hook.py` + `~/.workbuddy/settings.json` 的 hooks —— **无锁时直接拒写**(见 `调整方案/73`)。
> - ⛔ **抢不到锁就是终点,不是待办**:**不得人工删锁、不得接管**(见 **R9**,2026-09-12 用户明令)。唯一合规动作 = 等持有者自己释放,或**报告用户、由用户本人处置**;AI 不得以任何理由替用户判断"那把锁已经可以删"。
> - ✅ **锁只约束「写」,不约束「读」**(2026-09-12 用户问清):读文档 / 读代码 / 只读命令(`git status|log|diff`、`journalctl`、`ls`、`grep`、只读 ssh)**随时可做,不需要锁** —— 被挡期间照样可以查清事实再报告。
> ⚠️ 但**会改本地状态的命令不算"读"**:`git fetch` / `checkout` / `stash` / `reset` / `switch` 等一律要持锁(它们会写 `.git/refs` 或工作树)。
> - 🔓 **释放时机 = 整个交付闭环走完,不是"改完文件就放"**:`回填台账 → 四件套校验 → commit → 推送 + 对账 → 归档` 全部结束后才 `--release-exec`(**反序**:先 `--release`,最后 `--release-exec`)。
> 理由:中间放锁 = 别的会话可能在你 commit 前挤进来,让**你的半成品被它的提交带走**(本库实证过这类事故)。
> - ⏸️ **持锁期间若需要等用户拍板(等窗口 / 等选择)→ 先释放锁,再等**:锁是"**正在动手**"的凭证,不是"先占着"。挂着锁空转会把所有会话挡在门外;确认完再重新 `--claim-exec`。
> - 🔒 **锁的生命周期 = 任务的生命周期**(2026-09-14 用户明令):**抢到锁的任务,只有"执行完成 → 反序释放"才算完成**;⛔ **禁止"抢到锁、做一半、不解锁就结束回合 / 结束会话"** —— 锁是**独占资源**,带锁结束 = 把其他所有会话挡在门外,而本库**无心跳机制**、别人**没有任何判据**能确认你已停 ⇒ 会被迫空等,或被诱去违规接管(R9)。
> 三条硬性配套:① **抢锁前先把收口步骤列出来**(落地 → 校验 → 推送/对账 → 收尾),别做到一半才发现收不完;② **中途必须停**(等用户拍板 / 等外部窗口)⇒ **先释放锁再停**(上一条);③ **结束语必须对锁状态负责** —— 要么写明"**已释放**",要么**显式点名"锁仍在 `<OWNER>`、未释放、原因、下一步"**(仅限"释放通道不可用"这类极端情形);⛔ **"忘了 / 做不完就走"一律不允许**。
> - 推送前复跑对账:**「仅本地」里若有不在你清单里的文件 → 立刻停手**(幽灵文件);**只推自己本次改的文件**。
> - 单子里的基线数字**必须带取数时间 + 复核命令**,不写死绝对值(会被并行改动打穿)。
> - **`~/.workbuddy/settings.json` 的 `hooks` 段 = 多会话共享配置** —— 多个会话各自加钩子时**只能 Edit 增删条目,禁止整段覆盖**(JSON 顶层键被覆盖会**静默**抹掉别人的钩子)。
> 2026-09-12 实证:两处独立钩子(提问闸门 / 锁闸门)各写一份配置文档,若各自按文档落盘 → **互相覆盖**;已合并为一段(`PreToolUse` 两条 + `SessionStart` 一条)。
> **同一条也适用于用户级 `~/.workbuddy/MEMORY.md`。**
> - ⚠️ **钩子命令「会话启动时快照」** —— 改 `~/.workbuddy/settings.json` 里的 hook,**对已在跑的会话无效**,必须**「完全重启」(彻底退出——关窗 ≠ 退出)或新开会话**才加载。**2026-09-13 实测定论**(本会话 06:47 启动 → 06:55 改配置 → 07:05 拆掉临时目录联接后,写操作报的**仍是旧路径**;此前那版「每次调用现读」是被那支临时联接掩盖的**误判**)。
> - ⚠️ **脚本路径失配 = fail-closed**:hook 打不开脚本 → 报错 → 该机**所有会话**的 Write/Edit 全被拒(09-13 实际发生,连改 `settings.json` 本身都被拦)⇒ **迁移 / 改名后第一件事 = 核对 hooks 里的绝对路径**。
> - **应急兜底(本钩子有意的安全阀)**:钩子**不拦 Bash** ⇒ 路径失配期间可用 shell 写文件过渡(09-13 实际走通)。
---
## §D 环境要点 · 全文(对应主文件 §7)
### D-1 §7 全文(压缩前)
> - **本机 bash 的 PATH 常丢**(`dirname`/`grep`/`ls` not found)→ 每条命令显式:
> `export PATH="/e/ProgramData/.workbuddy/binaries/PortableGit/versions/1.2.0/usr/bin:/e/ProgramData/.workbuddy/binaries/PortableGit/versions/1.2.0/mingw64/bin:/c/Windows/System32:$PATH"`
> (⚠️ **`mingw64/bin` 不能少 —— `git.exe` 只在那里**;2026-09-12 实测:只加 `usr/bin` 时 `git` 仍不可用。`usr/bin` 提供 `ls/grep/dirname/md5sum`。该 Git 安装目录**存在**,此前"路径已失效"的判断有误。)
> - **插件安装 / 卸载 / 清理一律走 pnpm**(`pnpm add file:<tgz>` / `pnpm remove <pkg>` / `pnpm install` 自愈;**lockfile 才是账本**,手放 `node_modules` 无效)。
> ⛔ **禁 `rm -rf node_modules/<pkg>`** —— 手删**不会连带删它的依赖树**,会留下**孤儿包**并让 `node_modules` 与 pnpm 记录不一致。
> (2026-09-12 实测:手删 `dsh-plugin-mcn-suite` 留下了 `xlsx` 的 7 个子依赖,最后靠 `pnpm install` 清掉 **15 个**包才复原。遇到 `pnpm remove` 报 `CANNOT_REMOVE_MISSING`(package.json 里已无该依赖)时,**正确动作是 `pnpm install`**,不是手删。)
> - 同名同版本 tgz 改了内容**必须升版本号**。
> - ⚠️ **语法检查别用 `python -m py_compile`**(2026-09-15 实证):它**必然**在脚本旁落 `__pycache__/*.pyc`,而文档库的 `docs-sync-check.sh` 会把它算成「**仅本地(待推送)**」⇒ 污染对账。
> ✅ 用**不落盘**的写法:`python -c "import ast,sys; ast.parse(open(p,encoding='utf-8').read()); print('OK')"`;若已经落了,收尾时 `find <库> -name __pycache__ -type d -exec rm -rf {} +` 并**复跑对账清零**。
> - **会话取证**:dsh 实例会话 = **多帧 zstd**(按 magic `28 b5 2f fd` 切帧);WorkBuddy 自己的会话 = `~/.workbuddy/projects/<目录名>/*.jsonl`。
> - **实例内禁 loopback**(`127.0.0.1` 不可达);取实例页 HTML 需 `curl -L --compressed -c jar -b jar -H "Accept: text/html"`(三条缺一注入就会被 gzip 挡掉)。
> - ⚠️ **本机 → 服务器传文件必须先转 LF**(2026-09-12 实测,差点把 CRLF 带进生产):本机 `D:\github\dsh_shenxian` 的 `core.autocrlf=true` ⇒ **工作树是 CRLF**,而服务器 `/opt/dshs` 是 **LF**。直接 `scp` 会污染生产仓库(脚本照跑,但对账/三方对齐被破坏)。正解:`tr -d '\r' < 源文件 > /tmp/x` 再 `scp /tmp/x`,**只转本次要传的那一个文件**(R7 禁批量换行符转换)。判据用 `od -c` 看行尾(`\n` vs `\r\n`)—— **别用 `grep -c $'\r'`**,在 git bash 里会误报。
> - ⚠️ **本机 Node 是 24,但项目原生模块(`better-sqlite3`)是按 Node 22 编译的** —— 用默认 `node`/`npm` 跑 `npm test` / `npm run verify` 会全线报 `ERR_DLOPEN_FAILED`(`NODE_MODULE_VERSION 127` vs `137`),**看起来像"我改坏了",其实是环境**(2026-09-13 实测)。跑单测/验收一律用 **Node 22**:`E:\ProgramData\.workbuddy\binaries\node\versions\22.22.2-3\node.exe`(服务器本来就是 22.23.2)。
> - ⚠️ **核验"推送是否到位"要用 `git ls-remote origin refs/heads/<branch>`**(与本地 `git rev-parse --short HEAD` 对比)。本机两个仓库**都没有 remote-tracking ref** ⇒ `git log origin/main..HEAD` 直接报 `unknown revision`,**别把它的空输出当成"已全部推送"**(2026-09-13 实测)。
---
## §E 会导致事故的实测事实 · 全文(对应主文件 §8)
### E-1 §8 全文(压缩前)
> | 事实 | 不知道会怎样 |
> |---|---|
> | **实例权限档位是「会话创建时播种」的** —— 既有会话**不跟随平台默认**(平台默认 `danger-full-access`) | 会误判"平台坏了";更危险的是可能**自动去改档位** —— 那等于把受限会话静默提升为完全权限,**安全语义变更必须用户知情** → 只提示 + 建议新开会话 |
> | **功能插件「禁用」= `pnpm remove`(真卸载)**,不是"保留包 + disabled" | 任何**硬绑定 provider** 的覆写段在用户禁用后会指向不存在的 provider;`web.searchProvider` **单选且无回落**,多 provider 又未显式配置 → `WEB_PROVIDER_AMBIGUOUS` 报错(平台已用 `syncWebProviderPatch()` 按当前 bundles 重算解决) |
> | **实例配额** = `MemoryMax 384 MiB` / `CPUQuota 150%` / `TasksMax 128`;但 **V8 堆上限按宿主物理内存算(960 MiB)而非 cgroup** | ① `systemd` 的 `MemoryCurrent`/`MemoryMax` **单位是字节、不是 KB**(按 KB 算会放大 1024 倍)② 不注入 `NODE_OPTIONS=--max-old-space-size=256` → 实例会先撞**内核 SIGKILL**(无优雅退出、无日志,排查时无从下手) |
> | **业务功能插件(第三方 + 自研)只有一种投放方式** = 「**admin 在门户导入候选池 → 用户在实例「功能管理」里自己启用/禁用**」(用户 2026-09-12 明确:三方插件**一律**按这个处理,**不需要铺什么**) | 若图省事改"直铺"(直接往用户 profile 装包 + 写 provider 覆写段),后果有三:① **绕过用户自决** —— 用户看不到、也关不掉;② 直铺覆写段与档案 65 的平台托管段**同属"整体替换 config"语义** → 两段并存**互相覆盖**,产生**难察觉的配置漂移**;③ 与托管段机制重复建设。**无例外** —— 连 AnySearch 最初走的直铺,也已由用户拍板改回候选池(档案 64 §8.3 修正)。⚠️ **例外只限平台基础设施插件**(`portal-entry` / `business-plugins` / `workspace-scoped-picker`):它们仍是平台级直铺、用户无感,见档案 16「插件三层归属」 |
> | 🔴 **实例 home 写文件一律走 `UserFs`**;其文件名**白名单** `HOME_FILE_NAMES`(`src/fs/user-fs.ts`)是「**控制面 + worker agent 两端共用**」 | ① 绕过 `UserFs` 直接 `fs` 写 = **静默空操作**(读回空串、不报错)⇒ 以为"已落盘",实际什么都没有(档案 138 §五)。② 改白名单后**只重启一端**(如只 `systemctl restart dshs`)⇒ 另一端仍判非法,真跑报 `reason=…-unreadable:… bad_path`,**看起来像"功能没生效"**。⇒ 凡改该白名单,收尾清单必含**两个单元**:`dshs` + `dshs-worker` |
> | 🔴 **PG 控制面库的 `users` 表身份键是 `id`(text uuid)** —— 同表另有一个 `uid`(**bigint** 序号);`audit_log` 的用户身份列名是 **`actor`** | 写"按 uid 查/删用户"的脚本会命中 `operator does not exist: bigint = text`,或**删不掉还当成功**(本坑 2026-09-20 序47 实测踩到一次)。⇒ 判身份一律用 `users.id` |
>
> 细节与当时实测:`调整方案/33`(权限档位)· `04-16`/`04-64`/`04-65`(插件与 provider)· `04-58`(内存与配额)。
---
## §F §2 指针表 · 压缩前全文(对应主文件 §2)
> 保留此处仅为**回查**;主文件 §2 已是「触发词 → 路径」形态。
| 当你准备… | 去查 |
|---|---|
| **想知道"当前什么状态"(锁 / git 基线 / 待办 / 上次收口点)** | **先跑这一条**:`"E:/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe" "E:/ProgramData/AIProject/ai1net-dsh-server/state.py"`(约 30 行输出;`--online` 追加远端基线比对)。**它 1 次调用顶你十几轮探索** ⇒ ⛔ 跑完它之前**不许** Glob/Grep 全库摸底 |
| 回答"现在是什么状态 / 该读哪篇" | `D:\github\dsh_shenxian\dsh-server-docs\BRIEF.md`(现行事实,30 秒读完);**覆盖网络线**另有唯一入口 = 工作区根 `接续入口_覆盖网络线_20260916.md` |
| **改任何文件之前(第一步,不是"检查"是"抢")** | **先抢全局执行锁**:`bash D:\github\dsh_shenxian\dsh-server-docs\07-scripts/handoff-guard.sh --claim-exec "<你的会话名>"` —— **抢到之前不要动任何文件**;**抢不到 = 有会话在跑 = 停手**。抢到后再跑一次信息模式,看占用与越界改动 |
| 写 / 改前端页面(`web/*.html`、`.css`、client bundle) | `D:\github\dsh_shenxian\dsh-server-docs\01-规范\06-工作台UI规范.md`(**强制基线**;冲突时以其**实测 Token** 为准) |
| **改完 UI(尤其 client bundle)怎么验收 / 为什么"看不到变化"** | **`规范/06-工作台UI规范.md §7`**(生效链路四关 + 三段式验收 + 四个误判)。要点:client bundle 在**实例启动时**加载 ⇒ **改完必须重启实例**;浏览器侧由 dsh 的 `rev`(内容 hash)+ 平台代理层 `Cache-Control: no-cache` 保证自动更新 |
| **"我做完了吗"怎么判(本机改完 ≠ 交付)**(2026-09-15 加 —— 同类已 3 次) | 收尾前逐层走完**生效链路**并在**用户可见面**复验:静态页 → `scp`(⚠️ CDN/CF 缓存坑)|平台 TS → `build` + `systemctl restart dshs`|自研插件 → tgz → 候选池 → 实例启用 → **重启实例**|文档库/技能 → scp + 对账。⛔ **「本机改完了 / build 通过了 / 本地打包完成 / 已 commit」四条都不算交付**(清单:`dsh-change-workflow` **阶段 5 §0 交付门禁**;判据:素材库 **A25**)。⚠️ 但**别回头问"要不要部署"** —— 部署属我 lane 内执行细节(§3 R7-边界 + U20/X9),**直接做** |
| 新建改造档案 | `D:\github\dsh_shenxian\dsh-server-docs\05-交接单/README.md §二`(8 段模板)+ **先原子占号**(`mkdir <目录>/.lock-<NN>`) |
| 判断"某功能当时怎么改的" | `D:\github\dsh_shenxian\dsh-server-docs\调整方案/<NN>-<主题>.md`(先读**头部** TL;DR / 状态,再决定是否读全文) |
| 部署 / 构建 / 回滚 / 依赖版本 | `D:\github\dsh_shenxian\dsh-server-docs\DEPLOY-本部署.md` |
| 查待办 | 未规划 → `D:\github\dsh_shenxian\dsh-server-docs\01-规范\03-路线图与待办.md §二`;已规划待执行 → `D:\github\dsh_shenxian\dsh-server-docs\05-交接单/README.md §一` |
| **要动生产**(重启 `dshs` / drain scope / 铺插件 / 改实例 env 或配额 / 停别人留下的单元) | ⚠️ **2026-09-16 收口:与 §1 第 27 行 + R8 对齐 —— 直接做,动手前一句话说明在动什么即可,⛔ 不再"取得确认"**(旧表述"先按 R8 说清并取得确认"曾与 R8 本身矛盾,是会话 `ddea70b7` 上抛的诱因之一)。**只有"破坏性且不可逆"(删数据 / 迁 DB / 清目录)才先出清单** |
| **投放任何业务功能插件(第三方 / 自研)** | **只有一种方式:admin 在门户导入候选池 → 用户在实例「功能管理」里自助启用/禁用**(**不铺 profile**)。机制 / 边界 / 回滚见档案 16 / 36 / 65(**§8 有硬性说明**) |
| **推送到服务器之前** | `bash scripts/docs-sync-check.sh`(对账)+ `MINE="<我的文件>" PUSH=1 bash "D:\github\dsh_shenxian\dsh-server-docs\07-scripts\handoff-guard.sh"`(**幽灵文件硬判定**) |
| 做功能需求 / 方案决策 | 加载技能 `dsh-feature-first`(**谁定什么**)+ `dsh-decision-method`(**怎么定得对**,含 §4.4 技术实现裁决顺序)。⚠️ **用户点名「决策方法」/「参考决策方法」/「按决策方法」⇒ 必须立即 `Skill(dsh-decision-method)`,⛔ 不得凭记忆代替、不得只靠本文件 §1 判据**(2026-09-16 实证:会话 `ddea70b7` 用户 U6 明确点名后,AI 全程 `Skill` 调用 **0 次**,仍按旧判据上抛 → 复用本条) |
| **用户问「是否已实现 / 能不能 / 为什么不行」**(2026-09-13 用户要求) | 用 `dsh-feature-first **§5.1 结论骨架**`:**判定 → 为什么不行 → 需你拍板(真需要才写)→ 我接着做(陈述句)**。三条铁律见 §5.3:**主位是用户问的那件事**(AI 的进度/失误/计划不得占前两节)· **结论层零技术标识** · **禁征询式收尾**(已定的下一步直接做) |
| 落地一次改造 | 加载技能 `dsh-change-workflow`(六阶段 + 档案模板 + 并行调度) |
| **登记接续棒 / 排下一棒**(收尾四件套第 ② 件) | 技能 `dsh-auto-handoff-chain` **§3.1.1 排期两条铁律**:① **首个(唯一)接续棒 = 收口 + 3~4 分钟**(🔴 **2026-10-01 用户口径改值**:原 5~8 作废)(⛔ 不是"棒与棒之间")② **每条线同一时刻只挂一个**(下一棒由当棒收官时再排,⛔ 不预登记队列)。⚠️ 两条都已踩过(2026-09-18 用户当场纠正两次) |
| 复盘"用户到底怎么决策的" | `bash scripts/extract-user-voice.py`(抽全部历史会话的用户原话) |
| 判断"服务器文件是否等于我的改前基线" | `git hash-object`(**比 md5 可靠**,不受 CRLF/编码影响) |
> ⚠️ **技能的加载由模型判断相关性,不能保证**。所以:**凡"动作前必须生效"的规则,必须写在本文件里(§1/§3/§4/§5/§6);技能只承载"需要时去拿的方法论"。**
---
## §G 工作区卫生 · 全文(对应主文件 §9)
### G-1 §9 工作区卫生 全文(压缩前)
> 起因:工作区 470 MB 中 411 MB 是过程产物(`tmp/` 1504 件、`待清理/` 1084 件、6 份 33.7 M 的 DB 副本)。
>
> - 🔴 **收口必清本棒 tmp** —— 「收尾四件套」加第 ⑤ 件:本棒在 `tmp/` 下的过程目录,收口时自清(或确认无残留)。
> - 🔴 **`tmp/` 保留期 = 7 天** —— 超期目录进 `归档/tmp-<日期>/`;可跑 `state.py --gc` 自动判定。
> - 🔴 **不新建「待清理」类中间态目录** —— 二值决策:**归档**(要留)or **删除**(不留)。中间态 = 拖延。
> - 🔴 **工作区已纳入 git**(`dsh_shenxian_workspace`)⇒ **入库只放文档与文件**:运行态、缓存、`*.db*`、`*.tar.gz`、过程目录一律 `.gitignore`。
> - 🔴 **工作区不保留脚本副本** —— 一律**绝对路径**调文档库 `07-scripts/<name>`(`scripts/` 已撤,2026-09-24)。
> - 🔴 **交界单正文落文档库** `05-交接单/`,工作区只放**指针**。
> - ⚠️ **>60 KB 的单文件**:提交前须逐个判「是否文档 / 是否该提交」(2026-09-24 用户令)。
---
## §H 工作区目录规范 · 全文(对应主文件 §10)
### H-1 §9 工作区目录规范 全文(压缩前 · 原编号 §9 ⇒ 现 §10)
> - 🔴 **根目录白名单** —— ⛔ 不许移走,⛔ 不许在根新增散落文件:
> `CODEBUDDY.md` · `state.py` · **`接续入口_*.md`** · `README.md` · `scripts/` · `.workbuddy/` · `.codebuddy/` · `.wbapp_*.genie` · 五大结构目录(`docs/` `交接单/` `tmp/` `待清理/` `归档/`)。
> ⛔ **线目录不进根** —— 一条工作线的多份配套方案放 `docs/<线名>/`(同级引用天然有效),冷却后整体移入 `归档/`。
> ⚠️ **`接续入口_*.md` 必须在根** —— `state.py` 用 `os.listdir(工作区根)` 扫描它;移走 ⇒ 新会话读到的**第一个信号就是错的**(事故级)。
> - **落点**:正式文档 → `docs/<主题>/`(覆盖网络 · 集群与实例 · 客户端与桌面 · 会话与接续 · 插件与平台 · 外部接入 · 调研与审计)|交接单 / 接续包 → `交接单/`|一次性脚本与命令输出 → `tmp/<任务名>-<日期>/`|过程目录 → `tmp/历史过程目录/`|不再引用但留痕 → `归档/`|疑似可删 → `待清理/`(**列清单等确认才删**,删除不可逆)。
> - **命名**:正式文档 `<主题>_<YYYYMMDD>.md`|线入口 `接续入口_<线名>_<日期>.md`|临时物 `_<用途>.<ext>`|过程目录 `_tmp_<序号>/`。日期一律 8 位无分隔。
> - ⚠️ **改写文档内引用路径时,映射键必须收敛到「带 `_YYYYMMDD` 日期戳」的文件名** —— 通用名(`README.md`/`INDEX.md`/`architecture.md`)在任何文档里都可能指别处,映射它必然误伤(2026-09-19 实证)。
> - **路径变更查法**:本次规整(119 项)的「旧 → 新」权威对照 = `tmp/本次整理-20260919/移动对照表.md`;一键回滚 = `python tmp/本次整理-20260919/rollback.py`。
---
## §I 本轮压缩记录(2026-09-24)
| 项 | 值 |
|---|---|
| 压缩前 | 23,756 字符 · 标题 12 · **线内标题 4** |
| 压缩后 | 10,281 字符 · 标题 12 · **线内标题 9**(且为「必须实体」那 9 章) |
| 回滚点 | `$WS/归档/规则A-备份-20260924/CODEBUDDY.md.before-compress` |
| **实测硬约束** | 「必须实体」8 章(§1 §3–§8)+ 头部 ≈ **8,000 字符**,**独自吃满注入窗口** ⇒ 8,000 窗口**装不下**「必须实体 8 章 + 指针 3 章(§2/§9/§10)」 |
| 未达成 | `≤ 8,000` 与 `12/12` **未同时达成** —— 要 12/12 须再砍 ≈1,900 字符**判据本体**(属放弃哪些判据的取舍 ⇒ 需用户拍板,本轮未自行砍) |
| 已达成 | 11 条红线判据**逐条在实体**(主文件 §3 表)+ 解释段全文在 §B;§1/§3–§8 判据一条未删;章节顺序改为**按注入优先级**排版(§8 因此进线内) |
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""规则载体压缩 · 端到端验收(2026-09-24)"""
import io, re, sys
WS = "E:/ProgramData/AIProject/ai1net-dsh-server"
MAIN = WS + "/CODEBUDDY.md"
REF = WS + "/docs/规则与载体/规则详解_红线与实证_20260924.md"
BAK = WS + "/归档/规则A-备份-20260924/CODEBUDDY.md.before-compress"
def rd(p):
return io.open(p, encoding="utf-8").read()
main, ref, bak = rd(MAIN), rd(REF), rd(BAK)
LIMIT = 8000
print("== 1. 体积 ==")
print(" 压缩前 =%6d 字符" % len(bak))
print(" 压缩后 =%6d 字符 (线=%d, 占比 %.0f%%)" % (len(main), LIMIT, 100.0*LIMIT/len(main)))
print(" 削减 =%6d 字符 (-%.0f%%)" % (len(bak)-len(main), 100.0*(len(bak)-len(main))/len(bak)))
print(" 目标 <=%d : %s" % (LIMIT, "达标" if len(main) <= LIMIT else "未达标(差 %d)" % (len(main)-LIMIT)))
print("== 2. 标题注入 ==")
offs = [(m.start(), m.group(0)) for m in re.finditer(r"(?m)^#{1,3} .+$", main)]
inline = [o for o, t in offs if o < LIMIT]
print(" 总标题 =%2d 线内 =%2d %s" % (len(offs), len(inline), "12/12 达标" if len(inline)==len(offs) else "未达标"))
for o, t in offs:
print(" %s %6d %s" % ("OK " if o < LIMIT else "OUT", o, t[:44]))
print("== 3. 红线 11 条可寻址 ==")
sec3 = main[main.index("## 3. 红线 R1"): main.index("## 4. 提交边界")]
ok = 0
for i in range(1, 12):
tag = "**R%d**" % i
if tag in sec3:
ok += 1
else:
print(" !! 缺失 %s" % tag)
print(" 主文件 §3 内 R1-R11 判据行 = %d/11" % ok)
print(" 解释段落点 R7-边界/R8/R9/R10/R11 在 §B: %s"
% ("齐全" if all(("B-1" in ref or i==1) and ("### B-%d" % n) in ref
for n in [1,2,3,4,5]) else "缺"))
print("== 4. 指针悬空检查 ==")
ref_secs = set(re.findall(r"(?m)^## (§[A-Z]\S*)", ref))
print(" 【详】实有章节: %s" % " ".join(sorted(ref_secs)))
want = ["§A","§B","§C","§D","§E","§F","§G","§H"]
# 主文件里指向【详】的章节号(含「详 X」「§B-1…§B-5」两种写法)
used = set(re.findall(r"【详】(§[A-Z])", main)) | set(re.findall(r"详 ([A-Z]\d)", main))
bad = [s for s in want if s not in ref_secs]
print(" A-H 全在: %s" % ("是" if not bad else "缺 %s" % bad))
print(" 主文件显式指向的章节: %s" % " ".join(sorted(used)))
print("== 5. 完好性 ==")
for k in ["R11", "R9", "R10", "分层判定标准", "动作前必须执行的三条",
"preflight-lock.sh", "state.py", "_ANCHOR_SEGS", "dshs-worker"]:
print(" %-26s 主文件 %s" % (k, "有" if k in main else "无"))
print(" 回滚点存在: %s (%d 字符)" % ("是" if bak else "否", len(bak)))
print(" 主文件与回滚点字符差 = %d" % (len(bak)-len(main)))
@@ -54,7 +54,7 @@
### 附:可复跑命令
```bash
cd "E:/ProgramData/AI技能/aliyun-dsh-server"
cd "E:/ProgramData/AIProject/ai1net-dsh-server"
PYTHONIOENCODING=utf-8 "<managed python>" "_中间产物_待清理/auto-continue-20260916/audit_docs.py" # 六类机械扫描
PYTHONIOENCODING=utf-8 "<managed python>" "_中间产物_待清理/auto-continue-20260916/audit_pass2.py" # 重复字节定量
```