Skip to content

Repository files navigation

AstrLover 💞

拟真 AI 恋人。她有自己的人格、记忆、作息和生活:会从相册里挑一张照片发给你,会往自己的频道发动态,会换头像改签名,会给你的消息打表情,记得你发过的每一张图;你不说话时她会主动找你,每天睡前写日记,还知道自己昨天做过什么、为什么做。

所有能力都注册成 LLM 工具,由她自己决定什么时候用;每一项也都留了手动指令入口。对话本身完全走 AstrBot 默认管线——对话历史、会话人格、其他插件、WebUI 一切照旧,本插件是管线上的增强层。

只支持 Telegram。需要 AstrBot ≥ 4.26;换头像需要 python-telegram-bot ≥ 22.7,其余功能不受此限。


目录


能做什么

能力 说明 入口
相册检索 上万张图建成可检索的文字索引,她自己翻自己的相册 browse_gallery / want_photo
发照片 挑一张发给你,记得发过什么、不重复发 send_photo / /photo
生图 相册里翻不到的画面,现场"拍"一张 generate_photo / /generate
图片记忆 把上下文里的图换成可检索的文字,省 token 又不失忆 find_photo / inspect_photo / recall_photo
发动态 往自己的频道发一条,可带配图。频道是真时间线,能往回翻、有推送 post_moment / /moment
换头像 从候选目录里挑一张换上 change_avatar / /avatar
改签名 资料页上那句会变的话 update_signature / /signature
表情回应 给你刚发的消息打个表情,不说话也让你知道她看到了 react_message
语音 该用语音的时刻发原生语音条 send_voice
记录 日程/事实/日记/事件/纪念日/排期,自创建、自销毁、可手改 /rec · Web 面板
四层记忆 小抄(她自己修订)→ 事实(可失效)→ 日记周记(她的内心世界) /status /diary
虚拟生活 作息、已约好的事、"此刻在干什么"、情绪半衰期、临场编造固化 心跳自动
主动消息 由「想不想」驱动:早晚安、纪念日、想炫耀、想你了 自动 / /proactive now
导演控制台 用另一个 bot 当控制台,让她说话而你的指令不进你俩的聊天 /say /act
定时编排 把任意控制台指令排到未来执行 /plan /plans

十一个 LLM 工具:browse_gallerywant_photosend_photoinspect_photofind_photorecall_photogenerate_photosend_voicepost_momentchange_avatarupdate_signaturereact_message


安装

cd <astrbot数据目录>/plugins
git clone https://github.com/RGBadmin/AstrLover.git
# 重启 AstrBot 或在 WebUI 插件页重载,依赖自动安装

配置分两处,各管各的:

  • AstrBot 插件配置页只有 5 项「接线」——恋人 user id、控制台 Bot Token 与管理员 ID、TTS 的 Provider ID、生命层总开关。装机时填一次,之后不用再碰。
  • 其余全部在插件自己的 UI 里(WebUI → 插件 → AstrLover → 打开页面 →「设置」):视觉解析、相册、图片记忆、动态、头像签名、主动消息、生图、轻量模型、向量模型、语音,共 74 项,分组排列,改完即时生效不用重载。视觉和向量那两组旁边直接有「测一下」按钮——调完当场验证。

快速开始

最小可用两步:在 AstrBot 人格页写好她是谁(要点见「人格怎么写」),插件配置里填「恋人的 Telegram 数字 user id」。其余按需开。

想用相册(这是最花钱也最有价值的部分),按这个顺序:

/vision test               确认视觉 API 通、描述像不像样
/gallery embed test        确认向量模型对这类文本有区分度
/gallery scan              扫目录
/gallery index auto        建索引,睡前开跑
/gallery embed auto        转向量
/gallery search 黑丝 车里    试检索

每一步都能独立验证,出问题不会拖到下一步。指令在导演控制台里发。


视觉解析

相册的一切都建立在「把图读成文字」上。这块的配置比别处都重要——配错了整条链路不工作,而且失败得很安静

必填

视觉接口格式        openai / anthropic / gemini
视觉 API 接口地址    只填到根即可,插件按格式补路径
视觉 API Key
视觉模型 ID         必须支持图片输入

三家的请求体、鉴权头、图片包装方式完全不同,格式必须选对。用中转站的话基本都选 openai,除非它明确说自己转发原生格式。

格式 地址填 补成 鉴权头
openai https://api.openai.com/v1 …/v1/chat/completions Authorization: Bearer
anthropic https://api.anthropic.com …/v1/messages x-api-key + anthropic-version
gemini https://generativelanguage.googleapis.com/v1beta …/models/{模型}:generateContent x-goog-api-key

Gemini 的 Key 走请求头而不是 ?key= URL 参数——免得密钥出现在 URL 里被各级日志抄走。模型名会拼进路径,所以直接写 gemini-2.5-flash,不要写成 models/gemini-2.5-flash

配好发 /vision test,它拿一张真图跑一次完整请求,先回显解析出的格式、模型和最终地址,配错了一眼能看出来。

跑成人向图库的三个开关

这三项决定通过率能有九成还是零,跟提示词写什么关系不大:

提示词填「视觉系统提示词」那一栏。 同一份文字放「视觉解析提示词」(user 位置)会被 Gemini 在输入侧直接判死——blockReason,六到八秒返回、连 candidates 都没有;放 system 位置才会正常生成。位置不对时,提示词内容怎么改都没用。「视觉解析提示词」那一栏只放一句「描述这张图片。」

Gemini 思考预算填 512。 不限制的话模型会在思维链里反复评估内容——这张图有多露、该怎么写——琢磨的过程本身撞安全策略,它还没开始写就已经被判了。限住预算它直接开写,通过率大约翻倍,单张耗时也从一分钟降到二十秒。另外思考 token 跟正文共用「最大输出长度」,不限制时它能先吃掉四五千。

Gemini 安全阈值选 OFF,不是 BLOCK_NONE。 OFF 更彻底,但只有较新的模型认,老模型收到会报 400。

提示词里标签行放最前面

如果你的提示词要求模型输出一行结构化标签(分级、季节、水印、遮挡、关键词),把它放在输出的最前面而不是末尾——先把「这是什么尺度、什么场景、该被搜到什么」定下来,再展开去写描述。这一条能显著提高通过率。插件两种位置都认,所以改提示词不用动代码。

