Skip to content

Latest commit

 

History

History
106 lines (77 loc) · 6.36 KB

File metadata and controls

106 lines (77 loc) · 6.36 KB

它是怎么搭的

English

两半

package.json        双声明:dsh.bundle(宿主层)+ dsh.client(浏览器层)
src/org.ts          纯投影:会话事件 → 事务所 + 时间线(零 import、零 IO)
src/index.ts        Cordis 宿主:订阅事件、观察策略、挂 /abyss 端点
client/client.js    嵌入式面板(手写,无构建步骤)
lib/index.js        宿主层的编译产物
tests/              113 个用例

为什么宿主层要编译dsh 用普通 Node 跑插件,而 Node 拒绝对 node_modules 里的 TypeScript 做类型剥离。一个直接发 .ts 入口的插件在作者机器上(pnpm dsh 跑在 tsx 下)能用,用户装上就炸。所以宿主层用 tsdown 编到 lib/index.js;浏览器层本来就是纯 JavaScript。

零运行时依赖@deepseek-ai/cordis 是纯类型导入,构建时擦除;运行时只用 node: 内置模块。

投影:事件 → 事务所

src/org.ts 是一个纯函数集合,把会话事件流归约成一间事务所。它不认识网络、不认识文件、不认识 cordis。

用到的事件(dsh 一共声明 47 种,这里读其中 20 种):

事件 投影成什么
subagent/descriptor 一位新同事入职:岗位、厂商、正式/临时
session/title 案子的名字(和产品侧栏同一个名字)
request/header / request/context 模型路由、上下文窗口
turn/start / turn/end 上下班(考勤泳道的一段班次)
assistant/chunk 思考中 / 发言中
assistant/message 台词 + 令牌 + 工资 + 上下文占用
tool/call 活动状态;subagent → 派活,send_message → 私聊,report → 汇报
tool/result 失败计数(error 结果块的 isError
`tool-workflow/agent-start end`
approval/asked / approval/decided 等批准 / 已批复 + 阻塞时长
llm/retry 重拨
compaction/start / compaction/summary 去档案室 + 归档掉的令牌数
todo/write 员工档案里的待办清单

设计原则:

  • 每一句台词都是日志原文,没有模板句。取不到文本时宁可只改状态、不编一句话。
  • 返回相同引用表示无变化,宿主据此决定要不要广播。
  • 不猜:路由没广播上下文窗口就不画水位;shell 退出码非零不计入失败(dsh 把它当普通输出,去嗅探那段文字会让每个没匹配到的 grep 都变成事故)。

案子 = 会话树

dsh 的真实层级是:

进程(一个 dsh 实例)        事务所
 └─ 工区(cwd)              项目
     └─ 会话树               案子:主会话 + 它下面的全部子代理
         └─ agent 会话       一位成员

一个项目里开 N 个会话就是 N 个并行的案子,不是一张扁平花名册。面板因此有三档范围:本会话 / 本项目 / 全部,页签和底栏都跟着当前范围走。

超过 maxTeams 时,宿主只淘汰已经全部结束的案子,进行中的永不淘汰。

数据端点

全部挂在产品自己的 origin 下 /abyss没有 CORS 头——一个宽松的跨域头会让你访问的任意网站都能读到你的会话标题、工具名和花费。

端点 用途
GET /abyss/state 当前快照(成员、场景、组织树、主题)
GET /abyss/events SSE 实时流:snapshot / staff / scene / gone
GET /abyss/replay?team=<id> 磁盘日志重建一个案子(面板用它回放,也用它重建办公室)
GET /abyss/report?team=<id> 同一个重建结果渲染成 Markdown 复盘
GET /abyss/cases 档案库:磁盘上的案子列表(只读 header,不打开日志)

无 web 服务器的组合(headless / ACP)可以显式配 port 单独开一个数据端口;办公室视图本身是 Web UI 特性。

持久层怎么读

  • 实时ctx.on('session/event')
  • 历史:优先 ctx.sessionPersistence(durable 目录),回退到 ctx.sessions(只有本进程还开着的会话)。这一步是「重启后办公室还在」的全部原因——只读实时 store 的话,昨天的案子一律 404。
  • 读不到就上报:某个成员的日志损坏(比如 seq 断档)时,重建结果会带上 unreadable,复盘报告顶部和回放条上都会写明。一份安静地少算了的报告,比没有报告更糟。

浏览器层

  • 手写 lazy-CJS bundle,经 window.__ModuleLoader__ 注册,由产品的 /plugins 下发。
  • 所有插值都转义:会话标题、工具名、场景台词都是模型产物,一个没转义的引号就是注入产品自己的页面。
  • 渲染按帧节流且有上限:一屏最多 12 张卡片、浏览器侧最多 300 条场景。
  • 瞬时状态放在模型里,不放在 DOM 里:每次渲染都会替换楼面的 markup,只存在 DOM 里的气泡会被下一次重绘抹掉。
  • 组织树由面板自己推导,不依赖线路上的那份——否则从磁盘重建的办公室会出现「楼面 7 个人、组织树 0 个节点」。
  • 主题跟随 body[data-ds-dark-theme],语言跟随 html[lang],动效尊重 prefers-reduced-motion

测试策略

npm test        # 113 个:投影 46 + 宿主装配 15 + 浏览器 52
  • 浏览器测试驱动的是发布的那个 bundle,不是逻辑副本。本项目踩过的每一个客户端缺陷——全局被遮蔽、引号未转义、状态只存在 DOM 里、折叠开关反转、动画 class 落到文字上——对"重新实现一遍逻辑"的测试都是不可见的。
  • 宿主测试用真实事件形状驱动 apply(),覆盖端点、SSE、配置校验、持久层回退、损坏日志。
  • 真机联调:真实跑任务并在运行中观察面板、并发三个会话、真实触发审批升级与工具失败;并把插件报出的数字和原始日志逐项对账(排除损坏成员后逐项吻合)。

已知局限

  • 实时楼面在内存里,重启后靠磁盘重建;compactionllm/retry 两条路径有单测但缺真机证据(真实日志里它们分别出现 0 次和 1 次)。
  • 便签动画需要收发双方的卡片都在屏幕上,否则只有发送方说话。
  • 回放按压缩后的真实间隔走,长时间空档会被跳过而不是等过去。