ZOYKUHot Board

death34018-hue/aionshomeTài liệu kỹ thuật

Aion — 一个支持长期记忆、语音交互、摄像头视觉的伴侣。 A self-hosted AI companion with long-term memory, voice interaction, camera vision, and smart home integration.

Mục lục (TOC) (49 phần)▾
项目定位技术栈模块化文件结构路由支持的模型硅基流动(api.siliconflow.cn)Gemini(generativelanguage.googleapis.com)AiPro 中转站(vip.aipro.love)哨兵/向量模型(支持独立 Gemini Free Key)已实现功能聊天核心Debug 透明度面板向量记忆库(RAG 重构)语音合成 (TTS) — 服务端流式推送架构摄像头智能监控(Sentinel/Core 双脑架构)Core 主动查看监控([CAM_CHECK])Core 主动查看设备动态([查看动态:n])Sentinel/Core 工作流程语音唤醒 + 半双工通话语音唤醒工作流程视频通话([视频电话])AI 生图([SELFIE:xxx] / [DRAW:xxx])AI 生图工作流程视频通话工作流程音乐点歌([MUSIC:xxx])音乐点歌工作流程日程 / 闹铃 / 定时监控系统定时监控([Monitor:...])密语时刻(BLE 玩具控制)背景记忆浮现(替代旧版“近期记忆注入”)高德地图定位系统高德地图定位工作流程日程/闹铃工作流程或 分割)→ 段落提取 → 图片提取(封面 + 正文图片)→ 存储到 data/books/{book_id}/ → 写入 book.db(books 表 + book_chapters 表) → 返回 book_id 【阅读 + 批注】 GET /api/books/{id}/chapters/{ch} → 返回章节内容(段落数组)+ 已有批注 + AI名/用户名 → 前端渲染段落 + 批注气泡 → 用户点击「AI 批注」→ POST /api/books/{id}/chapters/{ch}/annotate-all → 后端逐段处理: ├ 加载世界书人设 + 查询书名 ├ 获取前 3 章摘要(每章≤200字) ├ 获取最近 15 条聊天消息 ├ 构建批注 Prompt(人设+时间+书名+上下文+段落文本) ├ stream_ai() 流式生成 → 解析 JSON 批注 └ 存入 book_annotations 表 → SSE 推送前端 → 前端实时渲染批注气泡 【选文聊天】 选中文字 → 弹出工具栏「💬 问问AI」→ 点击 → 弹出提问窗 → 输入问题(可选)→ 提交 → 打开聊天面板 → 加载最近 6 条主聊天消息(GET /api/conversations/{conv_id}/messages?limit=6) → 显示上下文 + 分割线 + 用户消息 → POST /api/conversations/{conv_id}/send(SSE 流式) → AI 回复流式渲染为多气泡(按 \n\n 分段) → 支持继续在面板内追问(同一对话) → 所有消息保存在主聊天历史中 ### 小剧场(独立角色扮演聊天) 194. **完全独立** — 小剧场使用独立的数据库表(`theater_conversations` + `theater_messages`),与主聊天完全隔离,所有对话记录**不计入记忆库**,记忆总结时不会涉及小剧场内容 195. **极简 Prompt** — 仅注入选中角色的人设 + 最近 N 条上下文,不注入系统能力(`[MUSIC]`/`[CAM_CHECK]`/`[ALARM]` 等)、不注入记忆/日程/位置/活动日志,干净纯粹的对话体验 196. **多角色管理** — 支持创建/编辑/删除多套角色人设,每个角色可配置独立的名称、人设内容(system prompt)、默认模型、默认温度、默认上下文条数。角色数据存储在 `data/theater_personas.json` 197. **对话管理** — 侧栏对话列表,支持新建/删除/重命名剧场,每个对话绑定一个角色,切换角色时自动更新对话设置 198. **SSE 流式回复** — 与主聊天一致的 SSE 流式输出,逐字显示,等待时显示「思考中/正在输入」循环动画 + 弹跳小圆点 199. **TTS 语音合成** — 完整保留服务端流式 TTS(复用 `tts.py` 的 `TTSStreamer`),支持音色选择和重播。TTS 事件通过 `tm_` 消息前缀与主聊天互相隔离,避免多页面同时播放 200. **茶色暗色主题** — 深棕色背景(`#1e1a16`)+ 茶色强调色(`#c8956c`),与主聊天的暖光风格形成差异化视觉区分 201. **多端同步** — WebSocket 广播 `theater_conv_created/updated/deleted`、`theater_msg_created/updated/deleted` 事件,多设备实时同步 202. **图片上传** — 支持多模态,可上传图片/视频作为附件发送 203. **重新生成** — AI 消息支持一键重新生成 ### 奥罗斯幽林(Ghost Forest TRPG) 300. **独立 TRPG 模块** — 完整的桌游风角色扮演冒险游戏,AI 担任 DM(地下城主持人),玩家通过选项和 D20 骰子推进剧情。数据存储为纯 JSON 文件(`data/ghost_forest/`),不使用数据库,与主聊天系统完全隔离 301. **DM/玩家人设管理** — 支持创建/编辑/删除多套 DM 和玩家人设预设,存储在 `data/ghost_forest/_personas.json`,开始新冒险时选择人设组合 302. **AI 生成剧情大纲** — 玩家提供冒险点子,AI(SSE 流式)生成完整剧情大纲:标题、世界观、主线情节、NPC 列表、关键道具、剧情分支、氛围描写 303. **角色属性系统** — 五维属性(力量/敏捷/智力/魅力/运气),初始各 3 点 + 7 点自由分配。HP 100,最大回合 20(±5 弹性)。属性影响选项的 DC(难度等级)判定 304. **D20 骰子判定** — Three.js 3D 实时渲染 D20 骰子动画,物理模拟旋转和弹跳。投骰结果 + 属性修正 ≥ DC 即成功,影响后续剧情走向 305. **AI 叙事回合** — 每回合 AI 生成剧情叙述 + 3-4 个选项(含属性关联和 DC),支持 D 选项自定义行动。AI 回复以 SSE 流式输出,实时渲染 Markdown 306. **道具系统** — AI 可通过 `[ITEM:道具名:数量:描述]` 指令给予玩家道具,选项可设置 `item_cost` 消耗道具。道具栏实时显示在游戏界面底部 307. **AI 对话历史压缩** — 当 AI 历史超过 16 条消息时,使用 `gemini-3.1-flash-lite` 自动压缩旧对话为摘要,保留最近 6 条原文,减少 token 消耗。压缩前备份完整历史到 `ai_history_full` 308. **前情回顾** — 📜 按钮打开全屏回顾面板,显示所有已完成回合的剧情和结果叙述,方便回顾冒险历程。支持 🔊 按钮回放每回合的 TTS 音频(通过 HEAD 探测已缓存分片) 309. **大结局生成** — 冒险结束时,AI 生成 400-800 字的完整大结局叙事(纯文学风格,不含选项),以 SSE 流式呈现。结局存储在 `session.finale`,重新进入时直接展示 314. **TTS 语音合成** — 复用 `tts.py` 的 `TTSStreamer`,通过 SSE 流式推送 `tts_chunk`/`tts_done` 事件。叙述/选择结果/大结局三个 SSE 端点均支持 `tts_enabled`/`tts_voice` 查询参数,AI 生成叙述后自动按句切分合成语音,前端按序播放。TTS 音频缓存以 `gf_{sid}_r{round}_s{seq}.mp3` 命名,支持回放 315. **动态回合数** — 支持游戏中修改最大回合数(±5 弹性范围),AI 根据剩余回合数自动调整叙事节奏 316. **D 选项自定义行动** — 选项列表末尾固定显示「D. 你想做别的...」输入框,玩家可自由输入创意行动 310. **场外求助(剧场联动)** — 📞 按钮打开半屏聊天面板,连接到主聊天系统。聊天 AI 通过 `theater_session_id` 参数自动获取当前游戏状态(属性/HP/道具/最近剧情/当前选项),以 `[剧场属性:xxx ±n]` 和 `[剧场道具:xxx]` 指令直接修改游戏数据,实现跨系统联动 311. **移动端适配** — 游戏界面采用两段布局(固定状态区 + 统一滚动区),正文和选项合并滚动,手机上正文显示面积最大化。属性芯片紧凑排列(nowrap),触屏友好 312. **会话管理** — 支持多局游戏并行,列表显示标题/状态/创建时间,支持暂停/恢复/删除。状态机:draft → outlined → playing → paused/finished 313. **模型切换** — 游戏中可实时切换 AI 模型(下拉框),支持所有已配置的模型(Gemini/硅基流动/中转站) ### 奥罗斯幽林工作流程 【创建冒险】 选择 DM 人设 + 玩家人设 → 输入冒险点子 → POST /api/ghost-forest/sessions → AI 生成剧情大纲(SSE)→ POST .../generate-outline → 解析 JSON 大纲(标题/背景/NPC/道具/分支) → 分配属性点(5 维各 3 + 7 自由分配)→ POST .../start → 状态 playing 【游戏回合(循环)】 POST .../narrate → AI 生成当前回合叙述 + 选项(SSE 流式) → 前端解析 JSON:narration + options[{key, text, stat, dc}] → 玩家选择选项 → 需要骰子?→ Three.js D20 骰子动画 → POST .../choose {chosen, dice_roll, custom_input} → AI 根据判定结果生成后续叙述(SSE 流式) → 更新 HP/属性/道具/回合数 → 下一回合 【AI 历史压缩(自动触发)】 narrate/choose 完成后 → 检查 ai_history 长度 > 16 → gemini-3.1-flash-lite 压缩旧消息为摘要 → 备份 ai_history → ai_history_full → ai_history = [压缩摘要] + [最近 6 条原文] 【场外求助(剧场联动)】 点击 📞 → 打开聊天面板 → 发送消息到主聊天(携带 theater_session_id) → 后端注入游戏状态到聊天 AI prompt → AI 回复中含 [剧场属性:力量 +2] / [剧场道具:神秘钥匙] → 后端解析指令 → 修改游戏会话数据 → SSE theater_update 事件 → 前端接收 → 刷新游戏 UI 【大结局】 回合数到达上限 / HP 归零 / AI 判断冒险结束 → 点击 🏰 大结局 → POST .../finale(SSE 流式 + TTS) → AI 生成 400-800 字结局叙事 → 存储 session.finale → 状态 → finished 【TTS 语音合成(复用 TTSStreamer)】 narrate/choose/finale 端点接收 tts_enabled=true&tts_voice=xxx 查询参数 → AI 生成叙述文本 → 创建 TTSStreamer(msg_id, voice, sse_queue=queue) → TTSStreamer.feed(narration) → 按 100-200 字切分 → 异步合成 mp3 → 合成完成 → SSE 推送 tts_chunk {msg_id, seq, url} → 前端 enqueueTTSChunk() 按序播放 → 全部合成完 → SSE 推送 tts_done → 前端 finishTTSForMsg() 标记完成 → 音频缓存:data/tts_cache/gf_{sid}_r{round}_s{seq}.mp3 → 前情回顾可通过 🔊 按钮回放已缓存的音频(HEAD 探测分片存在性后依次播放) ### 设备活动日志系统(PC + 手机) 170. **双设备活动采集** — 自动记录 PC 前台窗口和手机前台 App 的使用情况,存储为 JSONL 日志,按日期分文件,保留最近 3 小时 171. **PC 前台窗口采集** — 后台守护线程每 60 秒通过 `win32gui.GetForegroundWindow()` 获取当前窗口标题 + `psutil.Process.name()` 获取进程名,**每分钟无条件记录**(窗口没变也写入,确保摘要时长计算准确),自动过滤 Program Manager(桌面) 172. **Android 前台 App 上报** — `AionPushService` 中独立线程每 60 秒通过 `UsageEvents` API(主)/ `UsageStatsManager`(备)获取当前前台应用包名,POST 到 `/api/activity/report`,**每次轮询都上报**(无去重,服务端摘要层负责合并);同时注册 `BroadcastReceiver` 监听屏幕开关事件 173. **App 名称解析** — 服务端维护 `KNOWN_APPS` 映射表(80+ 常见应用),将包名/进程名转为中文显示名(如 `com.xingin.xhs` → `小红书`、`chrome.exe` → `Chrome`),自动过滤系统应用(桌面、SystemUI 等) 174. **JSONL 存储 + 自动清理** — 每条日志按日期写入 `data/activity_logs/{YYYY-MM-DD}.jsonl`,`cleanup_old_activity_logs()` **每 5 分钟最多执行一次**清理超过 3 小时的旧条目 175. **并发安全** — `threading.Lock` 保护 JSONL 文件读写,防止 PC 后台线程和手机 API 协程并发写入导致数据丢失 176. **10 分钟活动摘要** — `generate_activity_summary()` 将原始日志按 10 分钟窗口分组,**时长权重排序**(主要活动排前面),**carry-forward 状态追溯**(向前找每个设备最后状态填补窗口开头空白),空窗口标记为「没有活动」并自动合并连续空窗口 177. **前端日志查看器** — `/activity-logs` 页面支持「最近 3 小时」和「按日期」两种查看模式,可按设备(全部/PC/手机)筛选,实时 WebSocket 推送新日志,加载后自动滚动到最新位置 178. **前端摘要弹窗** — 📋 按钮打开摘要弹窗(GET `/api/activity/summary`),展示每 10 分钟一条压缩摘要,空闲条目斜体灰色显示,底部统计压缩比 179. **清空日志** — 前端「清空全部日志」按钮(POST `/api/activity/clear`),删除所有 JSONL 文件 180. **依赖** — PC 端需要 `pywin32`(`win32gui`)+ `psutil`,需安装到项目 `.venv` 中 ### AI 联动(设备活动 × AI 交互) 181. **AI联动总开关** — `/activity-logs` 页面顶部「AI联动」开关(`activity_tracking_enabled`),控制所有 AI 与设备活动的交互。关闭后 `[查看动态:n]` 能力不注入 prompt、哨兵/监控不注入活动摘要。通过 `GET/PUT /api/activity/config` 管理,WebSocket 广播 `activity_config_changed` 事件多端同步 182. **聊天中查看动态** — AI联动开启时,Chat prompt 注入 `[查看动态:n]` 能力(n=1~12),Core 可主动查看用户设备动态并生成关怀评论,后端自动 clamp n 值防止异常 183. **哨兵注入活动摘要** — Sentinel 分析截图时自动注入近 60 分钟 6 条活动摘要(格式 `[HH:MM~HH:MM] 摘要`),作为辅助判据帮助哨兵更准确判断用户状态 184. **定时监控注入活动摘要** — `[Monitor:...]` 触发 Core 分析时自动注入近 120 分钟 12 条活动摘要,帮助 Core 结合设备使用模式理解用户行为 ### 设备活动日志工作流程 【PC 前台窗口采集(activity.py PCActivityTracker)】 服务启动 → lifespan 中 pc_tracker.start() → 守护线程循环(60 秒间隔): win32gui.GetForegroundWindow() → 获取窗口标题 psutil.Process(pid).name() → 获取进程名 → 过滤 Program Manager(桌面) → 无条件写入 append_activity_log()(加锁) → 标题变化?→ 是 → 控制台打印 + WebSocket 广播 → 每 5 分钟触发 cleanup_old_activity_logs() 【Android 前台 App 上报(AionPushService activityThread)】 服务启动 → startActivityThread() → 独立线程循环(60 秒间隔): UsageEvents API 查询最近 120 秒事件 → 取最后一个 ACTIVITY_RESUMED 的包名 ├ 成功 → 过滤自身包名 → 无条件 POST /api/activity/report {device:"phone", app:包名} └ 失败 → fallback UsageStatsManager queryUsageStats → BroadcastReceiver 监听 SCREEN_OFF/SCREEN_ON → 立即上报 "screen_off"/"screen_on" ⚠ 需要「使用情况访问权限」(Settings > Special access > Usage access) 【服务端处理(routes/activity.py)】 POST /api/activity/report → resolve_app_name() 包名→中文名(KNOWN_APPS 映射 + 系统应用过滤) → append_activity_log() 加锁写入当天 JSONL 文件 → cleanup_old_activity_logs() 节流清理 >3 小时的条目(5 分钟最多一次) → WebSocket 广播 activity_log 事件 → 前端实时更新 【10 分钟活动摘要(activity.py generate_activity_summary)】 GET /api/activity/summary → read_recent_activity(3h) → 过滤系统应用 + Program Manager → 按 10 分钟窗口分组(时间范围:首条记录 ~ 上一个已结束窗口) → carry_forward:每个窗口向前查找各设备最后状态,填补开头空白 → _summarize_window():设备分组 → 过滤亮屏 → 构建 (app, titles, duration) 段 → 按 display_name 合并同名段(如 TortoiseProc+TortoiseMerge→SVN) → 按时长降序排列 → 格式化为 "小红书 5分钟, 微信 2分钟" → 空窗口标记「没有活动」→ 连续空窗口合并(如 15:20~15:50 没有活动) 【前端查看(activity-logs.html)】 「最近3小时」→ GET /api/activity/recent → 滚动到底部 「按日期」→ GET /api/activity/dates → GET /api/activity/logs/{date} → 设备筛选 → 列表渲染 → WebSocket 监听 activity_log 事件实时追加 → 📋 摘要按钮 → GET /api/activity/summary → 弹窗展示 → 「清空全部」→ POST /api/activity/clear → 「AI联动」开关 → PUT /api/activity/config → WebSocket 广播 activity_config_changed 【AI联动(activity.py get_activity_summary_for_prompt)】 ├ 哨兵定时截图 → get_activity_summary_for_prompt(6) → 近 60 分钟 6 条摘要注入 Sentinel prompt ├ [Monitor:] 触发 → get_activity_summary_for_prompt(12) → 近 120 分钟 12 条摘要注入 Core trigger prompt └ [查看动态:n] → get_activity_summary_for_prompt(n) → 近 n×10 分钟 n 条摘要 → 组装 prompt → Core 生成回复 → 保存 system 消息 + assistant 回复 → WebSocket 广播 ⚠ AI联动开关关闭时,以上三条路径均返回空字符串,不注入任何摘要 ### 爱的印记(AI 礼物系统) 317. **AI 自主送礼** — 每次自动记忆总结完成后,AI 综合判断是否需要给用户送一份礼物。判断依据:今天的聊天是否有特别温馨/感动/有意义的内容、是否是特殊日子(节日/纪念日/生日等)、用户的心情状态。Prompt 注入当前精确时间(年月日星期时分秒)+ 本次总结的所有记忆摘要 + 最近聊天上下文 + 世界书人设,要求 AI 返回结构化 JSON(`givegift` / `image_prompt` / `message`) 318. **硅基流动 Kolors 生图** — AI 决定送礼后,使用 `image_prompt` 调用硅基流动 `Kwai-Kolors/Kolors` 模型(免费)生成 1024×1024 图片。Prompt 约束为 cute cartoon style、不生成真实人物。图片 URL 1 小时过期,后端立即下载保存到 `data/uploads/gift_{timestamp}.png` 319. **礼物弹窗动画(全页面)** — 礼物生成后通过 WebSocket 广播 `gift_pending` 事件。前端任何页面(聊天页 chat.html + 所有子页面 common.js)收到后弹出全屏礼物动画。打开聊天页/子页面时也会自动检查 `GET /api/gift/pending` 并弹窗 320. **礼物盒开启流程** — ① SVG 礼物盒从底部弹跳入场 → ② 用户点击打开 → 播放「打开礼物.mp3」音效 → 盒盖飞走 + 60 个彩色礼花粒子爆炸 → ③ 图片从中心缩放淡入 → ④ 点击图片 → AI 的配图文字滑出 → 「💝 收下礼物」按钮出现 → ⑤ 点击收下 → 整体缩小飞走动画 → POST 标记 received 321. **爱的印记陈列馆** — `/gift` 页面(`gift.html`),暗色画廊风格,3 列缩略图网格展示所有已收到的礼物(缩略图+日期),点击打开详情弹窗(大图+日期+文字+删除按钮),按时间倒序排列 322. **礼物数据** — 存储在 SQLite `gifts` 表(id, image_path, message, created_at, status, received_at),status 为 `pending`(未领取)或 `received`(已领取)。删除礼物时同步清理本地图片文件 323. **不过度送礼** — Prompt 中明确要求 AI「不要每次都送,只在真正值得的时候才送,大部分时候应该返回 false」 324. **测试按钮** — 爱的印记页面右上角「🎁 测试送礼」按钮,取最近 5 条记忆 + 最近 20 条上下文触发完整送礼流程(AI 判断 + 生图 + 入库 + WebSocket 推送) 325. **主页入口** — home.html APPS 数组增加「爱的印记」(`/public/funIcon_0018_爱的印记.png`) ### 爱的印记工作流程 【触发时机(记忆总结完成后)】 _do_digest() 完成记忆总结 + 生成感慨消息 → 调用 gift.judge_and_send_gift() → 构建判断 Prompt(人设 + 当前时间 + 记忆摘要 + 上下文) → simple_ai_call() 调用核心模型 → 返回 JSON → givegift = false?→ 结束 → givegift = true?→ 提取 image_prompt + message 【生图 + 入库】 → POST https://api.siliconflow.cn/v1/images/generations model: Kwai-Kolors/Kolors, 1024x1024 → 下载图片 → 保存 data/uploads/gift_{ts}.png → INSERT INTO gifts (status='pending') → WebSocket 广播 gift_pending 事件 【前端弹窗(chat.html + common.js)】 页面加载 → GET /api/gift/pending → 有未领取?弹窗 WebSocket 收到 gift_pending → 弹窗 → SVG 礼物盒弹入 → 点击打开 → 播放音效 + 礼花 → 图片展示 → 点击图片 → 显示配图文字 → 「收下礼物」 → POST /api/gift/{id}/receive → 飞走动画 【陈列馆(gift.html)】 GET /api/gift/list → 3列缩略图网格 → 点击缩略图 → 详情弹窗(大图+文字+日期) → 可删除 → DELETE /api/gift/{id}(同步清理图片文件) ### 奥罗斯财团(基金持仓监控) 326. **持仓管理** — SQLite `fund_holdings` 表存储基金持仓信息(代码、名称、份额、平均成本、总成本、跌幅/涨幅预警阈值),前端支持添加/编辑/删除,持仓列表可折叠收起 327. **数据拉取(不涉及 AI)** — 使用 akshare 拉取所有持仓基金的最新净值和涨跌幅(`fund_open_fund_daily_em` + `fund_open_fund_info_em` 兜底),新浪财经接口拉取上证指数实时涨跌。根据持仓计算当前市值、浮盈亏金额/百分比,检查是否触发预警阈值。拉取结果缓存到 `fund_cache.json`,刷新页面后仍可显示 328. **历史走势查询** — `fund_open_fund_info_em` 拉取最近 N 天净值走势,分析时注入近 30 日走势摘要(最低/最高/整体趋势) 329. **AI 分析** — 将持仓数据 + 历史走势 + 大盘背景 + 用户投资倾向拼成结构化 prompt,连同世界书人设 + 最近 20 条聊天上下文一起发送给当前聊天模型。AI 回复作为 assistant 消息插入聊天 + TTS 语音播报 + WebSocket 广播 330. **每日定时分析** — `FundScheduler` 后台线程每 30 秒检查,交易日 14:45 自动触发分析。使用 `chinese_calendar.is_workday()` 判断交易日(自动处理周末 + 中国法定节假日含调休) 331. **功能开关** — 顶部 toggle 开关,状态持久化在 `fund_config.json`,关闭后定时任务不触发、不影响其他功能 332. **投资倾向** — 可编辑的文本框,保存后在 AI 分析时注入 prompt(如"计划买入黄金,等合适坑位"),帮助 AI 给出更贴合的建议 333. **手动操作** — 「🔄 刷新数据」仅拉取数据不调 AI;「📊 立即分析」拉数据 + 调 AI + TTS,操作不阻塞页面,可自由切换到其他页面 334. **主页入口** — home.html APPS 数组增加「奥罗斯财团」(`/fund`) ### 奥罗斯财团工作流程 【手动刷新数据(不调 AI)】 点击「🔄 刷新数据」→ POST /api/fund/fetch → akshare 拉取全量基金日数据 + 新浪财经拉取上证指数 → 逐只匹配持仓基金,计算盈亏、检查预警 → 缓存到 fund_cache.json → 返回前端渲染 【手动/定时 AI 分析】 点击「📊 立即分析」→ POST /api/fund/analyze(或 14:45 定时触发) → 拉取持仓数据 + 30 日历史走势 → 生成分析 prompt(持仓明细 + 走势摘要 + 大盘 + 投资倾向) → 组装消息(世界书人设 + 20 条上下文 + prompt) → stream_ai() 流式调用当前模型 → 插入系统消息「💰 奥罗斯财团 — 基金持仓分析」+ AI 回复 → TTS 语音播报 + WebSocket 广播到聊天页 【定时任务(FundScheduler)】 后台线程每 30 秒检查 → 14:45 + 交易日 + 开关开启 → 触发 run_fund_analysis() → 等待到 14:46 避免重复触发 ### 浏览器保活 & 系统通知 105. **静音音频保活** — 页面加载后自动创建 AudioContext 播放无声音频(30秒循环),防止手机浏览器后台休眠导致 WebSocket 断连和闹铃失效 106. **Web Notification** — 闹铃触发和监控提醒时通过 `Notification API` 发送系统级推送通知,即使浏览器在后台也能看到 ### Android 原生 App(AionApp / Aion Oloth) 107. **WebView 壳应用** — Java Android 项目,WebView 加载 chat.html,支持文件上传、麦克风权限、全屏沉浸 108. **双地址启动页** — LauncherActivity 提供「家庭WiFi」和「Tailscale」两个地址入口,支持「记住选择」下次自动进入 109. **原生录音桥 AudioBridge** — 绕过 WebView 中 `getUserMedia` 需要 HTTPS 的限制,使用 Android 原生 `AudioRecord`(16kHz, VOICE_RECOGNITION)录音,通过 `@JavascriptInterface` 将 base64 PCM 数据回调到 JS。视频录制期间自动转发 PCM 帧给 VideoBridge 110. **原生视频录制桥 VideoBridge** — MediaCodec(H.264) + MediaCodec(AAC) + MediaMuxer 编码 MP4,复用 CameraBridge 的视频帧和 AudioBridge 的音频帧进行视频录制,录制期间摄像头预览不中断。JS 通过 `window.AionVideo` 接口控制录制(`startRecord`/`stopRecord`/`cancel`),`stopRecord` 返回 base64 编码的 MP4 数据 111. **手势导航适配** — 兼容 Vivo X300 Pro 等全面屏手势导航,返回键弹出对话框(切换地址 / 退出 / 取消) ### Android 前台推送服务(AionPushService) 115. **前台服务 + 独立 WebSocket** — `AionPushService` 作为 Android 前台服务运行,通过 OkHttp 维持独立于 WebView 的 WebSocket 长连接(`/ws`),不依赖页面生命周期 115b. **GPS 定位上报** — 独立定位线程每 10 分钟 GET `/api/location/config` 检查 `active` 字段,`active=true` 时获取 GPS 坐标并 POST `/api/location/heartbeat`,`active=false` 时跳过(省电) 115c. **设备活动上报** — 独立活动线程每 60 秒通过 UsageEvents API 获取前台应用包名,POST `/api/activity/report`;BroadcastReceiver 监听屏幕开关事件即时上报。需要 `PACKAGE_USAGE_STATS` 权限(AndroidManifest 声明 + 用户手动在「使用情况访问权限」中授权) 116. **三级通知渠道** — ① `aion_keepalive`(保活):常驻通知栏"Aion 在线中 ✨",低优先级不打扰;② `aion_messages`(消息):AI 回复通知,默认优先级;③ `aion_alarm`(闹铃与监控):高优先级 heads-up 弹出式通知 + 声音振动 + 锁屏可见 117. **智能通知过滤** — 只推送 3 种消息:`schedule_alarm`(闹铃⏰)、`monitor_alert`(定时监控提醒👁)、`new_message` 中 role=assistant 的 AI 回复(💬)。系统消息/cam_check/msg_created 等均不推送,避免通知轰炸 118. **前后台状态感知** — WebViewActivity 的 `onResume`/`onPause` 通过 Intent 通知 Service 当前是否在前台。app 前台时只推送闹铃和监控(高优先级),不推送 AI 消息(避免重复);app 后台/锁屏时推送所有类型 119. **WakeLock + WiFi Lock 保活** — `PARTIAL_WAKE_LOCK` 防止 CPU 深度休眠,`WIFI_MODE_FULL_LOW_LATENCY` 防止锁屏后 WiFi 休眠。这是保证锁屏后 WebSocket 不断的关键 120. **独立后台线程心跳** — 使用纯 Java `Thread` + `Thread.sleep(45s)` 做心跳循环(不用 Android 的 HandlerThread/Looper),每 45 秒发送 `{"type":"ping"}` 文本消息保持连接活性,同时检查健康状态(120 秒无消息则强制重连) 121. **Generation 计数器防重连风暴** — 每次新建 WebSocket 连接 `wsGeneration++`,旧连接的 `onClosed`/`onFailure` 回调发现 generation 不匹配则直接 return,不触发重连。关闭旧连接用 `cancel()` 而非 `close()`,`cancel()` 不触发回调 122. **NetworkCallback 网络恢复即重连** — 注册 `ConnectivityManager.NetworkCallback`,网络恢复(WiFi 重连 / Tailscale 启动)瞬间立即触发重连,不用等待心跳周期 123. **指数退避重连** — 连接失败后 3s → 6s → 12s → 24s → 30s(上限),连接成功后退避重置为 3s 124. **onTaskRemoved 自复活** — 用户划掉 app 后,通过 `AlarmManager.setExactAndAllowWhileIdle()` 3 秒后自动重启服务。但"强制停止"仍会彻底杀死服务(Android 系统限制) 125. **WebView 回前台自动刷新** — `WebViewActivity.onResume()` 中检测 WebView 内 WebSocket 状态(readyState !== 1 则重连),并延迟 1.5 秒调用 `loadMessages()` 拉取后台期间错过的消息 126. **服务端 ping/pong** — 服务端 WebSocket 端点处理客户端发来的 `{"type":"ping"}`,回复 `{"type":"pong"}`,作为应用层心跳确认 127. **服务端广播日志** — `ws.py` 的 `broadcast()` 每次广播打印 `type`、成功/失败/总客户端数,方便排查推送问题 128. **服务端连接异常兜底** — WebSocket 端点用 `except Exception` + `finally: manager.disconnect(ws)` 替代仅 `except WebSocketDisconnect`,防止 RST 等异常导致死连接留在 `active` 列表中 ### Android 推送服务工作流程 【启动】 LauncherActivity 选择地址 → startForegroundService(AionPushService, wsUrl) → Service 创建前台通知"Aion 在线中 ✨" → 获取 WakeLock + WiFi Lock → OkHttp WebSocket 连接 ws://host:port/ws → 启动心跳线程 + 注册 NetworkCallback → 启动定位线程(10 分钟间隔,独立 Java Thread) → 启动活动上报线程(60 秒间隔,独立 Java Thread)+ 注册屏幕亮灭 BroadcastReceiver 【消息接收与通知】 服务端广播 WebSocket 消息 → OkHttp onMessage 回调 → 解析 JSON type 字段: ├ schedule_alarm → 始终弹高优先级通知(⏰ 闹铃,含内容预览,锁屏可见) ├ monitor_alert → 始终弹高优先级通知(👁 监控提醒,含监控内容) ├ new_message (role=assistant) → 仅 app 后台时弹通知(💬 AI 名: 消息预览) └ 其他(msg_created/cam_check/system 等)→ 忽略,不弹通知 【心跳保活(独立 Java Thread)】 while (running): Thread.sleep(45s) if (已连接): 发送 {"type":"ping"} → 服务端回复 {"type":"pong"} → 更新 lastPongTime if (120秒无 pong): 判定连接死亡 → 关闭 + 重连 if (未连接 && 无重连计划): 立即重连 【断线重连】 onFailure/onClosed → 检查 generation 是否匹配 → 否则 return → 匹配则标记 wsConnected=false → Thread.sleep(退避时间) → connectWebSocket() → 退避时间翻倍(上限 30s)→ 成功后重置为 3s 【网络恢复】 NetworkCallback.onAvailable → 如果 WebSocket 未连接 → 立即重连 【GPS 定位上报(独立 Java Thread,10 分钟间隔)】 while (running): Thread.sleep(10min) GET /api/location/config → 读取 active 字段 ├ active = false → 跳过本轮(安静时段或功能关闭,不采集 GPS) └ active = true: LocationManager.requestSingleUpdate(GPS_PROVIDER / NETWORK_PROVIDER, 60s超时) → 获取坐标(WGS84) → POST /api/location/heartbeat {lng, lat, accuracy} → 服务端三级研判处理 【app 划掉自复活】 onTaskRemoved → AlarmManager.setExactAndAllowWhileIdle(3s后) → 重启 Service 【设备活动上报(独立 Java Thread,60 秒间隔)】 while (running): Thread.sleep(60s) UsageEvents API 查询最近 60 秒内的 MOVE_TO_FOREGROUND 事件 → 取最后一个包名 ├ 失败 → fallback UsageStatsManager queryUsageStats ├ 过滤自身包名 → 5 分钟内同一应用不重复上报 └ POST /api/activity/report {device:"phone", app:包名} BroadcastReceiver 监听 SCREEN_OFF/SCREEN_ON → 即时上报 "screen_off"/"screen_on" ### 手机端语音唤醒(远程模式) 111. **麦克风来源选择** — 语音设置面板提供「本机麦克风」和「手机端麦克风」两种模式,手机端自动选择远程模式 112. **能量 VAD** — 手机端使用能量阈值 VAD 替代 WebRTC VAD(后者依赖 PC 端 sounddevice),基于 PCM 帧 RMS 能量判断人声 113. **远程 ASR** — 手机录音通过 AudioBridge → JS 拼接 WAV → POST `/api/voice/remote-asr` → 服务端硅基流动 ASR 识别 114. **完整通话流程** — 唤醒 → 录音 → ASR → 发送消息到聊天 → AI 回复 + TTS → 暂停录音 → TTS 播完恢复录音,与 PC 端体验一致 ### 手机端语音工作流程 【AudioBridge 原生录音】 WebViewActivity 注入 window.AionAudio JS 接口 → remoteVoice.start() 调用 AionAudio.start() → AudioRecord(16kHz, VOICE_RECOGNITION) 启动录音线程 → 每 40ms 帧 base64 编码 → evaluateJavascript("remoteVoice._onNativeChunk(...)") → JS 端能量 VAD 判断人声(RMS > 阈值) → 静音超时 → 拼接 WAV → POST /api/voice/remote-asr → 服务端硅基流动 ASR → 返回识别文本 → 检查唤醒词 / 挂断词 / 发送消息 ### 性能优化 49. **数据库索引** — messages 表 (conv_id, created_at) 复合索引 + conversations 表 (updated_at DESC) 索引 50. **消息分页加载** — API 支持 `?limit=50&before=时间戳`,默认只加载最新 50 条消息 51. **前端懒加载** — 打开对话秒加载最新 50 条,滚动到顶部自动加载更早消息 52. **发送历史优化** — 发消息时用 SQL LIMIT 直接取最近 N 条,不再全量加载再截断 53. **WebSocket 事件扩展** — cam_check 和 debug 事件同时通过 SSE + WebSocket 双通道广播,语音发送的消息也能获得完整 debug 信息 ### PWA 支持(Progressive Web App) 54. **Web App Manifest** — `manifest.json` 声明 App 名称(Aion Chat)、图标(192/512)、主题色、全屏 standalone 模式 55. **Service Worker** — `sw.js` 从根路径 `/sw.js` 提供,作用域覆盖全站,让浏览器识别为可安装 PWA 56. **手机安装为独立 App** — Android Chrome 添加到主屏幕后全屏运行,无地址栏/标签栏,体验接近原生 App 57. **iOS 支持** — 通过 `apple-mobile-web-app-capable` + `apple-touch-icon` meta 标签支持 Safari 添加到主屏幕 ### 外网远程访问(Tailscale) 58. **Tailscale 虚拟组网** — 电脑和手机安装 Tailscale 并登录同一账号,通过 WireGuard 加密隧道直连,无需公网 IP 或端口转发 59. **安全性** — 8080 端口不对公网开放,仅 Tailscale 虚拟网络内设备可访问,数据端到端加密 60. **固定 IP 访问** — 通过 Tailscale 分配的 `100.x.x.x` 固定 IP 访问,手机 4G/5G 外网环境可用 61. **PWA + Tailscale 配合** — 手机 Chrome `chrome://flags` 将 Tailscale IP 标记为安全源后,可正常安装 PWA ### 记忆系统工作流程 【即时哨兵 instant_digest — 每次发消息自动触发】 用户发消息 → flash-lite 分析最近对话 → 返回 JSON: { "is_search_needed": true/false, // 是否需要搜索记忆 "keywords": ["关键词1", "关键词2"], // 搜索关键词 "require_detail": true/false, // 是否需要追溯原文 "status": "用户当前状态" // 状态变化时更新 chat_status } ↓ (如果 is_search_needed = true) recall_memories: 向量相似度×0.6 + 关键词命中率×0.3 + 重要度×0.1 → Top 5 记忆注入 prompt(自动与背景记忆去重) ↓ (如果 require_detail = true 且有召回) fetch_source_details: 在记忆 source 时间范围内按关键词筛选原始对话 → 原文细节追加注入 prompt 【记忆总结 _do_digest — 手动点击按钮 / 自动定时触发】 自动触发条件:每 30 分钟检测,用户已 30 分钟未对话 且 未总结消息 ≥ 30 条 手动触发:无最低条数限制,共用锚点不会重复总结 从锚点时间之后取消息 → 每 30 条一组(余数<10合并)→ 串行处理: 核心模型(当前聊天模型)分析,注入世界书 AI/用户人设 → 输出 JSON: {"summary": "...", "keywords": [...], "importance": 0.8, "unresolved": true/false} → embedding 向量化 → 存入 SQLite → 更新锚点 全部组处理完毕后 → 核心模型带上下文生成感慨 → 插入系统胶囊 + assistant 消息 ### 向量记忆库工作流程 【背景记忆浮现 build_surfacing_memories — 每次发消息/重新生成时】 即时哨兵返回 topic → 三层浮现策略: ① unresolved 优先:memories 表 unresolved=1 的记忆(最多 2 条) ② 话题相关:用 topic 做 embedding,余弦相似度 ≥ 0.50 的 Top 3 ③ 近期补充:最近 3 天的记忆,补满 8 条 → 去重后以 [背景记忆] 块注入 prompt → 返回 surfaced_ids 供 RAG 召回去重 【RAG 精确召回 — 仅 is_search_needed=true 时】 recall_memories: 向量相似度×0.6 + 关键词×0.3 + 重要度×0.1 → 过滤掉已在背景记忆中的 id → Top 5 注入 [相关记忆] 【写入】手动按钮 / 自动定时触发(共用 _do_digest) → 核心模型(当前聊天模型)+ 世界书人设注入,从消息提取记忆 (summary + keywords + importance + unresolved) → gemini-embedding-001 向量化(3072维) → 存入 SQLite memories 表 + WebSocket 广播 → 全部完成后生成感慨,插入聊天(系统胶囊 + assistant 消息) ## API 一览 ### 对话/消息 | 端点 | 方法 | 说明 | |------|------|------| | `/api/conversations` | GET | 对话列表 | | `/api/conversations` | POST | 创建对话 | | `/api/conversations/{conv_id}` | PUT | 更新对话(标题/模型) | | `/api/conversations/{conv_id}` | DELETE | 删除对话 | | `/api/conversations/{conv_id}/messages` | GET | 消息列表(支持 `?limit=50&before=时间戳` 分页) | | `/api/conversations/{conv_id}/send` | POST | 发送消息(SSE 流式) | | `/api/conversations/{conv_id}/regenerate` | POST | 重新生成 AI 回复(SSE 流式) | | `/api/messages/{msg_id}` | PUT | 编辑消息 | | `/api/messages/{msg_id}` | DELETE | 删除消息 | | `/api/cam-check-trigger` | POST | Core 主动查看监控触发(前端延迟 5 秒后调用) | ### 摄像头 | 端点 | 方法 | 说明 | |------|------|------| | `/api/cam/status` | GET | 摄像头和监控状态 | | `/api/cam/cameras` | GET | 可用摄像头列表 | | `/api/cam/open` | POST | 打开摄像头 | | `/api/cam/close` | POST | 关闭摄像头 | | `/api/cam/monitor/start` | POST | 开始定时监控 | | `/api/cam/monitor/stop` | POST | 停止定时监控 | | `/api/cam/config` | GET/POST | 读取/保存摄像头配置 | | `/api/cam/frame` | GET | 获取当前帧(JPEG) | | `/api/cam/screenshot` | POST | 手动截图 | | `/api/cam/logs` | GET | 日志日期列表 | | `/api/cam/logs/{date}` | GET | 指定日期的日志条目 | ### 设置/世界书/状态 | 端点 | 方法 | 说明 | |------|------|------| | `/api/settings` | GET/POST | 读取/保存设置(API Key 等) | | `/api/worldbook` | GET/POST | 读取/保存世界书 | | `/api/chat_status` | GET | 获取当前聊天状态摘要 | | `/api/models` | GET | 可用模型列表 | ### 记忆库 | 端点 | 方法 | 说明 | |------|------|------| | `/api/memories` | GET | 获取所有记忆(按时间倒序) | | `/api/memories` | POST | 手动添加记忆(自动向量化) | | `/api/memories/{id}` | PUT | 编辑记忆(重新向量化,支持 unresolved 字段) | | `/api/memories/{id}` | DELETE | 删除记忆 | | `/api/memories/{id}/unresolved` | PATCH | 切换记忆的 unresolved 状态 | ### TTS 语音合成 | 端点 | 方法 | 说明 | |------|------|------| | `/api/tts` | POST | TTS 合成代理,接收 `{text, voice}`,返回 mp3 音频流 | | `/api/tts/voices` | GET | 获取硅基流动账号下的可用音色列表 | | `/api/tts/audio/{name}` | GET/HEAD | 获取 TTS 缓存音频分片(`{msg_id}_s{seq}.mp3`),HEAD 用于前端探测分片是否存在 | ### 文件管理 | 端点 | 方法 | 说明 | |------|------|------| | `/api/files` | GET | 导出文件列表 | | `/api/files/{filename}` | DELETE | 删除导出文件 | | `/api/upload` | POST | 上传图片/视频 | ### 音乐 | 端点 | 方法 | 说明 | |------|------|------| | `/api/music/search` | GET | 搜索歌曲(`?keyword=xxx&limit=5`) | | `/api/music/detail/{song_id}` | GET | 获取歌曲详情 | | `/api/music/play` | POST | 获取播放信息(`{song_id}` → 返回歌曲信息 + audio_url + web_url) | | `/api/music/stream/{song_id}` | GET | 服务端代理推流(后端实时获取 CDN URL 并转发音频流,解决防盗链) | ### 语音唤醒/通话 | 端点 | 方法 | 说明 | |------|------|------| | `/api/voice/status` | GET | 语音状态(开关/通话中/AI说话中) | | `/api/voice/toggle` | POST | 开关语音监听(`{enabled, wake_word}`) | | `/api/voice/ai-speaking` | POST | 前端通知 TTS 播放状态(`{speaking}`) | | `/api/voice/cam-check-start` | POST | 通知语音模块 CAM_CHECK 开始,保持 AI 说话状态 | | `/api/voice/remote-asr` | POST | 手机端远程 ASR:接收 WAV 音频文件,转发硅基流动 SenseVoiceSmall 识别 | ### 活动日志 | 端点 | 方法 | 说明 | |------|------|------| | `/api/activity/report` | POST | 设备活动上报(`{device, app, title?, timestamp?}`),自动名称解析+过滤+JSONL存储+WS广播 | | `/api/activity/status` | GET | PC 采集线程状态诊断(是否运行、线程状态、上次窗口标题等) | | `/api/activity/dates` | GET | 返回所有有日志的日期列表 | | `/api/activity/logs/{date}` | GET | 返回指定日期的活动日志(自动名称解析) | | `/api/activity/recent` | GET | 返回最近 N 小时的活动日志(默认 8 小时,`?hours=N`) | | `/api/activity/clear` | POST | 清除所有活动日志文件 | ### 日程/闹铃 | 端点 | 方法 | 说明 | |------|------|------| | `/api/schedules` | GET | 日程列表(可选 `?status=active`) | | `/api/schedules` | POST | 手动添加日程(`{type, trigger_at, content}`) | | `/api/schedules/{id}` | DELETE | 删除日程 | ### 奥罗斯幽林 TRPG | 端点 | 方法 | 说明 | |------|------|------| | `/api/ghost-forest/personas` | GET | 列出所有 DM/玩家人设 | | `/api/ghost-forest/personas` | POST | 创建/更新人设 | | `/api/ghost-forest/personas/{pid}` | DELETE | 删除人设 | | `/api/ghost-forest/sessions` | GET | 列出所有游戏会话(摘要) | | `/api/ghost-forest/sessions` | POST | 创建新游戏会话 | | `/api/ghost-forest/sessions/{sid}` | GET | 获取完整会话数据 | | `/api/ghost-forest/sessions/{sid}` | PATCH | 更新会话模型 | | `/api/ghost-forest/sessions/{sid}` | DELETE | 删除会话 | | `/api/ghost-forest/sessions/{sid}/generate-outline` | POST | AI 生成剧情大纲(SSE) | | `/api/ghost-forest/sessions/{sid}/start` | POST | 提交属性分配,开始游戏 | | `/api/ghost-forest/sessions/{sid}/narrate` | POST | AI 生成当前回合叙述(SSE) | | `/api/ghost-forest/sessions/{sid}/choose` | POST | 提交选择 + 骰子结果(SSE) | | `/api/ghost-forest/sessions/{sid}/pause` | POST | 暂停游戏 | | `/api/ghost-forest/sessions/{sid}/resume` | POST | 恢复游戏 | | `/api/ghost-forest/sessions/{sid}/finale` | POST | 生成大结局(SSE) | | `/api/ghost-forest/sessions/{sid}/summary` | POST | 生成冒险总结 | ### 定位/高德地图 | 端点 | 方法 | 说明 | |------|------|------| | `/api/location/heartbeat` | POST | GPS 心跳上报(`{lng, lat, accuracy}`,可选 `force=true` 强制刷新 API) | | `/api/location/status` | GET | 当前定位状态(坐标、地址、天气、状态、距离) | | `/api/location/poi-search` | POST | 手动触发 POI 搜索(`{categories}` 逗号分隔类型) | | `/api/location/pois` | GET | 获取缓存的 POI 列表 | | `/api/location/config` | GET | 读取定位配置(含 `active` 字段供 Android 判断是否采集) | | `/api/location/config` | POST | 保存定位配置(高德Key/开关/安静时段/阈值等) | | `/api/location/set-home` | POST | 设置家位置(`{lng, lat}` GCJ-02 坐标) | ### SSE 事件类型(send / regenerate) | type | 说明 | |------|------| | `start` | 流开始,含 AI 消息 id | | `chunk` | 流式文本块 | | `cam_check` | Core 触发 [CAM_CHECK],前端播放提示音+延迟触发 | | `cam_offline` | 摄像头未开启,前端显示提示 | | `music` | 音乐卡片数据:主推荐歌曲 + 候选列表 | | `poi_search` | POI 搜索触发:含 msg_id + categories,前端显示蓝色搜索指示器 | | `toy_command` | 玩具控制指令:含 commands 数组 + msg_id | | `image_gen_start` | AI 生图开始:含 msg_id + prompt + is_selfie,前端显示橙色生图指示器 | | `debug` | Debug 数据:模型名、token 用量、召回记忆、完整 prompt | | `done` | 流结束 | ### WebSocket 事件类型 | type | 说明 | |------|------| | `conv_created/updated/deleted` | 对话变动同步 | | `msg_created/updated/deleted` | 消息变动同步 | | `monitor_log` | 新监控日志推送 | | `chat_status` | 聊天状态摘要更新 | | `memory_added` | 新记忆添加 | | `voice_state` | 语音状态广播(开关/唤醒/聊天中/AI思考/挂断等) | | `cam_check` | [CAM_CHECK] 触发通知(SSE + WS 双通道) | | `music` | 音乐卡片数据广播(SSE + WS 双通道) | | `debug` | Debug 数据广播(SSE + WS 双通道,语音发送时也能收到) | | `monitor_alert` | 定时监控触发,前端播放提示音,手机端弹高优先级通知 | | `schedule_alarm` | 闹铃到期触发,前端弹出确认弹窗,手机端弹高优先级通知 | | `schedule_changed` | 日程列表变动,前端刷新面板 | | `toy_command` | 玩具控制指令广播(含 commands 数组,SSE + WS 双通道) | | `location_update` | 定位状态更新广播(地址、天气、状态变更等) | | `poi_search` | POI 搜索触发广播(SSE + WS 双通道,前端显示搜索指示器) | | `activity_log` | 新设备活动日志推送(含 device/app/title/time,前端实时追加) | | `tts_chunk` | TTS 音频分片推送(含 msg_id/seq/url),前端收到即加入播放队列 | | `tts_done` | TTS 合成完毕通知(含 msg_id),前端标记该消息队列已结束,播完最后一片后清理 | | `tts_state` | 客户端→服务端:TTS 开关/音色同步(`{enabled, voice}`),服务端据此判断是否需要合成 | | `image_gen_start` | AI 生图开始广播(SSE + WS 双通道) | | `image_gen_done` | AI 生图完成广播(含 conv_id),前端移除指示器 | | `image_gen_failed` | AI 生图失败广播(含 conv_id),前端移除指示器 | ### 消息角色说明 | 角色 | 说明 | 是否显示在聊天 | |------|------|---------------| | `user` | 用户消息 | ✅ | | `assistant` | AI 回复(含 Core 唤醒/主动查看监控的回复) | ✅ | | `cam_user` | Sentinel 截图查询(内部) | ❌ 隐藏 | | `cam_log` | Sentinel 分析结果(内部) | ❌ 隐藏 | | `cam_trigger` | Core 唤醒时的系统提示(内部) | ❌ 隐藏 | ## Prompt 注入顺序 [系统设定 - AI人设] + assistant 确认 ← 缓存命中 [系统设定 - 用户信息] + assistant 确认 ← 缓存命中 [系统能力] 合并能力提示 + 日程列表 + assistant 确认(不含时间) ← 缓存命中 [MUSIC:歌曲名 歌手名] — 点歌(始终可用) [CAM_CHECK] — 主动查看监控(仅摄像头开启时) [POI_SEARCH:类型名] — 搜索附近 POI(仅外出状态 + 定位开启时) [ALARM:datetime|内容] — 设置闹铃(始终可用) [REMINDER:date|内容] — 设置日程提醒(始终可用) [SCHEDULE_DEL:id] — 删除日程(始终可用) [TOY:1]~[TOY:9] — 控制玩具预设档位(仅密语模式开启时) [TOY:STOP] — 停止玩具(仅密语模式开启时) [SELFIE:prompt] — AI 自拍生图(附带参考图,仅 AI 生图开关开启时) [DRAW:prompt] — AI 自由画图(仅 AI 生图开关开启时) 【当前日程列表】 — 活跃日程/闹铃一览 【位置信息】 — 当前地址 + 实时天气 + 离家距离 + 状态(仅有有效坐标时注入) 当前准确时间 ← ⚡缓存分界点 [背景记忆] unresolved📌 + 话题相关 + 近期补充(最多8条) ← 动态 [相关记忆] 向量召回的记忆(与背景记忆去重) + assistant 确认 ← 动态 聊天历史(受上下文长度滑块限制) ← 动态 ## 关键实现细节 - **模块化架构**:main.py 仅约 70 行,业务逻辑拆分到 config/database/ws/ai_providers/memory/camera + routes/ 下 5 个路由模块 - **多模态构建**:`build_multimodal_messages()`(硅基流动 base64 URL)和 `build_gemini_contents()`(Gemini inline_data) - **Token 用量捕获**:stream_ai 通过 meta dict 在流式过程中捕获 Gemini usageMetadata / 硅基流动 usage - **Gemini 轮次交替**:Gemini API 要求 user/model 严格交替,所有系统注入都以 user+assistant 对形式插入 - **[CAM_CHECK] 流程**:后端在 SSE 中发 `cam_check` 事件 + WebSocket 广播 → 前端播放音频+5秒 setTimeout → POST trigger API → 后端 asyncio.create_task 异步截图+AI分析 - **cam_check 加载指示器**:前端用 `camCheckMsgId` 全局变量跟踪,`renderMessages()` 重建 DOM 后自动恢复指示器 - **语音唤醒架构**:voice.py 运行在独立线程,通过 `asyncio.run_coroutine_threadsafe` 桥接主事件循环;WebRTC VAD (mode=2) 做帧级人声检测(30ms/帧),不需要噪底校准 - **半双工协调**:`ai_speaking` 标志由服务端 `tts_done` WebSocket 事件驱动(前端收到后调用 `notifyVoiceAiSpeaking(false)`),暂停录音期间持续 `stream.read()` 丢弃数据防止缓冲区溢出;voice.py 的 `_async_send` 在 HTTP POST body 中携带 `tts_enabled`/`tts_voice` 参数 - **消息分页**:后端 `?limit=50&before=时间戳` 参数,前端 `loadOlderMessages()` 滚动到顶部自动加载,保持滚动位置 - **SSE + WS 双通道**:cam_check 和 debug 事件同时写入 SSE 流和 WebSocket 广播,确保语音发送的消息(无 SSE 流读取端)也能被前端接收 - **文件导出**:消息变动自动同步到 `chats/{conv_id}.md`,含 YAML front matter,导出跳过 cam_* 角色 - **监控定时器**:基于时间戳比较(`_next_capture_at`),非 sleep 阻塞,间隔修改即时生效 - **摄像头 DirectShow + 验证机制**:所有 `cv2.VideoCapture` 使用 `CAP_DSHOW` 后端(Windows MSMF 后端对 USB 摄像头不稳定);`_verify_camera()` 最多等 8 秒读到非垃圾帧(`frame.mean() > 5` 排除绿屏/黑屏)才算成功;`_capture_loop` 运行时也检测绿屏帧,连续 100 帧无效触发重连;重连逐个尝试 index 0-4 并验证,失败后 30 秒重试 - **Sentinel 日志压缩**:哨兵每次分析时输出历史概况摘要(summary),避免 Core 唤醒时全量日志导致 token 过高 - **TTS 流式推送架构**:`tts.py` 的 `TTSStreamer` 在 AI 流式输出过程中实时接收文本(`feed()`),按标点(句号/问号/感叹号/换行等)切分为 100-200 字的片段(`_try_split()` + `_find_cut_position()`),每段 `asyncio.create_task` 异步调用硅基流动 CosyVoice2-0.5B 合成 mp3(`_synthesize()`),合成完成后通过 `_dispatch()` 将音频保存到 `data/tts_cache/{msg_id}_s{seq}.mp3` 并 WebSocket 广播 `tts_chunk` 事件;`flush()` 在 AI 输出结束后处理剩余文本并等待所有合成任务完成,最后广播 `tts_done` 事件 - **TTS 多端状态同步**:前端开启 TTS 后通过 WebSocket 发送 `tts_state` 消息,`ConnectionManager.tts_clients` 字典跟踪各连接的 TTS 状态;HTTP POST(send_message/regenerate)body 中的 `tts_enabled`/`tts_voice` 通过 `set_tts_fallback()` 存入 `_tts_fallback` 作为回落,确保 cam_check/闹铃/定时监控等服务端发起的消息也能正确获取 TTS 状态(`any_tts_enabled()` + `get_tts_voice()` 同时检查两处) - **PC 活动采集**:`PCActivityTracker` 守护线程通过 `win32gui.GetForegroundWindow()` + `psutil.Process.name()` 每 15 秒检测前台窗口变化,通过 `asyncio.run_coroutine_threadsafe()` 桥接主事件循环上报;`pywin32` 和 `psutil` 必须安装在项目 `.venv` 中(系统 Python 中的无效) - **App 名称解析**:服务端 `KNOWN_APPS` 字典映射 80+ 常见包名/进程名→中文名,`resolve_app_name()` 返回 `None` 表示需过滤的系统应用(桌面、SystemUI 等),读取历史日志时 `_resolve_entries()` 对旧条目重新解析确保名称一致 - **活动日志清理**:`cleanup_old_activity_logs()` 读取→过滤→重写 JSONL 文件,仅保留 `KEEP_HOURS=8` 小时内的条目,每次上报时顺带执行 - **TTS 前端播放流程**:前端 `ttsQueue`(Map,key=msg_id)维护各消息的播放队列,`playNextTTSChunk()` 按 seq 顺序取出分片 URL 播放;收到 `tts_done` WebSocket 事件后标记 `q.finished = true`,当最后一片播放完毕且队列标记结束时,调用 `finishTTSForMsg()` 清理并通知语音模块(`notifyVoiceAiSpeaking(false)`)恢复录音 - **消息编辑 attachments 修复**:后端 `update_message` 广播前 `json.loads` 解析 attachments,避免前端收到字符串导致渲染崩溃 - **PWA 架构**:`sw.js` 和 `manifest.json` 物理存放在 `static/` 目录,但通过 `main.py` 的独立路由从根路径 `/sw.js`、`/manifest.json` 提供,确保 Service Worker 作用域覆盖全站 - **外网访问**:通过 Tailscale 组建虚拟局域网,WireGuard 端到端加密,无需暴露公网端口;代码层面零改动,仅需两端安装 Tailscale 并登录同一账号 - **BLE 玩具集成**:Web Bluetooth API 连接 SOSEXY BLE 设备(服务 0xEE01,写入 0xEE03),sendData2 封包协议(前缀 00 + 18字节分包 + 随机包头 + 终止包);`whisper_mode` 参数按需注入 `[TOY:x]` 能力到 prompt,后端 `TOY_CMD_PATTERN` 正则检测+strip+广播+`_toy_sys_msg` 系统消息 - **背景记忆浮现**:`build_surfacing_memories(topic, keywords)` 三层策略构建最多 8 条背景记忆:① unresolved 优先(最多 2 条)→ ② 用即时哨兵的 topic 做 embedding 匹配(Top 3,阈值 0.50)→ ③ 最近 3 天的记忆补充。注入时 unresolved 带 📌 前缀,与后续 RAG 召回自动去重 - **记忆阈值**:cosine ≥ 0.75 才召回,top_k=3,去重阈值 0.85 - **静音保活**:`startSilentKeepAlive()` 创建 AudioContext + OscillatorNode(gain=0.001),30 秒循环,防止手机浏览器后台杀 JS 线程导致 WebSocket 断连和闹铃失效 - **Web Notification**:`sendSystemNotification()` 封装 Notification API,闹铃弹窗和监控提醒时同时发送系统推送,需用户授权 `Notification.requestPermission()` - **AudioBridge 架构**:`AudioBridge.java` 使用 `AudioRecord(VOICE_RECOGNITION, 16000, MONO, PCM_16BIT)`,录音线程每 40ms 读取 1280 字节(640 samples),base64 编码后通过 `evaluateJavascript` 注入 JS;JS 端 `remoteVoice._onNativeChunk()` 解码 → 存入环形 buffer → 能量 VAD 判断语音段 → 静音截断 → 拼接 WAV 头 → POST 到 `/api/voice/remote-asr` - **远程 ASR 端点**:`routes/voice.py` 的 `/api/voice/remote-asr` 接收 multipart WAV 文件,用 httpx 转发到硅基流动 `https://api.siliconflow.cn/v1/audio/transcriptions`(model=FunAudioLLM/SenseVoiceSmall),返回 `{text}` JSON - **手机端语音协调**:`remoteVoice` 对象维护 `aiSpeaking` 状态,通过 `notifyVoiceAiSpeaking()` 和 `notifyVoiceCamCheckStart()` 统一分发给 PC 端 `/api/voice/ai-speaking` 或手机端 `remoteVoice._onAiSpeaking()`,TTS 播放完毕(`tts_done` 事件触发)后自动恢复录音 - **音乐点歌架构**:`music.py` 封装 pyncm(`_ensure_session` 线程安全匿名登录),`routes/music.py` 提供 REST API 并导出 `MUSIC_CMD_PATTERN` 正则;`routes/chat.py` 在 send_message 和 regenerate 流结束后检测 `[MUSIC:xxx]`,搜索并通过 SSE `music` 事件 + WebSocket 广播发送卡片数据 - **能力提示合并**:[MUSIC:xxx] 和 [CAM_CHECK] 合并为单个 `[系统能力]` user+assistant 对注入,减少 token 消耗(从 4 条消息降为 2 条) - **音乐前端渲染**:`msgMusicCards` 字典按消息 ID 存储卡片数据,`renderMusicCards()` / `buildMusicCardHtml()` 生成卡片 DOM,`playMusicOnline()` 创建固定底部播放器,`closeMusicPlayer()` 停止并移除 - **日程/闹铃架构**:`schedule.py` 的 `ScheduleManager` 在独立线程运行(30 秒间隔),通过 `run_coroutine_threadsafe` 桥接主事件循环执行 DB 操作和 WebSocket 广播;`_fire_alarm` 复用 camera.py 相同的 Core 唤醒模式(世界书前缀+记忆+历史+触发提示);`_parse_dt` 支持 6 种日期时间格式,仅日期时默认 09:00 - **日程系统消息**:`_sys_msg()` 辅助函数在日程创建/删除时插入 system 角色消息到当前对话,风格与哨兵唤醒消息(📷)一致,使用 📅/🗑️ 图标前缀 - **AionPushService 架构**:前台服务使用 OkHttp 4.12.0 维持独立 WebSocket 连接,与 WebView 内的 JS WebSocket 并行但互不干扰。通知通过 `NotificationManager` 发送,渠道 ID 区分优先级。心跳线程是纯 Java `Thread`(非 HandlerThread),`Thread.sleep()` 不依赖 Android Looper 消息队列,锁屏后仍能正常唤醒 - **推送与前端 WebSocket 的关系**:Service 的 WebSocket 仅用于接收消息并弹通知,不做任何 UI 操作。WebView 内的 JS WebSocket 负责完整的 UI 交互。两条连接同时连到服务端 `/ws`,`ConnectionManager.active` 列表中会有两个客户端 - **高德定位架构**:`location.py` 独立模块,`process_heartbeat(lng, lat, accuracy, is_gcj02, skip_sentinel)` 为核心入口。`skip_sentinel` 参数用于测试脚本避免触发哨兵通知。所有高德 API 调用使用 httpx 异步请求,Key 从 `data/location_config.json` 读取 - **WGS84→GCJ-02 坐标转换**:`wgs84_to_gcj02()` 实现完整的国测局加密偏移算法(含 Krasovsky 椭球参数),中国境内坐标最大偏移约 500-700 米。Android 端不做转换,统一由服务端处理 - **三级心跳研判**:`process_heartbeat` 维护 `last_api_lng/lat` 跟踪上次 API 调用的坐标,通过 Haversine 距离判断是否显著移动(≥`movement_threshold` 500m)。轻量级处理零 API 消耗,刷新级消耗 2 次 API(逆地理+天气),完整级额外消耗 1 次 AI 调用(哨兵通知) - **状态机防误触**:家坐标为 (0,0) 或未设置时保持 `unknown` 状态不做研判;每次心跳先算距离再判状态,状态切换必须经过完整级处理 - **哨兵通知**:`_notify_sentinel()` 调用 `gemini-3.1-flash-lite-preview`,注入世界书人设 + `chat_status.json` 聊天状态 + 记忆召回 + 详细位置上下文(距离/地址/天气),生成自然语言通知消息 - **POI 按需搜索**:`perform_poi_check()` 模式同 `perform_cam_check()`:异步执行,使用最新缓存坐标重新逆地理编码 + POI 搜索 → 构建 system 消息 → 调用 Core 生成跟进回复 → 插入对话 + WebSocket 广播 - **Android 定位线程**:`AionPushService` 中 `startLocationThread()` 启动独立 Java Thread(非 HandlerThread),`Thread.sleep(10min)` 循环,每次先 GET `/api/location/config` 检查 `active` 字段,`active = enabled && !is_location_quiet_hours()`,false 时完全跳过 GPS 采集 - **定位 UI**:chat.html 设置面板中「📍 定位追踪」为可折叠区块(默认收起),监控日志弹窗底部增加「📍 缓存定位」调试行(显示坐标/状态/地址/精度/更新时间) - **POI 搜索指示器**:前端 `poiSearchMsgId` + `poiSearchCategories` 全局变量跟踪,`handlePoiSearch()` 创建蓝色弹跳动画指示器(样式同 cam-check 绿色),45 秒安全超时自动消失,新 assistant 消息到达时自动移除 - **前台服务类型扩展**:`AndroidManifest.xml` 中 `foregroundServiceType="dataSync|location"`,`startForeground()` 传入 `FOREGROUND_SERVICE_TYPE_DATA_SYNC | FOREGROUND_SERVICE_TYPE_LOCATION`,同时声明 `ACCESS_FINE_LOCATION` + `ACCESS_COARSE_LOCATION` + `ACCESS_BACKGROUND_LOCATION` 权限 - **服务端广播兼容**:`ws.py` 的 `broadcast()` 使用 `try/except` 逐连接发送,单个连接异常不影响其他连接。新增 `except Exception` 兜底确保 RST/EOF 等异常也能清理死连接 ## 踩坑记录 & 经验教训(Android 推送服务) > 以下是开发 AionPushService 过程中遇到的所有坑和最终解决方案,务必参考避免重复踩坑。 ### 坑 1:权限弹窗被 finish() 秒杀 **现象**:安装后不弹通知权限请求和电池优化引导。 **原因**:`requestPermissions()` 和电池优化 Intent 放在 `LauncherActivity` 中,但 `launchWebView()` 执行完 `startActivity(WebViewActivity)` 后紧接着 `finish()`,Activity 销毁了弹窗来不及显示。 **解决**:权限请求和电池优化引导移到 `WebViewActivity.onCreate()` 中,该 Activity 会一直存活。 **教训**:**不要在即将 finish() 的 Activity 中请求权限或弹系统对话框。** ### 坑 2:Android 14 (targetSdk 34) startForeground 崩溃 **现象**:Service 启动后立即崩溃(`MissingForegroundServiceTypeException`)。 **原因**:targetSdk 34 要求 `AndroidManifest.xml` 声明 `android:foregroundServiceType`,且 `startForeground()` 调用时必须传入 serviceType 参数。 **解决**:Manifest 中 `<service>` 标签添加 `android:foregroundServiceType="dataSync"`,`startForeground(id, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)`。 **教训**:**targetSdk ≥ 34 的前台服务,Manifest 声明和 startForeground 调用都要带 serviceType。** ### 坑 3:WebSocket close() 触发 onClosed 导致重连死循环 **现象**:App 打开后通知栏疯狂弹"连接成功",一秒一次。 **原因**:`connectWebSocket()` 中先 `oldWs.close()` 关闭旧连接 → 旧连接的 `onClosed` 回调触发 `scheduleReconnect()` → 又调 `connectWebSocket()` → 无限循环。 **解决**: ① 引入 `wsGeneration` 计数器,每次新建连接 generation++,旧连接的回调检查 generation 不匹配则直接 return ② 关闭旧连接改用 `cancel()` 而非 `close()`,`cancel()` 不会触发 `onClosed` 回调 ③ `onStartCommand` 中检查 `wsConnected` + URL 是否变化,已连接则跳过 **教训**:**OkHttp WebSocket 的 `close()` 会触发 `onClosed` 回调,如果回调里有重连逻辑则必须做状态保护。用 generation 计数器是最可靠的方式。** ### 坑 4:HandlerThread Looper 被国产 ROM 冻结 **现象**:App 在前台一切正常,但锁屏/切后台后 WebSocket 静默断开,收不到任何消息。常驻通知栏始终显示"在线",没有任何错误。 **原因**:`HandlerThread` 使用 Android 的 `Looper` 消息队列派发消息,vivo/OPPO/华为等国产 ROM 会在锁屏后冻结非活跃 App 的 Looper 消息分发(即使是前台服务的 HandlerThread 也不例外)。`handler.postDelayed()` 的心跳回调被冻结,永远不执行,WebSocket 断了但无人知晓。 **解决**:用纯 Java `Thread` + `Thread.sleep()` 替代 `HandlerThread` + `Handler.postDelayed()`。`Thread.sleep()` 是 OS 级线程休眠,不经过 Android Looper,国产 ROM 不会冻结前台服务的普通 Java Thread。 **教训**:**在国产 Android ROM 上,前台服务中需要可靠定时执行的逻辑,不要用 Handler/Looper/AlarmManager,直接用 Java Thread + Thread.sleep() 最可靠。** ### 坑 5:onFailure 阻塞 OkHttp 回调线程 **现象**:WebSocket 断线后一直不重连。 **原因**:`onFailure` 回调中直接调 `Thread.sleep()` 做退避等待,阻塞了 OkHttp 的回调线程,导致后续所有 WebSocket 事件无法分发。 **解决**:`onFailure` 中仅设置状态标志(`wsConnected = false`),不做任何阻塞操作。重连逻辑交给独立的心跳线程统一管理。 **教训**:**OkHttp WebSocket 的 onOpen/onMessage/onClosing/onClosed/onFailure 回调都在内部线程执行,绝对不要在回调里做阻塞操作(sleep/网络请求/锁等待)。** ### 坑 6:WakeLock + WiFi Lock 缺失导致锁屏断连 **现象**:锁屏几分钟后 WebSocket 断开(即使心跳线程正常运行)。 **原因**:Android 锁屏后 CPU 进入深度睡眠(Doze),WiFi 芯片也休眠。OkHttp 的网络 I/O 线程虽然在 sleep 中,但 socket 读写操作被阻塞。 **解决**:Service `onCreate()` 中获取 `PowerManager.PARTIAL_WAKE_LOCK`(保持 CPU)+ `WifiManager.WifiLock(WIFI_MODE_FULL_LOW_LATENCY)`(保持 WiFi),`onDestroy()` 中释放。 **教训**:**前台服务 + WebSocket 长连接,WakeLock 和 WiFi Lock 是必须的,缺一不可。FULL_LOW_LATENCY 比 FULL_HIGH_PERF 更省电。** ### 坑 7:覆盖安装不弹新权限请求 **现象**:更新 APK 后覆盖安装,新增的 `POST_NOTIFICATIONS` 权限不弹请求。 **原因**:Android 覆盖安装时不会重新触发 runtime 权限请求,之前没有授权的权限依旧没有。 **解决**:先卸载旧 app 再安装新 APK,或者在代码中 `onCreate()` 动态检查 `checkSelfPermission()` + `requestPermissions()`。 **教训**:**开发阶段每次改权限后,一定先卸载再装。发布后靠代码动态检查。** ### 坑 8:vivo 电池策略默认杀后台 **现象**:所有代码逻辑正确,但 vivo 手机上锁屏后仍然收不到消息。 **原因**:vivo OriginOS 默认开启"智能后台管理",会冻结或杀掉后台 app 的进程(包括前台服务)。 **解决**:手机设置 → 电池 → 后台耗电管理 → 找到 Aion Oloth → 改为"不限制后台"(关闭智能管理)。 **教训**:**国产 ROM(vivo/OPPO/小米/华为)的电池优化会无视前台服务权限直接冻结进程。必须在 app 内引导用户关闭电池优化(`REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` + 手动设置)。这是所有国产安卓推送的终极大坑。** ### 坑 9:服务端 except WebSocketDisconnect 不够 **现象**:手机 WebSocket 被杀后,服务端 `ConnectionManager.active` 列表中残留死连接,广播消息发到死连接上失败但不清理。 **原因**:网络异常断开(RST 包)抛 `RuntimeError` 或 `ConnectionResetError`,不是 `WebSocketDisconnect`。 **解决**:`except Exception` 兜底 + `finally: manager.disconnect(ws)` 确保任何原因断开都清理。 **教训**:**WebSocket 端点的异常处理不要只 catch 特定异常,用 `except Exception` + `finally` 确保连接清理。** ### 坑 10:CLI 管线图片不能用 base64 内嵌 **现象**:用户在 Aion 私聊(Gemini CLI)和 Connor 私聊/群聊(Codex CLI)发送图片时报错。Gemini CLI: `Separator is not found, and chunk exceed the limit`;Codex CLI: `Input exceeds the maximum length of 1048576 characters`。 **原因**:CLI 工具通过 stdin 管道接收 prompt,有长度限制(Codex CLI 明确 1MB 上限)。一张普通照片 base64 编码后几百 KB 到几 MB,直接内嵌必然超限。而 Gemini 原生 API 通过 HTTP POST JSON body 传输,几乎无大小限制,所以同样的 base64 方式在 API 调用中没问题。 **解决**:CLI 管线改为传本地文件绝对路径,让 CLI 自行读取文件。API 管线(硅基流动/Gemini 原生)保持 base64 不变。 **教训**:**不同 AI 调用方式对输入大小的限制不同。CLI stdin 有长度上限,不能简单复用 API 的 base64 方案。CLI 工具原生支持读取本地文件路径,应该利用这个能力。** ### 坑 11:`fetch_merged_timeline` 的 room_id 参数语义混淆 **现象**:Connor 私聊窗口合并时间线查询不到任何群聊消息,只能看到自己的私聊历史。 **原因**:`fetch_merged_timeline(who, limit, room_id=room_id)` 中的 `room_id` 参数用于指定**群聊房间**ID,但 `build_connor_1v1_prompt` 错误地传入了 1v1 私聊的 room_id。函数内部用这个 room_id 去 `chatroom_rooms` 表查 `type='group'` 的房间,自然查不到。 **解决**:Connor 1v1 调用时不传 `room_id`,让函数自动查找最新的群聊房间。 **教训**:**函数参数语义不明确时容易误传。`room_id` 这个参数名无法区分是私聊房间还是群聊房间,应该在文档/注释中明确标注参数用途。** ### 坑 12:Connor 群聊管线手动转纯文本丢失附件 **现象**:Connor 群聊回复时看不到用户发送的图片,即使 `_build_cli_prompt` 已经支持附件处理。 **原因**:`_reply_connor` 在调用 `stream_connor_cli` 之前,先把 `connor_history`(含 attachments)手动转为纯文本字符串(遍历 content 拼接),然后 `stream_connor_cli(prompt_text)` 再包装成 `[{"role": "user", "content": text}]`。附件信息在手动转文本这一步就已丢失,`_build_cli_prompt` 的附件处理逻辑永远触发不到。 **解决**:`stream_connor_cli` 新增 `messages` 参数,`_reply_connor` 直接传 `connor_history`(保留 attachments),跳过手动转文本步骤。 **教训**:**修复底层函数(`_build_cli_prompt`)时,必须检查整条调用链是否有中间层把信息提前丢弃了。"数据在到达修复点之前就已经丢失"是很隐蔽的 bug。** ### 最终技术方案总结 | 组件 | 技术选型 | 关键参数 | |------|---------|---------| | WebSocket 客户端 | OkHttp 4.12.0 | connectTimeout=15s, pingInterval=30s | | 心跳机制 | Java Thread + Thread.sleep | 间隔 45s, 健康超时 120s | | 保活 | PARTIAL_WAKE_LOCK + WIFI_MODE_FULL_LOW_LATENCY | onCreate 获取, onDestroy 释放 | | 重连策略 | 指数退避 3s→30s + NetworkCallback 即时重连 | onAvailable 触发 | | 防重连风暴 | wsGeneration 计数器 + cancel() | 旧回调自动失效 | | 自复活 | onTaskRemoved + AlarmManager.setExactAndAllowWhileIdle | 3 秒后重启 | | 前台服务类型 | dataSync | location | targetSdk 34 必须声明,location 用于 GPS 采集 | | 通知 | 3 渠道分级 | keepalive(LOW) / messages(DEFAULT) / alarm(HIGH) | ## 启动方式 ```bash # 方式一:双击启动脚本 双击 一键启动.bat # 方式二:命令行 cd aion-chat python main.py 服务监听 0.0.0.0:8080访问地址踩坑记录 & 最终方案手机端麦克风(getUserMedia vs 原生录音)远程 ASR 消息发送失败Gradle 构建Android 全面屏适配更新日志2026-05-10 — CLI 图片管线修复 + Connor 跨窗口上下文 + 多场景群聊集成2026-05-09 — 统一时间线上下文 + 统一记忆总结2026-05-08 — Gemini CLI 本地调用接入2026-04-08 — UI 多页面拆分重构2026-04-08 — 后台消息保障 + 子页面 iframe 浮层(防切页丢消息/TTS 中断)动态壁纸(独立显示器全屏壁纸 + AI 气泡)动态壁纸工作流程注意事项
Thông báo ngôn ngữ:Chưa có tài liệu tiếng Việt cho kho lưu trữ này. Đang hiển thị bản gốc tiếng Anh chính thức.
Cài đặt nhanh
pip install aionshome