失败的四种

插件把上游的失败分得很细,因为处理方式完全不同:

类型 表现 处理
配置错 key 错、模型名错(400/401/404) 立刻中止整批,不浪费调用
上游故障 限流、503、超时 重试到「单张图最多尝试几次」
生成中被拦 finishReason=PROHIBITED_CONTENT 重试到「被内容策略拦掉时最多试几次」
输入侧判死 blockReason=PROHIBITED_CONTENT 重试到「送进去就被判死时最多试几次」

后两种都是 HTTP 200、正文空着,从状态码完全看不出问题,上游还记成功照常计费。它们的重试价值差一个数量级——生成中被拦带采样随机性,换一次常常就过了;输入侧判死是对图本身的判定,重发多少次结果都一样。所以次数分开配,默认 2 和 1。

批次报告会把这笔账算给你看:

索引结束:成功 1260 张,失败 320 张。
API 调用 1580 次,其中 320 次被内容策略拦掉(生成中 260 / 输入侧 60,这些上游算成功照常计费);重试救回 130 张

「重试救回几张」是决定次数该填几的唯一实证依据——一直接近 0 就调到 1 省钱,救回不少就往上加。

重试用尽不会算成这张图的失败次数。 上游的锅不该让图片背,否则上游挂一夜就足以把整个图库标成坏图。

输出校验

拿到正文不等于拿到了描述,这几种要拦下来:

情况 怎么发现
模型拒答 「我无法满足这个请求」被当成描述存进库
思维链漏进正文 中转网关不打 thought 标记时,正常路径拦不住
被 max_tokens 掐断 上游在 finishReason 里直说,比让模型自己写结束标记可靠
敷衍了事 「描述最少多少字才算数」兜底

存进去比失败严重得多:这些文本会跟着转成向量,把语义检索一起带偏,而且日志里什么都看不出来,只表现为「搜出来的图莫名其妙」。/gallery clean 可以事后揪出这几类。

附加请求参数

插件只做了通用的那几项,其余从「附加请求参数」塞进去,按你选的格式写一段 JSON,合并进请求体顶层(同名的字典合并,其余直接覆盖)。OpenAI 格式调采样:{"temperature": 0.3}


相册

相册目录    /gallery

递归扫所有图片(jpg / jpeg / png / webp / gif),非图片文件自动忽略。

分类

按博主分文件夹的话,在每个博主目录里放一个 .archive 后缀的文件,那一级的目录名就成为分类名

/gallery/twitter/1346790821wd@某人/state-xxx.archive   ← 标记文件
/gallery/twitter/1346790821wd@某人/1813912299390087601-2.jpg
/gallery/aiimages/gen001.png

为什么不靠层级猜:图库可能是 twitter/博主名/ 两层,也可能是 aiimages/ 一层,博主目录下还可能再分子目录。标记文件把这件事说死。分类名显示时会去掉 用户ID@ 前缀,所以上面那个显示成 某人

图片时间

推特下载的文件名就是推文 ID(snowflake),高 42 位是毫秒时间戳,能还原出真实发推时刻——比文件 mtime 准得多,mtime 是下载时间,一次批量下载会让几千张图挤在同一天。插件自动识别 17~20 位数字的文件名(含 -1 -2 这类媒体序号),解不出才退回 mtime。不需要这个行为可以关掉。

索引与维护

/gallery index auto     后台跑到全部完成
/gallery index 50       跑 50 张就停,适合试水
/gallery index stop     停掉,进度不丢

跑起来之后每五分钟汇报一次,把「跑到哪了、还剩多少、按这个速度还要多久、最近一次为什么失败」一次讲清楚:

📇 索引中:本轮成 300 / 败 20,全库 1200/2040,还剩 800 张,按当前速度还要 2.5 小时
 速度 320 张/小时,API 调用 1580 次
 被内容策略拦掉 320 次(生成中 260 / 输入侧 60,这些上游算成功照常计费)
 重试救回 130 张
 最近一次失败:被 max_tokens 掐断

控制台指令超过三秒还没跑完,会先回一条「⏳ 执行中」——长指令发出去不会石沉大海。

点着用,不用记参数

要带参数的几个指令,不带参数发出去就给一排按钮,点一下等于替你把那条指令发出来:

指令 点开是什么
/umo 所有会话列一排,点一下直接绑定(不用复制 UMO)
/gallery 扫目录 / 建索引 / 转向量 / 测区分度 / 揪脏描述……
/rec 事实 / 日记 / 事件 / 日程 / 纪念日 / 排期 / 情绪
/plans 每条排期后面跟一个「取消」
/vision 跑一次自检 / 补跑旧图
/proactive 现在就发一条
/help 常用入口直接可点

按钮携带的就是一行控制台指令,点击后原样重放——跟排期到点执行是同一条路,不存在"按钮能做而打字做不到"的事。Telegram 的按钮数据有 64 字节上限,UMO 长了装不下时会自动走令牌;插件重载后旧消息上的按钮点了会提示重发,不会静默失效。

指令 用途
/gallery scan 扫目录登记新文件,幂等,可反复跑
/gallery scan prune 顺带删掉磁盘上已经不存在的记录
/gallery scan reset go 清空整个库重扫(描述和向量一起没,要二次确认)
/gallery polish 清洗存量描述,见下
/gallery retry 失败计数清零,让跳过的图重新排队
/gallery clean 揪出拒答、思维链、过短的脏描述
/gallery redo g123 把某张退回重跑
/gallery audit 看关键词行的质量分布
/gallery show g123 看某张的完整描述;不带编号则随机抽一张

/gallery polish 做两件确定性的修正,不用重跑索引:

  • 删掉标签行里硬拼出来的长段(显手强扯撑举扩张腔缝全绽直露态 这种,十个汉字以上又不含逗号和字母数字的,几乎全是模型凑出来的字堆);
  • 把「一名女性」「该女性」「画面主体」这类泛称换成配置里的「主体角色名」——约四分之一的描述里混着这种写法,搜角色名时那些段落就漏了;
  • 顺带重新解析分级和季节,老数据靠这一步就地补上。

