拟真 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_gallery、want_photo、send_photo、inspect_photo、find_photo、recall_photo、generate_photo、send_voice、post_moment、change_avatar、update_signature、react_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 即可),本插件不参与。
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] 前缀。
按顺序查:
/vision test看接口通不通;- 提示词是不是填在了「视觉解析提示词」那一栏——要填「视觉系统提示词」;
- 思考预算是不是留空了——填 512;
- 安全阈值是不是 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 次/周、记忆沉淀(空闲批处理)、相册索引(一次性)时出场。
MIT