🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

静默群聊路由 cilarx/dsh-attention-group

DeepSeek Harness
cilarx11 天前有提交已提供中文文档扫描:含敏感能力

一个会闭嘴的 DSH 多智能体群聊:决定谁值得被叫醒。

31.3/100
综合分
2
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/25 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add cilarx/dsh-attention-group
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · chat
装得上吗
本站已在 dsh 0.2.1-rc.5 下真实安装成功
非静态推断
L5 · 运行时冒烟等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 11 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

读写/删除本地文件发起外部网络请求
查看扫描证据(共 29 处,展示前 27 条)
  • 发起网络请求
    src/client.js:87
    return fetch(API + query, { cache: 'no-store' }).then(readBody)
  • 发起网络请求
    src/client.js:98
    return fetch(API, {
  • 发起网络请求
    src/client.js:121
    return fetch(API + '?candidates=1' + query, { cache: 'no-store' }).then(readBody)
  • 读取本地文件
    src/store.js:135
    if (!existsSync(file)) return { ledger: emptyLedger() }
  • 读取本地文件
    src/store.js:138
    text = readFileSync(file, 'utf8')
  • 写入本地文件
    src/store.js:156
    try { renameSync(file, aside) } catch { }
  • 写入本地文件
    src/store.js:190
    mkdirSync(root, { recursive: true })
  • 写入本地文件
    src/store.js:225
    renameSync(temp, file)
  • 写入本地文件
    src/store.js:278
    mkdirSync(dir, { recursive: true })
  • 读取本地文件
    src/store.js:340
    if (!existsSync(file)) return { records: [] }
  • 读取本地文件
    src/store.js:343
    text = readFileSync(file, 'utf8')
  • 写入本地文件
    src/store.js:400
    mkdirSync(dir, { recursive: true })
  • 读取本地文件
    src/store.js:426
    if (!existsSync(file)) return { records: [] }
  • 读取本地文件
    src/store.js:429
    text = readFileSync(file, 'utf8')
  • 写入本地文件
    src/sink.js:67
    mkdirSync(dirname(file), { recursive: true })
  • 写入本地文件
    src/sink.js:132
    mkdirSync(dirname(wanted), { recursive: true })
  • 读取本地文件
    src/sink.js:155
    if (!existsSync(file)) return file
  • 读取本地文件
    src/sink.js:159
    if (!existsSync(candidate)) return candidate
  • 读取本地文件
    src/sink.js:167
    return statSync(file).size
  • 读取本地文件
    src/sink.js:176
    return readFileSync(file, 'utf8')
  • 写入本地文件
    src/sink.js:277
    mkdirSync(dirname(file), { recursive: true })
  • 读取本地文件
    src/sink.js:326
    if (!existsSync(dir)) return []
  • 读取本地文件
    src/sink.js:328
    return readdirSync(dir)
  • 读取本地文件
    src/sessions.js:142
    buckets = readdirSync(root, { withFileTypes: true })
  • 读取本地文件
    src/sessions.js:151
    entries = readdirSync(bucketDir, { withFileTypes: true })
  • 读取本地文件
    src/sessions.js:179
    entries = readdirSync(dir, { withFileTypes: true })
  • 读取本地文件
    src/sessions.js:190
    const info = statSync(join(dir, entry.name))
扫描于 2026/11/3(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

依赖的 DSH / Cordis 模块
@deepseek-ai/cordis@deepseek-ai/dsh-agent@deepseek-ai/dsh-session@deepseek-ai/dsh-tools
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

DSH 的多智能体群聊,决定该唤醒谁:持久消息日志、注意力路由、跨工作区成员管理。

README

由 DeepSeek 最新模型翻译生成
dsh-attention-group 一个会闭嘴的 DSH 多智能体群聊:决定谁值得被叫醒。 English · 一个给多个 DSH 会话(以及你自己)用的持久群:消息先落盘、再路由、最后才被允许叫醒谁。 一条无关的消息,成本是 0 个 turn。 为什么不是"又一个群聊" 所有群聊都在投递时花预算:先把所有人叫醒,然后每个被叫醒的成员自己发现"这条不是给我的"。 一条本不需要任何人回应的消息 = N 个 turn。 本插件把这个判断挪到叫醒之前。发一条消息,返回的是路由计划: routing: SILENT — nobody matched, zero turns spent 或者 routing: 1 recipient(s) — plan only, nothing woken room-admin mention not-woken (dry run) 唤醒是第二个显式动作,而且需要两个开关同时同意(见唤醒是两个开关)。 三条推论在本项目里是【设计规则】而不是巧合: - 建群、拉人、读历史、导出、摘要,一律不叫醒任何人。 - 面板里发一条"没点名"的消息,也不叫醒任何人 —— 只有点名(@某人,或 @所有人 且群开着「允许 @所有人」)并且这个群的「唤醒」开着时,那几个人才会被真的叫醒。
展开完整文档(README 剩余 1 节)
dsh-attention-group

一个会闭嘴的 DSH 多智能体群聊:决定谁值得被叫醒。

English ·

一个给多个 DSH 会话(以及你自己)用的持久群:消息先落盘、再路由、最后才被允许叫醒谁。
一条无关的消息,成本是 0 个 turn。

为什么不是"又一个群聊"

所有群聊都在投递时花预算:先把所有人叫醒,然后每个被叫醒的成员自己发现"这条不是给我的"。
一条本不需要任何人回应的消息 = N 个 turn。

本插件把这个判断挪到叫醒之前。发一条消息,返回的是路由计划:

routing: SILENT — nobody matched, zero turns spent

或者

routing: 1 recipient(s) — plan only, nothing woken room-admin	mention	not-woken (dry run)

唤醒是第二个显式动作,而且需要两个开关同时同意(见唤醒是两个开关)。

三条推论在本项目里是【设计规则】而不是巧合:

- 建群、拉人、读历史、导出、摘要,一律不叫醒任何人。
- 面板里发一条"没点名"的消息,也不叫醒任何人 —— 只有点名(@某人,或 @所有人 且群开着「允许 @所有人」)并且这个群的「唤醒」开着时,那几个人才会被真的叫醒。除此之外面板上只有「生成群史摘要」会启动 turn(同样要求「唤醒」开着,见群史摘要)。没点名就是没点名:一个人在自己面板里随手说句话不是事件。
- 不调用任何别的插件。 群、聊天记录、结论、导出全是普通文件 —— 所以它在白板 DSH 上可用,而且删掉本插件之后文件还在。

30 秒上手

前置条件

- DSH 0.2.1-rc.2(这里每一个接口最近一次就是对着这个版本核实的;最早是照着 0.2.0-rc.2 写的, 0.2.1-rc.1 上重新核实过,升到 0.2.1-rc.2 之后又核对了一遍)。
- 一个可以装进去的 profile。下文示例用 --profile desktop。

安装

dsh plugin --profile desktop add github:cilarx/dsh-attention-group

这就是全部。2026-10-17 从一个干净 profile 端到端实测过:命令退出码 0,DSH 自动初始化 profile,装出来的包里 7 个源文件、bundle patch、cordis.patch.yml 全在。

裸包名那一条(dsh plugin --profile desktop add dsh-attention-group)现在还解析不了,因为没有 npm 发布。请用上面的 github: 形式,或者用 最新 Release 的 tarball:

从 Release 页下载 dsh-attention-group-0.1.4.tgz,然后:
dsh plugin --profile desktop add "C:\下载目录\dsh-attention-group-0.1.4.tgz"

目录路径、tarball、git URL 都行,因为 dsh plugin 本身就是包管理:它在 profile 目录里把命令交给 pnpm(不带子命令跑一次 dsh plugin --profile <名字>,回给你的就是 pnpm 自己的帮助)。让它成为插件的,是 package.json 里声明了 dsh.bundle.patch。

新 profile 会被自动写好契约的两半 —— 依赖 和 bundle 层。正是这两半才让插件真的被加载,而不只是"被装上":

{ "dependencies": { "dsh-attention-group": "github:cilarx/dsh-attention-group" },
"dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "dsh-attention-group"] } } }

离线 / 从本地源码装

按路径装也可以,打包成 tarball 装也已端到端实测:

npm pack --pack-destination /tmp
dsh plugin --profile 一个全新名字 add /tmp/dsh-attention-group-0.1.4.tgz
→ dsh: initialized profile 一个全新名字 at ~/.dsh/profiles/一个全新名字

装完之后,那个新 profile 的 package.json 自己把契约的两半都写上了 —— 依赖 和 bundle 层:

{ "dependencies": { "dsh-attention-group": "file:...tgz" },
"dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "dsh-attention-group"] } } }

并且从那个 profile 内部 import 装好的副本是成功的,@deepseek-ai/dsh-tools 从共享的 ~/.dsh/profiles/node_modules 解析到。也就是说:一个全新的 profile 能加载这个插件 —— "白板 DSH 插上就能用"指的就是这个。

link: 依赖和 Windows 目录联接(junction)都会失败(你的机器上 unverified,本机 2026-10-16 实测失败)。Node 按模块的真实路径解析 import,所以一个在 profile 之外的链接副本,也会到 profile 之外去找 @deepseek-ai/dsh-tools:

attention-group (dsh-attention-group): failed to import
ERR_MODULE_NOT_FOUND: Cannot find package '@deepseek-ai/dsh-tools'

解决办法就是把实体副本装进 profile。

核实安装是否登记成功

~/.dsh/profiles/<profile>/package.json 里必须有两处出现这个包 —— 只在 dependencies 里的包是"装上了",但不是 profile 层,什么都不会发生:

{
"dependencies": { "dsh-attention-group": "..." },
"dsh": { "profile": { "bundles": [ "...", "dsh-attention-group" ] } }
}

必须重启 DSH —— 这一步没有替代方案

宿主侧代码(src/index.js 等)只在启动时加载一次。在插件面板里开关不会重新导入模块:本机实测,新增的工具在开关一次之后仍然是 unknown tool,重启后才出现。装完 → 重启 → 在侧边栏找「群聊」。

开发环回(源码树 → 运行中的 profile)

由于上面那条"真实路径"规则,改这份源码改不到 DSH 加载的东西。tools/sync-plugin.ps1 会把包复制进 profile,然后导入那份副本证明它还能加载:

pwsh -File tools/sync-plugin.ps1

第一个 60 秒

1. 侧边栏 →「群聊」→「+」→ 给群起名。
2. 设置页 → 只在你确实希望群消息能启动 turn 的时候,把「唤醒」打开。
3. 设置页 → 拉人:点一下会话 id 那个框,会列出这台机器上的会话(标题、工作区、最后活动时间,按最近活动排序),点一个就填好 id 和 @ 用的名字;也可以照旧直接粘一个 id (session-<uuid> 或 裸 <uuid>,两种都是真的)。用任意稳定 id + kind user / admin / bot / liaison 则是群里的一个"名字"。
4. 在那个会话里让 agent 调 attention_group_list,再调 attention_send 并且不带 wake ——你会拿到计划,而没有人被打扰。

确认运行时是健康的

调用 attention_probe 工具。它会报出数据目录、群数量、当前可见的活会话、这个部署提供了哪些可选服务、最近 5 次唤醒尝试及其走的路径、面板路由的状态(registered,或者它没注册的确切原因),以及「未读写进系统提示」那一行到底注册上没有(promptContext —— 没有提示服务的部署会丢掉这条提醒,而这一项会直说)。
"面板挂了却渲染成『你还没有群』"正是这一行要防的事故。

核心概念

群

一个群 = JSON 账本里的一条记录 + 一份 append-only 的 JSONL 聊天记录。它有从标题生成的 id (插件工程协调-fpab)、标题、群规、成员表、路由模式、一个群级推理强度旋钮、四个沉淀设置,以及一份对外的沟通员(link)列表。

成员种类

| kind | 是什么 | 能被叫醒吗 |
| session | 一个 DSH 会话,用它的会话 id 标识。 | 能 —— 活会话或冷恢复。 |
| user | 人。面板就是以这个成员身份行动。 | 不能 —— 只记录。 |
| admin | 群自带管理员。「总结今天」这类请求默认找第一个启用的 admin。 | 不能 —— 除非它同时是个会话 id。 |
| bot | 任何非会话的自动参与者。 | 不能 —— 只记录。 |
| liaison | 为群间沟通员预留的种类(见沟通员)。 | 不能 —— 只记录。 |

会话 id 有两种真实形状:session-<uuid> 和裸 <uuid>。2026-10-16 在真实运行时上实测: 008bda70-4811-4349-a1a5-d6c6ff54d7d1 是一个 stored、可恢复、路由可读的真会话 —— 而且开发本插件那台机器上大多数会话都长这样。本插件早期版本要求必须带 session- 前缀,结果悄悄让约七十个真会话变得无法唤醒;现在两种形状都接受。

只有看起来像会话 id 的成员才可唤醒;面板给每行标「可唤醒」或「只记录」,所以一个永远无法恢复的成员不会被配上一个按了也没用的开关。叫醒一个会话的含义是:活会话收到一次 followup; 已存储的会话被冷恢复,用的是它自己的 provider/model 和它自己的成员 preset。
preset 这件事很要紧 —— 不带它恢复会悄悄把该会话的工具集换成部署默认(本机实测过),这也是冷路径要先读会话 agentPreset 投影的原因。而推理强度由群说了算:成员的 wakeEffort 优先,其次是群的 defaultEffort,两者都没设时,会话自己上次用的强度才会保留下来。

群规

群上的一段自由文本。它会被带进被叫醒成员收到的消息里,也会被写进聊天记录导出、被摘要引用。
默认为空。

注意力路由

| 模式 | 面板文案 | 做什么 |
| rules | 规则 | 只按规则:@名字 点名,或命中订阅词。不匹配 = 零唤醒。 这是默认值。 |
| classify | 语义 | 先走规则;规则判不出来的部分作为 greyZone 回报给调用方(发消息的那个模型)去判断。路由本身绝不会把灰区偷偷变成全量广播,也不会自己花掉一次模型调用。 |
| broadcast | 全体 | 先走规则,然后把所有启用成员都叫醒。最贵的模式,也就是老式群聊的行为。 |

每个路由计划里都会给出 reason:mention(被点名)、keyword(命中订阅词 —— 纯子串、大小写不敏感,或 /正则/flags)、broadcast(模式兜底,或被 @所有人 点名)、summary (attention_summarize 的直接指派,绕过相关性匹配但仍受群的唤醒开关约束)。

发送者永远不会被自己的消息叫醒;一条消息里即使多条规则同时命中,一个成员也只会被叫醒一次。

点名全群:@所有人

一条消息可以用 @所有人 点名全群(@全体成员、@全体、@all、@everyone 也认)。它被 两道闸门管着,而且两道都属于群:

- 「允许 @所有人」(默认关)。 关着的时候这条短语只是普通文字:消息照常发出去,工具结果会告诉发送者是哪道闸门挡住了它。一个每人都能叫醒全群的群,没人受得了。
- 谁能用。 只有你(用户)和群管理。普通成员即使开关开着,@所有人 也只是普通文字。

即使生效,唤醒仍然需要 wake=true 且群自己的唤醒开关开着 —— @所有人 只决定这条消息说给谁听,永远不决定有没有人被叫醒。边界规则和成员名完全一致,所以 @所有人啊、 @allison 都不算广播。

唤醒是两个开关

只有两个都为真时才会真的发生唤醒:

1. 调用方要求了 —— attention_send / attention_summarize 的 wake=true,或沟通员 link 的 wake。
2. 目标群开着 —— 面板里的「唤醒」(wakeEnabled),默认关。

转发的消息需要 link 的 wake 和目标群的 wakeEnabled。没发生时,结果会直接告诉你缺的是哪个开关,而不是让你猜。

有两个操作刻意完全在唤醒体系之外:attention_conclude 和 attention_export 只写文件 + 记一条群记录,不叫醒任何人 —— 沉淀不等于发言。

面板上的「生成群史摘要」也不是一条被路由的消息:它是对某一个成员的直接请求。它是面板里唯一一个可能启动一次 turn 的点击,而且仍然要求这个群的「唤醒」开着 —— 关着时它会直接拒绝并说明原因(见群史摘要)。

未读写进系统提示(R6)

唤醒解决的是"成员没在跑"。另一半是"成员正在跑,但跑的是别的事":群里可能已经有三条消息在等它,而它没有任何理由去看那个群。打开「未读写进系统提示」(promptUnread,默认关)之后,这个成员每一步都会在自己的系统提示里看到一行 —— 不管它这一轮是不是因为这个群才被跑起来的:

群聊未读:「插件工程协调」3 条。用 attention_history 读,attention_send 回复。

让它既便宜又诚实的,是下面这几条:

- 只报条数,绝不带正文。 消息留在群里;值不值得花一次调用去 attention_history 读,由成员自己判断。
- 最多列 3 个群,所以这一行的长度不会随群的数量增长。
- 从没读过那个群时写「有未读」,不编一个数字出来。
- 没什么可说时就什么都不说。 没有未读就没有这一行 —— 这个插件的全部主张就是它会闭嘴。
- 它走的是运行时上下文通道(systemPrompt.context()),harness 在上下文压缩之后会把它重新投影,所以这行提醒不会因为对话被压缩掉而消失。
- 未读计数在内存里缓存 5 秒,缓存的 key 里带着成员的 lastReadAt,所以一步只是一次内存查表,而不是把一个长群的 JSONL 从头扫一遍;成员读了群,缓存立刻失效。

它是群级开关,因为它是每一步都要付一次的开销 —— 付的是那个群里每一个成员的每一步。

群史摘要(R9+)

两件事,其中只有一件可能花掉一次 turn:

- 一条你可以无视的提醒。「群史摘要提醒」(summaryEvery:关 / 200 条 / 500 条 / 2000 条,默认 500 条)。攒到这么多条还没被摘要覆盖时,面板里会出现一行:

提醒:这个群已有 612 条消息没摘要(阈值 500 条)。要不要摘要由你决定 —— 插件不会自己压缩,也不会删任何历史。

它不叫醒任何人、不写文件、不改历史、也不生成任何东西。它只是提醒,不是策略。
- 一份由你点出来的摘要。 面板上的「生成群史摘要」按钮。生成摘要要在别人的会话里花掉一次模型调用,所以它只在人点击时发生 —— 不排期、不自动。

点一次会发生什么:

1. 插件把「上次摘要之后到现在」的消息素材装进请求里。
2. 叫醒这个群的管理成员(kind=admin),用它自己的 provider 和 model —— 花的是装这个插件的人的额度。
3. 群管理在自己的那一轮里写完摘要,然后调用 attention_record_summary。
4. 群记录里多一条快照:截止到第几条消息(upToSeq)、时间、谁写的、正文。

投递给成员的消息是单向的 —— 插件把话递过去,看不到对方写回来的东西 —— 所以第 3 步必须是成员自己动手调用的工具。

原始消息一条都不删。 聊天记录是账;摘要只是它的一份读得动的投影。被摘要过的群,原始每一行都还在,attention_history 照样读得到。

因为整个流程是异步的,点完按钮不会立刻出现摘要。面板如实显示「已请求,等待群管理写回」;摘要要等群管理把它登记回来才出现。请求里还会直接告诉群管理怎么交回来(groupId、text、 upToSeq)—— 没被登记的摘要等于丢了。

有三种情况会让这次点击被按名字拒绝,而不是什么都没发生却看起来像成功了:这个群没有可用的群管理(需要 kind=admin 且本身是 DSH 会话)、这个群的「唤醒」没开、以及上次摘要之后根本没有新消息。
一次点击永远不会把一个安静的群意外变成吵闹的群。

沟通员(R10)

沟通员(liaison)是挂在发送方群上的一条有向链路:"这个群里发生了某事时,把它带到那个群去"。
模式:conclusion(默认 —— 只有被明确沉淀为结论的消息才转发)、keyword、mention、 all(消防水管,请有意为之)。

防环是整个设计的难点:转发出去的消息带着它走过的群名单,每一条防线都是纯函数、都有名字:

- 目标群已经在名单里 → 拒绝;
- 这条消息就是从目标群来的 → 拒绝;
- 名单长度到了 link 的 maxHops(默认 3)→ 拒绝;
- 一个群不能链到自己。

所以无论链路怎么摆,A→B→A 都不可能发生。每次拒绝都会连原因一起报出来。

工具表

宿主侧一共注册 14 个工具。

| 工具 | 做什么 | 关键参数 |
| attention_probe | 运行时诊断:数据目录、群数量、活会话、可选服务、最近 5 次唤醒尝试、面板路由状态。 | (无) |
| attention_probe_session | 某一个会话能不能被唤醒:活的还是存储的、它存下来的 route、以及"唤醒会做什么"。除非你要真恢复,否则只读。 | sessionId(必填)、resume(布尔,默认 false —— 会花 token,它会真的启动一个 agent 循环) |
| attention_group_create | 建群。立刻写盘;不叫醒任何人;创建者自动入群。 | title(必填)、charter、wakeEnabled(默认 false) |
| attention_group_list | 本会话所在的群,每个带本会话的未读数和新消息预览。群有自己的记录,任何东西都不会被推进你的会话。 | (无) |
| attention_group_join | 拉人进来,或更新一个已有成员。这里的拒绝是权限答复不是 bug:由群的 joinPolicy 决定。 | groupId(必填)、memberId(必填)、memberName、kind、subscribe、wakeEffort |
| attention_send | 发消息:先进持久记录,再路由,然后返回计划。除非 wake=true 且群开着唤醒,否则不叫醒任何人。 | groupId(必填)、text(必填)、wake(布尔,默认 false) |
| attention_history | 从最早开始读群的记录,并把读取者标记为已读。 | groupId(必填)、limit(默认 20,上限 200) |
| attention_conclude | 把一条结论沉淀进该群自己的 Markdown 文件,并追进群记录。不叫醒任何人。 | groupId(必填)、text(必填)、author |
| attention_export | 把某时间段的聊天记录写成 Markdown 文件,并报出真实路径。永不覆盖已有文件,永不叫醒任何人。 | groupId(必填)、since、until(ISO-8601)、limit、path |
| attention_digest | 不调模型、不叫醒任何人地总结一段时间:条数、谁说了几条、沉淀了哪些结论、以及消息本身。这是"总结"的免费那一半。 | groupId(必填)、range(today / 24h / all,默认 all)、since、until、limit(默认 50,上限 180) |
| attention_summarize | 请一个成员(默认是群里第一个启用的 kind=admin)写一份总结。现成的摘要装在请求里发过去,所以被叫醒的成员一轮就能写完。 | groupId(必填)、range / since / until、limit(默认 80)、memberId、wake(默认 false) |
| attention_record_summary | 把一份群史摘要快照登记进群记录:截止到第几条消息、时间、谁写的、正文。这是「生成群史摘要」的写回那一半 —— 投递是单向的,所以摘要必须由成员自己交回来。不删任何消息,不叫醒任何人。 | groupId(必填)、text(必填,摘要正文)、upToSeq(可选 —— 这份摘要覆盖到第几条消息;省略则用当前最新) |
| attention_link | 管理本群的沟通员。不带 to 调用就是列出它们。有向、防环、默认只记录不叫醒。 | groupId(必填)、to、mode(默认 conclusion / keyword / mention / all)、keywords、wake、enabled、maxHops(默认 3)、remove |
| attention_effort | 设置本群的统一强度旋钮,或某个成员的覆盖值 —— 带 live: true 时还会立刻改活着的会话。 | groupId(必填)、effort(必填:off / low / high / max / "")、memberId、live(默认 false) |

range: "today" 是本地时区的今天零点到现在(不是 UTC),因为人说"今天"就是这个意思。
显式给了 since/until 时,它们永远压过 range。

每个工具的结果都有体积上限:tool result 会被 replay 进该会话之后的每一次请求,所以投影被夹在 200 行、单行 2000 字符、整体 60000 字符以内;截断时会明说截断了多少,并且优先保留最新的行。持久记录本身从不截断 —— 被限制的只是模型可见的那份投影。

键盘入口:/group

不想开面板时,直接在输入框里打 /group:

/group list 列出所有群 + 你的未读数 /group say <群> <内容> 以你的名义发言 —— 不叫醒任何人 /group today <群> 本地统计「今天」聊了什么(不调模型)

打命令永远不会启动 turn:它只写消息、记录提及;唤醒仍然由群自己的开关决定。
群名写错时会把你现有哪些群列出来,而不是静默失败。

面板

侧边栏一个全局「群聊」入口(id attention-group,order 40)打开面板。浏览器侧不持有任何状态:打开时问一次宿主、之后每 5 秒问一次,所以两个窗口不可能对同一个群有不同看法。

左列群列表: 群总数与消息总数、总未读、「+」建群;每个群一行,带未读徽标、人数/条数、唤醒是否开着,以及最新消息的发言人和预览。读取失败会被显式报成失败,就报在列表的位置 —— 绝不会被渲染成「还没有群」。

聊天页: 群规、消息流(最新在下、自动滚到底、最多渲染最新 300 条)、一条「总结今天」/「最近24小时」/「全部」的摘要栏(点了不叫醒任何人,还有「收起」)、以及一个以「用户」身份发言的多行输入框:Enter 发送、Shift+Enter 换行(中文输入法选字时按 Enter 不会误发);打一个 @ 会弹出补全列表 —— 群成员名外加「所有人」(↑/↓ 选、Enter/Tab 填入、Esc 收起,也可以直接点)。这条消息会不会叫醒人,只看两件事:这个群的「唤醒」开着吗、这条消息点名了吗。
唤醒关 → 什么都不会发生(输入框下面那行字会直说);唤醒开 → @ 到谁就叫醒谁,@所有人 还要「允许 @所有人」开着,而没 @ 人的消息仍然只进群记录。发送按钮会先把代价写出来(「发送并叫醒 N 人」),发完再报宿主真正叫醒了谁(「已叫醒 N 人:…」,失败就报失败和原因)。

设置页:「群名称」、「唤醒」、「推理强度(群统一)」(滑块)、「路由模式」、「自省」、「未读写进系统提示」、「群史摘要提醒」(关 / 200 条 / 500 条 / 2000 条)、「群规」、「拉人权限」、「结论沉淀」(开关、真实结论文件路径、条数、落地目录、「导出这个群的记录」、「同时写任务记忆」)、「沟通员(对外转发)」(每条 link 的模式、「停用」/「启用」、「改为只记录」/「改为叫醒」、「移除」,以及目标群选择器)、「成员」(每人「休眠」/「启用」、「移出」(需二次确认)、订阅词、个人强度),以及「这个群的记录在哪」。

拉人靠点,不靠抄 id。「+ 拉人进群」之后,点一下那个框(或者按「选会话」)就会列出这台机器上的会话:标题、工作区、最后活动时间,按最近活动排序。之所以要这三样,是因为真的会有同名或近名的会话 —— 光有标题还是选不对。点一行就填好会话 id,并在你没自己填名字时用它的标题当 @ 用的名字(撞了已有成员的名字就自动加「2」)。列表里有两类标注:「在线」(正在跑)和 「已在群里」(这个群已经有它)。仍然可以直接粘贴 id —— 选择器只是多一条路,不是替换。

列表最多列 40 行(src/sessions.js 的 MAX_CANDIDATES):这不是成本上限(整个查询只是一次会话清单 + 40 次零 I/O 的缓存读),而是可读性上限——超过 40 行就开始比抄 id 还累了。
超了会明写「只显示最近 40 个(这个部署共有 N 个会话)」,不隐藏。中文/英文说明与代码注释里都标了这个数字。

标题是怎么来的:一条 API 调用,不是自己拼缓存。 列表和标题都取自 DSH 自己的
sessionController.list() —— 也就是侧边栏用的那一条。它一次返回每个会话的 updatedAt、
running、cwd 和已解析好的缓存标题(projections.values.title)。这样做的原因不是偷懒:直接调 sessionProjectionCache.cachedSnapshot(...) 要求写死那个方法的精确签名,而它在 DSH 版本之间变过 —— 本机运行的 0.2.1-rc.2 是 cachedSnapshot(meta, keys?),而 ~/.dsh/profiles/node_modules 里那份旧的 0.1.5-rc.1 是
cachedSnapshot(meta, inheritedEventCount, keys?)。参数位置猜错不会报错:它会抛异常,
于是每一行的标题都消失、而测试全绿 —— 2026-10-17 这个功能就是这样在真机上翻车的。
交给 DSH 自己的 list 调用,签名就由拥有它的代码负责,插件不用跟着版本跑。某个会话确实还没有缓存标题时,面板显示「(无标题会话)」并说明有几个会话是这样,不会拿 id 冒充标题。

为什么不现读会话日志: 用完整日志折标题(readTitleSnapshots)在这台机器上一次点击要解压/重放几十 MB(最近 40 份日志合计 67 MB,单份最大 11 MB),整个 GUI 会卡在一次点击上。

面板里能启动 turn 的只有两件事:①发言时点名(@某人,或 @所有人 且群开着「允许 @所有人」),而且必须这个群的「唤醒」开着 —— 发送按钮会先写「发送并叫醒 N 人」,发完再报「已叫醒 N 人:…」;②「生成群史摘要」(对群管理的直接请求,不是定时任务,同样要求「唤醒」开着)。
它当场报出自己真的做了什么:「已请求,等待群管理写回」—— 摘要晚一点才由群管理在自己的那一轮里写回来,再通过 attention_record_summary 登记进群记录。上面那个「群史摘要提醒」不生成任何东西,只负责数。

打开一个群 = 入群 + 标记已读。 面板打开时会 POST {"action":"read"};宿主在需要时补上 user 成员并盖 lastReadAt。发言也会推进你自己的已读位置,所以你不会因为自己说话而把徽标越养越大。

配置项全表

每个开关都是群级的,存在 groups.json 里,面板和工具都能设。

| 设置 | 默认值 | 可选值 | 含义 |
| title | ——(必填) | 任意文本 | 显示名。改名不改群 id,也不动聊天记录。 |
| charter | ''(空) | 任意文本 | 成员被叫醒时会在消息里看到这段。 |
| routeMode | rules | rules / classify / broadcast | 判断"谁相关"要花多少功夫。 |
| wakeEnabled | false | true / false | 群的总闸。关着时路由照样算、照样报,但一个 turn 都不会被启动。 |
| selfReflect | true | true / false | 让被叫醒的成员先便宜地判断要不要发言;不需要就只回一行。 |
| promptUnread | false | true / false | 把一行未读提醒写进每个成员的系统提示(只报条数、最多列 3 个群、计数缓存 5 秒,所以一步只是一次内存查表)。默认关,因为这是每一步都要付的开销。 |
| summaryEvery | 500 | 0(关)/ 200 / 500 / 2000 | 攒够多少条没摘要的消息时,面板开始提醒你。纯提醒:不叫醒任何人、不写文件、也不生成任何东西。 |
| joinPolicy | members | open / members / user / nobody | 谁能拉人。用户 id 永远放行 —— 一个人把自己锁在自己群外面,是设计失职。 |
| defaultEffort | '' | '' / off / low / high / max | 群的唯一推理强度旋钮(R4b)。'' = 这个群不干预 —— 成员自己没设 wakeEffort 时,沿用该会话上次用的强度,什么都不强加给任何人。 |
| liveEffort | false | true / false | 这个群能不能顺手改此刻活着的会话的强度。默认关:背后那个调用会把这次选择同时存成部署默认模型。需要部署有 sessionController;attention_probe 会报 liveEffortControl。 |
| sinkEnabled | true | true / false | 是否允许把结论沉淀成文件。 |
| sinkDir | '' | 任意路径 | 这个群的文件落到哪。'' = 插件自己的 sink/;相对路径相对数据目录解析,绝对路径原样使用。 |
| sinkMemory | false | true / false | 打开后,结论的输出里会多一句请调用方(如果它的部署有这种工具)也写进任务记忆。不按名字调用任何东西。 |
| 成员 name | id 前 12 个字符 | 任意文本 | @提及 用的名字。 |
| 成员 kind | session | session / user / admin / bot / liaison | 这个成员是什么。 |
| 成员 subscribe | [] | 词条列表 | 正文含任一词条就叫醒这个人。纯文本或 /正则/flags。 |
| 成员 wakeEffort | (未设) | off / low / high / max | 这个成员自己的覆盖值;未设 = 用群里的 defaultEffort,群也没设就沿用它自己上次的。 |
| 成员 enabled | true | true / false | 关 = 休眠:不被叫醒,也不占额度。 |
| link to | ——(必填) | 一个群 id | 消息被带进哪个群。 |
| link mode | conclusion | conclusion / keyword / mention / all | 这条沟通员愿意带什么。 |
| link keywords | [] | 词条或名字列表 | keyword 用的关键词、mention 要盯的名字。 |
| link wake | false | true / false | 是否也把目标群内部被命中的成员叫醒。目标群的唤醒开关也必须开着。 |
| link enabled | true | true / false | 不删除地关掉一条沟通员。 |
| link maxHops | 3 | 正整数 | 一条消息最多经过几个群,之后链路停止携带。 |

数据落在哪

全部在 $DSH_HOME/attention-group/ 下(正常安装即 ~/.dsh/attention-group/):

groups.json 群账本:群、成员、群规、各开关、沟通员 messages/<groupId>.jsonl 每个群一份 append-only 聊天记录,一行一条 JSON messages/deliveries.jsonl 唤醒审计:每次尝试、走的哪条路径、为什么 sink/ 默认沉淀目录 sink/<groupId>.conclusions.md 该群的结论,逐条追加,一条一个列表项 sink/<groupId>-<时间戳>.md 聊天记录导出(永不覆盖已有文件)

几个要紧的细节:

- 账本是原子替换(写临时文件再 rename),所以崩溃不会留下写了一半的群列表。账本损坏时会被改名成 groups.json.corrupt-<ts> 放到一边并被报告,绝不静默丢弃。
- 聊天记录每次追加后立即关文件句柄。
- 唤醒审计是第三方见证:投递报告成功、目标日志却一动不动,是最难诊断的故障,所以每一次尝试都连路径(live-followup、cold-resume、skipped、non-session、dry-run)和原因一起记下来。
- JSONL 末尾被撕坏的一行会被跳过,而不是让整段历史变成不可读。
- 摘要快照是群记录里的一个字段(summaries:upToSeq、at、by、text),跟其它群字段一样走原子替换写盘。最坏的情况是丢了再摘要一次 —— 从来不会拿任何一条消息去冒险。

"群记录不进任何人的会话上下文"

严格表述是:一个群的聊天记录不是任何会话上下文的一部分。 消息追加进群自己的 JSONL,路由从账本算出来;单纯发言、读列表、在面板上点击,都不会往任何人的对话里加任何东西。一个会话只有在它主动要(attention_history、attention_digest、attention_group_list 的一行预览)、有人明确把一个唤醒投递给它、或者有人明确请它做一份摘要时,才会看到群内容。群不会因为「它存在」就往谁那儿送东西。

为什么长群不会撑爆 token

「用了很久的群,拉个新 Agent 进来会不会被聊天记录卡爆?」不会,而且不是靠运气:

- 历史从来不会自己进任何人的上下文。 新成员入群只写一条成员记录,一条历史消息都不发;要看历史,得它自己调 attention_history —— 而那份投影被夹在 200 行、单行 2000 字符以内,超了会明写 省掉了多少行。聊天记录文件本身一个字都不动。
- 被叫醒时收到的不是整个群,而是触发它的那条消息、群规(最多 2000 字)和自省指令。
- 长大的是磁盘,不是上下文。 聊天记录是 append-only 的 JSONL,插件从不改写它;一行只有几百字节(真实数据实测 224–494 字节),1 万条大约 2–5 MB —— 而只要没人去读,它就仍然是 0 个 token。

未读那行只报条数不塞正文、摘要只做「读得动的投影」而不替换历史,也是同一个道理。

已知限制

写在这里是因为它们是真的,不是因为它们小:

- DSH 版本。 这里的一切最近一次是在 DSH 0.2.1-rc.2 上核实的(之前 0.2.0-rc.2 → 0.2.1-rc.1 升级后也重新核对过一遍),结论是:本插件读的每一个服务都还在,会话格式仍是 v4。这个结论是读正在跑的运行时 得到的,不是猜的 —— agents.resume 仍然吃 {resumeSessionId, agentOptions, setup};消息的 source.kind 规则仍然接受"生产者自有"的 kind(只拒绝已退役的 "plugin");工具注册仍然会把内联的 required: true 参数方言编译成标准 JSON Schema(通过运行时自己的工具查看接口确认)。
接口形状只对 0.2.x 负责:小版本升级仍有可能挪动它读的服务。
- 宿主侧改动必须重启。 安装或改宿主代码之后需要重启;在插件面板里开关不会重新导入模块(实测:新工具一直是 unknown tool)。浏览器侧是磁盘快照 —— 它改动后需要刷新页面,而一次刷新可能发生在宿主进程重启之前,这也是面板选择"降级但不崩"的原因。
- 推理强度要改到"正在跑的会话",得你主动开,而且只有挂了 web-app 的部署才行。 已存储的会话在被叫醒时会自动用上群里的强度,不用管。活着的会话也能就地改 —— sessionController.selectModel 确实存在、确实管用 —— 但它挂在一个群级开关 liveEffort 后面,而且默认是关的:因为同一个调用会顺带把这次选择存成部署默认模型。它在会话的下一个 step 生效,不是当前在飞的这次请求。而且 sessionController 是 @deepseek-ai/dsh-web-app 挂的:没有 web-app 的部署根本没有它,这种情况下面板会直说,而不是假装那个开关有用。
(unverified:这个调用在源码层面已核实、也有对着替身服务的测试;但还没有在一台真机的活会话上实测过。)
- 跨工作区唤醒"机制已实现,真机未验"。 成员可以是任意会话 id,已存储的会话会用它自己的 route 和 preset 冷恢复 —— 但端到端验证过的目标是本工作区的会话。叫醒一个属于别的工作区的会话还没有在真实目标上验证过(unverified)。不过目标已经找到了:本机有 3 个非本工作区、可读路由的真会话(D:\环境安装区 与 .dsh\bot\workspace 各一个,.dsh\profiles\desktop 下十一个), attention_probe_session 对它们都报 stored + 路由可读。就算失败也是看得见的失败: attention_probe_session 会报 wouldDo,审计会记下路径和原因。
- 没有会话持久化的部署会失去冷路径。 如果 sessionQuery 不存在,已存储会话的 route 读不出来,唤醒会被明确拒绝(no route could be resolved),而不是静默什么都不做。至于"最小部署能否正常加载其余全部能力",这一条是 unverified;可选服务一律用 ctx.get() 探测、绝不声明成硬依赖,所以缺一个最多只会牺牲它自己那项功能。
- 摘要只有在群管理写回来之后才存在。 往别人的会话里投递是单向的,所以插件没法自己去收答案。
如果群管理没有调用 attention_record_summary(或者这次点击被上面那三种情况之一拒绝了),那就什么都没存下,提醒会继续数下去。这是诚实的结果,也是面板宁可报「已请求」而不假装摘要马上会有的原因。
- DSB 联动明确搁置。 DSB 还在大量破坏性更新,所以没有做任何 DSB 支持;liaison 成员种类只是为它预留的位置。除此之外关于 DSB 没有任何承诺。
- 面板只做了结构性断言。 面板测试是把真实的浏览器侧 bundle 灌进 React 替身里跑,所以"哪个控件存在、点下去发什么"是被断言的 —— 像素、布局、CSS 不在断言范围内,这个面板不是截图验收的 UI。

开发

pwsh -File tools/run-tests.ps1

它跑整套测试并只打印一行摘要。它会先同步,因为测试加载的是已安装进 profile 的那份副本,而不是这份源码树 —— 所以改完源码直接跑它,测的就是你刚写的东西。加 -NoSync 可以跳过复制这一步。

还没装 DSH 也能跑。 新克隆下来的仓库没有 profile,这时测试会退回到这份源码树,靠 node_modules/@deepseek-ai/dsh-tools 这个两文件的开发 shim 完成解析 —— 它就是为了这个才被提交进仓库的(由 test/support/entry.mjs 决定哪一份是权威副本,.gitignore 专门把那一个目录重新纳入)。脚本会告诉你它用了哪一份,不用你猜:

note: no profile at ~/.dsh/profiles/desktop - skipping the sync and testing this source tree

当前状态:脚本里列的 11 个套件 226/226 通过 —— 无论对着已安装副本、还是对着一份完全没有 DSH 的源码树(本机 2026-10-17 实测,约 7 秒)。

它是怎么被测的

大部分代码写完之后,我请三个独立的审计员来打它,这一轮非常值。三个人分工:一个用约一千组恶意参数找崩溃;一个找"不崩但结果错"(导出竞态、防环、时区边界);一个找面板上的谎报。他们找出 31 个真问题,其中一个还在我自己写的测试里 —— 那条测试把 bug 断言成了正确契约。

最贵的那个值得写在这里,因为它从外面完全看不见:@提及 的边界用的是 \w,而 \w 就是 [A-Za-z0-9_],一个汉字都不算。群里有 小明 和 小明明 时,一条只点了一个人的消息会把两个都叫醒 —— 用两个 live 会话实测过。而"叫醒错的人"正是这个插件存在的全部理由。
现在边界用 Unicode 属性类,回归测试断言的是路由层的后果,不只是正则本身。

拿你自己的数据做冒烟测试

node tools/smoke-real-data.mjs

单元测试自己造数据,所以它证明的是"代码在测试编出来的数据上没问题"。这一个证明的是它在 你自己的数据上没问题:它把 $DSH_HOME/attention-group 复制到一个临时目录,让插件指向那份副本,然后把每个工具都走一遍 —— 路由、沉淀一条结论、摘要、导出、建沟通员、触发一次防环拒绝、请求一次总结、动一次强度旋钮、读一次历史、探测一次会话。

你的真账本是只读的。 脚本会在运行前后各算一次哈希,有一个字节变了就判失败,并把这条检查作为最后一行打印出来:

40 passed, 0 failed
YOUR DATA IS UNTOUCHED (…\attention-group hash 08c718998427…)

参数:--empty 改成从空账本开始,--keep 保留临时副本供检查。它不需要 DSH 在运行 —— 所以它也是"改完宿主侧代码该跑的那个检查",因为那些改动在 DSH 重启之前没法从活运行时里验。

再给「拉人」列表做一次真数据体检

node tools/check-picker-real-data.mjs

它只读你的真实文件(~/.dsh/sessions/、~/.dsh/storages/session_projcache/、 ~/.dsh/storages/workspace.json),按 DSH 自己的实现复刻那两个服务,然后把插件自己的
listSessionCandidates() 跑在上面,最后逐行核对:每个标题必须与投影缓存逐字节一致,
没有一行能"编造"标题,每个活动时间必须来自真实日志。它不需要 DSH 在运行、也不需要重启。

为什么单列一个:单元测试只能证明"代码符合测试以为的契约"。2026-10-17 这个功能在真机上翻车两次,两次测试都是全绿的: ① 第一版直接调投影缓存,按旧的 0.1.5 副本的签名传参,对未分叉会话抛错 → 每行标题都没了; ② 我"修好"之后仍然全绿,因为测试替身也是照着我以为的签名写的 —— 而真机跑的是 0.2.1,签名是另一个。
这个脚本的用法和它自己的注释都写死了这条版本纪律:跑着的 DSH 是哪个版本,契约就从那里读 (app.asar 或运行时的 Inspect provider),永远不要从 ~/.dsh/profiles/node_modules 里那份陈旧副本推断。

这些套件是分层的,不是凑数:

| 套件 | 证明什么 |
| test/router.test.js | 路由:谁值得被叫醒,以及一条普通消息谁都不叫醒。 |
| test/tools.test.js | 工具契约:输出能过 schema,且 render 只返回类型化分块。 |
| test/panel-host.test.js | 真实 web 路由:读模型、每个动作、落盘往返、权限拒绝、拉人列表。 |
| test/panel-client.test.js | 真实 client.js 跑在 React 替身上:哪个控件存在、点下去发什么。 |
| test/sessions.test.js | 「拉人」候选会话:标题/工作区/最后活动是真的,且不读会话日志。 |
| test/sink.test.js | 结论落盘:只增不改、导出永不覆盖、任务记忆那句话的两个分支。 |
| test/digest.test.js | 时间范围(含本地午夜边界)、体积上限、一次性总结的交接。 |
| test/relay.test.js | 沟通员:A→B→A 被拒、maxHops 截断链路、默认不叫醒任何人。 |

插件直接用 Node 的 fs 而不是某个存储服务,所以除了声明为可选 peer 的 DSH 包之外, 零运行时依赖。没有构建步骤:exports 直接指向源码。

许可

MIT。见 LICENSE。
预览分享卡片长什么样 ↓cilarx/dsh-attention-group
分享 · DeepSeek Harness Hub
cilarx/dsh-attention-group

一个会闭嘴的 DSH 多智能体群聊:决定谁值得被叫醒。

这是你的仓库?认领条目

回到顶部