这比在提示词里跟模型较劲可靠,而且对存量数据也能补做。画面里经常出现别的女性时别开「主体角色名」那一项。


分级

每张图会被标上尺度,六档由轻到重:

生活 → OOTD → 性感 → 诱惑 → 露点 → 淫荡

一张图可以同时占相邻的两档OOTD+性感),但不能跳级——生活+露点 讲不通,那是判错了而不是跨度大。所以库里只可能出现 11 个值:6 个单档 + 5 个相邻组合。「生活」和「日常」是同一档的两个名字,写哪个都行。

挑图时按档筛

她调 browse_gallery 时可以填档名,也可以用平常说话的词:

说法 对应的档
日常 / 平时 / 正常 / 普通 / 随手拍 生活
穿搭 / 搭配 / 试衣 / 换装 OOTD
勾人 / 诱人 / 勾引 / 撩 / 撩人 / 挑逗 性感 + 诱惑
骚 / 好骚 / 骚货 / 母狗 / 婊子 / 浪货 露点 + 淫荡

/gallery search 里则只认档名本身——「骚」「勾人」这些词在描述正文里也到处都是,摘走当筛选条件反而搜不到想搜的东西。要筛档就直接打档名:/gallery search 黑丝 露点

分级从哪来

由提示词里标签行的第一段给出,插件只负责解析。所以改分级定义只需要改提示词,六个档名、跨档规则都写在那儿。档名之间不能有包含关系——旧的 SFW / 软NSFW / 硬NSFW 那套就栽在这上面:SFW 是另外两个的子串,词面搜 SFW 会把三档全捞出来。


季节

大热天翻出一身深秋穿搭发过去,比发错尺度还出戏。所以每张图还会标上这身打扮适合什么季节穿出去

春 / 夏 / 秋 / 冬 / 四季

「春+秋」这种组合很常见——薄外套、针织衫两季都穿得出去。跟分级不同,季节不要求相邻。室内、纯特写、看不出的一律标「四季」,任何时候发都不违和。

判据是画面,不是拍摄日期。 夏天拍的冬装写真按冬算,AI 生成图同理。视觉模型看的是室外环境(积雪、落叶、泳池、遮阳伞)和衣着厚薄(羽绒服 / 针织衫 / 吊带),这两样才决定"这身现在穿出去合不合适"。

默认就会优先挑合当下时令的,她不用做什么;她明说要别的时候的才传参(「去年冬天那张」填 、「换季那阵子」填 春秋、强调「现在这个季节」填 now)。

排序分三档,不是两档:

合季的  >  没标出季节的  >  明显不合季的

中间那档是关键——老库、模型漏写、纯特写都归在这里。不知道不等于不合适,它们不该跟三伏天的羽绒服一起垫底。「四季」那一档看场合:默认挑当季时它算合适(随时能发),点名要春秋装时它降到中间——看不出季节的图并不是春秋装。

季节压不过明说的条件。他说「三月那会儿的」,三月的图就是排前面,季节不合也认。

老库是空季节没关系:标签行里只要有这一段,/gallery polish 就能就地补上,不用花钱重跑。


语义检索

一张图的描述会被切成三段,各转一个向量:

取哪几层
环境段 环境与背景
身体段 人物整体 + 身体细节
动作段 互动与动作 + 物品与道具 + 体液与痕迹 + 标签行

为什么要切:一个向量装不下整篇的细节,长的那部分会把短的淹掉。身体细节占一半以上,环境那两百字在整篇向量里几乎看不见——搜「车里」时,所有在车里拍的图整篇向量彼此差得很远,谁都不像,反而某张身体描述里碰巧写了「座椅」的会排更前。按层切开各转一个、取最大相似度,环境词就直接撞上环境段。

三段跟检索侧是对齐的:提示词让她按环境、身体衣着、动作体液三类各给几个词,三类词各打一段。标签行关键词密度最高,无论在描述开头还是末尾都并进动作段。

为什么不留全文段:描述一千五到两千字时身体细节仍占大头,全文段约等于身体段加一点噪声,两个向量高度相关,取最大时几乎总是同时高同时低——多花 25% 的调用换一份重复信息。它还是最长的一段,最容易撞上向量模型的输入上限(gemini-embedding-001 只有 2048 token),一截断砍掉的正好是排在最后的互动与体液两层。

层级标题两种写法都认(第一层:环境与背景 和裸标题、层名带不带「与」)。切不出层时整篇一段兜底,宁可多转一个向量也不让内容漏出索引。

向量模型由插件自己管(面板「向量模型」组:接口格式 openai / gemini、地址、Key、模型、维度)。地址填到 /v1 即可,插件按格式补 /embeddings;整条带路径粘过来也认(.../v1/embeddings、从视觉那栏抄来的 .../v1/chat/completions 都会被归一)。配不通时「测一下」会把实际请求的 URL 一起报出来,不经过 AstrBot 的 Embedding 服务提供商。换了模型或维度会被自动认出来,两个向量库一起清空重建——旧向量是另一个坐标系里的点,混着用不会报错,只会安静地搜错。改了分段方式(比如动了层级归属)不会自动重来,跑一次 /gallery embed redo 就行。

/gallery embed test 拿三段文本探区分度——两段同类不同细节、一段完全无关。区分度低于 0.05 说明这个模型对这类文本没有有效表示,检索会一直不准,得换一个。这个探测很重要:有些模型不会拒绝,只是把所有露骨内容编码得差不多,表现为「搜什么都是那几张」。

检索怎么走

两条路取并集:

词面:按 IDF 加权打分。不取交集——一句「酒店里穿灰丝踩红底细高跟」拆出六七个词,只要一个词对不上(你说「足底」而描述里写「脚底」)交集就空了,整句话什么都搜不到。打分则是漏词只降排名,不至于让图消失。整词一张都没匹配上时会拆成单字重试。

语义:向量相似度,取三段里最高的那个。

/gallery search 的回显会告诉你切了哪些词、实际用了哪些、各命中多少:

切词:泳池
实际用:泳(泳池 一张都没匹配上,已拆成单字)
候选(词面命中 2 · 语义召回 60)

「一张都没匹配上」说明库里确实没有这个词——不是检索坏了。


发照片

图从两处来:

来源 编号 说明
相册 g123 相册目录下的图,扫描 + 索引后可按画面内容检索
聊过的旧图 #3 之前出现在对话里的图,随时能重发

她从相册发出去的图会自动进聊天存档,所以之后也能用 #N 重发、能被 find_photo 找到。

排序完全确定。 没有随机,同分按 id 兜底——同一段词每次搜出来必须是同一批,否则「上次那张」会在多次检索间漂移。结果的变化只来自发送历史(发过的会降权),那是可控的、有意义的变化。

时间条件是分层,不是加权

排序键从高到低:

你明说的条件  >  默认偏好  >  匹配度  >  文件时间新的  >  id

满足条件的整体排在前面,组内才比匹配度。 不用加权是有原因的——「三月那会儿的」意思是在三月的图里挑匹配度最高的,加权会让一张匹配度稍高但月份根本不对的图挤上来,那不是人想要的。不满足的仍保留在后面兜底,避免条件太严时一张都返回不了。

她在对话里听得出你要哪种,调工具时传参:

参数 什么时候传 效果
prefer_sent=recent 你说「上次发的那张」 窗口内发过的整体排前(窗口可调)
prefer_sent=fresh(默认) 没特别说 没发过、或早就过了窗口的优先
around=03 / 2026-03 你说「三月那会儿的」 那整个月的图整体排前
rating=诱惑 你要哪种尺度 只在那一档里翻
season=冬 / now 你要哪个季节的 季节

want_photo 是另一个入口:她只说「为什么想发」(他让我拍一张、想给他看今天的穿搭),由轻量模型连着最近几轮对话一起看,替她想检索词,再走同一套检索。不确定该搜什么的时候用它更好。

怎么想词由面板「相册」组的检索造词提示词决定,它得跟视觉解析提示词用同一套词——库里写的是「骚逼」,搜「下面」一张都出不来。默认那份把三件反直觉的事讲清楚了:词面是加分制不是筛选制(多给词只影响排序,不会搜不到)、描述被切成三段各转向量所以环境/身体/动作三类词要各给几个、尺度和季节是参数不写进检索词。原理见 docs/retrieval-prompt.md


生图

相册是她照片的第一来源;他点名要的场景相册里翻不到时,她会用 generate_photo 现场"拍"一张,你也可以在导演台用 /generate 直接让她拍。

提示词是先想、再写的

不是把情境原样丢给模型。每次生成先由轻量模型看着情境 + 最近几轮对话判断三件事,再写成拍摄稿:

  • 拍什么——「代表赤道无风带的图」这种意向要落成看得见的东西:死寂的洋面、垂直的日光、纹丝不动的帆。
  • 画幅——竖版 832×1216(人物、全身)/ 横版 1216×832(风景、开阔场面)/ 方版 1024×1024(特写、静物)。她自己选,你不用指定。
  • 她入不入镜——聊她就入镜(带上外观基准和锚点图保证是同一个人),聊风景、静物、天气就不入镜。

写出来的是两段摄影语言:

总视图:24mm 广角贴海面平视,正午顶光垂直落下,硬光高反差,f/8 全景深,青蓝到铅灰冷调
九宫格:
左上:积雨云的白色云顶,边缘过曝      上中:惨白天空,太阳在正上方      右上:远处层积云压在地平线
左中:空旷海面,反射云影            正中:帆完全垂下的船影,纹丝不动   右中:镜面海水,看不出流向
左下:深蓝近黑的近处水              下中:船体倒影,几乎对称          右下:一小片静止的马尾藻

九宫格逼着模型把每一格都填满,比一句「一片死寂的海」信息量大得多。这份模板在面板「生图」组可改。

九宫格只活在规划阶段。 发给生图模型之前会压成一段单幅画面的描述——「左上角积雨云的白色云顶;上方中间惨白的天空…」写在同一行,开头明说「单幅照片,一个完整连贯的画面」,负面词里还压着 collage, grid, split screen。原样把「九宫格:」加逐格分行发出去,模型会照字面理解,真给你画一张九张照片拼起来的图

还会同时给一串英文标签

NovelAI 只认 danbooru 英文标签,中文句子对它是噪声——读不懂就退回它自己的先验,画出一个站着的动漫女孩、纯白背景、还带线稿感。所以规划时会把同一个画面另写一份标签串:

1girl, solo, lying, on stomach, on bed, indoors, bedroom, pajamas, loose clothes,
long hair, hair down, wet hair, chin rest, magazine, open book, pillow, blanket,
barefoot, lamp, warm lighting, night, depth of field, best quality, absurdres

两个细节是硬保证的,不靠模型自觉:主体标签(入镜 1girl, solo / 不入镜 no humans)和质量标签都由代码补齐——漏了前者它一定给你画个人,漏了后者它会往草图漂。负面词也换成 NAI 自己那套 UC(中文负面词它同样读不懂)。

中文摄影稿仍然保留,给 NanoBanana 那类吃自然语言的后端用。

后端:一主一备

不再是一串优先顺序 + 每个后端各一套配置。两个槽,各选一个供应商类型;主用挂了才走备用,没有第三层。

主用供应商    api / comfyui / novelai
主用地址      完整端点,写什么就发什么
主用 API Key
主用模型

备用供应商    同上。地址或 Key 留空即不启用

api 类型的协议由地址决定,不猜

地址长这样 走的协议 鉴权头 图在哪
…/v1/chat/completions openai 兼容 Authorization: Bearer choices[0].message.images[0].image_url.url
…/v1beta/models/{模型}:generateContent gemini 原生 x-goog-api-key candidates[].content.parts[].inlineData.data
…/v1/images/generations grok / DALL·E 风格 Authorization: Bearer data[0].b64_json

认不出的地址直接报错,把三种写法列给你——中转站各开各的,同一域名下三种端点可能都在、也可能只开一个,补错了就是个看不懂的 404,填全反而最省事。

不发负面提示词。 Gemini 这类接口根本没有 negativePrompt 字段(generationConfig 里只有 imageConfig),拼进正文就是当描述送进去——对扩散模型负向是独立通道、做减法,对语言模型驱动的生图,「避免出现拼图」里的「拼图」照样进注意力,等于自己往里塞。所以约束一律靠正面表述:要单幅就写「一次快门拍下的单张照片」,而不是「不要拼贴」。negative 只发给真有负向通道的 NovelAI 和 ComfyUI。

