← 返回列表
✓ 可直接安装
长时间或复杂的工作之所以停滞,通常不是因为“工具不够”。而是执行器无法替换、状态没有唯一事实来源、边界没有断言——一个跑…
自动检查通过:npm 包已发布且 engines 声明满足基线(声明 Node >=22.19.0);该结论来自程序自动检查,未经人工实机验证。 · 最近上游提交 2026/9/17 · 已提供中文文档
一个用于在 DeepSeek Harness 中构建、编排和扩展多智能体系统的插件化微内核架构。
综合分
43.9
GitHub 分
43.9
用户评分
—
★ Stars
23
周下载量
—
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add dsh-flownpm 包 dsh-flow 已校验归属本仓库,走 npm 安装最省事
数据截至 2026/9/18(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
安装兼容性检查✓ 自动检查通过
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✓npm 包dsh-flow @ 0.0.1
✓Node 引擎要求 >=22.19.0 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明
验证方式:npm registry 存在性 + package.json 静态校验 · 最后验证 2026/9/18 07:09:04
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动
README
dsh-flow
· English
长时间或复杂的工作之所以停滞,通常不是因为“工具不够”。而是执行器无法替换、状态没有唯一事实来源、边界没有断言——一个跑了几个小时的任务崩溃一次,就说不清进行到哪了;改变工作的执行方式意味着改动核心,增加一种团队来源也是如此。
dsh-flow 在内核形态层面解决了这三个问题:rules/ 是纯核心(无 IO、无 ctx),执行和来源是声明的接缝,而 kernel.js 是唯一的组合根,决定本次部署挂载哪个实现。
它与 dsh-agent-teams 是两种内核形态:相同的能力表面,相反的内部形态。这种对立正是本文的起点。
Agent canvas: requirement timeline + team hierarchy + artwork inspector
= 22.19.0">
两种内核形态
用宿主自己的话说,区别在于一个插件是否把自身内部划分为服务。宿主将此表述为“一切皆插件”和“插件,而非循环改动”,落到代码上就是 core / seam 的划分:
| 常见说法 | 宿主自己的术语 | 证据 |
| --- | --- | --- |
| 微内核 | 让 core 尽可能小,把每项能力挂在声明的接缝上;新行为加到扩展点,而不是加进 core | dsh-flow:一个 rules/ 纯核心(无 IO、无 ctx)+ 三个服务 + 一个组合根 |
| 宏内核 | 没有接缝;所有能力都住在 core 里 | dsh-agent-teams:src/ 是一个扁平平面(17 个 TS 模块 + client/ 下 13 个),彼此自由导入,没有任何机制断言模块边界 |
一个接缝由三个角色组成——服务定义 / 提供者 / 消费者——三者齐备才算完整。在 dsh-flow 中它们对应得明明白白:runner/interface.js 是定义,manual.js 和 subagents.js 是两个提供者,tools/ 是消费者。
两者都是 DSH 插件,并且都附着在宿主提供的接缝上——区别在于插件自身是否有接缝。一旦功能被拆分为核心与接缝,“添加另一种执行方式”就是添加一个 Provider,而“添加另一种团队来源”就是一次 register() 调用;在扁平的 src/ 上,这项工作就是在改动核心。
所以这是对立,而非吸收:能力表面可以对齐(13 个工具与 agent_teams_ 一一对应,pnpm test:diff 中的 31 项差异检查守护着这条线),但两种内核形态无法给出同一组保证。
使用起来是什么感觉
| | dsh-flow | dsh-agent-teams |
| --- | --- | --- |
| 安装 | 克隆下来就能跑,无需安装任何东西 | 拉取一整棵依赖树(24 个 peer,包括 React 18) |
| 宿主升级 | 不声明宿主版本;运行时探测能力,探测不到就走回退分支 | 在其 peer 中锁定 4 个宿主版本;超出这些版本就无法工作 |
| 改一行画布代码 | 保存 → 刷新页面(0 次构建,0 次重启) | 重新构建客户端 bundle → 重启宿主 |
| 停止执行,保留视图 | runner: manual——只看和编辑,什么都不运行 | 没有等价开关 |
| 一个任务的各次尝试如何结束 | 点击任务行查看尝试时间线:每次尝试如何结束、哪次被回滚 | 一个单调递增的尝试计数器,无法说明任何一次是如何结束的 |
| 无法解析的行 | 进入导入报告,在画布上带行号列出 | — |
| 无法到达画布的团队 | 在工具栏中计数;不会无声消失 | — |
| 批准计划 | 直接在画布上编辑并批准,走与 flow_edit_plan / flow_approve 相同的校验 | 面板编辑 |
| 画布 | 会话时间线与团队层级合为一图(团队嵌套在需求轮次之下) | 一个团队树面板 |
| 卸载另一个插件 | 一切照常工作——从来就不存在第二份记录 | — |
上表中你能“感觉到”的一切都来自架构:刷新即生效,因为画布模块通过 HTTP 逐个提供(src/ 由 mtime + ETag 重新验证使其失效),没有打包步骤;runner: manual 存在,因为执行是一个接缝,在组合时固定;卸载另一个插件毫无影响,因为团队记录是本插件自己的仅追加日志,另一个插件只是来源注册表中的一个条目。改动 client.js(标签页注册层)在两边仍然需要重启宿主——那一层确实会进入宿主的客户端 bundle。
测量数据
以下全部是本仓库自己的数字,用一条命令即可复现:
| 项目 | 数字 |
| --- | --- |
| 插件在宿主启动路径上的开销(import './index.js') | 4.3 ms |
| 组合根装配(import './kernel.js',冷启动) | 33 ms |
| 对 src/canvas/ 的更改生效 | 0 次构建,0 次重启* |
| Canvas 首次绘制 | 41 个请求 / 449 KB;之后的每个请求都通过 ETag 重新验证,因此未更改的文件返回 304 |
| 层边界门禁(解析 107 个模块 + 断言 7 个层 + serve 允许列表) | 4.0 s |
| 全部 434 个测试 | 1.1 s |
为什么这里没有“快 N 倍”
跨实现的性能比较要求双方都在本机上运行。dsh-agent-teams 的完整流水线做不到:它没有 node_modules,也没有 lib/(其发布产物),而且它的 snapshot.ts 直接导入宿主运行时包(@deepseek-ai/dsh-llm、@deepseek-ai/dsh-agent)。用桩替换这些再计时,测的是桩,而不是它。它的状态读取层(src/state.ts)只依赖 Node 内置模块,原则上可以在 Node 24 下直接运行,但那是另一个仓库的代码,运行它需要你的明确同意。
因此,本仓库中任何“快 N 倍”的说法都会是编造的。能给出的是上面那种差异——无需运行任何东西即可确认——再加上我自己可复现的测量结果。
长时间运行的工作
运行时间越长,两件事就越重要:崩溃时无法说明进行到了哪里,以及状态在你不知情的情况下分叉。
- 事实来源是仅追加的 events.jsonl;state.json 只是它的一种读取结果。 当两者不一致时,日志胜出,并且这种不一致会被 teamDiffEvents 判定为错误,而不是被悄悄抹平——对账会拒绝它无法表达的差异,而不是挑选一个胜者。运行时间越长,状态分叉的可能性就越大,而悄悄挑选一个胜者正是错误累积的方式。
- 一个任务尝试了多少次、每次如何结束、哪次被回滚——点击任务行查看尝试时间线。这一信息只存在于协议层:单调递增的尝试计数器无法说明任何单次尝试是如何结束的。
- 无法读取的一行永远不会停止解析,但它会进入导入报告,在画布上连同其行号一起列出(kind / member / line / reason)。行号是让一个运行过自身且已损坏的文件变得可修复的唯一东西。
- 如果它失控了,你可以保留视图并停止工作:runner: manual 让你在没有任何执行的情况下查看和编辑。这是一个组合时的选择,而不是运行时热插拔——把这一点说清楚,比保留一个假开关要好。
- 边界被写了下来:团队存储和 workspaces.json 都是单写入者,跨进程锁是建议性的,两个打开的副本仍然可能互相覆盖。
复杂的工作
复杂的工作在判断之间断裂:画布显示的不是队长所决定的,编辑和批准各自判断一次,而在迁移期间,两套记录会相互冲突。
- 判定逻辑位于纯核心中,因此画布和模型看到的是同一套判定。 K9 裁决 / K10 阻塞项 / K11 覆盖矩阵都在快照中执行,与模型调用 flow_status 时看到的同一套计算——因此你从画布上读到的结论不会偏离队长所决定的结论。
- 一张图承载整个层级结构:用户的需求轮次停留在主时间线上,团队在其下方作为嵌套区域生长,每个成员获得一个子区域,其中容纳他们的任务卡片(按依赖深度连线)和发言卡片(按时间顺序排列)。分配通过包含关系表达,依赖通过箭头表达,对话流通过跨越区域的轮次链表达。
- 编辑和审批在画布上进行,但只判定一次:暂存团队的检查器编辑任务 / 成员及其依赖关系并批准它们,走的是与 flow_edit_plan / flow_approve 相同的 applyTeamEdits / applyTeamApproval——K8 门禁只判定一次,画布不是绕过工具层的后门。
- 迁移期间两套团队都可见:源注册表将本次部署的日志与 .agent-teams 并列列出,因此切换只需一行配置,而非大爆炸式迁移。
- 纯核心既不接触 IO 也不接触 ctx:rules/ 下的 25 个模块在纯 Node 下即可导入,434 个测试在 1.1 秒内运行完毕——判定逻辑可以被穷尽测试,这是承载复杂规则的前提。
扩展
- 另一种执行方式 = 另一个 Provider:runner/interface.js 定义了接缝,manual.js / subagents.js 是它的两个 Provider。核心和工具都不需要改变。
- 另一种团队来源 = 一次 register():ctx.flowTeamSources 是一个注册表,因为这些实现确实共存。
- 边界由目录表达,并由门禁逐层断言:七层,每个不变量都在 pnpm run build 中断言(纯核心不得接触 IO 或 ctx,画布不得接触 node: 或任何宿主层,宿主层不得进入 serve 允许列表,依赖必须只指向内部)。无人断言的不变量会随时间腐化,剩下的只是一个恰好放在那里的目录。
- 修改一行画布代码:保存 → 刷新页面。 没有构建步骤;src/* 通过 mtime + ETag 重新验证来失效。
- 零运行时依赖:files 列出的是 .js 而非 lib/,因此克隆下来即可运行——没有“先编译”这一步。
快速开始
你需要一个支持 profile 插件机制的 DeepSeek Harness、Node.js >= 22.19.0,以及 web profile。
dsh plugin --profile web add github:rootkiller6788/dsh-flow
dsh web
此插件未发布到 npm:dsh-flow 这个名字在注册表上属于另一个项目,因此请不要使用 dsh plugin add dsh-flow。
一旦启动,对话区域上方的标签栏会新增一个 Agent Canvas 标签页;你也可以直接打开 /dsh-flow/。
画布上有什么
一页、一个引擎、一张图。节点分为两类:
| 节点 | 绘制内容 | 数据来源 |
| --- | --- | --- |
| 会话轮次卡片 | 一轮对话(问题 + 回答),通过 DSH 原生的分叉关系接入分支树;可从这里进行追问 / 分支 / 归档。Agent 事件轮次——成员转述、子代理通知——渲染为派生标签(论文手 → 队长),绝不显示原始协议文本 | 宿主的 sessions + workspaces 服务,投影到磁盘的 flow/workspaces.json |
| 团队区域 | 在团队标题栏下方,嵌套的成员子区域:每个成员一个单元格,容纳其任务芯片(按依赖深度连线)和发言卡片(按时间顺序排列)。分配关系通过包含关系表达,依赖关系通过箭头表达,对话流通过跨区域的轮次链表达 | 团队数据存放在 dsh-flow 自己的存储中(//events.jsonl);当部署中存在 .agent-teams/ 时,它会作为只读来源一并列出 |
多代理对话
打开一张轮次卡片,检查器会显示一个参与者气泡流:用户消息、成员转述消息(作品头像 + 发送者 → 接收者 方向)以及子代理通知各自作为独立区块,工具调用则折叠进过程记录中。
agent-teams(或宿主)会将成员消息以类似 Agent sent a message:【sender → recipient】body 的信封形式转述到宿主会话中。本插件在投影层将信封拆解为结构化数据(message.agent),渲染层对旧数据应用同样的规则作为回退——UUID 和协议文本永远不会出现在画布上。
团队检查器
点击团队标题栏或成员子区域,右侧面板会显示整个编排:成员作品行(头像 + 角色 + 模型 + 进度 + 当前活动)、任务依赖列表(状态芯片)、队长信箱(真实的成员 → 队长消息),外加三个判定面板:
- K9 裁决 / K10 阻塞项 / K11 覆盖矩阵——与模型调用 flow_status 时看到的同一套计算,因此你从画布上读到的内容不会与队长的决策产生偏差
- 点击任务行可展开该任务的尝试时间线:尝试了多少次、每次尝试如何结束、哪一次被回滚
- 暂存团队可以直接在检查器中编辑和批准,走的是与 flow_edit_plan / flow_approve 相同的校验流程
当团队数据包含无法读取的行时,团队卡片上会长出一个 数据损坏 N 徽章,检查器会逐条列出,包含其类型 / 成员 / 行号 / 原因——行号是让损坏文件可修复的唯一依据。完全无法到达画布的团队由工具栏中的部署级计数覆盖;否则它们根本不会出现在画布上。
图稿
assets/ 附带 15 张角色/动作图片(9 种职业 + 6 种状态)。成员名称和角色关键词会自动映射到图稿(research/data → 分析师,modelling/science → 科学家,verify/review → QA,solve/implement → 工程师,paper/writing → 研究员,captain → 队长……),回退为一个带有首字符的色块。图稿容器的背景跟随浅色/深色主题。
团队数据存放位置
团队结构(成员 / 任务 / 依赖 / 邮箱)存放在 dsh-flow 自己的存储中,每个团队一个目录:
/ # defaults to .dsh-flow
└── /
├── events.jsonl # the append-only source of truth — everything the team did
├── state.json # a checkpoint: one reading of events.jsonl, discardable and rebuildable
├── manifest.json # metadata such as creation time and the initial goal
└── mail/ # one .jsonl mailbox per member
日志是事实;检查点是缓存。当两者不一致时,日志胜出,并且这种不一致会被 teamDiffEvents 判定为错误,而不是悄悄抹平——对账会拒绝它无法表达的差异,而不是选出一个胜者。无法读取的行永远不会中断解析,但它确实会进入导入报告。
团队的来源是一个源注册表(ctx.flowTeamSources):此部署自己的日志始终被注册,而当 .agent-teams/ 存在时,它会同时被列为只读源。在迁移期间你会看到两套数据,因此切换只需一行配置,而不是大爆炸式迁移(canAppend 为 false 的源无法从画布编辑)。
对话编织本身来自对话投影中的中继解析,不依赖任何外部插件。
使用它
- /dsh-flow 命令:/dsh-flow [--profile ] 使当前会话成为队长并组建一个团队。每个 profile 还会获得一个别名 /dsh-flow-(profile 名称必须是小写字母数字加连字符才可寻址;像 bug fix 这样的名称不会产生别名,而不是进行猜测性的规范化)。该命令只创建团队并停在暂存计划处——它绝不会在同一轮中批准,因为审查正是暂存存在的理由。
- 拖拽与记忆:卡片可以拖拽;坐标作为纯视觉元数据存入浏览器 localStorage——节点的身份始终是真实的东西(DSH 会话 / teamId / 成员名称),位置从不决定身份。“重置”会回到自动布局。
- Inspector:点击卡片(不是按钮)打开右侧检查器,它还会将当前 DSH 会话切换到该卡片——无需离开画布。按 Esc 关闭。
- Follow-up / branch:检查器页脚和卡片角落会在画布上打开一张草稿卡片;输入在画布上进行,这是唯一的写入入口。快捷短语可编辑(最多 12 条,每条 16 个字符)。
- DSH button:切换回宿主的 Chat 标签页并锚定到该轮次;完整的过程记录在原生对话中查看。
- Archive:卡片上的归档按钮会将会话移出画布(记录在 hiddenSessionIds 中);刷新 DSH 列表不会将其重建。
- Theme:浅色 / 深色跟随宿主(theme/change 事件 → data-theme),深色由相同的设计令牌驱动,画作容器背景也随之切换。
- 滚轮在卡片上滚动卡片自身的答案,在空白处缩放画布。
Configuration
在配置文件的 cordis.patch.yml 中覆盖此插件的配置:
- insert:
- id: dsh-flow
name: dsh-flow
config:
dataFile: !!js dshHomePath('flow/workspaces.json')
autoProjection: true
projectionWorkspaceTitle: DSH 任务
trustedHosts: []
stateDir: .dsh-flow # where teams live
runner: subagents # manual | subagents (fixed at composition time)
memberProvider: spawn
maxMembers: 8
agentTeamsStateDir: .agent-teams # open this only while migrating
profiles: {...} # see below
| Key | Default | Meaning |
| --- | --- | --- |
| dataFile | dshHomePath('flow/workspaces.json') | 画布图形的持久化路径。必填 |
| autoProjection | true | 是否自动将 DSH 会话投影到画布上(监听 session/created 和 session/event) |
| projectionWorkspaceTitle | DSH 任务 | 当无法从 cwd 推导出工作区名称时的回退标题 |
| trustedHosts | [] | Host 检查额外接受的权威来源(localhost 和 127.0.0.1 始终允许) |
| stateDir | .dsh-flow | 团队日志和邮箱的根目录;相对路径会相对于会话工作目录解析,从而避免两个部署的团队互相干扰 |
| runner | subagents | 组合时的选择:subagents 是真实内核,manual 表示“你可以查看和编辑,但不会执行任何内容” |
| memberProvider | spawn | 成员启动时使用的子代理提供程序 |
| agentTeamsStateDir | 无 | 指定后,将该 .agent-teams 目录注册为只读来源;迁移期间两组团队都可见,而 flow_ 工具仍作用于我们的团队 |
| maxMembers | — | 配置档案名册及其生成的团队成员数量上限 |
| profiles | 无 | 团队配置档案表:一个命名名册 + 种子任务 + 审查策略 |
profiles 是部署配置而非协议——一次部署提供哪些团队是它自己的事,所以它存在于配置中而非代码里。一个 profile 的 taskPlanning 恰好有两个值:seed(下面的任务定义了整张图)或 captain(名单已给定,图由 captain 来设计)。名单中给出的 role / reasoning_effort 决定成员的默认路由。
/dsh-flow 路由不在 DSH /api 的浏览器信任围栏内,因此插件会自行检查 Host 头以防范 DNS 重绑定;要从非本地地址访问它,请将主机名加入 trustedHosts。
架构
一个包,零运行时依赖,无需构建。源码即产物:files 列出的是 .js,而非 lib/——克隆下来即可运行,没有“先编译”这一步。
宿主的设计哲学是一切皆插件,并将服务分类为 core / seam / bundle。dsh-flow 也以同样的方式划分自身:一个纯核心,两个接缝,一个组合根。
三个服务
| 服务 | 类别 | 形式 | 为何必须是这种形式 |
| --- | --- | --- | --- |
| ctx.flowTeams | core | 单实例 | 此部署恰好只有一条团队记录,且其他插件会消费它 |
| ctx.flowTeamSources | seam | 注册表 | 这些实现确实共存:在迁移期间,原生团队和 .agent-teams 团队必须同时可见 |
| ctx.flowRunner | seam | 二选一,在组合时选定 | 两个实例会互相冲突:两个调度器会争抢同一个任务 |
一个名称只能有一个提供者(在同一名称下第二次 ctx.provide 会抛错),因此上述区分不是风格选择,而是硬性约束——共存的东西成为注册表,互斥的东西必须在组合时确定。
kernel.js 是组合根:唯一将 store / runner / tools 并排放置的地方。它本身不实现任何能力;它只决定此部署使用哪个实现,以及它们挂接到哪些接缝上。
七层,每层的不变量由门禁断言
边界由目录表达,并由 pnpm run build 逐层断言——无人断言的边界会随时间腐化,剩下的只是一个恰好摆在那里的目录。
| 层 | 模块数 | 门禁断言的不变量 |
| --- | --- | --- |
| rules/ | 25 | 纯核心:无 node:,无 ctx——可被普通 Node 导入 |
| canvas/ | 12 | 浏览器侧:无 node:;它只能读取纯核心,别无其他,导入任何宿主层都是越界 |
| store/ | 5 | 宿主侧:不得进入 serve 允许列表(进入即意味着将其发送到浏览器) |
| sources/ | 3 | 同上 |
| runner/ | 11 | 同上 |
| tools/ | 8 | 同上 |
| config/ | 2 | 同上 |
| 所有层 | — | 依赖只向内指向;任何模块都不得导入 canvas/(它是叶子,不是库) |
dsh-flow(纯 JS,无运行时依赖)
├── src/
│ ├── rules/ # 25 — 纯核心:K1–K14 + 事件协议 + 投影 + 对账
│ │ # entities / gates / project / reconcile / coverage / delivery …
│ ├── store/ # 5 — 团队注册表 + 仅追加日志 + 邮箱 + 快照 + 导入报告
│ │ # → ctx.flowTeams(核心)
│ ├── sources/ # 3 — 团队来源注册表:此部署的日志 / 导入的 .agent-teams
│ │ # → ctx.flowTeamSources(接缝,注册表形式)
│ ├── runner/ # 11 — 执行器接缝:interface.js 定义它,manual / subagents 实现它
│ │ # → ctx.flowRunner(接缝,组合时二选一)
│ ├── tools/ # 8 — 13 个 flow_* 工具的注册与实现
│ ├── config/ # 2 — 部署配置:profile 表 + 将配置转换为事件的两个钩子
│ └── canvas/ # 12 — 画布页面(唯一通过 HTTP 提供服务的层)
│ ├── canvas.js # 入口:宿主桥接、实时回复、轮询、启动
│ ├── core.js # 共享状态、几何、localStorage、宿主桥接、成员颜色
│ ├── html.js # 转义及其他纯辅助函数(拆分出来以便面板模块可在 Node 中测试)
│ ├── markdown.js # Markdown 渲染(带 ■ 分节规范化)
│ ├── relay.js # agent 信封解析(中继 / 成员消息 / 子代理通知)
│ ├── session.js # 会话投影数据层(增量合并 + 游标)+ 回合卡片 + 分支布局
│ ├── teams.js # 团队轮询(实时 → 快照回退)+ 层级区域布局
│ ├── team-panels.js # 面板的纯渲染(K9/K10/K11、尝试时间线、损坏行)
│ ├── scene.js # 场景组装:需求时间线 + 嵌套区域 + 类型化边
│ ├── view.js # 相机、虚拟化挂载、节点渲染、检查器、主渲染
│ ├── artwork.js # 美术映射(角色关键词 → 职业图像,状态 → 动作图像)
│ └── actions.js # 交互:草稿 / 跟进 / 分支 / 归档 / 快捷短语 / 选择并提问
├── index.js # 宿主侧入口:WorkspaceStore + 会话事件投影 + 信封解析 + 路由
├── client.js # 客户端:一个 conversation.view 标签页(嵌入式 iframe)+ 主题跟随 + 操作中继
├── engine.js # 画布引擎:相机 / 手势 / 视口剔除 / 边几何 / 拖拽绑定
├── theme.css # 设计令牌(明暗模式共用一组变量)+ 所有组件样式
├── assets/ # 15 张美术图像(9 种职业 + 6 种状态)
├── kernel.js # 组合根:将 store / runner / tools 并排放置的唯一位置
├── cordis.patch.yml # 插入 dsh-flow 服务并为其提供配置
└── package.json # dsh.bundle.patch + dsh.client.inject
依赖只向内指向。 rules 不依赖任何层;store / sources / runner / tools / config 只依赖 rules;canvas 只读取 rules(团队数据通过 HTTP 到达,而不是通过导入);组合根依赖它们全部。上述每一条边都在门禁中被断言。
有三条规则最容易被悄无声息地破坏,而门禁对每一条都做了显式处理:一个 canvas 模块执行 import 'node:fs' 在 pnpm test 下处处都是绿的,只有浏览器才会察觉;一个模块从 serve 允许列表中缺失,只能通过 404 被发现;而一个宿主模块反向导入 canvas/ 在 Node 中同样是绿的。所以这些不是约定,而是断言。
如何观察团队活动:通过它自己的通道,而不发出会话事件。 事实来源是 //events.jsonl(一个仅追加的事件日志),它被投影到 GET /dsh-flow/map-api/teams 供 canvas 读取。
没有 dsh-flow/ 事件被写入会话,因为宿主不接受这样的事件:KNOWN_SESSION_EVENT_TYPES 是一个在构建时生成的封闭集合,其注释明确说明下游插件的事件“按构造就在此列表之外”,并且注册接口“推迟到有真实消费者时再定”;而 Session.append 没有提供设置信封 ignorable 标志的方法——没有它,无法识别的类型会被视为必需,读取方宁可拒绝重建整个会话。因此这样的事件要么被丢弃,要么破坏它所落入的日志。完整论证见 kernel.js 末尾的注释。
参考
HTTP 路由
全部经过 Host 检查(默认允许 localhost / 127.0.0.1)。
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| GET | /dsh-flow | 302 → /dsh-flow/ |
| GET | /dsh-flow/ | canvas 页面 |
| GET | /dsh-flow/engine.js · /dsh-flow/src/canvas/.js · /dsh-flow/src/rules/.js · /dsh-flow/theme.css | canvas 资源 |
| GET | /dsh-flow/assets/.png | 美术资源(仅允许 [a-z0-9-]+.png) |
| GET | /dsh-flow/map · /dsh-flow/map/ | 302 → /dsh-flow/(旧路径) |
| * | /dsh-flow/map-api/ | 见下文 |
Canvas API
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| POST | /map-api/reset | 清空每个工作区并隐藏所有当前 DSH 会话 |
| GET | /map-api/workspaces | 工作区摘要列表 |
| POST | /map-api/workspaces | 创建工作区 { title } |
| GET | /map-api/workspaces/:id | 完整工作区(含线程 / 消息) |
| POST | /map-api/workspaces/:id | 在工作区中创建节点 { title, parentId?, dshSessionId?, position?, color? } |
| POST | /map-api/threads/:id/branch | 从节点分支 { title?, dshSessionId?, position?, color? } |
| POST | /map-api/threads/:id/messages | 追加一条消息 { text } |
| PATCH | /map-api/threads/:id | 修改 title / position |
| DELETE | /map-api/threads/:id | 删除一个节点及其所有后代,并隐藏匹配的 DSH 会话 |
| POST | /map-api/sessions/sync | 将画布与主机的会话列表 { sessions, removedSessionIds } 对齐 |
| POST | /map-api/projection | 增量读取:{ sessionIds, cursors } → 仅返回这些会话所属的线程,每个线程仅携带 rev 超过游标的消息;始终返回 threadIds,以便客户端可以修剪已归档的节点 |
| GET | /map-api/teams | 团队快照 { teams, damaged }:源注册表中当前可见的每个团队,每个团队都有自己的 warnings(导入报告);damaged 是整个部署范围的计数——一个甚至无法到达画布的团队只能在这里看到 |
| GET | /map-api/profiles | 可寻址的配置文件列表(那些具有真值 profileCommandName 的配置文件) |
| GET | /map-api/teams/:teamId/tasks/:taskId | 一个任务的尝试时间线(包括回滚)。按需获取,从不包含在一秒轮询中 |
| POST | /map-api/teams/:teamId/plan | 从画布编辑暂存的计划。与 flow_edit_plan 共享 applyTeamEdits,因此 K8 只评判一次 |
| POST | /map-api/teams/:teamId/approve | 从画布批准暂存的计划。与 flow_approve 共享 applyTeamApproval |
最后两个是一个人通过自己的画布对自己的团队进行操作,由 Host 检查而非会话身份授权——HTTP 请求不携带身份,而编造一个身份将是保证的表象而非保证。有两个约束防止它们成为绕过工具层的方式:它们经过相同的 applyTeamEdits / applyTeamApproval,并且它们只接受 canAppend 为 true 的源(从 .agent-teams 导入的团队是他人的记录,不得追加)。
postMessage 协议
在画布页面和主机客户端之间,只有 { source: 'dsh-flow', type, ...payload } 会被识别,并且 origin 和 event.source 都必须为此插件挂载的框架。
| 方向 | 消息 | 载荷 | 行为 |
| --- | --- | --- | --- |
| 画布 → 主机 | flow:request-current | — | 发回当前工作区和会话 |
| 画布 → 主机 | flow:open-session | sessionId, seq? | sessions.open() + 切换回 Chat 标签页 + 将滚动锚定在 seq |
| 画布 → 主机 | flow:activate-session | sessionId | sessions.open() 而不离开画布 |
| 画布 → 主机 | flow:fork-session | sessionId, atSeq?, requestId | sessions.fork() |
| 画布 → 主机 | flow:send-message | sessionId, text, requestId | session.prompt(text, 'queue') |
| 画布 → 主机 | flow:create-session | workspaceId?, cwd?, requestId | sessions.create() |
| 主机 → 画布 | flow:theme · flow:locale | dark / locale | 主题和区域设置跟随 |
| 主机 → 画布 | flow:workspaces · flow:current-session | 工作区 / 当前会话 | 画布的输入 |
| 宿主 → 画布 | flow:live-reply | sessionId, running, text | 生成过程中的实时回复 |
| 宿主 → 画布 | flow:forked-session · flow:created-session · flow:message-sent · flow:bridge-error | requestId, … | 结算画布发起的 RPC |
携带 requestId 的调用会被画布的 dshRpc() 等待,超时时间为 20 秒;直接在浏览器中打开画布页面(在 DSH 之外)会立即被拒绝。
本地存储
| 位置 | 内容 |
| --- | --- |
| /flow/workspaces.json(+ .lock) | 工作区 / 节点 / 投影消息,以 gzip 压缩存储(纯 JSON 也可读);仅允许单写入者 |
| //events.jsonl | 团队的事实来源:仅追加的事件日志(默认 stateDir 为 .dsh-flow) |
| //state.json · manifest.json · mail/ | 检查点、元数据,以及每个成员一个邮箱 |
| dsh-flow:map-card-positions:v3 | 会话卡片坐标 |
| dsh-flow:cluster-positions:v1 | 团队区域卡片坐标 |
| dsh-flow:map-collapsed-cards:v1 | 折叠状态 |
| dsh-flow:map-quick-phrases:v1 | 快捷短语 |
| dsh-flow:map-branch-anchors | 分支锚点 |
浏览器中存储的一切都仅是视觉元数据;真正的会话始终存在于 DSH 中——清除缓存只会丢失布局,别无其他。
设计决策
一个画布,层级化排布。 会话和团队是同一项工作的两种视图:团队由会话发起,任务在会话中汇报。将它们拆成两个页面只会意味着两套引擎和两种外观。引擎(engine.js)只处理相机、手势、剔除和边,对领域一无所知;会话和团队都只是它之上的节点和边,团队作为需求时间线下方的嵌套区域生长——层级靠包含关系,时间靠列。
团队数据存放在本地。 团队结构落在本插件自己的仅追加日志中,画布读取自己的 map-api,不镜像任何人的状态。直接后果是卸载 agent-teams 不会改变任何东西:不存在“降级为冻结快照”的中间状态,因为从来就没有第二份记录。当 agent-teams 安装时,它是注册表中的只读来源——一个开关,而非依赖。
中继消息在投影层进行结构化。 信封解析位于宿主侧的 index.js 中,而不是渲染层,因为落到磁盘上的已经是脏数据,晚洗不如早洗;渲染层只是作为回退,对更旧的数据应用同样的规则。
写入统一经由草稿卡片。 画布上的跟进 / 分支 / 新会话都经过那一个入口点,其他一切仍在宿主的原生对话中完成——画布不会构建第二个编辑器。
作品即身份。 成员名称和角色关键词会哈希到 assets/ 中的职业作品:成员卡片、检查器气泡和成员行都从同一来源读取,回退到带有首字符的色块。容器背景跟随浅色/深色主题。
开发
pnpm test # 434 项测试:规则核心、真实文件系统上的存储、调度器、工具、整个内核
pnpm run build # 语法 + 层边界 + 服务允许列表 + 主题令牌纪律
pnpm test:diff # 针对 dsh-agent-teams 的 31 项差异检查(相同函数、相同输入、比较结论)
pnpm test:rehearsal # 针对真实的 .agent-teams 目录进行演练:磁盘上的一切都可访问
dsh web
点击对话区域上方的 Agent Canvas 标签页
更改 src/ 下的任何内容后,务必运行 pnpm run build。 它不仅仅是语法检查:每一层的不变量都在那里被断言(纯核心不得接触 IO 或 ctx,画布不得接触 node: 或任何宿主层,宿主层不得进入服务允许列表,依赖必须只指向内部,每一层的入口必须存在)。这些错误的共同点是在 pnpm test 下显示为绿色——一个执行 import 'node:fs' 的画布模块只有在浏览器中才能被发现,而一个不在允许列表中的模块只能通过 404 被发现。
没有人断言的边界会随时间腐化,剩下的只是一个恰好位于那里的目录。
更改 client.js 需要重启宿主:客户端包携带一个 rev 哈希,并且只在重启时重新打包。engine.js / src/ / theme.css / assets/.png 通过以 mtime 为键的内存缓存,并使用 cache-control: no-cache + ETag 重新验证——每个请求都会确认文件未更改(如果已更改,则返回 200 和新内容),因此刷新页面就足够了。
已知限制
- 画布的实际渲染没有自动化测试:434 项测试覆盖宿主侧(规则、存储、调度器、工具、内核)以及面板模块的纯渲染函数。画布的 DOM 行为——拖拽、 交互、可点击的任务行、编辑后重绘——必须在真实浏览器中走查,而这部分没有 CI 支持。
- .agent-teams 源是只读的:它可以被列出和读取,但不能从画布编辑(canAppend 为 false)。它是别人的记录,追加会在我们并不拥有的 id 下把团队的第二个副本写入我们的日志。
- 团队与其会话之间没有边:团队记录只说明是哪个会话发起了它;画布上的对话链来自会话投影本身。
- 两个数据位置都是单写入者:workspaces.json 和团队存储各自都有一个跨进程锁以及一条“已被另一个实例修改”的警告,但该锁是建议性的,两个打开的副本仍然可能相互覆盖。
- 画布自身的文本未国际化:标签页名称遵循宿主语言,画布的内部文本目前是中文。
- 画布页面不携带导航界面元素:其唯一入口是宿主标签栏。仅当存在会话时该标签页才会出现(对于空白会话,宿主返回 null)。如果宿主的 slots 服务缺失,该标签页不会被注册,只有直接链接可用。
许可证
MIT © 2026 rootkiller6788 — 参见 LICENSE。同作者(rootkiller6788)的其他插件
扫码进群