Repository navigation
Releases: tdcq9594/dsh-roost
Release list
v0.12.1 — 语音合成选装
语音合成(选装,默认关)
设置页新增「语音合成」卡片,三种接法:
- 本地模型(推荐):点「一键安装」自动下 23MB 引擎 + 30MB 模型(aishell3),装到数据目录,删目录即卸载。完全离线、不要 Key。
- GPT-SoVITS:填安装目录点「一键启动」,自动挑端口、拉进程、扫权重角色,就绪后把地址/路由填好。9880 在 Windows 保留段里,会自动避开。
- OpenAI 兼容:任何
/audio/speech服务。
不配就完全静默:不请求上游、不加载模型、不占提示词。
修复
qq_send拒收下游标记(say/image/voice/button/silence),不再原样发进群- 打字 + 发语音时,语音转写不再丢失
- 合并窗口带上整批附件(先发图再补一句话不再丢图)
- 图片存本地并给路径,不再给会过期的 QQ URL
- 配置密钥统一掩码,保存时回填;非字符串值不再落盘
引擎说明
sherpa 固定 2.0.1,并显式关闭 enableExternalBuffer——它默认开启,在长驻进程里会抛 External buffers are not allowed。
v0.12.0:角色卡人格能装了
v0.12.0:角色卡人格能装了
面向「把 QQ-Agent 那 346 份角色卡人格迁进来」的一批能力,内置 8 份人格行为不变:
- 中文 id:safeId 不再把中文删光(以前「若叶睦」→ default,面板读正文串台)
- selfName:人格可声明自己的名字,盖过全局「大肥鱼」(角色卡的身份就是名字)
- 安装上限 8000→20000、<20 字拒装、label 去 __persona- 尾巴
- 注入预算:config.persona.maxChars 默认 8000,按小节优先级裁剪,
demos 独立限 1500,超了标注「设定不全」 - 去 AI 味骨架:PERSONA_BASE 扩到约 600 字(antiAiFlavor + humanRhythm + notModerator)
- 编号小节解析:认「九、回复示例」这类标题,示例里群友/来者归一成用户、斜杠拆条
测试 934 全绿。配套:人格独立仓库 dsh-roost-personas 补了索引脚本 + CI。
v0.11.2:修工具注册——新宿主上所有工具都装不上
v0.11.2:修工具注册——新宿主上所有工具都装不上
新版 DSH 对裸 JSON Schema 有严格子集校验:type: 'object' 的节点
additionalProperties 必须显式布尔。我们的 6 处 output.schema 和
toJsonSchema() 产出的参数 schema 一处都没写,注册直接抛
unsupported JSON schema: schema.additionalProperties must be explicitly true or false
而注册器第一个失败就 return,面板上只看得到 memory_search 挂了,实际记忆、发言、
发文件、表情包这些工具全部没装上。
本机一直没暴露:import('@deepseek-ai/dsh-tools') 解析不到(打包在 app.asar 里),
走的是普通对象那条路;别人机器上 defineTool 解析成功就炸。
顺带把两处文档里残留的旧仓库地址、以及「老客户端兜底」这个不准确的说法改掉
(0.11.0 还能拉旧地址,≤0.10.x 会被 minVersion 挡住,但内置人格不受影响)。
测试 917 条全绿,新增 tool-schema.test.js(含审查器自测,防假绿)。
v0.11.1:能发文件了,人格源独立
v0.11.1:能发文件了,人格源独立
- feat(qq):qq_send_file 工具与被动回复 files 通道。发之前先问对面支不支持(老版本 ai-chat 直接拒,不做「发你了」的空头承诺);路径默认不限制,一轮一个,200MB 上限,>5MB 自动分片
- feat(persona):人格源改为独立仓库 dsh-roost-personas,personas/ 冻结为出厂默认快照,只供 npm seed 与老客户端兜底
- qq_send_file 明确不进 OUTPUT_TOOLS:那个常量在 planPersonaTools 里无条件并回去,加进去等于群友档也能外发任意文件
912 条测试全绿,人格索引校验通过。
v0.11.0:市场并入人格、接入页加组件版本
面板:市场并入人格
- 页签 10 → 9。删「市场」,
her改名「人格」,页内用「我的人格 / 人格仓库」分段——
两个页签都在列人格、都能点「用这个」,而「市场」里本来就只有人格。 - 「此刻情绪」搬到「主动搭话」(它只影响要不要开口、开口的语气);
「表情包」「语音条」搬到设置页。
接入页:组件版本
框架与 ai-chat 的「当前 → 最新」一目了然,有新版直接给做法:框架解压覆盖、插件去市场点更新。
框架最新版走 releases/latest 的 302 跳转(不占 API 限额),插件最新读插件仓库索引,
半小时缓存。查不到只写「查不到最新版」,不报错也不当成有更新。
扫码 / 填配置三处都补了一句:换账号要去 Roost 面板的「配置」里改,这个页面只建第一个。
修掉一个请求死循环
面板的加载 effect 依赖里带着 mem/chats/her,而它们每次拉取都会变成新对象——
setMem → 依赖变 → 再拉 → 再 setMem,自我驱动。实测「记忆」页 30 轮渲染打了
30 次 /memory + /graph + /closing,「会话」页 30 次 /chats;真实浏览器没有轮数上限。
顺带:判空改成按字段(!her?.stickers),接口失败一次后能重试,不再永久停在「读不到」;
同一份数据在飞时去重,切页不再连拉两次;切会话不再双拉记忆。
版本
0.10.22 → 0.11.0。tools: [] 这个安全默认是 0.11 才有的,人格索引的 minVersion
同步到 0.11.0——老版本(0.10.x)打开市场会提示先升级,这是有意的:
否则老客户端装市场人格会拿到全部工具。
测试 863 条。
v0.10.22 — 用量记账、四段耗时、主干开始拆分
用量面板:这个群到底花了多少 token
面板「上下文」页新增「05 用量」:近 7 天的计费 / 输出 token、缓存命中率,以及最费的三个会话。按「天 × 会话」记在 data/usage.json,只存数字不存内容。
不折算金额——订阅制或免费额度下算出来的美元数不是账单,显示出来只会误导。
几个容易错的地方都处理了:DSH 的 tokenUsage 是会话累计值,直接累加会指数膨胀;压缩后累计归零按全量记;读数落盘,重启不会重复计数;压缩关掉时也照样量(否则关压缩 = 统计静默失效)。
四段耗时:知道慢在哪一段
一条消息穿过四段,现在前三段都能看到:
| 段 | 从哪到哪 |
|---|---|
| 排队 | 内核收到事件 → 插件拿到 |
| 准备 | 插件准备 → 宿主开始处理 |
| 生成 | 宿主这一轮(模型 + 落盘) |
面板「最近一轮」显示 排队 3ms / 准备 12ms / 生成 1830ms。缺时间点显示 — 而不是 0,时钟回拨夹到 0。第四段(回发)在 ai-chat 那边,宿主看不到,不假装有。
主干开始拆(阶段 8 前两刀)
applyMemoryMarkers(190 行)和 journalAdd 各自成模块,依赖从参数进来。这两块以前零测试——因为闭包吃遍了 memory/journal/self/closing/mood,想测就得起整个宿主。现在补了 16 项行为测试,拆分前后逐行 diff 为 0。
v0.10.21 — 记忆能改口、每条消息有链路 id、面板鉴权只认 Peer
记忆能改口了
新增 <<replace: 旧说法 | 新说法>>:同槽位取代只认字面相似(0.75),「住在杭州」→「搬到上海」这种它抓不到。现在模型可以说清楚在纠正哪句,宿主按片段定位旧事实并打 until(不删,面板还看得见历史)。命中 0 条或多条都不猜。
每条消息有链路 id
进站生成 traceId,每轮日志前缀 [trace:xxx],lastTurn 和面板概览都能看到。用户说「刚才那条没回」,拿这个 id 就能定位那次请求。
面板鉴权只认宿主 Peer
/quick 删掉了 body.actor 判权——那是调用方自己填的,写 {bucket:owner} 就能自称主人。现在只认 fence() 从 connection.admit() 拿到的 Peer。写操作落 data/audit.log(字段白名单,不记参数内容)。
标记不再漏到群里
全角冒号(<<remember:x>>)、未闭合标记、下游标记(image/voice/button/say/silence)三种漏法都修了;applyMemoryMarkers 补了 15 项测试。
记忆工具重名要说清楚
注册前先读 registry.schemas(),命中同名工具直接返回「工具名被占用:memory_search」,不再等 register 抛一句含糊的 already registered。
v0.10.20:端口不再单向漂移,配置保存不再静默失败
这一版修的是「面板上说得好好的,实际没生效」那一类毛病。两个问题都是从代码里查出来、再用真实配置复现过的。
一、17321 老是被占?其实是它从来没被尝试过
面板长期挂着「17322(配置 17321,被占了)」,可 Get-NetTCPConnection -LocalPort 17321 一条记录都没有,net 直接 bind 成功。
问题出在候选端口的顺序:candidates(want, lastActual) 把上次实际用的排在配置值前面。于是只要历史上因为任何原因(旧实例没退干净、手工改过配置)漂到过 17322,之后每次启动都直接绑它并成功,17321 一次都不会被尝试 —— 而且只会一路往后爬,17323、17324……
面板那句话也是自己脑补的:它只比较 wantPort !== actualPort 就写死「被占了」,既没问过系统,也没问过服务端到底撞没撞 EADDRINUSE。
改成 want 优先、lastActual 第二:配置端口空着就回它,真被占才退到上次那个,书签照样有效。面板改说「已顺延」,不再替用户断言原因。
已经漂走的:重启 DSH 就会绑回 17321,ai-chat 的地址由 syncAdapterPort 自动同步,不用手改。
二、「配置全文」的五个静默失败
设置页那个直接编辑 JSON 的框,承诺是「改什么就是什么」,实际有五处对不上。
- 删掉的字段删不掉。
saveConfig是merge(loadConfig(), next),只遍历你提交的 key —— 没提交的从磁盘旧值补回来。实测删掉memory.deepEvery和整个workspace段,保存后原样还在。现在基线换成 DEFAULTS,删掉 = 回落默认值。 tokenSet污染配置文件。它是publicConfig为了让面板知道「密钥设过没有」加的展示字段,但面板从来没读过它,而它会被原样提交回来写进config.json,并且写进去就删不掉(merge会从旧值补回)。现在publicConfig先裁剪再打码。- 提示存好了,磁盘没改。
JSON.parse('null' | '[]' | '"abc"' | '123')都能过 parse,而merge见到非对象直接返回原配置,界面照样弹「存好了」。现在前后端都拦,返回 400 并说清该传什么形状。 - 端口填错也能存。填
"abc"保存成功,然后listen('abc')抛 EACCES,而顺延逻辑只认 EADDRINUSE —— 服务直接起不来,面板只有一句permission denied abc。现在校验 1–65535,并说清范围。 - 服务端的警告被吞。端口换了但 ai-chat 没同步时,服务端把原因放在 message 里,面板成功分支却固定显示「存好了(下一轮生效)」。现在透传服务端 message。
三、顺手修的
- 只有端口或密钥变了才重启监听。以前无条件
startServer():改一个记忆条数也会关掉再重建收消息服务,那几百毫秒里 ai-chat 推过来就是 connection refused,端口还会重新 bind 一次。 - 保存后回填 draft。服务端会做归一化(补默认值、删展示字段、升级旧默认值),不回填的话你看到的是一份并不存在的配置,下次改别的字段就基于它继续改。
- 分节说明补齐
closing/qqTools/update三段;测试也加了反向断言 —— 以前只查「前端列的 ⊂ 后端有」,后端新加的段漏列永远抓不到。 - 段名点击改成直接定位选中。原来提示「在下面的配置全文里搜」,但浏览器的页内查找不搜 textarea 的内容,按提示去搜根本搜不到。
四、一个差点带出去的回归
saveConfig 基线换成 DEFAULTS 之后,/config 里那句 if (token === '******') delete body.server.token 就变成了主动提交「没有 token」 —— 真密钥会被默认空串顶掉。以前基线是磁盘配置,delete 之后 merge 会保留真值,所以那句话在当时是对的。
实测确认会被冲掉,改成显式回填,并加测试钉死。没有跑测试就交付的话,保存一次配置密钥就没了。
五、验证
测试 667 → 675。三次变异验证都抓到回归:基线换回 loadConfig、去掉裁剪、密钥改回 delete,三次都红。
端到端用真实配置的副本跑过:面板看不到 tokenSet、磁盘上的清掉了、其余字段逐项一致、删段回落默认、persona.byChat 与数组都保住。
pruneTo 有两个必须的特殊规则,是踩过一次才写对的:空对象当开放字典(persona.byChat 的键是群 openid,按形状裁会清空用户的按群人格配置)、数组整体保留。
升级
npm i dsh-roost@0.10.20
改的是插件子模块,必须重启 DSH 进程才生效(重载无效)。
v0.10.19:记忆可以按会话关掉
这一版给「某个群别带记忆」一个正经开关,顺带修一个只在点下去才炸的错。
一、记忆可以按会话关掉了
全局的 memory.enabled 一关就是全关,可真实场景往往是「就这个群别带记忆」——群里有人要求别记,或者不想让她把别处的梗带进来。现在能按会话关,别处照常。
默认空数组,不装不配就是原来的行为。
三条语义(照 dsh-pure-mode 的做法):
- 写入也停。只挡注入的话,关着时说的话第二天还会从日记里被检索出来,等于没关。
- 不动已有记忆。关掉不等于清空,关回来还在;要清是另一个动作(「清空本会话记忆」),两个按钮分开放——误点代价差太远。
- 整轮一致。注入、检索、写日记、记忆工具全按同一个判断走。
实现上收口成一个 memoryOff(chatId),替掉了 5 处分散的判断(写日记 / 静态注入 / 动态注入 / 检索 / 记忆工具注册),和全局开关取「或」。分散着写必然漏一处——漏在写日记上是「关着还照记」,漏在注入上是「关了还照说」,两种在面板上都看不出来。
面板记忆页加了一个按钮,状态直接写在旁边,不用猜现在是开还是关。
二、修一个只在运行时才炸的错
第一版做上面那个开关时,quick 处理器里写了 setCfg(...)。而 setCfg 只是传给 createSetup() 的一个属性,不在 apply 的闭包里 —— 运行时 ReferenceError: setCfg is not defined,还被外层 try/catch 吞成一句 toast,界面上看到的是「操作失败」,不是代码错。
改成直接 cfg = saveConfig(...)。
为什么两道检查都没拦住:node --check 只查语法,查不出未定义标识符;dry-run.mjs 只跑 apply()、不跑 handler,处理器里的代码没人调用就永远不执行。这类错只有真点一下按钮才暴露。
补了一条测试:用假 ctx 捕获 r.handler,按面板的真实调用方式(回环 Host + JSON body + async 迭代器)真的调一遍 /quick,断言 200、muted 值、以及盘上 config.json 里的 offChats。做了变异验证——把 setCfg(...) 换回去,这条用例失败;换回修复,通过。
升级
- 不需要迁移,默认关闭,老库直接读。
- 改的是插件子模块,要重启 DSH 才生效,重载无效。
- 测试 663 → 667。
v0.10.18:记忆注入按分数取,facts 加双时态
这一版修两个「记住了却用不上」的问题,顺手删掉一个从没接线的死方法。
一、注入按分数,不再按位置
淘汰一直按「权重 × 时间衰减」算分,但注入是按数组位置取的(slice(-6),取最近记的 6 条)。两套口径的后果很具体:
一条「主人对花生过敏」被提过五次(w=5),只要最近没人再提,它就永久沉在数组前段 —— 平时那 6 个名额永远轮不到它,只有每 15 轮的全量重注才可能露一次。权重于是只服务于「该不该被淘汰」,完全没服务于「记得牢」。
现在注入走 listRanked():按分数从高到低、同分新的优先,排序键和淘汰那侧完全一致。全量重注也按同一顺序 —— 万一预算不够要砍,砍掉的是分数低的尾巴,而不是最新的那几条(后者恰恰是最想铺回来的)。
老库不用迁移:没有元数据的事实按 w=1、不衰减算,行为跟以前一致。
二、facts 有了双时态
「主人住在杭州」和「主人住在杭州市」以前会同时留在库里、同时注入,白占两个名额,还得让模型自己挑一条。
meta 新增 until:新说法顶掉旧说法时,旧的不删,只打时间戳(和关系图的双时态一个路子)。注入只取活的,面板还看得见历史。没有 until 的老数据一律当活的,降级也安全。
判定用字符二元组 Jaccard ≥ 0.75,纯本地、零成本、当场生效:
| 例子 | 相似度 | 结果 |
|---|---|---|
| 主人住在杭州 → 主人住在杭州市 | 0.83 | 取代 |
| 主人对花生过敏 → …过敏,严重 | 0.75 | 取代 |
| 主人养了猫 vs 主人养了狗 | 0.43 | 两条都留 |
| 主人在杭州工作 vs 主人住在杭州 | 0.50 | 两条都留 |
阈值取保守值:宁可漏判,不可误判。漏了只是两条都留着让模型挑;误了是把不同的事当成改口抹掉。
已知边界:这套抓不到真正的语义改口 —— 「搬到上海」和「住在杭州」的字面相似度只有 0.11,不会触发。那类要模型判定(巩固阶段本来就在调模型),这一版没做。
配套两个决定:历史分数 ×0.5(不注入但仍占名额,打折让它先被淘汰);重提一条历史说法 = 重新确认,until 清掉复活。
三、删掉 memory.touch()
它写着「注入进提示词、这一轮确实回了话就 +0.5 权重」,但从写下来那天起就没有任何生产调用点,只有测试调过。注释比没有这个方法更误导。
而且它的语义现在是错的:配上按分数注入,「被注入就加分」就是注入即加冕 —— 越常被注入的越容易被注入,列表冻结,再也换不出一批新的。同一个坑在日记那边踩过并锁死了(accessed 只由检索写入),facts 走同一条结论。
留了 tombstone 注释说明不许加回来。
四、面板能看见历史
被取代的说法不注入,但仍占着 40 个名额。面板上不露出来就成了「看不见也删不掉」,只能等它自然被淘汰。记忆页新增默认折叠的「历史」区:删除线 + 「X 天前起不再用」+ 删按钮。
升级
- 不需要迁移,老库直接读。
- 改的是插件子模块,要重启 DSH 才生效,重载无效。
- 测试 659 → 663。