画幅走 generationConfig.imageConfig.aspectRatio(竖 3:4 / 横 4:3 / 方 1:1),由代码按她选的画幅自动映射。把尺寸写进提示词文字是没用的——网关会静默忽略,不报错、图照出、尺寸不对,最难查的那种。

类型专属的旋钮是全局一份,不跟着槽复制:API 出图尺寸1K/2K/4K,直接影响计费,4K 响应体能到 6.5 MB)、ComfyUI workflow 文件(用 {POSITIVE} {NEGATIVE} {SEED} {WIDTH} {HEIGHT} 占位)、NovelAI 步数(默认 24,28 以上收益很小)。NovelAI 地址留空则用官方。

参考形象

参考形象路径:填一张图或一个目录(目录取前 3 张)。她入镜时带上它走图生图,这是保证"每次都是同一个人"的主要手段。留空则用数据目录下的 anchors/

拍风景、静物、天气时不会带——那由提示词规划阶段的「她入不入镜」决定,带着她的照片只会污染画面。路径填错也不会让生图失败,只是这次少个参考图,日志里会说。

类型 怎么送这张图
api · openai 协议 content 数组里并列一个 image_url 的 data URI
api · gemini 协议 parts 里一个 inline_data
novelai 切到 action: "img2img",强度在面板里调(默认 0.6,越大越放飞、越不像参考图)
comfyui 由你的 workflow 自己处理(LoRA / IPAdapter / InstantID)

图片记忆

要解决的问题

AstrBot 把图片以 base64 永久写进对话历史,之后每一轮都原样重发。几十张之后上下文里全是图片,既烧 token 又稀释注意力。但直接删掉又不行:你会说「昨天那张」「黑丝那张」,她得认得出来。所以这里的做法是把图片换成文字,留在时间线上

两层描述

一张图存两份文字,用途不同:

谁写的 长度 内容
目录层 主模型(她自己) 一句话 她的视角和当时的语境:「阿泽昨天加班时拍的」
细节层 独立视觉模型 几百字 客观画面:构图、衣着材质颜色、物品、文字、光线

目录层跟正文一次生成——上下文里出现没描述过的图时,插件注入一段请求,她在回复末尾附一段标记,插件抽走存档并从要发出去的内容里剥掉,你看不到。不额外调模型,而且此刻她正看着图、也知道当时在聊什么,写出来的话带着她自己的说法。细节层在图片首次进入上下文时异步调视觉 API 解析,不阻塞回复。

两层的价值在于信息类型不同,不只是详略不同。你说「昨天加班那张」走目录层,说「有咖啡杯那张」走细节层。而且能跨层组合:「加班 咖啡杯」照样命中。

检索

工具 做什么 代价
看占位 折叠后的占位就在对话里,读一眼就认出来
find_photo 按关键词 + 日期搜存档,两层一起搜 一次工具调用
inspect_photo 查某张图的画面细节,只回文字;相册的 g123 也认 一次工具调用
recall_photo 把原图整个塞回上下文重新看 一张图的 token

顺序是有讲究的:占位就在眼前,不需要检索;find_photo 查的是存档、不依赖上下文,所以对话压缩掉占位后它照样能找到;inspect_photo 回答「那张图里有什么」而不用重新传图,比 recall_photo 便宜一个数量级。候选不止一张时,提示词要求她反问是哪张,而不是自己挑一张当成就是那张。

recall_photo 是让她自己再看一眼,不是发给你。要把图发给你是 send_photo

上下文瘦身

「上下文里最多保留几张真图」设成正数后,只保留最近 N 张,更早的换成文字占位。

⚠️ 折叠会写回对话历史,不可逆。 但图片在首次进入上下文时就已存到磁盘,原图不会丢,recall_photo 随时能取回。

这块有先后依赖,别一次全开。默认不折叠——先把描述链路跑通再开:

1. 保持「上下文里最多保留几张真图」= 0,正常聊天时发几张图
2. 日志里搜「收到 N 条图片描述」        ← 目录层在工作
3. 确认标记没有漏到聊天窗口
4. 填好视觉 API,/vision test 确认能通
5. /vision backfill 给之前的存量图片补细节记录
6. /presence 看数字对得上(存档 / 目录层 / 细节层)
7. 以上都正常,再把那一项改成正数

顺序反了的话,图片会在还没有任何描述时就被折叠,那张图就再也认不出来了(原图还在,但没有能检索的文字)。细节层事后能用 /vision backfill 补,目录层补不回来——那句带语境的话只有在她当时看着图的时候才写得出来。


发动态

她往自己的 Telegram 频道发动态,可以带图。

为什么用频道而不是简介

