Repository navigation
Releases: mengyunqwq/wechat-bot
Release list
v2.0.2 — 更像一个人,也更不容易坏
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 — 好感度七阶、群梗、抖音静音刷
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 — 她开始像一个人:记忆会遗忘、性格会长大
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:艾宾浩斯遗忘曲线 + 情绪加权
为什么重做记忆系统
旧系统实际上是一个按时间滚动的环形缓冲:
- 每条记忆只有
{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 项)、全链路全绿
中途修掉的两个设计缺陷
- 旧数据会被集体判死:
baseDays=3时 7 天后 R 仅 10%,一开启就把老记忆全归档
→ 改 7 天基准 + 判据从"R 阈值"改为"R×重要度权重" + 迁移条目重算强度 - 重要记忆也会被忘:测试发现"重要但很久没提"的会被归档
→ 加铁律阈值 0.85,这类永不自动忘
升级后怎么验
面板 → 记忆页 → 选群 → 读取:
- 顶部显示「生效 N 条|平均保持率 x%|未来 7 天预计遗忘 M 条」
- 每行有彩色保持率进度条(
R% · S=x.xd) - 点「固定」→ 出现 📌;点「复习」→ 提示
强度 7.9d → 12.xd - 点「跑一次遗忘巡检」→ 显示
巡检完成:N 个群|归档 0|清理 0|淘汰 0 - 群里聊几句 → 插件日志出现
记忆检索:命中 x/y 条(平均保持率 …)
备份与回滚:D:\微信deepseek\backups\memory-v2-20261028-103051(facts 全量 + 代码 + 配置 + RESTORE.md)
v1.4.16 — 面板 12 处功能性缺陷修复 + 移除「模型接口」页
这次修的是「面板说成功、其实失败」那类问题
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 卡片的「音色模型」。 - 移除「模型接口」页(按用户要求):连同只服务它的死代码(
renderProviders92 行、bindProviderEvents65 行、PROVIDER_PRESETS等共约 250 行)。保留「表情包」页底部的生图端点配置(不受影响)。诊断页提示改为去「人设模型」页选 provider/model。
一条无法复现
「群成员昵称全空」——实测 /api/members?room=48684538221@chatroom 返回 35 人、35 个都有名字。可能是 contact.db 尚未打开的时刻,未改代码。
过程中踩的坑(记录一下)
第一次用「文本锚点切割」删「模型接口」代码时,切点落进了模板字符串内部 → 面板脚本语法崩溃(Unexpected end of input)。随后从备份恢复,改用花括号配对扫描按函数边界删除,再重新把前两轮 20 处修复逐条打回去(恢复备份会把它们一起带回旧版)。最终用 vm.Script 真解析验证。
升级后请这样验
刷新面板(DSH 侧栏按 Ctrl+F5):
- 运维日志页点「立即自检」→ 按钮变「检查中…」、列表刷新
- 诊断页顶部有「清空 hires 缓存」按钮
- 手动操作 → 发送到不存在的群 → 显示「发送失败:rc=-1」而不是「已执行」
- 语音页点「停止」→「开机自启」勾选框自动变空
- 总览点「看诊断」→ 跳到诊断页
- 左侧没有「模型接口」了,共 16 页
v1.4.15 — 面板可控制 TTS 服务 + 修「点重启就把 DSH 卡死」
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 点,崩溃时能看出崩在哪一步之后。
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 — 修「当前生效」卡片显示旧模型(其实模型早就换成功了)
先给结论
模型其实早就换成功了,骗人的只是那张卡片。
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 那样的联动下拉
问题
「人设模型」页里 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 / 不再卡死)
fix(1.4.12): 热重载改成「只预检、不自替换」——彻底不再把面板搞 404 / 不再卡死
背景(1.4.10/1.4.11 都没修好,两次都把面板搞成 404):
插件无法在自己的进程内安全替换自己。
实测三条路全部失败:
- 先 dispose 旧 fiber → 旧 ctx 变 inactive,新实例建不起来
日志:cannot create effect on inactive context - 先建新 fiber 再 dispose 旧 fiber → 新实例挂载撞车
日志:webserver: duplicate prefix route "/wechat-bot"
且新 fiber 的父 ctx 就是旧实例,父一死子也死 - 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 的真因:路由撞车
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
根因链:
- 「先建后拆」时新实例立刻尝试挂载路由
- 但旧实例还占着 /wechat-bot → ws.register 抛 duplicate prefix route
- 新实例没挂上,1.5 秒后旧实例被 dispose(路由随之移除)
- 结果:新旧都没挂 → 整站 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」。