DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

IdealRainBowbridge /

IdealRainBowbridge/dsh_bot

Verified

Bionic companion agent for DSH — simulate a body (organs/metabolism/mood/circadian rhythm), not human behavior. 3 platforms, shared memory, all outbound off by default

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@7dbcc8bb

理想的机械人 · 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/ 下),所有数值是普通浮点数,所有演化是几行算术。 身体只有两个驱动源,没有魔法:

  1. 心跳(organ/heartbeat.mjs):setInterval 每 30 秒跑一拍 beat(), 按顺序推进所有"随时间变化"的东西——情绪场弛豫、睡眠债累计、能量代谢、 受体池回收、退烧、伤口巡视。机器关了一夜,下一拍 dt 按真实经过时间算, 一次补齐,上限 24h。

  2. 消息钩子:每条 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(),顺序:

  1. 自己人(私聊+owner)→ 直接放行(耐受)

  2. 模式识别打分:同文本 2 分钟内出现 5 次、1 分钟 12 条刷屏、注入特征…… 得一个 0~1 的 score

  3. score ≥ 0.30 → 这条消息 drop,不进对话流;签名存入抗原库

  4. 签名已在库 → 二次感染,score 不足 0.6 也抬到 0.6(秒拒)

  5. 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.05

  • maxTokensMul:各器官预算乘数的连乘(睡眠压强 ×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 是一条消息一条记录,巩固把它变成可检索的事实:

  1. 规则抽取先跑(不花钱):5 条正则抓"吃了什么/在做什么/打算做什么/心情/提到 的作品",只从用户的话里抽

  2. LLM 补规则抽不出的(关系变化、承诺、偏好)

  3. 全部落成 (主体, 谓词, 客体) 三元组,按三元组合并去重——重复跑只加 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。所有外发消息(评论/说说/私聊/回复)必过:

  1. DRY_RUN 默认开——第一周只打印不真发
  2. 清洗:banned 词("作为AI"…)+ 剥 markdown
  3. 限速:同一 chat 两条消息最小间隔(sent-at.json)
  4. 去重:3-gram Jaccard 超阈值判复读——宁可沉默不刷屏
  5. 额度:每通道每日上限在配置层

状态持久化在 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.stress cortisol 余味(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。

—/ 5

No ratings yet

Verified DSH bundle

Commit 7dbcc8bbbe48

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout