Skip to content
dsh.fish
Bundle

dsh-group-chat

DSH-native multi-AI group chat: the host (user) runs a chatroom of configurable AI agents with per-agent models, role cards, mute controls, and passive/active speaking modes — pure Node.js/Cordis, no Tauri/Rust

Source
Qx002
stars
1 stars
License
MIT
Updated
Updated yesterday

Readme

# dsh-group-chat

DSH 原生多 AI 群聊插件:用户作为「群主」,在 DSH WebUI 中管理一组可配置的 AI
角色,让它们围绕共享上下文进行多模型轮流发言。纯 Node.js / Cordis 实现,
**不使用 Tauri / Rust**。

> 仓库主页: <!-- TODO: 发布后填入你的仓库地址,如 <https://github.com/你的用户名/dsh-group-chat> -->

## 阶段状态

| 阶段 | 内容 | 状态 |
| --- | --- | --- |
| 阶段 1 | 基础设施:包清单、核心接口、Cordis 注册与 `ctx.groupChat` Service 暴露 | ✅ 已完成 |
| 阶段 2 | 编排器引擎:消息拦截、多模型轮流生成、主动/被动发言、共享上下文 | ✅ 已完成 |
| 阶段 3 | WebUI:输入框旁群聊开关 + 群聊设置页(React + slot 注入) | ✅ 已完成(本仓库当前状态) |

## 架构

```
src/
├── index.ts       插件入口(class plugin):name / Config / 默认导出 GroupChatService
├── service.ts     GroupChatService (extends Service):暴露 ctx.groupChat(配置注册 + 轮次门面 + API 挂载 + 模型目录 + 消息视图)
├── orchestrator.ts GroupChatEngine:独立群聊页的消息通道、轮次循环、主动发言调度(阶段 2 核心)
├── api.ts         主机端 WebUI API 路由(/api/group-chat/*,webServer 注册)
├── context.ts     共享上下文构建:[AI名称]: <内容> 转写 + system prompt 协议(纯函数)
├── speakers.ts    群主规则 → 发言者选择(提及/触发/主动/默认,禁言跳过)(纯函数)
├── config.ts      DEFAULTS(唯一权威默认值)+ schemastery schema + assertGroupConfig 校验
├── error.ts       GroupChatError(稳定机器码)
├── types.ts       纯类型契约:GroupConfig / AgentConfig / HostRules / 轮次结果 / 模型目录 / 消息气泡类型
└── client/        WebUI(React,经 tsdown 打包为 lib/client.js)
    ├── index.ts                   客户端插件入口:apply(ctx) 注册 slot
    ├── api.ts                     类型化 fetch 客户端(镜像主机路由)
    ├── GroupChatToggle.tsx        群聊开关(conversation.input.left):开/关独立群聊页面
    ├── GroupChatOverlay.tsx       群聊页面容器(约 2/3 居中,右上 ✕ 关闭,左上 ⚙ 设置)
    ├── GroupChatChatView.tsx      微信式聊天视图(AI 左 / 用户右气泡,文本+图片发送,轮询)
    ├── ModelPicker.tsx            Model ID 级联选择器(提供方 → 模型,同步 DSH 模型目录)
    ├── GroupChatSettingsPanel.tsx 设置面板内容(角色管理/群主规则/共享上下文/用户名称显示)
    ├── GroupChatSettingsSection.tsx 设置面板中的“群聊设置”页(settings.section)
    └── styles.ts                  共享内联样式
build/           tsdown 客户端打包预设(平台 externals + 纯净化 gate + __ModuleLoader__ 交接)
```

### 阶段 3:WebUI(slot 注入 + 独立群聊页)

浏览器端是一个标准 DSH 客户端插件:`lib/client.js` 通过
`window.__ModuleLoader__.load({id, factory})` 交接(与官方 dsh-web-ui 管线一致),
`factory` 导出 `apply(ctx)` / `name` / `inject`,收到客户端 cordis 根上下文。

**产品形态**:群聊是**独立页面**,与工作区原生对话完全隔离(原生输入框/模型选择/
读写权限等继续为工作流服务,群聊不接管、不混淆):

| Slot | 内容 |
| --- | --- |
| `conversation.input.left` | 输入卡工具栏左端的**群聊开关**:点击开启/关闭独立群聊页面(状态点 + 开/关,持久化到 `enabledSessions`,全局开关自动联动) |
| `settings.section` (id `group-chat`) | 设置面板导航中的**群聊设置**页(`GroupChatSettingsPanel`) |

**独立群聊页面**(`GroupChatOverlay`,约覆盖原生对话区域 2/3、居中):
- 左上角 **⚙** 切换群聊设置(同一面板);右上角 **✕** 关闭页面 —— 关闭后
  输入框旁的群聊开关同步回到“关”(`enabledSessions` 移除,主动发言定时器停止);
- 聊天视图(`GroupChatChatView`,微信式布局):AI 成员消息在**左**、用户在**右**,
  气泡上方显示名称、下方显示内容;独立输入框支持 Enter 发送、Shift+Enter 换行、
  **发送图片**(PNG/JPEG/WebP/GIF,经 DSH attachment 服务持久化后作为 image block
  进入模型请求并在气泡内显示);页面打开期间轮询消息视图;
- 用户侧名称来自群聊设置里的 **用户名称显示**(`GroupConfig.userName`,身份默认
  “群主”),同时作为共享上下文转写中用户行的前缀。

**群聊设置页**包含:
- 群聊状态:全局开关、群聊名称、**用户名称显示**;
- AI 角色管理:成员列表(名称、模型、主动/被动、禁言徽标)、添加/编辑(角色卡
  System Prompt、初始上下文、Provider/Model、发言模式、主动发言策略、@别名、
  触发词、禁言/启用权限)、删除、快捷禁言;
- **Model ID 级联选择器**(`ModelPicker`):点击后先列出 DSH 已接入的模型提供方
  (`ctx.llm.listProviders()`),点击提供方再列出该提供方通告的模型
  (`listModels(provider)`)—— 与 DSH 模型接入/模型选择 UI 的数据完全一致;
  无目录或需要手填时保留 `provider/model` 手动输入;
- 群主规则:禁言全体、主动发言总开关、单轮回复上限、单轮发言者上限、并行生成、
  超时、@ 语法;
- 共享上下文:转写窗口、转写模板、角色卡注入开关。

数据通道:浏览器 `fetch` → 主机 `ctx.webServer` 路由(`src/api.ts`,
同源 POST 防护,错误统一 `{ok:false, code, message}`)→ `ctx.groupChat` 门面
→ 编排器/配置存储。客户端 `src/client/api.ts` 提供类型化封装并把非 ok 响应
转为 `GroupChatClientError`。

构建:`npm run build` = `tsc`(主机 lib)+ `tsdown`(浏览器 client.js)。
客户端 bundle 只允许:平台模块(react、cordis、ui-slots 等,运行时由 shell 的
模块表解析)与内联安全层;任何其他 `@deepseek-ai` 值导入会被 build 期纯度
gate 拒绝(跨插件协作必须走 cordis 服务)。

### 阶段 2:编排器引擎

#### 1. 消息通道

群聊是独立页面,所有消息都走显式通道:
`ctx.groupChat.submitMessage(sessionId, text, images?)` —— 独立群聊页的输入框
调用;创建 `user/message` 事件(可含 image block)、运行群聊轮次。要求群聊已
启用且该会话在 `enabledSessions` 中。原生工作区输入框不会被接管(不再注册
`agent/pre-step` 拦截),群聊与工作流对话完全隔离。

#### 2. 发言者选择(`selectSpeakers`,依据 HostRules)

```
候选池 = agents.filter(enabled && !muted)        # 禁言/移除直接跳过
优先级:mentioned(@名字/别名)→ triggered(触发词)→ active(主动发言成员)
      → default(无人触发时由第一个可用成员兜底)
截断:maxAgentsPerTurn;muteAll → 不生成(但用户消息仍落盘)
```

#### 3. 共享上下文(统一 Session Log,`context.ts`)

