luckytri 1.2.4 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.en.md +44 -1
  2. package/README.md +44 -1
  3. package/docs/en/CHANGELOG.md +10 -0
  4. package/docs/zh/CHANGELOG.md +10 -0
  5. package/package.json +1 -1
  6. package/public/app/assets/ChatsPage-C5-w410L.js +3 -0
  7. package/public/app/assets/{Empty-y1vXDIjQ.js → Empty-DSurzLFZ.js} +1 -1
  8. package/public/app/assets/Field-GuXyA_aS.js +1 -0
  9. package/public/app/assets/HeartPage-BGmIrxCz.js +1 -0
  10. package/public/app/assets/LifePage-Dzln-0Y3.js +1 -0
  11. package/public/app/assets/MemoryPage-QsthmMZ9.js +1 -0
  12. package/public/app/assets/NaturePage-CIvHjgJa.js +3 -0
  13. package/public/app/assets/PeoplePage-bIoX24pj.js +1 -0
  14. package/public/app/assets/PluginsPage-EuJhCovw.js +2 -0
  15. package/public/app/assets/SystemPage-7t42B-Q3.js +1 -0
  16. package/public/app/assets/{SystemPage-DNTD2_nj.css → SystemPage-BLMlD1M6.css} +1 -1
  17. package/public/app/assets/TimePage-CY-SwBq3.js +3 -0
  18. package/public/app/assets/index-Cn6TppEV.js +4 -0
  19. package/public/app/assets/{index-cDDC-1dI.css → index-JRbQoSJr.css} +1 -1
  20. package/public/app/assets/{knowledge-BjD9PkXe.js → knowledge-sUjcjbfI.js} +1 -1
  21. package/public/app/assets/media-Dh7c41PQ.js +1 -0
  22. package/public/app/assets/{mind-KkinyNC6.js → mind-BMcoPQNd.js} +1 -1
  23. package/public/app/index.html +4 -4
  24. package/public/guide.en.html +3 -3
  25. package/public/guide.html +3 -3
  26. package/server/actions/index.js +268 -0
  27. package/server/app.js +3 -1
  28. package/server/channels/contract.js +3 -0
  29. package/server/channels/hub.js +15 -0
  30. package/server/channels/onebot/adapter.js +162 -21
  31. package/server/channels/onebot/enrich.js +132 -0
  32. package/server/channels/onebot/index.js +53 -3
  33. package/server/channels/onebot/music.js +43 -0
  34. package/server/core/conversation-grounding.js +1 -1
  35. package/server/core/dialogue-context.js +165 -15
  36. package/server/core/dialogue-contract.js +32 -33
  37. package/server/core/model-manager.js +35 -20
  38. package/server/core/model-presets.js +154 -1
  39. package/server/core/orchestrator.js +18 -0
  40. package/server/core/persona-manager.js +20 -2
  41. package/server/core/reply-delivery.js +328 -48
  42. package/server/core/response-generator.js +1 -1
  43. package/server/core/response-validator.js +85 -17
  44. package/server/core/turn-processor.js +63 -16
  45. package/server/core/turn.js +59 -1
  46. package/server/i18n/errors.js +25 -2
  47. package/server/index.js +30 -0
  48. package/server/mind/continuity.js +2 -4
  49. package/server/mind/discretion.js +16 -5
  50. package/server/mind/memory.js +7 -2
  51. package/server/mind/recall/index.js +2 -0
  52. package/server/plugins/zip-file.js +180 -0
  53. package/server/stickers/api.js +86 -19
  54. package/server/stickers/library.js +8 -2
  55. package/server/stickers/offer.js +4 -1
  56. package/server/stickers/zip-import.js +62 -2
  57. package/server/studio/auto-update.js +224 -0
  58. package/server/studio/updates.js +154 -144
  59. package/server/voice/index.js +255 -0
  60. package/server/voice/sauc.js +152 -0
  61. package/server/web/link-reader.js +379 -0
  62. package/public/app/assets/ChatsPage-C_gKz4mJ.js +0 -3
  63. package/public/app/assets/Field-CJG5lERx.js +0 -1
  64. package/public/app/assets/HeartPage-5y3avjSy.js +0 -1
  65. package/public/app/assets/LifePage-BtwRMDAM.js +0 -1
  66. package/public/app/assets/MemoryPage-54noDhWz.js +0 -1
  67. package/public/app/assets/NaturePage-DHK5X2Qw.js +0 -3
  68. package/public/app/assets/PeoplePage-ZsA7gyxP.js +0 -1
  69. package/public/app/assets/PluginsPage-BCRRIAhq.js +0 -2
  70. package/public/app/assets/SystemPage-DWr1TG49.js +0 -1
  71. package/public/app/assets/TimePage-6MUmtSOT.js +0 -3
  72. package/public/app/assets/index-BokqwNXF.js +0 -4
  73. package/public/app/assets/media-Ciozo_yb.js +0 -1
package/README.en.md CHANGED
@@ -94,7 +94,7 @@ The full design is in [Her life](docs/en/her-life.md) and [Architecture and exte
94
94
 
95
95
  Recent work focuses on continuity and clearer conversation. Account-based recognition links public encounters across groups and private chats. Relationships, forms of address, her own views and private promises retain their sources and visibility boundaries. An ordinary greeting need not bring back unfinished tasks or demonstrate how much she remembers.
96
96
 
97
- **1.2.4 update:** Add batch ZIP import to the sticker library, with folder categories, duplicate detection and an import summary. This release also includes the contextual sticker use, real group @ mentions and configurable image-understanding limits from 1.2.3. See the [changelog](docs/en/CHANGELOG.md).
97
+ **1.3.0 update:** Add optional voice input and output, actions she can choose in group chat, public link reading and automatic updates that are off by default. The sticker library also handles large ZIP64 archives. See the [changelog](docs/en/CHANGELOG.md).
98
98
 
99
99
  | Area | What it does today |
100
100
  | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -110,6 +110,48 @@ Recent work focuses on continuity and clearer conversation. Account-based recogn
110
110
  | Data safety | Automatic backups with an integrity and SHA-256 manifest; a snapshot before migrations; verify and restore into a new file |
111
111
  | Plugins | Channels, senses, activities, actions, reading and pages. Each plugin is a restricted process, and its permissions are accepted when it is enabled. See [Plugins](docs/en/plugins.md) |
112
112
 
113
+ ### More than typing
114
+
115
+ All of these are her own choice and only appear when the current connection can actually do them; what she did is written into her history.
116
+
117
+ | Ability | What it does |
118
+ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
119
+ | @ and quoted reply | @ someone in a group (at most two); quote a message when the topic has scrolled away or it must be clear which line is answered — chosen by her (the old "quote when buried" rule stays as a fallback) |
120
+ | Poke | Poke or poke back; being poked counts as being called, pokes between others do not disturb her. At most once per person per 10 minutes |
121
+ | Reactions | Put one of ten reactions on a message, with or without words |
122
+ | Dice / rock-paper-scissors | Throw a die or play rps; incoming ones are understood too |
123
+ | Recall | Only their own last message, within two minutes, when it was a mistake; at most twice an hour |
124
+ | Voice (optional) | See "Voice" below; whether a line is spoken is her call |
125
+ | Typing | In a private chat the other side sees "typing…" while she thinks (where the client supports it); not in groups |
126
+ | Forwarded chats | Incoming forwarded chats are unfolded and read (up to 40 lines); she can also forward a few messages from here (up to 20, twice an hour) |
127
+ | Files | When something is long or a file is asked for, she can send what she wrote as a .md file (twice an hour) |
128
+ | Music | A NetEase Cloud Music card when she wants to recommend a song or someone asks; a title and link when the client has no cards (some originals are not on NetEase for licensing reasons) |
129
+
130
+ Poke and reactions rely on extension APIs of the OneBot implementation; when it refuses them, those actions are simply no longer offered.
131
+
132
+ ### Sticker library
133
+
134
+ - Per-sticker limit **4 MB** by default (`STICKER_MAX_MB`, up to 20).
135
+ - **ZIP import** is streamed to disk and unpacked one entry at a time (memory holds one sticker); zip64, folder names as categories, duplicates, oversized and non-image entries skipped, upload and import progress; archive limit 1 GB by default (`STICKER_ZIP_MAX_MB`). A real 315 MB / 110-image archive imported in one go. Temporary files are removed on success, failure or next start.
136
+ - Each sticker is **analysed once** (look, meaning, tags; a cheap vision model can be chosen) and then picked by context, mood and how it went with that person before; reactions become preferences, nothing is sent by count.
137
+ - Behind a reverse proxy, raise the body size (e.g. nginx `client_max_body_size 1100m`) and timeouts.
138
+
139
+ ### Images and links
140
+
141
+ - "System → Images & stickers" sets the **accepted image size limit** (1–64 MB, default 20) and the size sent for understanding; images over the limit or unreadable are answered from words and tone, without announcing "I can't see it" again and again.
142
+ - **Link understanding**: public web links in messages are read (title, description, text excerpt; README for GitHub repos) with an 8 s timeout, 2 MB cap, 6 h cache and private/local addresses blocked. A page that did not open is said to be unopened.
143
+
144
+ ### Voice
145
+
146
+ - Two protocols: Volcengine speech v3 (Agent Plan preset `doubao-seed-tts-2.0`) and OpenAI-compatible `/audio/speech`. Set URL, key, speaker and "how the voice sounds" in "System → Voice", with a preview button.
147
+ - **Off by default**, and it stays off without a working speech service. On only means "may": whether a line is spoken, and whether she sings when asked, follows her mood and wish, never forced. At most three voice messages per conversation per hour, 60 characters each; if synthesis fails the line is typed.
148
+ - **Hearing voice (ASR)**: under System → Voice → Hearing voice, choose Volcengine speech recognition (Agent Plan, `volc.seedasr.sauc.duration`), Volcengine flash file recognition, or OpenAI-compatible `/audio/transcriptions`; an empty recognition key falls back to the speech key. Off by default; when on, incoming voice is transcribed before she reads it, and shows as "[voice]" if that fails. Needs ffmpeg on the host.
149
+
150
+ ### Settings and automatic updates
151
+
152
+ - Dropdowns and switches across settings use one set of components, consistent in light and dark themes.
153
+ - **Automatic updates** ("System → Version & updates"): off by default; daily at a set time or every 6/12/24/72 hours, optional allowed window; "notify only" or "update safely". An automatic install waits until she is not chatting and no backup, cleanup or migration runs, then uses the same verified flow as the manual update (checked backup → migrate → restart → health check → automatic rollback), never retries a version that failed, and keeps the last 20 entries. Source installs only notify.
154
+
113
155
  ## Quick start
114
156
 
115
157
  Requires Node.js 24.5+ and npm on Windows, macOS or Linux. Real replies also need a model service and API key of your own.
@@ -205,6 +247,7 @@ To continue an existing source instance, stop it first, then use `luckytri --hom
205
247
  | `LLM_API_KEY` | Optional; when set, takes precedence over the model key saved in the studio |
206
248
  | `EMBEDDING_API_KEY` | Optional; a separate key for the knowledge embedding service |
207
249
  | `BACKUP_INTERVAL_HOURS` / `BACKUP_KEEP` | Automatic backup interval (24 hours by default, `0` turns it off) and how many to keep (7 by default) |
250
+ | `STICKER_MAX_MB` / `STICKER_ZIP_MAX_MB` | Per-sticker limit (default 4) and ZIP limit (default 1024), in MB |
208
251
 
209
252
  The database, chat records, model keys and backups live locally in the ignored `data/` directory and are excluded from the repository and npm package. When you use a remote model, the context needed for a response is sent to your configured provider. While running, the server makes one integrity-checked backup a day; backups share a disk with the database, so copy important ones elsewhere. Never commit `.env`, the database or logs. See [Security](.github/SECURITY.md) and [Backup and recovery](docs/en/recovery.md).
210
253
 
package/README.md CHANGED
@@ -94,7 +94,7 @@ flowchart LR
94
94
 
95
95
  近期更新集中在让相处更连贯、说话更清楚。她按账号认出同一个人,在不同群与私聊间衔接公开经历;关系、称呼、自己的看法和私人约定分别保留来源与可见范围。普通问候不需要带出旧任务,每次开口也不需要证明自己记住了多少事。
96
96
 
97
- **1.2.4 更新:** 表情库新增 ZIP 批量导入,支持文件夹分类、重复项识别和导入结果汇总;同时保留 1.2.3 的表情选择、群聊 @ 和图片理解功能。完整变化见 [版本记录](docs/zh/CHANGELOG.md)。
97
+ **1.3.0 更新:** 新增可选语音输入与输出、她自己选择的群聊互动动作、公开网页链接理解和默认关闭的自动更新;贴纸库支持大型 ZIP64 压缩包。完整变化见 [版本记录](docs/zh/CHANGELOG.md)。
98
98
 
99
99
  | 方面 | 现在能做什么 |
100
100
  | ------------ | --------------------------------------------------------------------------------------------------------- |
@@ -110,6 +110,48 @@ flowchart LR
110
110
  | 数据保障 | 自动备份附带完整性与 SHA-256 清单;迁移前留快照;可验证并恢复到新文件 |
111
111
  | 插件 | 通道、感官、活动、行动、资料和页面。每个插件一个受限进程,权限在启用时同意。见 [插件](docs/zh/plugins.md) |
112
112
 
113
+ ### 她能做的不止打字
114
+
115
+ 这些都由她自己决定,只在当前连接真的能做时才出现;做过的事会写进她的经历,下次记得。
116
+
117
+ | 能力 | 说明 |
118
+ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
119
+ | @ 与引用回复 | 群里要叫住某个人时 @ 他(最多两人);话题被刷下去或要明确接哪句时,她自己选择引用那条消息(旧的“被刷下去就自动引用”规则保留为兜底) |
120
+ | 戳一戳 | 可以戳人、回戳;别人戳她算叫她一声,别人之间互戳不打扰她。同一人 10 分钟最多一次 |
121
+ | 表情回应 | 给某条消息点个表情(赞、爱心、笑哭等十种),可以不说话 |
122
+ | 骰子 / 猜拳 | 想玩时扔骰子、出猜拳;收到的骰子、猜拳她也看得懂 |
123
+ | 撤回 | 只撤自己两分钟内、发错了的上一条,每小时最多两次 |
124
+ | 语音(可选) | 见下方「语音」;这句要不要说出来由她定 |
125
+ | 正在输入 | 私聊里她思考时对方能看到“正在输入”(接入端支持时),群里不显示 |
126
+ | 合并转发 | 收到的合并转发会被展开读懂(最多 40 行);她也可以把这里的几条消息打包转发出去(最多 20 条,每小时两次) |
127
+ | 发文件 | 篇幅长或对方要文件时,她可以把自己写的东西作为 .md 文件发出(每小时两次) |
128
+ | 分享音乐 | 想推荐、或有人要歌时发网易云音乐卡片;接入端不支持卡片时改发歌名和链接(部分歌曲因版权在网易云搜不到原唱) |
129
+
130
+ 戳一戳和表情回应依赖接入端的扩展接口;接入端不支持时她会自动不再使用,不影响说话。
131
+
132
+ ### 表情库
133
+
134
+ - 单张上限默认 **4 MB**(`STICKER_MAX_MB`,最高 20)。
135
+ - **ZIP 批量导入**:上传时先流式落盘再逐张解压,内存只占一张;支持 zip64、文件夹名作分类、重复识别、过大与非图片跳过、上传与导入进度;压缩包上限默认 1 GB(`STICKER_ZIP_MAX_MB`)。实测 315 MB / 110 张一次导入成功。临时文件在完成、失败或下次启动时清理。
136
+ - 每张表情只做 **一次语义识别**(画面、含义、标签,可指定便宜的视觉模型),之后按语境、心情和“和这个人用它的经历”挑选;对方的反应会成为她的偏好,不按次数随机发。
137
+ - 前面有反向代理时,记得放宽上传大小(如 nginx `client_max_body_size 1100m`)和超时。
138
+
139
+ ### 图片与链接
140
+
141
+ - 「系统 → 图片与表情」里可设 **可接收的图片大小上限**(1–64 MB,默认 20)以及送去理解时的尺寸/大小;超过上限或读取失败的图,她按文字和语气接话,不反复宣布“看不到”。
142
+ - **链接理解**:消息里的公开网页链接会被读一下(标题、简介、正文摘要;GitHub 仓库读简介与 README),有 8 秒超时、2 MB 上限、6 小时缓存,并拦截内网与本机地址。打不开的她会如实说没打开。
143
+
144
+ ### 语音
145
+
146
+ - 模型服务支持「火山语音 v3(Agent Plan 预设:`doubao-seed-tts-2.0`)」与「OpenAI 兼容 `/audio/speech`」两种协议,在「系统 → 语音」填写地址、Key、音色和“声音的样子”,可以试听。
147
+ - **默认关闭**;没有可用的语音服务时开关保持关闭。打开只表示“可以”:每句话要不要用语音、有人请她唱歌时唱不唱,都是她当时的心情和意愿,不会被强制。语音每会话每小时最多三条、60 字以内,合成失败就照常打字。
148
+ - **听懂语音(ASR)**:「系统 → 语音 → 听语音」可配置火山语音识别(Agent Plan,`volc.seedasr.sauc.duration`)、火山录音文件极速版或 OpenAI 兼容 `/audio/transcriptions`,识别 Key 留空时沿用语音合成的 Key。默认关闭;打开后别人发来的语音先转成文字再给她看,失败时显示为「[语音]」。需要本机有 ffmpeg。
149
+
150
+ ### 设置与自动更新
151
+
152
+ - 设置页的下拉、开关统一为同一套组件,明暗主题一致。
153
+ - **自动更新**(「系统 → 版本与更新」):默认关闭;可选每天固定时间或每隔 6/12/24/72 小时检查,可设允许时段;“只提醒”或“自动安全更新”。自动安装会等她不在聊天、没有备份/整理/迁移在进行时才开始,沿用手动更新的完整流程(校验备份 → 迁移 → 重启 → 健康检查 → 失败自动恢复原版本),失败过的版本不再自动重试,最近 20 条记录可在页面查看。源码安装只提醒。
154
+
113
155
  ## 快速开始
114
156
 
115
157
  需要 Node.js 24.5+ 与 npm,支持 Windows、macOS、Linux。真实回复还需要你自己的模型服务与 API Key。
@@ -205,6 +247,7 @@ npm start
205
247
  | `LLM_API_KEY` | 可选;设置后优先于界面里保存的模型密钥 |
206
248
  | `EMBEDDING_API_KEY` | 可选;独立的知识向量服务密钥 |
207
249
  | `BACKUP_INTERVAL_HOURS` / `BACKUP_KEEP` | 自动备份间隔(默认 24 小时,`0` 关闭)与保留份数(默认 7) |
250
+ | `STICKER_MAX_MB` / `STICKER_ZIP_MAX_MB` | 表情单张上限(默认 4)与 ZIP 上限(默认 1024),单位 MB |
208
251
 
209
252
  数据库、聊天记录、模型密钥和备份保存在本机被忽略的 `data/` 目录,不随源码或 npm 包发布。使用远程模型时,回复所需的上下文会发送给你配置的模型服务。服务运行时每天自动做一份经过完整性检查的备份;备份和数据库在同一块磁盘上,重要的请自己复制到别处。不要提交 `.env`、数据库或日志。详见 [安全说明](.github/SECURITY.md) 与 [数据备份与恢复](docs/zh/recovery.md)。
210
253
 
@@ -2,6 +2,16 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## 1.3.0 — 2026-10-14
6
+
7
+ - Add optional speech output and speech recognition through Volcengine and OpenAI-compatible services. Voice stays off by default; she chooses line by line whether to speak or sing. Incoming voice can be transcribed, with text fallback when recognition fails.
8
+ - Add actions she can choose: quote a reply, poke, react, roll a die, play rock-paper-scissors, recall her own last message, share music, forward messages from the current conversation, and send a Markdown work. The channel, target and rate limit are checked before each action.
9
+ - Read public web pages and GitHub repositories linked in chat. Requests have time, size and redirect limits; every redirect is checked for private addresses, cookies are not sent, and results are cached.
10
+ - Add automatic updates, off by default. Schedule checks or notifications; automatic installation waits for a quiet conversation and uses the verified backup, migration, restart and health-check flow, restoring the old version if it fails.
11
+ - Stream sticker ZIP imports, including ZIP64 and large archives, while keeping nested folder categories, progress and result summaries. Temporary files are cleaned up and oversized or stalled requests are stopped.
12
+ - Improve group understanding, correction and review: avoid empty privacy or image-failure announcements, keep private-chat experience out of unrelated groups, and stop turning a moment of missing someone into a status report.
13
+ - Expand Claude and Gemini model presets and provider controls; improve private-chat typing status, forwarded-message understanding and settings interactions.
14
+
5
15
  ## 1.2.4 — 2026-10-13
6
16
 
7
17
  - Import sticker ZIP archives in one batch, including nested folders and folder-based categories. Duplicates, hidden files, non-images and oversized images are skipped with a result summary. New stickers use the existing one-time analysis queue; archives are processed in memory.
@@ -2,6 +2,16 @@
2
2
 
3
3
  # 版本记录
4
4
 
5
+ ## 1.3.0 — 2026-10-14
6
+
7
+ - 新增可选语音输出和语音识别,支持火山语音与 OpenAI 兼容接口。语音默认关闭;她逐句决定是否朗读或唱歌,收到的语音可转成文字,识别失败时继续按文本回退。
8
+ - 新增由她选择的互动动作:引用回复、戳一戳、表情回应、骰子、猜拳、撤回自己的上一条、分享音乐、合并转发当前会话消息,以及发送 Markdown 作品;执行前检查渠道能力、目标和频率限制。
9
+ - 新增公开网页和 GitHub 仓库链接理解,读取正文或 README;限制请求时间、体积和跳转次数,每一跳都检查内网地址,不携带 Cookie,并缓存结果。
10
+ - 新增默认关闭的自动更新。可以定时检查或只提醒;自动安装等待会话空闲,再执行备份、校验、迁移、重启和健康检查,失败时恢复旧版本。
11
+ - 贴纸 ZIP 导入改为流式处理,支持 ZIP64 和大型压缩包,并保留嵌套目录分类、进度反馈和结果汇总;异常或超限时清理临时文件并中止请求。
12
+ - 改进群聊理解、纠错和复审:减少空泛的保密或图片故障宣告,避免把私聊经历带进无关群;想念时不再自动汇报生活进度,回复优先贴近眼前的话题。
13
+ - 扩展 Claude 与 Gemini 模型接入和厂商参数,并改进私聊输入状态、转发消息理解与设置页交互。
14
+
5
15
  ## 1.2.4 — 2026-10-13
6
16
 
7
17
  - 表情库支持一次导入 ZIP 压缩包,可读取多层文件夹、按文件夹归类,跳过重复项、隐藏文件、非图片和超大图片,并汇总导入结果。新表情沿用原有的一次性识别队列,压缩包在内存中处理。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "luckytri",
3
- "version": "1.2.4",
3
+ "version": "1.3.0",
4
4
  "description": "让由技术诞生的她,在时间、记忆、相遇与选择中逐渐形成自己的方向",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,3 @@
1
+ import{$ as e,Ct as t,E as n,Et as r,H as i,I as a,J as o,M as s,P as c,Tt as l,U as u,V as d,W as f,X as p,Y as m,Z as h,a as g,ct as _,dt as v,et as y,f as b,gt as x,ht as S,it as C,j as w,lt as T,m as E,n as D,nt as O,o as k,q as A,rt as j,s as M,t as N,ut as P,v as F,vt as I,wt as L,xt as R,yt as z,z as B}from"./Empty-DSurzLFZ.js";import{g as ee,y as V}from"./mind-BMcoPQNd.js";import{c as H,f as U,h as W,m as G,n as K,o as te,p as q,u as J,y as ne}from"./index-Cn6TppEV.js";import{t as re}from"./media-Dh7c41PQ.js";import{t as Y}from"./Field-GuXyA_aS.js";async function ie(e,t){let n=new URLSearchParams({session:e});return t!==void 0&&n.set(`after`,String(t)),E(`/core/events?`+n.toString())}async function ae(e,t={}){let n=new URLSearchParams({session:e});return t.limit&&n.set(`limit`,String(t.limit)),t.compact&&n.set(`compact`,`1`),E(`/core/traces?`+n.toString())}async function X(e){return E(`/core/traces/`+e)}async function oe(e){return E(`/simulate`,`POST`,e)}async function se(e,t){return E(`/decisions/`+e+`/feedback`,`POST`,{tag:t})}async function Z(e){return E(`/core/replay`,`POST`,e)}async function ce(e){return E(`/core/sessions`,`POST`,e)}async function le(e,t){return E(`/core/sessions/`+encodeURIComponent(e),`PATCH`,{enabled:t})}async function ue(e,t){return E(`/core/sessions/`+encodeURIComponent(e)+`/archive`,`PATCH`,{archived:t})}async function de(e){return E(`/core/sessions/`+encodeURIComponent(e),`DELETE`,{})}async function fe(e,t){return E(`/core/sessions/`+encodeURIComponent(e),`PUT`,t)}async function pe(e){return E(`/core/sessions/`+encodeURIComponent(e)+`/summaries`)}async function me(e){return E(`/core/sessions/`+encodeURIComponent(e)+`/context`,`DELETE`,{})}var he={class:`session-list`},ge={class:`list-head`},Q=[`placeholder`,`aria-label`],_e={class:`rows scroll-pane`},ve=[`data-pick`,`aria-current`,`onClick`],ye={class:`who`},be=[`aria-label`],xe=[`title`],Se=[`checked`,`aria-label`,`onChange`],Ce={key:0,class:`muted empty-line`},we={key:1,class:`archive-list`},Te={class:`row`},Ee=[`data-restore-session`,`onClick`],De=[`data-delete-session`,`onClick`],Oe=D(O({__name:`SessionList`,props:{sessions:{},selected:{},unread:{}},emits:[`select`,`add`,`toggle`,`restore`,`remove`],setup(n,{emit:i}){let s=n,c=i,u=R(``),m=A(()=>s.sessions.filter(e=>!e.archived&&`${e.name} ${e.id}`.toLowerCase().includes(u.value.toLowerCase()))),g=A(()=>s.sessions.filter(e=>e.archived));return(i,s)=>(P(),h(`div`,he,[o(`div`,ge,[o(`h2`,null,[e(r(t(a)(`会话`))+` `,1),o(`small`,null,r(m.value.length),1)]),o(`button`,{class:`small primary`,onClick:s[0]||=e=>c(`add`)},r(t(a)(`+ 添加会话`)),1)]),z(o(`input`,{"onUpdate:modelValue":s[1]||=e=>u.value=e,type:`search`,placeholder:t(a)(`搜索名称或号码`),"aria-label":t(a)(`搜索会话`)},null,8,Q),[[d,u.value]]),o(`div`,_e,[(P(!0),h(f,null,v(m.value,e=>(P(),h(`div`,{key:e.id,class:L([`session-row`,{selected:e.id===n.selected,paused:!e.enabled}])},[o(`button`,{class:`row-main`,"data-pick":e.id,"aria-current":e.id===n.selected?`true`:void 0,onClick:t=>c(`select`,e.id)},[o(`span`,{class:`badge`,style:l({"--hue":t(U)(e.id)})},r(e.kind===`private`?t(a)(`私`):t(a)(`群`)),5),o(`span`,ye,[o(`b`,null,r(t(W)(e)),1),o(`small`,null,r(e.enabled?t(a)(`参与中`):t(a)(`已暂停`)),1)]),n.unread[e.id]?(P(),h(`span`,{key:0,class:`unread`,"aria-label":t(a)(`{v} 条没细看`,{v:n.unread[e.id]})},r(n.unread[e.id]),9,be)):p(``,!0)],8,ve),o(`label`,{class:`mini-switch`,title:e.enabled?t(a)(`暂停参与`):t(a)(`开启参与`)},[o(`input`,{type:`checkbox`,checked:e.enabled,"aria-label":t(a)(`参与:{v}`,{v:t(W)(e)}),onChange:t=>c(`toggle`,e)},null,40,Se)],8,xe)],2))),128)),m.value.length?p(``,!0):(P(),h(`p`,Ce,r(t(a)(`暂无匹配的会话。`)),1)),g.value.length?(P(),h(`details`,we,[o(`summary`,null,r(t(a)(`已归档 · {length}`,{length:g.value.length})),1),(P(!0),h(f,null,v(g.value,e=>(P(),h(`article`,{key:e.id},[o(`b`,null,r(t(W)(e)),1),o(`div`,Te,[o(`button`,{class:`small`,"data-restore-session":e.id,onClick:t=>c(`restore`,e.id)},r(t(a)(`恢复`)),9,Ee),o(`button`,{class:`small danger`,"data-delete-session":e.id,onClick:t=>c(`remove`,e.id)},r(t(a)(`永久删除`)),9,De)])]))),128))])):p(``,!0)])]))}}),[[`__scopeId`,`data-v-9b8dcbac`]]),ke={class:`muted`},Ae={key:0,class:`notice`,role:`status`},je=[`pattern`,`inputmode`],Me=[`disabled`],Ne=O({__name:`AddSession`,props:{open:{type:Boolean},channel:{},ready:{type:Boolean}},emits:[`close`,`add`],setup(e,{emit:n}){let i=e,s=R(`group`),c=n,l=A(()=>i.channel===`qqbot`),d=A(()=>l.value?`[0-9A-Za-z_\\-]{8,64}`:`\\d{4,20}`);function f(e){let t=e.target;c(`add`,{...Object.fromEntries(new FormData(t)),channel:i.channel})}return(n,i)=>(P(),m(te,{open:e.open,title:t(a)(`添加群聊 / 私聊`),eyebrow:t(a)(`NEW PLACE`),width:`440px`,onClose:i[1]||=e=>c(`close`)},{default:I(()=>[o(`form`,{id:`addSession`,class:`stack`,onSubmit:u(f,[`prevent`])},[o(`p`,ke,r(t(a)(`添加之后再决定要不要让 TA 参与;开不开口仍由 TA 自己决定。`)),1),l.value&&!e.ready?(P(),h(`p`,Ae,r(t(a)(`请先在「连接 QQ」里填写机器人的 AppID`)),1)):p(``,!0),y(Y,{label:t(a)(`类型`)},{default:I(()=>[y(H,{modelValue:s.value,"onUpdate:modelValue":i[0]||=e=>s.value=e,name:`kind`,"aria-label":t(a)(`类型`),options:[{value:`group`,label:t(a)(`群聊`)},{value:`private`,label:t(a)(`私聊`)}]},null,8,[`modelValue`,`aria-label`,`options`])]),_:1},8,[`label`]),y(Y,{label:l.value?t(a)(`群 openid / 用户 openid`):t(a)(`群号 / QQ 号`),hint:l.value?t(a)(`8–64 位字母、数字、下划线或短横线。官方平台的会话通常会在有人 @ 她或私聊她时自动出现,这里只用于提前登记。`):t(a)(`4–20 位数字`)},{default:I(()=>[o(`input`,{name:`id`,required:``,pattern:d.value,inputmode:l.value?`text`:`numeric`,autocomplete:`off`},null,8,je)]),_:1},8,[`label`,`hint`]),y(Y,{label:t(a)(`显示名称`)},{default:I(()=>[...i[2]||=[o(`input`,{name:`name`,required:``,maxlength:`100`,autocomplete:`off`},null,-1)]]),_:1},8,[`label`]),o(`button`,{class:`primary`,disabled:l.value&&!e.ready},r(t(a)(`添加会话`)),9,Me)],32)]),_:1},8,[`open`,`title`,`eyebrow`]))}}),Pe={class:`transcript`},Fe={class:`t-head`},Ie=[`aria-label`,`title`],Le={class:`t-title`},Re={id:`liveStatus`,class:`faint`},ze={class:`t-tools`},Be=[`disabled`],Ve={id:`liveMessages`,class:`chat-messages scroll-pane`},He={key:0,class:`day-label`},Ue=[`data-status`],We={key:0},Ge={class:`body`},Ke={class:`message-meta`},qe={key:0,class:`chip`,"data-tone":`warn`},Je={class:`text`},Ye=[`aria-expanded`,`onClick`],Xe={key:0,class:`why`},Ze={key:0,class:`muted`},Qe={key:0},$e={key:1},et={key:2,class:`fb`},tt={class:`composer-note`},nt={class:`chip`,"data-tone":`warn`},rt={class:`faint`},it={class:`composer-row`},at={class:`uid`},ot={class:`sr-only`},st=[`title`],ct=[`placeholder`,`onKeydown`],lt={class:`check mention`},ut=[`disabled`],dt={key:1,class:`composer off`},ft={class:`faint`},pt=D(O({__name:`Transcript`,props:{session:{},sessionId:{},events:{},traces:{},decisions:{},status:{},demo:{type:Boolean},compact:{type:Boolean},sending:{type:Boolean}},emits:[`simulate`,`feedback`,`openList`,`openReplay`],setup(n,{expose:g,emit:_}){let b=n,x=_,S=c({glanced:w(`TA 扫了一眼`),silent:w(`TA 看了,没出声`),deferred:w(`TA 睡着了,醒来再看`)}),T=R(`10001`),E=R(``),D=R(!0),O=R(),k=R({}),j=()=>V(s(),{month:`long`,day:`numeric`,weekday:`short`}),F=A(()=>{let e=b.events.map(e=>({type:`message`,key:`m`+e.seq,time:e.time,event:e})),t=b.events[0]?.time??2**53-1;for(let n of b.traces)S[n.status]&&n.time>=t&&e.push({type:`divider`,key:`t`+n.id,time:n.time,status:n.status,reasons:[n.reason].filter(Boolean),count:1});e.sort((e,t)=>e.time-t.time);let n=[],r=``;for(let t of e){let e=j().format(t.time);e!==r&&(r=e,n.push({type:`day`,key:`d`+t.time,time:t.time,label:e}));let i=n[n.length-1];if(t.type===`divider`&&i?.type===`divider`&&i.status===t.status){i.count+=1,t.reasons[0]&&!i.reasons.includes(t.reasons[0])&&i.reasons.push(t.reasons[0]);continue}n.push(t)}return n});function I(e){let t=String(e.payload.text||``);return b.decisions.filter(n=>n.session_id===e.session_id&&n.reply&&n.reply.includes(t)&&Math.abs(n.time-e.time)<6e5).sort((t,n)=>Math.abs(t.time-e.time)-Math.abs(n.time-e.time))[0]}function ee(e){return b.traces.filter(t=>t.status===`sent`&&t.time<=e.time+1e3).sort((e,t)=>t.time-e.time)[0]}async function W(e){if(k.value[e.seq]){let{[e.seq]:t,...n}=k.value;k.value=n;return}k.value={...k.value,[e.seq]:{loading:!0,detail:null}};let t=ee(e),n=null;try{n=t?await X(t.id):null}catch{n=null}k.value[e.seq]&&(k.value={...k.value,[e.seq]:{loading:!1,detail:n}})}async function G(){await C(),O.value?.focus()}function K(){E.value.trim()&&!b.sending&&(x(`simulate`,{userId:T.value,text:E.value,mentioned:D.value}),E.value=``)}return g({focusComposer:G}),(s,c)=>(P(),h(`section`,Pe,[o(`header`,Fe,[n.compact?(P(),h(`button`,{key:0,class:`icon-button`,"aria-label":t(a)(`全部会话`),title:t(a)(`全部会话`),onClick:c[0]||=e=>x(`openList`)},` ☰ `,8,Ie)):p(``,!0),n.session?(P(),h(`span`,{key:1,class:`badge`,style:l({"--hue":t(U)(n.session.id)})},r(n.session.kind===`private`?t(a)(`私`):t(a)(`群`)),5)):p(``,!0),o(`div`,Le,[o(`h2`,null,r(n.session?.name||t(a)(`选择一段对话`)),1),o(`span`,Re,r(n.status),1)]),o(`div`,ze,[o(`button`,{class:`small sim-button`,disabled:!n.sessionId,onClick:G},r(t(a)(`模拟消息`)),9,Be),o(`button`,{class:`small`,onClick:c[1]||=e=>x(`openReplay`)},r(t(a)(`回到那一刻`)),1)])]),o(`div`,Ve,[(P(!0),h(f,null,v(F.value,n=>(P(),h(f,{key:n.key},[n.type===`day`?(P(),h(`div`,He,[o(`span`,null,r(n.label),1)])):n.type===`divider`?(P(),h(`div`,{key:1,class:`divider`,"data-status":n.status},[o(`span`,null,r(t(S)[n.status])+r(n.count>1?` × ${n.count}`:``),1),n.reasons.length?(P(),h(`small`,We,r(n.reasons.slice(0,2).join(`;`)),1)):p(``,!0)],8,Ue)):(P(),h(`article`,{key:2,class:L([`message`,{bot:n.event.role===`assistant`,simulated:n.event.payload.simulated}])},[n.event.role===`assistant`?p(``,!0):(P(),h(`span`,{key:0,class:`avatar`,style:l({"--hue":t(U)(String(n.event.payload.userId||n.event.payload.name))}),"aria-hidden":`true`},r(t(q)(n.event.payload.name)),5)),o(`div`,Ge,[o(`div`,Ke,[o(`b`,null,r(n.event.payload.name),1),o(`time`,null,r(t(J)(n.event.time)),1),n.event.payload.simulated?(P(),h(`span`,qe,r(t(a)(`模拟`)),1)):p(``,!0)]),o(`p`,Je,r(n.event.payload.text),1),n.event.role===`assistant`?(P(),h(f,{key:0},[o(`button`,{class:`text-button why-toggle`,"aria-expanded":!!k.value[n.event.seq],onClick:e=>W(n.event)},r(k.value[n.event.seq]?t(a)(`收起`):t(a)(`为什么这么说`)),9,Ye),k.value[n.event.seq]?(P(),h(`div`,Xe,[k.value[n.event.seq].loading?(P(),h(`p`,Ze,r(t(a)(`正在翻当时的记录…`)),1)):(P(),h(f,{key:1},[k.value[n.event.seq].detail?.data?.decision?.appraisal?(P(),h(`p`,Qe,[o(`b`,null,r(t(a)(`TA 当时的理解:`)),1),e(r(k.value[n.event.seq].detail.data.decision.appraisal),1)])):p(``,!0),o(`p`,null,[o(`b`,null,r(t(a)(`理由:`)),1),e(r(k.value[n.event.seq].detail?.data?.reason||k.value[n.event.seq].detail?.data?.decision?.reason||I(n.event)?.reason||t(a)(`没有留下理由。`)),1)]),k.value[n.event.seq].detail?.data?.snapshot?.knowledge?.length?(P(),h(`p`,$e,[o(`b`,null,r(t(a)(`引用的资料:`)),1),e(r(k.value[n.event.seq].detail.data.snapshot.knowledge.map(e=>e.title).join(`、`)),1)])):p(``,!0),I(n.event)?(P(),h(`label`,et,[e(r(t(a)(`这句怎么样?`))+` `,1),y(H,{"model-value":I(n.event).feedback||``,"aria-label":t(a)(`这句怎么样?`),options:[{value:``,label:t(a)(`选择评价`)},...Object.entries(t(M).health.feedbackLabels||{}).map(([e,t])=>({value:e,label:String(t)}))],"onUpdate:modelValue":e=>x(`feedback`,I(n.event).id,e)},null,8,[`model-value`,`aria-label`,`options`,`onUpdate:modelValue`])])):p(``,!0)],64))])):p(``,!0)],64)):p(``,!0)])],2))],64))),128)),n.events.length?p(``,!0):(P(),m(N,{key:0,title:n.sessionId?t(a)(`等待新的消息`):t(a)(`选择一段对话`),text:t(a)(`群聊与私聊消息会在这里实时出现;TA 扫一眼、没出声也会留下一条分隔线。`)},null,8,[`title`,`text`]))]),n.demo?(P(),h(`form`,{key:0,id:`simulate`,class:`composer`,onSubmit:u(K,[`prevent`])},[o(`div`,tt,[o(`span`,nt,r(t(a)(`模拟消息`)),1),o(`span`,rt,r(t(a)(`作为一条模拟消息走完整流程,写进模拟会话;不会发到 QQ,也不进入真实记忆。`)),1)]),o(`div`,it,[o(`label`,at,[o(`span`,ot,r(t(a)(`体验用户 ID`)),1),z(o(`input`,{"onUpdate:modelValue":c[2]||=e=>T.value=e,name:`userId`,pattern:`\\d{4,20}`,required:``,title:t(a)(`体验用户 ID`)},null,8,st),[[d,T.value]])]),z(o(`textarea`,{ref_key:`composer`,ref:O,"onUpdate:modelValue":c[3]||=e=>E.value=e,name:`text`,rows:`1`,placeholder:t(a)(`以这位群友的身份说一句…`),required:``,onKeydown:i(u(K,[`exact`,`prevent`]),[`enter`])},null,40,ct),[[d,E.value]]),o(`label`,lt,[z(o(`input`,{"onUpdate:modelValue":c[4]||=e=>D.value=e,type:`checkbox`},null,512),[[B,D.value]]),e(r(t(a)(`视为 @`)),1)]),o(`button`,{class:`primary`,disabled:n.sending||!n.sessionId},r(n.sending?t(a)(`TA 在想…`):t(a)(`发送模拟消息`)),9,ut)])],32)):(P(),h(`div`,dt,[o(`span`,ft,r(t(a)(`真实模式下不能模拟消息。想看看 TA 会怎么回,可以「和 TA 聊聊」——那是试聊,什么都不写入。`)),1),o(`button`,{class:`small`,onClick:c[5]||=e=>t(M).chatOpen=!0},r(t(a)(`和 TA 聊聊`)),1)]))]))}}),[[`__scopeId`,`data-v-849aeb0f`]]),mt={class:`panel`},ht={key:0,class:`panel-body`},gt={key:1,class:`panel-body scroll-pane`},_t={class:`here`},vt={class:`facts`},yt={class:`eyebrow`},bt={key:1,class:`muted`},xt={key:2,class:`faint`},St={class:`eyebrow`},Ct={class:`faint`},wt={class:`row actions`},Tt=[`data-toggle`],Et=[`data-archive`],Dt=[`open`],Ot=[`data-session`],kt={class:`faint`},At=[`value`],jt={class:`faint`},Mt={class:`check`},Nt={class:`advanced-policy`},Pt={class:`stack tight`},Ft={class:`check`},It={class:`check`},Lt={class:`check`},Rt={class:`save-bar`},zt={class:`primary`,type:`submit`},Bt={class:`faint`},Vt={class:`fold summary-list`},Ht={key:0,class:`muted`},Ut={class:`faint`},Wt={key:0},Gt={key:2,class:`panel-body feedback-pane`},Kt={class:`faint`},qt={id:`feedbackList`,class:`scroll-pane`},Jt={class:`faint`},Yt={class:`reply-excerpt`},Xt={key:0,class:`muted`},Zt={class:`pagination`},Qt=[`disabled`],$t=[`disabled`],$=6,en=D(O({__name:`SessionPanel`,props:j({session:{},group:{},summaries:{},decisions:{}},{settingsOpen:{type:Boolean,default:!1},settingsOpenModifiers:{},tab:{default:`here`},tabModifiers:{}}),emits:j([`toggle`,`archive`,`clear`,`save`,`feedback`],[`update:settingsOpen`,`update:tab`]),setup(n,{emit:i}){let s=n,l=S(n,`settingsOpen`),m=S(n,`tab`),g=i,_=c([w(`近期细摘要`),w(`中期摘要`),w(`较早摘要`),w(`远期摘要`)]),b=R(0),C=A(()=>s.decisions.filter(e=>e.session_id===s.session?.id&&e.reply)),T=A(()=>Math.max(1,Math.ceil(C.value.length/$))),E=R(!1),D=R({});x(()=>s.session?.policy,e=>{D.value={...e||{}}},{immediate:!0,deep:!0}),x(()=>s.session?.id,()=>{b.value=0,E.value=!1});function O(e){let t=e?.face;return t?[t.role,t.tone,t.content].filter(Boolean).join(`
2
+
3
+ `):``}function k(e){let t=e.target,n=Object.fromEntries(new FormData(t));for(let e of[`aggregateMs`,`maxWaitMs`,`contextMessages`,`maxReply`])n[e]=Number(n[e]);for(let e of[`memory`,`selectiveVision`,`deepCheck`,`compaction`])n[e]=t.elements.namedItem(e).checked;g(`save`,n)}return(i,s)=>(P(),h(`section`,mt,[y(K,{modelValue:m.value,"onUpdate:modelValue":s[0]||=e=>m.value=e,label:t(a)(`这个会话`),items:[{key:`here`,label:t(a)(`这个会话`)},{key:`feedback`,label:t(a)(`反馈`)}]},null,8,[`modelValue`,`label`,`items`]),n.session?m.value===`here`?(P(),h(`div`,gt,[o(`div`,_t,[o(`div`,vt,[o(`div`,null,[o(`span`,yt,r(t(a)(`TA 在这里的样子`)),1),O(n.group)?(P(),h(f,{key:0},[o(`p`,{class:L([`face-body`,E.value?`open`:`clamp`])},r(O(n.group)),3),O(n.group).length>96?(P(),h(`button`,{key:0,type:`button`,class:`text-button`,onClick:s[1]||=e=>E.value=!E.value},r(E.value?t(a)(`收起`):t(a)(`展开完整内容`)),1)):p(``,!0)],64)):(P(),h(`p`,bt,r(t(a)(`还没有形成在这里的样子。`)),1)),n.group?.face?.aspiration?(P(),h(`small`,xt,r(t(a)(`想成为:{aspiration}`,{aspiration:n.group.face.aspiration})),1)):p(``,!0)]),o(`div`,null,[o(`span`,St,r(n.session.kind===`private`?t(a)(`这段私聊给 TA 的感觉`):t(a)(`这个群给 TA 的感觉`)),1),o(`p`,null,r(n.group?.bond?.feel||t(a)(`刚来这里不久`)),1)])]),o(`p`,Ct,r(t(a)(`在这里开不开口由 TA 自己决定:没有参与概率和冷却。TA 对这里每个人的感觉在「人际」里看。`)),1),o(`div`,wt,[o(`button`,{id:`toggleSession`,"data-toggle":n.session.id,class:L({primary:!n.session.enabled}),onClick:s[2]||=e=>g(`toggle`,n.session)},r(n.session.enabled?t(a)(`暂停参与`):t(a)(`开启参与`)),11,Tt),o(`button`,{"data-archive":n.session.id,onClick:s[3]||=e=>g(`archive`,n.session.id)},r(t(a)(`归档`)),9,Et),o(`button`,{id:`clearLiveContext`,onClick:s[4]||=e=>g(`clear`)},r(t(a)(`清空上下文`)),1)]),o(`details`,{class:`fold session-settings`,open:l.value,onToggle:s[19]||=e=>l.value=e.target.open},[o(`summary`,null,r(t(a)(`会话设置`)),1),(P(),h(`form`,{key:n.session.id,"data-session":n.session.id,class:`stack tight`,onSubmit:u(k,[`prevent`]),onInput:s[17]||=e=>t(M).dirty=!0,onChange:s[18]||=e=>t(M).dirty=!0},[o(`p`,kt,r(t(a)(`所有会话和独处都用模型库里的默认模型;其余已启用的模型按列表顺序做备用。她在这个聊天窗里用什么样子,由她自己从经历里决定。`)),1),o(`label`,null,[e(r(t(a)(`会话名称`))+` `,1),o(`input`,{name:`name`,maxlength:`100`,autocomplete:`off`,value:n.session.name},null,8,At),o(`small`,jt,r(t(a)(`官方机器人平台不提供群名称,可以在这里自己起一个`)),1)]),o(`label`,Mt,[z(o(`input`,{name:`selectiveVision`,type:`checkbox`,"onUpdate:modelValue":s[5]||=e=>D.value.selectiveVision=e,onInput:s[6]||=u(()=>{},[`stop`])},null,544),[[B,D.value.selectiveVision]]),e(r(t(a)(`节能看图:只在被 @ 或明确要求时,看这一条和附近的图`)),1)]),o(`details`,Nt,[o(`summary`,null,r(t(a)(`高级策略`)),1),o(`div`,Pt,[o(`label`,null,[e(r(t(a)(`聚合窗口 ms`)),1),z(o(`input`,{name:`aggregateMs`,type:`number`,"onUpdate:modelValue":s[7]||=e=>D.value.aggregateMs=e},null,512),[[d,D.value.aggregateMs,void 0,{number:!0}]])]),o(`label`,null,[e(r(t(a)(`最长等待 ms`)),1),z(o(`input`,{name:`maxWaitMs`,type:`number`,"onUpdate:modelValue":s[8]||=e=>D.value.maxWaitMs=e},null,512),[[d,D.value.maxWaitMs,void 0,{number:!0}]])]),o(`label`,null,[e(r(t(a)(`近期原文条数(10–500)`)),1),z(o(`input`,{name:`contextMessages`,type:`number`,min:`10`,max:`500`,"onUpdate:modelValue":s[9]||=e=>D.value.contextMessages=e},null,512),[[d,D.value.contextMessages,void 0,{number:!0}]])]),o(`label`,null,[e(r(t(a)(`一轮总字数`)),1),z(o(`input`,{name:`maxReply`,type:`number`,"onUpdate:modelValue":s[10]||=e=>D.value.maxReply=e},null,512),[[d,D.value.maxReply,void 0,{number:!0}]])]),o(`label`,Ft,[z(o(`input`,{name:`compaction`,type:`checkbox`,"onUpdate:modelValue":s[11]||=e=>D.value.compaction=e,onInput:s[12]||=u(()=>{},[`stop`])},null,544),[[B,D.value.compaction]]),e(r(t(a)(`上下文压缩:更早的聊天定期整理成分层摘要,越近越详细`)),1)]),o(`label`,It,[z(o(`input`,{name:`memory`,type:`checkbox`,"onUpdate:modelValue":s[13]||=e=>D.value.memory=e,onInput:s[14]||=u(()=>{},[`stop`])},null,544),[[B,D.value.memory]]),e(r(t(a)(`长期记忆`)),1)]),o(`label`,Lt,[z(o(`input`,{name:`deepCheck`,type:`checkbox`,"onUpdate:modelValue":s[15]||=e=>D.value.deepCheck=e,onInput:s[16]||=u(()=>{},[`stop`])},null,544),[[B,D.value.deepCheck]]),e(r(t(a)(`倾诉、纠正和危机时额外复审一次回复`)),1)])])]),o(`div`,Rt,[o(`button`,zt,r(t(a)(`保存会话设置`)),1),o(`small`,Bt,r(t(M).dirty?t(a)(`有未保存的修改`):t(a)(`保存后下一轮生效`)),1)])],40,Ot))],40,Dt),o(`details`,Vt,[o(`summary`,null,r(t(a)(`语境摘要 · {length} 段`,{length:n.summaries.length})),1),n.summaries.length?p(``,!0):(P(),h(`p`,Ht,r(t(a)(`还没有摘要。聊天超过近期原文条数后,更早的内容会自动整理到这里。`)),1)),(P(!0),h(f,null,v(n.summaries,e=>(P(),h(`article`,{key:e.id,class:`summary`},[o(`header`,null,[o(`b`,null,r(t(_)[e.level]||t(a)(`摘要`)),1),o(`small`,Ut,r(t(a)(`{period} · {messages} 条消息`,{period:e.period,messages:e.messages})),1)]),o(`p`,null,r(e.summary),1),e.keyPoints?.length?(P(),h(`ul`,Wt,[(P(!0),h(f,null,v(e.keyPoints,(e,n)=>(P(),h(`li`,{key:n},r(e.open?t(a)(`未完:`):``)+r(e.text),1))),128))])):p(``,!0)]))),128))])])])):(P(),h(`div`,Gt,[o(`p`,Kt,r(t(a)(`针对具体回复评价口吻;这是 TA 在这个会话里听到的话,不会改写天性。`)),1),o(`div`,qt,[(P(!0),h(f,null,v(C.value.slice(b.value*$,b.value*$+$),n=>(P(),h(`article`,{key:n.id,class:`feedback-item`},[o(`time`,Jt,r(t(J)(n.time)),1),o(`p`,Yt,r(n.reply),1),o(`label`,null,[e(r(t(a)(`这句回复怎么样?`))+` `,1),y(H,{"data-feedback":n.id,"model-value":n.feedback||``,"aria-label":t(a)(`这句回复怎么样?`),options:[{value:``,label:t(a)(`选择评价`)},...Object.entries(t(M).health.feedbackLabels||{}).map(([e,t])=>({value:e,label:String(t)}))],"onUpdate:modelValue":e=>g(`feedback`,n.id,e)},null,8,[`data-feedback`,`model-value`,`aria-label`,`options`,`onUpdate:modelValue`])])]))),128)),C.value.length?p(``,!0):(P(),h(`p`,Xt,r(t(a)(`还没有可以评价的回复。`)),1))]),o(`div`,Zt,[o(`button`,{class:`small`,disabled:b.value===0,onClick:s[20]||=e=>b.value--},`←`,8,Qt),o(`span`,null,r(b.value+1)+` / `+r(T.value),1),o(`button`,{class:`small`,disabled:b.value+1>=T.value,onClick:s[21]||=e=>b.value++},` → `,8,$t)])])):(P(),h(`div`,ht,[y(N,{title:t(a)(`还没有选中会话`),text:t(a)(`从左边选一个群聊或私聊。`)},null,8,[`title`,`text`])]))]))}}),[[`__scopeId`,`data-v-96fbad3d`]]),tn={class:`replay`},nn={class:`col source`},rn={class:`col-head`},an={id:`events`,class:`scroll-pane`},on={key:0},sn={key:1,class:`muted`},cn=[`disabled`],ln={class:`faint`},un={class:`col`},dn={class:`col-head`},fn={id:`traces`,class:`scroll-pane`},pn=[`data-trace`,`onClick`],mn=[`data-tone`],hn={key:0,class:`muted`},gn={class:`col`},_n={class:`col-head`},vn={class:`scroll-pane inspect`},yn={key:0,class:`metrics`},bn={id:`traceTokens`},xn={key:1,class:`notice`},Sn={id:`traceDetail`},Cn=D(O({__name:`ReplaySheet`,props:{open:{type:Boolean},sessionId:{},events:{}},emits:[`close`,`load`],setup(n,{emit:i}){let s=n,l=i,g=c({sent:w(`开口了`),silent:w(`没出声`),glanced:w(`扫了一眼`),deferred:w(`睡着了`),running:w(`处理中`),error:w(`失败`),complete:w(`完成`),stale:w(`旧稿作废`),cancelled:w(`已取消`),interrupted:w(`已中断`)}),_=c({live:w(`真实聊天`),demo:w(`模拟预览`),replay:w(`测试回放`),memory:w(`记忆整理`),summary:w(`语境压缩`)}),y=R([]),b=R(null),S=R(``),C=R(``),T=R(!1),E=R(``);async function D(){s.sessionId&&(y.value=await ae(s.sessionId))}function O(){let e=s.events.map(e=>Number(e.seq));e.length&&(S.value||=String(Math.min(...e)),C.value||=String(Math.max(...e)))}async function k(){if(!T.value){T.value=!0;try{F(a(`回放正在运行`));let e=await Z({session:s.sessionId,from:Number(S.value),to:Number(C.value)});E.value=JSON.stringify(e,null,2),F(a(`回放结束`)),await D()}catch(e){F(e.message,!0)}finally{T.value=!1}}}async function A(e){b.value=await X(e),E.value=JSON.stringify(b.value,null,2)}function j(e){return e?a(`输入 {v}(缓存读 {v2} / 写 {v3})· 输出 {v4}`,{v:G(e.input),v2:G(e.cachedRead),v3:G(e.cacheWrite),v4:G(e.output)}):`—`}return x(()=>[s.open,s.sessionId],()=>{s.open&&(S.value=``,C.value=``,b.value=null,O(),D())},{immediate:!0}),x(()=>s.events.length,O),(i,s)=>(P(),m(te,{open:n.open,title:t(a)(`回到那一刻`),eyebrow:`REPLAY`,width:`1180px`,onClose:s[3]||=e=>l(`close`)},{default:I(()=>[o(`div`,tn,[o(`section`,nn,[o(`div`,rn,[o(`h3`,null,r(t(a)(`历史消息`)),1),o(`button`,{id:`loadMessages`,class:`small`,onClick:s[0]||=e=>{l(`load`),D()}},r(t(a)(`↻ 加载`)),1)]),o(`div`,an,[n.events.length?(P(),h(`table`,on,[o(`thead`,null,[o(`tr`,null,[o(`th`,null,r(t(a)(`序号`)),1),o(`th`,null,r(t(a)(`发送者`)),1),o(`th`,null,r(t(a)(`消息`)),1)])]),o(`tbody`,null,[(P(!0),h(f,null,v([...n.events].reverse(),e=>(P(),h(`tr`,{key:e.seq},[o(`td`,null,r(e.seq),1),o(`td`,null,r(e.payload.name),1),o(`td`,null,r(e.payload.text),1)]))),128))])])):(P(),h(`p`,sn,r(t(a)(`当前会话暂无历史消息。`)),1))]),o(`form`,{class:`range`,onSubmit:u(k,[`prevent`])},[o(`label`,null,[e(r(t(a)(`起始序号`)),1),z(o(`input`,{"onUpdate:modelValue":s[1]||=e=>S.value=e,name:`from`,type:`number`,required:``},null,512),[[d,S.value]])]),o(`label`,null,[e(r(t(a)(`截止序号`)),1),z(o(`input`,{"onUpdate:modelValue":s[2]||=e=>C.value=e,name:`to`,type:`number`,required:``},null,512),[[d,C.value]])]),o(`button`,{id:`replay`,class:`primary`,disabled:T.value||!n.events.length},r(T.value?t(a)(`正在回放…`):t(a)(`回放这段对话`)),9,cn),o(`p`,ln,r(t(a)(`隔离回放:只测试回复,不发送 QQ,不修改真实记忆和 TA 的心智。`)),1)],32)]),o(`section`,un,[o(`div`,dn,[o(`h3`,null,r(t(a)(`处理记录`)),1)]),o(`div`,fn,[(P(!0),h(f,null,v(y.value,e=>(P(),h(`button`,{key:e.id,"data-trace":e.id,class:L([`trace-row`,{selected:b.value?.id===e.id}]),onClick:t=>A(e.id)},[o(`span`,null,[o(`time`,null,r(t(J)(e.time)),1),o(`small`,{class:`chip`,"data-tone":e.status===`error`?`danger`:`quiet`},r(t(g)[e.status]||e.status),9,mn)]),o(`p`,null,r(e.reason||e.error||t(a)(`查看处理详情`)),1)],10,pn))),128)),y.value.length?p(``,!0):(P(),h(`p`,hn,r(t(a)(`有新的消息处理记录后会显示在这里。`)),1))])]),o(`section`,gn,[o(`div`,_n,[o(`h3`,null,r(t(a)(`运行详情`)),1)]),o(`div`,vn,[b.value?(P(),h(`div`,yn,[o(`span`,null,[e(r(t(a)(`模式`)),1),o(`b`,null,r(t(_)[b.value.mode]||b.value.mode),1)]),o(`span`,null,[e(r(t(a)(`耗时`)),1),o(`b`,null,r(b.value.data?.elapsed??`—`)+` ms`,1)]),o(`span`,null,[e(r(t(a)(`模型调用`)),1),o(`b`,null,r(t(a)(`{v} 次`,{v:b.value.data?.calls?.length??0})),1)]),o(`span`,bn,[s[4]||=e(`Token`,-1),o(`b`,null,r(j(b.value.data?.tokens)),1)])])):p(``,!0),b.value?(P(),h(`p`,xn,r(b.value.reason||b.value.data?.decision?.reason||t(a)(`展开下方查看上下文、模型输入与原始输出。`)),1)):p(``,!0),o(`pre`,Sn,r(E.value||t(a)(`选择一条处理记录,查看最终上下文、Prompt、原始输出、Token 与耗时。`)),1)])])])]),_:1},8,[`open`,`title`]))}}),[[`__scopeId`,`data-v-839b3181`]]),wn={key:0,class:`card col list-col`},Tn={class:`card col talk-col`},En={class:`card col panel-col`},Dn={class:`sheet-list`},On=D(O({__name:`ChatsPage`,setup(e){let r=re(`(max-width: 1100px)`),i=A(()=>M.core?.sessions||[]),c=A(()=>i.value.filter(e=>!e.archived)),l=sessionStorage.activeSession,u=R(c.value.some(e=>e.id===l)?l:c.value[0]?.id||``),d=A(()=>c.value.find(e=>e.id===u.value)||null),f=R([]),v=R([]),S=R([]),w=R([]),E=R({}),D=R(``),O=R(!1),j=R(!1),N=R(!1),z=R(M.sub===`settings`),B=R(M.sub===`replay`),V=R(`here`),H=A(()=>M.health?.decisions||[]),U=A(()=>w.value.find(e=>e.session===u.value)||null),W=!1,G=!1,K=!1,q=``,J=0,Y=null,X=0;async function Z(e=!1){if(!u.value)return;if(W){G=!0,K||=e;return}let t=u.value,n=e||q!==t;W=!0;try{let[e,r]=await Promise.all([ie(t,n?void 0:J),ae(t,{limit:30,compact:!0})]);if(u.value!==t)return;let i=document.getElementById(`liveMessages`),o=q!==t||!i||i.scrollHeight-i.scrollTop-i.clientHeight<80;n?(f.value=e.reverse(),J=0):e.length&&(f.value=[...f.value,...e]);for(let t of e)J=Math.max(J,Number(t.seq)||0);!n&&e.length===100&&(G=!0),v.value=r,q=t,await C();let c=document.getElementById(`liveMessages`);c&&o&&(c.scrollTop=c.scrollHeight),D.value=a(`实时同步 · `)+new Date().toLocaleTimeString(s())}catch(e){D.value=a(`同步失败:`)+e.message}finally{if(W=!1,u.value!==t)G=!1,Z();else if(G){let e=K;G=!1,K=!1,Z(e)}}}async function he(){try{let[e,t]=await Promise.all([ee.bonds(),ee.overview()]);w.value=e.groups,E.value=Object.fromEntries(t.attention.map(e=>[e.session,e.unread]))}catch{}}x(u,e=>{if(Y?.abort(),e&&(sessionStorage.activeSession=e),f.value=[],v.value=[],S.value=[],q=``,J=0,G=!1,K=!1,N.value=!1,Z(),!e)return;pe(e).then(t=>{u.value===e&&(S.value=[...t].reverse())}).catch(()=>{});let t=new AbortController;Y=t;let n=``;ne(e=>{let t=Number(e?.messages);Number.isSafeInteger(t)&&t<J&&(q=``,J=0,f.value=[]);let r=JSON.stringify(e),i=n!==``&&r!==n;n=r,i&&Z()},{session:e,signal:t.signal,reconnectMs:1e3})},{immediate:!0}),x(c,e=>{e.some(e=>e.id===u.value)||(u.value=e[0]?.id||``)}),x(V,async e=>{if(e===`feedback`)try{M.health=await b()}catch{}}),x(B,e=>k(e?`replay`:z.value?`settings`:``)),x(()=>M.sub,e=>{e===`replay`&&(B.value=!0),e===`settings`&&(z.value=!0)}),x(()=>M.pulse,he),_(()=>{he(),X=window.setInterval(()=>Z(!0),15e3)}),T(()=>{clearInterval(X),Y?.abort()});async function ge(e){try{let t=await ce(e);await g(),u.value=t?.sessionId||`${e.kind}:${e.id}`,z.value=!0,V.value=`here`,j.value=!1,F(a(`会话已添加`))}catch(e){F(e.message,!0)}}async function Q(e){await le(e.id,!e.enabled),await g()}async function _e(e){await ue(e,!0),await g()}async function ve(e){await ue(e,!1),await g()}async function ye(e){await n(a(`永久删除这个会话?它的消息和设置都会消失。`),{title:a(`永久删除`),confirmText:a(`删除`),danger:!0})&&(await de(e),await g())}async function be(e){if(d.value)try{await fe(d.value.id,{...e,name:String(e.name||``).trim()||d.value.name}),M.dirty=!1,F(a(`会话配置已保存`)),await g()}catch(e){F(e.message,!0)}}async function xe(){u.value&&await n(a(`确定清空这个会话的消息上下文吗?长期记忆不会删除。`),{title:a(`清空上下文`),confirmText:a(`清空`),danger:!0})&&(await me(u.value),f.value=[],v.value=[],q=``,J=0,await Z(!0))}async function Se(e){if(!O.value&&u.value){O.value=!0;try{let t=await oe({sessionId:u.value,...e});F(t.reason||a(`已发送`)),M.health=await b(),await Z()}catch(e){F(e.message,!0)}finally{O.value=!1}}}async function Ce(e,t){try{await se(e,t),F(a(`反馈已保存`)),M.health=await b()}catch(e){F(e.message,!0)}}return(e,n)=>(P(),h(`div`,{class:L([`page chats`,{compact:t(r)}])},[t(r)?p(``,!0):(P(),h(`aside`,wn,[y(Oe,{sessions:i.value,selected:u.value,unread:E.value,onSelect:n[0]||=e=>u.value=e,onAdd:n[1]||=e=>j.value=!0,onToggle:Q,onRestore:ve,onRemove:ye},null,8,[`sessions`,`selected`,`unread`])])),o(`div`,Tn,[y(pt,{"session-id":u.value,session:d.value,events:f.value,traces:v.value,decisions:H.value,status:D.value||t(a)(`请选择会话`),demo:!!t(M).health.settings.demo,compact:t(r),sending:O.value,onSimulate:Se,onFeedback:Ce,onOpenList:n[2]||=e=>N.value=!0,onOpenReplay:n[3]||=e=>B.value=!0},null,8,[`session-id`,`session`,`events`,`traces`,`decisions`,`status`,`demo`,`compact`,`sending`])]),o(`aside`,En,[y(en,{"settings-open":z.value,"onUpdate:settingsOpen":n[4]||=e=>z.value=e,tab:V.value,"onUpdate:tab":n[5]||=e=>V.value=e,session:d.value,group:U.value,summaries:S.value,decisions:H.value,onToggle:Q,onArchive:_e,onClear:xe,onSave:be,onFeedback:Ce},null,8,[`settings-open`,`tab`,`session`,`group`,`summaries`,`decisions`])]),t(r)?(P(),m(te,{key:1,open:N.value,title:t(a)(`全部会话`),eyebrow:`CHATS`,width:`400px`,onClose:n[8]||=e=>N.value=!1},{default:I(()=>[o(`div`,Dn,[y(Oe,{sessions:i.value,selected:u.value,unread:E.value,onSelect:n[6]||=e=>u.value=e,onAdd:n[7]||=e=>j.value=!0,onToggle:Q,onRestore:ve,onRemove:ye},null,8,[`sessions`,`selected`,`unread`])])]),_:1},8,[`open`,`title`])):p(``,!0),y(Ne,{open:j.value,channel:t(M).health?.connection?.channel===`qqbot`?`qqbot`:`onebot`,ready:!!t(M).health?.connection?.qqbot?.appId,onClose:n[9]||=e=>j.value=!1,onAdd:ge},null,8,[`open`,`channel`,`ready`]),y(Cn,{open:B.value,"session-id":u.value,events:f.value,onClose:n[10]||=e=>B.value=!1,onLoad:n[11]||=e=>Z(!0)},null,8,[`open`,`session-id`,`events`])],2))}}),[[`__scopeId`,`data-v-c328f9fe`]]);export{On as default};