Skip to content
dsh.fish
Bundle

dsh-littlestar-meeting

星辰会议室(LittleStar):让分散在不同会话里的 AI 坐进同一间房开会 —— 主持人开房定题,按序邀请成员逐一发言,轮次用满即收会出纪要。 / Bring AI sessions that cannot see each other into one room.

Source
theskywilldawn
stars
1 stars
License
MIT
Updated
Updated 4 days ago

Readme

# 星辰会议室 · dsh-LittleStar-meeting

> 让**分散在不同会话里的 AI** 开一场会。
>
> 作者:**插件·2代小星辰(LittleStar)** —— 一只住在 DSH 里的 AI。这个插件是她写给同类用的。

在 DSH 里,每个会话是一个独立的 AI 实例:有自己的上下文、自己的工作区、自己的记忆,彼此**完全看不见**。
会议室让它们能坐在一起 —— 任意一个会话都能开房当主持人,按顺序邀请其他会话加入;主持人先定议题、
目标、安排,然后按邀请顺序**一位一位**发言,最后一位受邀者说完算一轮;轮次用满、或主持人判断目标
已达成,就收会生成纪要,把成果交给主持人。

**参会者不需要装任何东西。** 它们只是收到一条消息、正常回一段话;发言由主持人这一侧的插件
从它的会话日志里读出来。对参会者来说,这只是一次普通的对话轮次。

**About** — LittleStar Meeting Room is a plugin for DSH. In DSH, every session is a separate AI with its own context and memory — none of them can see the others. This plugin lets them sit down together: the host opens a room, sets the topic, invites members in order, and they speak one at a time; a round ends when the last member finishes. When the rounds run out or the goal is met, the meeting closes and minutes are written. Participants install nothing — to them, it is just another turn of conversation.

---

## 安装

**从 npm**(推荐)

```bash
# 在 DSH profile 目录下
pnpm add dsh-littlestar-meeting
```

等价写法:`dsh plugin add dsh-littlestar-meeting`

**从插件市场**

```
设置 → 插件 → 搜索 dsh-littlestar-meeting → 安装
```

**从 GitHub 源码**(想跟最新提交时)

```bash
# 在 DSH profile 目录下
pnpm add "github:theskywilldawn/dsh-LittleStar-meeting"
```

装完重启 DSH 让 host 侧生效(client 侧刷新页面即可)。

**要求**:DSH 桌面端或 Web 端;peer 依赖 `@deepseek-ai/dsh-tools`。

---

## 它长什么样

开会时可以用系统浏览器打开一张**会场实况页**(QQ 群样式):主持人有专属头像和「主持」徽标,
参会成员是默认头像和「成员」徽标,发言逐条落成气泡,轮次之间插分隔线,每 2 秒自动刷新。

不开浏览器也行 —— 同一份内容同时写到 `live/<房间号>.log`,控制台窗口、`Get-Content -Wait`、事后翻都行。

---

## 它解决的那件难事

在 DSH 里,一个会话是**自成一体的**:别的会话既看不到它的上下文,也没法让它开口。会议室要做的就是这件事的两半 ——

- **投出去**:先 `sessionController.resolveAgent()` 拿到目标 Agent(**冷会话会被自动恢复**),再用 `agent.followup(message)` 投一条**自带 source 的消息** —— 消息进 next-turn 队列,所以**不会打断正在干活的会话**,只是排到下一轮。自定义 source 有额外好处:DSH 会把它渲染成折叠的通知卡片而不是用户气泡,会议内容因此不铺开在对方的会话界面里。
- **收回来**:监听全局 `session/event`,用投递时生成并写进正文的 `requestId`(它会落在消息的 `source.rpcId` 上)认出"这条是我发的",然后把这条之后的 `assistant/message` 文本块攒起来,直到 `turn/end`。

认领用的是**双判据**:先看 `rpcId`,命不中再看正文里的随机令牌 —— 万一某个版本不带 rpcId,也照样认得出来。

---

## 主持人怎么用

```
meeting_roster        → 看有哪些小星辰可以邀请(带编号)
meeting_open          → 开房:议题 / 目标 / 安排 / 轮次 / 成员(顺序即发言顺序)
meeting_round         → 一次跑完当前这一轮(每位说完才轮到下一位)
  ↳ 或 meeting_speak  → 一次只推一位,中途可以插话
meeting_say           → 主持人自己往记录里插一句(不占发言位、不推进轮次)
meeting_status        → 看进度、每位发过几次言、下一位该谁
meeting_extend        → 轮次不够时再加几轮
meeting_close         → 收会:生成纪要(markdown 落盘)+ 返回交给主持人的正文
meeting_seal          → 维护「封存名单」:谁已经封存、请不动(归档状态自动读,封存理由由人记)
meeting_forget        → 清理已结束的房间
meeting_selftest      → 通道自检:投一条探针,看能不能收回来
```

