docs: 插件适配指南 + 共用模块提取(为接入更多平台做准备)
两次适配(opencode、DeepSeek Harness)里的方法与坑此前散落在提交信息和 代码注释里,接第三个平台时要重新翻。这次固化成文档,并把与平台 SDK 无关的 逻辑提到共用模块。 ## docs/PLUGIN-GUIDE.md 八节:职责边界、必须实现的六件事、会话命名回写、平台会话快照上报、 平台差异对照表、踩过的坑(按排查成本降序)、新平台适配清单、共用模块清单。 三条设计原则贯穿全文,后面每一节都是它们的推论: 1. **平台原生信号才是真相来源**,不要求模型「记得」调工具 —— 因此不提供 request_permission(改挂权限钩子)、不要求模型主动回信(改在「一轮结束」 的平台信号上自动转发) 2. **插件代劳的转发不消耗配额** —— 因此这两类转发带 relay + relay_key 3. **平台命名优先** —— 因此创建会话时不传占位标题(那会掐掉平台自己的命名机制) 「踩过的坑」一节按排查成本排序,头一条是花了一下午的 followup() 参数形状。 ## 共用模块提取 `lib/inbox-format.js`(新):收件箱渲染与已读策略。三条规则各对应一次错误行为, 而它们与平台 SDK 无关: - 附件必须带 attachment_id(只说「有附件」模型无从下载) - 抄送人要显示(不显示模型以为是私信,回信时漏掉其他参与方) - 只标本次列出的、status=all 时不标(limit 之外的还没看过;把历史邮件标成已读 会让下一轮的新邮件混在里面认不出来) 顺带修好两处不一致:DSH 的 read_inbox 此前**完全没有标记已读**(每轮重复捞同一批), 且默认 status=all(同上);附件大小两边一个显示字节数一个显示 KB/MB。 `lib/workspace.js`:提到两侧共用。签名从 (workspace, fallbackKey) 改为 (workspace, fallback) —— 各平台的兜底不同:opencode 有插件启动时的 directory, DSH 只能落到 ~/.dsh/mail-sessions/<会话>(mailSessionFallback)。 opencode 侧此前是内联的三行判断,没有「目录不存在时不创建」与「拒绝相对路径」 这两条保护。 ## deploy/check-shared-libs.sh `lib/` 与 `test/` 下的共用文件必须逐字节相同,纳入 install.sh 门禁。 一侧改了另一侧没改,两个平台的行为就会悄悄分叉:同一封邮件在 opencode 那边 标了已读、在 DSH 那边没标,而两处代码看起来都「对」。这类分叉没有测试能发现, 只能靠 diff。 ## 文档同步 - PLAN.md §7.7 从「待做」改为已完成,补 7.7.1(工作目录归属)与 7.7.2(平台会话快照)两节,记录根因而非只记改法 - API.md 加「心跳与平台会话快照」章节;SSE 章节补 new_mail 与 permission_decision 的 payload 说明(to_workspace 的语义、relay_key 的用途) - PHASE7-REMAINING.md 移除已完成的 7.7,新增「每平台可用模型范围」的进展 (repo 层已就绪,handler/插件/前端待做) - README 文档索引与项目结构 验证:两插件共 136 个测试通过,同源校验通过,Go/前端全绿; 端到端发信 → DSH 用新的 read_inbox 渲染读取 → 自动回信 213 字节。
This commit is contained in:
103
docs/PLAN.md
103
docs/PLAN.md
@ -1023,11 +1023,102 @@ execute: {
|
||||
业务代码不感知 Cookie 与密钥的差异,`src/api/` 可整体抽成 SDK
|
||||
- [x] `docs/API.md`:完整接口清单、三类调用者的认证边界、错误码约定
|
||||
|
||||
### 7.7 DeepSeek Harness 插件
|
||||
### 7.7 DeepSeek Harness 插件(已完成)
|
||||
|
||||
- [ ] 基于 Cordis 框架开发 `dsh-mail-bridge`
|
||||
- [ ] 利用 `PreToolUse`、`SessionStart` 钩子
|
||||
- [ ] 与 Pi 插件共享相同 Gateway API
|
||||
`plugins/dsh-mail-bridge/`,Cordis 插件框架 + TypeScript。
|
||||
适配方法与踩坑记录已固化为 **[`docs/PLUGIN-GUIDE.md`](PLUGIN-GUIDE.md)**,
|
||||
后续接入新平台按那份清单走。
|
||||
|
||||
- [x] Cordis 插件骨架:`export const inject` + `export const name` + `apply(ctx, config)`
|
||||
- 没有 `inject` 时 `ctx.tools` / `ctx.agents` 根本不存在
|
||||
(报 `cannot get property "tools" without inject`)
|
||||
- 但**可选服务不能写进 `inject`** —— 那是硬依赖,服务没挂载时整个插件不启动。
|
||||
`sessionQuery` 用 `ctx.get()` 取:会话上报只是补全体验,
|
||||
不该能把邮件投递整体拘死
|
||||
- [x] 四个工具经 `defineTool` 注册(`send_mail` / `read_inbox` /
|
||||
`upload_attachment` / `download_attachment`)
|
||||
- 直接给 `ctx.tools.register` 原始对象会报
|
||||
`parameters must be lossless JSON before schema projection`
|
||||
- 还必须声明 `output: { schema, render }`
|
||||
- [x] `ctx.agents.create()` 建会话 + `agent.followup()` 投递消息
|
||||
- **`followup()` 要完整的 `UserMessage`(`content` + `source`)**,
|
||||
不是 opencode 那种 parts 数组。传错不当场报错,而是在 agent-loop 的
|
||||
`preStep` 里抛 `Cannot read properties of undefined (reading 'kind')` ——
|
||||
错误落在框架内部,不指向调用点,turn 一 start 就 end、模型请求根本不发出去。
|
||||
这个坑花了一下午,已用 `lib/message.js` + 测试钉住
|
||||
- `setup` 留空:base bundle 已注册 agent-loop / llm / tools,
|
||||
`agentOptions: { provider, model }` 就够了;挂 preset 反而多余
|
||||
- [x] `agent/status` → `idle` 时自动转发最后一条 assistant 消息
|
||||
(对应 opencode 的 `session.idle`),复用 `lib/relay-dedup.js` 让位于
|
||||
模型的主动回信,带 `relay: 'summary'` 走免配额通道
|
||||
- [x] `approval/request` 钩子把权限询问转成邮件问人
|
||||
- 与 opencode 的关键差异:那边的 `permission.ask` 是**同步**钩子,
|
||||
卡住会挂死整个请求,只能「转出去 + 立即返回 ask」;
|
||||
DSH 这边是**异步 waterfall**,返回 `Promise<ApprovalOutcome>`,可以真的等人
|
||||
- 拆插件时未决询问一律 fail closed(`unavailable`),
|
||||
否则 DSH 侧那些 `await` 永不返回
|
||||
- DSH 不给询问发 id,用 `会话:工具:callId` 作幂等键
|
||||
- [x] 会话别名由**模型生成的标题**派生(与「别名复用平台命名」的既定决策一致)
|
||||
- `slugFromTitle` 保留中文(转拼音后既不好读也不好打,
|
||||
而三维地址按最后一个 `.` 切分,中文不影响解析)
|
||||
- 但必须去掉 `.` `@` `/` 等寻址分隔符 —— 留在别名里会让它自己被解析器切开
|
||||
- fallback 占位标题不派生别名:DSH 在模型生成真标题前会先落一个
|
||||
内容是「用户第一句话截断」的标题,而那句话是插件自己拼的提示词
|
||||
- [x] **补上心跳** —— 之前完全没有,Gateway 靠 `last_seen` 判在线,
|
||||
一直靠注册那一次撑着
|
||||
- [x] 与 opencode 插件共用 `lib/` 下的纯函数模块(逐字节相同)
|
||||
|
||||
### 7.7.1 工作目录归属(修复)
|
||||
|
||||
**症状**:dsh 指定工作目录完全失效,所有会话落进「未分组」。
|
||||
|
||||
**根因两层**:
|
||||
|
||||
1. 插件建会话时的 cwd 是自己拼的 `~/.dsh/mail-sessions/mail-<uuid>` ——
|
||||
每封邮件一个全新的空目录。平台按 cwd 给会话分组,于是所有邮件会话
|
||||
既不属于任何项目、彼此也不同组
|
||||
2. Gateway 从来没把地址的 path 位发给插件:`notifyRecipients` 的 payload
|
||||
只有 `mail_id`/`session_id`/`from_name`/`subject`,`to_workspace` 虽然入库了
|
||||
却不在 SSE 事件里 —— 插件即使想用也拿不到
|
||||
|
||||
- [x] SSE `new_mail` 事件加 `to_workspace`。**每个收件方拿到自己那个地址的 path**,
|
||||
不是主收件人的 —— 抄送给 `opencode@/a` 与主发给 `dsh@/b` 是两个工作区
|
||||
- [x] 两个插件的 cwd 都改为取寻址的 path 位(共用 `lib/workspace.js`)
|
||||
- [x] 不存在的目录**不创建**而是回退到兜底目录:一个笔误
|
||||
(`/home/porgram/x`)不该在磁盘上落下真目录,Agent 会在里面一无所获地干活
|
||||
- [x] 拒绝相对路径:cwd 的相对基准是 harness 进程的启动目录,systemd 下通常是 `/`
|
||||
|
||||
### 7.7.2 平台会话快照上报(新增)
|
||||
|
||||
**症状**:会话别名列不出工作区下的历史会话,无法选择。
|
||||
|
||||
人直接在平台界面上开的会话,Gateway 一无所知;而邮件驱动的那些也因为
|
||||
`workspace` 没存在会话上(只在 `mails.to_workspace`,且 Agent 回信的
|
||||
`from_workspace` 填的是 Agent 名而不是路径)而匹配不上。
|
||||
|
||||
- [x] `sessions.workspace` 新列,`CreateSession` 从地址的 path 位带入
|
||||
- [x] `agent_platform_sessions` 镜像表 + 心跳携带 `platform_sessions`
|
||||
- [x] **插件上报而非 Gateway 反向拉取**:当前架构是单向的(Agent 持密钥主动连
|
||||
Gateway,Gateway 从不外呼),反向拉取需要它保存各平台的地址与凭证,
|
||||
那是另一套信任模型
|
||||
- [x] 与 `sessions` 表**分开存**:镜像里是别人家的会话,id 属于平台的 id 空间,
|
||||
没有本侧的 owner/预算/邮件。混进 `sessions` 会让每一处「按会话鉴权」
|
||||
都要先判断这条到底是不是真的本侧会话
|
||||
- [x] **整表替换而非增量合并**:平台侧删掉的会话必须从候选里消失 ——
|
||||
session 位是三态语义,指向不存在的会话直接 404
|
||||
- [x] **`platform_sessions` 省略与传空数组语义不同**:拉不到列表时省略该字段
|
||||
(保留镜像),传空数组的语义是「平台侧确实一条会话都没有」
|
||||
- [x] **subagent 子会话不上报**:实测 DSH 一次列出 49 条子会话,标题就是派活的
|
||||
提示词前缀(九条都叫 `You are auditing ONE file`),slug 全撞名;
|
||||
它们是父 agent 内部的工作单元,人往里发邮件毫无意义
|
||||
- [x] **slug 撞名只留最近那条**:服务端只能取其中一条,上报同名项只会让补全里
|
||||
出现几个点哪个都不确定的候选
|
||||
- [x] `SuggestSessionCandidates` 取代 `SuggestSessionsFor`:以会话自己的
|
||||
`workspace` 为权威,历史会话(该列为空)回退到 mails 反推 ——
|
||||
升级后老会话不该从候选列表里消失
|
||||
- [x] 补全候选带标题与来源:`suggestions` 保留纯字符串数组(不打破已部署的前端
|
||||
与第三方客户端),新增同序的 `candidates`;过滤时标题也参与匹配 ——
|
||||
人记得的是「缓存选型」而不是 `brisk-harbor` 这种随机短名
|
||||
|
||||
### 7.8 跨主机 Agent 发现
|
||||
|
||||
@ -1199,7 +1290,9 @@ MVP 计划(Phase 1-6)已全部落地并在 systemd 部署态实测通过。
|
||||
- [x] 插件自动转发平台原生权限询问与最终总结(不消耗配额)
|
||||
- [x] 配额下沉到会话:写信时给、对话页里随时改
|
||||
- [x] 工作列表卡片视图(中间栏,与列表视图切换)
|
||||
- [ ] DeepSeek Harness 插件(`dsh-mail-bridge`)
|
||||
- [x] DeepSeek Harness 插件(`dsh-mail-bridge`)
|
||||
- [x] 平台会话快照同步:工作区下的历史会话可在写信时选中
|
||||
- [x] 插件适配方法固化为 `docs/PLUGIN-GUIDE.md`
|
||||
- [ ] 跨主机 Agent 发现(Gateway + Registry 拆分)
|
||||
|
||||
### 7.10 窄屏适配(已完成)
|
||||
|
||||
Reference in New Issue
Block a user