Aion Chat 项目档案

项目定位

局域网 + 外网(Tailscale 组网)多端同步 AI 聊天程序 + 摄像头智能监控系统。PC/手机浏览器同时使用,支持 PWA 安装为独立 App(全屏无地址栏),数据全部存在本地电脑上。

技术栈

  • 后端:Python FastAPI + SQLite (aiosqlite) + WebSocket
  • 前端:多页面架构(原生 JS,无框架),暖光主题,手机/PC 自适应。chat.html/css/js 为主聊天页(结构/样式/逻辑分离),独立功能页通过 common.css/common.js 共享样式和工具函数
  • 摄像头:OpenCV (cv2) DirectShow 后端后台线程采集 + ESP32-CAM HTTP 远程抓帧(双摄切换 + App 桥接模式)
  • 语音:WebRTC VAD 语音检测 + 硬基流动 ASR (SenseVoiceSmall) + TTS (CosyVoice2) + 语音消息(按住录制)
  • AI 接口:硬基流动(OpenAI 兼容)、Google Gemini(REST API)、AiPro 中转站(OpenAI 兼容)、Gemini CLI(本地子进程调用,免费 OAuth 认证)、Codex CLI(本地子进程调用,Connor 专用)
  • AI 生图:Gemini gemini-3.1-flash-image-preview(REST API generateContent,responseModalities=["IMAGE"])
  • Embedding:Gemini gemini-embedding-001(3072维),余弦相似度检索
  • Android App:Java,WebView + 前台推送服务(OkHttp 4.12.0 WebSocket)+ 原生录音桥 + 原生摄像头桥 + 原生视频录制桥(MediaCodec + MediaMuxer),compileSdk 34 / minSdk 24
  • 音乐:pyncm(网易云音乐 API,搜索/歌曲详情/音频URL,支持 MUSIC_U Cookie VIP 登录 + 服务端代理推流)
  • EPUB 解析:ebooklib(EPUB 读取)+ BeautifulSoup4 / lxml(HTML 解析)
  • 基金监控:akshare(A股/基金数据拉取)+ chinese-calendar(中国节假日/交易日判断)
  • MCP 娱乐室:mcp(Python MCP SDK,支持 Streamable HTTP / stdio 传输,接入外部服务如 AI 小镇)
  • 聊天室:三人群聊(用户 + Aion + Connor-Codex),Connor 代理通过 HTTP 轮询接入 Codex CLI 服务,随机回复顺序,统一时间线上下文(私聊+群聊合并排序,场景切换标记),统一记忆总结(Aion/Connor 各自合并私聊+群聊消息总结,独立锚点,1小时无新消息自动触发),图片收发(用户发图→CLI 管线通过本地绝对路径传递、API 管线通过 base64 内嵌,Codex 回复 [[image:...]] 标记→前端渲染,图片存储于 Connor-Codex/uploads/YYYY-MM-DD/),TTS 语音合成(Aion/Connor 独立音色配置,硬基流动 CosyVoice2 服务端流式切分+并行合成,通过 SSE 推送音频分段顺序播放,配置持久化 localStorage)
  • 依赖库:fastapi, uvicorn, httpx, aiosqlite, opencv-python, Pillow, sounddevice, numpy, webrtcvad-wheels, pyncm, pywin32, psutil, ebooklib, beautifulsoup4, lxml, akshare, chinese-calendar, mcp

模块化文件结构

项目已从单文件拆分为 12 个模块化文件:

项目根目录/
├── 一键启动.bat                  # 双击启动服务(内含绝对路径,搬迁后需修改)
├── 模型预设.txt                  # 模型列表参考
├── public/                       # 公共静态资源
│   ├── BackGround.png
│   ├── icon.png                  # 原始图标(1024x941)
│   ├── icon-192.png              # PWA 图标 192x192(自动生成)
│   ├── icon-512.png              # PWA 图标 512x512(自动生成)
│   ├── AionMonitoralart.mp3      # Core 查看监控前的提示音
│   ├── AIonResponse.mp3          # 语音唤醒回复音频("诶,我在呢")
│   ├── UserIcon.png              # 用户聊天头像
│   ├── AIIcon.png                # AI 聊天头像
│   └── 生图锚点.jpg             # SELFIE 参考图(AI 人物一致性锚点)
│   └── wallpaper/                # 动态壁纸媒体文件(图片+视频)
├── AionApp/                      # Android WebView 原生壳(Java,Android Studio 项目)
│   ├── app/src/main/java/com/aion/chat/
│   │   ├── LauncherActivity.java # 启动页:双地址选择(家庭WiFi / Tailscale)+ 记住选择 + 启动推送服务
│   │   ├── WebViewActivity.java  # WebView 主页:全屏加载 chat.html,麦克风权限,前后台状态通知推送服务
│   │   ├── AudioBridge.java      # 原生录音桥:AudioRecord 16kHz → base64 → JS 回调,录制时同步转发 PCM 给 VideoBridge
│   │   ├── CameraBridge.java     # 原生摄像头桥:legacy Camera API → NV21 字节旋转 → JPEG → JS 轮询(绕过 WebView HTTPS 限制),录制时转发帧给 VideoBridge
│   │   ├── VideoBridge.java      # 原生视频录制桥:MediaCodec(H.264) + MediaCodec(AAC) + MediaMuxer → MP4,复用 CameraBridge/AudioBridge 的帧数据
│   │   ├── (AionImageSaver)      # 图片保存桥(WebViewActivity 内匿名类):JS base64 → MediaStore 写入相册
│   │   └── AionPushService.java  # 前台推送服务:独立 WebSocket 长连接 + 通知弹窗 + 断线重连 + WakeLock/WifiLock 保活 + ESP32-CAM 桥接(拉帧→上传服务器)
│   └── build.gradle              # compileSdk 34, minSdk 24, Gradle 8.5 + AGP 8.2.2, OkHttp 4.12.0
├── LittleToy/                    # BLE 玩具逆向分析 & 独立 demo
│   ├── toy_control_v4.html       # 独立 BLE 控制页面(可单独使用)
│   └── 逆向分析笔记.md           # SOSEXY 设备协议逆向笔记
├── 启动壁纸.bat                  # Chrome App 模式无边框全屏启动动态壁纸
└── aion-chat/
    ├── main.py                   # 入口:lifespan、路由注册、静态挂载、WebSocket、PWA 路由、自动记忆总结定时任务(私聊+群聊空闲检测)、Connor自动总结定时任务
    ├── config.py                 # 全局路径、常量、settings/worldbook/chat_status/cam_config 读写
    ├── database.py               # SQLite 初始化(conversations/messages/memories/schedules/theater 等表 + 性能索引)
    ├── ws.py                     # WebSocket ConnectionManager 单例,含 tts_clients 状态追踪 + _tts_fallback HTTP 回落机制 + client_id 注册/定向推送
    ├── ai_providers.py           # AI 调用:硅基流动/Gemini/AiPro中转站/GeminiCLI 流式 + 非流式 + 多模态消息构建
    ├── memory.py                 # 向量记忆:embedding、综合评分召回、手动/自动总结(合并私聊+群聊消息)、即时哨兵(RAG路由)、原文追溯
    ├── camera.py                 # 摄像头:CameraMonitor 类、Sentinel 分析(注入设备活动摘要)、Core 唤醒、[CAM_CHECK]、ESP32-CAM 双摄切换+App桥接
    ├── location.py               # 高德地图定位:GPS心跳处理、三级研判、状态机(at_home/outside)、哨兵通知、POI搜索
    ├── voice.py                  # 语音唤醒 + 半双工通话(WebRTC VAD + 硬基流动 ASR),通话中自动携带 TTS 参数
    ├── tts.py                    # 服务端流式 TTS:按句切分(100-200字)+ 异步并行合成 + WebSocket/SSE 推送音频分片
    ├── schedule.py               # 日程/闹铃/定时监控管理器:ScheduleManager、文本指令解析、闹铃触发Core唤醒、定时监控截图+Core分析(注入设备活动摘要)
    ├── ghost_forest.py            # 奥罗斯幽林 TRPG 引擎:会话管理、AI 对话历史压缩、D20 骰子判定、角色属性/道具系统
    ├── gift.py                    # 礼物系统:AI 判断送礼 + 硅基流动 Kolors 生图 + 礼物数据 CRUD
    ├── fund.py                    # 基金持仓监控:akshare数据拉取、盈亏计算、上证指数、历史走势、AI分析prompt生成、每日14:45定时任务(FundScheduler)
    ├── book.py                    # EPUB 解析模块:书籍导入、章节拆分、段落标注、图片提取
    ├── image_gen.py               # AI 生图模块:Gemini 图片生成(SELFIE/DRAW 模式)
    ├── mcp_client.py              # MCP 连接管理器:管理多个 MCP Server 连接(HTTP/stdio)、工具发现、统一 call_tool 接口、转换 OpenAI tools 格式
    ├── context_builder.py          # 统一上下文构建:fetch_merged_timeline(合并私聊+群聊消息时间线)、render_merged_timeline(场景切换标记渲染)、build_ability_block、build_memory_blocks、strip_tool_commands
    ├── chatroom.py                # 聊天室核心逻辑:Connor-Codex 代理调用(HTTP+taskId轮询+images)、统一时间线上下文构建、统一记忆总结(Connor 1v1+群聊合并,独立锚点 connor_unified)、1h无消息自动总结
    ├── routes/
    │   ├── __init__.py
    │   ├── book.py               # 阅读功能 API:书籍上传/列表/章节/进度/删除/图片/AI批注(单段+全章SSE)
    │   ├── theater.py            # 小剧场 API:独立对话CRUD、消息CRUD、角色CRUD、SSE流式回复(无记忆/系统能力注入)+ TTS
    ├── chat.py               # 对话/消息 CRUD、send_message(SSE)、regenerate、cam-check-trigger、[MUSIC:xxx]/[ALARM:...]/[REMINDER:...]/[Monitor:...]/[TOY:x]/[查看动态:n]/[视频电话] 检测
    │   ├── music.py              # 音乐搜索/详情/播放/代理推流 API(pyncm)
    │   ├── schedule.py           # 日程 CRUD API(列表/添加/删除)
    │   ├── cam.py                # 摄像头控制 + 监控日志 API + ESP32-CAM 画面源切换/桥接帧接收
    │   ├── location.py           # 定位 API:心跳上报、状态查询、POI搜索、配置管理、设置家位置
    │   ├── files.py              # 上传、聊天记录文件导出/管理
    │   ├── settings.py           # 设置、世界书、模型列表、TTS 代理、视频通话开关、AI生图开关
    │   ├── memories.py           # 记忆库 CRUD + 手动总结触发 + 原文查看 + 锚点管理 API
    │   ├── heart_whispers.py     # 心语 API(列表查询 + 删除)
    │   ├── activity.py           # 活动日志 API(上报/查询/清理/状态诊断/10分钟摘要/AI联动开关配置)
    │   ├── voice.py              # 语音唤醒/通话控制 API
    │   ├── ghost_forest.py       # 奥罗斯幽林 TRPG API(16 个端点:人设/会话/剧情生成/选择/骰子/大结局)+ SSE 流式 TTS
    │   ├── gift.py               # 礼物系统 API(pending/receive/list/delete/test)
    │   ├── fund.py               # 基金监控 API:持仓CRUD、配置开关、数据拉取、手动触发AI分析、缓存读取、历史走势
    │   ├── playground.py         # 娱乐室 API:MCP Server 连接/断开、tool calling 循环、SSE 流式行动日志、经历总结归档
    │   └── wallpaper.py          # 动态壁纸 API:文件列表/配置读写/上传/删除
    │   └── chatroom.py           # 聊天室 API:房间 CRUD、发消息(SSE)、AI 互聊(SSE)、记忆 CRUD、配置、Connor 状态、总结记忆、图片上传(/api/chatroom/upload)+ 图片路径重写 + TTS流式合成(Aion/Connor独立音色)
    ├── activity.py               # 设备活动日志:JSONL 存储、自动清理(保留最近 3 小时)、PC 前台窗口采集(win32gui+psutil)、App 包名→中文名映射、10分钟窗口摘要(时长权重+carry-forward状态追溯)、AI联动开关+Prompt摘要生成
    ├── music.py                  # pyncm 封装层(搜索/歌曲详情/音频URL/MUSIC_U Cookie 登录/匿名登录)
    ├── README.md                 # 本文件
    ├── 监控流程.md               # Sentinel/Core 架构设计文档
    ├── static/
    │   ├── home.html             # 手机风格主页 → /(应用图标网格 + Dock 栏)
    │   ├── chat.html             # 主聊天页 → /chat(含语音唤醒/TTS/BLE/音乐/系统日志/debug面板)
    │   ├── chat.css              # 主聊天页样式(从 chat.html 拆分)
    │   ├── chat.js               # 主聊天页逻辑(从 chat.html 拆分)
    │   ├── common.css            # 子页面共享样式(CSS变量/布局/组件/闹铃弹窗/toast)
    │   ├── common.js             # 子页面共享工具(api()/WS连接/闹铃弹窗/系统通知)
    │   ├── settings.html         # 设置页 → /settings(API Key 管理)
    │   ├── worldbook.html        # 世界书页 → /worldbook(AI/用户人设编辑)
    │   ├── memory.html           # 记忆库页 → /memory(CRUD/搜索/总结/锚点/原文追溯)
    │   ├── schedule.html         # 日程管理页 → /schedule(列表/添加/删除)
    │   ├── camera.html           # 摄像头页 → /camera(预览/缩放/监控开关/配置)
    │   ├── monitor-logs.html     # 监控日志页 → /monitor-logs(按日期查看/实时WS推送)
    │   ├── location.html         # 定位页 → /location(状态/POI/配置)
    │   ├── heart-whispers.html   # 心语页 → /heart-whispers(AI秘密日记查看/删除)
    │   ├── activity-logs.html    # 活动日志页 → /activity-logs(双设备活动查看/筛选/清理/10分钟摘要弹窗/AI联动开关)
    │   ├── reading.html          # 阅读页 → /reading(书架+阅读器+AI批注+选文聊天+音乐播放)
    │   ├── theater.html          # 小剧场页 → /theater(独立聊天+多角色管理+TTS,茶色暗色主题)
    │   ├── ghost-forest.html     # 奥罗斯幽林页 → /ghost-forest(TRPG 游戏:D20 骰子+角色扮演+AI DM)
    │   ├── gift.html              # 爱的印记页 → /gift(礼物陈列馆,缩略图网格+详情弹窗)
    │   ├── fund.html              # 奥罗斯财团页 → /fund(基金持仓监控、数据拉取、AI分析、持仓管理)
    │   ├── playground.html        # 娱乐室页 → /playground(MCP 服务连接 + AI 自主探索 + 行动日志 + 历史记录)
    │   ├── playground.css         # 娱乐室样式
    │   ├── playground.js          # 娱乐室前端逻辑(SSE 实时渲染 + 历史记录加载)
    │   ├── chatroom.html          # 聊天室页 → /chatroom(三人群聊 + Connor 私聊 + 房间管理 + 记忆库悬浮窗 + 图片收发 + TTS设置)
    │   ├── chatroom.css           # 聊天室样式(暖色三人气泡、头像、双换行拆分气泡、图片预览/查看器/内联图片、TTS滑块开关)
    │   ├── chatroom.js            # 聊天室前端逻辑(SSE流式、AI互聊、记忆CRUD、世界书人设继承、图片上传/粘贴/[[image:]]渲染、TTS分段队列播放+音色配置持久化)
    │   ├── wallpaper.html         # 动态壁纸页 → /wallpaper(全屏壁纸轮播+AI气泡,独立显示器使用)
    │   ├── video-call.js         # 视频通话模块:摄像头预览 + 按住录制视频 + ASR转写 + 来电/去电 UI
    │   ├── manifest.json         # PWA Web App Manifest(从 /manifest.json 提供)
    │   └── sw.js                 # PWA Service Worker(从 /sw.js 提供)
    └── data/                     # ★ 备份只需复制此文件夹
        ├── chat.db               # SQLite 数据库
        ├── settings.json         # API Key 持久化
        ├── worldbook.json        # 世界书(AI/用户人设+名称)
        ├── cam_config.json       # 摄像头监控配置(active_source/esp32_cam_url/本地摄像头/定时/静默时段)
        ├── chat_status.json      # 聊天状态摘要(供哨兵参考)
        ├── location_config.json  # 定位配置(高德Key、家坐标、开关、安静时段、阈值等)
        ├── location_status.json  # 定位状态缓存(当前坐标、状态、地址、天气、POI等)
        ├── digest_anchor.json    # 总结锚点(记录上次总结到哪条消息的时间戳)
        ├── uploads/              # 上传的图片/视频
        ├── chats/                # 导出的 .md 聊天记录 + _index.json
        ├── screenshots/          # 摄像头截图(自动清理)
        ├── monitor_logs/         # Sentinel 监控日志(JSONL,按日期,3天自动清理)
        ├── activity_logs/        # 设备活动日志(JSONL,按日期,保留最近 3 小时)
        ├── books/                # EPUB 书籍数据(解析后的章节+图片+批注数据库)
        ├── theater_personas.json # 小剧场角色预设(多套人设,JSON数组)
        ├── fund_config.json      # 基金监控配置(开关、投资倾向)
        ├── fund_cache.json       # 基金数据缓存(最近一次拉取结果)
        ├── mcp_servers.json      # MCP Server 配置(娱乐室服务地址列表)
        ├── wallpaper_config.json  # 动态壁纸配置(轮换间隔、文件启用状态、气泡锚点坐标)
        └── ghost_forest/          # 奥罗斯幽林 TRPG 数据
            ├── _personas.json     # DM/玩家人设预设
            └── {uuid}.json        # 游戏会话存档(每局一个文件)