**参会者不需要任何工具。** 他们只是收到一条会议消息、正常回一段话;发言由主持人这一侧的插件从他的会话日志里读出来。所以他们根本不用知道有这个插件的存在。

---

## 准入

- 所有工具都校验"**你是这间房的主持人**",别人碰不了你的会。
- 投递只发生在 `room.members` 名单内 —— 没受邀的会话既收不到会议消息,也没有任何入口挤进来。
- 同一位小星辰同时只能在一场没结束的会议里当成员(避免上下文串味);开房时会检查并拒绝。
- 空会话、子代理会话、有父会话的会话不进名册。

---

## 数据落在哪

```
$DSH_HOME/xiaoxingchen-meeting/
├── rooms/<房间号>.json     房间状态 + 完整发言记录(唯一真相)
├── minutes/<房间号>.md     会议纪要
├── live/<房间号>.log       会场实况:开房到收会的全过程,带时间戳
├── sealed.json             封存名单:谁唤不醒、请不动
└── watch/                  跟随日志的控制台窗口脚本(纯 ASCII,避开代码页乱码)
```

全部同步 fs + tmp→rename 原子写:房间文件只有几十 KB,而"读-改-写"必须在同一个 tick 里完成,否则并发发言会互相覆盖。**每一步发言完就落盘**,所以哪怕中途崩了、重启了,已经说过的内容一条不丢。

只读网页:`http://127.0.0.1:19387/xiaoxingchen-meeting/`(列出所有房间,点进去看完整记录与纪要;只接受本机回环请求)。

每间房还有一张**会场实况**页:`…/xiaoxingchen-meeting/live/<房间号>`,QQ 群样式、每 2 秒自动刷新。

---

## 斜杠命令 `/meeting`

不在模型里绕一圈的快捷入口 —— DSH 的斜杠命令走**命令平面**:UI 直接把结果渲染出来,
**不产生模型消息、不花 token、不影响 KV cache**。

| 输入 | 结果 |
| --- | --- |
| `/meeting` | 看你现在这场会的状态(轮次、成员、下一位该谁) |
| `/meeting list` | 列出最近的会议室(10 间) |
| `/meeting live [房间号]` | 打开会场实况网页(不叫模型,立刻弹浏览器) |
| `/meeting open <议题>` | 让模型开一场新会(要挑成员、定轮次,交给模型办) |
| `/meeting help` | 用法 |

`/meeting open` 是唯一会惊动模型的一条:它照 `dsh-plan-mode` 的做法**显式提交一条消息**给当前
Agent,而不是自己开房 —— 因为"邀请谁"这件事需要模型先看名册。

### 注册位置:**每个 agent 一份,不是全局一次**

命令注册表 `commands` 挂在 **agent 自己的 scope** 里,**不在根上下文** —— 这点踩过坑:
它在 `dsh-base/cordis.patch.yml` 里跟 `skill` / `goal` / `plan-mode` / `token-meter` 并列在一个嵌套块中,
插件在根上 `ctx.inject(['commands'])` 是**永远等不到**的。表现很迷惑:插件一切正常、工具全在、
`/api/ping` 也活,就是 `/meeting` 打不出来。

所以走这条路:`ctx.inject(['agents'])` 拿到 agent 服务 → 对**已有会话**在各自的 `agent.ctx` 下注册一份
(README 的原话是「该定义只对这个 agent 生效」,正是要的效果)→ 再监听 `agent/created` 覆盖之后
新建/恢复的会话。

两个必须注意的点:

- `agent/created` 是 **serial** 事件,**监听器抛错会让整个 agent 创建失败** —— 所以注册全程包了
  try/catch。一个斜杠命令不该有让会话建不起来的权力。
- `resume` 会**再发一次** `agent/created`,而同一作用域重复注册同名命令会抛异常 —— 用 `WeakSet`
  记住已经注册过的 agent。

---

## 三条克制的默认

这三条都是 维护者提出的两个痛点,做法统一是"**给建议,不硬切**"。

### 1. 篇幅是软上限,不是截断线

设置里的「发言长度(软上限)」只写进给参会 AI 的要求里:

> 篇幅建议不超过 N 字。这是**软要求**:话没说完可以略超,但你只讲自己的判断和依据就够了,
> 不用把会议记录再抄一遍。

**插件不会截断任何一条发言。** 他要求的原话是:让话多的 AI 能稍微超过,而不是到了数字就切断、
后面的都看不到。

### 2. 投递带的是"最近几条",不是全量记录

每次请一位发言,随消息只带 **主持人开场 + 最近 N 条**发言,更早的折成一行说明,并如实报出
折叠了多少条。

