Skip to content

Releases: tdcq9594/dsh-roost

v0.12.1 — 语音合成选装

Choose a tag to compare

@tdcq9594 tdcq9594 released this 03 Nov 16:36

语音合成(选装,默认关)

设置页新增「语音合成」卡片,三种接法:

  • 本地模型(推荐):点「一键安装」自动下 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:角色卡人格能装了

Choose a tag to compare

@tdcq9594 tdcq9594 released this 02 Nov 15:01

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:修工具注册——新宿主上所有工具都装不上

Choose a tag to compare

@tdcq9594 tdcq9594 released this 02 Nov 14:21

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:能发文件了,人格源独立

Choose a tag to compare

@tdcq9594 tdcq9594 released this 02 Nov 13:55

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:市场并入人格、接入页加组件版本

Choose a tag to compare

@tdcq9594 tdcq9594 released this 02 Nov 03:47

面板:市场并入人格

  • 页签 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 — 用量记账、四段耗时、主干开始拆分

Choose a tag to compare

@tdcq9594 tdcq9594 released this 31 Oct 17:25

用量面板:这个群到底花了多少 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

Choose a tag to compare

@tdcq9594 tdcq9594 released this 31 Oct 15:57

记忆能改口了

新增 <<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:端口不再单向漂移,配置保存不再静默失败

Choose a tag to compare

@tdcq9594 tdcq9594 released this 30 Oct 14:29

这一版修的是「面板上说得好好的,实际没生效」那一类毛病。两个问题都是从代码里查出来、再用真实配置复现过的。

一、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:记忆可以按会话关掉

Choose a tag to compare

@tdcq9594 tdcq9594 released this 30 Oct 01:18

这一版给「某个群别带记忆」一个正经开关,顺带修一个只在点下去才炸的错。

一、记忆可以按会话关掉了

全局的 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 加双时态

Choose a tag to compare

@tdcq9594 tdcq9594 released this 30 Oct 01:01

这一版修两个「记住了却用不上」的问题,顺手删掉一个从没接线的死方法。

一、注入按分数,不再按位置

淘汰一直按「权重 × 时间衰减」算分,但注入是按数组位置取的(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。