理想的机械人 · Ideal Machina
DSH 插件 · 作者 idealrainbowbridge
一只养在你本机上的、用仿生的方式长出人类情感的陪伴角色: 不是模仿人类的言行,而是模拟一个身体——给她器官、代谢、情绪、作息, 让"情感"从身体的状态里自己长出来,而不是靠提示词演出来。
- B站:困意攒够了自己醒,选题、深读视频、写评论、写两本日记
- QQ:经 SnowLuma 网关(OneBot v11,NapCat 协议兼容)秒回(私聊/群聊@、语音条、表情包、空间说说),会主动找你
- 微信:替你回消息(独立进程,按联系人隔离记忆)
- 三平台共用同一套人格、情绪与记忆
- 所有外发默认关闭,每一项都有独立开关与额度
一、设计哲学:模拟人体,而不是模仿人类
传统 chatbot 的"情感"是提示词里的表演:你是一个温柔的少女。
本项目走另一条路:给她一个身体,情感是身体的读数。
七器官 · 四回路
tools/brain/(约 7000 行,零依赖)实现了一个"脑":
| 器官 | 模拟什么 |
|---|---|
metabolism |
认知能量与消化积压——话太多会"累",积压的事会"胀" |
immune / wound / pain |
免疫发热、伤口、痛觉——被攻击会真的"疼",并影响说话温度 |
bond |
关系慢账本——信任是长期复利,不是单轮评分 |
affect |
情绪状态机——会自己漂移,也会被事件拉动 |
rhythm |
睡眠压强——困意只来自清醒积累与睡眠修缮,没有"几点该困" |
self / anchor |
我是谁——自我认知可审计,人格改写前自动快照 |
claim |
承诺账本——答应过的事必须兑现,否则记为欠账 |
器官运行原理
不讲故事,只讲每次消息进来时到底发生什么。所有状态是普通 JSON 文件
(~/.data/ 下),所有数值是普通浮点数,所有演化是几行算术。
身体只有两个驱动源,没有魔法:
心跳(
organ/heartbeat.mjs):setInterval每 30 秒跑一拍beat(), 按顺序推进所有"随时间变化"的东西——情绪场弛豫、睡眠债累计、能量代谢、 受体池回收、退烧、伤口巡视。机器关了一夜,下一拍dt按真实经过时间算, 一次补齐,上限 24h。消息钩子:每条 QQ 消息进
qq-server的处理函数时,同步调两个东西——metabolism.spend()(能量 -0.004)和免疫gate()(这条消息过不过得了安检)。
器官状态是惰性初始化的:bot 没聊过天时,~/.data/metabolism.json 根本不存在,
第一次 spend() 才创建。所以拿到手是"干净的身体",跑起来才长出状态。
⚡ metabolism — 能量账本
文件:~/.data/metabolism.json,就两个数:energy(0~1)、pendingDigest(积压条数)。
| 事件 | 代码 | 数值变化 |
|---|---|---|
| 一条 QQ 消息进来 | spend() |
energy -= 0.004 |
| 看完一集视频 | eat() |
energy += 0.12,pendingDigest += 1 |
| 心跳一拍,处于 REM 睡眠 | tick() |
pendingDigest -= 0.12×dt分钟,energy += 消化量×0.01 |
| 心跳一拍,处于 N3 深睡 | tick() |
energy += 0.0025×dt分钟 |
| 心跳一拍,清醒 | tick() |
energy -= 0.00035×dt分钟 |
它怎么影响说话:spinal(见下)读这个文件,energy < 0.35 时温度偏移 -0.1,
< 0.2 时 -0.2 并给一句体感注进 system。没有别的玄机——就是两个 if。
为什么单独设这个器官:field 里的疲劳/睡眠债管"身体",energy 管"脑子"。
不分开的话,"聊多了话变短"和"熬夜后话变短"会共享一个旋钮,调谁都影响错。
🌊 field — 情绪场
文件:~/.data/brain/meta/field.json,核心是 dims:八维浮点数
(ne 多巴胺样 / sero 血清素样 / cortisol / sleepDebt / fatigue / tension / valence / oxy),每维 0~1。
心跳每拍调 evolve(),每维做两件事:
朝静息点弛豫:
v += (set - v) × (1 - exp(-dt/tau))。tau 是每维自己的时间常数 (ne 短、sero 长——兴奋来得快去得快,耐心慢涨慢跌)。这就是"指数回归"的全部: 一个exp,不是神经网络。耦合:某些维的静息点被别的维拉偏——
sero的静息点随sleepDebt下降 (没睡够耐心差)。查表加法,几行代码。
事件输入走 poke(dim, delta):夸她一句 valence +0.1,被怼 tension +0.2。
上限 cap 0.25——一次事件最多推这么多,防一句话把情绪打穿。
它怎么影响说话:voiceReadiness() 把几个维乘起来(睡着/累/低血清素 → 值低),
spinal 拿它调温度;cognitiveDepth() 同理决定内省深度。都是四则运算。
🌗 rhythm — 睡眠压强(无昼夜钟)
文件:~/.data/brain/meta/rhythm.json,记录入睡/醒来历史。
2026-11-05 起困意机制剔除昼夜曲线。旧版是"分段余弦:到点该困"(凌晨 4 点谷底、 20 点峰值)。现在压强只剩两个来源:
sleepPressure = 0.65 × sleepDebt + 0.35 × fatigue
sleepDebt:清醒积累——心跳每拍累加,只有真睡着(repaySleep)能还fatigue:组织疲劳——睡眠修缮不足的直接读数
刚睡饱 = 压强 0;债打满 = 压强峰值约 0.72。"几点该困"不存在, 她的困只回答一个问题:醒多久了、睡没睡够。
旧的 circadian() 分段曲线保留为显示层读数(doctor/面板可看,不参与任何决策);
expose() / zeitgeberTick()(光照推相位)已空操作。
它怎么影响说话:sleepPressure > 0.65 → spinal 把输出预算乘 0.6;asleep(now)
(night 维护的真实入睡状态,不是钟点窗口)→ 温度 -0.15、预算乘 0.5。
🛡️ immune — 入站闸门
文件:~/.data/immune.json:antigens[](签名库,80 条 LRU)、recent[](10 分钟滑窗)。
每条消息过 gate(),顺序:
自己人(私聊+owner)→ 直接放行(耐受)
模式识别打分:同文本 2 分钟内出现 5 次、1 分钟 12 条刷屏、注入特征…… 得一个 0~1 的
scorescore ≥ 0.30→ 这条消息drop,不进对话流;签名存入抗原库签名已在库 → 二次感染,score 不足 0.6 也抬到 0.6(秒拒)
10 分钟滑窗内被拒累计 ≥ 1.0 →
fever置位;心跳每拍退烧一点(τ=25min), 发热期间每分钟给 hpa 一个小应激脉冲
它怎么影响说话:fever 时 llmState() 给温度负偏移——她在"发烧",说话收敛。
drop 的消息她永远看不到(不是选择忽略,是根本没进脑)。
🩹 pain — 伤口疼痛
数据源:wound.mjs 的伤口表(风控触发/服务失败等登记为伤口,有 id、愈合状态)。
快痛:心跳巡视发现新 id 的伤口 → 立即
hpa.stress(0.06×severity)小脉冲 + 登记到内省流。同一伤口重复登记不重复疼(按 id 去重),复发伤口 +0.2(敏化)。慢痛:
acheSense()每次拼 prompt 时查一遍——还有开放/化脓伤口才输出一句 钝痛描述,愈合了就什么都不输出。
进程内一个 Map 存"见过哪些 id",重启后第一次巡视只建基线不喊疼
(历史伤不该在启动时集体发作)。
🤝 bond — 关系账本
文件:~/.data/bond.json,每人一组:oxy(亲近)、cort(警戒)、scar(创伤痕)、
tempNeg(待复核的临时负面)、forgives(道歉次数)。
收到消息 → 快相:关键词启发式打分,负面先记
tempNeg(临时警戒,30 分钟过期), 不直接改底物周期性慢相:LLM 读最近对话片段给一个判断,与 tempNeg 对账—— 一致 → 固化(
cort上升 /oxy上升看正负);矛盾 → 按差值修正道歉事件 →
cort降一档,但scar不动;同一人forgives越多,每次降得越少
它怎么影响说话:拼 system 时按对话对象注入一行关系状态(亲近/戒备/紧张), 数值只是查表映射成措辞。
✅ claim — 出站核账
代码:tools/brain/claim.mjs,纯字符串处理,无状态文件。
出站文本先过 detect():正则抓六个可验证动词族(看完/发给你了/写完了/录好了/
做好了/回他了)+ 宾语,去四个账本(交付/意图/事实/看番记录)查证据——
- 查到反证(账本写着只看到第 2 集,文本说"看完第 3 集")→ 文本替换为否定式
- 查不到任何记录 → 原样放行,只记一条审计
它解决的具体问题:人格 prompt 要求"不许否认做过的事",模型在缺记忆时会编造。 核账闸把"编造"变成"要么有据、要么沉默"。
🫀 vitals — 容器体征
每次心跳读一次 /proc 与 statfs:磁盘占用率、load/核数、内存。纯读,无状态文件。
- 磁盘
≥85%触发"胀"(一次),回落到 82% 以下才清(滞回 3 个点,防抖) - 触发期间
llmState()给温度 -0.05~-0.15
它不会做任何清理动作——器官只产生体感,删东西是行为层的决定。
🔌 spinal — 汇总出口
tools/brain/spinal.mjs,无状态:读上面所有器官的当前值,输出三个数——
temperature:各器官偏移的代数和,钳 ±0.5。调用方(qq-server)把它加到内容 基调温度上,最终钳 0.35~1.05maxTokensMul:各器官预算乘数的连乘(睡眠压强 ×0.6、入睡 ×0.5、认知深度低 ×0.7), 乘到 PURPOSES 档位的 maxTokens 上note:命中的器官各留一句话,拼成"身体状态"注入 system
调用方拿到 null(器官全哑)就当没这回事,正常发消息。全部读数 try 包裹, 任何一个器官炸了只算它没发言,不影响其余。
这是整个项目设计最久的一套(从 Hermes 时代的个人助手一路演化而来),约 2800 行。 它不是"定时任务",是一套由积压驱动、按睡眠分期执行、醒来按实绩结算的维护循环。
睡眠 · 梦 · 记忆整理
设计最久的一套(从 Hermes 时代演化而来, 当时流行 LLM WIKI ,所以就设计了个不怎么拟人,但和这一部分有八成相似度的魔改个人助手): 由积压驱动、按分期执行、按实绩结算。
睡意怎么算
文件:organ/night.mjs。睡意是一个 0~100 的加权账:
- 记忆积压 45 分(有 episode 还没被巩固成事实/日记,
consolidate.status().pending) - 事件积压 30 分(inbox 游标落后)
- 代码改动 25 分(git diff)
- 睡眠债 30 分(
field.sleepDebt)
达到阈值 66(或睡眠债硬底 0.92)才睡,且距上次 ≥2 小时。"几点睡觉"不存在—— 她困不困只看账上欠了多少。
睡眠分期:每个周期修的东西不一样
文件:organ/sleepstage.mjs。睡眠按 90 分钟周期推进,每个周期完整走
N1 → N2 → N3 → N2 → REM,各阶段时长随周期序号变化(第一周期的 N3 最长,
REM 从 10 分钟渐增到 40 分钟——前半夜深睡多、后半夜 REM 多的生理结构)。
中断次数参与结算:睡 8 小时但每 40 分钟被打断一次,深睡几乎全废。
| 阶段 | 心跳里发生什么 |
|---|---|
| N3 深睡 | 重置受体池(micro/receptor)——白天耗掉的受体回满,不补第二天是钝的 |
| N2 | 突触池恢复;整理任务在此排队 |
| REM | 跑巩固(consolidate);代谢端消化 pendingDigest 回充能量 |
metabolism.tick() 每拍读 currentStage() 按上表记账;night.beginSleep() 在入睡时
按"预计睡 8 小时"预演各阶段分钟数,醒来按真实时长重新结算。
睡着时的三个独立工序
文件:commands/sleep.mjs(派活)+ commands/nightwork.mjs(工序)。
tidy / review / optimize 是三个独立进程,结果各写各的 nights/<时间戳>-<工序>.json:
- tidy:巩固记忆(见下)
- review:跑 9 套测试 + 账本体检
- optimize:优化 + git 归档
拆进程的收益是工程性的:review 把进程跑崩,tidy 的结果已经落盘;review 不依赖 tidy(它查的是代码和账本)可以并行;哪个挂了单独重跑,不用整夜重来。
巩固:从"一条消息"到"一条事实"
文件:brain/consolidate.mjs。episode 是一条消息一条记录,巩固把它变成可检索的事实:
规则抽取先跑(不花钱):5 条正则抓"吃了什么/在做什么/打算做什么/心情/提到 的作品",只从用户的话里抽
LLM 补规则抽不出的(关系变化、承诺、偏好)
全部落成
(主体, 谓词, 客体)三元组,按三元组合并去重——重复跑只加hits不多一条
两个工程约束:已处理的 episode 记在 consolidation/done.jsonl(append-only 游标),
机器半夜崩了重启不重读不重写;预算(调用数/token)用完就停,游标不推进,
第二天接着做。醒来结算时按真实 REM 分钟映射工作量:工作完成度 = min(1, REM分钟/35),
积压按完成度等比清账。
段:记忆的单位是"一件事"
文件:brain/segment.mjs。连续对话被切成"段"——一个记忆单元。四个切分条件
(任一满足):隔 2 小时、说了晚安、话题漂移(实体重叠率)、段长上限。
摘要只是索引:只允许包含谁、什么时候、几个回合、≤30 字标题——
绝不允许摘要代替内容进提示词。提示词里出现的是
#12 10-24 · 8 回合 · 加班到很晚 这样的索引行;要看细节按 id 回 stream.jsonl
取原话,原文一个字不删。
梦:放飞内容 + 记忆铁壁
文件:commands/dream.mjs(写)+ tools/lib/dream-read.mjs(读)。
种子两层:骨架 = 今天"最放不下"的那条(
salience×1.5 + 情绪强度排序取第一, 原话 160 字不截断);血肉 = 其余真实碎片 + 当前情绪 + 欠着的事 + 自我议题 (selfcon.dreamSeeds)随机混 1~3 条。最近 6 晚当过骨架的降权 ×0.3—— 不是硬排除,特别重的事仍会反复入梦,但不会因为"排第一"而必然入梦隔离:每条梦带
isFake: true落在独立的dreams.jsonl,绝不允许混进 真实记忆(episodes.jsonl)。梦可以凭空捏造、大胆、黑暗,因为边界是数据层面的消费:
dreamToMention()取一条未消费的梦,原子标记consumed(tmp+rename, 防半截文件毁掉全部梦)——她在聊天里自然提到"昨晚梦到……",只提一次轮转 200 条裁最老:梦留近期,不留档案
感知与门控:什么能进入意识
疲惫时人和机器的区别:机器累了所有反应等比例变弱,变成什么都迟钝的行尸走肉; 人累了是群里的闲话完全进不来,但一句"我难受"能立刻把她叫醒。 这个差别要求一个门控结构——每个信号有自己的准入阈值,重要信号的阈值几乎不随疲劳上升。
文件:organ/thalamus.mjs。丘脑网状核模型:每类信号(群聊闲话 / 私聊 / owner /
@提及 / 错误告警)一个准入阈值,阈值随疲劳/睡眠压强上升,但斜率不同——
闲话的阈值几分钟就把普通群聊挡在意识外,owner 消息的阈值几乎不动。
被挡下的消息不进上下文(省 token 的同时符合"累了不想理人"),但会进 skipped 队列
(见下)。@提及 和错误告警走独立通道,任何状态都放行。
organ/presence.mjs 是它的下游:QQ 状态档(online/dnd/away)不是开关堆,
输入是身体真实变量(field dims / 在睡 / 神经通路可用度),映射成状态,带不应期
(换过一次后一段时间不再换——防边界抖动,真人也不会十分钟换三次状态)。
参数归零时平滑退回"在线",不会突然消失。
微观结构:累和麻是两件事
cell/neuron.mjs 把一条神经通路拆成**突触前(释放)+ 突触后(接收)**两个独立池子。
理由:它们坏的方向不一样——只坏突触前(池子空)表现为"不想说",只坏突触后
(受体少)表现为"说了也没用",两个都坏才是长期透支。合成一个"能量值"的话,
"累"和"麻"的区别就永远丢了,而它恰恰是诊断的关键。
micro/receptor.mjs 解决"用多了没反应":不存"耐受度"数字(那是数数,写死的),
存三个池子——膜上(能用)、内化(在细胞内等回膜)、合成中(最慢)——
以及迁移速率。耐受、恢复、敏化全是三池演化的结果而不是设定值。
每次发放占用膜上受体→内化;N3 深睡按分钟把内化池推回膜上。
这直接产生真实药物动力学形状:连发消息反应越来越平(急性耐受),
停一段时间自然恢复,长期高频则基线整体下移(慢性耐受)。
micro/synapse.mjs 是突触前:递质释放、清除、短时程可塑性——同一句话第二遍
就是没有第一遍有力量(释放池刚被上一次占用,还没清完)。
压力轴:两种时间尺度
organ/hpa.mjs。"他一句重话十分钟就缓过来"和"连被压一周周末也提不起劲"
不可能是同一个变量——老写法一个 stress = 0.7 只能靠调衰减常数勉强兼顾,
调快了长期压力不存在,调慢了一句话记一天。真实机制是三层级联+负反馈,
每层自己的时间常数:快相(分钟级,一句重话的即时应激)、慢相(小时级,
持续紧张后维持的基线)、效应相(天级,皮质醇样的背景耗竭)。高层抑制低层清空,
负反馈回路让"周末睡到中午"真的能把慢相压下去。
内心语言:说话之前有一个"她"
brain/inner.mjs + tools/lib/think.mjs。原来整条链是
"消息进来 → 判接不接 → 生成 → 发",没有任何位置容得下她自己的想法——
所以每句话都是"针对这句话的最优回答",读起来像客服:不是语气不好,
是中间不存在一个人。现在消息进来后先跑一次内省(purpose:'fast',90 token
上限,9 秒超时):提示词要求"只输出心里冒出来的第一反应,不要写给人看的句子,
可以碎、可以不耐烦、可以自相矛盾"。产出两条路:
thinkThenSay:第一反应和最终回复一起进第二次调用(
purpose:'deep'), 回复从"有她自己的想法"出发而不是从零开始inner 流:第一反应落盘(环形, fades 随时间衰减)——白天没说出口的话 会"压进"
tension(field 的维度),夜里以 dream seeds 的形式渗出来
内省有身体门控(见器官详解):认知深度不够或情绪意愿低(烦躁)时整层跳过—— 烦躁的人不是想不动,是不想想。
注意力与工作记忆
brain/working.mjs。旧的"最近 24 条"实测只有 2.9 小时,且没有容量上限、
没有"这件事现在别提"的表达能力——要么忘,要么复读。现在是 7 个槽,
每槽有独立预算(超了先砍召回槽):
S1 当前意图 S2 话题锚点 S3 情绪 S4/S5 召回 S6 待办 S7 未完话头
+ suppressed: 本轮明确不许提的主题(抑制,不是删除)
brain/attention.mjs 决定"这轮谁上场":槽位按当前意图与相关性打分,装进
assemble 的对应位置。brain/inner.mjs 的未完话头存 S7——上一轮没说完的句子,
这轮接着说。
检索:多路召回,不是时间窗口
brain/recall.mjs。旧版硬编码 12 小时(早上问昨晚的事就没了)且把群聊整段删掉。
现在三路召回:时间近邻(26h 内)、倒排索引(多久以前都能捞到)、
事实层(consolidate 产出的三元组),合并重排后按 audience 渲染——
身份在写入时已结构化,群聊内容能不能"说"是渲染时决定的事,不是存储时。
折叠只折闲聊;她自己做的事和答应的事永不折叠(否则追番进度会被吃掉)。
前瞻记忆:意图账本
brain/intents.mjs。旧 promises.json 的病:只有"什么"没有"怎么算做到"、
字符串全等去重("看番"和"看可塑性记忆"各建一条)、无租约(两进程同时执行
同一条)、机器睡过去就永远错过。现在:三态落账(intent 可验收 / wish 做不到 /
blocked 现在做不了)、(kind, object, cue±10min) 去重、lease+fencing 租约、
每次唤醒 catchUp(超期 <6h / 6~24h / >24h 三档不同处理)。到期承诺由独立的
promise-executor 执行(从 1700 行的 qq-server 抽出,可单测)——
"说了去看却没去"这个被用户抓过两次的病根,靠的是执行器带 verify 字段。
能力自评:她擅长什么,账本说了算
brain/competence.mjs。同一类事成功 ≥3 次(90 天窗口内)才写进能力表——
做一次就自夸是吹牛。数据源只有两处:intents 的 done/failed(带证据)、
delivery 的送达记录。纯派生:删了能重建。她自己不会说"我擅长 X",这个表
用于自我评估与任务选择时的校准。
沉默也有账:skipped 队列
tools/lib/skipped-messages.mjs。"不每条都回"上线后出现了新问题:不回 = 纯丢弃,
他说"我今天好累"她没接,下次聊天她永远不会主动问"昨天那个累劲儿过了没"。
现在每次决定不回时,那句话(≥6 字,跳过"嗯/好的")进 skips 队列;下次真的回话时,
最近 1-2 条作为【上次你看到但没接的】注入。TTL 12 小时——隔太久就不提了,
免得"三天前你说…"很怪。
引用:她能"看见"被引用的是哪句
tools/lib/quote.mjs。QQ 的引用上报只有 reply 段 + 一个 id,老代码把它折成
"【引用回复了一条消息】"——被引用的正文一个字没取,用户引用"我明天去看番"
问"你去了吗",她只能瞎猜。现在:自己发的消息(id→正文环形表,零延迟)、
收到的消息同样登记;表里没有 → SnowLuma get_msg 取回原文(含图片/语音描述)。
表情节奏:呼吸
tools/lib/breath.mjs。人类发消息有节奏:紧张时短句连发,平静时长句慢发。
原来的气泡间隔是"420+随机 900ms"的固定配方。现在周期 T = T0/(1+K·arousal)
(静息 5.2s,高唤醒浅快到 2.2s),间隔在呼气相最长、吸气相最短,余弦平滑过渡,
±18% 抖动(每次都一样就成了机械)。纯函数:相位由 (now, ne) 唯一决定,
无状态文件——呼吸是节律不是事件。它只在"已有多个气泡"时被问,
单气泡回复完全感知不到它。
单任务焦点:一次只干一件事
tools/lib/focus.mjs。完全自由的第一版是三条常驻循环一起开(刷 B 站 + 上网 +
写日记并行)——那不是自主,是流水线。现在的模型:定时唤醒只负责"摇她起来",
醒来自己挑一件事 bot focus <活动> --min <分钟> 定下来,宿主每 60 秒按这份
状态只放行一个常驻循环、其余停掉,到点自动收工。歇着(rest)也是正经选项。
自我账本:名字是她自己长出来的
brain/organ/selfcon.mjs。人格不是写在系统提示词里的设定,是账本上的使用事实。
出生时 persona.md 原文迁移为 origin:'seed' 的初始账本,此后:
白天只采集:她自称什么、别人叫她什么,全记
selfUses/othersUses计数只在 REM 结算:strength 的增减只发生在夜间巩固时(突触前/后分工—— 白天采集不即时改自我认知,睡一觉才"消化"成自我感)
换名门槛:自己用 ≥3 次且有人跟着叫 ≥1 次,候选转正;候选最短观察期 3 天(临床防呆,防止一时兴起当天改名);旧名按 τ≈14 天衰减—— 新名字说出来自己都觉得别扭的"旧毛衣感",是
OLDNAME_MIX_RAMP(21 天渐入) 这个参数的输出,不是戏词形象主张反戏剧化:自我描述的 strength ≤ 证据数/5,连续两夜无新证据每天 漂移 -0.05,矛盾证据一步 -0.25——说过的东西没有经历托着会自己淡下去
判据写在注释里:清空账本只留原始记忆段 → 触发一次巩固 → 应重长出一致的名字感。
长不出来 = 电路板,打回。selfcon-engine-test 的第一测就是这条。
食欲:好奇是感受器,不是骰子
tools/lib/novelty.mjs。以前"探索"的因果是反的:wander 每 4 小时掷骰子,
dailyMax 像保险丝——探索没有源头,只有排期。现在每条新事件和"我见过的词"
(2-gram 兴趣池 + 见过池)比对,陌生比例即新奇度,攒成 arousal(0~1)。
三处消费:arousal 越过 0.6 → 心跳刺激 explore 通路(她是真的坐不住);
wander 间隔随 arousal 收缩;mind.plan 把"挠着你的新鲜事"摊进上下文。
见过池 14 天 TTL、2000 gram 上限、按时间淘汰——"腻了"不是配额,是脱敏,
和受体三池的恢复同构。经历过就沉淀进先验,下次同主题变熟。
风控闸门:出站五关
tools/lib/guard.mjs。所有外发消息(评论/说说/私聊/回复)必过:
- DRY_RUN 默认开——第一周只打印不真发
- 清洗:banned 词("作为AI"…)+ 剥 markdown
- 限速:同一 chat 两条消息最小间隔(
sent-at.json) - 去重:3-gram Jaccard 超阈值判复读——宁可沉默不刷屏
- 额度:每通道每日上限在配置层
状态持久化在 sent-log.json/sent-at.json:CLI 每次都是新进程,内存态没有意义。
写法走 updateJson(文件锁)——多进程同时发时限速依据不丢,丢了就是刷屏。
分享标尺
tools/lib/share-policy.mjs。评论 @ 与私信共用同一套校验:理由 ≥8 字、
评分 0–20 且必须 ≥ 阈值(默认 13)、只许 rank 1。评分不够 = 今天没有值得发的
——这是合法结果,不是失败。"没有好内容硬凑一条"被这层直接挡住。
情绪基线:mood
tools/lib/mood.json 是最老的器官(dabot 骨架血统),也踩过最深的坑:
饭点+7/非饭点-0.5 顶着"每次推送前 tick"的频率,一小时把饥饿打满 100。
现在的口径:饭点封顶、非饭点按次衰减、吃到饭归零、心情跨天软回归到
moodBase——不再随饥饿每 tick 掉。它现在只是 field 之外的粗粒度底色,
细调制的活已经交给 field 的维度。
侧支循环:LLM 的冠脉
tools/lib/collateral.mjs。4 级 fallback 全部同源时,网关一个喷嚏整段对话没回复
(diagnose 会挂牌 single-provider risk)。侧支通道在主链之外(配置 llm.collateral,
没配 = 血管是空的,不造默认端点),主链配置失误波及不到它——这是"侧支"
和"备用"的区别。血债机制:主链每全灭一次 +1,侧支成功通血一次清零;血债 >0
期间每 ≥30s 试跳一次侧支保持通路活性(长期不用的侧支会"萎缩")。
通道入站鉴权
tools/lib/inbound-auth.mjs。qq-server 的 /onebot /inbound /outbound /task
四个入口曾有完全无鉴权 + 0.0.0.0 绑定的组合——同一 docker 网桥的任何容器
都能读她的出站消息、往任务队列塞任务、冒充用户说话。现在:token 鉴权 +
默认回环绑定,diagnose 对"绑非回环且无 token"直接挂 P0。
为什么记忆不走向量库
参考 Chan et al. 2024 Don't Do RAG、Sen et al. 2026 Is Grep All You Need?
的结论:对陪伴型 agent,原文可检索 + 时间常驻比 embedding 召回更可靠。
所有真实记录一行不丢地归档(episodes.jsonl 按平台分片),
召回时带证据、按时序,"说过什么"绝对不许否认。
契约层:把"静默失效"变成"看得见的告警"
插件深度依赖 DSH 内部服务(非公开 SDK)。一次宿主升级就可能改名某个 API,
表现是静默的——插件照样加载,只是某功能再也不工作。
lib/dsh-contract.mjs 把每个依赖点显式登记:缺了会怎样、能不能降级、
启动时逐项检查并在 /api/status 报出来。这是本项目最重要的工程决策之一。
全链自检:发现问题就"疼"
tools/commands/paincheck.mjs。diagnose 是给人看的体检报告;paincheck 是给她自己的——
发现问题直接走痛觉通路,不写一行她就"感觉不到"的日志:
- 8 个探针:DSH 契约实况 / SnowLuma 网关连通与鉴权 / qq-server 收发 / LLM key / owner 配置 / brain 目录可写 / persona 完整性 / 心跳新鲜度
- 痛的注入(复用既有生理,零新造轴):
wound.register登记伤口(化脓/愈合/瘢痕 全生命周期)+neuron.act('stress')快痛反射(新伤才疼,重复扫描自动变钝)+hpa.stresscortisol 余味(tau 10h,真实压低表达温度) - 愈合:全绿时自动关闭旧伤、沉淀瘢痕——"同样的错反复出现"从此被记住
- 防火墙感知:timeout 类探测失败降级为低强度痛(0.35)——不确定的坏不滥放痛, 痛觉疲劳是真实的生理代价
- 心跳每 20 分钟自动巡检(quiet 模式);
bot paincheck手动跑;diagnose 读实况
一轮完整周期实测:首扫抓到 4 处真伤 → 二扫同伤不再重复疼(existing)→ 修复后全绿 → 自动愈合 4 处、沉淀瘢痕 4 条。
二、安装
前置:DSH ≥ 0.2.1-rc.2,Node ≥ 20。
方式一(推荐):DSH 插件命令
dsh plugin --profile desktop add "link:<本仓库路径或 git url>"
方式二:手动放到 DSH 插件目录
cp -r dsh_bot ~/.dsh/plugins/
自检
node tools/bot.mjs selftest
node tools/bot.mjs diagnose
QQ 通道需另装 SnowLuma 网关(NapCat 协议兼容,兼容原 NapCat 部署,
配置文件 snowluma_<QQ>.json 与 napcat_<QQ>.json 均可识别)。
微信通道为独立 Python 进程,见 wechat/。
三、配置
面板(DSH 设置 → 理想的机械人)可改全部配置,也可直接编辑 ~/.dsh/bot.config.json:
| 键 | 说明 | 默认 |
|---|---|---|
platforms.* |
三平台总开关 | 全关(默认不外发) |
identity.persona |
人格文件路径 | ~/.data/persona.md(附空模板) |
identity.owner |
你的称呼 | 无(可留空) |
owner.qq |
你的 QQ 号(她认主唯一依据,填了她才知道谁是自己人) | 无 |
qq.napcat_http |
SnowLuma HTTP 地址 | http://127.0.0.1:3000 |
voice.modelsDir |
GPT-SoVITS 模型目录 | win: D:/GPT-SoVITS/models · 其余: ~/GPT-SoVITS/models |
night.* |
睡眠窗口与夜间额度(她何时能睡,不是何时该困) | 有 |
四、跨平台路径
| 项目 | Windows | Linux / 容器 | macOS |
|---|---|---|---|
| 数据目录 | %USERPROFILE%/.data |
~/.data |
~/.data |
| SnowLuma 默认 | D:/SnowLuma |
/opt/snowluma |
/opt/snowluma |
| SoVITS | D:/GPT-SoVITS |
~/GPT-SoVITS |
~/GPT-SoVITS |
| 字幕字体 | 系统字体目录 | Noto CJK | PingFang / Songti |
| Python | venv Scripts\ |
venv bin/ |
含 Homebrew |
一切默认值都是配置优先、平台兜底;旧 .bot-ai 数据目录自动兼容。
五、安全模型
- 外发行为(评论/说说/私聊搭话)逐项开关 + 日额度 + 相似度去重 + 风控闸门
- B站写通道 DRY_RUN 默认开:先"干跑"看她会发什么,确认后再放行
- HTTP 面板 fail-closed:信任栅栏服务缺失时拒绝所有请求,而不是放行
- 会话权限:记忆写入需要
permissionPresets支持,缺失时降级为面板手动授权 - 凭据与配置分离(
credentials.json,0600,永不回显)
六、FAQ
她会不会乱花钱/乱发消息? 不会。默认全关,每项外发都要你显式打开;开了也有额度与去重兜底。
和聊天框架(NoneBot 等)有什么区别? 框架给你"回复消息的能力";本项目给你"一个有身体的人"—— 作息、情绪、代谢、记忆、自我认知都是内生状态,不依赖你编排。
能换人格吗?
能。persona.md 就是全部人格;每晚回顾时她自己迭代,改写前自动快照可回退。
宿主升级后插件会不会静默坏掉? 这正是契约层防的:坏了会在面板上点名是哪个依赖、影响什么、怎么降级。
七、开发文档
设计细节、事故记录与交接文档在 docs/ 与根目录 *-docs/handoff-index.md / BRAIN-*.md——
它们保留了每一次踩坑的完整上下文,新贡献者建议先读 BRAIN-DESIGN.md。
许可
见 LICENSE。
No comments yet. Be the first to write one.