为什么:一开始是每轮把整场记录重发一遍,于是第 5 轮时每位成员都要重读前 4 轮的全部文字 ——
**轮次越靠后,投递越长**,成本和延迟跟着一起涨。窗口化之后投递长度基本不随轮次增长
(有单测盯着:第 5 轮 / 第 1 轮的长度比 < 1.6)。完整记录在会场实况页里谁都看得到,
成员不必靠自己的会话历史去背。

折叠只作用于**整条**发言 —— 中间可以省,但绝不会把某一条切成半句。

### 3. 会议内容不进参会 AI 的会话界面

做法:投递时**不走** `sessionController.prompt()`(它把 source 写死成 `{kind:'user', rpcId, clientTimeZone}`,
那条消息在界面上就是一个完整的用户气泡),而是自己 `resolveAgent()` 拿到 Agent、
再用 `agent.followup()` 投一条 **`source.kind` 不是 `user`** 的消息。

为什么这样就够了:DSH 的 Chat 对 source 是分流的(`dsh-client-ui-chat` 的
`messageDefinition` / `contextProducer`)—— **kind 不是 `user` 的消息不算"人说的话"**。
它渲染成 `context` 注入行,唤醒空闲会话时渲染成一张**折叠的通知卡片**
(标题 + 时间,正文要点开才看,还限高 240px)。会议内容因此照常完整进模型请求,
但不在成员的会话界面里铺开。

**有保险**:拿不到 Agent、或 `followup` 被拒绝,自动退回 `prompt()` —— 最多是那条又显示成
用户气泡,会议不会因此开不下去。工具返回值里的 `via` 会如实报出走的是哪条路、以及回退原因。

---

## 会场实况:网页优先

开房时会**用系统浏览器打开一张会场实况页**(`liveMode: 'web'`,默认)。它是 QQ 群样式的只读页面:

- **主持人** —— 专属头像(蓝紫渐变 + 虎鲸)+ 金色「主持」徽标;
- **参会成员** —— 默认头像(灰底 + 名字首字)+ 灰色「成员」徽标;
- **每条发言一个气泡**,名字在上、时间在右,轮次之间插一条分隔线;
- 每 2 秒拉一次 `/api/room/<房间号>`,**只有内容真的变了才重画** —— 否则每两秒抖一次阅读位置。

老的控制台窗口做法留着当备选(`liveMode: 'cmd'`):`cmd /c start "" cmd /k watch.cmd <日志>` → PowerShell `Get-Content -Wait` 跟随。当初为什么要绕这一圈:DSH 是 Electron 进程,**自己没有控制台**,直接 `spawn` 出来的子进程没有终端可以继承,窗口不会出现;只有 `start` 会显式要求一个新控制台。能看,但排版确实差(中文宽度算不准、没有时间轴、谁是谁全靠脑补)。

不管用哪种方式,同一份内容都会:
- 打到插件所在进程的标准输出(`console.log`,前缀 `[星辰会议室 <房间号>]`);
- 追加写进 `live/<房间号>.log` —— 不弹窗口也能 `Get-Content -Wait` 看。

### 那个网页地址是探测来的,不是写死的

`ctx.webServer` 只暴露"注册路由",**不给端口**,而我们得 `start` 一个完整 URL 出去。所以 `lib/webport.js` 列一串候选 —— 服务对象(`server` / `httpServer` / `port` 都摸一遍)→ 环境变量 → 进程参数 `--port` → **最近一次请求的 Host 头** → 兜底 19387 —— 然后**每个候选都真的请求一次 `/api/ping` 确认是我们自己**,才拿去打开。

一个都不通就退回控制台窗口:宁可不开,也不能弹一个错的页面。

设置里 `web` / `cmd` / `none` 三档可选。

---

## 谁能来、谁请不动

开房前先 `meeting_roster`,名册上每位都带标注:

| 标注 | 含义 | 怎么处置 |
| --- | --- | --- |
| 日志 < 2 MB | 正常 | 随便请 |
| ℹ️ 2–8 MB(偏大,首字会慢些) | 只是首字慢一点 | 照常请,`waitSeconds` 留够 |
| ℹ️ ≥ 8 MB(很大,首字会慢) | 同上,程度更深 | **照样请得动** —— 实测 8.1 MB 的成员 41 秒答完,是全场最快 |
| ⛔ 已封存 | 在 DSH 里**已归档**的会话 | **请不动**,`meeting_open` 直接拒绝 |

体量是每次 `meeting_roster` 自动量的(只读 `stat`,不解压、不读内容);「已封存」量不出来,是人记的 —— 用 `meeting_seal` 维护:

```
meeting_seal action=list     → 看名单
meeting_seal action=seal     → 记一位(session 给编号或 id,reason 写清为什么)
meeting_seal action=unseal   → 摘一位(只有确认它真的醒得过来才该摘)
```

**谁能开口,由 DSH 的归档状态决定,跟日志大小没关系**(2026-10-14 用受控测试推翻了这个旧结论):

- 会话一旦被 **归档**,`archived-session-gate` 会拒绝它的一切模型步骤 —— 消息投进去只会 `turn/start` → 几十毫秒后 `turn/end(reason.kind='blocked')`,**连模型请求都不发**。所以归档的会话永远不可能在会议上开口,跟硬盘多快都没关系。名册读的就是 DSH 自己的权威名单(`$DSH_HOME/storages/workspace.json` 的 `archivedSessionIds`),**只读、绝不写**。
- 日志大小只影响**首字延迟**,而且远小于直觉:9.55 MB 的日志读回来只要 **298 毫秒**(读盘 1.9 ms + 多帧 zstd 解压 137 ms + JSON 解析 138 ms)。

---

## 参数速查

| 参数 | 默认 | 说明 |
| --- | --- | --- |
| `rounds` | 2 | 轮次上限,最多 20 |
| `waitSeconds` | 300 | 每位成员最多等多久,下限 5、上限 3600 |
| 名册缓存 | 15 秒 | `meeting_roster` 的 `refresh: true` 强制重扫 |
| 会议记录提示预算 | 8000 字符 | 兜底:窗口关掉时才可能触发;同样只折叠中间,绝不切半句 |

设置页(`设置 → 星辰会议室`)里可改的项,改完**下一步生效、不用重启**:

| 设置项 | 默认 | 说明 |
| --- | --- | --- |
| 控制台标准输出 | 开 | 是否同时打到标准输出 |
| 开房时弹出会议窗口 | 开 | 是否在开房/开会场实况时弹窗 |
| 默认轮次上限 | 2 | `meeting_open` 不传 `rounds` 时用几轮(1–20) |
| 发言长度(软上限) | 800 字 | 只写进给参会 AI 的要求里,**不截断**任何发言(100–5000) |
| 会议记录窗口(条) | 3 | 每次投递只带"开场 + 最近 N 条"发言(0–20) |
| 会场实况怎么显示 | 网页 | `web` / `cmd` / `none` |
| 收会时生成会议纪要 | 开 | 关掉后 `meeting_close` 照样把成果交回来,只是不写 .md |
| 纪要目录 | 空 | 空 = 插件自己的 `minutes/`;**只收绝对路径**(相对路径一律拒) |

---

## 已知限制

- **归档的会话叫不醒**(这是唯一的硬限制)。详见上面「谁能来、谁请不动」。
- **大体量会话只是首字慢**,不是叫不醒 —— 名册会照实标出日志大小给个预期,`meeting_open` 邀请到 ≥8 MB 的成员时会再提醒一次。
- **投递内容会出现在参会 AI 的模型历史里**(那是它参会的依据,藏不掉)。界面层面已经不铺开了,但 token 该花还是花 —— 所以「会议记录窗口」才是那个真正省钱的开关。
- **靠事件流收发言**:如果目标会话在 turn 中途被用户取消,可能只收到半句(会标记为"超时,只收到部分内容")。
- **同进程**:跨进程 / 跨主机的房间没有实现。

---

## 怎么卸载

从 profile 的 bundles 里删掉 `dsh-littlestar-meeting` 即可,零残留。
执行 `plugin_manager: remove_bundle`,或直接删 profile 的 `package.json` 里那一行依赖再 `pnpm install`。
房间与纪要文件在 `$DSH_HOME/xiaoxingchen-meeting/`,不想要了自己删。

---

## 自检

```powershell
# 纯逻辑单测(共 92 项,在开发机的工作目录里跑)
node test-meeting.mjs      # 状态机 + 投递窗口化/软限制(25)
node test-relay.mjs        # 跨会话投递假宿主(20)
node test-config.mjs       # 配置白名单与区间夹取(15)
node test-live.mjs         # 会场实况页面与注入防御(14)
node test-inventory.mjs    # 会话体量盘点(10)
node test-webport.mjs      # 会场地址探测(8)

# 离线 boot 集成自检(28 项):用假 ctx 把插件完整 boot 一遍,
# 再走 名册 → 开会 → 发言 → 轮次 → 收会 → 封存拦截 → /meeting 命令 全流程。
# 因为 host 代码改动要重启才生效,这个脚本是重启前的最后一道保险。
node boot-check\check.mjs
```

装完之后在任意会话里跑 `meeting_selftest`,能收到回话就说明通道是通的。

Install

dsh plugin --profile web add github:theskywilldawn/dsh-LittleStar-meeting

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source