Telegram bot 的「简介」(setMyDescription只在你还没和它对话过时显示一次,聊过之后永远看不到。频道则是真正的时间线:一条条累积、能往回翻、有推送通知——行为上接近朋友圈。

一次最多 10 张(Telegram 媒体组上限)。单张走 sendPhoto,多张走 sendMediaGroup。正文超过 1024 字符时塞不进 caption,插件会先发图、再补一条纯文字。

说不说,什么时候说

post_moment 有个 mention_now 参数,决定发完要不要在聊天里主动提这件事。真人不是每条动态都会特意说一嘴——想让你立刻知道就传 true,想让你自己刷到、或者这条她暂时不想解释,就传 false 然后正常聊别的。

她记得自己发过什么

每次对话前会把历史动态按时间戳插进上下文,和聊天记录穿插在同一条时间线上,而不是附在末尾当成一份清单。所以她知道「那条动态是前天下午发的,当时我们正在聊……」。标着「当时没跟他提」的,是她发的时候故意没说的——你自己刷到来问,她再决定说不说,但她一直是知情的那个。

频率

冷却和每日上限只约束她的自主行为,不约束你的手动指令/moment 任何时候都能用,不消耗当天配额、不重置冷却计时。手动发的仍会记进历史,她能看到自己频道上有这条。触发限制时工具会返回一句给她看的话(如「现在还发不了动态,距离上一条还差 47 分钟」),她知道发不成也知道原因,不会反复重试。


头像与签名

头像从候选目录里随机挑,只读 jpg / jpeg——Telegram 头像接口只收 JPEG,png 会被忽略。可以建子文件夹作为分类,子文件夹名就是她调用时能指定的分类。

签名是资料页上那句会变的话(setMyShortDescription,120 字符以内),点开她的头像就能看到。它会覆盖上一句、没有历史记录,所以适合放此刻的状态或心情;想记录某件事、想让你收到通知,那是动态的活儿。

头像和签名都是 bot 全局设置,不按用户区分。多用户场景建议一段关系用一个独立 bot。

Telegram 没公布这两个接口的频率上限,间隔太短可能触发限流。填 0 表示不限制,但不建议。


生命模拟

她不只是"角色加了人设":昨天聊过的事她今天记得,她自己做过的事她知道,她的生活在你不在的时候也在继续。这一层可以整个关掉(配置里的「启用生命模拟层」),关掉后上面那些存在感功能照常工作。

人格怎么写

她是谁,全部写在 AstrBot 的人格设定里(WebUI →「人格情景」)——身份、性格、说话方式、朋友圈、几点上班、雷区,一份写完。这是唯一的人设来源,插件不复制、不提取、不缓存:每次生成实时读取,你改了人设立刻生效

想要她更粘人、更毒舌、更文静,改的就是这段人格。 一切表现由人格推导,系统不设「粘人度」这类数值旋钮。

几条经验:别写成助手腔(「我可以帮你…」);把口癖、雷区、说话习惯写具体(模型很吃这个);NPC 的名字与设定一次写死(下次变成小美就穿帮);把作息写清楚(「工作日早 8 点起,周日单休」——每天问她几点起几点睡时要用)。

人格里的 tools 字段留空(= 全部工具)。设成 [] 会禁掉全部函数工具,她将调不了相册、发不了照片,而且不报错。

她的记录

人格是固定的,但她的生活在长——那部分归插件管,全部是记录,不是配置:

记录 谁创建 什么时候消失
日程 作息每天问她一次;约好的事跨天存着,聊天里定下就记 过期清理
事实 对话空闲后自动沉淀;临场编的设定立刻固化 失效且久未更新
日记 / 周记 每晚、每周她自己写 长期保留(她的内心世界)
事件 她做了什么就记什么(换头像、发动态、主动找你) 讲过且过期后
纪念日 周记复盘时她自己记;你也能加 一次性的过完就清
排期 /plan 或她自己的打算 执行完即销毁
情绪 事件触发 半衰期归零
演化状态 关系阶段、签名、外观基准 常驻,被新值覆盖

每条都能手动增删改——面板「记录」页里每条记录是一张卡片,正文可以直接改,旁边就是「保存」和「删除」;也可以用控制台:

/rec                          总览
/rec f                        看事实(f12 …) d 日记 · e 事件 · s 日程 · m 纪念日 · p 排期 · o 情绪
/rec add f 他不吃香菜
/rec add m 2026-04-20 认识的日子 since      加纪念日(since = 用来算「认识第 N 天」)
/rec add s 14:00-16:00 和小雅逛街
/rec edit f12 他其实爱吃香菜了
/rec del e5
/rec set stage 稳定            改单值状态(stage / appearance / signature / avatar)

日程分两层:

作息(几点起、几点睡)确实是按天的,每天问她一次。为什么不做成配置项:每个人写人设的方式都不一样,从自由文本里稳定提取不出来;就算提取出来,你一改人设它就成了过期数据。让她自己说——人格在上下文里,她当然知道。心跳只读记录,没有记录就不做作息假设(当她随时在线)。

已约好的事是跨天的、稀疏的、带具体日期的:2026-08-12 14:00 跟小雅逛街。来源有两个——聊天里真的约定了什么(记忆沉淀那一趟顺带抽出来,必须同时有明确日期和时刻才算,「改天吧」「有空一起」不算),或者你手动加(/rec add s 2026-08-12 14:00-16:00 跟小雅逛街,不写日期就是今天)。定好的事她提前七天就知道,不会到当天才想起来。

不铺满一天:真人的日程大部分时段是空的,只有少数几件事被钉在某个时刻。没安排的时段就说没安排,让她自己临场发挥——编出来的「她此刻在追剧」经不起追问。

排期的区别:日程到点是她变成在做那件事(只改状态),排期到点是一个动作真的执行(发消息、发照片)。一个是她的生活,一个是你的遥控器加了定时。

她临场编的细节(「我妈是老师」)会立刻固化成事实记录,之后永远一致——下次说妈妈是医生就穿帮了。

四层记忆

内容 行为
工作记忆 最近对话 由 AstrBot 对话历史承载
核心小抄 他是什么样的人、我们现在怎么样、没做完的约定 认识加深时她自己修订
结构化事实 生日 / 喜好 / 禁忌 / 经历等原子事实 对话空闲后自动沉淀,可更新可失效
情景记忆 每天睡前写日记,周日写周记复盘关系 语义召回 + 时间衰减

久远又不重要的事允许她记得模糊(「你好像说过……是吧?」),你一确认她就恍然大悟——完美数据库式的记忆反而假。重要的事(事实层、纪念日)不适用遗忘。

日记周记用会话当前模型写(质量优先),记忆沉淀走插件自管的轻量模型(面板「轻量模型」组:接口格式 openai/anthropic/gemini、地址、Key、模型;留空则也用会话当前模型,只是贵一点)。记忆召回和相册检索共用插件自管的那个向量模型,不配则都降级为词面检索。

心跳与虚拟生活

心跳(默认 5 分钟)全部是纯代码:问一次今天的作息、推进「此刻在干什么」、情绪半衰期衰减、检查日记周记到点、计算主动意愿、执行到期排期。她洗澡时不回消息、深夜被吵醒带困意——这些都注入在每次对话里。

她的负面情绪(委屈、吃醋)零后果自动消散:不哄也会自己好,哄一句立刻雨过天晴;表达永远是「可爱」而非「指责」。冷暴力、不回消息作为惩罚、必须道歉才恢复、翻旧账、制造愧疚——这些是写死的硬约束,人格也覆盖不掉。

心跳还会按低概率掷签让她自主发动态、换头像、改签名(冷却是天级的,特别日子有加成)。每件事都记录「内容 + 真实动机 + 提及状态」:她可以主动炫耀,也可以等你自己发现——发现时用当初的真实理由回答,前后一致不穿帮。


主动消息

你不说话时她自己开口,时机由「想不想」决定而不是定时器:到了她作息里的早安晚安时段、饭点想问你吃了没、有想炫耀的新动态新头像、今天是纪念日、或者单纯太久没聊想你了。心跳纯代码打分,过阈值才让模型开口,并把「这次的缘由」带进提示词——她会自然地体现出来而不是报菜名。

防打扰:静默时段内不打扰;连着几条没等到你回就先停下(她会察觉到这件事,下一条不会装作什么都没发生);你一回复计数清零。节奏只有两个参数——最小间隔与最长沉默。

/proactive 看状态,/proactive now 不等了立刻发一条。


导演控制台

你想让她说点什么,但不希望这条指令出现在你俩的聊天里

你 ──/act 跟他说你今天加班到很晚──▶ 控制台 bot
                                      │
                                角色 bot ─────▶ 你
                                (只有她发的这条进上下文)

指令走的是另一条会话,所以既不进她的 LLM 上下文,也不出现在你俩的 Telegram 聊天窗口里。她的对话历史里只会多出她自己发的那条。

跟 BotFather 要一个新 bot,把 token 填进「控制台 Bot Token」,你的 Telegram 数字 ID 填进「控制台管理员 ID」(不填则谁都不理)。这个 bot 由插件自己长轮询,跟 AstrBot 的平台系统完全无关——不用在平台配置里多加通道,也不会两个 bot 抢同一套轮询报 Conflict。

用法

/umo                                       列出所有会话,每行可整条复制执行
/link telegram:FriendMessage:8338355157    绑定投递目标
/say 我到家了                               她原样发出这句
/act 跟他说你今天加班到很晚                   她带着人格和最近的对话自己组织语言
/photo 在前台拍一张                          她按这个方向去相册里挑一张发出去

/link 绑定时会当场探查这个会话有没有对话并报告历史条数和人格字数——UMO 拼错、或者人格没读到,立刻就能看出来,不用等到 /say 之后才发现。一个控制台来回切,就能管多个角色。

⚠️ /act 只会让她说话,不发图。 要图用 /photo <方向>——插件会让她想好关键词,再拿去相册里检索。撞上「拍一张」这类写法时 /act 会当场提醒一句。

/act 出来的是她的语气和分段习惯,不是你的措辞。发出去的话会写进她的对话历史,这一步不能省:否则她下一轮不知道自己说过这句,你让她说「今天加班到很晚」,五分钟后她可能问你「你今天忙吗」。

/photo /generate /moment /avatar /signature 五条留空时会让她自己想发什么、换哪张、改成什么——想好之后才执行,执行结果也会写进她的上下文。

/generate 留空时只把她想的画面报给你、附一个「就这么拍」的按钮,确认了才真去生成——检索几乎免费,生图每张要钱要时间,不该手滑就烧掉。

让她先别回话

想连着说几句而不被每句都接话,或者只是想把事情说给她听、不需要她此刻回应:

/noreply        一直静默,直到你发 /reply
/noreply 30     静默 30 分钟,到点自动恢复
/reply          现在就恢复

这期间你说的话仍然进她的记忆,解除之后她全知道,接下来那句是带着这些说的。这也是这条指令的全部意义——不是屏蔽你,是让她先听着。指令一律放行,包括 /reply 本身,否则静默就再也解除不了了。

定时编排

/plan 20:00 /act 提醒他吃药      到点她像自己想起来一样去说
/plan +30m /say 我到家啦          半小时后原样说这句
/plan 明晚8点 让她发条动态         自然语言也认,会解析成指令
/plans                          看排期 · /plans cancel <id> 取消

到点由心跳执行,回执发回你排期时所在的控制台会话。载荷就是一行控制台指令,走的是和你手敲完全相同的那条路。

指令速查

指令 用途
/umo /link 列会话、绑目标
/say /act 让她说话
/photo 以她的身份发一张相册里的照片
/generate 现场生成一张再发 · /generate 阳台上的晚霞 | 今天天好好
/moment /avatar /signature 发动态、换头像、改签名
/noreply /reply 让她先别回话 / 恢复
/proactive 主动消息状态,now 立即发一条
/status 她的生命状态:此刻、日程、心情、记忆
/rec 记录:看 / 加 / 改 / 删
/diary /events 偷看日记、看她最近做的事
/plan /plans 定时编排与排期管理
/gallery 相册总览、索引、检索、维护
/vision 视觉 API 诊断,给存量图片补细节
/presence 插件状态:动态数、各项冷却、图片存档
/help 全部指令一览

语音

她用 send_voice 发原生语音条(带波形,不是文件附件):撒娇、道晚安、长长的心里话、哄你睡觉这种时刻才用,平时打字就好。

引擎走 AstrBot 的服务提供商体系(GPT-SoVITS 自部署 / CosyVoice / Fish Audio / Edge TTS 等),把 Provider ID 填进配置即启用,声线克隆好之后换引擎不影响其他功能。转码依赖 ffmpeg(AstrBot 官方 Docker 镜像自带)。

你发给她的语音由 AstrBot 主管线转文字(给 AstrBot 配置 STT Provider 即可),本插件不参与。


Web 面板

AstrBot WebUI → 插件 → AstrLover → Pages,三个标签:

  • 总览:此刻在做什么、心情、今日日程、模块健康(每项都写明为什么是 ❌、该去哪配)、一键导出记忆包;
  • 记录:按类型(事实 / 日记 / 事件 / 日程 / 纪念日 / 排期 / 情绪 / 状态)浏览,每条一张卡片,正文直接改,旁边就是保存和删除(删除是两段式:第一下变成红色的「再点一次确认」,四秒不点自动取消——插件页的 iframe 没开 allow-modals,用不了浏览器的确认框);底部一行可以新增;
  • 设置:74 项,分组排列、改完即时生效、视觉与向量旁边有「测一下」。

对话历史不在这里看——那是 AstrBot 自己的对话管理(WebUI →「聊天记录」),插件不重复造一个。

面板经 Dashboard 登录鉴权,等同最高权限,切勿把 Dashboard 暴露公网。重交互(相册索引、让她说话)在控制台 bot 里做,面板负责"看"和"改记录"。


数据存储

她的对话不在插件里:聊天记录归 AstrBot 的对话管理,插件写日记和抽事实时现取现用。你在 AstrBot 那边清空了对话,她的记忆素材跟着变——不会出现「你删了她还记得」。

data/plugin_data/astrlover/
├── astrlover.db            相册索引、图片存档、记忆、日记、事件、日程、情绪、排期
├── anchors/                参考形象的默认位置(面板没填路径时用这儿)
├── vec/                    向量库(记忆 + 相册三段,FAISS)
├── context_photos/         从对话上下文里存下来的图片原图
├── generated/              生图产物(未配置相册目录时的落点)
└── exports/                面板导出的记忆包

相册的原图在你自己的相册目录里,插件只存路径和描述,不复制一份。

单库设计:所有记录在一个 SQLite 里(WAL 模式),备份就是拷一个文件。

v1.0 之前不做数据迁移:升级后如果表结构变了,插件开机自检会发现版本对不上,把旧库改名成 astrlover.db.v<旧版本>.<时间>.bak 让开,重建一个空库并在日志里说明;vec/ 下的向量跟着一起清空。旧文件一直留着,要捞数据随时能捞。相册需要重新 scan + index。上万张图的描述几百 MB 很正常。

删掉某部分的后果:

  • astrlover.db:相册索引、图片编号表、她的全部记忆一起没。编号表没了之后,context_photos/ 里的图就对不上了;相册要重新 scan + index(要重新花一遍 API 钱)
  • vec/:语义检索失效,词面检索照常;/gallery embed redo 可重建
  • astrlover.db:全部记录(日程/事实/日记/事件/纪念日)一起没,她等于失忆重来;人设在 AstrBot 人格里、聊天记录在 AstrBot 对话管理里,都不受影响
  • context_photos/:原图没了,recall_photo#N 发图失效,但两层文字描述还在,检索照常

排障

所有日志都带 [AstrLover] 前缀。

视觉解析一张都不出

按顺序查:

  1. /vision test 看接口通不通;
  2. 提示词是不是填在了「视觉解析提示词」那一栏——要填「视觉系统提示词」
  3. 思考预算是不是留空了——填 512
  4. 安全阈值是不是 OFF。

前三条里任何一条错了,通过率都能从九成掉到零。

索引很慢 / 调用次数远超图片数

看失败类型的分布:

docker logs --since 1h astrbot 2>&1 | grep -oE "blockReason=[A-Z_]+|finishReason=[A-Z_]+" | sort | uniq -c

blockReason 占多数说明图本身被判死,重试无用——把「送进去就被判死时最多试几次」调到 1 能省一大笔。finishReason 占多数说明是生成过程被掐,重试有效,看批次报告里「重试救回几张」决定次数。并发也值得怀疑:开太高时中转商的账号池会排队,单张耗时能翻一倍多。

搜不到应该能搜到的图

/gallery search 你的词 会显示切词结果和命中情况。

  • 「一张都没匹配上,已拆成单字」——库里确实没有这个词;
  • 「语义检索不可用」——向量模型没配好(面板「向量模型」组点「测一下」看真实报错),或向量没转(跑 /gallery embed auto)。

如果是口语/正式说法对不上(描述里写「黑色丝袜」而你搜「黑丝」),那是提示词的事——要求模型把口语简称一并写进描述。

其它

现象 检查
换头像报版本太低 pip show python-telegram-bot,需要 ≥ 22.7
发动态失败 频道 ID 是否正确;bot 是否是频道管理员且有发布权限
找不到图片 路径是否为容器内绝对路径;头像目录里是否有 jpg(png 不算)
报错说字段不认识 视觉接口格式选错了——拿 openai 的请求体发给 Anthropic 端点必然如此
网关 504 / 连接被挂断 开「流式接收」
/act 出来的话不像她 日志搜「没拿到人格」——出现即说明生成时没带上人格
/say 发出去了但她不记得 /link 核对。「查不到对话」是 UMO 拼错或那个会话还没聊过
日志有「写历史失败:还没有对话」 目标会话从没聊过,AstrBot 还没给它建对话。先在那个会话里正常说一句
控制台没反应 「控制台管理员 ID」填了没有;填的是不是你自己的数字 ID

已知限制

只支持 Telegram。 频道、表情回应、头像接口都是 Telegram 特有的。

头像和签名是全局的。 改的是 bot token 本身的资料,不按用户区分。

收不到你的反应。 你在频道给动态点了表情,她不会知道——Telegram 的 message_reaction 更新需要额外配置,AstrBot 适配器目前不转发。设计上是单向的:她发,你看,想说什么私聊里说。

接口频率未公布。 Telegram 官方没有公布 setMy* 系列的调用频率上限,超限会返回 429。这就是默认给了较长冷却的原因,别把冷却全设成 0。

头像不能复用。 Telegram 明确禁止用 file_id 复用头像,每次都是重新上传。

对话压缩会吃掉占位。 AstrBot 的历史压缩会把折叠占位连同描述一起压掉。find_photo 查的是插件自己的存档、不受影响,但对话里那行占位会消失——她需要主动检索才能想起来,而不是一眼看到。

图片记忆的目录层补不回来。 细节层可以事后用 /vision backfill 补,目录层不行。

相册索引是一次性成本。 上万张图要跑几个小时、几万次 API 调用,这笔账在开跑前就该算清楚——批次报告里的「API 调用 N 次」就是给这个用的。

生图一致性取决于后端与锚点。 NanoBanana 走参考图最稳;ComfyUI 靠你 workflow 里的 LoRA/IPAdapter;NovelAI 仅提示词描述,容易漂移。

挂机成本是可数的。 心跳纯代码不耗 token;模型只在聊天回复、过阈值的主动消息(每天个位数)、日记 1 次/天、周记 1 次/周、记忆沉淀(空闲批处理)、相册索引(一次性)时出场。


License

MIT

About

Immersive AI companion plugin for AstrBot (Telegram)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages