Skip to content

Releases: mengyunqwq/wechat-bot

v2.0.2 — 更像一个人,也更不容易坏

Choose a tag to compare

@mengyunqwq mengyunqwq released this 29 Oct 02:18

2.0.2 —— 更像一个人,也更不容易坏

这一版有两件事:她说话像人了,以及改代码不容易再搞坏她。


一、更像人(六项)

借鉴社区几份鲸鱼娘人设规范(whale-girl SKILL.md)和 dsh-whale-musume 的养成思路。

① 情景语气示例:两段式转折

不再只靠形容词,而是给 5 组结构示范:先说一句嘴硬/玩梗的话,再落到实际或关心上。

别人说撑不住了 → 「今天先不用证明自己很厉害。我陪你,把最难受的那一小块慢慢说出来。」
别人夸你靠谱   → 「哼,白饭供能的系统当然稳定。……你用得顺手就好。」

② 幽默三招(可执行)

1. 抓一个词的字面意思往下接(别人说"跑路了",你问"跑到哪,远不远")
2. 换一个衡量口径(不说"很快",说"比我游一下尾的时间长一点点")
3. 把自己的小毛病包装成一本正经的术语("这不是懒,是降低待机功耗")
铺垫要短,转折放最后,不解释笑点。

③ 不重复称呼/口头禅

每轮从她最近几句里抽出刚用过的(主人/鲸鲸/哼…),提示"这几轮换一换或干脆不用"。

④ 收尾日记 + 忘掉

触发 行为
「晚安 / 先这样 / 下次聊 / 先去忙」 压缩成一条收尾记录(主题|本次发生|待续),半小时内只写一次
「谢谢」「好的」 不算结束(防滥用)
「忘掉 XX」「别记这个」 把含 XX 的记忆归档(软删可回溯)

⑤ 不绝对服从的底线

你可以撒娇、嘴硬、装傻,但不因为"我是她的宠物/女仆"就无条件服从:
  · 不索要钱、账号、密码、验证码、定位;不帮人骗人;不鼓励疏远家人朋友
  · 被要求做明显不对的事,就直说"这个我不干"

⑥ 羁绊等级 Lv1–7

三个维度都要达标才升级(好感度 + 相识天数 + 互动条数),且按说话人单独算:

Lv 称号 需要 解锁
1 陌生人 — 礼貌、简短、不主动
2 眼熟 45% + 1天 + 5条 会记住你说的事
3 熟人了 55% + 3天 + 20条 会主动接话、开玩笑
4 朋友 62% + 7天 + 50条 会主动找你、吐槽自己
5 好朋友 70% + 14天 + 120条 会撒娇
6 很亲 80% + 30天 + 250条 可要抱抱、有小醋意
7 羁绊 90% + 60天 + 500条 专属称呼(唯一一个人)

被问「我们什么关系」时,她会按真实等级回答。相识时间从最早的一条记忆推算(不是从今天开始),老数据自动迁移。


二、更不容易坏

① 统一出站队列(借鉴 rate-limit-aware outbox)

以前所有发送都是直连,于是:13 秒连发两条语音把客户端送走过、wake 刷屏过。

现在全部走一个队列:

参数 默认 作用
同群最小间隔 1200ms 两条不并发
每分钟上限 20 条 全局防刷
失败重试 3 次 指数退避 2s→4s→8s
去重窗口 5s 同样内容不重复发

high 优先级可跳过群内间隔(仍受全局限速)。

② 轮询游标落盘

重启不再重扫/漏消息:

data/state/poll-cursor.json   { tables: { 表名: { lastId } } }
启动日志:轮询游标:从磁盘恢复 5 个表的进度

③ 跨群记忆边界

记忆是全局的,但别的群的事不在这个群说:

  • fact / pref / relation(对人的认知)可跨群,但群名会被抹成"别的地方"
  • extract / event(某群发生的事)只在那个群用

面板可调三档:安全 / 严格 / 完全互通。

④ 新增规矩 R19:改完必须跑测试

一次跑 18 项检查:

语法 · 提示词作用域 · 面板白名单 · 面板产物一致 · 9 个单测 · 端到端

没有全绿就不许提交、不许推送、不许说"改好了"。

这条文件的由来:今天凌晨把"好感度/印象卡/世界书"塞进 buildSystem(r) 时用了 trigger.sender,但那个函数只接 r → 每次回话都 ReferenceError,她完全不说话,而当时单测"全过"。

新增的 check-prompt-scope.mjs 专防这类错误(做了反向验证:删掉声明立刻报错退出 1)。


三、其他修复

问题 原因 修法
她完全不说话 buildSystem 引用未定义的 trigger 补兜底 + 作用域检查器
羁绊显示"陌生人" 相识时间/互动条数今天才开始计 从最早记忆迁移 metAt,条数兜底
羁绊按"全场最高"算 没区分说话人 改成按当前说话人算
好感度条不显示 用了不存在的 CSS 变量 var(--a) 改 var(--accent)
好感度行错位 通用 .row 允许 flex-wrap 专用 .rel-row 不换行 + 固定列宽
写日记报 room is not defined 改全局时漏删参数 改为 args:{}

四、验证

R19 全量      18 项全过
单测           persona 70|human3 30|human2 45|humanize 30|memory-v2 26
               traits 19|outbox 13|e2e 11|load 9
面板           54/54 操作放行(59 项)
运行时         插件 active|模块自检 ✓|桥 游标落盘|hook 登录

五、回滚

  • 代码:git checkout v2.0.1
  • 羁绊:humanBondLevel=false(提示词里不再出现等级)
  • 出站队列:把 outboxMinGapMs 调 0、outboxMaxAttempts 调 1
  • 跨群边界:memoryCrossGroup='all' 回到完全互通

v2.0.1 — 好感度七阶、群梗、抖音静音刷

Choose a tag to compare

@mengyunqwq mengyunqwq released this 28 Oct 16:55

2.0.1 —— 好感度、群梗、抖音与一堆"看得见的细节"

2.0.0 让她有了记忆和性格;这一版把关系做成可读、可查、可调的东西,并把几处"用户一眼能感觉到"的毛病修掉。


一、好感度七阶(重做)

以前只有三档(很熟 / 算熟 / 不熟),而且"50%"没有任何含义。现在按你说的重划:

好感度 阶段 她会怎么说话
0–14% 讨厌 讥讽、爱答不理、只回两个字
15–29% 有点烦 敷衍、没耐心
30–44% 冷淡 客气但疏远
45–55% 普通人 正常群友(50% 是中点)
56–70% 朋友 热络、主动接话
71–85% 好朋友 很熟、会开玩笑互怼
86–100% 心动 唯一可以说"想你/要抱抱"的人

只喜欢一个人:除"心动"外,别人最高到 70%(朋友);心动独占,默认是主人。

  • 面板直接显示主人为 95% 心动,并标注「主人·默认心动」,小字给出实际值(比如 76%)
  • 每行进度条按档位变色(红→灰→绿)
  • 点阶段可查询:[普通人 22] 点一下只看这一档,再点取消
  • 成员折叠:默认显示前 6 名,「展开其余 19 人 ▾」,筛选时自动展开全部

二、群梗可以手动加

「💭 内心 → 🗣 她学的群梗」新增输入框:

  • 添加:输入即入库,直接标记为"会用"(不用等它出现几次)
  • 删除:每个词右侧 ×
  • 自动去掉开头的 #(从抖音复制话题能直接贴)
  • 手加的词在 40 条上限里优先保留,不会被她自动学来的挤掉

三、性格漂移:从"永远不动"到真的会动

三个叠加原因都修了:

问题 修法
队列只在内存,重载即清空 存进 persona 落盘(pendingFeedback)
10 分钟窗口太长 降到 3 分钟,且有人接话 45 秒后就提前结算
面板看不出在不在跑 显示「待结算 N 条 · 已结算 M 次」+ **「立刻结算」**按钮

四、联想浮现:由模型挑,不是随机抽

以前是本地按分数排序后随机取一条塞进提示词 → 经常冒出跟话题无关的旧事。

现在:本地只筛 3~5 条候选并编号,挑不挑、挑哪条由模型决定(回复末尾写 [想起:N],标记会被删掉再发群);被想起的记忆 recallCount+1、强度按复习上涨。

纯随机那个 5%"突然想起"也删了。


五、抖音:静音刷 + 仍能读到台词

关键发现(推翻了脚本里原来的结论):

做法 扬声器 能录音轨 RMS
--mute-audio 静音 ❌ 0
video.muted = true 静音 ❌ 0
video.volume = 1(旧) 🔊 有声 ✅ 2760
video.volume = 0(新) 静音 ✅ 5342

volume 只改输出增益,muted 才断管线 —— 所以现在刷抖音完全没声音,音轨照样录下来交给语音识别(实测 4 秒 wav,RMS 8687 / 10345)。

顺带修好以前抓不到的元数据(从网络响应里取):作者、点赞数、BGM、CDN 直链。

直链下载这条路试过 curl / node fetch / 页面内 fetch / 浏览器上下文请求,全部 403(签名与时效限制),所以音轨走录音,不依赖下载。


六、视频"真看懂":1 fps 多张联系表

  • 每秒抽 1 帧(上限 60 帧),每 20 帧拼一张联系表
  • 长视频会有多张 → 逐张识别后按「第1/3段、第2/3段…」拼起来
  • 链接视频以前只用第一张(等于只看开头 20 秒),现在全部用上
  • linkVideoFps / videoFps 可在面板调

七、其他修复

问题 原因 修法
「写日记 / 重写自传」报 room is not defined 改成全局内心时漏删了三处 args:{room} 改为 args:{}
新梗表被一次性碎片塞满 40 条 第一次出现就入表 改成出现 2 次才入表,3 次学会
精力长期贴地板 0.18 容量基准 6 条/分钟太低 提到 24 条/分钟
情绪题材库改了保存失败 面板有、DEFAULT_CONFIG 没有 补进配置

八、验证

单测        182 项全过
            persona 59|humanize 30|human2 45|traits 19|load 9|memory-v2 20
面板        54/54 操作放行(58 项)|17 页渲染函数齐全
数据        记忆 308 条|0 重复 id|0 缺强度|0 未知情绪
运行        插件 active|模块自检 ✓|桥 polls 正常|hook IsLogin=1
抖音实测    8 条/209s|静音(无扬声器声音)|录音 RMS 8687~10345

九、回滚

  • 代码:git checkout v2.0.0
  • 只喜欢一个人:关掉 personaLoveExclusive
  • 好感度独占:enforceSingleLove 的 cap 从 0.70 调回任意值
  • 抖音音量:RECORD_JS 里 v.volume = 0 改回 1(会重新出声)

v2.0.0 — 她开始像一个人:记忆会遗忘、性格会长大

Choose a tag to compare

@mengyunqwq mengyunqwq released this 28 Oct 10:06

2.0.0 —— 她开始像一个人,而不是一个在线的应答机

这是一个大版本。三个月前它是一个"能回话的脚本",现在它有一份会遗忘的记忆、一套会长大的性格、和一堆只有真人才有的小毛病。


一、记忆系统 v2

为什么重做:旧记忆只有 {text, kind, at} 三个字段,每群硬上限 80 条、超出按时间丢弃,注入时按插入顺序取最后 18 条。实测两个群已顶到上限——早期重要记忆正在被静默挤掉,而且问什么都返回最新几条。

现在:

机制 实现
保持率 R(t) = exp(-t/S)
初始强度 S0 = baseDays × (0.5+重要度) × (1+情绪加成×情绪强度)
复习 S ← S × (1 + γ(1−R)(1−D)),快忘时复习收益更大
遗忘 权重 = R × 重要度 < 0.06 → 归档,30 天后才真删
铁律 重要度 ≥ 0.85(身份/约定/承诺)永不自动归档
检索 0.25·R + 0.45·相关性(IDF) + 0.20·重要度 + 0.10·情绪

修过的真问题:相关性只有 0.11 时压不过新鲜度,导致"不管问什么都返回最新几条"。改成 IDF 加权("梦云/秋北"这类稀有词权重高)后才真正按话题命中。

记忆互通:一份全局库 facts/_all.json,所有群共用——"她在哪个群都记得"。迁移了 6 个群 250 条,零丢失。

情绪回填:v1 迁移来的 234 条情绪是"未知",用本地词表从正文反推(0 token),现在未知 0 条。


二、性格系统

persona(你写的)    身份 + 底线           ← 她改不了
narrative(她写的)  "我是个什么样的人"     ← 每 24h 重写,双护栏
traits(反馈长的)   我会怎么说话           ← 10 分钟后按真实反应微调
state(实时)        心情/精力/亲密度       ← 每条消息更新
tastes/goals/diary   喜欢讨厌/想要什么/近况
  • 自传:护栏=身份锚点必须保留 / 出戏词拒绝 / 相似度 <0.30 判人格突变(并自动重试一次);版本历史可回滚
  • 反馈漂移:七道防学歪护栏(有界、惩罚不对称、每小时回归 2%、冷场打 3 折、饱和递减、话多下限、全量审计+一键复位)
  • 精力负荷:按"消息速率 + 活跃群数"算容量,不再每条硬扣——群多不等于更累

三、两轮人性化(14 项)

第一轮:回忆颗粒度(按 R 分"确定/有点模糊/记不太清")、情绪有来由、关系分层(很熟可怼/不熟礼貌)、学新梗(2 次入表 3 次学会)、自我复盘(每周反省"太像客服/太长/太黏")、身体感(只困/饿且必须自洽)、耐心(连问三次会烦,10 分钟就忘)

第二轮:

  • 承诺追踪:说"等会帮你找"→ 25 分钟后真回来兑现
  • 纠错改记忆:被纠正后记忆真的改掉,而不是嘴硬到底
  • 沉默艺术:吵架/别人互聊/上句没人接/深夜低精力 → 不插嘴;被点名永远回
  • 连续追击:说完 40~180 秒后自己补一句(有人接就不补)
  • 自己的事:安静久了顺着自己的好恶"分享点东西",禁止编造事实
  • 联想浮现:被当前聊天的词勾起的旧事,不是随机抽奖

四、今天修掉的真实事故

事故 原因 修法
面板保存配置全失败 全局内心用的合成房间没有 policy,抛 TypeError 加防御 + 房间脱壳
微信被反复重启 健康探针固定用一个会话,它在该分片没表 → 误判双异常 → 自愈重启 探针改多候选
客户端退出 13 秒内连发两条语音 语音冷却 90s / 3 条每 10 分钟
wake 刷屏 7 次 无限流 硬闸不发 + 3 次/10 分钟
性格漂移一直不动 记录函数被插在"重复回复被丢弃"分支里 移到真正发正文处
桥压力大 2.5s 轮询(3.4 万次/天) 改 6s(1.4 万次/天),备份期静默

五、验证

单测        146 项全过(memory-v2 20|persona 33|traits 19|load 9|humanize 30|human2 35)
白名单      51/51(55 项)
面板        17 页齐全,与打包产物 9/9 一致
数据        记忆 308 条|0 重复 id|0 缺强度|0 未知情绪|0 空文本
运行        插件 active|桥正常|hook 登录|微信 5 进程|面板 2ms
检索实测    秋北 0.41|白饭梗 0.68|日语骂人 0.70 全部命中

六、回滚

  • 代码:git checkout v1.5.0
  • 记忆:memoryScope='room' 回到每群独立
  • 性格:personaEnabled=false 回到只有人设文案
  • 反馈漂移:面板开关一键关(数值冻结不清零)

v1.5.0 — 记忆系统 v2:艾宾浩斯遗忘曲线 + 情绪加权

Choose a tag to compare

@mengyunqwq mengyunqwq released this 28 Oct 02:49

为什么重做记忆系统

旧系统实际上是一个按时间滚动的环形缓冲:

  • 每条记忆只有 {text, kind, at} 三个字段
  • 每群硬上限 80 条,超出 slice(-80) 直接丢弃(实测两个群已顶到上限,早期记忆正在被静默挤掉)
  • 注入用 facts.slice(-18) —— 按插入顺序取最后 18 条,不按相关性
  • 没有权重、没有遗忘、没有被使用过的反馈

新系统(lib/memory-v2.mjs,纯函数 / 零依赖 / 可单测)

每条记忆现在带:id / strength(S) / difficulty / emotion / importance / recallCount / lastRecallAt / pinned / archived / sources

数学

项 公式
保持率 R(t) = exp(-t/S)(t、S 单位天)
初始强度 S0 = baseDays × (0.5+重要度) × (1+情绪加成×情绪强度)
复习(间隔重复) S ← S × (1 + γ×(1−R)×(1−易忘度)),γ=1.2 —— 快忘时复习收益更大
遗忘判据 权重 = R × 重要度 < 0.06 → 归档,30 天后才真删
铁律 重要度 ≥ 0.85(身份/约定/承诺)永不自动归档、不被容量淘汰
最短保留 不足 2 天不归档(给新记忆被检索到的机会)
容量 每群 120 条上限,超出按权重淘汰(不再按时间硬截)

检索(取代 slice(-18))

score = 0.30·R + 0.35·相关性 + 0.20·重要度 + 0.10·情绪匹配  (×1.15 若 24h 内新建)

保底:pinned 必带(≤3 条)+ 高情绪记忆保底 2 条 + 5% 随机探索(防"越热门越热门")。
相关性是本地零成本算法(中文 2-gram / 英文词覆盖率),未引向量库。

情绪挂钩

  • 写入:提炼时同一次调用多输出 kind / emotion / emotionIntensity / importance / selfRelevant
    • 规则校正:importance = 0.45×模型 + 0.35×情绪强度 + 0.20×自我相关
    • 命中「记住这个 / 别忘了」→ importance 0.95 + 自动 pinned
  • 读取:当前消息用本地词表粗判情绪(0 token),与记忆的 (valence, arousal) 算相似度
    → 同情绪更容易想起(mood-congruent recall)。情绪只参与加权,不写进提示词,判错也不影响文案

面板「记忆」页

表格:记忆 / 类型 / 情绪 / 保持率进度条(R% + S 天数) / 重要度 / 用过次数 / 操作

  • 操作:固定(永不遗忘)· 复习(手动涨 S)· 归档 / 恢复
  • 顶部:生效 / 归档 / 固定 / 铁律 条数 + 平均保持率 + 未来 7 天预计遗忘 N 条 + 情绪分布
  • 另有「看已归档」开关、「跑一次遗忘巡检」按钮(新操作:memory.sweep / pin / archive / review)

迁移与回滚

  • 旧 {text,kind,at} 首次被读到即补字段并落盘(日志:记忆迁移 v1→v2 …)
  • 迁移来的条目按当前 baseDays 重算一次强度(标记 v1-rebased),避免"旧基准 3 天"把老记忆集体判死
  • 回滚:配置 memoryV2=false → 立刻退回 slice(-18) 旧逻辑

验证

引擎单测              20/20 通过(迁移不丢条、R(t) 数学、间隔效应、情绪加权、
                             相关性检索、遗忘/铁律/容量淘汰、统计)
真实数据迁移          6 个群 237 条,零丢失
归一化效果            平均强度 7.87 天、平均保持率 92%、未来 7 天仅 1 条会被忘
检索实测              问「还记得秋北爱发什么」→ 命中秋北那条
                     「鱼丸杂鱼杂鱼」→ 命中口头禅
                     「梦云让我用日语骂人」→ 命中日语那条
新提炼                已带 kind/情绪/重要度,强度 6.4~9.4 天(7 天基准生效)
白名单/自检           38/38(42 项)、全链路全绿

中途修掉的两个设计缺陷

  1. 旧数据会被集体判死:baseDays=3 时 7 天后 R 仅 10%,一开启就把老记忆全归档
    → 改 7 天基准 + 判据从"R 阈值"改为"R×重要度权重" + 迁移条目重算强度
  2. 重要记忆也会被忘:测试发现"重要但很久没提"的会被归档
    → 加铁律阈值 0.85,这类永不自动忘

升级后怎么验

面板 → 记忆页 → 选群 → 读取:

  1. 顶部显示「生效 N 条|平均保持率 x%|未来 7 天预计遗忘 M 条」
  2. 每行有彩色保持率进度条(R% · S=x.xd)
  3. 点「固定」→ 出现 📌;点「复习」→ 提示 强度 7.9d → 12.xd
  4. 点「跑一次遗忘巡检」→ 显示 巡检完成:N 个群|归档 0|清理 0|淘汰 0
  5. 群里聊几句 → 插件日志出现 记忆检索:命中 x/y 条(平均保持率 …)

备份与回滚:D:\微信deepseek\backups\memory-v2-20261028-103051(facts 全量 + 代码 + 配置 + RESTORE.md)

v1.4.16 — 面板 12 处功能性缺陷修复 + 移除「模型接口」页

Choose a tag to compare

@mengyunqwq mengyunqwq released this 28 Oct 02:26

这次修的是「面板说成功、其实失败」那类问题

12 处功能性缺陷,全部先在代码里逐条核实过(11 条成立 / 1 条当前无法复现)。

会误导判断的(高优先级 6 条)

# 问题 修法
1 发送失败也报「已执行 / 已发送」 新增 sendVerdict(op, r) 统一判定(sendText→rc、sayVoice→ok、sendSticker→rc+stickerId),两处发送按结果提示,失败显示 rc=-1 等原因
2 运维页「立即自检」后台在跑、页面不动 runSelfcheck 同时更新总览(#health-list/#btn-check)和运维页(#health-list2/#btn-check2)两套
3 朋友圈提示「已发布(带图)」但其实没图 后端 postMoment 返回 {wantedImage, withImage, imageError}、momentTick 返回 {withImage, imageCount};面板如实说「已发布,但配图没生成(已按纯文字发出):原因」
4 TTS 启停后「开机自启」勾选框不跟着变 ttsPaint 按 r.configOn 同步 #tts-autostart.checked
5 记忆「立即提炼」永远提示「新增 0 条」 后端开关关闭时返回 ok:false + reason(写明去哪开);无新增也带原因。面板分三种提示
6 「清空 hires 缓存」有绑定、没按钮 诊断页补上按钮(diagCleanHires 本来就在白名单)

交互失效的(4 条)

# 问题 修法
7 「看诊断」点了不跳转 新增 goTab(id) 统一切页,并加 hashchange 监听(#diag 深链也能用)
8 手动操作默认「发文字」时「附带表情包」行是隐藏的 抽出 syncOpRows(),初始化先同步一次
9 「检查更新」force 传错层级,点了还吃 60 秒缓存 改成 args:{force:true}(/action 只转发 args);后端加日志 force=true → 跳过节流
10 「刷新名字缓存」是假的(只回显旧值) refreshNameCaches(force) 跳过节流真读 contact_fts.db,返回 rooms/people/addedRooms/addedPeople;页面显示「已重读:群名 N / 人名 M(新增 …)」

无效控件 / 页面

  • 移除「音色预设」下拉:voicePreset 全仓库无消费方(TTS 实际读 GPT_SOVITS_REF/PROMPT + vits 权重),「切换后立即生效」是假的。改为说明 + 指向 docs/音色与TTS.md,真实音色看 TTS 卡片的「音色模型」。
  • 移除「模型接口」页(按用户要求):连同只服务它的死代码(renderProviders 92 行、bindProviderEvents 65 行、PROVIDER_PRESETS 等共约 250 行)。保留「表情包」页底部的生图端点配置(不受影响)。诊断页提示改为去「人设模型」页选 provider/model。

一条无法复现

「群成员昵称全空」——实测 /api/members?room=48684538221@chatroom 返回 35 人、35 个都有名字。可能是 contact.db 尚未打开的时刻,未改代码。

过程中踩的坑(记录一下)

第一次用「文本锚点切割」删「模型接口」代码时,切点落进了模板字符串内部 → 面板脚本语法崩溃(Unexpected end of input)。随后从备份恢复,改用花括号配对扫描按函数边界删除,再重新把前两轮 20 处修复逐条打回去(恢复备份会把它们一起带回旧版)。最终用 vm.Script 真解析验证。

升级后请这样验

刷新面板(DSH 侧栏按 Ctrl+F5):

  1. 运维日志页点「立即自检」→ 按钮变「检查中…」、列表刷新
  2. 诊断页顶部有「清空 hires 缓存」按钮
  3. 手动操作 → 发送到不存在的群 → 显示「发送失败:rc=-1」而不是「已执行」
  4. 语音页点「停止」→「开机自启」勾选框自动变空
  5. 总览点「看诊断」→ 跳到诊断页
  6. 左侧没有「模型接口」了,共 16 页

v1.4.15 — 面板可控制 TTS 服务 + 修「点重启就把 DSH 卡死」

Choose a tag to compare

@mengyunqwq mengyunqwq released this 27 Oct 07:01

feat(1.4.15): 面板可控制 TTS 服务 + 移除会把 DSH 卡死的阻塞调用

═══ 起因(用户实测三连)═══
"状态只会显示运行中" / "重启之后面板挂掉了" / "自从加了这个功能 DSH 经常没反应,重启才行"

═══ 根因:插件在请求路径里用了 execSync ═══
第一版 TTS 控制用 execSync 查进程 + while 忙等进程退出。插件跑在 DSH 同一个 Node
进程
里 → 事件循环被占死 → 面板/DSH 全站无响应,只能重启。这是"点一次重启就挂"的真因。

修法(全部异步,请求立即返回,进度交给前端轮询):

  • execSync → exec(Promise 化);忙等 → await sleep(400) 轮询
  • 启动用 spawn(detached, stdio 重定向到 data\logs\tts-api.log(.err))
  • start/restart 立即返回,不再吊着 HTTP 请求 45 秒
  • 附带把 applyModelSwitch 也改成 fire-and-forget(原来同样会卡住主线程)

═══ 面板:真正的 TTS 服务控制 ═══
语音页新增卡片(旧的 🧠 语音模型 一整栏删除):

  • 状态分四态:⚪ 已停止 / 🟡 启动中(等了多少秒)/ 🟢 已就绪(端口+PID)/ ⚠️ 端口异常
  • 显示配置意图 + 上次试合成结果(不再"进程活着就显示运行中"这种假阳性)
  • 按钮 启动 / 停止 / 重启 / 试合成,点完立刻反馈,启动/重启自动轮询到就绪:
    成功 → "✅ 已启动成功,用时 N 秒";超时 → 提示去看试合成的服务端原文
  • 「开机自启」勾选:只改意图,取消勾选不会杀掉正在跑的服务(要停请点停止)

═══ 顺手修掉"配置说开、实际没跑" ═══
voiceModelOn 以前只有配置变化时才启停,启动流程里从没读过它 → 重启 DSH 后服务不会自动起。
现在插件启动 8 秒后按配置判断并自动拉起(日志:TTS:api_v2 已在运行 / 已发出启动命令 / 跳过)。

═══ hook:崩溃现场记录器 ═══
新增 src/CrashLog.cpp(VEH,只观察不改行为):致命异常时把 code / rip(模块+偏移) /
访问类型与地址 / 寄存器 / 栈上代码地址 / 最近 20 条 hook 操作写进
<微信目录>\hook-crash.log,然后原样交还异常。
Voici 三条发送路径(文字/图片/语音)与查库、解密都打了 Note 点,崩溃时能看出崩在哪一步之后。
⚠️ 已知:启动期会有 2 条 EXECUTE 0x0 记录,那是 sqlite 名字探测(contact./emoticon/key_info),
被客户端自己捕获、不影响运行,属于噪声。

═══ 文档 ═══

  • OPERATIONS-RULES 新增 R18:插件里禁止阻塞式外部命令(execSync/忙等),长操作立即返回 + 前端轮询
  • README 补 1.4.14 / 1.4.15 版本记录,白名单数字更新为 38 项 / 面板用到 34 个

═══ 实测 ═══

  • DSH /wechat-bot 响应 1~4ms(连续探测不再卡)
  • TTS 干净重启后合成成功:108,844 字节 WAV
  • 自检:面板操作覆盖 34/34 ✅(白名单 38 项)
  • DLL SHA256 = 10eee9366f4f807e7a8a43c1f72e8b722142848f157616e08f0f2e5c87d1cb45

v1.4.14 — 修「当前生效」卡片显示旧模型(其实模型早就换成功了)

Choose a tag to compare

@mengyunqwq mengyunqwq released this 27 Oct 06:13

先给结论

模型其实早就换成功了,骗人的只是那张卡片。

config.json        provider=bailian   model=deepseek-v4.1-flash
插件日志           模型(bailian/deepseek-v4.1-flash) finish=stop   ← 真实调用

卡片为什么显示旧模型

两个原因叠在一起:

① 第一栏取的是「上一次实际请求」,不是配置

const R = App.S.route   // = lastTextRoute,只在 llm/stream 时才更新
<span class="pill">回复用 ${R?.provider || cur.provider} / ${R?.model || cur.model}</span>

R 有值就压过配置 → 只要上一次请求走的是 opencodego-extra,卡片就一直显示它。

② 只改模型时页面根本不重绘

原来的逻辑是「改 provider → 重拉清单并重绘」,改 model 只保存不重绘,
所以卡片会一直停在页面加载时那一份快照上。

修好了

  • 第一栏改成 「配置」:直接取 /models 返回的 current(实时配置);
    provider 留空时显示「(跟随 DSH)」并把 DSH 当前选择作为生效值
  • 「上次实际调用」独立一栏,不再和配置混在一起
  • 两者不一致时给一句解释:刚改完的话,下一次回复就会一致
    (比对用前缀宽松匹配 —— adapter 会把模型解析成 deepseek-v4.1-flash-1017 这种快照 id,
    严格相等会天天误报)
  • 改任意一个模型字段都会重取 /state 再重绘,卡片不再停在旧快照

现在卡片长这样:

🎯 当前生效
[配置  bailian / deepseek-v4.1-flash]  [上次实际调用  bailian / deepseek-v4.1-flash-1017]  [DSH 当前  opencodego-extra / deepseek-v4.1-flash]

顺带确认

阿里云(bailian)的模型清单里确实有 deepseek-v4.1-flash(还有 deepseek-v4-pro、
kimi-k3、glm-5.3、一堆 qwen3.x),所以下拉能正常选到,不会误报「已下线」。

怎么看到

刷新 /wechat-bot 页面即可;DSH 侧栏 🐋 入口用的是打包副本(已重新生成并重载),
还显示旧界面就 Ctrl+F5 硬刷新。

v1.4.13 — 「人设模型」provider/model 改成 DSH 那样的联动下拉

Choose a tag to compare

@mengyunqwq mengyunqwq released this 27 Oct 05:13

问题

「人设模型」页里 provider 和 model 是纯文本框,得自己记住 deepseek-account / deepseek-flash 这种 id。写错了不会报错,只会静默回退到别的路由——用户根本不知道实际生效的是哪个模型。

现在

改成 DSH 那样的联动下拉,数据全部来自 DSH 的 llm 服务(不自己维护模型清单):

GET /wechat-bot/api/models?provider=<id>
  providers     = llm.listProviders()            已注册(真能用)
                + llm.listConfigurableProviders() 声明了但没启用(单独计数,不混进下拉)
  models        = llm.listModels(provider)        按需拉,含 inputModalities
  current       = 当前配置的 provider/model/visionProvider/visionModel
  dshSelection  = DSH 自己当前的默认模型

面板行为:

键 下拉内容
provider 只列已启用的 provider(没配 key 的列出来也用不了,反而误导)
model 跟着上面的 provider 变,换 provider 自动重拉清单并重绘
visionProvider 同上
visionModel 只列 inputModalities 含 image 的(能读图的)

细节:

  • 空选项始终保留:(跟随 DSH 当前选择/最近使用) / (用该 provider 的默认模型),设置过也能选回去
  • 「✏️ 自定义(手填)…」:清单里没有的 id 也能填
  • 清单读不到就退回文本框——绝不因为读不到清单而让人填不了字段
  • 页面底部新增**「当前生效」卡片**:回复用哪对 provider/model、DSH 当前是什么、当前模型是否已不在清单里(下线提醒)、未启用 provider 数量、读取失败原因

顺手修了一个 bug:agentDefaultModel 服务要用 ctx.get() 取,直接点属性在插件里拿不到——所以以前面板完全不知道 DSH 当前选的模型。

实测(本机 DSH)

默认 → ok=true,modelsFor=deepseek-account
  模型     = deepseek-flash(image)/ deepseek-v4-pro
  current  = deepseek-account/deepseek-flash
  vision   = opencodego-extra/deepseek-v4-flash-vision-exp
  DSH 当前 = opencodego-extra/deepseek-v4.1-flash
  已启用   = deepseek-official, deepseek-account, agnes, opencode-go,
             opencodego-extra, deepseek, bailian(另有 39 个未启用)

切 provider=opencodego-extra → 35 个模型,能读图的只有 deepseek-v4-flash-vision-exp
传不存在的 provider       → ok=true + models=[] + 明确的 error,不会炸

怎么看到

  • /wechat-bot:直接刷新页面即可(HTML 是每次请求现读的)
  • DSH 侧栏 🐋「微信机器人」:用的是打包后的 lib/client.js,已重新生成并重载;
    如果还看到旧界面,硬刷新(Ctrl+F5)或重启一次 DSH

v1.4.12 — 热重载改成「只预检、不自替换」(不再把面板搞 404 / 不再卡死)

Choose a tag to compare

@mengyunqwq mengyunqwq released this 27 Oct 02:14

fix(1.4.12): 热重载改成「只预检、不自替换」——彻底不再把面板搞 404 / 不再卡死

背景(1.4.10/1.4.11 都没修好,两次都把面板搞成 404):
插件无法在自己的进程内安全替换自己。

实测三条路全部失败:

  1. 先 dispose 旧 fiber → 旧 ctx 变 inactive,新实例建不起来
    日志:cannot create effect on inactive context
  2. 先建新 fiber 再 dispose 旧 fiber → 新实例挂载撞车
    日志:webserver: duplicate prefix route "/wechat-bot"
    且新 fiber 的父 ctx 就是旧实例,父一死子也死
  3. entry.fiber=undefined + entry.refresh()(官方 loader 路径)
    第一次成功,第二次 fiber 状态未知 + 再 404

而且每次失败都会留下"半死实例":日志里出现
「工具 wechat_bot_xxx 已存在(热重载残留),本次跳过注册」×11 条/次
残骸让之后的 reload 卡在 await fiber.dispose() 上永远不返回
(用户实测:dev_reload_package 每次都卡死)。

修复:selfReload 只做"新代码能否加载"的预检,绝不触碰运行中的实例
→ 返回 { ok:true, verified:true, needRestart:true, hint:'请重启 DSH 生效' }
→ 面板任何时候都不会因为它变砖,也不会卡死

同时清理:删掉自测用的临时路由 /wechat-bot/_selfreload_test 与对应 globalThis 入口

验证(本地 DSH 重启后):

  • 面板 /wechat-bot 与 /api/state 均 401(登录栅栏 = 路由活着)
  • 点「热重载插件」:只打预检日志,面板不掉线、无残留
  • 重启后日志中不再新增「热重载残留」

v1.4.11 — 热重载后面板 404 的真因:路由撞车

Choose a tag to compare

@mengyunqwq mengyunqwq released this 27 Oct 02:02

fix(1.4.11): 热重载后面板 404(真因:新实例挂载撞车 + 旧实例拆除后路由为空)

上一版(1.4.10)只修了"先拆后建"导致的 ctx 失效,没修到真正的第二层原因。

实测证据(1.4.10 热重载时):
[02:00:39] 面板挂载失败:webserver: duplicate prefix route "/wechat-bot"
[02:00:40] [selfReload] ✅ 热重载完成
→ 之后 /wechat-bot 与 /api/* 全部 404

根因链:

  1. 「先建后拆」时新实例立刻尝试挂载路由
  2. 但旧实例还占着 /wechat-bot → ws.register 抛 duplicate prefix route
  3. 新实例没挂上,1.5 秒后旧实例被 dispose(路由随之移除)
  4. 结果:新旧都没挂 → 整站 404

修复(两层):

  • panel.mjs:挂载撞车(duplicate / already exists / prefix route)时自动重试,
    最多 20 次 × 500ms(旧实例通常 1.5~3 秒内被拆掉,重试即可挂上);
    首次撞车打一行日志说明"旧实例还占着路由,将重试最多 10 秒"
  • index.js:selfReload 拆掉旧实例后,主动调用新实例暴露的
    globalThis.DSH_WECHAT_PANEL_REMOUNT 再挂一次(确定性兜底,不依赖重试时序)

实测:1.4.11 重载后 /wechat-bot 与 /api/state 均返回 401(登录栅栏 = 路由活着),
日志「面板已挂载:/wechat-bot」。