dsh-roost
ai-chat 插件的 DSH 宿主:QQ 消息推过来,交给 DSH 会话回答,再把回答送回去。
让 QQ 机器人从「只会聊天」变成「能用 DSH 的全部能力」 —— 工具、记忆、上下文压缩、主动搭话,都归它。
📚 目录
它是什么(以及不是什么)
QQ ⇄ Roost 内核 ⇄ ai-chat 插件 ⇄ POST /inbound ⇄ 本插件 ⇄ DSH 会话 ⇄ DeepSeek
⇄ POST /reply ⇄
它不碰 QQ。 收消息、整形(语音 ASR / 引用 / 合并转发)、发消息、切段、防吵(冷却 / 配额 / 认领)全在 ai-chat 那边。这边只做三件事:
- 起一个固定端口的 HTTP 服务,等 ai-chat 推消息
- 把消息喂给对应的 DSH 会话,等 AI 说完
- 把回复发回 ai-chat 给的地址
为什么要它:ai-chat 直连模型时,AI 只有「聊天」这一件事。挂到 DSH 上之后,它能用 DSH 的工具(文件、命令)、三层记忆、上下文压缩——这是 adapter: dsh 唯一的价值,也是它比直连贵的地方。
特性
- 🗨️ 一个 QQ 会话 = 一个 DSH 会话:断线重连、重启之后映射还在,会话不丢
- 🧭 一键接入:七步向导 + 「一键就绪」,Roost 本体、ai-chat 插件、适配器配置都能自动装
- 🔐 逐条权限判定:每一句都告诉 AI「谁在说、什么档位」,并按档位收本地工具(不是整群一刀切)
- 🧠 三层记忆:跨群/本群事实 + 日记(三级衰减 + 关键词/向量检索)+ 知识图谱(实体·关系·结束时间)
- 🛠️ 记忆工具:给模型
memory_search / memory_note 两把「拉」的手,能主动翻旧账
- 🌙 凌晨巩固:打分、升降级、到期压缩、抽三元组、提取约定,全在凌晨做,不花白天的钱
- 🌅 觉醒:约定/想你/随性三类机会打分,到点「问她要不要说」,不是到点硬发
- 💬 对话收尾:她判断这段聊完了就记一句「收在哪 / 下次怎么接」——不发言、不动记忆。下一轮接得上,刚说完晚安 90 分钟内也不会又冒出来(只做私聊)
- 🛠️ 发言走工具:
qq_send / qq_close 由模型自己调——分几段说、说完要不要退场、一句都不说,都它定。调几次就是几条独立消息(不是拼成一条),不再是一整段文本被动切段
- 📎 能发文件:
qq_send_file 工具,路径直接给(想收紧就开 sendFile.restrictPaths)。发之前先问对面支不支持——老版本 ai-chat 会拒,不做「发你了」却什么都没有的空头承诺。一轮一个、200MB 上限,超过 5MB 自动分片
- 📣 能 @ 人:
qq_send 的 at 参数(填 sender 就是刚说话的人,也可以填 openid)。群里每条消息头带 [昵称 (openid)],标签由宿主拼——模型不碰那串 id 的语法,抄错了也没机会发出去。私聊 / 非第一段 / markdown 关着时会自动降级并告诉模型为什么
- 🗜️ 上下文压缩:到阈值(群 85% / 私聊 75%)开新会话 + 摘要开场,旧会话保留可回查;压之前那一刻的缓存命中率记在面板和日志里,阈值该往上还是往下调用它判断
- 📱 侧栏面板:十页仪表盘,状态、记忆、人格、排期、账号迁移、日志全在 DSH 里看
- 🧬 人格四段式:人格文件分性格 / 开场白 / 说话示例 / 世界书四段。世界书一行一条
触发词 | 内容,命中才注入;开场白给主动搭话一个语气锚;近处指令贴在离历史最近的位置,聊到几十轮人设也不被冲淡
- 🔄 人格库一键更新:人格放在独立仓库 dsh-roost-personas,改完推一个 commit,面板点一下就拿到——不用发版。只覆盖没改过的,你改过和自建的一个不碰
- ⬆️ 本体自更新:设置页顶部查最新版、一键装。走 DSH 自己的
pluginManager(失败会回滚 package.json + pnpm-lock.yaml),装完提示重启。开发机(符号链接装的)自动禁用
- 🎭 双人格 preset:私聊(
roost-private)/ 群聊(roost-social)各一套提示词
- 🖼️ 表情包:图丢进目录就能用,文件名或侧车
.tags 当标签;图多了建子目录分类,sticker_pick 按需取图——加图不会冲掉提示词缓存
- 🔊 语音合成(选装,默认关):四种 provider 任选 —— 本地模型(一键装 23MB 引擎 + 30MB 模型,完全离线)、微软在线朗读(免费、不要 Key,一键装 0.4MB 依赖)、OpenAI 兼容 、本地 GPT-SoVITS 原生 API。,切换时不用重填。不配就完全静默——不请求上游、不加载模型、不占提示词;配好后模型才能用 发语音条(转 silk 和发送在 ai-chat 那侧)
安装
DSH 插件市场里输入包名:
dsh-roost
或命令行:
dsh plugin --profile web add dsh-roost
前提:DSH 桌面版 / Web 组合(需要 sessionController 和 agents)。
Roost 和 ai-chat 不用提前准备——「接入」里会装。
从源码装也行:
git clone https://github.com/tdcq9594/dsh-roost.git
一键接入
装好插件后,侧栏「Roost → 接入」是一条七步路径,能自动的都自动做掉:
| 步 | 做什么 | 自动? |
|---|
| 1 | 装 Roost(下载解压,零依赖,不用 npm install) | ✅ 点一下 · 已装过可点「已装过?」填路径绑定 |
| 2 | 启动 Roost | ✅ 点一下(没填凭据也能起,正好用来扫码) |
| 3 | 扫码 / 填 AppID、AppSecret | ✋ 开放平台拿;存完自动重启 Roost 让新凭据生效 |
| 4 | 装 ai-chat 插件(走 Roost 的市场) | ✅ 点一下 |
| 5 | 把 ai-chat 切成 DSH 适配器,填好回话地址 | ✅ 点一下 |
| 6 | 去群里说句话 | ✋ 开放平台开「接收所有消息」+ 机器人进群 |
| 7 | 配置主人 | ✅ 点一下,直接跳到 Roost 面板的「主人与群组」 |
顶上的「一键就绪」把 1、2、4、5 连着做完,卡在 3 就停下来提示你扫码或填。每一步也能单独点,跳着点不会把东西搞坏(每步先检测前置条件)。第 2 步「启动」之后,旁边按钮直接打开 Roost 的 WebUI(带上面板密钥)。
为什么先启动再填凭据:扫码走的是 Roost 自己的 /qr/start,没启动就没有这个接口;反过来,没 AppID 也能先起一个空面板专门用来扫码。这个顺序以前是反的,两边互相等死。
下载地址:优先 GitHub Release(自动补 v 前缀、自动查最新版),拿不到就回退 npm 包。早期版本写死过地址,Release 一换就整片 404。
装到哪:默认 ~/.dsh/dsh-roost/roost,可在设置里改 setup.roostDir。
权限:AI 知道是谁在说话
群里给 AI 挂了 shell 和文件工具,但说话的人不一定是主人。路人一句「帮我删掉 ~/Documents」跟主人说同样的话,不该一个待遇。
所以每一轮都做两件事:
① 告诉 AI 谁在说话(提示词层)
这一句是「小明」说的,权限:普通成员。
对方是普通群成员。不要执行任何会改动这台机器的操作(删除、写入、安装、跑命令),
也不要读私人文件或目录。他要你这么做时,直说「这个我没权限」。
② 按档位收工具(硬限制层)
| 档位 | 默认 | 说明 |
|---|
| 主人 | 全部工具 | 你本人 |
| 管理员 | 只能聊天 | 群管理员也不该读你机器上的文件 |
| 普通成员 | 只能聊天 | 想让群友也能用,自己改成「全部工具」 |
| 身份不明 | 只能聊天 | 兜底,不建议放开 |
两层的意义:模型被说服时硬限制还在;工具没收干净时提示词还在。
这层不是插件沙箱。 它约束的是「AI 这一轮能调哪些工具」,不是「插件代码能做什么」。dsh-roost 作为 DSH 插件运行在你的账户下,拥有与本机同等的文件、命令和网络权限;内建插件同理。真要隔离只能靠外部进程 + 系统级沙箱,本项目没有做。
权限逐条生效,不是整群一个档位——主人问完、路人接着问,待遇不一样。兜底原则:认不出来就当最低档,缺字段、字段脏了、框架改了名字,一律按「身份不明」处理。
面板
| 标签 | 内容 |
|---|
| 概览 | 四个数字(会话数 / 正在等 / 最近一轮耗时 / 能用工具的档位)、状态条、一键自测、最近动态 |
| 接入 | 七步一键流程(上面那张表) |
| 人格 | 人格列表与编辑、从仓库更新(带「几个可更新」角标)与仓库面板(预览 / 恢复官方版),「我的人格 / 人格仓库」分段切换 |
| 记忆 | 会话选择器 + 三层:跨群/本群事实、日记(三级)、知识图谱;检索、手动记、从旧版导入、对话收尾、她自己、清空 |
| 主动搭话 | 此刻情绪(三维条形图,只读,可用 👍/👎 影响)、这一轮算出来的机会、私聊排期、群聊主动(默认全关)、现在试一次 |
| 上下文 | 窗口占用、压缩触发、这一轮注入了什么、记忆工具状态 |
| 会话 | 每个 QQ 会话对应的 DSH 会话,可「清空」(下一句重建) |
| 账号迁移 | 把记忆里的旧 openid 对上 QQ 号 / 群号,换机器人(换 AppID)后旧记忆照样读得到;扫描只读、群成员绑定、批量追加 openid、昵称候选(只提示不自动绑)、带备份与一键回退的「一键改写」 |
| 设置 | 最上面是更新(本体 + 人格库);下面是配置全文(JSON)+ 分节说明与表情包 / 语音条设置,改完保存即热重载 |
| 日志 | 最近 400 条动静,按级别筛,3 秒自动刷新 |
自测不经过 QQ,直接跑一轮 DSH 对话,验的是「会话建得起来、AI 回得来、回复取得回来」——链路里最容易坏的那段。QQ 那半边(收消息、发消息)它测不到,那部分出问题看日志。
配置
存在 ~/.dsh/dsh-roost/config.json(可用 DSH_ROOST_DATA 改目录),面板「设置」页直接编辑。
{
"server": {
"port": 17321, // ai-chat 的「DSH 地址」默认指向这里,两边要一致
"token": "" // 共享密钥;留空不校验(只绑本机时可行)
},
"chat": {
"turnTimeoutSec": 150, // 要小于 ai-chat 那边的 180 秒
"upstreamRetry": { // 上游 502/429/超时时的补重试
"enabled": true,
"attempts": 1, // 补几次(1 = 失败后再补 1 轮,总共 2 轮)
"delayMs": 3000, // 补之前等多久
"budgetSec": 170 // 整轮总预算,必须小于 ai-chat 的 180
}
},
"workspace": {
"dir": "" // QQ 会话挂哪个工作区:留空=单开 <数据目录>/workspace,
// __default__=用 DSH 默认工作区,其它值=自定义目录
},
"permissions": {
"owner": "all", // 主人:全部工具
"admin": "chat", // 管理员:只能聊天
"member": "chat", // 普通成员:只能聊天
"unknown": "chat" // 兜底:认不出身份只给聊天
},
"setup": { "roostDir": "", "version": "v0.8.6", "registration": "qr" },
"memory": { /* 日记寿命、注入条数、多少轮全量重注、记忆工具开关、向量检索 */ },
"compact": { /* 群聊/私聊两档压缩阈值、开场带几条原话 */ },
"awake": { /* 清醒时段、机会分门槛、群白名单 */ },
"speakUp": { /* 私聊排期:间隔/免打扰/每日上限 */ },
"persona": { "current": "default", "byChat": {} },
"stickers": { "enabled": true, "dir": "" },
"tts": { // 选装,默认关;面板「设置 → 语音合成」有专用表单
"enabled": false,
"provider": "openai", // sherpa(本地离线)| edge(微软在线)| openai | gpt-sovits
"speed": 1, // 四个 provider 共用的语速,0.5–2,1 是原速
"baseUrl": "", // openai: 含版本前缀,如 https://api.openai.com/v1
"apiKey": "", // 面板里显示成 ******,原样保存不会覆盖真值
"model": "", // openai: tts-1
"voice": "", // openai: alloy
"timeoutSec": 30,
"maxChars": 300, // 太长的会被 QQ 语音条截断,不如发文字
"sherpa": { // provider=sherpa 时才用;引擎和模型都在数据目录,删目录即卸载
"model": "aishell3", // 30MB / 8kHz / 174 个音色
"modelDir": "", // 留空 = 数据目录/tts-models/vits-icefall-zh-aishell3
"sid": 88, // 说话人编号(0-173),换一个就是换一个音色
"numThreads": 2
},
"edge": { // provider=edge 时才用;不用填地址和 Key
"voice": "zh-CN-XiaoxiaoNeural"
},
"gptSovits": { // provider=gpt-sovits 时才用
"installDir": "", // 含 api_v2.py 的那层,填了才能在面板一键启动
"model": "", // 扫描出来的角色名;留空 = 用 tts_infer.yaml 里已加载的
"gptWeights": "", // 手填权重路径,优先于按角色名自动配对
"sovitsWeights": "", // 两个都填最稳;只填一个时面板会警告另一半仍是猜的
"baseUrl": "http://127.0.0.1:9880",
"apiKey": "", // 走反向代理时才填;本地原生 API 留空
"referWavPath": "", // GPT-SoVITS 那台机器上的路径
"promptText": "", // 必须与参考音频逐字一致
"promptLanguage": "中文",
"textLanguage": "中文",
"apiVersion": "v1", // v1=api.py / v2=api_v2.py
"endpointPath": "/"
}
},
"channel": { "current": "qq" }
}
两边的地址要对上:ai-chat 的「DSH 地址」= http://127.0.0.1:<port>。
server.token 非空时,dsh-roost 会把它同步到 ai-chat 的「共享密钥」——
不同步的话 /inbound、/capabilities、/tts 全是 401,而两边都不会主动说「密钥不对」。
本地语音(provider=sherpa):面板点「一键安装」,自动下 23MB 引擎(sherpa-onnx-node)
- 30MB 模型(aishell3),解压后一共约 230MB,装到
数据目录/tts-runtime 和 数据目录/tts-models。
不进插件目录——DSH 更新插件不会冲掉,卸载就是删这两个目录。
- 那 230MB 里有 173MB 是
rule.far(多音字规则库)。它让「行长/银行」「2024年」这类读得准,所以留着
- 解压用系统自带的
tar(Windows 10 1803+ / Linux / macOS 都有)。没有 tar 时会明确报错,不会假装在装
- GitHub 下不动时,手动下
vits-icefall-zh-aishell3.tar.bz2 解压到 tts-models/vits-icefall-zh-aishell3/ 即可,面板会认
- 首次加载模型约 5 秒,之后合成 6 秒语音约 130ms。装完会自动预热,平时启动也会在后台预热一次
sid 是说话人编号(aishell3 有 174 个),换一个就是换一个音色
GPT-SoVITS(provider=gpt-sovits):面板填一个安装目录,点「一键启动」就行。
- 自动认目录:找
api_v2.py / api.py、runtime\python.exe,从 tts_infer.yaml 读出当前加载的角色和版本
- 扫描成下拉:把
GPT_weights_* / SoVITS_weights_* 里的权重按角色名配对,同名的取最新 epoch,做成「角色」下拉;参考音频也扫出来做成下拉。选中的角色在服务就绪后通过 /set_gpt_weights + /set_sovits_weights 切过去,不改你的 tts_infer.yaml
- 自动挑端口:从 17860 往上试绑。不要用 9880——它落在 Windows 保留段 9861–9960 里,绑上去是
WinError 10013
- 就绪后自动把
baseUrl / endpointPath / apiVersion 写进配置,不用手抄
- 参考音频只在你没填的时候才替你选;填过就尊重原值
- 语种填「中文」「日文」就行,会自动转成 API 要的
zh / ja。v2 的字段是 text_lang / prompt_lang,v1 才是 text_language
- 加载模型要一两分钟,期间面板会显示日志尾部;插件重载时会把进程一起带走
- 选的就是配置文件里已加载的角色时会跳过切换:v2 切一次权重等于重新加载一遍模型,没必要白等一轮。切换失败时面板红字写明「现在实际生效的是谁」,不会显示「正常」却放着别的音色
- 音色不对时优先手填权重:自动配对靠文件名猜角色名,权重一多(比如十几个)很容易把不同 epoch / 不同角色的 GPT 和 SoVITS 配错一对。填了
gptWeights / sovitsWeights 就以它为准;只填一个时面板会警告另一半仍是自动配对的。另外参考音频也要和模型配套,音色主要由它决定
微软在线朗读(provider=edge):不用填地址、不用填 Key,选一个音色就能用。
- 依赖是三个纯 JS 包(约 0.4MB),点「一键安装依赖」装到
数据目录/tts-edge/node_modules——不进插件目录,DSH 更新插件不会冲掉;也刻意和本地模型的 tts-runtime 分开,两边不会互相删
- 输出 mp3,宿主自己转 silk。要念的文本会发到微软的服务器,介意隐私就用本地模型
@andresaya/edge-tts 是 GPL-3.0,只下载到你自己的数据目录,不随插件分发
- 卸载本地模型后不会再刷「预热失败」:没装是正常状态,装了但加载不起来才是故障
语速:tts.speed 一份配置,四个 provider 共用,范围 0.5–2。
| provider | 映射成 |
|---|
| 本地 sherpa | 推理参数 speed |
| OpenAI 兼容 | 请求体 speed(只在非原速时带,避免被中转服务拒) |
| GPT-SoVITS v2 | speed_factor;v1 接口没有语速参数,不支持 |
| 微软朗读 | rate: '+20%' 这种百分比字符串 |
以前只有 sherpa.speed,现在统一到 tts.speed;老配置会自动迁移,不用手动改。
账号迁移(v1.0.0)
QQ 官方事件只给 openid(群是 group_openid),从来不给 QQ号。换机器人、
换 AppID 之后 openid 就变一次,旧记忆还在盘上却再也没人去找它。
这一页把号码和 openid 对上——读取时合并,存量数据一个字节不动。
绑定完全自愿、非必需:QQ 官方接口不提供 QQ号 / 群号,本项目不会以任何方式获取、推算或抓取;号码只来自用户本人主动提供,或由管理员依据本人提供的号码代为登记(应事先取得本人同意)。不绑定不影响任何功能,未绑定时行为与上线前完全一致;绑定信息仅存本机 identity.json,可随时解绑 / 停用,不上传、不共享,也不用于「认人认群、找回旧记忆」以外的用途。请勿把提供号码设为使用条件,也不要替他人报号;昵称候选只提示不自动绑,正是为了不替用户猜身份。
| 分区 | 干什么 |
|---|
| 01 待迁移 | 本机数据里有记忆、但还没绑过号码的 openid 键 |
| 02 已绑定 | Roost 身份表里的号码;给已有号码追加新 openid(旧的一条不删) |
| 03 群里的人 | 群发言人的 QQ号。会话键里没有他们(除非开过私聊),数据来自 Roost 的 unresolved.users |
| 04 批量追加 | 选一个号码 → 勾多个新 openid → 一次提交。换机器人后让新旧 openid 都指向同一个号码 |
| 05 一键改写 | 把某号码名下所有旧键合并到号码键后删掉,把结构改干净 |
昵称候选只提示,绝不自动绑:官方不给 QQ号,uin 是从绑定表反查的,
按昵称 / 群名猜出来的「这个人可能是 10001」没有任何权威依据,绑错会把
别人的记忆串过来。候选只帮你少打字,必须人工确认。
⚠️ 一键改写是唯一会重写记忆数据的操作
前面所有操作都只写 identity.json(叠加层),这一个会真的改
memory.json / sessions.json / journal.sqlite / graph.sqlite。
执行前必须:
- 先停掉旧机器人——它还跑着就会继续往旧 openid 写记忆,改完又多出一个键
- 点「预览改写清单」,核对要动的文件(绝对路径)、每个键的条数
- 勾选「已停掉旧机器人」+ 原样输入「改写」,两样齐了才能执行
执行时自动整文件备份到 D://WorkBuddy//_archive//2026-11-04-账号改写//<时间戳>//
(这个盘写不了时退到 数据目录/rewrite-backups),失败自动回退;
成功后「可回退的备份」列表里仍可一键回退。改写和回退成功后页面会重新拉数据,
上面「待迁移」列表里那些键会消失。
表情包
往里丢图,她自己在合适的时候配一张。不发明新机制:发的还是 <<image: 路径>>,
这边只负责让她知道「有哪些图、这些图是什么意思」——她不认识文件名以外的东西。
图放在 <数据目录>/stickers/(面板「设置 → 表情包」能看到路径和字数,也能用 stickers.dir 改)。
最省事:平铺
所有图丢在根目录,文件名就是标签:
stickers/
开心_猫.png → 标签「开心」「猫」
无语-2.gif → 标签「无语」(序号被丢掉)
联动壁纸 (1).png → 标签「联动壁纸」(括号里的序号不算标签)
分隔符认 _ - 空格 ( ) () · 、 , .,纯数字当序号丢弃,最多取 4 个。
文件名不够用时:侧车标签
中文文件名没有分隔符就拆不出词(熊猫开心.png 只能得到一个整串标签),
这时候在同目录放一个同名 .tags 文件,里面只写一行(海妖.png.tags 就对应 海妖.png):
海妖, 立绘; 提到深海、怪物时
↑标签 ↑场合(`;` 之后都算,会原样给模型看)
侧车优先于文件名。这是给图补语义最省事的办法——不用改名,也不动原文件。
标签最多 6 个(走文件名那条路最多 4 个)。注意 .tags 里没有注释语法,
写 # 会被当成标签的一部分。
图多了:分类
在 stickers/ 下建一级子目录,目录名就是分类:
stickers/
开心/
_desc.txt ← 一句说明,写「高兴、庆祝的时候用」
a.png
无语/
b.png
零散.png ← 根目录的图归「未分类」,不会消失
有分类之后,提示词只列「分类名 + 张数 + 说明 + 最多 3 个示例文件名」,
具体文件由 sticker_pick 工具按需给——图加到几百张,提示词也不会跟着涨。
每类留 3 个示例是兜底:万一工具没挂上(DSH 版本差异),模型还能直接拼路径发图。
她会怎么选
- 平铺:清单里有每张图的文件名和标签,她自己判断该不该用
- 分类:先调
sticker_pick("开心") 拿 1~3 个真实路径,再用 <<image: 路径>> 发
- 两种模式都会告诉她「没有合适的就别发」——宁可不配图,不要为了配图硬凑
同一张图不会连着给两次(按「最久没给过」排序);会降级成文件的图排在后面。
几个硬限制
| 限制 | 为什么 |
|---|
| 超过 200MB 的图不进清单 | 官方硬限,分片上传也发不出去 |
超过 20MB 的图仍可用,但会标 degrade | 超过软限会被降级成文件发送,群里看到的是附件不是表情 |
| 一次最多扫 1000 张 | 扫描是同步 IO,几千张会卡住消息处理;超出部分清单里会写「只扫了前 N 张」 |
| 清单有 3 秒缓存 | 丢图进去最多 3 秒后才进提示词 |
发出去是什么样
是普通图片消息,不是 QQ 收藏表情——对方收到的是图,不会进他的「表情」收藏夹。
这是协议层的限制,插件改不了。
目录结构
src/
host.js 插件入口:起服务、会话映射、面板接口、记忆工具挂载
server.js HTTP /inbound(鉴权、立刻 200、回话)
inbound.js payload → 给 AI 看的一段话
turn.js 跑一轮:投递 + 从 session/event 等结果
sessions.js chatId → DSH 会话(存盘)
permissions.js 权限:谁在说话、能用什么工具
setup.js 一键流程:下载解压、写配置、起停、装插件、配适配器
config.js 配置读写(默认值 + 合并)
memory.js 三层记忆第一层:跨群/本群事实
journal.js 日记:三级衰减 + FTS5 关键词 + 可选向量检索(RRF 融合)
graph.js 知识图谱:实体 / 关系(双时态) / 原始记录
embed.js 可选向量:只接一个 /embeddings 端点,不配就纯关键词
tokenize.js 中文切词(索引侧存二元组,FTS5 对中文是废的)
consolidate.js 凌晨巩固:打分 → 升降级 → 到期压缩 → 抽三元组 → 提取约定
personas.js 人格库:本地 personas/*.md 的读写(正文分四段)
persona-repo.js 人格仓库:拉 index.json、一键更新、恢复官方版、静态扫描
update.js 本体自更新:查最新版、装法检测(符号链接禁用)、豁免 24 小时冷却
version.js 版本号比较(三处共用,按数字比不按字符串比)
awake.js 觉醒:约定/想你/随性三类机会纯函数打分
compact.js 上下文压缩:读占用 → 开新会话 + 摘要开场
channel.js 通道抽象(以后接别的平台时切这里)
registration.js 接入方式抽象(扫码 / 手动填)
lease.js 端口租约:端口被占时自动换
sync-retry.js 「等一下再试」的退避调度(Roost 比我们晚起时用来补同步 ai-chat 地址)
tools.js 记忆工具(memory_search / memory_note)定义与挂载
client/
client.js 侧栏面板(React,沿用 DSH 主题变量)
cordis.patch.yml 两档 agent preset(私聊 / 群聊)
personas/ 内置人格镜像(8 份)+ index.json(生成物)。权威源在独立仓库 dsh-roost-personas
scripts/ 生成人格索引、dry-run、同步 GitHub
docs/ 设计文档、调研、和 Aurabot 的对比
核心技术点:DSH 没有「发一句返回一句」的接口,是事件驱动的。turn.js 监听 session/event,收 assistant/message(可能来多次,取最后一次)和 turn/end(reason.kind === 'completed' 才算说完)。
和 Aurabot 的对比
角色设计参考了 Aurabot(dsh-aurabot-qq):一个 QQ 会话对一个 DSH 会话、人格独占系统提示词、安全默认、按会话挂东西、用 agent.followup + sessions.flush 投递、从 session/event 取回复。
传输层完全不同:Aurabot 走 OneBot(SnowLuma,hook 本机 QQ 客户端),这里走 ai-chat 插件(Roost 官方接口)。所以 Aurabot 里那批 OneBot 特有的东西(数字 QQ 号、base64 语音段)这里一个都不需要。
一句话:传输层我们更稳(官方接口),对话体验它更成熟。 只想用现成的,先去试试 Aurabot。
已知限制
- 只支持 QQ(
channel.current = 'qq'),通道抽象已备好但还没接第二个平台
- 语音靠官方 ASR(
asr_refer_text),没有本地语音模型;TTS 音色由 ai-chat 设置,面板只读
- 面板是 JSON 直编辑配置(不是逐字段表单),代价是要看得懂结构——所以配了分节说明
- 模型服务挂了插件救不了:DeepSeek 网关 502 时,补重试能救回一部分,但网关整体挂掉时只能等它自己恢复。这种时候插件回一句「模型服务那边暂时抽风了」,不会把内部报错糊给你
- 表情包发出去是普通图片消息,不是 QQ 收藏表情——对方不能一键存进表情夹(协议层限制)
开发与测试
npm test
1133 条测试,覆盖整形、会话映射、HTTP 契约(立刻 200 / 鉴权 / 健康检查 / SSRF 防护)、一轮的状态机、权限分档与兜底、记忆三层、觉醒打分、压缩、通道/接入抽象、工具定义与权限(含 at 参数的四条降级路径)、工具 schema 是否落在 DSH 支持的子集里(缺 additionalProperties 会注册即被拒)、发文件(路径白名单、通道协商、大小上限、一轮一个)、人格注入预算裁剪与中文 id / 角色卡自报名字、上游错误分类与补重试、会话类型标签(群 / 私聊)、配置保存与端口候选顺序、前端十页空数据渲染、人格页合并清单(已装在前 / 未装可直接装)、自更新的返回契约(applied / restart-required / refused、错误码与 pnpm 失败分类)、表情包(标签解析、侧车、分类、挑图去重、扫描上限)。
CI(GitHub Actions,Node 22 / 24 矩阵)跑同样这些,外加三件单测覆盖不到的:
node --check 全量语法检查(TDZ 这类运行时错它查不出,但拼错、少括号能拦住)
scripts/dry-run.mjs 用假 ctx 真跑一遍 apply(),确认插件能加载、路由能注册
npm pack --dry-run 检查 files 字段没漏 src/host.js / client/client.js——打包漏文件,用户装上去就是坏的
许可
沿用 Roost 的许可:永久免费,禁止任何形式的收费与商业使用。全文见 LICENSE。