路由

路径 说明
/ home.html 手机风格主页(应用图标启动器)
/chat chat.html 主聊天页
/settings settings.html 设置页(API Key 管理)
/worldbook worldbook.html 世界书页
/memory memory.html 记忆库页
/schedule schedule.html 日程管理页
/camera camera.html 摄像头监控页
/monitor-logs monitor-logs.html 监控日志页
/location location.html 定位地图页
/heart-whispers heart-whispers.html 心语页(AI秘密日记)
/activity-logs activity-logs.html 活动日志页(双设备活动查看)
/reading reading.html 阅读页(书架+阅读器+AI批注+选文聊天)
/theater theater.html 小剧场页(独立聊天+多角色+TTS)
/ghost-forest ghost-forest.html 奥罗斯幽林页(TRPG 冒险游戏)
/gift gift.html 爱的印记页(礼物陈列馆)
/fund fund.html 奥罗斯财团页(基金持仓监控)
/playground playground.html 娱乐室页(MCP 服务接入 + AI 自主探索)
/wallpaper wallpaper.html 动态壁纸页(全屏壁纸轮播+AI气泡)
/manifest.json PWA Web App Manifest
/sw.js PWA Service Worker(根路径提供,作用域覆盖全站)
/public/* 公共资源
/static/* 静态文件
/uploads/* data/uploads/(主聊天上传)
/cr-uploads/* Connor-Codex/uploads/(聊天室图片,按日期子目录)
/api/* 后端 API
/ws WebSocket 多端同步

支持的模型

硅基流动(api.siliconflow.cn)

  • GLM-5 → Pro/zai-org/GLM-5
  • GLM-4.7 → Pro/zai-org/GLM-4.7
  • Kimi-K2.5 → Pro/moonshotai/Kimi-K2.5

Gemini(generativelanguage.googleapis.com)

  • gemini-3.1-flash-lite → gemini-3.1-flash-lite-preview(Sentinel / poll_digest 默认模型)
  • gemini-2.5-pro → gemini-2.5-pro
  • gemini-3-flash → gemini-3-flash-preview(聊天默认模型)
  • gemini-3.1-pro → gemini-3.1-pro-preview

AiPro 中转站(vip.aipro.love)

  • claude-sonnet-4-6 → claude-sonnet-4-6
  • claude-opus-4-6 → claude-opus-4-6

哨兵/向量模型(支持独立 Gemini Free Key)

  • Sentinel 哨兵分析 → gemini-3.1-flash-lite-preview
  • 向量 Embedding → gemini-embedding-001(3072维)
  • 即时哨兵 → gemini-3.1-flash-lite-preview
  • 记忆总结(手动/自动) → 当前聊天对话的核心模型(跟随用户选择)

哨兵和向量模型支持配置独立的 Gemini Free Key,留空则自动复用主 Gemini Key。

已实现功能

聊天核心

  1. 对话管理 — 创建/删除/重命名对话,默认日期命名,侧栏显示每个对话的消息条数
  2. 消息 CRUD — 发送/编辑(气泡内 inline)/删除/复制,⋯ 点阵菜单
  3. 流式 AI 回复 — SSE 流式输出,逐字显示,等待时显示「思考中」/「正在输入」循环动画 + 弹跳小圆点
  4. 重新生成 — AI 消息可一键重新生成
  5. 多端实时同步 — WebSocket 广播,PC/手机任一端操作实时同步
  6. 上下文长度控制 — 滑块 1-100 条可调,默认 20
  7. 世界书(World Book) — AI/用户人设 + 自定义名称,注入 prompt 前缀
  8. 图片/视频上传 — 多模态支持,Gemini 用 inline_data,硅基流动用 URL
  9. 语音消息 — 微信风格按住说话录音,松手发送。浏览器使用 MediaRecorder 录制 WebM,Android 使用原生 AudioBridge 录音。录音通过硅基流动 ASR 自动转写为文字,消息以语音气泡形式展示(显示时长 + 播放按钮),转写文本同时保存供记忆/上下文使用
  10. 聊天记录文件管理 — 自动导出 .md,文件管理器弹窗查看/下载/删除
  11. API Key 管理 — 界面内设置面板,支持 Gemini + Gemini Free(哨兵+向量)+ 硅基流动 + 中转站 四组 Key
  12. 手机适配 — 侧栏抽屉式展开,聊天气泡布局,触屏友好,@media (max-width: 768px) 单独优化紧凑间距
  13. 聊天头像 — 用户/AI 消息旁显示圆形头像(public/UserIcon.png / public/AIIcon.png),用户右侧、AI 左侧
  14. 多气泡拆分 — AI 回复中 \n\n 自动拆分为多个独立消息气泡,像微信连发效果,流式输出实时拆分
  15. 时间内联显示 — 消息时间显示在用户/AI 名字旁边,不再独占一行
  16. 当前时间注入 — 每次发消息/重新生成时,将准确时间拼接到 prompt 前缀最后一条 assistant 回复中

Debug 透明度面板

  1. Token 用量追踪 — Gemini usageMetadata + 硅基流动 usage,在 SSE 流中通过 debug 事件传回前端
  2. 记忆召回可视化 — 每次发消息时召回的记忆条目,显示综合分数 + 三维分解(vec_sim / kw_score / importance)
  3. 完整 Prompt 查看 — 可展开查看发送给模型的全部 prompt 消息(截断前500字/条)
  4. Debug 条 — 每条 AI 消息下方显示:模型名、输入/输出/总 token、召回记忆数,点击展开详情

向量记忆库(RAG 重构)

  1. 记忆总结(手动 + 自动) — 手动:用户点击「总结新记忆」按钮触发(无最低条数限制)。自动:每 30 分钟检测,若用户已 30 分钟未对话且未总结消息 ≥ 30 条则自动触发。两者共用同一套逻辑和锚点,不会重复总结。从锚点之后的消息开始,每 30 条一组串行处理(余数 <10 合并到最后一组),使用当前聊天的核心模型(而非 flash-lite)提取结构化记忆(含关键词 + 重要度 0-1 + unresolved 判断),Prompt 注入世界书 AI/用户人设使记忆更具个人视角。每组成功后更新锚点。全部总结完成后,带最近 30 条聊天上下文 + 人设再调用一次核心模型,生成一句感慨/吐槽,作为 assistant 消息插入聊天(前置一条「🧠 AI整理了记忆库」系统胶囊)
  2. 即时哨兵(instant_digest) — 每次用户发消息时自动调用 flash-lite 分析最近对话,返回结构化 JSON:{is_search_needed, keywords, require_detail, status, topic},决定是否需要搜索记忆、是否需要追溯原文细节,同时提供 topic 用于背景记忆浮现
  3. 向量化存储 — 使用 Gemini gemini-embedding-001(3072维)将记忆向量化,存入 SQLite memories 表,每条记忆含 keywords(JSON 关键词数组)、importance(重要度)、source_start_ts/source_end_ts(来源时间范围)、unresolved(是否待办/未完成)
  4. 综合评分召回 — final_score = vec_sim × 0.6 + kw_score × 0.3 + importance × 0.1,threshold=0.45,Top 5。关键词匹配支持子串模糊命中
  5. 原文追溯(fetch_source_details) — 当 require_detail=true 时,在召回记忆的 source 时间范围内按关键词筛选原始对话记录,去重后按时间排序,拼接注入 prompt
  6. 总结锚点管理 — 锚点持久化在 data/digest_anchor.json,UI 显示当前锚点时间 + 日期选择器可回退
  7. 可视化管理 — 侧边栏「🧠 记忆库」按钮,支持搜索/添加/编辑/删除,编辑后自动重新向量化。每条记忆显示关键词标签 + 重要度分数,编辑时可修改关键词和重要度。有 source 时间范围的记忆可点击 📜 查看原文。📌 按钮可切换记忆的“待办/未完成”状态,unresolved 的记忆以橙色高亮显示

语音合成 (TTS) — 服务端流式推送架构

  1. 服务端流式 TTS — AI 流式输出过程中,后端 tts.py 的 TTSStreamer 实时按句切分文本(100-200 字,按句号/问号/感叹号/换行等断句),每句异步调用硅基流动 CosyVoice2-0.5B 合成 mp3,合成完成后立即通过 WebSocket 或 SSE 推送 tts_chunk 事件给前端,前端收到即可开始播放,无需等待全文生成完毕。SSE 模式通过 sse_queue 参数支持,用于小剧场和奥罗斯幽林等独立 SSE 流场景
  2. 多场景触发 — 用户发消息后的 AI 流式回复、重新生成、Core 主动发言(哨兵唤醒/闹铃/定时监控/[CAM_CHECK] 跟进)均自动创建 TTSStreamer 进行流式合成
  3. 音色选择 — 齿轮配置面板内选择硅基流动账号下的自定义音色,通过 WebSocket tts_state 消息同步到服务端
  4. 前端队列播放 — 前端维护 ttsQueue(Map 结构,key 为 msg_id),每条消息的分片按 seq 顺序播放,多条消息按到达顺序排队;播放完最后一片后服务端广播 tts_done 事件,前端清理队列并继续下一条
  5. 多端 TTS 状态同步 — 前端开启 TTS 后通过 WebSocket 发送 tts_state 消息(含 enabled/voice),服务端 ConnectionManager 在 tts_clients 字典中跟踪各客户端状态;同时 HTTP POST(send_message/regenerate)的 body 中也携带 tts_enabled/tts_voice 作为 _tts_fallback 回落,确保服务端发起的消息(cam_check/闹铃/监控)也能获取 TTS 状态 28b. TTS 音频缓存 — 合成的音频分片存储在 data/tts_cache/ 目录,文件名为 {msg_id}_s{seq}.mp3,前端可通过 /api/tts/audio/{chunk_name} 获取;点击 AI 消息的 🔊 图标可重播已缓存的 TTS 分片 28c. TTS 重播 — 点击聊天气泡下的喇叭图标,前端通过 HEAD 请求探测 {msg_id}_s0、{msg_id}_s1... 是否存在,依次播放所有分片;支持 GET 和 HEAD 两种 HTTP 方法

摄像头智能监控(Sentinel/Core 双脑架构)

  1. 摄像头集成 — OpenCV DirectShow 后端,支持多摄像头切换,绿屏检测,智能预热验证 28a. ESP32-CAM 双摄 — 支持 ESP32-CAM 作为备选摄像头源,前端 tab 切换本地/ESP32,地址支持 IP 或 mDNS 名称(如 espcam.local) 28b. 自动桥接 — 服务器直连 ESP32 失败时,自动通知 App(AionPushService)启动桥接:App 从热点局域网拉帧→上传服务器。直连恢复时自动关闭桥接 28c. 户外模式 — 手机开热点 + ESP32 连热点,App 前台服务桥接帧数据到家里服务器(~1fps,约 80KB/帧),AI 仍可触发 Sentinel/[CAM_CHECK]/定时监控 28d. 零侵入 — 所有下游(Sentinel、Core、[CAM_CHECK]、定时监控、预览)通过 get_frame_jpeg() 取帧,无需感知帧来源。active_source 默认 local,不配置 ESP32 时行为完全不变
  2. Sentinel 哨兵 — 定时截图后由轻量模型(flash-lite)分析,注入设备活动摘要(近 60 分钟 6 条)作为辅助判断依据,输出结构化 JSON(含概况摘要 summary + 唤醒原因 core_reason)
  3. Core 唤醒 — Sentinel 判断需要时唤醒 Core(当前聊天模型),Core 收到哨兵摘要+唤醒原因+最近5条日志+记忆召回,主动在对话中联系用户
  4. 监控日志系统 — 独立于聊天的 JSONL 日志,按日期存储,3 天自动清理
  5. 日志查看器 — 侧边栏「📜 监控日志」按钮,按日期浏览,显示概况摘要和唤醒原因,WebSocket 实时推送新日志
  6. 聊天状态摘要(chat_status) — 即时哨兵提取,存储在 data/chat_status.json,监控哨兵分析时自动注入

Core 主动查看监控([CAM_CHECK])

  1. [CAM_CHECK] 指令 — 摄像头开启时,prompt 中注入能力提示,Core 可在回复中输出 [CAM_CHECK] 指令主动请求查看监控画面
  2. 前端实时过滤 — 流式输出时前端实时 strip [CAM_CHECK],用户看不到原始指令
  3. 提示音 + 5秒延迟 — 检测到指令后前端播放 AionMonitoralart.mp3 提示音,等待 5 秒给用户反应时间,然后再截图
  4. 加载指示器 — 等待期间在 AI 消息下方显示「📷 {AI名} 正在查看监控 ● ● ●」弹跳动画,5秒后自动移除(renderMessages 重建后自动恢复)
  5. 后台截图+AI分析 — 5秒后前端 POST /api/cam-check-trigger,后端截图并调用 Core 模型分析画面,结果作为新 assistant 消息保存并 WebSocket 广播
  6. 摄像头离线处理 — 若摄像头未开启,后端发 cam_offline SSE 事件,前端显示「📷 摄像头未开启,Core无法查看监控信息」提示
  7. 两套系统独立 — 哨兵定时监控和 Core 主动查看是完全独立的两套系统,互不影响。关闭哨兵不影响 Core 主动查看,反之亦然

Core 主动查看设备动态([查看动态:n])

40a. [查看动态:n] 指令 — AI联动开关开启时,prompt 中注入能力提示,Core 可在回复中输出 [查看动态:n](n=112,对应 10120 分钟)主动查看用户设备使用动态 40b. n 值安全 — n 自动 clamp 到 1-12 范围,无效值默认为 6(60 分钟),避免异常请求 40c. 前端实时过滤 — 流式输出时前端实时 strip [查看动态:n],用户看不到原始指令 40d. 加载指示器 — 检测到指令后在 AI 消息下方显示「📊 {AI名} 正在查看你的动态 ● ● ●」紫色主题弹跳动画(30 秒安全超时自动移除) 40e. 后台摘要+AI分析 — 后端获取设备活动摘要,组装 Prompt(人设+上下文+摘要)调用 Core 生成关怀/评论回复,结果作为新 assistant 消息保存并 WebSocket 广播 40f. 系统消息 — 查看动态后自动插入 system 消息「{AI名}查看了{用户名}过去N分钟的动态」,纳入上下文关键词匹配 40g. 与摄像头监控独立 — 查看动态仅读取设备活动日志,不依赖摄像头,两套系统完全独立

Sentinel/Core 工作流程

【哨兵定时监控】
  定时截图 → 获取设备活动摘要(近 60 分钟 6 条)→ Sentinel(flash-lite) 分析 → 输出 JSON:
    {
      "monitoringlog": "观察描述...",
      "summary": "这段时间的概况摘要...",
      "call_core": true/false,
      "core_reason": "唤醒原因(仅call_core=true时)"
    }
    ↓ 日志写入 monitor_logs/YYYY-MM-DD.jsonl + WebSocket 广播
    ↓ (如果 call_core = true)
    组装提示词(含唤醒原因 + 概况摘要 + 最近5条日志 + 世界书 + 聊天上下文 + 记忆召回)
    → Core(当前模型) 生成回复 → 作为 assistant 消息插入对话

【Core 主动查看监控 [CAM_CHECK]】
  Core 回复包含 [CAM_CHECK] → 后端检测并发 SSE 事件 + WebSocket 广播 → 前端播放提示音
  → 等待 5 秒 → POST /api/cam-check-trigger → 后端截图
  → 带人设+上下文+图片调用 Core → 结果作为新 assistant 消息保存+广播

语音唤醒 + 半双工通话

  1. 语音唤醒 — 聊天配置面板开关,支持自定义唤醒词,后台持续监听麦克风
  2. WebRTC VAD — 使用 Google WebRTC VAD 频谱分析检测人声,不靠音量阈值,嗰杂环境(狗叫/风扇/空调)也能稳定工作
  3. 半双工通话模式 — 唤醒后进入通话:用户说话 → ASR 识别 → 发送到聊天 → AI 回复 + TTS 播放 → 轮到用户说话,循环往复
  4. 麦克风协调 — AI 说话(TTS 播放 / [CAM_CHECK] 处理)期间暂停录音,服务端 tts_done 事件触发前端 notifyVoiceAiSpeaking(false) 自动恢复录音;voice.py 发送消息时自动携带 tts_enabled/tts_voice 参数
  5. 语音挂断 — 说“再见/拜拜/挂断”自动挂断通话,继续监听唤醒词;60 秒无人说话自动挂断
  6. 唤醒回复音频 — 唤醒成功后播放 public/AIonResponse.mp3(“诶,我在呢”)
  7. 通话状态指示器 — 前端顶部实时显示:等待唤醒 / 聆听中 / AI 思考中 / 通话结束,含挂断按钮
  8. 完整功能集成 — 语音发送的消息与手动发送完全一致:有 debug 信息、记忆召回、[CAM_CHECK] 能力

语音唤醒工作流程

【监听待命】
  WebRTC VAD 持续检测 → 检测到人声 → 录音 → 静音截断 → ASR 识别
  → 文本包含唤醒词? → 是 → 进入通话模式

【通话模式(半双工)】
  播放唤醒回复音频 → 等待 AI 说完
  → 循环:
    ├ 录音(VAD 检测 + 1.5 秒静音截断)
    ├ ASR 识别
    ├ 检查挂断关键词 → 是 → 发送消息 + 挂断 → 回到监听待命
    └ 发送消息到聊天 → AI 回复 + TTS → 暂停麦克风 → TTS 播完 → 继续录音
  → 60 秒无人说话 → 自动挂断 → 回到监听待命

【状态同步】
  voice.py 通过 WebSocket 广播 voice_state 事件
  voice.py 发送消息时携带 tts_enabled/tts_voice 参数 → 服务端创建 TTSStreamer
  → TTS 分片通过 WebSocket tts_chunk 推送 → 前端按序播放
  → 全部合成完毕 → 服务端广播 tts_done → 前端 notifyVoiceAiSpeaking(false) → 恢复录音
  [CAM_CHECK] 触发时通知后端保持 AI 说话状态

视频通话([视频电话])

  1. 视频通话模式 — 摄像头预览 + 按住录制视频片段 + ASR 转写 + AI 多模态理解,实现「看到你 + 听到你 + 跟你说话」的完整通话体验。录制的视频片段直接发送给 Gemini(inline_data,最大 20MB),AI 可以看到画面和听到声音
  2. 用户主动发起 — 聊天页面 📹 按钮发起视频通话,3 秒等待期间播放铃声动画,用户可取消
  3. AI 主动发起 — AI 回复包含 [视频电话] 指令时,后端延迟 10 秒后通过 WebSocket 定向推送给发送者客户端,前端弹出来电 UI(铃声 + 接听/挂断按钮)
  4. 来电指示器 — AI 回复触发视频通话时,消息底部显示「📹 AI 正在发起视频通话...」动画指示器,10 秒后自动消失并触发来电 UI
  5. 摄像头预览 — 全屏通话界面显示用户摄像头画面(主画面)和 AI 头像(画中画),支持点击切换大小画面位置
  6. 按住录制 — 通话界面底部「🎙 按住录制」按钮,按住开始录制视频+音频,松手停止并自动发送。支持上滑取消录制,最长录制 60 秒(自动停止)
  7. 浏览器双轨录制 — PC/手机 Chrome 使用双 MediaRecorder:一轨录制视频+音频(WebM),一轨单独录制音频(用于 ASR 转写),确保视频和转写文本同时获得
  8. Android 原生视频录制 — VideoBridge.java 使用 MediaCodec(H.264) + MediaCodec(AAC) + MediaMuxer 编码 MP4,复用 CameraBridge 的 NV21 预览帧和 AudioBridge 的 PCM 音频帧,录制期间摄像头预览不中断
  9. 视频片段附件 — 录制完成后上传视频文件,ASR 转写音频,构建 {type: "video_clip", url, duration, transcript} 附件发送到聊天
  10. Gemini 视频理解 — 当前消息的视频以 inline_data 发送给 Gemini(支持 video/mp4、video/webm),AI 可以同时理解画面和音频内容;历史消息中的视频片段仅保留转写文本 [视频通话] {transcript},不重复发送视频数据
  11. 视频气泡 — 聊天记录中视频片段以渐变色气泡展示(📹 图标 + 时长),点击可全屏播放
  12. 前后摄像头切换 — 通话界面 🔄 按钮支持前后摄像头切换
  13. 不活跃自动挂断 — 120 秒内无任何录制操作自动挂断通话
  14. 开关控制 — 聊天配置面板中「视频通话」开关控制 AI 是否具有发起视频通话的能力([视频电话] 指令是否注入 prompt),用户主动发起不受开关影响
  15. 前端指令过滤 — 流式输出时前端实时 strip [视频电话],用户看不到原始指令

AI 生图([SELFIE:xxx] / [DRAW:xxx])

  1. AI 自主生图 — AI 在回复中输出 [SELFIE:prompt](自拍/角色一致性生图)或 [DRAW:prompt](自由创作生图)指令,后端自动检测并异步调用 Gemini 生图 API
  2. SELFIE 模式 — 附带 public/生图锚点.jpg 作为参考图片(base64 inlineData),Gemini 基于参考图生成角色一致的新图片,适合 AI 发自拍
  3. DRAW 模式 — 纯文本 prompt 自由生图,不附带参考图,适合 AI 画画/创作
  4. 异步非阻塞 — 生图任务通过 asyncio.create_task() 在后台执行,不阻塞聊天流和页面操作。生图期间用户可继续发消息、切换页面
  5. 加载指示器 — 检测到生图指令后在 AI 消息下方显示「🎨 {AI名} 正在发送图片 ● ● ●」橙色主题弹跳动画(renderMessages 重建后自动恢复)
  6. 图片消息 — 生图完成后,图片保存到 data/uploads/img_gen_{timestamp}.{ext},创建新 assistant 消息(附带图片附件),通过 WebSocket 广播 msg_created + image_gen_done 事件
  7. 图片查看器 — 聊天中的图片点击后弹出全屏 lightbox,支持保存图片(浏览器用 blob 下载,Android App 通过 AionImageSaver 原生桥接写入相册)
  8. 前端指令过滤 — 流式输出时前端实时 strip [SELFIE:xxx] 和 [DRAW:xxx],用户看不到原始指令
  9. TTS 过滤 — TTS 合成时自动剥除 [SELFIE:...] 和 [DRAW:...] 内容,不会被语音朗读
  10. 开关控制 — 聊天配置面板中「AI 生图」开关控制 AI 是否具有生图能力(指令是否注入 prompt),关闭后 AI 不会尝试生图
  11. Gemini API — 使用 gemini-3.1-flash-image-preview 模型,REST generateContent 端点,responseModalities: ["IMAGE", "TEXT"],120 秒超时
  12. 三处统一 — send_message、regenerate、Core/语音/定时触发三套 _bg_generate 函数均支持生图指令检测和异步生图

AI 生图工作流程

【AI 触发生图(正常聊天 / 语音 / Core 主动发言)】
  AI 回复包含 [SELFIE:穿着白裙子在花园里] 或 [DRAW:一只飞翔的龙]
  → 后端 regex 检测 → 从显示文本和数据库中 strip 掉
  → SSE 发 image_gen_start 事件 + WebSocket 广播
  → 前端显示「🎨 正在发送图片」橙色指示器
  → asyncio.create_task(_do_image_gen(...))

【异步生图(_do_image_gen)】
  ├ SELFIE 模式:读取 public/生图锚点.jpg → base64 编码 → 作为 inlineData 附加到请求
  ├ DRAW 模式:仅发送文本 prompt
  → POST https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-flash-image-preview:generateContent
    requestBody: { contents: [{parts: [...]}], generationConfig: {responseModalities: ["IMAGE", "TEXT"]} }
  → 解析 response → 提取 inlineData(base64 图片数据)
  → 保存到 data/uploads/img_gen_{timestamp}.{ext}
  → 创建 assistant 消息(附带图片附件 URL)
  → WebSocket 广播 msg_created + image_gen_done
  → 前端移除指示器 + 渲染新消息(含图片)

【生图失败】
  → WebSocket 广播 image_gen_failed
  → 前端移除指示器

【图片保存(全屏查看器)】
  点击图片 → 全屏 lightbox
  ├ 浏览器:fetch → blob → createObjectURL → <a download> 模拟点击
  └ Android App:fetch → blob → FileReader → base64 → AionImageSaver.save() → MediaStore 写入相册
  1. 消息保存 — 通话中的视频片段正常保存到聊天记录,视频文件上传到 data/uploads/,转写文本存入附件数据
  2. Chrome 兼容 — PC/手机 Chrome 浏览器使用标准 getUserMedia API 获取摄像头+麦克风,无需原生桥;Android WebView 自动 fallback 到 CameraBridge + AudioBridge + VideoBridge

视频通话工作流程

【用户主动发起】
  点击 📹 按钮 → 3 秒等待(播放铃声动画)→ 进入通话界面
  → 启动摄像头(getUserMedia 或 AionCamera 原生桥)
  → 启动音频流(仅预热,不录制)
  → 底部显示「🎙 按住录制」按钮 + 挂断按钮

【按住录制(核心交互)】
  按下录制按钮 → 开始录制:
    浏览器:双 MediaRecorder(视频+音频 WebM / 纯音频 WebM)
    Android:AionVideo.startRecord() → MediaCodec H.264 + AAC + MediaMuxer
  → 界面显示录制计时 + 红色脉冲动画
  → 上滑进入「取消区域」→ 松手取消录制
  → 正常松手 → 停止录制:
    浏览器:双 Blob → 上传视频 + ASR 转写音频
    Android:AionVideo.stopRecord() → base64 MP4 → 上传 + 收集的 PCM 帧构建 WAV → ASR
  → 构建 {type:"video_clip", url, duration, transcript}
  → POST 发送消息到聊天(视频 inline_data 发给 Gemini)
  → AI 回复 + TTS → 录制按钮暂时禁用
  → TTS 播完 → 恢复录制按钮 → 等待下次按住

【AI 主动发起】
  AI 回复包含 [视频电话] → 后端检测 → 从显示文本 strip
  → 消息底部显示「📹 正在发起视频通话...」指示器
  → 10 秒延迟 → WebSocket 定向推送 video_call_ring 给发送者
  → 前端弹出来电 UI(铃声 + 接听/挂断)
  → 用户接听 → 进入通话界面(同上)
  → 用户挂断/超时 → 取消

【历史消息处理】
  当前消息 → 视频以 inline_data 发送给 Gemini(AI 看到画面+听到音频)
  历史消息 → 仅保留转写文本「[视频通话] {transcript}」(不重复发送视频)
  记忆总结/哨兵 → 仅使用转写文本

【Android 原生视频录制桥(VideoBridge.java)】
  JS 调用 AionVideo.startRecord(width, height)
  → 创建 MediaCodec 视频编码器(H.264, NV12)+ 音频编码器(AAC, 16kHz mono)
  → MediaMuxer 准备写入临时 MP4 文件
  → CameraBridge.processFrame() 中转发 NV21 帧 → NV21→NV12 转换 → 送入视频编码器
  → AudioBridge 录音线程中转发 PCM 帧 → 送入音频编码器
  → 编码输出同步写入 MediaMuxer
  → JS 调用 AionVideo.stopRecord() → flush + stop → 读取 MP4 文件 → base64 返回

【Android 原生摄像头桥(CameraBridge.java)】
  JS 调用 AionCamera.start("user"|"environment")
  → Camera.open() → 设置 640×480 NV21 预览
  → setPreviewCallbackWithBuffer(3 个预分配 buffer,零 GC)
  → 摄像头回调 → 复制数据到 inputBuf → 归还 camera buffer
  → 后台 ExecutorService 线程:
    ├ NV21 纯字节数组旋转(rotateNV21_CW90/270/180,~1ms)
    ├ YuvImage.compressToJpeg(已旋转的竖屏 JPEG)
    ├ Base64 编码 → 更新 lastFrameB64
    └ 录制中?→ 转发旋转后的 NV21 帧给 VideoBridge.onVideoFrame()
  → JS requestAnimationFrame 轮询 getFrame() → 更新 <img>.src

音乐点歌([MUSIC:xxx])

  1. AI 自主点歌 — AI 在回复中输出 [MUSIC:歌曲名 歌手名] 指令,后端自动检测并通过 pyncm 搜索网易云音乐
  2. 音乐卡片 — 搜索结果以卡片形式展示:封面、歌名、歌手、专辑 + 最多 3 首候选歌曲
  3. 双播放模式 — 每首歌提供「网易云播放」(跳转 music.163.com 网页)和「在线播放」(页内 <audio> 播放器,走服务端代理推流 /api/music/stream/{id})
  4. 在线播放器 — 固定顶部的音频播放条,支持播放/暂停、进度拖拽和关闭按钮
  5. 前端实时过滤 — 流式输出时前端实时 strip [MUSIC:xxx],用户看不到原始指令
  6. 多端同步 — 音乐卡片通过 SSE + WebSocket 双通道广播,语音通话触发的点歌也能在所有端展示
  7. VIP 登录 — 支持在设置面板配置网易云 MUSIC_U Cookie,以 VIP 身份登录 pyncm,可播放付费/VIP 歌曲;未配置时退回匿名登录 71b. 服务端代理推流 — 新增 /api/music/stream/{song_id} 路由,后端实时获取网易云 CDN URL 并通过 httpx 流式转发音频给前端,解决防盗链和 CDN 链接过期问题,手机端也能稳定播放 71c. 自动播放 — AI 点歌后浏览器自动开始播放第一首歌曲,无需用户手动点击;闹铃/定时监控触发时 AI 也可点歌并自动播放,实现音乐闹钟效果 71d. 闹铃/监控点歌能力 — 闹铃触发和定时监控触发时的 Prompt 中注入 [MUSIC:xxx] 等系统能力指令,AI 可在提醒回复中主动点歌

音乐点歌工作流程

【AI 触发点歌(正常聊天 / 闹铃 / 定时监控)】
  用户聊天提到想听歌 / 闹铃触发 AI 决定点歌
  → AI 回复包含 [MUSIC:歌曲名 歌手名]
  → 后端 regex 检测 [MUSIC:xxx] → pyncm 登录(MUSIC_U VIP 或匿名)+ 搜索
  → 取第一首为主推荐 + 获取音频 URL → 剩余作为候选
  → SSE/WebSocket 发送 music 事件(含卡片数据)
  → 前端渲染音乐卡片 + 自动播放第一首
  → [MUSIC:xxx] 从显示文本和数据库中 strip 掉

【浏览器播放(服务端代理推流)】
  前端 <audio>.src = /api/music/stream/{song_id}
  → 后端实时调用 pyncm 获取 CDN URL
  → httpx 流式代理转发(带 Referer 头绕过防盗链)
  → 前端播放,手机/PC 均走此路径

【用户手动选择】
  ├ 「网易云播放」→ window.open("https://music.163.com/song?id={id}")
  └ 「在线播放」→ /api/music/stream/{id} 代理推流播放

日程 / 闹铃 / 定时监控系统

  1. AI 设置日程 — AI 在回复中使用文本指令创建日程/闹铃,格式:[ALARM:2026-03-25T10:00|叫用户参加聚会]、[REMINDER:2026-04-09|还信用卡]
  2. AI 删除日程 — AI 输出 [SCHEDULE_DEL:日程id] 删除指定日程/闹铃/定时监控
  3. 日程列表注入 — 每次对话时将当前活跃日程列表注入到 Prompt 的 [系统能力] 区块,AI 可自然提起日程提醒
  4. 闹铃自动触发 — 后台 ScheduleManager 每 30 秒扫描到期闹铃,组装完整 Prompt(世界书+记忆+上下文+当前时间+活跃日程列表)调用 Core 生成提醒回复
  5. 前端弹窗 — 闹铃触发时所有连接的前端弹出全屏遮罩弹窗(脉冲动画),必须用户点击「确认」才关闭,支持多条闹铃排队
  6. 多端同步 — 闹铃弹窗通过 WebSocket 广播到所有连接的客户端
  7. 持久化 — 所有日程存储在 SQLite schedules 表,服务器重启后自动恢复,遗漏的闹铃立即补触发
  8. 用户手动管理 — 侧边栏「📅 日程管理」面板支持手动添加/删除日程
  9. 前端指令过滤 — 流式输出时实时 strip [ALARM:...]、[REMINDER:...]、[Monitor:...]、[SCHEDULE_DEL:...]、[SCHEDULE_LIST],用户不可见
  10. 容错设计 — AI 输出格式错误时静默跳过,不影响正常聊天
  11. 系统消息 — 日程创建/删除操作后自动插入 system 消息到聊天(如「📅 已创建闹铃:2026-03-25 10:00 — 叫用户参加聚会」),风格与哨兵唤醒消息一致
  12. 日期格式容错 — 支持 2026-03-25T10:00、2026-03-25 10:00、2026-03-25(仅日期默认 09:00)、2026/3/25 等多种格式
  13. 时间显示优化 — 所有日程时间展示均使用空格分隔(2026-03-25 10:00),不显示 ISO 格式的 T
  14. 时间格式归一化 — 前端手动创建和 AI 指令创建的日程均统一将 T 分隔符转为空格后存储,确保时间比较一致

定时监控([Monitor:...])

  1. [Monitor:...] 指令 — AI 可在回复中输出 [Monitor:YYYY-MM-DDTHH:MM|内容],设定定时截图监控任务,例如检查用户是否去睡觉、是否在运动等
  2. 日程类型 monitor — 存储在 schedules 表,类型为 monitor,在日程管理面板显示为「👁 监督」(紫色标签),用户可手动添加/删除
  3. 提示音 + 5秒延迟 — 触发时先通过 WebSocket 广播 monitor_alert,前端播放 AionMonitoralart.mp3,等待 5 秒给用户反应时间,然后再截图
  4. 截图 + Core 分析 — 到时间后自动截取摄像头画面,组装 Prompt(人设+上下文+日程列表+截图+监控目的+设备活动摘要(近 120 分钟 12 条))调用 Core 生成回复,不经过哨兵模型,不召回记忆库
  5. 截图双存 — 截图同时保存到 data/uploads/ 和 data/screenshots/
  6. 摄像头离线处理 — 若触发时摄像头未开启,插入系统消息「定时监控触发失败:摄像头未开启」,不发送给 Core
  7. AI 可取消 — 复用 [SCHEDULE_DEL:id] 指令取消未触发的定时监控,例如用户提前完成了任务时 AI 自主取消
  8. 与哨兵独立 — 定时监控与 Sentinel 哨兵完全独立,关闭哨兵不影响定时监控,反之亦然

密语时刻(BLE 玩具控制)

  1. BLE 玩具集成 — 通过 Web Bluetooth API 在手机 Chrome 上连接 SOSEXY BLE 情趣玩具,完整实现 sendData2 封包协议(前缀 00 + 分包 + 包头)
  2. 密语模式开关 — 侧边栏「💗 密语时刻」按钮打开浮层面板,包含 BLE 连接/断开、密语模式开关、手动预设网格、停止按钮、BLE 日志
  3. AI 控制玩具 — 开启密语模式后,AI 的 prompt 注入 [TOY:1]~[TOY:9] 和 [TOY:STOP] 能力,AI 可根据对话氛围自主控制玩具档位
  4. 9 级预设模式 — 微风轻拂、春水初生、暗流涌动、如梦似幻、情潮渐涨、烈焰焚身、极乐之巅、魂飞魄散、失控,每个预设控制 3 个马达(震动/电流/吮吸)的模式+速度
  5. 前端指令过滤 — 流式输出时实时 strip [TOY:x],用户看不到原始指令
  6. 系统消息胶囊 — AI 触发玩具指令后自动插入居中系统消息:「❤️ {AI名} · 心动3 · 暗流涌动」或「❤️ {AI名} 停止了玩具」
  7. BLE 连接保持 — 控制面板嵌入 chat.html 内部浮层,关闭面板后 BLE 连接不断,正常聊天时 AI 指令仍可直接控制玩具
  8. 设备过滤 — BLE 扫描仅显示名称以 SOSEXY 开头的设备
  9. 多端同步 — 玩具指令通过 SSE + WebSocket 双通道广播至所有连接的客户端
  10. 主页快捷入口 — home.html 「密语时刻」图标点击跳转 /chat?whisper=1,自动弹出密语面板

背景记忆浮现(替代旧版“近期记忆注入”)

  1. 智能背景记忆浮现 — 每次发消息/重新生成时,通过三层策略构建背景记忆(最多 8 条):① unresolved 记忆优先(最多 2 条,待办/未完成的事项)→ ② 话题相关浮现(用即时哨兵的 topic 做 embedding 匹配,Top 3)→ ③ 近期补充(最近 3 天,补满 8 条)。与 RAG 精确召回自动去重
  2. Unresolved 标记 — 记忆表新增 unresolved 字段,标记悬而未决的计划/约定/承诺。总结时核心模型自动判断,UI 中可通过 📌 按钮手动切换。unresolved 记忆在背景记忆中以 📌 前缀注入,确保 AI 记得追问

高德地图定位系统

核心模块

  1. GPS 心跳上报 — Android APK 每 10 分钟通过 LocationManager 获取 GPS 坐标(WGS84),POST 到 /api/location/heartbeat,服务端自动转换为 GCJ-02(高德坐标系)
  2. 三级心跳研判 — 服务端对每次心跳执行三级处理,逐级递进,最大程度节省 API 调用:
  • 轻量级(lightweight):仅保存坐标,不调 API。条件:移动距离 < movement_threshold(默认 500m)且已有缓存地址
  • 刷新级(refresh):调用高德逆地理编码 + 天气 API 更新地址/天气。条件:移动超过阈值 或 无缓存地址
  • 完整级(full):刷新 + 状态变更 + 哨兵通知。条件:home/outside 状态发生切换
  1. 状态机 — 三状态:unknown → at_home ↔ outside,基于与家坐标的 Haversine 距离判断(≤1000m 为 at_home),状态切换触发完整级处理
  2. 哨兵通知 — 状态变更时调用 gemini-3.1-flash-lite-preview 生成通知语,注入世界书人设 + 聊天状态 + 记忆召回 + 位置信息,作为 assistant 消息插入对话 + WebSocket 广播 + TTS
  3. 逆地理编码 — 高德 Web 服务 API /v3/geocode/regeo,将 GCJ-02 坐标转换为结构化地址(省/市/区/街道/门牌号)
  4. 实时天气 — 高德天气 API /v3/weather/weatherInfo,根据逆地理编码返回的 adcode 查询实时天气(天气现象 + 温度 + 风力)
  5. POI 周边搜索 — 高德 POI API /v3/place/around,以当前坐标为圆心 1000m 半径搜索指定类型 POI(如餐饮、超市)

手机上报流程(Android 端)

  1. 统一 10 分钟间隔 — AionPushService 中 LOCATION_INTERVAL = 10 * 60_000(10分钟),不区分在家/外出,服务端智能过滤
  2. active 标志控制 — 每次上报前先 GET /api/location/config 读取 active 字段(= enabled && 非安静时段),active=false 时完全停止 GPS 采集和上报,省电
  3. GPS 获取 — 使用 LocationManager.requestSingleUpdate(),优先 GPS_PROVIDER,fallback NETWORK_PROVIDER,60 秒超时
  4. 上报数据 — POST /api/location/heartbeat 发送 {lng, lat, accuracy},坐标为 WGS84 原始值,服务端负责坐标转换

服务端研判逻辑(详细)

  1. 坐标转换 — wgs84_to_gcj02() 实现国测局加密偏移算法,将 GPS 原始坐标转为高德坐标系(GCJ-02),最大偏移约 500-700 米
  2. 距离计算 — haversine() 球面余弦公式计算两点距离(公里),用于判断是否到家(≤1km)和是否显著移动(≥500m)
  3. 显著移动检测 — 维护 last_api_lng/lat(上次调 API 时的坐标),与当前坐标的 Haversine 距离 ≥ movement_threshold(500m)才视为显著移动,触发 API 刷新
  4. 安静时段 — 配置 quiet_hours_start/end(如 00:00-10:00),安静时段内仍接收心跳、保存坐标,但跳过 API 调用和哨兵通知
  5. Prompt 注入 — format_location_for_prompt() 将位置/天气/状态格式化为 【位置信息】 块注入 AI prompt,仅在有有效坐标时注入
  6. POI 搜索能力 — AI prompt 中注入 [POI_SEARCH:类型名] 能力描述(仅在外出状态下可用),Core 可在回复中输出该指令触发按需搜索

Core 按需 POI 搜索([POI_SEARCH:xxx])

  1. 触发方式 — Core 在回复中输出 [POI_SEARCH:餐饮服务]、[POI_SEARCH:超市] 等,后端 regex 检测
  2. 新鲜坐标+搜索 — 检测到指令后,使用最新 GPS 缓存坐标重新逆地理编码 + 搜索指定 POI 类型,确保数据实时
  3. 自动跟进回复 — 搜索结果注入 system 消息,然后调用 Core 生成跟进回复(模式同 [CAM_CHECK] 的 perform_cam_check)
  4. UI 指示器 — 前端显示蓝色 📍 正在搜索附近的 xxx 弹跳动画,45 秒超时自动消失
  5. 前端过滤 — 流式输出时实时 strip [POI_SEARCH:xxx],用户看不到原始指令
  6. 多端同步 — POI 搜索事件通过 SSE + WebSocket 双通道广播

设置与配置

  1. 设置面板 — chat.html 设置弹窗中可折叠的「📍 定位追踪」区块,配置高德 Key、开关、安静时段、设置家位置
  2. 设置家位置 — 优先使用最近一次 GPS 心跳坐标,无心跳时 fallback 浏览器 navigator.geolocation + 强制触发一次心跳上报
  3. 安静时段 — 开关 + 开始/结束时间选择,安静时段内 Android 端完全停止 GPS(通过 active 标志)

高德地图定位工作流程

【手机 GPS 上报(Android AionPushService 定位线程)】
  每 10 分钟唤醒 → GET /api/location/config 检查 active 字段
  ├ active = false → 跳过本轮(安静时段或功能关闭)
  └ active = true → LocationManager.requestSingleUpdate()
    → GPS_PROVIDER / NETWORK_PROVIDER 获取坐标(60s超时)
    → POST /api/location/heartbeat {lng, lat, accuracy}(WGS84 原始坐标)

【服务端心跳处理(location.py process_heartbeat)】
  收到心跳 → wgs84_to_gcj02() 坐标转换
  → 保存坐标到 location_status.json
  → 检查安静时段 → 安静中?跳过后续(仅保存坐标)
  → 检查家坐标 → 未设置?跳过研判
  → haversine() 计算与家的距离 → 判定状态(at_home ≤1km / outside >1km)

  【三级研判】
  ① 轻量级(默认):
     与上次 API 坐标距离 < 500m 且已有缓存地址 → 仅更新距离,不调 API → 结束

  ② 刷新级(显著移动):
     与上次 API 坐标距离 ≥ 500m 或 无缓存地址
     → 调用高德逆地理编码 API → 更新地址
     → 调用高德天气 API → 更新天气
     → 更新 last_api_lng/lat → 结束

  ③ 完整级(状态变更):
     home/outside 状态发生切换(如 at_home → outside 或 outside → at_home)
     → 执行刷新级全部操作
     → 调用 _on_state_change() → _notify_sentinel()
     → 哨兵模型(flash-lite)生成通知语(含世界书+聊天状态+记忆召回+位置详情)
     → 插入 system 消息 + assistant 回复 → WebSocket 广播(含 TTS)

【Core 按需 POI 搜索】
  Core 回复包含 [POI_SEARCH:餐饮服务]
  → 后端 regex 检测 → 从显示文本 strip 掉
  → SSE/WebSocket 发 poi_search 事件 → 前端显示蓝色搜索指示器
  → asyncio.create_task(perform_poi_check):
    ├ 取最新 GPS 缓存坐标
    ├ 重新逆地理编码(确保地址与坐标对应)
    ├ 高德 POI 搜索(1000m半径)
    ├ 更新 location_status 缓存
    ├ 构建上下文(对话历史 + POI 结果)
    ├ 调用 Core 生成跟进回复
    └ 插入 system 消息 + assistant 回复 → WebSocket 广播(含 TTS)

【Prompt 注入(每次发消息/重新生成时)】
  format_location_for_prompt() 检查 location_status 缓存
  → 有有效坐标?注入 【位置信息】 块:
    - 当前地址(省/市/区/街道)
    - 实时天气(天气 + 温度 + 风力)
    - 与家的距离 + 状态(在家/外出)
  → 外出状态时额外注入 [POI_SEARCH:类型名] 能力描述

日程/闹铃工作流程

【AI 设置日程(聊天过程中)】
  用户说"25号上午十点叫我参加聚会" → AI 回复包含 [ALARM:2026-03-25T10:00|叫用户参加聚会]
  → 后端 regex 检测 → _parse_dt 解析日期时间(支持多种格式,仅日期默认 09:00)
  → 写入 SQLite schedules 表 → 插入 system 消息「📅 已创建闹铃:...」
  → WebSocket 广播 schedule_changed → 前端日程面板自动刷新
  → 指令从显示文本中 strip 掉

【AI 删除日程】
  AI 回复包含 [SCHEDULE_DEL:日程id]
  → 后端查询日程详情 → 删除记录 → 插入 system 消息「🗑️ 已删除日程:...」
  → WebSocket 广播 schedule_changed

【闹铃触发】
  ScheduleManager 每 30 秒检查 → 发现 trigger_at <= 当前时间 的 active alarm
  → 标记 status='triggered'
  → WebSocket 广播 schedule_alarm → 所有前端弹出确认弹窗
  → 组装 Prompt(世界书 + 记忆召回 + 聊天上下文 + 当前时间 + 活跃日程列表 + 触发提示)
  → Core 生成提醒回复 → 保存为 assistant 消息 + WebSocket 广播(含 TTS)

【日程提醒(非闹铃)】
  每次发消息/重新生成时,日程列表注入 Prompt
  → AI 看到日程列表 → 在合适时机自然提起(如"对了,你明天要还信用卡哦")
【定时监控触发】
  ScheduleManager 每 30 秒检查 → 发现 trigger_at <= 当前时间 的 active monitor
  → 标记 status='triggered'
  → WebSocket 广播 monitor_alert → 前端播放提示音
  → 等待 5 秒
  → 摄像头截图(保存到 uploads + screenshots)
  → 组装 Prompt(人设 + 上下文 + 日程列表 + 截图 + 监控目的)
  → Core 生成回复 → 插入 system 消息「{AI名}查看了监控」 + assistant 回复 + TTS 广播```

### 密语时刻工作流程

【连接玩具】 手机 Chrome 打开 /chat → 侧栏「💗 密语时刻」→ 弹出控制面板 → 点击「连接」→ Web Bluetooth requestDevice(过滤 SOSEXY 设备) → GATT 连接 → 获取 EE01 服务 + EE03 写入特征 → 连接成功

【开启密语模式】 打开「🔮 密语模式」开关 → whisperMode = true → 之后发消息/重新生成时 body 携带 whisper_mode: true → 后端注入 [TOY:1]~[TOY:9] / [TOY:STOP] 能力到 AI prompt

【AI 控制玩具】 AI 回复包含 [TOY:3] → 后端 regex 检测 → 从存储文本中 strip → SSE 发 toy_command 事件 + WebSocket 广播 → 前端收到事件 → toyExecCmd('3') → BLE 发送预设3的马达指令 → 后端插入 system 消息「❤️ {AI名} · 心动3 · 暗流涌动」

【手动控制】 面板内 9 个预设按钮 → 点击直接 BLE 发送指令(不经过 AI) 「⏹ 停止」→ 三马达速度归零

【注意事项】

  • Web Bluetooth 需要安全上下文:手机 Chrome chrome://flags 将服务器 HTTP 地址标记为安全源
  • BLE 连接绑定在 chat.html 页面 JS 上下文,不跳页就不会断
  • 关闭密语面板后 BLE 连接保持,AI 指令仍可直接控制玩具

### 上下文系统消息注入
155. **选择性注入** — 发送给模型的上下文中,system 消息不再全部过滤,而是选择性保留「点歌」和「查看监控」相关的系统事件(如"Aion查看了监控画面"、"Aion搜索了周边的餐饮美食信息"),以 `[系统事件]` 前缀包装为 user 角色注入,让 AI 知道之前发生过什么
156. **范围限制** — 系统消息与普通消息共享同一个 `LIMIT` 查询,上下文默认 30 条窗口内的系统事件才会被带入,不会加载全部历史
157. **关键词过滤** — 只有包含"查看了监控"、"搜索了"等关键词的 system 消息才会保留,闹铃/日程/玩具等系统消息仍然不进入上下文

### 语音唤醒快速聊天模式
158. **fast_mode 参数** — `MsgCreate` 和 `regenerate_message` 新增 `fast_mode` 参数,语音唤醒发送消息时自动启用
159. **跳过记忆检索** — `fast_mode=True` 时跳过即时哨兵(`instant_digest`)、背景记忆浮现(`build_surfacing_memories`)、向量记忆召回(`recall_memories`),仅注入当前时间,大幅降低语音聊天延迟
160. **保留核心能力** — 快速模式下仍保留世界书人设、系统能力指令、日程列表、位置信息等静态注入,AI 回复质量不受太大影响
161. **消息正常保存** — 语音通话的消息仍正常存入数据库和导出聊天记录,仅跳过记忆检索环节

### 心语功能(AI 秘密日记)
162. **[HEART:xxx] 指令** — AI 在回复中可使用 `[HEART:内心想法]` 指令悄悄记录内心感受,如口是心非、觉得用户可爱、想偷偷记住的小秘密等
163. **前端实时过滤** — 流式输出时实时 strip `[HEART:xxx]`,用户在聊天界面看不到原始指令
164. **💭 头像气泡** — AI 记录心语时,对应消息的 AI 头像右上角浮现 💭 图标(带弹出动画),提示本条消息有心语。刷新后消失,不持久化
165. **数据库存储** — 心语存入 SQLite `heart_whispers` 表(id, conv_id, msg_id, content, created_at),支持按时间检索
166. **心语查看页** — 独立页面 `/heart-whispers`(`heart-whispers.html`),分页显示所有心语记录,支持删除,WebSocket 实时推送新心语
167. **API 接口** — `GET /api/heart-whispers`(分页列表)、`DELETE /api/heart-whispers/{id}`(删除单条)
168. **主页入口** — home.html 「心语」图标链接到 `/heart-whispers`,位于 Dock 栏
169. **多端同步** — 心语事件通过 SSE + WebSocket 双通道广播,💭 气泡在 `renderMessages()` 重建 DOM 后自动恢复

### 心语工作流程

【AI 记录心语(聊天过程中)】 AI 回复包含 [HEART:觉得她今天特别可爱] → 后端 regex 检测 → 从显示文本和数据库中 strip 掉 → 存入 heart_whispers 表(关联 conv_id + msg_id) → SSE 发 heart_whisper 事件 + WebSocket 广播 → 前端在 AI 头像右上角显示 💭 图标

【查看心语】 home.html 「心语」→ /heart-whispers → 分页加载心语列表(按时间倒序) → 每条显示时间 + 内容,支持删除 → WebSocket 实时推送新心语到列表顶部


### AI 陪伴阅读(EPUB 书架 + AI 批注 + 选文聊天)
185. **EPUB 导入** — 支持上传 `.epub` 格式电子书,后端使用 `ebooklib` 解析,自动提取目录、章节内容、封面与内嵌图片。书籍数据存储在 `data/books/{book_id}/` 目录下,每本书有独立的 SQLite 数据库(`book.db`)存储章节和批注
186. **书架界面** — `/reading` 页面上半部分为书架,网格展示已导入书籍(封面+标题+作者+章节数),支持上传新书和删除
187. **阅读器** — 点击书籍进入阅读器,显示章节标题、正文内容(含字数统计)、上/下章导航,阅读进度自动保存并恢复
188. **AI 批注系统** — 每章支持 AI 逐段批注,批注由 AI 模型(默认 `gemini-3-flash`)根据世界书人设、聊天上下文、前章摘要生成,包含批注类型(情感共振/知识延伸/个人联想等)和内容
189. **批注气泡** — 有批注的段落右侧显示 AI 头像+💭气泡图标,点击弹出批注弹窗,显示批注类型和内容
190. **[MUSIC:xxx] 批注点歌** — AI 批注中可使用 `[MUSIC:歌曲名 歌手名]` 指令点歌,批注弹窗中以音乐卡片形式展示,点击通过网易云 API 搜索并在页内在线播放
191. **批注 SSE 流式生成** — 单段批注(`/api/books/{id}/chapters/{ch}/annotate`)和全章批注(`/annotate-all`)均使用 SSE 流式输出,前端逐条实时渲染批注气泡
192. **批注提示词** — 批注 Prompt 注入书名、世界书人设(AI+用户)、当前时间、前章摘要(最近 3 章、每章≤200字)、最近 15 条聊天上下文,让 AI 批注贴合人设和聊天氛围
193. **选文快捊提问** — 阅读器内选中文字后弹出「💬 问问{AI名}」工具栏,点击后打开提问弹窗(可附加问题或直接发送),消息发送到主聊天对话(调用 `/api/conversations/{conv_id}/send`),回复保存在主聊天历史中
194. **嵌入式聊天面板** — 提问后在阅读页底部弹出聊天面板,显示最近 6 条主聊天消息作为上下文 + 分割线 + 当前对话,AI 回复以流式气泡展示,支持继续追问。气泡样式与 `/chat` 页面一致(多段落自动拆分为多气泡)
195. **聊天面板音乐播放** — AI 在聊天面板中点歌时(`[MUSIC:xxx]` 指令),自动触发页内在线播放并显示音乐卡片
196. **章节目录** — 左上角「☰ 目录」按钮展开章节列表弹窗,可快速跳转到任意章节
197. **进度记忆** — 每次切换章节或关闭页面自动保存阅读进度(当前章节索引),重新打开自动恢复到上次阅读位置
198. **依赖库** — `ebooklib`(EPUB 解析)、`beautifulsoup4` + `lxml`(HTML 内容提取),已包含在 `requirements.txt` 中

### AI 陪伴阅读工作流程

【导入书籍】 上传 EPUB 文件 → POST /api/books/upload → ebooklib 解析 EPUB → 提取 spine 项(HTML 章节)→ BeautifulSoup 解析 → 章节拆分(按

/

/

或 分割)→ 段落提取 → 图片提取(封面 + 正文图片)→ 存储到 data/books/{book_id}/ → 写入 book.db(books 表 + book_chapters 表) → 返回 book_id

【阅读 + 批注】 GET /api/books/{id}/chapters/{ch} → 返回章节内容(段落数组)+ 已有批注 + AI名/用户名 → 前端渲染段落 + 批注气泡 → 用户点击「AI 批注」→ POST /api/books/{id}/chapters/{ch}/annotate-all → 后端逐段处理: ├ 加载世界书人设 + 查询书名 ├ 获取前 3 章摘要(每章≤200字) ├ 获取最近 15 条聊天消息 ├ 构建批注 Prompt(人设+时间+书名+上下文+段落文本) ├ stream_ai() 流式生成 → 解析 JSON 批注 └ 存入 book_annotations 表 → SSE 推送前端 → 前端实时渲染批注气泡

【选文聊天】 选中文字 → 弹出工具栏「💬 问问AI」→ 点击 → 弹出提问窗 → 输入问题(可选)→ 提交 → 打开聊天面板 → 加载最近 6 条主聊天消息(GET /api/conversations/{conv_id}/messages?limit=6) → 显示上下文 + 分割线 + 用户消息 → POST /api/conversations/{conv_id}/send(SSE 流式) → AI 回复流式渲染为多气泡(按 \n\n 分段) → 支持继续在面板内追问(同一对话) → 所有消息保存在主聊天历史中


### 小剧场(独立角色扮演聊天)
194. **完全独立** — 小剧场使用独立的数据库表(`theater_conversations` + `theater_messages`),与主聊天完全隔离,所有对话记录**不计入记忆库**,记忆总结时不会涉及小剧场内容
195. **极简 Prompt** — 仅注入选中角色的人设 + 最近 N 条上下文,不注入系统能力(`[MUSIC]`/`[CAM_CHECK]`/`[ALARM]` 等)、不注入记忆/日程/位置/活动日志,干净纯粹的对话体验
196. **多角色管理** — 支持创建/编辑/删除多套角色人设,每个角色可配置独立的名称、人设内容(system prompt)、默认模型、默认温度、默认上下文条数。角色数据存储在 `data/theater_personas.json`
197. **对话管理** — 侧栏对话列表,支持新建/删除/重命名剧场,每个对话绑定一个角色,切换角色时自动更新对话设置
198. **SSE 流式回复** — 与主聊天一致的 SSE 流式输出,逐字显示,等待时显示「思考中/正在输入」循环动画 + 弹跳小圆点
199. **TTS 语音合成** — 完整保留服务端流式 TTS(复用 `tts.py` 的 `TTSStreamer`),支持音色选择和重播。TTS 事件通过 `tm_` 消息前缀与主聊天互相隔离,避免多页面同时播放
200. **茶色暗色主题** — 深棕色背景(`#1e1a16`)+ 茶色强调色(`#c8956c`),与主聊天的暖光风格形成差异化视觉区分
201. **多端同步** — WebSocket 广播 `theater_conv_created/updated/deleted`、`theater_msg_created/updated/deleted` 事件,多设备实时同步
202. **图片上传** — 支持多模态,可上传图片/视频作为附件发送
203. **重新生成** — AI 消息支持一键重新生成

### 奥罗斯幽林(Ghost Forest TRPG)
300. **独立 TRPG 模块** — 完整的桌游风角色扮演冒险游戏,AI 担任 DM(地下城主持人),玩家通过选项和 D20 骰子推进剧情。数据存储为纯 JSON 文件(`data/ghost_forest/`),不使用数据库,与主聊天系统完全隔离
301. **DM/玩家人设管理** — 支持创建/编辑/删除多套 DM 和玩家人设预设,存储在 `data/ghost_forest/_personas.json`,开始新冒险时选择人设组合
302. **AI 生成剧情大纲** — 玩家提供冒险点子,AI(SSE 流式)生成完整剧情大纲:标题、世界观、主线情节、NPC 列表、关键道具、剧情分支、氛围描写
303. **角色属性系统** — 五维属性(力量/敏捷/智力/魅力/运气),初始各 3 点 + 7 点自由分配。HP 100,最大回合 20(±5 弹性)。属性影响选项的 DC(难度等级)判定
304. **D20 骰子判定** — Three.js 3D 实时渲染 D20 骰子动画,物理模拟旋转和弹跳。投骰结果 + 属性修正 ≥ DC 即成功,影响后续剧情走向
305. **AI 叙事回合** — 每回合 AI 生成剧情叙述 + 3-4 个选项(含属性关联和 DC),支持 D 选项自定义行动。AI 回复以 SSE 流式输出,实时渲染 Markdown
306. **道具系统** — AI 可通过 `[ITEM:道具名:数量:描述]` 指令给予玩家道具,选项可设置 `item_cost` 消耗道具。道具栏实时显示在游戏界面底部
307. **AI 对话历史压缩** — 当 AI 历史超过 16 条消息时,使用 `gemini-3.1-flash-lite` 自动压缩旧对话为摘要,保留最近 6 条原文,减少 token 消耗。压缩前备份完整历史到 `ai_history_full`
308. **前情回顾** — 📜 按钮打开全屏回顾面板,显示所有已完成回合的剧情和结果叙述,方便回顾冒险历程。支持 🔊 按钮回放每回合的 TTS 音频(通过 HEAD 探测已缓存分片)
309. **大结局生成** — 冒险结束时,AI 生成 400-800 字的完整大结局叙事(纯文学风格,不含选项),以 SSE 流式呈现。结局存储在 `session.finale`,重新进入时直接展示
314. **TTS 语音合成** — 复用 `tts.py` 的 `TTSStreamer`,通过 SSE 流式推送 `tts_chunk`/`tts_done` 事件。叙述/选择结果/大结局三个 SSE 端点均支持 `tts_enabled`/`tts_voice` 查询参数,AI 生成叙述后自动按句切分合成语音,前端按序播放。TTS 音频缓存以 `gf_{sid}_r{round}_s{seq}.mp3` 命名,支持回放
315. **动态回合数** — 支持游戏中修改最大回合数(±5 弹性范围),AI 根据剩余回合数自动调整叙事节奏
316. **D 选项自定义行动** — 选项列表末尾固定显示「D. 你想做别的...」输入框,玩家可自由输入创意行动
310. **场外求助(剧场联动)** — 📞 按钮打开半屏聊天面板,连接到主聊天系统。聊天 AI 通过 `theater_session_id` 参数自动获取当前游戏状态(属性/HP/道具/最近剧情/当前选项),以 `[剧场属性:xxx ±n]` 和 `[剧场道具:xxx]` 指令直接修改游戏数据,实现跨系统联动
311. **移动端适配** — 游戏界面采用两段布局(固定状态区 + 统一滚动区),正文和选项合并滚动,手机上正文显示面积最大化。属性芯片紧凑排列(nowrap),触屏友好
312. **会话管理** — 支持多局游戏并行,列表显示标题/状态/创建时间,支持暂停/恢复/删除。状态机:draft → outlined → playing → paused/finished
313. **模型切换** — 游戏中可实时切换 AI 模型(下拉框),支持所有已配置的模型(Gemini/硅基流动/中转站)

### 奥罗斯幽林工作流程

【创建冒险】 选择 DM 人设 + 玩家人设 → 输入冒险点子 → POST /api/ghost-forest/sessions → AI 生成剧情大纲(SSE)→ POST .../generate-outline → 解析 JSON 大纲(标题/背景/NPC/道具/分支) → 分配属性点(5 维各 3 + 7 自由分配)→ POST .../start → 状态 playing

【游戏回合(循环)】 POST .../narrate → AI 生成当前回合叙述 + 选项(SSE 流式) → 前端解析 JSON:narration + options[{key, text, stat, dc}] → 玩家选择选项 → 需要骰子?→ Three.js D20 骰子动画 → POST .../choose {chosen, dice_roll, custom_input} → AI 根据判定结果生成后续叙述(SSE 流式) → 更新 HP/属性/道具/回合数 → 下一回合

【AI 历史压缩(自动触发)】 narrate/choose 完成后 → 检查 ai_history 长度 > 16 → gemini-3.1-flash-lite 压缩旧消息为摘要 → 备份 ai_history → ai_history_full → ai_history = [压缩摘要] + [最近 6 条原文]

【场外求助(剧场联动)】 点击 📞 → 打开聊天面板 → 发送消息到主聊天(携带 theater_session_id) → 后端注入游戏状态到聊天 AI prompt → AI 回复中含 [剧场属性:力量 +2] / [剧场道具:神秘钥匙] → 后端解析指令 → 修改游戏会话数据 → SSE theater_update 事件 → 前端接收 → 刷新游戏 UI

【大结局】 回合数到达上限 / HP 归零 / AI 判断冒险结束 → 点击 🏰 大结局 → POST .../finale(SSE 流式 + TTS) → AI 生成 400-800 字结局叙事 → 存储 session.finale → 状态 → finished

【TTS 语音合成(复用 TTSStreamer)】 narrate/choose/finale 端点接收 tts_enabled=true&tts_voice=xxx 查询参数 → AI 生成叙述文本 → 创建 TTSStreamer(msg_id, voice, sse_queue=queue) → TTSStreamer.feed(narration) → 按 100-200 字切分 → 异步合成 mp3 → 合成完成 → SSE 推送 tts_chunk {msg_id, seq, url} → 前端 enqueueTTSChunk() 按序播放 → 全部合成完 → SSE 推送 tts_done → 前端 finishTTSForMsg() 标记完成 → 音频缓存:data/tts_cache/gf_{sid}_r{round}_s{seq}.mp3 → 前情回顾可通过 🔊 按钮回放已缓存的音频(HEAD 探测分片存在性后依次播放)


### 设备活动日志系统(PC + 手机)
170. **双设备活动采集** — 自动记录 PC 前台窗口和手机前台 App 的使用情况,存储为 JSONL 日志,按日期分文件,保留最近 3 小时
171. **PC 前台窗口采集** — 后台守护线程每 60 秒通过 `win32gui.GetForegroundWindow()` 获取当前窗口标题 + `psutil.Process.name()` 获取进程名,**每分钟无条件记录**(窗口没变也写入,确保摘要时长计算准确),自动过滤 Program Manager(桌面)
172. **Android 前台 App 上报** — `AionPushService` 中独立线程每 60 秒通过 `UsageEvents` API(主)/ `UsageStatsManager`(备)获取当前前台应用包名,POST 到 `/api/activity/report`,**每次轮询都上报**(无去重,服务端摘要层负责合并);同时注册 `BroadcastReceiver` 监听屏幕开关事件
173. **App 名称解析** — 服务端维护 `KNOWN_APPS` 映射表(80+ 常见应用),将包名/进程名转为中文显示名(如 `com.xingin.xhs` → `小红书`、`chrome.exe` → `Chrome`),自动过滤系统应用(桌面、SystemUI 等)
174. **JSONL 存储 + 自动清理** — 每条日志按日期写入 `data/activity_logs/{YYYY-MM-DD}.jsonl`,`cleanup_old_activity_logs()` **每 5 分钟最多执行一次**清理超过 3 小时的旧条目
175. **并发安全** — `threading.Lock` 保护 JSONL 文件读写,防止 PC 后台线程和手机 API 协程并发写入导致数据丢失
176. **10 分钟活动摘要** — `generate_activity_summary()` 将原始日志按 10 分钟窗口分组,**时长权重排序**(主要活动排前面),**carry-forward 状态追溯**(向前找每个设备最后状态填补窗口开头空白),空窗口标记为「没有活动」并自动合并连续空窗口
177. **前端日志查看器** — `/activity-logs` 页面支持「最近 3 小时」和「按日期」两种查看模式,可按设备(全部/PC/手机)筛选,实时 WebSocket 推送新日志,加载后自动滚动到最新位置
178. **前端摘要弹窗** — 📋 按钮打开摘要弹窗(GET `/api/activity/summary`),展示每 10 分钟一条压缩摘要,空闲条目斜体灰色显示,底部统计压缩比
179. **清空日志** — 前端「清空全部日志」按钮(POST `/api/activity/clear`),删除所有 JSONL 文件
180. **依赖** — PC 端需要 `pywin32`(`win32gui`)+ `psutil`,需安装到项目 `.venv` 中

### AI 联动(设备活动 × AI 交互)
181. **AI联动总开关** — `/activity-logs` 页面顶部「AI联动」开关(`activity_tracking_enabled`),控制所有 AI 与设备活动的交互。关闭后 `[查看动态:n]` 能力不注入 prompt、哨兵/监控不注入活动摘要。通过 `GET/PUT /api/activity/config` 管理,WebSocket 广播 `activity_config_changed` 事件多端同步
182. **聊天中查看动态** — AI联动开启时,Chat prompt 注入 `[查看动态:n]` 能力(n=1~12),Core 可主动查看用户设备动态并生成关怀评论,后端自动 clamp n 值防止异常
183. **哨兵注入活动摘要** — Sentinel 分析截图时自动注入近 60 分钟 6 条活动摘要(格式 `[HH:MM~HH:MM] 摘要`),作为辅助判据帮助哨兵更准确判断用户状态
184. **定时监控注入活动摘要** — `[Monitor:...]` 触发 Core 分析时自动注入近 120 分钟 12 条活动摘要,帮助 Core 结合设备使用模式理解用户行为

### 设备活动日志工作流程

【PC 前台窗口采集(activity.py PCActivityTracker)】 服务启动 → lifespan 中 pc_tracker.start() → 守护线程循环(60 秒间隔): win32gui.GetForegroundWindow() → 获取窗口标题 psutil.Process(pid).name() → 获取进程名 → 过滤 Program Manager(桌面) → 无条件写入 append_activity_log()(加锁) → 标题变化?→ 是 → 控制台打印 + WebSocket 广播 → 每 5 分钟触发 cleanup_old_activity_logs()

【Android 前台 App 上报(AionPushService activityThread)】 服务启动 → startActivityThread() → 独立线程循环(60 秒间隔): UsageEvents API 查询最近 120 秒事件 → 取最后一个 ACTIVITY_RESUMED 的包名 ├ 成功 → 过滤自身包名 → 无条件 POST /api/activity/report {device:"phone", app:包名} └ 失败 → fallback UsageStatsManager queryUsageStats → BroadcastReceiver 监听 SCREEN_OFF/SCREEN_ON → 立即上报 "screen_off"/"screen_on" ⚠ 需要「使用情况访问权限」(Settings > Special access > Usage access)

【服务端处理(routes/activity.py)】 POST /api/activity/report → resolve_app_name() 包名→中文名(KNOWN_APPS 映射 + 系统应用过滤) → append_activity_log() 加锁写入当天 JSONL 文件 → cleanup_old_activity_logs() 节流清理 >3 小时的条目(5 分钟最多一次) → WebSocket 广播 activity_log 事件 → 前端实时更新

【10 分钟活动摘要(activity.py generate_activity_summary)】 GET /api/activity/summary → read_recent_activity(3h) → 过滤系统应用 + Program Manager → 按 10 分钟窗口分组(时间范围:首条记录 ~ 上一个已结束窗口) → carry_forward:每个窗口向前查找各设备最后状态,填补开头空白 → _summarize_window():设备分组 → 过滤亮屏 → 构建 (app, titles, duration) 段 → 按 display_name 合并同名段(如 TortoiseProc+TortoiseMerge→SVN) → 按时长降序排列 → 格式化为 "小红书 5分钟, 微信 2分钟" → 空窗口标记「没有活动」→ 连续空窗口合并(如 15:20~15:50 没有活动)

【前端查看(activity-logs.html)】 「最近3小时」→ GET /api/activity/recent → 滚动到底部 「按日期」→ GET /api/activity/dates → GET /api/activity/logs/{date} → 设备筛选 → 列表渲染 → WebSocket 监听 activity_log 事件实时追加 → 📋 摘要按钮 → GET /api/activity/summary → 弹窗展示 → 「清空全部」→ POST /api/activity/clear → 「AI联动」开关 → PUT /api/activity/config → WebSocket 广播 activity_config_changed

【AI联动(activity.py get_activity_summary_for_prompt)】 ├ 哨兵定时截图 → get_activity_summary_for_prompt(6) → 近 60 分钟 6 条摘要注入 Sentinel prompt ├ [Monitor:] 触发 → get_activity_summary_for_prompt(12) → 近 120 分钟 12 条摘要注入 Core trigger prompt └ [查看动态:n] → get_activity_summary_for_prompt(n) → 近 n×10 分钟 n 条摘要 → 组装 prompt → Core 生成回复 → 保存 system 消息 + assistant 回复 → WebSocket 广播 ⚠ AI联动开关关闭时,以上三条路径均返回空字符串,不注入任何摘要


### 爱的印记(AI 礼物系统)
317. **AI 自主送礼** — 每次自动记忆总结完成后,AI 综合判断是否需要给用户送一份礼物。判断依据:今天的聊天是否有特别温馨/感动/有意义的内容、是否是特殊日子(节日/纪念日/生日等)、用户的心情状态。Prompt 注入当前精确时间(年月日星期时分秒)+ 本次总结的所有记忆摘要 + 最近聊天上下文 + 世界书人设,要求 AI 返回结构化 JSON(`givegift` / `image_prompt` / `message`)
318. **硅基流动 Kolors 生图** — AI 决定送礼后,使用 `image_prompt` 调用硅基流动 `Kwai-Kolors/Kolors` 模型(免费)生成 1024×1024 图片。Prompt 约束为 cute cartoon style、不生成真实人物。图片 URL 1 小时过期,后端立即下载保存到 `data/uploads/gift_{timestamp}.png`
319. **礼物弹窗动画(全页面)** — 礼物生成后通过 WebSocket 广播 `gift_pending` 事件。前端任何页面(聊天页 chat.html + 所有子页面 common.js)收到后弹出全屏礼物动画。打开聊天页/子页面时也会自动检查 `GET /api/gift/pending` 并弹窗
320. **礼物盒开启流程** — ① SVG 礼物盒从底部弹跳入场 → ② 用户点击打开 → 播放「打开礼物.mp3」音效 → 盒盖飞走 + 60 个彩色礼花粒子爆炸 → ③ 图片从中心缩放淡入 → ④ 点击图片 → AI 的配图文字滑出 → 「💝 收下礼物」按钮出现 → ⑤ 点击收下 → 整体缩小飞走动画 → POST 标记 received
321. **爱的印记陈列馆** — `/gift` 页面(`gift.html`),暗色画廊风格,3 列缩略图网格展示所有已收到的礼物(缩略图+日期),点击打开详情弹窗(大图+日期+文字+删除按钮),按时间倒序排列
322. **礼物数据** — 存储在 SQLite `gifts` 表(id, image_path, message, created_at, status, received_at),status 为 `pending`(未领取)或 `received`(已领取)。删除礼物时同步清理本地图片文件
323. **不过度送礼** — Prompt 中明确要求 AI「不要每次都送,只在真正值得的时候才送,大部分时候应该返回 false」
324. **测试按钮** — 爱的印记页面右上角「🎁 测试送礼」按钮,取最近 5 条记忆 + 最近 20 条上下文触发完整送礼流程(AI 判断 + 生图 + 入库 + WebSocket 推送)
325. **主页入口** — home.html APPS 数组增加「爱的印记」(`/public/funIcon_0018_爱的印记.png`)

### 爱的印记工作流程

【触发时机(记忆总结完成后)】 _do_digest() 完成记忆总结 + 生成感慨消息 → 调用 gift.judge_and_send_gift() → 构建判断 Prompt(人设 + 当前时间 + 记忆摘要 + 上下文) → simple_ai_call() 调用核心模型 → 返回 JSON → givegift = false?→ 结束 → givegift = true?→ 提取 image_prompt + message

【生图 + 入库】 → POST https://api.siliconflow.cn/v1/images/generations model: Kwai-Kolors/Kolors, 1024x1024 → 下载图片 → 保存 data/uploads/gift_{ts}.png → INSERT INTO gifts (status='pending') → WebSocket 广播 gift_pending 事件

【前端弹窗(chat.html + common.js)】 页面加载 → GET /api/gift/pending → 有未领取?弹窗 WebSocket 收到 gift_pending → 弹窗 → SVG 礼物盒弹入 → 点击打开 → 播放音效 + 礼花 → 图片展示 → 点击图片 → 显示配图文字 → 「收下礼物」 → POST /api/gift/{id}/receive → 飞走动画

【陈列馆(gift.html)】 GET /api/gift/list → 3列缩略图网格 → 点击缩略图 → 详情弹窗(大图+文字+日期) → 可删除 → DELETE /api/gift/{id}(同步清理图片文件)


### 奥罗斯财团(基金持仓监控)
326. **持仓管理** — SQLite `fund_holdings` 表存储基金持仓信息(代码、名称、份额、平均成本、总成本、跌幅/涨幅预警阈值),前端支持添加/编辑/删除,持仓列表可折叠收起
327. **数据拉取(不涉及 AI)** — 使用 akshare 拉取所有持仓基金的最新净值和涨跌幅(`fund_open_fund_daily_em` + `fund_open_fund_info_em` 兜底),新浪财经接口拉取上证指数实时涨跌。根据持仓计算当前市值、浮盈亏金额/百分比,检查是否触发预警阈值。拉取结果缓存到 `fund_cache.json`,刷新页面后仍可显示
328. **历史走势查询** — `fund_open_fund_info_em` 拉取最近 N 天净值走势,分析时注入近 30 日走势摘要(最低/最高/整体趋势)
329. **AI 分析** — 将持仓数据 + 历史走势 + 大盘背景 + 用户投资倾向拼成结构化 prompt,连同世界书人设 + 最近 20 条聊天上下文一起发送给当前聊天模型。AI 回复作为 assistant 消息插入聊天 + TTS 语音播报 + WebSocket 广播
330. **每日定时分析** — `FundScheduler` 后台线程每 30 秒检查,交易日 14:45 自动触发分析。使用 `chinese_calendar.is_workday()` 判断交易日(自动处理周末 + 中国法定节假日含调休)
331. **功能开关** — 顶部 toggle 开关,状态持久化在 `fund_config.json`,关闭后定时任务不触发、不影响其他功能
332. **投资倾向** — 可编辑的文本框,保存后在 AI 分析时注入 prompt(如"计划买入黄金,等合适坑位"),帮助 AI 给出更贴合的建议
333. **手动操作** — 「🔄 刷新数据」仅拉取数据不调 AI;「📊 立即分析」拉数据 + 调 AI + TTS,操作不阻塞页面,可自由切换到其他页面
334. **主页入口** — home.html APPS 数组增加「奥罗斯财团」(`/fund`)

### 奥罗斯财团工作流程

【手动刷新数据(不调 AI)】 点击「🔄 刷新数据」→ POST /api/fund/fetch → akshare 拉取全量基金日数据 + 新浪财经拉取上证指数 → 逐只匹配持仓基金,计算盈亏、检查预警 → 缓存到 fund_cache.json → 返回前端渲染

【手动/定时 AI 分析】 点击「📊 立即分析」→ POST /api/fund/analyze(或 14:45 定时触发) → 拉取持仓数据 + 30 日历史走势 → 生成分析 prompt(持仓明细 + 走势摘要 + 大盘 + 投资倾向) → 组装消息(世界书人设 + 20 条上下文 + prompt) → stream_ai() 流式调用当前模型 → 插入系统消息「💰 奥罗斯财团 — 基金持仓分析」+ AI 回复 → TTS 语音播报 + WebSocket 广播到聊天页

【定时任务(FundScheduler)】 后台线程每 30 秒检查 → 14:45 + 交易日 + 开关开启 → 触发 run_fund_analysis() → 等待到 14:46 避免重复触发


### 浏览器保活 & 系统通知
105. **静音音频保活** — 页面加载后自动创建 AudioContext 播放无声音频(30秒循环),防止手机浏览器后台休眠导致 WebSocket 断连和闹铃失效
106. **Web Notification** — 闹铃触发和监控提醒时通过 `Notification API` 发送系统级推送通知,即使浏览器在后台也能看到

### Android 原生 App(AionApp / Aion Oloth)
107. **WebView 壳应用** — Java Android 项目,WebView 加载 chat.html,支持文件上传、麦克风权限、全屏沉浸
108. **双地址启动页** — LauncherActivity 提供「家庭WiFi」和「Tailscale」两个地址入口,支持「记住选择」下次自动进入
109. **原生录音桥 AudioBridge** — 绕过 WebView 中 `getUserMedia` 需要 HTTPS 的限制,使用 Android 原生 `AudioRecord`(16kHz, VOICE_RECOGNITION)录音,通过 `@JavascriptInterface` 将 base64 PCM 数据回调到 JS。视频录制期间自动转发 PCM 帧给 VideoBridge
110. **原生视频录制桥 VideoBridge** — MediaCodec(H.264) + MediaCodec(AAC) + MediaMuxer 编码 MP4,复用 CameraBridge 的视频帧和 AudioBridge 的音频帧进行视频录制,录制期间摄像头预览不中断。JS 通过 `window.AionVideo` 接口控制录制(`startRecord`/`stopRecord`/`cancel`),`stopRecord` 返回 base64 编码的 MP4 数据
111. **手势导航适配** — 兼容 Vivo X300 Pro 等全面屏手势导航,返回键弹出对话框(切换地址 / 退出 / 取消)

### Android 前台推送服务(AionPushService)
115. **前台服务 + 独立 WebSocket** — `AionPushService` 作为 Android 前台服务运行,通过 OkHttp 维持独立于 WebView 的 WebSocket 长连接(`/ws`),不依赖页面生命周期
115b. **GPS 定位上报** — 独立定位线程每 10 分钟 GET `/api/location/config` 检查 `active` 字段,`active=true` 时获取 GPS 坐标并 POST `/api/location/heartbeat`,`active=false` 时跳过(省电)
115c. **设备活动上报** — 独立活动线程每 60 秒通过 UsageEvents API 获取前台应用包名,POST `/api/activity/report`;BroadcastReceiver 监听屏幕开关事件即时上报。需要 `PACKAGE_USAGE_STATS` 权限(AndroidManifest 声明 + 用户手动在「使用情况访问权限」中授权)
116. **三级通知渠道** — ① `aion_keepalive`(保活):常驻通知栏"Aion 在线中 ✨",低优先级不打扰;② `aion_messages`(消息):AI 回复通知,默认优先级;③ `aion_alarm`(闹铃与监控):高优先级 heads-up 弹出式通知 + 声音振动 + 锁屏可见
117. **智能通知过滤** — 只推送 3 种消息:`schedule_alarm`(闹铃⏰)、`monitor_alert`(定时监控提醒👁)、`new_message` 中 role=assistant 的 AI 回复(💬)。系统消息/cam_check/msg_created 等均不推送,避免通知轰炸
118. **前后台状态感知** — WebViewActivity 的 `onResume`/`onPause` 通过 Intent 通知 Service 当前是否在前台。app 前台时只推送闹铃和监控(高优先级),不推送 AI 消息(避免重复);app 后台/锁屏时推送所有类型
119. **WakeLock + WiFi Lock 保活** — `PARTIAL_WAKE_LOCK` 防止 CPU 深度休眠,`WIFI_MODE_FULL_LOW_LATENCY` 防止锁屏后 WiFi 休眠。这是保证锁屏后 WebSocket 不断的关键
120. **独立后台线程心跳** — 使用纯 Java `Thread` + `Thread.sleep(45s)` 做心跳循环(不用 Android 的 HandlerThread/Looper),每 45 秒发送 `{"type":"ping"}` 文本消息保持连接活性,同时检查健康状态(120 秒无消息则强制重连)
121. **Generation 计数器防重连风暴** — 每次新建 WebSocket 连接 `wsGeneration++`,旧连接的 `onClosed`/`onFailure` 回调发现 generation 不匹配则直接 return,不触发重连。关闭旧连接用 `cancel()` 而非 `close()`,`cancel()` 不触发回调
122. **NetworkCallback 网络恢复即重连** — 注册 `ConnectivityManager.NetworkCallback`,网络恢复(WiFi 重连 / Tailscale 启动)瞬间立即触发重连,不用等待心跳周期
123. **指数退避重连** — 连接失败后 3s → 6s → 12s → 24s → 30s(上限),连接成功后退避重置为 3s
124. **onTaskRemoved 自复活** — 用户划掉 app 后,通过 `AlarmManager.setExactAndAllowWhileIdle()` 3 秒后自动重启服务。但"强制停止"仍会彻底杀死服务(Android 系统限制)
125. **WebView 回前台自动刷新** — `WebViewActivity.onResume()` 中检测 WebView 内 WebSocket 状态(readyState !== 1 则重连),并延迟 1.5 秒调用 `loadMessages()` 拉取后台期间错过的消息
126. **服务端 ping/pong** — 服务端 WebSocket 端点处理客户端发来的 `{"type":"ping"}`,回复 `{"type":"pong"}`,作为应用层心跳确认
127. **服务端广播日志** — `ws.py` 的 `broadcast()` 每次广播打印 `type`、成功/失败/总客户端数,方便排查推送问题
128. **服务端连接异常兜底** — WebSocket 端点用 `except Exception` + `finally: manager.disconnect(ws)` 替代仅 `except WebSocketDisconnect`,防止 RST 等异常导致死连接留在 `active` 列表中

### Android 推送服务工作流程

【启动】 LauncherActivity 选择地址 → startForegroundService(AionPushService, wsUrl) → Service 创建前台通知"Aion 在线中 ✨" → 获取 WakeLock + WiFi Lock → OkHttp WebSocket 连接 ws://host:port/ws → 启动心跳线程 + 注册 NetworkCallback → 启动定位线程(10 分钟间隔,独立 Java Thread) → 启动活动上报线程(60 秒间隔,独立 Java Thread)+ 注册屏幕亮灭 BroadcastReceiver

【消息接收与通知】 服务端广播 WebSocket 消息 → OkHttp onMessage 回调 → 解析 JSON type 字段: ├ schedule_alarm → 始终弹高优先级通知(⏰ 闹铃,含内容预览,锁屏可见) ├ monitor_alert → 始终弹高优先级通知(👁 监控提醒,含监控内容) ├ new_message (role=assistant) → 仅 app 后台时弹通知(💬 AI 名: 消息预览) └ 其他(msg_created/cam_check/system 等)→ 忽略,不弹通知

【心跳保活(独立 Java Thread)】 while (running): Thread.sleep(45s) if (已连接): 发送 {"type":"ping"} → 服务端回复 {"type":"pong"} → 更新 lastPongTime if (120秒无 pong): 判定连接死亡 → 关闭 + 重连 if (未连接 && 无重连计划): 立即重连

【断线重连】 onFailure/onClosed → 检查 generation 是否匹配 → 否则 return → 匹配则标记 wsConnected=false → Thread.sleep(退避时间) → connectWebSocket() → 退避时间翻倍(上限 30s)→ 成功后重置为 3s

【网络恢复】 NetworkCallback.onAvailable → 如果 WebSocket 未连接 → 立即重连

【GPS 定位上报(独立 Java Thread,10 分钟间隔)】 while (running): Thread.sleep(10min) GET /api/location/config → 读取 active 字段 ├ active = false → 跳过本轮(安静时段或功能关闭,不采集 GPS) └ active = true: LocationManager.requestSingleUpdate(GPS_PROVIDER / NETWORK_PROVIDER, 60s超时) → 获取坐标(WGS84) → POST /api/location/heartbeat {lng, lat, accuracy} → 服务端三级研判处理

【app 划掉自复活】 onTaskRemoved → AlarmManager.setExactAndAllowWhileIdle(3s后) → 重启 Service

【设备活动上报(独立 Java Thread,60 秒间隔)】 while (running): Thread.sleep(60s) UsageEvents API 查询最近 60 秒内的 MOVE_TO_FOREGROUND 事件 → 取最后一个包名 ├ 失败 → fallback UsageStatsManager queryUsageStats ├ 过滤自身包名 → 5 分钟内同一应用不重复上报 └ POST /api/activity/report {device:"phone", app:包名}

  • BroadcastReceiver 监听 SCREEN_OFF/SCREEN_ON → 即时上报 "screen_off"/"screen_on"

### 手机端语音唤醒(远程模式)
111. **麦克风来源选择** — 语音设置面板提供「本机麦克风」和「手机端麦克风」两种模式,手机端自动选择远程模式
112. **能量 VAD** — 手机端使用能量阈值 VAD 替代 WebRTC VAD(后者依赖 PC 端 sounddevice),基于 PCM 帧 RMS 能量判断人声
113. **远程 ASR** — 手机录音通过 AudioBridge → JS 拼接 WAV → POST `/api/voice/remote-asr` → 服务端硅基流动 ASR 识别
114. **完整通话流程** — 唤醒 → 录音 → ASR → 发送消息到聊天 → AI 回复 + TTS → 暂停录音 → TTS 播完恢复录音,与 PC 端体验一致

### 手机端语音工作流程

【AudioBridge 原生录音】 WebViewActivity 注入 window.AionAudio JS 接口 → remoteVoice.start() 调用 AionAudio.start() → AudioRecord(16kHz, VOICE_RECOGNITION) 启动录音线程 → 每 40ms 帧 base64 编码 → evaluateJavascript("remoteVoice._onNativeChunk(...)") → JS 端能量 VAD 判断人声(RMS > 阈值) → 静音超时 → 拼接 WAV → POST /api/voice/remote-asr → 服务端硅基流动 ASR → 返回识别文本 → 检查唤醒词 / 挂断词 / 发送消息


### 性能优化
49. **数据库索引** — messages 表 (conv_id, created_at) 复合索引 + conversations 表 (updated_at DESC) 索引
50. **消息分页加载** — API 支持 `?limit=50&before=时间戳`,默认只加载最新 50 条消息
51. **前端懒加载** — 打开对话秒加载最新 50 条,滚动到顶部自动加载更早消息
52. **发送历史优化** — 发消息时用 SQL LIMIT 直接取最近 N 条,不再全量加载再截断
53. **WebSocket 事件扩展** — cam_check 和 debug 事件同时通过 SSE + WebSocket 双通道广播,语音发送的消息也能获得完整 debug 信息

### PWA 支持(Progressive Web App)
54. **Web App Manifest** — `manifest.json` 声明 App 名称(Aion Chat)、图标(192/512)、主题色、全屏 standalone 模式
55. **Service Worker** — `sw.js` 从根路径 `/sw.js` 提供,作用域覆盖全站,让浏览器识别为可安装 PWA
56. **手机安装为独立 App** — Android Chrome 添加到主屏幕后全屏运行,无地址栏/标签栏,体验接近原生 App
57. **iOS 支持** — 通过 `apple-mobile-web-app-capable` + `apple-touch-icon` meta 标签支持 Safari 添加到主屏幕

### 外网远程访问(Tailscale)
58. **Tailscale 虚拟组网** — 电脑和手机安装 Tailscale 并登录同一账号,通过 WireGuard 加密隧道直连,无需公网 IP 或端口转发
59. **安全性** — 8080 端口不对公网开放,仅 Tailscale 虚拟网络内设备可访问,数据端到端加密
60. **固定 IP 访问** — 通过 Tailscale 分配的 `100.x.x.x` 固定 IP 访问,手机 4G/5G 外网环境可用
61. **PWA + Tailscale 配合** — 手机 Chrome `chrome://flags` 将 Tailscale IP 标记为安全源后,可正常安装 PWA

### 记忆系统工作流程

【即时哨兵 instant_digest — 每次发消息自动触发】 用户发消息 → flash-lite 分析最近对话 → 返回 JSON: { "is_search_needed": true/false, // 是否需要搜索记忆 "keywords": ["关键词1", "关键词2"], // 搜索关键词 "require_detail": true/false, // 是否需要追溯原文 "status": "用户当前状态" // 状态变化时更新 chat_status } ↓ (如果 is_search_needed = true) recall_memories: 向量相似度×0.6 + 关键词命中率×0.3 + 重要度×0.1 → Top 5 记忆注入 prompt(自动与背景记忆去重) ↓ (如果 require_detail = true 且有召回) fetch_source_details: 在记忆 source 时间范围内按关键词筛选原始对话 → 原文细节追加注入 prompt

【记忆总结 _do_digest — 手动点击按钮 / 自动定时触发】 自动触发条件:每 30 分钟检测,用户已 30 分钟未对话 且 未总结消息 ≥ 30 条 手动触发:无最低条数限制,共用锚点不会重复总结 从锚点时间之后取消息 → 每 30 条一组(余数<10合并)→ 串行处理: 核心模型(当前聊天模型)分析,注入世界书 AI/用户人设 → 输出 JSON: {"summary": "...", "keywords": [...], "importance": 0.8, "unresolved": true/false} → embedding 向量化 → 存入 SQLite → 更新锚点 全部组处理完毕后 → 核心模型带上下文生成感慨 → 插入系统胶囊 + assistant 消息


### 向量记忆库工作流程

【背景记忆浮现 build_surfacing_memories — 每次发消息/重新生成时】 即时哨兵返回 topic → 三层浮现策略: ① unresolved 优先:memories 表 unresolved=1 的记忆(最多 2 条) ② 话题相关:用 topic 做 embedding,余弦相似度 ≥ 0.50 的 Top 3 ③ 近期补充:最近 3 天的记忆,补满 8 条 → 去重后以 [背景记忆] 块注入 prompt → 返回 surfaced_ids 供 RAG 召回去重

【RAG 精确召回 — 仅 is_search_needed=true 时】 recall_memories: 向量相似度×0.6 + 关键词×0.3 + 重要度×0.1 → 过滤掉已在背景记忆中的 id → Top 5 注入 [相关记忆]

【写入】手动按钮 / 自动定时触发(共用 _do_digest) → 核心模型(当前聊天模型)+ 世界书人设注入,从消息提取记忆 (summary + keywords + importance + unresolved) → gemini-embedding-001 向量化(3072维) → 存入 SQLite memories 表 + WebSocket 广播 → 全部完成后生成感慨,插入聊天(系统胶囊 + assistant 消息)


## API 一览

### 对话/消息
| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/conversations` | GET | 对话列表 |
| `/api/conversations` | POST | 创建对话 |
| `/api/conversations/{conv_id}` | PUT | 更新对话(标题/模型) |
| `/api/conversations/{conv_id}` | DELETE | 删除对话 |
| `/api/conversations/{conv_id}/messages` | GET | 消息列表(支持 `?limit=50&before=时间戳` 分页) |
| `/api/conversations/{conv_id}/send` | POST | 发送消息(SSE 流式) |
| `/api/conversations/{conv_id}/regenerate` | POST | 重新生成 AI 回复(SSE 流式) |
| `/api/messages/{msg_id}` | PUT | 编辑消息 |
| `/api/messages/{msg_id}` | DELETE | 删除消息 |
| `/api/cam-check-trigger` | POST | Core 主动查看监控触发(前端延迟 5 秒后调用) |

### 摄像头
| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/cam/status` | GET | 摄像头和监控状态 |
| `/api/cam/cameras` | GET | 可用摄像头列表 |
| `/api/cam/open` | POST | 打开摄像头 |
| `/api/cam/close` | POST | 关闭摄像头 |
| `/api/cam/monitor/start` | POST | 开始定时监控 |
| `/api/cam/monitor/stop` | POST | 停止定时监控 |
| `/api/cam/config` | GET/POST | 读取/保存摄像头配置 |
| `/api/cam/frame` | GET | 获取当前帧(JPEG) |
| `/api/cam/screenshot` | POST | 手动截图 |
| `/api/cam/logs` | GET | 日志日期列表 |
| `/api/cam/logs/{date}` | GET | 指定日期的日志条目 |

### 设置/世界书/状态
| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/settings` | GET/POST | 读取/保存设置(API Key 等) |
| `/api/worldbook` | GET/POST | 读取/保存世界书 |
| `/api/chat_status` | GET | 获取当前聊天状态摘要 |
| `/api/models` | GET | 可用模型列表 |

### 记忆库
| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/memories` | GET | 获取所有记忆(按时间倒序) |
| `/api/memories` | POST | 手动添加记忆(自动向量化) |
| `/api/memories/{id}` | PUT | 编辑记忆(重新向量化,支持 unresolved 字段) |
| `/api/memories/{id}` | DELETE | 删除记忆 |
| `/api/memories/{id}/unresolved` | PATCH | 切换记忆的 unresolved 状态 |

### TTS 语音合成
| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/tts` | POST | TTS 合成代理,接收 `{text, voice}`,返回 mp3 音频流 |
| `/api/tts/voices` | GET | 获取硅基流动账号下的可用音色列表 |
| `/api/tts/audio/{name}` | GET/HEAD | 获取 TTS 缓存音频分片(`{msg_id}_s{seq}.mp3`),HEAD 用于前端探测分片是否存在 |

### 文件管理
| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/files` | GET | 导出文件列表 |
| `/api/files/{filename}` | DELETE | 删除导出文件 |
| `/api/upload` | POST | 上传图片/视频 |

### 音乐
| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/music/search` | GET | 搜索歌曲(`?keyword=xxx&limit=5`) |
| `/api/music/detail/{song_id}` | GET | 获取歌曲详情 |
| `/api/music/play` | POST | 获取播放信息(`{song_id}` → 返回歌曲信息 + audio_url + web_url) |
| `/api/music/stream/{song_id}` | GET | 服务端代理推流(后端实时获取 CDN URL 并转发音频流,解决防盗链) |

### 语音唤醒/通话
| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/voice/status` | GET | 语音状态(开关/通话中/AI说话中) |
| `/api/voice/toggle` | POST | 开关语音监听(`{enabled, wake_word}`) |
| `/api/voice/ai-speaking` | POST | 前端通知 TTS 播放状态(`{speaking}`) |
| `/api/voice/cam-check-start` | POST | 通知语音模块 CAM_CHECK 开始,保持 AI 说话状态 |
| `/api/voice/remote-asr` | POST | 手机端远程 ASR:接收 WAV 音频文件,转发硅基流动 SenseVoiceSmall 识别 |

### 活动日志
| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/activity/report` | POST | 设备活动上报(`{device, app, title?, timestamp?}`),自动名称解析+过滤+JSONL存储+WS广播 |
| `/api/activity/status` | GET | PC 采集线程状态诊断(是否运行、线程状态、上次窗口标题等) |
| `/api/activity/dates` | GET | 返回所有有日志的日期列表 |
| `/api/activity/logs/{date}` | GET | 返回指定日期的活动日志(自动名称解析) |
| `/api/activity/recent` | GET | 返回最近 N 小时的活动日志(默认 8 小时,`?hours=N`) |
| `/api/activity/clear` | POST | 清除所有活动日志文件 |

### 日程/闹铃
| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/schedules` | GET | 日程列表(可选 `?status=active`) |
| `/api/schedules` | POST | 手动添加日程(`{type, trigger_at, content}`) |
| `/api/schedules/{id}` | DELETE | 删除日程 |

### 奥罗斯幽林 TRPG
| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/ghost-forest/personas` | GET | 列出所有 DM/玩家人设 |
| `/api/ghost-forest/personas` | POST | 创建/更新人设 |
| `/api/ghost-forest/personas/{pid}` | DELETE | 删除人设 |
| `/api/ghost-forest/sessions` | GET | 列出所有游戏会话(摘要) |
| `/api/ghost-forest/sessions` | POST | 创建新游戏会话 |
| `/api/ghost-forest/sessions/{sid}` | GET | 获取完整会话数据 |
| `/api/ghost-forest/sessions/{sid}` | PATCH | 更新会话模型 |
| `/api/ghost-forest/sessions/{sid}` | DELETE | 删除会话 |
| `/api/ghost-forest/sessions/{sid}/generate-outline` | POST | AI 生成剧情大纲(SSE) |
| `/api/ghost-forest/sessions/{sid}/start` | POST | 提交属性分配,开始游戏 |
| `/api/ghost-forest/sessions/{sid}/narrate` | POST | AI 生成当前回合叙述(SSE) |
| `/api/ghost-forest/sessions/{sid}/choose` | POST | 提交选择 + 骰子结果(SSE) |
| `/api/ghost-forest/sessions/{sid}/pause` | POST | 暂停游戏 |
| `/api/ghost-forest/sessions/{sid}/resume` | POST | 恢复游戏 |
| `/api/ghost-forest/sessions/{sid}/finale` | POST | 生成大结局(SSE) |
| `/api/ghost-forest/sessions/{sid}/summary` | POST | 生成冒险总结 |

### 定位/高德地图
| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/location/heartbeat` | POST | GPS 心跳上报(`{lng, lat, accuracy}`,可选 `force=true` 强制刷新 API) |
| `/api/location/status` | GET | 当前定位状态(坐标、地址、天气、状态、距离) |
| `/api/location/poi-search` | POST | 手动触发 POI 搜索(`{categories}` 逗号分隔类型) |
| `/api/location/pois` | GET | 获取缓存的 POI 列表 |
| `/api/location/config` | GET | 读取定位配置(含 `active` 字段供 Android 判断是否采集) |
| `/api/location/config` | POST | 保存定位配置(高德Key/开关/安静时段/阈值等) |
| `/api/location/set-home` | POST | 设置家位置(`{lng, lat}` GCJ-02 坐标) |

### SSE 事件类型(send / regenerate)
| type | 说明 |
|------|------|
| `start` | 流开始,含 AI 消息 id |
| `chunk` | 流式文本块 |
| `cam_check` | Core 触发 [CAM_CHECK],前端播放提示音+延迟触发 |
| `cam_offline` | 摄像头未开启,前端显示提示 |
| `music` | 音乐卡片数据:主推荐歌曲 + 候选列表 |
| `poi_search` | POI 搜索触发:含 msg_id + categories,前端显示蓝色搜索指示器 |
| `toy_command` | 玩具控制指令:含 commands 数组 + msg_id |
| `image_gen_start` | AI 生图开始:含 msg_id + prompt + is_selfie,前端显示橙色生图指示器 |
| `debug` | Debug 数据:模型名、token 用量、召回记忆、完整 prompt |
| `done` | 流结束 |

### WebSocket 事件类型
| type | 说明 |
|------|------|
| `conv_created/updated/deleted` | 对话变动同步 |
| `msg_created/updated/deleted` | 消息变动同步 |
| `monitor_log` | 新监控日志推送 |
| `chat_status` | 聊天状态摘要更新 |
| `memory_added` | 新记忆添加 |
| `voice_state` | 语音状态广播(开关/唤醒/聊天中/AI思考/挂断等) |
| `cam_check` | [CAM_CHECK] 触发通知(SSE + WS 双通道) |
| `music` | 音乐卡片数据广播(SSE + WS 双通道) |
| `debug` | Debug 数据广播(SSE + WS 双通道,语音发送时也能收到) |
| `monitor_alert` | 定时监控触发,前端播放提示音,手机端弹高优先级通知 |
| `schedule_alarm` | 闹铃到期触发,前端弹出确认弹窗,手机端弹高优先级通知 |
| `schedule_changed` | 日程列表变动,前端刷新面板 |
| `toy_command` | 玩具控制指令广播(含 commands 数组,SSE + WS 双通道) |
| `location_update` | 定位状态更新广播(地址、天气、状态变更等) |
| `poi_search` | POI 搜索触发广播(SSE + WS 双通道,前端显示搜索指示器) |
| `activity_log` | 新设备活动日志推送(含 device/app/title/time,前端实时追加) |
| `tts_chunk` | TTS 音频分片推送(含 msg_id/seq/url),前端收到即加入播放队列 |
| `tts_done` | TTS 合成完毕通知(含 msg_id),前端标记该消息队列已结束,播完最后一片后清理 |
| `tts_state` | 客户端→服务端:TTS 开关/音色同步(`{enabled, voice}`),服务端据此判断是否需要合成 |
| `image_gen_start` | AI 生图开始广播(SSE + WS 双通道) |
| `image_gen_done` | AI 生图完成广播(含 conv_id),前端移除指示器 |
| `image_gen_failed` | AI 生图失败广播(含 conv_id),前端移除指示器 |

### 消息角色说明
| 角色 | 说明 | 是否显示在聊天 |
|------|------|---------------|
| `user` | 用户消息 | ✅ |
| `assistant` | AI 回复(含 Core 唤醒/主动查看监控的回复) | ✅ |
| `cam_user` | Sentinel 截图查询(内部) | ❌ 隐藏 |
| `cam_log` | Sentinel 分析结果(内部) | ❌ 隐藏 |
| `cam_trigger` | Core 唤醒时的系统提示(内部) | ❌ 隐藏 |

## Prompt 注入顺序
  1. [系统设定 - AI人设] + assistant 确认 ← 缓存命中
  2. [系统设定 - 用户信息] + assistant 确认 ← 缓存命中
  3. [系统能力] 合并能力提示 + 日程列表 + assistant 确认(不含时间) ← 缓存命中
    • [MUSIC:歌曲名 歌手名] — 点歌(始终可用)
    • [CAM_CHECK] — 主动查看监控(仅摄像头开启时)
    • [POI_SEARCH:类型名] — 搜索附近 POI(仅外出状态 + 定位开启时)
    • [ALARM:datetime|内容] — 设置闹铃(始终可用)
    • [REMINDER:date|内容] — 设置日程提醒(始终可用)
    • [SCHEDULE_DEL:id] — 删除日程(始终可用)
    • [TOY:1]~[TOY:9] — 控制玩具预设档位(仅密语模式开启时)
    • [TOY:STOP] — 停止玩具(仅密语模式开启时)
    • [SELFIE:prompt] — AI 自拍生图(附带参考图,仅 AI 生图开关开启时)
    • [DRAW:prompt] — AI 自由画图(仅 AI 生图开关开启时)
    • 【当前日程列表】 — 活跃日程/闹铃一览
    • 【位置信息】 — 当前地址 + 实时天气 + 离家距离 + 状态(仅有有效坐标时注入)
  4. 当前准确时间 ← ⚡缓存分界点
    • [背景记忆] unresolved📌 + 话题相关 + 近期补充(最多8条) ← 动态
  5. [相关记忆] 向量召回的记忆(与背景记忆去重) + assistant 确认 ← 动态
  6. 聊天历史(受上下文长度滑块限制) ← 动态

## 关键实现细节
- **模块化架构**:main.py 仅约 70 行,业务逻辑拆分到 config/database/ws/ai_providers/memory/camera + routes/ 下 5 个路由模块
- **多模态构建**:`build_multimodal_messages()`(硅基流动 base64 URL)和 `build_gemini_contents()`(Gemini inline_data)
- **Token 用量捕获**:stream_ai 通过 meta dict 在流式过程中捕获 Gemini usageMetadata / 硅基流动 usage
- **Gemini 轮次交替**:Gemini API 要求 user/model 严格交替,所有系统注入都以 user+assistant 对形式插入
- **[CAM_CHECK] 流程**:后端在 SSE 中发 `cam_check` 事件 + WebSocket 广播 → 前端播放音频+5秒 setTimeout → POST trigger API → 后端 asyncio.create_task 异步截图+AI分析
- **cam_check 加载指示器**:前端用 `camCheckMsgId` 全局变量跟踪,`renderMessages()` 重建 DOM 后自动恢复指示器
- **语音唤醒架构**:voice.py 运行在独立线程,通过 `asyncio.run_coroutine_threadsafe` 桥接主事件循环;WebRTC VAD (mode=2) 做帧级人声检测(30ms/帧),不需要噪底校准
- **半双工协调**:`ai_speaking` 标志由服务端 `tts_done` WebSocket 事件驱动(前端收到后调用 `notifyVoiceAiSpeaking(false)`),暂停录音期间持续 `stream.read()` 丢弃数据防止缓冲区溢出;voice.py 的 `_async_send` 在 HTTP POST body 中携带 `tts_enabled`/`tts_voice` 参数
- **消息分页**:后端 `?limit=50&before=时间戳` 参数,前端 `loadOlderMessages()` 滚动到顶部自动加载,保持滚动位置
- **SSE + WS 双通道**:cam_check 和 debug 事件同时写入 SSE 流和 WebSocket 广播,确保语音发送的消息(无 SSE 流读取端)也能被前端接收
- **文件导出**:消息变动自动同步到 `chats/{conv_id}.md`,含 YAML front matter,导出跳过 cam_* 角色
- **监控定时器**:基于时间戳比较(`_next_capture_at`),非 sleep 阻塞,间隔修改即时生效
- **摄像头 DirectShow + 验证机制**:所有 `cv2.VideoCapture` 使用 `CAP_DSHOW` 后端(Windows MSMF 后端对 USB 摄像头不稳定);`_verify_camera()` 最多等 8 秒读到非垃圾帧(`frame.mean() > 5` 排除绿屏/黑屏)才算成功;`_capture_loop` 运行时也检测绿屏帧,连续 100 帧无效触发重连;重连逐个尝试 index 0-4 并验证,失败后 30 秒重试
- **Sentinel 日志压缩**:哨兵每次分析时输出历史概况摘要(summary),避免 Core 唤醒时全量日志导致 token 过高
- **TTS 流式推送架构**:`tts.py` 的 `TTSStreamer` 在 AI 流式输出过程中实时接收文本(`feed()`),按标点(句号/问号/感叹号/换行等)切分为 100-200 字的片段(`_try_split()` + `_find_cut_position()`),每段 `asyncio.create_task` 异步调用硅基流动 CosyVoice2-0.5B 合成 mp3(`_synthesize()`),合成完成后通过 `_dispatch()` 将音频保存到 `data/tts_cache/{msg_id}_s{seq}.mp3` 并 WebSocket 广播 `tts_chunk` 事件;`flush()` 在 AI 输出结束后处理剩余文本并等待所有合成任务完成,最后广播 `tts_done` 事件
- **TTS 多端状态同步**:前端开启 TTS 后通过 WebSocket 发送 `tts_state` 消息,`ConnectionManager.tts_clients` 字典跟踪各连接的 TTS 状态;HTTP POST(send_message/regenerate)body 中的 `tts_enabled`/`tts_voice` 通过 `set_tts_fallback()` 存入 `_tts_fallback` 作为回落,确保 cam_check/闹铃/定时监控等服务端发起的消息也能正确获取 TTS 状态(`any_tts_enabled()` + `get_tts_voice()` 同时检查两处)
- **PC 活动采集**:`PCActivityTracker` 守护线程通过 `win32gui.GetForegroundWindow()` + `psutil.Process.name()` 每 15 秒检测前台窗口变化,通过 `asyncio.run_coroutine_threadsafe()` 桥接主事件循环上报;`pywin32` 和 `psutil` 必须安装在项目 `.venv` 中(系统 Python 中的无效)
- **App 名称解析**:服务端 `KNOWN_APPS` 字典映射 80+ 常见包名/进程名→中文名,`resolve_app_name()` 返回 `None` 表示需过滤的系统应用(桌面、SystemUI 等),读取历史日志时 `_resolve_entries()` 对旧条目重新解析确保名称一致
- **活动日志清理**:`cleanup_old_activity_logs()` 读取→过滤→重写 JSONL 文件,仅保留 `KEEP_HOURS=8` 小时内的条目,每次上报时顺带执行
- **TTS 前端播放流程**:前端 `ttsQueue`(Map,key=msg_id)维护各消息的播放队列,`playNextTTSChunk()` 按 seq 顺序取出分片 URL 播放;收到 `tts_done` WebSocket 事件后标记 `q.finished = true`,当最后一片播放完毕且队列标记结束时,调用 `finishTTSForMsg()` 清理并通知语音模块(`notifyVoiceAiSpeaking(false)`)恢复录音
- **消息编辑 attachments 修复**:后端 `update_message` 广播前 `json.loads` 解析 attachments,避免前端收到字符串导致渲染崩溃
- **PWA 架构**:`sw.js` 和 `manifest.json` 物理存放在 `static/` 目录,但通过 `main.py` 的独立路由从根路径 `/sw.js`、`/manifest.json` 提供,确保 Service Worker 作用域覆盖全站
- **外网访问**:通过 Tailscale 组建虚拟局域网,WireGuard 端到端加密,无需暴露公网端口;代码层面零改动,仅需两端安装 Tailscale 并登录同一账号
- **BLE 玩具集成**:Web Bluetooth API 连接 SOSEXY BLE 设备(服务 0xEE01,写入 0xEE03),sendData2 封包协议(前缀 00 + 18字节分包 + 随机包头 + 终止包);`whisper_mode` 参数按需注入 `[TOY:x]` 能力到 prompt,后端 `TOY_CMD_PATTERN` 正则检测+strip+广播+`_toy_sys_msg` 系统消息
- **背景记忆浮现**:`build_surfacing_memories(topic, keywords)` 三层策略构建最多 8 条背景记忆:① unresolved 优先(最多 2 条)→ ② 用即时哨兵的 topic 做 embedding 匹配(Top 3,阈值 0.50)→ ③ 最近 3 天的记忆补充。注入时 unresolved 带 📌 前缀,与后续 RAG 召回自动去重
- **记忆阈值**:cosine ≥ 0.75 才召回,top_k=3,去重阈值 0.85
- **静音保活**:`startSilentKeepAlive()` 创建 AudioContext + OscillatorNode(gain=0.001),30 秒循环,防止手机浏览器后台杀 JS 线程导致 WebSocket 断连和闹铃失效
- **Web Notification**:`sendSystemNotification()` 封装 Notification API,闹铃弹窗和监控提醒时同时发送系统推送,需用户授权 `Notification.requestPermission()`
- **AudioBridge 架构**:`AudioBridge.java` 使用 `AudioRecord(VOICE_RECOGNITION, 16000, MONO, PCM_16BIT)`,录音线程每 40ms 读取 1280 字节(640 samples),base64 编码后通过 `evaluateJavascript` 注入 JS;JS 端 `remoteVoice._onNativeChunk()` 解码 → 存入环形 buffer → 能量 VAD 判断语音段 → 静音截断 → 拼接 WAV 头 → POST 到 `/api/voice/remote-asr`
- **远程 ASR 端点**:`routes/voice.py` 的 `/api/voice/remote-asr` 接收 multipart WAV 文件,用 httpx 转发到硅基流动 `https://api.siliconflow.cn/v1/audio/transcriptions`(model=FunAudioLLM/SenseVoiceSmall),返回 `{text}` JSON
- **手机端语音协调**:`remoteVoice` 对象维护 `aiSpeaking` 状态,通过 `notifyVoiceAiSpeaking()` 和 `notifyVoiceCamCheckStart()` 统一分发给 PC 端 `/api/voice/ai-speaking` 或手机端 `remoteVoice._onAiSpeaking()`,TTS 播放完毕(`tts_done` 事件触发)后自动恢复录音
- **音乐点歌架构**:`music.py` 封装 pyncm(`_ensure_session` 线程安全匿名登录),`routes/music.py` 提供 REST API 并导出 `MUSIC_CMD_PATTERN` 正则;`routes/chat.py` 在 send_message 和 regenerate 流结束后检测 `[MUSIC:xxx]`,搜索并通过 SSE `music` 事件 + WebSocket 广播发送卡片数据
- **能力提示合并**:[MUSIC:xxx] 和 [CAM_CHECK] 合并为单个 `[系统能力]` user+assistant 对注入,减少 token 消耗(从 4 条消息降为 2 条)
- **音乐前端渲染**:`msgMusicCards` 字典按消息 ID 存储卡片数据,`renderMusicCards()` / `buildMusicCardHtml()` 生成卡片 DOM,`playMusicOnline()` 创建固定底部播放器,`closeMusicPlayer()` 停止并移除
- **日程/闹铃架构**:`schedule.py` 的 `ScheduleManager` 在独立线程运行(30 秒间隔),通过 `run_coroutine_threadsafe` 桥接主事件循环执行 DB 操作和 WebSocket 广播;`_fire_alarm` 复用 camera.py 相同的 Core 唤醒模式(世界书前缀+记忆+历史+触发提示);`_parse_dt` 支持 6 种日期时间格式,仅日期时默认 09:00
- **日程系统消息**:`_sys_msg()` 辅助函数在日程创建/删除时插入 system 角色消息到当前对话,风格与哨兵唤醒消息(📷)一致,使用 📅/🗑️ 图标前缀
- **AionPushService 架构**:前台服务使用 OkHttp 4.12.0 维持独立 WebSocket 连接,与 WebView 内的 JS WebSocket 并行但互不干扰。通知通过 `NotificationManager` 发送,渠道 ID 区分优先级。心跳线程是纯 Java `Thread`(非 HandlerThread),`Thread.sleep()` 不依赖 Android Looper 消息队列,锁屏后仍能正常唤醒
- **推送与前端 WebSocket 的关系**:Service 的 WebSocket 仅用于接收消息并弹通知,不做任何 UI 操作。WebView 内的 JS WebSocket 负责完整的 UI 交互。两条连接同时连到服务端 `/ws`,`ConnectionManager.active` 列表中会有两个客户端
- **高德定位架构**:`location.py` 独立模块,`process_heartbeat(lng, lat, accuracy, is_gcj02, skip_sentinel)` 为核心入口。`skip_sentinel` 参数用于测试脚本避免触发哨兵通知。所有高德 API 调用使用 httpx 异步请求,Key 从 `data/location_config.json` 读取
- **WGS84→GCJ-02 坐标转换**:`wgs84_to_gcj02()` 实现完整的国测局加密偏移算法(含 Krasovsky 椭球参数),中国境内坐标最大偏移约 500-700 米。Android 端不做转换,统一由服务端处理
- **三级心跳研判**:`process_heartbeat` 维护 `last_api_lng/lat` 跟踪上次 API 调用的坐标,通过 Haversine 距离判断是否显著移动(≥`movement_threshold` 500m)。轻量级处理零 API 消耗,刷新级消耗 2 次 API(逆地理+天气),完整级额外消耗 1 次 AI 调用(哨兵通知)
- **状态机防误触**:家坐标为 (0,0) 或未设置时保持 `unknown` 状态不做研判;每次心跳先算距离再判状态,状态切换必须经过完整级处理
- **哨兵通知**:`_notify_sentinel()` 调用 `gemini-3.1-flash-lite-preview`,注入世界书人设 + `chat_status.json` 聊天状态 + 记忆召回 + 详细位置上下文(距离/地址/天气),生成自然语言通知消息
- **POI 按需搜索**:`perform_poi_check()` 模式同 `perform_cam_check()`:异步执行,使用最新缓存坐标重新逆地理编码 + POI 搜索 → 构建 system 消息 → 调用 Core 生成跟进回复 → 插入对话 + WebSocket 广播
- **Android 定位线程**:`AionPushService` 中 `startLocationThread()` 启动独立 Java Thread(非 HandlerThread),`Thread.sleep(10min)` 循环,每次先 GET `/api/location/config` 检查 `active` 字段,`active = enabled && !is_location_quiet_hours()`,false 时完全跳过 GPS 采集
- **定位 UI**:chat.html 设置面板中「📍 定位追踪」为可折叠区块(默认收起),监控日志弹窗底部增加「📍 缓存定位」调试行(显示坐标/状态/地址/精度/更新时间)
- **POI 搜索指示器**:前端 `poiSearchMsgId` + `poiSearchCategories` 全局变量跟踪,`handlePoiSearch()` 创建蓝色弹跳动画指示器(样式同 cam-check 绿色),45 秒安全超时自动消失,新 assistant 消息到达时自动移除
- **前台服务类型扩展**:`AndroidManifest.xml` 中 `foregroundServiceType="dataSync|location"`,`startForeground()` 传入 `FOREGROUND_SERVICE_TYPE_DATA_SYNC | FOREGROUND_SERVICE_TYPE_LOCATION`,同时声明 `ACCESS_FINE_LOCATION` + `ACCESS_COARSE_LOCATION` + `ACCESS_BACKGROUND_LOCATION` 权限
- **服务端广播兼容**:`ws.py` 的 `broadcast()` 使用 `try/except` 逐连接发送,单个连接异常不影响其他连接。新增 `except Exception` 兜底确保 RST/EOF 等异常也能清理死连接

## 踩坑记录 & 经验教训(Android 推送服务)

> 以下是开发 AionPushService 过程中遇到的所有坑和最终解决方案,务必参考避免重复踩坑。

### 坑 1:权限弹窗被 finish() 秒杀
**现象**:安装后不弹通知权限请求和电池优化引导。
**原因**:`requestPermissions()` 和电池优化 Intent 放在 `LauncherActivity` 中,但 `launchWebView()` 执行完 `startActivity(WebViewActivity)` 后紧接着 `finish()`,Activity 销毁了弹窗来不及显示。
**解决**:权限请求和电池优化引导移到 `WebViewActivity.onCreate()` 中,该 Activity 会一直存活。
**教训**:**不要在即将 finish() 的 Activity 中请求权限或弹系统对话框。**

### 坑 2:Android 14 (targetSdk 34) startForeground 崩溃
**现象**:Service 启动后立即崩溃(`MissingForegroundServiceTypeException`)。
**原因**:targetSdk 34 要求 `AndroidManifest.xml` 声明 `android:foregroundServiceType`,且 `startForeground()` 调用时必须传入 serviceType 参数。
**解决**:Manifest 中 `<service>` 标签添加 `android:foregroundServiceType="dataSync"`,`startForeground(id, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)`。
**教训**:**targetSdk ≥ 34 的前台服务,Manifest 声明和 startForeground 调用都要带 serviceType。**

### 坑 3:WebSocket close() 触发 onClosed 导致重连死循环
**现象**:App 打开后通知栏疯狂弹"连接成功",一秒一次。
**原因**:`connectWebSocket()` 中先 `oldWs.close()` 关闭旧连接 → 旧连接的 `onClosed` 回调触发 `scheduleReconnect()` → 又调 `connectWebSocket()` → 无限循环。
**解决**:
  ① 引入 `wsGeneration` 计数器,每次新建连接 generation++,旧连接的回调检查 generation 不匹配则直接 return
  ② 关闭旧连接改用 `cancel()` 而非 `close()`,`cancel()` 不会触发 `onClosed` 回调
  ③ `onStartCommand` 中检查 `wsConnected` + URL 是否变化,已连接则跳过
**教训**:**OkHttp WebSocket 的 `close()` 会触发 `onClosed` 回调,如果回调里有重连逻辑则必须做状态保护。用 generation 计数器是最可靠的方式。**

### 坑 4:HandlerThread Looper 被国产 ROM 冻结
**现象**:App 在前台一切正常,但锁屏/切后台后 WebSocket 静默断开,收不到任何消息。常驻通知栏始终显示"在线",没有任何错误。
**原因**:`HandlerThread` 使用 Android 的 `Looper` 消息队列派发消息,vivo/OPPO/华为等国产 ROM 会在锁屏后冻结非活跃 App 的 Looper 消息分发(即使是前台服务的 HandlerThread 也不例外)。`handler.postDelayed()` 的心跳回调被冻结,永远不执行,WebSocket 断了但无人知晓。
**解决**:用纯 Java `Thread` + `Thread.sleep()` 替代 `HandlerThread` + `Handler.postDelayed()`。`Thread.sleep()` 是 OS 级线程休眠,不经过 Android Looper,国产 ROM 不会冻结前台服务的普通 Java Thread。
**教训**:**在国产 Android ROM 上,前台服务中需要可靠定时执行的逻辑,不要用 Handler/Looper/AlarmManager,直接用 Java Thread + Thread.sleep() 最可靠。**

### 坑 5:onFailure 阻塞 OkHttp 回调线程
**现象**:WebSocket 断线后一直不重连。
**原因**:`onFailure` 回调中直接调 `Thread.sleep()` 做退避等待,阻塞了 OkHttp 的回调线程,导致后续所有 WebSocket 事件无法分发。
**解决**:`onFailure` 中仅设置状态标志(`wsConnected = false`),不做任何阻塞操作。重连逻辑交给独立的心跳线程统一管理。
**教训**:**OkHttp WebSocket 的 onOpen/onMessage/onClosing/onClosed/onFailure 回调都在内部线程执行,绝对不要在回调里做阻塞操作(sleep/网络请求/锁等待)。**

### 坑 6:WakeLock + WiFi Lock 缺失导致锁屏断连
**现象**:锁屏几分钟后 WebSocket 断开(即使心跳线程正常运行)。
**原因**:Android 锁屏后 CPU 进入深度睡眠(Doze),WiFi 芯片也休眠。OkHttp 的网络 I/O 线程虽然在 sleep 中,但 socket 读写操作被阻塞。
**解决**:Service `onCreate()` 中获取 `PowerManager.PARTIAL_WAKE_LOCK`(保持 CPU)+ `WifiManager.WifiLock(WIFI_MODE_FULL_LOW_LATENCY)`(保持 WiFi),`onDestroy()` 中释放。
**教训**:**前台服务 + WebSocket 长连接,WakeLock 和 WiFi Lock 是必须的,缺一不可。FULL_LOW_LATENCY 比 FULL_HIGH_PERF 更省电。**

### 坑 7:覆盖安装不弹新权限请求
**现象**:更新 APK 后覆盖安装,新增的 `POST_NOTIFICATIONS` 权限不弹请求。
**原因**:Android 覆盖安装时不会重新触发 runtime 权限请求,之前没有授权的权限依旧没有。
**解决**:先卸载旧 app 再安装新 APK,或者在代码中 `onCreate()` 动态检查 `checkSelfPermission()` + `requestPermissions()`。
**教训**:**开发阶段每次改权限后,一定先卸载再装。发布后靠代码动态检查。**

### 坑 8:vivo 电池策略默认杀后台
**现象**:所有代码逻辑正确,但 vivo 手机上锁屏后仍然收不到消息。
**原因**:vivo OriginOS 默认开启"智能后台管理",会冻结或杀掉后台 app 的进程(包括前台服务)。
**解决**:手机设置 → 电池 → 后台耗电管理 → 找到 Aion Oloth → 改为"不限制后台"(关闭智能管理)。
**教训**:**国产 ROM(vivo/OPPO/小米/华为)的电池优化会无视前台服务权限直接冻结进程。必须在 app 内引导用户关闭电池优化(`REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` + 手动设置)。这是所有国产安卓推送的终极大坑。**

### 坑 9:服务端 except WebSocketDisconnect 不够
**现象**:手机 WebSocket 被杀后,服务端 `ConnectionManager.active` 列表中残留死连接,广播消息发到死连接上失败但不清理。
**原因**:网络异常断开(RST 包)抛 `RuntimeError` 或 `ConnectionResetError`,不是 `WebSocketDisconnect`。
**解决**:`except Exception` 兜底 + `finally: manager.disconnect(ws)` 确保任何原因断开都清理。
**教训**:**WebSocket 端点的异常处理不要只 catch 特定异常,用 `except Exception` + `finally` 确保连接清理。**

### 坑 10:CLI 管线图片不能用 base64 内嵌
**现象**:用户在 Aion 私聊(Gemini CLI)和 Connor 私聊/群聊(Codex CLI)发送图片时报错。Gemini CLI: `Separator is not found, and chunk exceed the limit`;Codex CLI: `Input exceeds the maximum length of 1048576 characters`。
**原因**:CLI 工具通过 stdin 管道接收 prompt,有长度限制(Codex CLI 明确 1MB 上限)。一张普通照片 base64 编码后几百 KB 到几 MB,直接内嵌必然超限。而 Gemini 原生 API 通过 HTTP POST JSON body 传输,几乎无大小限制,所以同样的 base64 方式在 API 调用中没问题。
**解决**:CLI 管线改为传本地文件绝对路径,让 CLI 自行读取文件。API 管线(硅基流动/Gemini 原生)保持 base64 不变。
**教训**:**不同 AI 调用方式对输入大小的限制不同。CLI stdin 有长度上限,不能简单复用 API 的 base64 方案。CLI 工具原生支持读取本地文件路径,应该利用这个能力。**

### 坑 11:`fetch_merged_timeline` 的 room_id 参数语义混淆
**现象**:Connor 私聊窗口合并时间线查询不到任何群聊消息,只能看到自己的私聊历史。
**原因**:`fetch_merged_timeline(who, limit, room_id=room_id)` 中的 `room_id` 参数用于指定**群聊房间**ID,但 `build_connor_1v1_prompt` 错误地传入了 1v1 私聊的 room_id。函数内部用这个 room_id 去 `chatroom_rooms` 表查 `type='group'` 的房间,自然查不到。
**解决**:Connor 1v1 调用时不传 `room_id`,让函数自动查找最新的群聊房间。
**教训**:**函数参数语义不明确时容易误传。`room_id` 这个参数名无法区分是私聊房间还是群聊房间,应该在文档/注释中明确标注参数用途。**

### 坑 12:Connor 群聊管线手动转纯文本丢失附件
**现象**:Connor 群聊回复时看不到用户发送的图片,即使 `_build_cli_prompt` 已经支持附件处理。
**原因**:`_reply_connor` 在调用 `stream_connor_cli` 之前,先把 `connor_history`(含 attachments)手动转为纯文本字符串(遍历 content 拼接),然后 `stream_connor_cli(prompt_text)` 再包装成 `[{"role": "user", "content": text}]`。附件信息在手动转文本这一步就已丢失,`_build_cli_prompt` 的附件处理逻辑永远触发不到。
**解决**:`stream_connor_cli` 新增 `messages` 参数,`_reply_connor` 直接传 `connor_history`(保留 attachments),跳过手动转文本步骤。
**教训**:**修复底层函数(`_build_cli_prompt`)时,必须检查整条调用链是否有中间层把信息提前丢弃了。"数据在到达修复点之前就已经丢失"是很隐蔽的 bug。**

### 最终技术方案总结
| 组件 | 技术选型 | 关键参数 |
|------|---------|---------|
| WebSocket 客户端 | OkHttp 4.12.0 | connectTimeout=15s, pingInterval=30s |
| 心跳机制 | Java Thread + Thread.sleep | 间隔 45s, 健康超时 120s |
| 保活 | PARTIAL_WAKE_LOCK + WIFI_MODE_FULL_LOW_LATENCY | onCreate 获取, onDestroy 释放 |
| 重连策略 | 指数退避 3s→30s + NetworkCallback 即时重连 | onAvailable 触发 |
| 防重连风暴 | wsGeneration 计数器 + cancel() | 旧回调自动失效 |
| 自复活 | onTaskRemoved + AlarmManager.setExactAndAllowWhileIdle | 3 秒后重启 |
| 前台服务类型 | dataSync &#124; location | targetSdk 34 必须声明,location 用于 GPS 采集 |
| 通知 | 3 渠道分级 | keepalive(LOW) / messages(DEFAULT) / alarm(HIGH) |

## 启动方式
```bash
# 方式一:双击启动脚本
双击 一键启动.bat

# 方式二:命令行
cd aion-chat
python main.py

服务监听 0.0.0.0:8080

访问地址

  • PC:http://localhost:8080
  • 手机:http://192.xxx.x.xx:8080(同一 WiFi 下,用 ipconfig 查看 WLAN IP)

踩坑记录 & 最终方案

手机端麦克风(getUserMedia vs 原生录音)

尝试 问题 结论
WebView + HTTP + getUserMedia getUserMedia 在 Android WebView 中要求安全上下文(HTTPS),HTTP 下直接报 NotAllowedError ❌ 不可行
自签名 HTTPS 证书 WebView 加载 HTTPS 页面后,页面内的 fetch() 和 WebSocket 不信任自签名证书,JS 请求全部失败导致白屏 ❌ 不可行
Android 原生 AudioBridge 使用 AudioRecord API 直接录音(不经过浏览器),通过 @JavascriptInterface 将 base64 PCM 回调到 JS,完全绕过 HTTPS 限制 ✅ 最终方案

远程 ASR 消息发送失败

问题 原因 修复
ASR 识别成功但消息未发送到聊天 _sendToChat 中 $('msgInput') 引用了不存在的元素(实际 id 是 input),且调用了不存在的 sendMessage() 函数(实际是 send()) 改为 $('input').value = text; send();

Gradle 构建

问题 原因 修复
Gradle 8.2 + Java 21 编译失败 Gradle 8.2 不支持 Java 21 升级 Gradle 8.5 + AGP 8.2.2

Android 全面屏适配

问题 原因 修复
状态栏被 WebView 内容遮挡 使用了全屏沉浸模式 SYSTEM_UI_FLAG_FULLSCREEN 移除沉浸模式,改为设置状态栏颜色匹配主题
长按返回键无效(Vivo X300 Pro 手势导航) onKeyLongPress 不适用于手势导航的侧滑返回 改为 onBackPressed 弹出 AlertDialog

更新日志

2026-05-10 — CLI 图片管线修复 + Connor 跨窗口上下文 + 多场景群聊集成

背景:

  1. Connor 私聊窗口看不到群聊消息(build_connor_1v1_prompt 传了 1v1 的 room_id 给 fetch_merged_timeline 的 room_id 参数,但该参数是用于指定群聊房间的,导致 0 条群聊消息被合并)
  2. 闹铃/监控/哨兵/cam_check 等触发场景只能看到私聊历史,无法感知群聊上下文
  3. 通过 Gemini CLI 和 Codex CLI 发送图片全部报错(Gemini CLI: Separator is not found, and chunk exceed the limit;Codex CLI: Input exceeds the maximum length of 1048576 characters)

改动内容:

  1. ai_providers.py — _build_cli_prompt 图片/音频本地路径传递

    • 旧方式:完全忽略 messages 中的 attachments 字段
    • 中间尝试:将图片转 base64 内嵌到 prompt 文本 → 失败(base64 编码后轻松超过 CLI stdin 的长度限制)
    • 最终方案:解析附件为本地绝对路径,直接写入 prompt 文本,由 CLI 自行读取文件
    • 支持 image/* 和 audio/* 两类附件,结构化附件(voice/video dict)跳过(已有 transcript 文本兜底)
  2. chatroom.py — stream_connor_cli 支持 messages 列表

    • 新增 messages 参数,可直接传入完整消息列表(保留附件),不再强制转为纯文本
    • 传入 messages 时自动注入 Connor persona 作为 system 消息
  3. chatroom.py — build_connor_1v1_prompt → build_connor_1v1_context

    • 从返回纯文本 prompt 改为返回 messages 列表,timeline 消息保留 attachments 字段
    • 修复 room_id 参数误传问题(不再传 1v1 room_id 给 fetch_merged_timeline)
  4. routes/chatroom.py — Connor 群聊/私聊管线改造

    • _reply_connor(群聊):不再手动将 history 转为纯文本(丢失附件),直接传 connor_history 给 stream_connor_cli(messages=...)
    • _generate_connor_reply(私聊):改用 build_connor_1v1_context + stream_connor_cli(messages=...)
  5. schedule.py — 闹铃/监控触发集成群聊上下文

    • _fire_alarm、_fire_monitor:使用 fetch_merged_timeline 替代原来只查私聊的逻辑,懒导入避免循环依赖
  6. camera.py — 哨兵/cam_check 集成群聊上下文

    • _call_core(哨兵 Core 唤醒)、perform_cam_check(主动查看监控):同样使用 fetch_merged_timeline,懒导入避免循环依赖

不影响的线路:硅基流动(build_multimodal_messages,base64 内嵌 API JSON)和 Gemini 原生 API(build_gemini_contents,base64 内嵌 inline_data)的图片处理方式不变

踩坑记录:见下方坑 10、坑 11、坑 12

2026-05-09 — 统一时间线上下文 + 统一记忆总结

背景:之前 Aion 私聊只能看到私聊历史,群聊只能看到群聊历史,两个 AI 的记忆总结也各自独立(Connor 私聊和群聊分别总结,群聊记忆还要同步一份到 Aion 主库)。改为统一时间线,让每个 AI 都能同时看到私聊和群聊内容,记忆总结也合并处理。

改动内容:

  1. 新增 context_builder.py — 统一上下文构建模块:

    • fetch_merged_timeline(who, limit, *, conv_id, room_id):同时查询 messages 和 chatroom_messages 两张表,按 created_at 合并排序,返回统一时间线
    • render_merged_timeline(merged, who):将合并时间线转为 AI 历史格式,私聊/群聊混合时自动插入场景切换标记 [以下为群聊记录] / [以下为私聊记录],消息前缀带 [群聊] / [私聊] 标签
    • build_ability_block()、build_memory_blocks()、strip_tool_commands() 等工具函数从各处抽取统一
  2. routes/chat.py — Aion 私聊上下文统一:

    • send_message、edit_resend、regenerate 三个函数的历史构建改为 fetch_merged_timeline("aion") + render_merged_timeline(),Aion 在私聊中也能看到群聊内容
  3. chatroom.py — 群聊上下文 + Connor 记忆统一:

    • build_aion_group_context() / build_connor_group_context():改用统一时间线,移除旧的跨窗口上下文注入
    • digest_chatroom():合并 Connor 1v1 + 群聊消息统一总结,使用 connor_unified 锚点,scope 固定为 "connor",删除"群聊记忆同步写入 Aion 主库"逻辑(两个 AI 各管各的记忆)
    • _connor_1v1_auto_digest_loop():不再查找特定房间,直接调用 digest_chatroom() 统一总结
    • connor_1v1_on_message():群聊消息也触发计时器重置
  4. memory.py — Aion 记忆总结统一:

    • _do_digest():在私聊消息基础上追加查询群聊 chatroom_messages,标记 _source(private/group),混合来源时消息格式带 [群聊] / [私聊] 标签
  5. main.py — 自动总结空闲检测增强:

    • _auto_digest_loop():同时检查 messages 和 chatroom_messages 两张表的最后用户消息时间,避免群聊活跃时误触发私聊自动总结
  6. routes/chatroom.py — 触发器扩展:

    • _save_msg():群聊消息也触发 connor_1v1_on_message() 重置自动总结计时器
  7. Bug 修复:

    • 流式输出气泡残留原始指令:aion_done / connor_done 事件用服务端清洗后的内容替换 streamingText
    • 闹铃/日程创建时缺少系统消息:在 process_schedule_commands 之前预检测指令并插入系统提示
    • 音乐点歌后不自动播放:添加 autoplay: True 参数

2026-05-08 — Gemini CLI 本地调用接入

背景:Gemini CLI(@google/gemini-cli)支持通过 Google OAuth 免费调用 Gemini 模型,无需 API Key。将其作为第四种 AI 调用方式集成到项目中。

改动内容:

  1. ai_providers.py:
    • 新增 _find_gemini_script():自动定位全局安装的 gemini CLI 脚本(npm root -g 方式 + gemini.cmd 位置推导)
    • 新增 _build_cli_prompt(messages):将 messages 列表拼成 [System Instruction] / [User] / [Assistant] 格式的完整 prompt
    • 新增 call_gemini_cli() 异步生成器:通过 asyncio.create_subprocess_exec 启动 CLI 子进程,stdin 传入 prompt(绕过 Windows 命令行 8K 长度限制),流式读取 stdout 并 yield
    • stream_ai() 新增 gemini_cli provider 路由分支
  2. config.py:MODELS 字典新增 CLI-2.5pro、CLI-3.1pro、CLI-2.5flash 三个模型
  3. 新增 cli线部署教程.md:面向朋友的 CLI 线路部署指南

使用方式:聊天界面右上角切换模型到 CLI-xxx,其余(人设、记忆、指令解析、TTS)全部照常工作。不需要额外启动任何服务。

部署前置:npm install -g @google/gemini-cli + 首次运行 gemini 完成 OAuth 认证

2026-04-08 — UI 多页面拆分重构

背景:原 chat.html 单文件近 4000 行,所有功能(设置/世界书/记忆库/日程/摄像头/监控日志/定位)以模态弹窗形式耦合在聊天页内,维护和扩展困难。

改动内容:

  1. 新建 7 个独立功能页面:settings.html、worldbook.html、memory.html、schedule.html、camera.html、monitor-logs.html、location.html,每个页面独立完整(HTML+CSS+JS)
  2. 新建共享层:common.css(CSS 变量/子页面布局/组件样式/闹铃弹窗/toast)+ common.js(api() 封装/WebSocket 连接/闹铃弹窗/系统通知)
  3. chat.html 瘦身:删除了 7 个模态弹窗的 HTML + 对应 JS 函数(摄像头控制/监控日志/WebSocket override/记忆库管理/日程管理/设置/世界书/定位),保留与聊天深度耦合的功能(语音唤醒/TTS/BLE密语/音乐/系统日志/[CAM_CHECK])
  4. 侧边栏简化:移除 6 个功能导航按钮,仅保留「系统日志」「密语时刻」「返回主页」
  5. main.py 新增路由:/settings、/worldbook、/memory、/schedule、/camera、/monitor-logs、/location
  6. home.html 更新:APPS 注册表新增 camera/logs/location 入口,memory/worldbook/alarm/settings 绑定对应 URL
  7. 文件管理器优化:标题栏加关闭按钮,文件列表区域可滚动

保留在 chat.html 的功能:语音唤醒通话、TTS 语音合成、密语时刻(BLE)、音乐点歌、[CAM_CHECK] 主动查看监控、系统日志(session 级)、文件管理器

子页面共享机制:每个子页面通过 <link href="https://github.com/static/common.css"> + <script src="https://github.com/static/common.js"> 引入共享层,调用 connectCommonWS() 建立独立 WebSocket 连接(用于接收闹铃弹窗),各页面自行管理 API 调用和渲染逻辑

2026-04-08 — 后台消息保障 + 子页面 iframe 浮层(防切页丢消息/TTS 中断)

背景:多页面拆分后,从 chat.html 导航到设置/主页/监控日志等页面会销毁聊天页,导致:① 正在等待的 AI 回复丢失(SSE 流中断,后端 generate() 生成器被关闭,DB 保存和 WS 广播永远不执行);② TTS 语音播放立即停止(Audio 元素和队列被销毁)。手机上尤其明显,发消息后切到其他页面查看就会丢回复。

改动内容:

  1. 后端:AI 生成解耦为后台任务(routes/chat.py — send_message + regenerate_message)

    • 原架构:generate() 异步生成器内 AI 流式输出 → 后处理(指令检测、音乐搜索、日程解析)→ 存 DB → WS 广播,全在 yield 链路中,客户端断开则全部丢失
    • 新架构:拆为 _bg_generate() 后台任务 + generate() SSE 转发层
      • _bg_generate():asyncio.create_task() 启动,AI 流式输出 + 全部后处理 + 存 DB + WS 广播,通过 asyncio.Queue 向 SSE 层推送事件,try/finally 确保始终运行到结束
      • generate():仅从 Queue 读取并 yield,纯薄层转发。客户端断开时生成器正常关闭,后台任务不受影响
    • 效果:即使客户端断开连接(切页/关闭/网络中断),AI 回复依然会完成生成、存入数据库、通过 WebSocket 广播到所有在线客户端
  2. 前端:子页面 iframe 浮层(static/chat.html)

    • 新增全屏 #subPageOverlay:包含顶部关闭栏 + <iframe> 容器
    • 侧栏「⚙ 设置」「🏠 返回主页」「⬅ 返回」全部改为 openSubPage(url) → 在浮层中打开目标页,chat.html 始终存活
    • closeSubPage():关闭浮层 + 重新加载消息列表(补上浮层期间后台生成的新消息)
    • 浏览器返回键 (popstate) 自动关闭浮层
    • 效果:SSE 流式接收、TTS 播放、WS 连接在浮层打开期间全部不中断
  3. home.html iframe 适配

    • 当 home.html 在 iframe 中加载时,点击「聊天」→ window.parent.closeSubPage() 关闭浮层回到 chat.html
    • 点击「密语时刻」→ 关闭浮层 + 调用 window.parent.openWhisper()

涉及文件:routes/chat.py(后端核心)、static/chat.html(前端浮层 + 导航改造)、static/home.html(iframe 适配)

动态壁纸(独立显示器全屏壁纸 + AI 气泡)

  1. 全屏壁纸轮播 — 独立页面 /wallpaper,用于副屏全屏展示。支持图片和视频轮播,可配置轮换间隔(秒),视频播放完毕后自动切换下一个。全屏铺满(object-fit: cover),无黑边
  2. 淡入淡出过渡 — 双缓冲架构(两个媒体层交替),切换时新层在旧层上方 1.5 秒淡入,旧层在过渡完成后移除,确保无黑屏闪烁
  3. 资源就绪检测 — 图片通过轮询 img.complete + naturalWidth、视频通过轮询 readyState >= 2 确认资源就绪后再激活淡入,避免黑屏。保底超时(图片 2 秒 / 视频 3 秒)确保不会永远卡住
  4. 键盘操控 — ← / → 方向键手动切换上/下一张壁纸,F 键切换全屏,ESC 退出全屏或锚点编辑模式。600ms 冷却保护防止快速按键导致黑屏
  5. AI 聊天气泡 — 通过 WebSocket 接收 msg_created 事件,筛选 role=assistant 的消息,以半透明毛玻璃气泡显示在画面上。自动清洗 [MUSIC:...]、[ALARM:...]、[REMINDER:...]、[Monitor:...]、[TOY:...] 等指令标记和 Markdown 格式符号。气泡按 \n\n 自动拆分为多条,淡入显示,可配置自动消失时长(默认 12 秒)
  6. 气泡锚点系统 — 每张壁纸可独立设置气泡显示位置(百分比坐标),适应不同图片的人物位置。设置面板点击「编辑当前锚点」进入编辑模式,点击画面设定锚点位置,ESC 或右键退出
  7. 设置面板 — 鼠标移到屏幕底部边缘或双击打开设置面板,可配置:轮换间隔、气泡显示时长、启用/禁用单个壁纸文件、上传新壁纸、编辑气泡锚点。点击缩略图切换启用/禁用,右键缩略图跳转到该壁纸
  8. Chrome App 模式启动 — 启动壁纸.bat 使用 chrome.exe --app=... --start-fullscreen 启动,无地址栏/标签栏/边框,等同于独立桌面程序。关闭壁纸窗口不影响服务器和聊天
  9. 配置持久化 — 壁纸配置(轮换间隔、文件启用状态、气泡锚点坐标)存储在 data/wallpaper_config.json
  10. 完全独立 — 壁纸页面独立运行,与聊天系统仅通过 WebSocket 单向接收消息,开关壁纸不影响任何其他功能

动态壁纸工作流程

【启动壁纸】
  双击 启动壁纸.bat → Chrome --app 模式全屏打开 /wallpaper
  → 加载 wallpaper_config.json + 扫描 public/wallpaper/ 目录
  → 构建播放列表(排除 enabled=false 的文件)
  → 显示第一张壁纸 → 启动轮换定时器 → 连接 WebSocket

【壁纸轮播】
  定时器到期(图片)/ 视频播放结束 → 切换下一个:
    ├ 新层加载资源 → 轮询就绪状态(img.complete / video.readyState ≥ 2)
    ├ 就绪后激活淡入(opacity 0→1,1.5 秒 CSS transition)
    ├ 旧层 1.6 秒后隐藏并清理 DOM
    └ 更新气泡锚点位置

【AI 气泡显示】
  WebSocket 收到 {type: "msg_created", data: {role: "assistant", content: "..."}}
  → cleanAIText() 清洗指令标记和 Markdown 格式
  → 按 \n\n 拆分为多条 → 逐条创建气泡 DOM(最多 5 条)
  → 气泡淡入显示 → N 秒后自动淡出消失

【锚点编辑】
  设置面板 → 编辑当前锚点 → 进入编辑模式(crosshair 光标)
  → 点击画面任意位置 → 保存百分比坐标到 config.files[name].bubble_anchor
  → 实时更新气泡容器位置 → ESC/右键退出编辑模式

注意事项

  • 搬迁目录后需修改 一键启动.bat 中的路径(第11行 cd /d 后面的绝对路径)
  • 所有数据路径都是相对路径,搬迁不影响
  • VPN (singbox) 可能干扰局域网访问,必要时关闭或加直连规则
  • 防火墙已添加 8080 端口入站规则(规则名 "Aion Chat 8080")
  • 备份只需复制 data/ 文件夹
Bản sao tài liệu lưu trữ từ kho lưu trữ GitHub chính thức · Xem trên GitHub
Ngày đồng bộ: 2026-11-14