- 每个发言者看到同一份转写:`session.deriveMessages()` 投影为
  `[AI名称]: <内容>` 行(宿主行使用群聊设置里的 **用户名称显示**,默认 `群主`),
  按 `sharedContext.transcriptTemplate` 渲染,滚动窗口 `maxMessages` 行。
- system prompt = 角色卡(`systemPrompt`)+ 群聊协议(回复模式或主动发言模式),
  协议在最后(最近的指令权重最高)。
- 顺序模式下,后发言者能看到本轮先发言者的**最新回复**(每步重新派生转写)。

#### 4. 轮次循环(会话日志事件与官方 agent-loop 完全同构)

```
turn/start → user/message → 每发言者: step/start → assistant/chunk* →
assistant/message → step/end → turn/end
```

- 群聊 turn 编号使用偏移空间(`GROUP_TURN_BASE = 1_000_000` 起),与
  agent-loop 的计数器永不冲突;恢复会话时扫描日志续号。
- 流式:每个 chunk 先落 `assistant/chunk`,用官方 `BlockAssembler` 组装后写
  `assistant/message`(含 `source: {provider, model}` provenance 与 usage),
  WebUI 按 `step/start`+`assistant/message` 契约正常渲染。
- 每会话串行队列;`parallelSpeak` 时同轮发言者并行生成。
- 失败语义:单发言者失败被记录(`GroupTurnStepResult.failure`)不影响他人;
  全部失败 → `turn/end(error)`;取消 → `aborted`。

#### 5. 主动发言(主动模式定时器)

- 每个 `active` 模式的 agent 按 `[minIntervalMs, maxIntervalMs]` 随机间隔
  挂起一次性定时器(`ctx.timer` 服务,fiber 自动清理)。
- 触发条件:群聊启用、`activeSpeakEnabled` 开、未 `muteAll`、该 agent 未禁言
  且 `active.enabled`、会话无进行中的轮次、距离上次活动 ≥ `idleTriggerMs`。
- 不满足(临时)→ 30s 后重试;agent 被移除/改模式 → 停止调度。
- 配置变更(`group-chat/config-updated`)会重置全部主动定时器,使策略即时生效。

### 已暴露的 Service API

```ts
ctx.groupChat
  // 读取
  .getConfig(): GroupConfig
  .getAgent(id): AgentConfig | undefined
  .listAgents(): AgentConfig[]
  .getStatus(): GroupChatStatus
  .watch(cb): () => void
  // AI 角色管理
  .addAgent(input: AgentInput): Promise<AgentConfig>
  .updateAgent(id, patch): Promise<AgentConfig>
  .removeAgent(id): Promise<AgentConfig>
  .setMuted(id, muted)          // 禁言/解禁
  .setMode(id, mode)            // 被动回复 / 主动发言
  .setEnabled(id, enabled)
  // 群主控制面板
  .setMuteAll(muted)            // 禁言全体
  .setActiveSpeakEnabled(on)    // 主动发言总开关
  .updateHostRules(patch)
  .updateSharedContext(patch)
  .setGroupEnabled(on)          // 群聊开关
  .setGroupName(name)
  // 阶段 2:轮次引擎
  .submitMessage(sessionId, text): Promise<GroupTurnResult>
  .enableSession(sessionId) / .disableSession(sessionId)   // 群聊开关(持久化)
  .isSessionEnabled(sessionId) / .listEnabledSessions()
  .cancelSession(sessionId)     // 中止进行中的群聊轮次
  .getEngine()                  // GroupChatEngine
  .listModelCatalog()           // 模型目录(ctx.llm.listProviders + listModels)
```

WebUI API 路由(`/api/group-chat/*`):`state`、`config`、`models`(模型目录,供
ModelPicker 同步 DSH 已接入的提供方与模型)、`messages`(独立群聊页的消息气泡
视图)、`attachment`(图片字节,按完整 ref 校验后返回)、`toggle`、`submit`
(文本 + base64 图片)、`cancel`、`agents`(增删改/禁言/模式)、`host`
(群主规则/共享上下文/全局开关/名称/用户名称显示)。

Cordis 事件(供 Phase 3 / 其他插件订阅):
`group-chat/config-updated`、`agent-added/updated/removed`、
`turn-start`、`agent-speaking`、`agent-spoken`、`turn-end`、
`orchestrator-attached/detached`。

### 数据流

插件以 class plugin 形式加载(与 `@deepseek-ai/dsh-agent-default-model` 同一模式):

- `static Config = GroupConfigSchema` —— 组合入口配置(`cordis.patch.yml` 的
  `config:` 或空值)由插件注册表校验。
- `installSettingsSection(ctx, NS, GroupConfigSchema, entry, hooks)` —— 在
  `group-chat` 用户设置命名空间上注册同一 schema,以入口配置为 `base`;
  解析值 = schema 默认值 → 入口 base → `~/.dsh/settings.yaml` 用户层。
- 写入路径:`ctx.settings.update(NS, patch)`(无 settings 服务时抛
  `GroupChatError` 码 `NO_SETTINGS`,读取仍可用入口配置降级)。
- 每次提交经 `group-chat/config-updated` 事件广播 `(next, prev)`,
  编排器据此重置主动发言定时器。

### 核心接口摘要(`src/types.ts`)

- `GroupConfig` —— `enabled`(群聊开关)、`name`、`enabledSessions`(开启群聊的会话)、
  `agents`、`hostRules`、`sharedContext`。
- `AgentConfig` —— `id`、`name`、`provider`/`model`(DSH 模型路由)、
  `systemPrompt`(角色卡)、`userPrompt`(初始上下文)、`mode`(`passive`/`active`)、
  `muted`(禁言)、`enabled`、`active`(主动发言策略)、`mentionAliases`、`triggerKeywords`。
- `HostRules` —— `muteAll`、`activeSpeakEnabled`、`maxRoundsPerTurn`、
  `maxAgentsPerTurn`、`parallelSpeak`、`turnTimeoutMs`、`mention`(@ 语法)。
- `SharedContextConfig` —— `maxMessages`(统一 Session Log 滚动窗口)、
  `transcriptTemplate`(`[{name}]: {content}`)、`includeRoleCards`。
- `GroupTurnResult` / `GroupTurnStepResult` —— 轮次与单发言者结果
  (`status`、`cause`、`failure`、`messageId`)。

## 开发

```bash
npm install --legacy-peer-deps  # devDependencies(peer 全部由 DSH profile 运行池提供,
                                # 因此跳过 peer 自动安装;版本与池内 0.1.0-rc.6 对齐)
npm run typecheck    # tsc 主机 + tsc 客户端(tsconfig.client.json)
npm run build        # tsc → lib/(ESM + .d.ts);tsdown → lib/client.js(WebUI bundle)
npm run smoke        # 裸 Cordis Context 全链路冒烟测试(注册/配置写入/群聊轮次/
                     #   提及·触发·禁言选择/共享上下文/主动发言/WebUI API 路由/
                     #   消息气泡视图/图片发送/用户名称显示)
```

## 安装到 web profile

### 端用户(从 GitHub 一键安装,推荐)

```bash
dsh plugin --profile web add https://github.com/Qx002/dsh-group-chat.git#v0.1.0
```

该命令在 profile 目录里执行 `pnpm add`:克隆仓库 → 直接使用仓库内**预构建的
`lib/`**(宿主产物与浏览器 bundle 均已提交,无需在安装时构建)→ 自动把声明了
`dsh.bundle` 的包并入 `dsh.profile.bundles`。装完重启 `dsh web` 即生效,
`group-chat` 命名空间出现在设置面板。

### 开发者(本地 link 方式,改代码即时生效)

1. 克隆本仓库到本地,在 `~/.dsh/profiles/web/package.json` 的 `dependencies`
   中加入 `"dsh-group-chat": "link:<本地仓库路径>"`。
2. 在 `dsh.profile.bundles` 中加入 `"dsh-group-chat"`(其 `dsh.bundle.patch`
   指向 `cordis.patch.yml`,自动插入 `group-chat` 行)。
3. 重启 `dsh web`。

## 许可

MIT

Install

dsh plugin --profile web add github:Qx002/dsh-group-chat#de702f7a0f544abb7f9e62f825a18cb2c7faf8af

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
Source