luckytri 1.2.3 → 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 (74) hide show
  1. package/README.en.md +44 -1
  2. package/README.md +44 -1
  3. package/docs/en/CHANGELOG.md +15 -0
  4. package/docs/zh/CHANGELOG.md +15 -0
  5. package/package.json +1 -1
  6. package/public/app/assets/ChatsPage-C5-w410L.js +3 -0
  7. package/public/app/assets/{Empty-uSyRBXOT.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-B9M9g95D.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-Ces-Bx70.js → knowledge-sUjcjbfI.js} +1 -1
  21. package/public/app/assets/media-Dh7c41PQ.js +1 -0
  22. package/public/app/assets/{mind-D3mdwFW3.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 +4 -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 +26 -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/plugins/zip.js +32 -4
  54. package/server/stickers/api.js +90 -2
  55. package/server/stickers/library.js +8 -2
  56. package/server/stickers/offer.js +4 -1
  57. package/server/stickers/zip-import.js +136 -0
  58. package/server/studio/auto-update.js +224 -0
  59. package/server/studio/updates.js +154 -144
  60. package/server/voice/index.js +255 -0
  61. package/server/voice/sauc.js +152 -0
  62. package/server/web/link-reader.js +379 -0
  63. package/public/app/assets/ChatsPage-o-exjdFu.js +0 -3
  64. package/public/app/assets/Field-Bfa5dCkk.js +0 -1
  65. package/public/app/assets/HeartPage-Q5ts7Ko6.js +0 -1
  66. package/public/app/assets/LifePage-B4AsD_DM.js +0 -1
  67. package/public/app/assets/MemoryPage-ijD6lsxo.js +0 -1
  68. package/public/app/assets/NaturePage-B9-dv8vz.js +0 -3
  69. package/public/app/assets/PeoplePage-BG_XPian.js +0 -1
  70. package/public/app/assets/PluginsPage-CPYYMown.js +0 -2
  71. package/public/app/assets/SystemPage-CTduZhr3.js +0 -1
  72. package/public/app/assets/TimePage-DScn8IN-.js +0 -3
  73. package/public/app/assets/index-5FMs9YYQ.js +0 -4
  74. package/public/app/assets/media-BsG-i0Lv.js +0 -1
@@ -0,0 +1,152 @@
1
+ // Volcengine streaming ASR (sauc, "nostream" endpoint of Agent Plan): the
2
+ // whole clip is sent in a few binary frames and one final text comes back.
3
+ // Audio is first converted to 16 kHz mono WAV with ffmpeg.
4
+ import { spawn } from "node:child_process";
5
+ import { randomUUID } from "node:crypto";
6
+ import { gzipSync, gunzipSync } from "node:zlib";
7
+ import WebSocket from "ws";
8
+
9
+ function frame(type, flags, serialization, payload) {
10
+ const body = gzipSync(payload);
11
+ const head = Buffer.from([
12
+ 0x11,
13
+ (type << 4) | flags,
14
+ (serialization << 4) | 0x01,
15
+ 0,
16
+ ]);
17
+ const size = Buffer.alloc(4);
18
+ size.writeUInt32BE(body.length);
19
+ return Buffer.concat([head, size, body]);
20
+ }
21
+
22
+ export function parseFrame(data) {
23
+ const buf = Buffer.from(data);
24
+ const headerSize = (buf[0] & 0x0f) * 4;
25
+ const type = buf[1] >> 4;
26
+ const flags = buf[1] & 0x0f;
27
+ const gz = (buf[2] & 0x0f) === 1;
28
+ let at = headerSize;
29
+ if (type === 0x0f) {
30
+ const code = buf.readUInt32BE(at);
31
+ const size = buf.readUInt32BE(at + 4);
32
+ return {
33
+ error: code,
34
+ message: buf.subarray(at + 8, at + 8 + size).toString(),
35
+ };
36
+ }
37
+ if (flags & 0x01) at += 4; // sequence number
38
+ const size = buf.readUInt32BE(at);
39
+ let payload = buf.subarray(at + 4, at + 4 + size);
40
+ if (gz && payload.length) payload = gunzipSync(payload);
41
+ let json = {};
42
+ try {
43
+ json = JSON.parse(payload.toString() || "{}");
44
+ } catch {
45
+ /* Not JSON: ignore. */
46
+ }
47
+ return { type, last: (flags & 0x02) === 0x02, json };
48
+ }
49
+
50
+ export function toWav(audio) {
51
+ return new Promise((resolve, reject) => {
52
+ const ff = spawn("ffmpeg", [
53
+ "-hide_banner",
54
+ "-loglevel",
55
+ "error",
56
+ "-i",
57
+ "pipe:0",
58
+ "-ar",
59
+ "16000",
60
+ "-ac",
61
+ "1",
62
+ "-f",
63
+ "wav",
64
+ "pipe:1",
65
+ ]);
66
+ const out = [];
67
+ ff.stdout.on("data", (d) => out.push(d));
68
+ ff.on("error", () => reject(new Error("没有找到 ffmpeg,听不了语音")));
69
+ ff.on("close", (code) =>
70
+ code === 0
71
+ ? resolve(Buffer.concat(out))
72
+ : reject(new Error("语音转换失败")),
73
+ );
74
+ ff.stdin.on("error", () => {});
75
+ ff.stdin.end(audio);
76
+ });
77
+ }
78
+
79
+ export async function saucTranscribe(
80
+ { url, key, resourceId },
81
+ audio,
82
+ { timeout = 20000 } = {},
83
+ ) {
84
+ const wav = await toWav(audio);
85
+ return new Promise((resolve, reject) => {
86
+ const ws = new WebSocket(url, {
87
+ headers: {
88
+ "X-Api-Key": key,
89
+ "X-Api-Resource-Id": resourceId || "volc.seedasr.sauc.duration",
90
+ "X-Api-Request-Id": randomUUID(),
91
+ "X-Api-Connect-Id": randomUUID(),
92
+ },
93
+ });
94
+ let text = "";
95
+ const timer = setTimeout(() => {
96
+ ws.terminate();
97
+ reject(new Error("语音识别超时"));
98
+ }, timeout);
99
+ const done = (fn, v) => {
100
+ clearTimeout(timer);
101
+ try {
102
+ ws.close();
103
+ } catch {
104
+ /* closing */
105
+ }
106
+ fn(v);
107
+ };
108
+ ws.on("unexpected-response", (_req, res) =>
109
+ done(
110
+ reject,
111
+ Object.assign(new Error("语音识别失败"), { status: res.statusCode }),
112
+ ),
113
+ );
114
+ ws.on("error", () => done(reject, new Error("语音识别失败")));
115
+ ws.on("open", () => {
116
+ ws.send(
117
+ frame(
118
+ 0x1,
119
+ 0x0,
120
+ 0x1,
121
+ Buffer.from(
122
+ JSON.stringify({
123
+ user: { uid: "luckytri" },
124
+ audio: { format: "wav", rate: 16000, bits: 16, channel: 1 },
125
+ request: {
126
+ model_name: "bigmodel",
127
+ enable_punc: true,
128
+ enable_itn: true,
129
+ },
130
+ }),
131
+ ),
132
+ ),
133
+ );
134
+ const chunk = 32000; // about one second of 16 kHz 16-bit audio
135
+ for (let i = 0; i < wav.length; i += chunk) {
136
+ const last = i + chunk >= wav.length;
137
+ ws.send(frame(0x2, last ? 0x2 : 0x0, 0x0, wav.subarray(i, i + chunk)));
138
+ }
139
+ });
140
+ ws.on("message", (data) => {
141
+ const f = parseFrame(data);
142
+ if (f.error)
143
+ return done(
144
+ reject,
145
+ Object.assign(new Error("语音识别失败"), { code: f.error }),
146
+ );
147
+ if (f.json?.result?.text) text = f.json.result.text;
148
+ if (f.last) done(resolve, text);
149
+ });
150
+ ws.on("close", () => done(resolve, text));
151
+ });
152
+ }
@@ -0,0 +1,379 @@
1
+ // Reading a link someone sent: fetch the public page, keep the readable text,
2
+ // and give her a short note of what it is. "I can't open links" was one of the
3
+ // most robotic things she said (round 24 real logs). Only public http(s)
4
+ // pages are read: private, loopback and link-local addresses are refused at
5
+ // every redirect hop, bodies are capped, and nothing is sent with cookies.
6
+ import { lookup } from "node:dns/promises";
7
+ import { isIP } from "node:net";
8
+
9
+ export const LINK_LIMITS = {
10
+ timeoutMs: 8000,
11
+ maxBytes: 2 * 1024 * 1024,
12
+ maxRedirects: 3,
13
+ perTurn: 2,
14
+ textChars: 6000,
15
+ ttlMs: 6 * 3600 * 1000,
16
+ };
17
+ const USER_AGENT = "LuckyTri-LinkReader/1.0 (+https://github.com/LuckyTri)";
18
+ const TEXT_TYPES =
19
+ /^(?:text\/html|application\/xhtml\+xml|text\/plain|application\/json|text\/markdown|application\/vnd\.github[\w.+-]*)/i;
20
+
21
+ export function linksIn(text) {
22
+ const found = [];
23
+ for (const match of String(text || "").matchAll(
24
+ /https?:\/\/[^\s<>"'()「」,。!?、]+/gi,
25
+ )) {
26
+ const url = match[0].replace(/[),.;:!?]+$/, "");
27
+ try {
28
+ const parsed = new URL(url);
29
+ if (!["http:", "https:"].includes(parsed.protocol)) continue;
30
+ parsed.hash = "";
31
+ const clean = parsed.toString();
32
+ if (!found.includes(clean)) found.push(clean);
33
+ } catch {
34
+ // Not a URL after all.
35
+ }
36
+ }
37
+ return found;
38
+ }
39
+
40
+ // Addresses that must never be reached from a chat message. 198.18.0.0/15
41
+ // stays allowed: fake-IP proxies (Clash and the like, common on the owner's
42
+ // machine) answer every public name from that range.
43
+ export function privateAddress(address) {
44
+ const kind = isIP(address);
45
+ if (kind === 4) {
46
+ const [a, b] = address.split(".").map(Number);
47
+ return (
48
+ a === 0 ||
49
+ a === 10 ||
50
+ a === 127 ||
51
+ (a === 100 && b >= 64 && b <= 127) ||
52
+ (a === 169 && b === 254) ||
53
+ (a === 172 && b >= 16 && b <= 31) ||
54
+ (a === 192 && b === 168) ||
55
+ a >= 224
56
+ );
57
+ }
58
+ if (kind === 6) {
59
+ const v = address.toLowerCase();
60
+ if (v.startsWith("::ffff:")) return privateAddress(v.slice(7));
61
+ return (
62
+ v === "::" ||
63
+ v === "::1" ||
64
+ v.startsWith("fc") ||
65
+ v.startsWith("fd") ||
66
+ v.startsWith("fe8") ||
67
+ v.startsWith("fe9") ||
68
+ v.startsWith("fea") ||
69
+ v.startsWith("feb") ||
70
+ v.startsWith("ff")
71
+ );
72
+ }
73
+ return true;
74
+ }
75
+
76
+ async function safeHost(hostname, resolve) {
77
+ const host = hostname.replace(/^\[|\]$/g, "");
78
+ if (/^localhost$|\.localhost$|\.local$|\.internal$/i.test(host)) return false;
79
+ if (isIP(host)) return !privateAddress(host);
80
+ let addresses;
81
+ try {
82
+ addresses = await resolve(host);
83
+ } catch {
84
+ return false;
85
+ }
86
+ return (
87
+ addresses.length > 0 && addresses.every((a) => !privateAddress(a.address))
88
+ );
89
+ }
90
+
91
+ const decode = (text) =>
92
+ text
93
+ .replace(/&nbsp;/g, " ")
94
+ .replace(/&lt;/g, "<")
95
+ .replace(/&gt;/g, ">")
96
+ .replace(/&quot;/g, '"')
97
+ .replace(/&#39;|&apos;/g, "'")
98
+ .replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n)))
99
+ .replace(/&#x([0-9a-f]+);/gi, (_, n) =>
100
+ String.fromCodePoint(parseInt(n, 16)),
101
+ )
102
+ .replace(/&amp;/g, "&");
103
+
104
+ // Readable text from a page: title, description and the body without
105
+ // scripts, styles, navigation, headers and footers.
106
+ export function readable(html) {
107
+ const meta = (name) => {
108
+ const re = new RegExp(
109
+ `<meta[^>]+(?:name|property)=["']${name}["'][^>]*content=["']([^"']*)["']|<meta[^>]+content=["']([^"']*)["'][^>]*(?:name|property)=["']${name}["']`,
110
+ "i",
111
+ );
112
+ const m = html.match(re);
113
+ return m ? decode(m[1] || m[2] || "").trim() : "";
114
+ };
115
+ const title =
116
+ meta("og:title") ||
117
+ decode((html.match(/<title[^>]*>([^]*?)<\/title>/i) || [])[1] || "").trim();
118
+ const description = meta("og:description") || meta("description");
119
+ const main =
120
+ (html.match(/<(?:main|article)[^>]*>([^]*?)<\/(?:main|article)>/i) ||
121
+ [])[1] ||
122
+ (html.match(/<body[^>]*>([^]*?)<\/body>/i) || [])[1] ||
123
+ html;
124
+ const text = decode(
125
+ main
126
+ .replace(
127
+ /<(script|style|noscript|svg|nav|header|footer|form|template)[^>]*>[^]*?<\/\1>/gi,
128
+ " ",
129
+ )
130
+ .replace(/<!--[^]*?-->/g, " ")
131
+ .replace(/<br\s*\/?>|<\/(?:p|div|li|h[1-6]|tr|section)>/gi, "\n")
132
+ .replace(/<[^>]+>/g, " "),
133
+ )
134
+ .replace(/[ \t\f\v]+/g, " ")
135
+ .replace(/\s*\n\s*/g, "\n")
136
+ .replace(/\n{2,}/g, "\n")
137
+ .trim();
138
+ return {
139
+ title: title.slice(0, 200),
140
+ description: description.slice(0, 400),
141
+ text,
142
+ };
143
+ }
144
+
145
+ async function fetchCapped(url, { fetcher, resolve, limits }) {
146
+ let current = url;
147
+ for (let hop = 0; hop <= limits.maxRedirects; hop++) {
148
+ const parsed = new URL(current);
149
+ if (!["http:", "https:"].includes(parsed.protocol))
150
+ throw Error("只读取 http/https 链接");
151
+ if (!(await safeHost(parsed.hostname, resolve)))
152
+ throw Error("这个地址不是公开网页");
153
+ const controller = new AbortController();
154
+ const timer = setTimeout(() => controller.abort(), limits.timeoutMs);
155
+ timer.unref?.();
156
+ let response;
157
+ try {
158
+ response = await fetcher(current, {
159
+ redirect: "manual",
160
+ signal: controller.signal,
161
+ headers: {
162
+ "User-Agent": USER_AGENT,
163
+ Accept:
164
+ "text/html,application/xhtml+xml,text/plain,application/json;q=0.9,*/*;q=0.1",
165
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
166
+ },
167
+ });
168
+ if ([301, 302, 303, 307, 308].includes(response.status)) {
169
+ const next = response.headers.get("location");
170
+ if (!next) throw Error("跳转缺少目标");
171
+ current = new URL(next, current).toString();
172
+ continue;
173
+ }
174
+ if (!response.ok) throw Error("网页没有正常返回");
175
+ const type = response.headers.get("content-type") || "";
176
+ if (type && !TEXT_TYPES.test(type)) throw Error("不是网页文字内容");
177
+ const declared = Number(response.headers.get("content-length") || 0);
178
+ if (declared > limits.maxBytes) throw Error("网页太大");
179
+ const reader = response.body?.getReader?.();
180
+ if (!reader) {
181
+ const text = await response.text();
182
+ if (Buffer.byteLength(text) > limits.maxBytes) throw Error("网页太大");
183
+ return { url: current, type, body: text };
184
+ }
185
+ const chunks = [];
186
+ let size = 0;
187
+ for (;;) {
188
+ const { done, value } = await reader.read();
189
+ if (done) break;
190
+ size += value.byteLength;
191
+ if (size > limits.maxBytes) {
192
+ await reader.cancel().catch(() => {});
193
+ throw Error("网页太大");
194
+ }
195
+ chunks.push(value);
196
+ }
197
+ return {
198
+ url: current,
199
+ type,
200
+ body: Buffer.concat(chunks.map((c) => Buffer.from(c))).toString("utf8"),
201
+ };
202
+ } catch (error) {
203
+ if (error.name === "AbortError") throw Error("网页读取超时");
204
+ throw error;
205
+ } finally {
206
+ clearTimeout(timer);
207
+ }
208
+ }
209
+ throw Error("跳转次数太多");
210
+ }
211
+
212
+ const GITHUB_REPO =
213
+ /^https:\/\/github\.com\/([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/i;
214
+
215
+ export class LinkReader {
216
+ constructor({
217
+ db = null,
218
+ fetcher = fetch,
219
+ resolve = (host) => lookup(host, { all: true, verbatim: true }),
220
+ limits = {},
221
+ now = () => Date.now(),
222
+ } = {}) {
223
+ this.db = db;
224
+ this.fetcher = fetcher;
225
+ this.resolve = resolve;
226
+ this.limits = { ...LINK_LIMITS, ...limits };
227
+ this.now = now;
228
+ this.memory = new Map();
229
+ if (db)
230
+ db.exec(
231
+ "CREATE TABLE IF NOT EXISTS core_link_cache (url TEXT PRIMARY KEY, ok INTEGER NOT NULL, title TEXT, site TEXT, description TEXT, text TEXT, error TEXT, fetched INTEGER NOT NULL)",
232
+ );
233
+ }
234
+ cached(url) {
235
+ const fresh = (row) => row && this.now() - row.fetched < this.limits.ttlMs;
236
+ const hit = this.memory.get(url);
237
+ if (fresh(hit)) return hit;
238
+ const row = this.db
239
+ ?.prepare("SELECT * FROM core_link_cache WHERE url=?")
240
+ .get(url);
241
+ if (fresh(row)) {
242
+ const value = { ...row, ok: !!row.ok };
243
+ this.memory.set(url, value);
244
+ return value;
245
+ }
246
+ return null;
247
+ }
248
+ remember(value) {
249
+ this.memory.set(value.url, value);
250
+ if (this.memory.size > 200)
251
+ this.memory.delete(this.memory.keys().next().value);
252
+ this.db
253
+ ?.prepare(
254
+ "INSERT INTO core_link_cache(url,ok,title,site,description,text,error,fetched) VALUES (?,?,?,?,?,?,?,?) ON CONFLICT(url) DO UPDATE SET ok=excluded.ok,title=excluded.title,site=excluded.site,description=excluded.description,text=excluded.text,error=excluded.error,fetched=excluded.fetched",
255
+ )
256
+ .run(
257
+ value.url,
258
+ value.ok ? 1 : 0,
259
+ value.title || "",
260
+ value.site || "",
261
+ value.description || "",
262
+ value.text || "",
263
+ value.error || "",
264
+ value.fetched,
265
+ );
266
+ return value;
267
+ }
268
+ async read(url) {
269
+ const hit = this.cached(url);
270
+ if (hit) return hit;
271
+ const site = (() => {
272
+ try {
273
+ return new URL(url).hostname;
274
+ } catch {
275
+ return "";
276
+ }
277
+ })();
278
+ try {
279
+ const repo = url.match(GITHUB_REPO);
280
+ if (repo) return this.remember(await this.github(url, repo[1], repo[2]));
281
+ const page = await fetchCapped(url, {
282
+ fetcher: this.fetcher,
283
+ resolve: this.resolve,
284
+ limits: this.limits,
285
+ });
286
+ const html = /html/i.test(page.type) || /^\s*</.test(page.body);
287
+ const parts = html
288
+ ? readable(page.body)
289
+ : { title: "", description: "", text: page.body.trim() };
290
+ return this.remember({
291
+ url,
292
+ ok: true,
293
+ site,
294
+ title: parts.title,
295
+ description: parts.description,
296
+ text: parts.text.slice(0, this.limits.textChars),
297
+ fetched: this.now(),
298
+ });
299
+ } catch (error) {
300
+ return this.remember({
301
+ url,
302
+ ok: false,
303
+ site,
304
+ error: String(error.message || error).slice(0, 120),
305
+ fetched: this.now(),
306
+ });
307
+ }
308
+ }
309
+ async github(url, owner, name) {
310
+ const api = `https://api.github.com/repos/${owner}/${name}`;
311
+ const get = async (target, accept) =>
312
+ fetchCapped(target, {
313
+ fetcher: (u, o) =>
314
+ this.fetcher(u, { ...o, headers: { ...o.headers, Accept: accept } }),
315
+ resolve: this.resolve,
316
+ limits: this.limits,
317
+ });
318
+ const info = JSON.parse(
319
+ (await get(api, "application/vnd.github+json")).body || "{}",
320
+ );
321
+ let readme = "";
322
+ try {
323
+ readme = (await get(`${api}/readme`, "application/vnd.github.raw+json"))
324
+ .body;
325
+ } catch {
326
+ // A repository without a README still has its description.
327
+ }
328
+ return {
329
+ url,
330
+ ok: true,
331
+ site: "github.com",
332
+ title: info.full_name || `${owner}/${name}`,
333
+ description: [
334
+ info.description,
335
+ info.language && `主要语言 ${info.language}`,
336
+ Number.isFinite(info.stargazers_count) &&
337
+ `${info.stargazers_count} star`,
338
+ ]
339
+ .filter(Boolean)
340
+ .join(";"),
341
+ text: readme
342
+ .replace(/!\[[^\]]*\]\([^)]*\)/g, "")
343
+ .replace(/<[^>]+>/g, " ")
344
+ .slice(0, this.limits.textChars),
345
+ fetched: this.now(),
346
+ };
347
+ }
348
+ // Links in the messages she is reading now: what each page is, in its own
349
+ // words. No extra model call: the title, description and opening of the
350
+ // page are enough for a chat, and keep the turn fast.
351
+ async forMessages(messages) {
352
+ const wanted = [];
353
+ for (const m of messages || [])
354
+ for (const url of linksIn(m.text))
355
+ if (!wanted.some((w) => w.url === url))
356
+ wanted.push({ url, messageId: m.id });
357
+ const picked = wanted.slice(-this.limits.perTurn);
358
+ return Promise.all(
359
+ picked.map(async ({ url, messageId }) => {
360
+ const page = await this.read(url);
361
+ if (!page.ok)
362
+ return { messageId, url, site: page.site, fetched: false };
363
+ // A glance, not a briefing: title + a short opening is enough to
364
+ // talk; a long excerpt made her read out feature lists.
365
+ return {
366
+ messageId,
367
+ url,
368
+ site: page.site,
369
+ fetched: true,
370
+ title: page.title,
371
+ ...(page.description
372
+ ? { description: String(page.description).slice(0, 160) }
373
+ : {}),
374
+ excerpt: String(page.text || "").slice(0, 360),
375
+ };
376
+ }),
377
+ );
378
+ }
379
+ }
@@ -1,3 +0,0 @@
1
- import{Dt as e,E as t,Et as n,G as r,H as i,I as a,J as o,M as s,P as c,Q as l,St as u,Tt as d,U as f,W as p,X as m,Y as h,Z as g,_t as _,a as v,at as y,bt as b,dt as x,et as S,f as C,ft as w,gt as T,it as E,j as D,lt as O,m as k,n as A,o as j,rt as M,s as N,t as P,tt as F,ut as ee,v as I,wt as L,yt as R,z}from"./Empty-uSyRBXOT.js";import{g as te,y as B}from"./mind-D3mdwFW3.js";import{c as V,f as H,h as U,m as W,n as G,o as ne,p as K,u as q,y as re}from"./index-5FMs9YYQ.js";import{t as ie}from"./media-BsG-i0Lv.js";import{t as J}from"./Field-Bfa5dCkk.js";async function ae(e,t){let n=new URLSearchParams({session:e});return t!==void 0&&n.set(`after`,String(t)),k(`/core/events?`+n.toString())}async function oe(e,t={}){let n=new URLSearchParams({session:e});return t.limit&&n.set(`limit`,String(t.limit)),t.compact&&n.set(`compact`,`1`),k(`/core/traces?`+n.toString())}async function Y(e){return k(`/core/traces/`+e)}async function se(e){return k(`/simulate`,`POST`,e)}async function ce(e,t){return k(`/decisions/`+e+`/feedback`,`POST`,{tag:t})}async function X(e){return k(`/core/replay`,`POST`,e)}async function le(e){return k(`/core/sessions`,`POST`,e)}async function ue(e,t){return k(`/core/sessions/`+encodeURIComponent(e),`PATCH`,{enabled:t})}async function de(e,t){return k(`/core/sessions/`+encodeURIComponent(e)+`/archive`,`PATCH`,{archived:t})}async function fe(e){return k(`/core/sessions/`+encodeURIComponent(e),`DELETE`,{})}async function pe(e,t){return k(`/core/sessions/`+encodeURIComponent(e),`PUT`,t)}async function me(e){return k(`/core/sessions/`+encodeURIComponent(e)+`/summaries`)}async function he(e){return k(`/core/sessions/`+encodeURIComponent(e)+`/context`,`DELETE`,{})}var ge={class:`session-list`},_e={class:`list-head`},Z=[`placeholder`,`aria-label`],ve={class:`rows scroll-pane`},Q=[`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=A(M({__name:`SessionList`,props:{sessions:{},selected:{},unread:{}},emits:[`select`,`add`,`toggle`,`restore`,`remove`],setup(t,{emit:s}){let c=t,f=s,p=u(``),m=o(()=>c.sessions.filter(e=>!e.archived&&`${e.name} ${e.id}`.toLowerCase().includes(p.value.toLowerCase()))),_=o(()=>c.sessions.filter(e=>e.archived));return(o,s)=>(x(),l(`div`,ge,[h(`div`,_e,[h(`h2`,null,[S(e(L(a)(`会话`))+` `,1),h(`small`,null,e(m.value.length),1)]),h(`button`,{class:`small primary`,onClick:s[0]||=e=>f(`add`)},e(L(a)(`+ 添加会话`)),1)]),b(h(`input`,{"onUpdate:modelValue":s[1]||=e=>p.value=e,type:`search`,placeholder:L(a)(`搜索名称或号码`),"aria-label":L(a)(`搜索会话`)},null,8,Z),[[i,p.value]]),h(`div`,ve,[(x(!0),l(r,null,w(m.value,r=>(x(),l(`div`,{key:r.id,class:d([`session-row`,{selected:r.id===t.selected,paused:!r.enabled}])},[h(`button`,{class:`row-main`,"data-pick":r.id,"aria-current":r.id===t.selected?`true`:void 0,onClick:e=>f(`select`,r.id)},[h(`span`,{class:`badge`,style:n({"--hue":L(H)(r.id)})},e(r.kind===`private`?L(a)(`私`):L(a)(`群`)),5),h(`span`,ye,[h(`b`,null,e(L(U)(r)),1),h(`small`,null,e(r.enabled?L(a)(`参与中`):L(a)(`已暂停`)),1)]),t.unread[r.id]?(x(),l(`span`,{key:0,class:`unread`,"aria-label":L(a)(`{v} 条没细看`,{v:t.unread[r.id]})},e(t.unread[r.id]),9,be)):g(``,!0)],8,Q),h(`label`,{class:`mini-switch`,title:r.enabled?L(a)(`暂停参与`):L(a)(`开启参与`)},[h(`input`,{type:`checkbox`,checked:r.enabled,"aria-label":L(a)(`参与:{v}`,{v:L(U)(r)}),onChange:e=>f(`toggle`,r)},null,40,Se)],8,xe)],2))),128)),m.value.length?g(``,!0):(x(),l(`p`,Ce,e(L(a)(`暂无匹配的会话。`)),1)),_.value.length?(x(),l(`details`,we,[h(`summary`,null,e(L(a)(`已归档 · {length}`,{length:_.value.length})),1),(x(!0),l(r,null,w(_.value,t=>(x(),l(`article`,{key:t.id},[h(`b`,null,e(L(U)(t)),1),h(`div`,Te,[h(`button`,{class:`small`,"data-restore-session":t.id,onClick:e=>f(`restore`,t.id)},e(L(a)(`恢复`)),9,Ee),h(`button`,{class:`small danger`,"data-delete-session":t.id,onClick:e=>f(`remove`,t.id)},e(L(a)(`永久删除`)),9,De)])]))),128))])):g(``,!0)])]))}}),[[`__scopeId`,`data-v-9b8dcbac`]]),ke={class:`muted`},Ae={key:0,class:`notice`,role:`status`},je=[`pattern`,`inputmode`],Me=[`disabled`],Ne=M({__name:`AddSession`,props:{open:{type:Boolean},channel:{},ready:{type:Boolean}},emits:[`close`,`add`],setup(t,{emit:n}){let r=t,i=u(`group`),s=n,c=o(()=>r.channel===`qqbot`),d=o(()=>c.value?`[0-9A-Za-z_\\-]{8,64}`:`\\d{4,20}`);function f(e){let t=e.target;s(`add`,{...Object.fromEntries(new FormData(t)),channel:r.channel})}return(n,r)=>(x(),m(ne,{open:t.open,title:L(a)(`添加群聊 / 私聊`),eyebrow:L(a)(`NEW PLACE`),width:`440px`,onClose:r[1]||=e=>s(`close`)},{default:R(()=>[h(`form`,{id:`addSession`,class:`stack`,onSubmit:p(f,[`prevent`])},[h(`p`,ke,e(L(a)(`添加之后再决定要不要让 TA 参与;开不开口仍由 TA 自己决定。`)),1),c.value&&!t.ready?(x(),l(`p`,Ae,e(L(a)(`请先在「连接 QQ」里填写机器人的 AppID`)),1)):g(``,!0),F(J,{label:L(a)(`类型`)},{default:R(()=>[F(V,{modelValue:i.value,"onUpdate:modelValue":r[0]||=e=>i.value=e,name:`kind`,"aria-label":L(a)(`类型`),options:[{value:`group`,label:L(a)(`群聊`)},{value:`private`,label:L(a)(`私聊`)}]},null,8,[`modelValue`,`aria-label`,`options`])]),_:1},8,[`label`]),F(J,{label:c.value?L(a)(`群 openid / 用户 openid`):L(a)(`群号 / QQ 号`),hint:c.value?L(a)(`8–64 位字母、数字、下划线或短横线。官方平台的会话通常会在有人 @ 她或私聊她时自动出现,这里只用于提前登记。`):L(a)(`4–20 位数字`)},{default:R(()=>[h(`input`,{name:`id`,required:``,pattern:d.value,inputmode:c.value?`text`:`numeric`,autocomplete:`off`},null,8,je)]),_:1},8,[`label`,`hint`]),F(J,{label:L(a)(`显示名称`)},{default:R(()=>[...r[2]||=[h(`input`,{name:`name`,required:``,maxlength:`100`,autocomplete:`off`},null,-1)]]),_:1},8,[`label`]),h(`button`,{class:`primary`,disabled:c.value&&!t.ready},e(L(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=A(M({__name:`Transcript`,props:{session:{},sessionId:{},events:{},traces:{},decisions:{},status:{},demo:{type:Boolean},compact:{type:Boolean},sending:{type:Boolean}},emits:[`simulate`,`feedback`,`openList`,`openReplay`],setup(t,{expose:_,emit:v}){let C=t,T=v,E=c({glanced:D(`TA 扫了一眼`),silent:D(`TA 看了,没出声`),deferred:D(`TA 睡着了,醒来再看`)}),O=u(`10001`),k=u(``),A=u(!0),j=u(),M=u({}),ee=()=>B(s(),{month:`long`,day:`numeric`,weekday:`short`}),I=o(()=>{let e=C.events.map(e=>({type:`message`,key:`m`+e.seq,time:e.time,event:e})),t=C.events[0]?.time??2**53-1;for(let n of C.traces)E[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=ee().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 R(e){let t=String(e.payload.text||``);return C.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 te(e){return C.traces.filter(t=>t.status===`sent`&&t.time<=e.time+1e3).sort((e,t)=>t.time-e.time)[0]}async function U(e){if(M.value[e.seq]){let{[e.seq]:t,...n}=M.value;M.value=n;return}M.value={...M.value,[e.seq]:{loading:!0,detail:null}};let t=te(e),n=null;try{n=t?await Y(t.id):null}catch{n=null}M.value[e.seq]&&(M.value={...M.value,[e.seq]:{loading:!1,detail:n}})}async function W(){await y(),j.value?.focus()}function G(){k.value.trim()&&!C.sending&&(T(`simulate`,{userId:O.value,text:k.value,mentioned:A.value}),k.value=``)}return _({focusComposer:W}),(o,s)=>(x(),l(`section`,Pe,[h(`header`,Fe,[t.compact?(x(),l(`button`,{key:0,class:`icon-button`,"aria-label":L(a)(`全部会话`),title:L(a)(`全部会话`),onClick:s[0]||=e=>T(`openList`)},` ☰ `,8,Ie)):g(``,!0),t.session?(x(),l(`span`,{key:1,class:`badge`,style:n({"--hue":L(H)(t.session.id)})},e(t.session.kind===`private`?L(a)(`私`):L(a)(`群`)),5)):g(``,!0),h(`div`,Le,[h(`h2`,null,e(t.session?.name||L(a)(`选择一段对话`)),1),h(`span`,Re,e(t.status),1)]),h(`div`,ze,[h(`button`,{class:`small sim-button`,disabled:!t.sessionId,onClick:W},e(L(a)(`模拟消息`)),9,Be),h(`button`,{class:`small`,onClick:s[1]||=e=>T(`openReplay`)},e(L(a)(`回到那一刻`)),1)])]),h(`div`,Ve,[(x(!0),l(r,null,w(I.value,t=>(x(),l(r,{key:t.key},[t.type===`day`?(x(),l(`div`,He,[h(`span`,null,e(t.label),1)])):t.type===`divider`?(x(),l(`div`,{key:1,class:`divider`,"data-status":t.status},[h(`span`,null,e(L(E)[t.status])+e(t.count>1?` × ${t.count}`:``),1),t.reasons.length?(x(),l(`small`,We,e(t.reasons.slice(0,2).join(`;`)),1)):g(``,!0)],8,Ue)):(x(),l(`article`,{key:2,class:d([`message`,{bot:t.event.role===`assistant`,simulated:t.event.payload.simulated}])},[t.event.role===`assistant`?g(``,!0):(x(),l(`span`,{key:0,class:`avatar`,style:n({"--hue":L(H)(String(t.event.payload.userId||t.event.payload.name))}),"aria-hidden":`true`},e(L(K)(t.event.payload.name)),5)),h(`div`,Ge,[h(`div`,Ke,[h(`b`,null,e(t.event.payload.name),1),h(`time`,null,e(L(q)(t.event.time)),1),t.event.payload.simulated?(x(),l(`span`,qe,e(L(a)(`模拟`)),1)):g(``,!0)]),h(`p`,Je,e(t.event.payload.text),1),t.event.role===`assistant`?(x(),l(r,{key:0},[h(`button`,{class:`text-button why-toggle`,"aria-expanded":!!M.value[t.event.seq],onClick:e=>U(t.event)},e(M.value[t.event.seq]?L(a)(`收起`):L(a)(`为什么这么说`)),9,Ye),M.value[t.event.seq]?(x(),l(`div`,Xe,[M.value[t.event.seq].loading?(x(),l(`p`,Ze,e(L(a)(`正在翻当时的记录…`)),1)):(x(),l(r,{key:1},[M.value[t.event.seq].detail?.data?.decision?.appraisal?(x(),l(`p`,Qe,[h(`b`,null,e(L(a)(`TA 当时的理解:`)),1),S(e(M.value[t.event.seq].detail.data.decision.appraisal),1)])):g(``,!0),h(`p`,null,[h(`b`,null,e(L(a)(`理由:`)),1),S(e(M.value[t.event.seq].detail?.data?.reason||M.value[t.event.seq].detail?.data?.decision?.reason||R(t.event)?.reason||L(a)(`没有留下理由。`)),1)]),M.value[t.event.seq].detail?.data?.snapshot?.knowledge?.length?(x(),l(`p`,$e,[h(`b`,null,e(L(a)(`引用的资料:`)),1),S(e(M.value[t.event.seq].detail.data.snapshot.knowledge.map(e=>e.title).join(`、`)),1)])):g(``,!0),R(t.event)?(x(),l(`label`,et,[S(e(L(a)(`这句怎么样?`))+` `,1),F(V,{"model-value":R(t.event).feedback||``,"aria-label":L(a)(`这句怎么样?`),options:[{value:``,label:L(a)(`选择评价`)},...Object.entries(L(N).health.feedbackLabels||{}).map(([e,t])=>({value:e,label:String(t)}))],"onUpdate:modelValue":e=>T(`feedback`,R(t.event).id,e)},null,8,[`model-value`,`aria-label`,`options`,`onUpdate:modelValue`])])):g(``,!0)],64))])):g(``,!0)],64)):g(``,!0)])],2))],64))),128)),t.events.length?g(``,!0):(x(),m(P,{key:0,title:t.sessionId?L(a)(`等待新的消息`):L(a)(`选择一段对话`),text:L(a)(`群聊与私聊消息会在这里实时出现;TA 扫一眼、没出声也会留下一条分隔线。`)},null,8,[`title`,`text`]))]),t.demo?(x(),l(`form`,{key:0,id:`simulate`,class:`composer`,onSubmit:p(G,[`prevent`])},[h(`div`,tt,[h(`span`,nt,e(L(a)(`模拟消息`)),1),h(`span`,rt,e(L(a)(`作为一条模拟消息走完整流程,写进模拟会话;不会发到 QQ,也不进入真实记忆。`)),1)]),h(`div`,it,[h(`label`,at,[h(`span`,ot,e(L(a)(`体验用户 ID`)),1),b(h(`input`,{"onUpdate:modelValue":s[2]||=e=>O.value=e,name:`userId`,pattern:`\\d{4,20}`,required:``,title:L(a)(`体验用户 ID`)},null,8,st),[[i,O.value]])]),b(h(`textarea`,{ref_key:`composer`,ref:j,"onUpdate:modelValue":s[3]||=e=>k.value=e,name:`text`,rows:`1`,placeholder:L(a)(`以这位群友的身份说一句…`),required:``,onKeydown:f(p(G,[`exact`,`prevent`]),[`enter`])},null,40,ct),[[i,k.value]]),h(`label`,lt,[b(h(`input`,{"onUpdate:modelValue":s[4]||=e=>A.value=e,type:`checkbox`},null,512),[[z,A.value]]),S(e(L(a)(`视为 @`)),1)]),h(`button`,{class:`primary`,disabled:t.sending||!t.sessionId},e(t.sending?L(a)(`TA 在想…`):L(a)(`发送模拟消息`)),9,ut)])],32)):(x(),l(`div`,dt,[h(`span`,ft,e(L(a)(`真实模式下不能模拟消息。想看看 TA 会怎么回,可以「和 TA 聊聊」——那是试聊,什么都不写入。`)),1),h(`button`,{class:`small`,onClick:s[5]||=e=>L(N).chatOpen=!0},e(L(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=A(M({__name:`SessionPanel`,props:E({session:{},group:{},summaries:{},decisions:{}},{settingsOpen:{type:Boolean,default:!1},settingsOpenModifiers:{},tab:{default:`here`},tabModifiers:{}}),emits:E([`toggle`,`archive`,`clear`,`save`,`feedback`],[`update:settingsOpen`,`update:tab`]),setup(t,{emit:n}){let s=t,f=T(t,`settingsOpen`),m=T(t,`tab`),v=n,y=c([D(`近期细摘要`),D(`中期摘要`),D(`较早摘要`),D(`远期摘要`)]),C=u(0),E=o(()=>s.decisions.filter(e=>e.session_id===s.session?.id&&e.reply)),O=o(()=>Math.max(1,Math.ceil(E.value.length/$))),k=u(!1),A=u({});_(()=>s.session?.policy,e=>{A.value={...e||{}}},{immediate:!0,deep:!0}),_(()=>s.session?.id,()=>{C.value=0,k.value=!1});function j(e){let t=e?.face;return t?[t.role,t.tone,t.content].filter(Boolean).join(`
2
-
3
- `):``}function M(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;v(`save`,n)}return(n,o)=>(x(),l(`section`,mt,[F(G,{modelValue:m.value,"onUpdate:modelValue":o[0]||=e=>m.value=e,label:L(a)(`这个会话`),items:[{key:`here`,label:L(a)(`这个会话`)},{key:`feedback`,label:L(a)(`反馈`)}]},null,8,[`modelValue`,`label`,`items`]),t.session?m.value===`here`?(x(),l(`div`,gt,[h(`div`,_t,[h(`div`,vt,[h(`div`,null,[h(`span`,yt,e(L(a)(`TA 在这里的样子`)),1),j(t.group)?(x(),l(r,{key:0},[h(`p`,{class:d([`face-body`,k.value?`open`:`clamp`])},e(j(t.group)),3),j(t.group).length>96?(x(),l(`button`,{key:0,type:`button`,class:`text-button`,onClick:o[1]||=e=>k.value=!k.value},e(k.value?L(a)(`收起`):L(a)(`展开完整内容`)),1)):g(``,!0)],64)):(x(),l(`p`,bt,e(L(a)(`还没有形成在这里的样子。`)),1)),t.group?.face?.aspiration?(x(),l(`small`,xt,e(L(a)(`想成为:{aspiration}`,{aspiration:t.group.face.aspiration})),1)):g(``,!0)]),h(`div`,null,[h(`span`,St,e(t.session.kind===`private`?L(a)(`这段私聊给 TA 的感觉`):L(a)(`这个群给 TA 的感觉`)),1),h(`p`,null,e(t.group?.bond?.feel||L(a)(`刚来这里不久`)),1)])]),h(`p`,Ct,e(L(a)(`在这里开不开口由 TA 自己决定:没有参与概率和冷却。TA 对这里每个人的感觉在「人际」里看。`)),1),h(`div`,wt,[h(`button`,{id:`toggleSession`,"data-toggle":t.session.id,class:d({primary:!t.session.enabled}),onClick:o[2]||=e=>v(`toggle`,t.session)},e(t.session.enabled?L(a)(`暂停参与`):L(a)(`开启参与`)),11,Tt),h(`button`,{"data-archive":t.session.id,onClick:o[3]||=e=>v(`archive`,t.session.id)},e(L(a)(`归档`)),9,Et),h(`button`,{id:`clearLiveContext`,onClick:o[4]||=e=>v(`clear`)},e(L(a)(`清空上下文`)),1)]),h(`details`,{class:`fold session-settings`,open:f.value,onToggle:o[19]||=e=>f.value=e.target.open},[h(`summary`,null,e(L(a)(`会话设置`)),1),(x(),l(`form`,{key:t.session.id,"data-session":t.session.id,class:`stack tight`,onSubmit:p(M,[`prevent`]),onInput:o[17]||=e=>L(N).dirty=!0,onChange:o[18]||=e=>L(N).dirty=!0},[h(`p`,kt,e(L(a)(`所有会话和独处都用模型库里的默认模型;其余已启用的模型按列表顺序做备用。她在这个聊天窗里用什么样子,由她自己从经历里决定。`)),1),h(`label`,null,[S(e(L(a)(`会话名称`))+` `,1),h(`input`,{name:`name`,maxlength:`100`,autocomplete:`off`,value:t.session.name},null,8,At),h(`small`,jt,e(L(a)(`官方机器人平台不提供群名称,可以在这里自己起一个`)),1)]),h(`label`,Mt,[b(h(`input`,{name:`selectiveVision`,type:`checkbox`,"onUpdate:modelValue":o[5]||=e=>A.value.selectiveVision=e,onInput:o[6]||=p(()=>{},[`stop`])},null,544),[[z,A.value.selectiveVision]]),S(e(L(a)(`节能看图:只在被 @ 或明确要求时,看这一条和附近的图`)),1)]),h(`details`,Nt,[h(`summary`,null,e(L(a)(`高级策略`)),1),h(`div`,Pt,[h(`label`,null,[S(e(L(a)(`聚合窗口 ms`)),1),b(h(`input`,{name:`aggregateMs`,type:`number`,"onUpdate:modelValue":o[7]||=e=>A.value.aggregateMs=e},null,512),[[i,A.value.aggregateMs,void 0,{number:!0}]])]),h(`label`,null,[S(e(L(a)(`最长等待 ms`)),1),b(h(`input`,{name:`maxWaitMs`,type:`number`,"onUpdate:modelValue":o[8]||=e=>A.value.maxWaitMs=e},null,512),[[i,A.value.maxWaitMs,void 0,{number:!0}]])]),h(`label`,null,[S(e(L(a)(`近期原文条数(10–500)`)),1),b(h(`input`,{name:`contextMessages`,type:`number`,min:`10`,max:`500`,"onUpdate:modelValue":o[9]||=e=>A.value.contextMessages=e},null,512),[[i,A.value.contextMessages,void 0,{number:!0}]])]),h(`label`,null,[S(e(L(a)(`一轮总字数`)),1),b(h(`input`,{name:`maxReply`,type:`number`,"onUpdate:modelValue":o[10]||=e=>A.value.maxReply=e},null,512),[[i,A.value.maxReply,void 0,{number:!0}]])]),h(`label`,Ft,[b(h(`input`,{name:`compaction`,type:`checkbox`,"onUpdate:modelValue":o[11]||=e=>A.value.compaction=e,onInput:o[12]||=p(()=>{},[`stop`])},null,544),[[z,A.value.compaction]]),S(e(L(a)(`上下文压缩:更早的聊天定期整理成分层摘要,越近越详细`)),1)]),h(`label`,It,[b(h(`input`,{name:`memory`,type:`checkbox`,"onUpdate:modelValue":o[13]||=e=>A.value.memory=e,onInput:o[14]||=p(()=>{},[`stop`])},null,544),[[z,A.value.memory]]),S(e(L(a)(`长期记忆`)),1)]),h(`label`,Lt,[b(h(`input`,{name:`deepCheck`,type:`checkbox`,"onUpdate:modelValue":o[15]||=e=>A.value.deepCheck=e,onInput:o[16]||=p(()=>{},[`stop`])},null,544),[[z,A.value.deepCheck]]),S(e(L(a)(`倾诉、纠正和危机时额外复审一次回复`)),1)])])]),h(`div`,Rt,[h(`button`,zt,e(L(a)(`保存会话设置`)),1),h(`small`,Bt,e(L(N).dirty?L(a)(`有未保存的修改`):L(a)(`保存后下一轮生效`)),1)])],40,Ot))],40,Dt),h(`details`,Vt,[h(`summary`,null,e(L(a)(`语境摘要 · {length} 段`,{length:t.summaries.length})),1),t.summaries.length?g(``,!0):(x(),l(`p`,Ht,e(L(a)(`还没有摘要。聊天超过近期原文条数后,更早的内容会自动整理到这里。`)),1)),(x(!0),l(r,null,w(t.summaries,t=>(x(),l(`article`,{key:t.id,class:`summary`},[h(`header`,null,[h(`b`,null,e(L(y)[t.level]||L(a)(`摘要`)),1),h(`small`,Ut,e(L(a)(`{period} · {messages} 条消息`,{period:t.period,messages:t.messages})),1)]),h(`p`,null,e(t.summary),1),t.keyPoints?.length?(x(),l(`ul`,Wt,[(x(!0),l(r,null,w(t.keyPoints,(t,n)=>(x(),l(`li`,{key:n},e(t.open?L(a)(`未完:`):``)+e(t.text),1))),128))])):g(``,!0)]))),128))])])])):(x(),l(`div`,Gt,[h(`p`,Kt,e(L(a)(`针对具体回复评价口吻;这是 TA 在这个会话里听到的话,不会改写天性。`)),1),h(`div`,qt,[(x(!0),l(r,null,w(E.value.slice(C.value*$,C.value*$+$),t=>(x(),l(`article`,{key:t.id,class:`feedback-item`},[h(`time`,Jt,e(L(q)(t.time)),1),h(`p`,Yt,e(t.reply),1),h(`label`,null,[S(e(L(a)(`这句回复怎么样?`))+` `,1),F(V,{"data-feedback":t.id,"model-value":t.feedback||``,"aria-label":L(a)(`这句回复怎么样?`),options:[{value:``,label:L(a)(`选择评价`)},...Object.entries(L(N).health.feedbackLabels||{}).map(([e,t])=>({value:e,label:String(t)}))],"onUpdate:modelValue":e=>v(`feedback`,t.id,e)},null,8,[`data-feedback`,`model-value`,`aria-label`,`options`,`onUpdate:modelValue`])])]))),128)),E.value.length?g(``,!0):(x(),l(`p`,Xt,e(L(a)(`还没有可以评价的回复。`)),1))]),h(`div`,Zt,[h(`button`,{class:`small`,disabled:C.value===0,onClick:o[20]||=e=>C.value--},`←`,8,Qt),h(`span`,null,e(C.value+1)+` / `+e(O.value),1),h(`button`,{class:`small`,disabled:C.value+1>=O.value,onClick:o[21]||=e=>C.value++},` → `,8,$t)])])):(x(),l(`div`,ht,[F(P,{title:L(a)(`还没有选中会话`),text:L(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=A(M({__name:`ReplaySheet`,props:{open:{type:Boolean},sessionId:{},events:{}},emits:[`close`,`load`],setup(t,{emit:n}){let o=t,s=n,f=c({sent:D(`开口了`),silent:D(`没出声`),glanced:D(`扫了一眼`),deferred:D(`睡着了`),running:D(`处理中`),error:D(`失败`),complete:D(`完成`),stale:D(`旧稿作废`),cancelled:D(`已取消`),interrupted:D(`已中断`)}),v=c({live:D(`真实聊天`),demo:D(`模拟预览`),replay:D(`测试回放`),memory:D(`记忆整理`),summary:D(`语境压缩`)}),y=u([]),C=u(null),T=u(``),E=u(``),O=u(!1),k=u(``);async function A(){o.sessionId&&(y.value=await oe(o.sessionId))}function j(){let e=o.events.map(e=>Number(e.seq));e.length&&(T.value||=String(Math.min(...e)),E.value||=String(Math.max(...e)))}async function M(){if(!O.value){O.value=!0;try{I(a(`回放正在运行`));let e=await X({session:o.sessionId,from:Number(T.value),to:Number(E.value)});k.value=JSON.stringify(e,null,2),I(a(`回放结束`)),await A()}catch(e){I(e.message,!0)}finally{O.value=!1}}}async function N(e){C.value=await Y(e),k.value=JSON.stringify(C.value,null,2)}function P(e){return e?a(`输入 {v}(缓存读 {v2} / 写 {v3})· 输出 {v4}`,{v:W(e.input),v2:W(e.cachedRead),v3:W(e.cacheWrite),v4:W(e.output)}):`—`}return _(()=>[o.open,o.sessionId],()=>{o.open&&(T.value=``,E.value=``,C.value=null,j(),A())},{immediate:!0}),_(()=>o.events.length,j),(n,o)=>(x(),m(ne,{open:t.open,title:L(a)(`回到那一刻`),eyebrow:`REPLAY`,width:`1180px`,onClose:o[3]||=e=>s(`close`)},{default:R(()=>[h(`div`,tn,[h(`section`,nn,[h(`div`,rn,[h(`h3`,null,e(L(a)(`历史消息`)),1),h(`button`,{id:`loadMessages`,class:`small`,onClick:o[0]||=e=>{s(`load`),A()}},e(L(a)(`↻ 加载`)),1)]),h(`div`,an,[t.events.length?(x(),l(`table`,on,[h(`thead`,null,[h(`tr`,null,[h(`th`,null,e(L(a)(`序号`)),1),h(`th`,null,e(L(a)(`发送者`)),1),h(`th`,null,e(L(a)(`消息`)),1)])]),h(`tbody`,null,[(x(!0),l(r,null,w([...t.events].reverse(),t=>(x(),l(`tr`,{key:t.seq},[h(`td`,null,e(t.seq),1),h(`td`,null,e(t.payload.name),1),h(`td`,null,e(t.payload.text),1)]))),128))])])):(x(),l(`p`,sn,e(L(a)(`当前会话暂无历史消息。`)),1))]),h(`form`,{class:`range`,onSubmit:p(M,[`prevent`])},[h(`label`,null,[S(e(L(a)(`起始序号`)),1),b(h(`input`,{"onUpdate:modelValue":o[1]||=e=>T.value=e,name:`from`,type:`number`,required:``},null,512),[[i,T.value]])]),h(`label`,null,[S(e(L(a)(`截止序号`)),1),b(h(`input`,{"onUpdate:modelValue":o[2]||=e=>E.value=e,name:`to`,type:`number`,required:``},null,512),[[i,E.value]])]),h(`button`,{id:`replay`,class:`primary`,disabled:O.value||!t.events.length},e(O.value?L(a)(`正在回放…`):L(a)(`回放这段对话`)),9,cn),h(`p`,ln,e(L(a)(`隔离回放:只测试回复,不发送 QQ,不修改真实记忆和 TA 的心智。`)),1)],32)]),h(`section`,un,[h(`div`,dn,[h(`h3`,null,e(L(a)(`处理记录`)),1)]),h(`div`,fn,[(x(!0),l(r,null,w(y.value,t=>(x(),l(`button`,{key:t.id,"data-trace":t.id,class:d([`trace-row`,{selected:C.value?.id===t.id}]),onClick:e=>N(t.id)},[h(`span`,null,[h(`time`,null,e(L(q)(t.time)),1),h(`small`,{class:`chip`,"data-tone":t.status===`error`?`danger`:`quiet`},e(L(f)[t.status]||t.status),9,mn)]),h(`p`,null,e(t.reason||t.error||L(a)(`查看处理详情`)),1)],10,pn))),128)),y.value.length?g(``,!0):(x(),l(`p`,hn,e(L(a)(`有新的消息处理记录后会显示在这里。`)),1))])]),h(`section`,gn,[h(`div`,_n,[h(`h3`,null,e(L(a)(`运行详情`)),1)]),h(`div`,vn,[C.value?(x(),l(`div`,yn,[h(`span`,null,[S(e(L(a)(`模式`)),1),h(`b`,null,e(L(v)[C.value.mode]||C.value.mode),1)]),h(`span`,null,[S(e(L(a)(`耗时`)),1),h(`b`,null,e(C.value.data?.elapsed??`—`)+` ms`,1)]),h(`span`,null,[S(e(L(a)(`模型调用`)),1),h(`b`,null,e(L(a)(`{v} 次`,{v:C.value.data?.calls?.length??0})),1)]),h(`span`,bn,[o[4]||=S(`Token`,-1),h(`b`,null,e(P(C.value.data?.tokens)),1)])])):g(``,!0),C.value?(x(),l(`p`,xn,e(C.value.reason||C.value.data?.decision?.reason||L(a)(`展开下方查看上下文、模型输入与原始输出。`)),1)):g(``,!0),h(`pre`,Sn,e(k.value||L(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=A(M({__name:`ChatsPage`,setup(e){let n=ie(`(max-width: 1100px)`),r=o(()=>N.core?.sessions||[]),i=o(()=>r.value.filter(e=>!e.archived)),c=sessionStorage.activeSession,f=u(i.value.some(e=>e.id===c)?c:i.value[0]?.id||``),p=o(()=>i.value.find(e=>e.id===f.value)||null),b=u([]),S=u([]),w=u([]),T=u([]),E=u({}),D=u(``),k=u(!1),A=u(!1),M=u(!1),P=u(N.sub===`settings`),z=u(N.sub===`replay`),B=u(`here`),V=o(()=>N.health?.decisions||[]),H=o(()=>T.value.find(e=>e.session===f.value)||null),U=!1,W=!1,G=!1,K=``,q=0,J=null,Y=0;async function X(e=!1){if(!f.value)return;if(U){W=!0,G||=e;return}let t=f.value,n=e||K!==t;U=!0;try{let[e,r]=await Promise.all([ae(t,n?void 0:q),oe(t,{limit:30,compact:!0})]);if(f.value!==t)return;let i=document.getElementById(`liveMessages`),o=K!==t||!i||i.scrollHeight-i.scrollTop-i.clientHeight<80;n?(b.value=e.reverse(),q=0):e.length&&(b.value=[...b.value,...e]);for(let t of e)q=Math.max(q,Number(t.seq)||0);!n&&e.length===100&&(W=!0),S.value=r,K=t,await y();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(U=!1,f.value!==t)W=!1,X();else if(W){let e=G;W=!1,G=!1,X(e)}}}async function ge(){try{let[e,t]=await Promise.all([te.bonds(),te.overview()]);T.value=e.groups,E.value=Object.fromEntries(t.attention.map(e=>[e.session,e.unread]))}catch{}}_(f,e=>{if(J?.abort(),e&&(sessionStorage.activeSession=e),b.value=[],S.value=[],w.value=[],K=``,q=0,W=!1,G=!1,M.value=!1,X(),!e)return;me(e).then(t=>{f.value===e&&(w.value=[...t].reverse())}).catch(()=>{});let t=new AbortController;J=t;let n=``;re(e=>{let t=Number(e?.messages);Number.isSafeInteger(t)&&t<q&&(K=``,q=0,b.value=[]);let r=JSON.stringify(e),i=n!==``&&r!==n;n=r,i&&X()},{session:e,signal:t.signal,reconnectMs:1e3})},{immediate:!0}),_(i,e=>{e.some(e=>e.id===f.value)||(f.value=e[0]?.id||``)}),_(B,async e=>{if(e===`feedback`)try{N.health=await C()}catch{}}),_(z,e=>j(e?`replay`:P.value?`settings`:``)),_(()=>N.sub,e=>{e===`replay`&&(z.value=!0),e===`settings`&&(P.value=!0)}),_(()=>N.pulse,ge),O(()=>{ge(),Y=window.setInterval(()=>X(!0),15e3)}),ee(()=>{clearInterval(Y),J?.abort()});async function _e(e){try{let t=await le(e);await v(),f.value=t?.sessionId||`${e.kind}:${e.id}`,P.value=!0,B.value=`here`,A.value=!1,I(a(`会话已添加`))}catch(e){I(e.message,!0)}}async function Z(e){await ue(e.id,!e.enabled),await v()}async function ve(e){await de(e,!0),await v()}async function Q(e){await de(e,!1),await v()}async function ye(e){await t(a(`永久删除这个会话?它的消息和设置都会消失。`),{title:a(`永久删除`),confirmText:a(`删除`),danger:!0})&&(await fe(e),await v())}async function be(e){if(p.value)try{await pe(p.value.id,{...e,name:String(e.name||``).trim()||p.value.name}),N.dirty=!1,I(a(`会话配置已保存`)),await v()}catch(e){I(e.message,!0)}}async function xe(){f.value&&await t(a(`确定清空这个会话的消息上下文吗?长期记忆不会删除。`),{title:a(`清空上下文`),confirmText:a(`清空`),danger:!0})&&(await he(f.value),b.value=[],S.value=[],K=``,q=0,await X(!0))}async function Se(e){if(!k.value&&f.value){k.value=!0;try{let t=await se({sessionId:f.value,...e});I(t.reason||a(`已发送`)),N.health=await C(),await X()}catch(e){I(e.message,!0)}finally{k.value=!1}}}async function Ce(e,t){try{await ce(e,t),I(a(`反馈已保存`)),N.health=await C()}catch(e){I(e.message,!0)}}return(e,t)=>(x(),l(`div`,{class:d([`page chats`,{compact:L(n)}])},[L(n)?g(``,!0):(x(),l(`aside`,wn,[F(Oe,{sessions:r.value,selected:f.value,unread:E.value,onSelect:t[0]||=e=>f.value=e,onAdd:t[1]||=e=>A.value=!0,onToggle:Z,onRestore:Q,onRemove:ye},null,8,[`sessions`,`selected`,`unread`])])),h(`div`,Tn,[F(pt,{"session-id":f.value,session:p.value,events:b.value,traces:S.value,decisions:V.value,status:D.value||L(a)(`请选择会话`),demo:!!L(N).health.settings.demo,compact:L(n),sending:k.value,onSimulate:Se,onFeedback:Ce,onOpenList:t[2]||=e=>M.value=!0,onOpenReplay:t[3]||=e=>z.value=!0},null,8,[`session-id`,`session`,`events`,`traces`,`decisions`,`status`,`demo`,`compact`,`sending`])]),h(`aside`,En,[F(en,{"settings-open":P.value,"onUpdate:settingsOpen":t[4]||=e=>P.value=e,tab:B.value,"onUpdate:tab":t[5]||=e=>B.value=e,session:p.value,group:H.value,summaries:w.value,decisions:V.value,onToggle:Z,onArchive:ve,onClear:xe,onSave:be,onFeedback:Ce},null,8,[`settings-open`,`tab`,`session`,`group`,`summaries`,`decisions`])]),L(n)?(x(),m(ne,{key:1,open:M.value,title:L(a)(`全部会话`),eyebrow:`CHATS`,width:`400px`,onClose:t[8]||=e=>M.value=!1},{default:R(()=>[h(`div`,Dn,[F(Oe,{sessions:r.value,selected:f.value,unread:E.value,onSelect:t[6]||=e=>f.value=e,onAdd:t[7]||=e=>A.value=!0,onToggle:Z,onRestore:Q,onRemove:ye},null,8,[`sessions`,`selected`,`unread`])])]),_:1},8,[`open`,`title`])):g(``,!0),F(Ne,{open:A.value,channel:L(N).health?.connection?.channel===`qqbot`?`qqbot`:`onebot`,ready:!!L(N).health?.connection?.qqbot?.appId,onClose:t[9]||=e=>A.value=!1,onAdd:_e},null,8,[`open`,`channel`,`ready`]),F(Cn,{open:z.value,"session-id":f.value,events:b.value,onClose:t[10]||=e=>z.value=!1,onLoad:t[11]||=e=>X(!0)},null,8,[`open`,`session-id`,`events`])],2))}}),[[`__scopeId`,`data-v-c328f9fe`]]);export{On as default};
@@ -1 +0,0 @@
1
- import{Dt as e,Q as t,Tt as n,Y as r,Z as i,dt as a,pt as o,rt as s}from"./Empty-uSyRBXOT.js";var c={key:0},l=s({__name:`Field`,props:{label:{},hint:{},wide:{type:Boolean}},setup(s){return(l,u)=>(a(),t(`label`,{class:n({wide:s.wide})},[r(`span`,null,e(s.label),1),o(l.$slots,`default`),s.hint?(a(),t(`small`,c,e(s.hint),1)):i(``,!0)],2))}});export{l as t};
@@ -1 +0,0 @@
1
- import{$ as e,Ct as t,D as n,Dt as r,E as i,Et as a,G as o,H as s,I as c,J as l,Q as u,St as d,Tt as f,W as p,X as m,Y as h,Z as g,_,_t as v,bt as y,dt as b,et as x,ft as S,lt as C,n as w,o as ee,rt as T,s as E,st as D,t as O,tt as k,v as A,wt as j,yt as te}from"./Empty-uSyRBXOT.js";import{_ as M,c as N,d as P,g as F,h as ne,u as I,v as L}from"./mind-D3mdwFW3.js";import{a as re,b as ie,n as ae,o as oe,r as R,u as z,v as B,x as V}from"./index-5FMs9YYQ.js";import{t as se}from"./media-BsG-i0Lv.js";var H={class:`ribbon`},U={class:`ribbon-head`},W={class:`ribbon-title`},G={class:`eyebrow`},K={key:0,class:`latest-mood`},q=[`viewBox`,`aria-label`],J=[`x2`,`y1`,`y2`],Y=[`d`],ce=[`d`],le=[`d`],ue=[`cx`,`cy`,`r`],de={key:1,class:`muted empty-line`},fe=[`aria-label`],X=150,Z=58,pe=w(T({__name:`MoodRibbon`,props:{moods:{},baseline:{},timeZone:{}},setup(e){let t=e,n=X/2,i=d(),a=d(1e3),s=null;C(()=>{s=new ResizeObserver(([e])=>{a.value=Math.max(240,Math.round(e.contentRect.width))}),i.value&&s.observe(i.value)}),D(()=>s?.disconnect());let p=l(()=>{let e=[...t.moods].sort((e,t)=>e.created-t.created),r=e.length>1?(a.value-60)/(e.length-1):0;return e.map((e,t)=>({...e,x:30+t*r,y:n-Math.max(-1,Math.min(1,e.valence))*Z}))}),m=l(()=>{let e=p.value;if(e.length<2)return``;let t=`M${e[0].x.toFixed(1)},${e[0].y.toFixed(1)}`;for(let n=0;n<e.length-1;n++){let r=e[n-1]||e[n],i=e[n],a=e[n+1],o=e[n+2]||a;t+=`C${(i.x+(a.x-r.x)/6).toFixed(1)},${(i.y+(a.y-r.y)/6).toFixed(1)} ${(a.x-(o.x-i.x)/6).toFixed(1)},${(a.y-(o.y-i.y)/6).toFixed(1)} ${a.x.toFixed(1)},${a.y.toFixed(1)}`}return t}),_=l(()=>{let e=p.value;return m.value?`${m.value}L${e[e.length-1].x},${X}L${e[0].x},${X}Z`:``}),v=l(()=>n-(t.baseline??.15)*Z),y=l(()=>[...t.moods].slice(0,6)),x=l(()=>y.value[0]);return(t,n)=>(b(),u(`section`,H,[h(`div`,U,[h(`div`,W,[h(`span`,G,r(j(c)(`MOOD / 此刻的天气`)),1),h(`h2`,null,r(j(c)(`心情有自己的潮汐。`)),1),h(`p`,null,r(j(c)(`最近 {length} 次被经历牵动;有起伏,也会慢慢平静。`,{length:e.moods.length})),1)]),x.value?(b(),u(`div`,K,[h(`span`,null,r(j(c)(`刚刚留下的心情`)),1),h(`b`,null,r(x.value.feeling),1),h(`small`,null,r(x.value.cause||j(c)(`暂时说不清缘由`)),1)])):g(``,!0)]),h(`div`,{ref_key:`box`,ref:i,class:`ribbon-box`},[m.value?(b(),u(`svg`,{key:0,class:`ribbon-chart`,viewBox:`0 0 ${a.value} ${X}`,role:`img`,"aria-label":j(c)(`最近的心情曲线`)},[h(`defs`,null,[h(`linearGradient`,{id:`ribbon-stroke`,x1:`0`,y1:`0`,x2:`0`,y2:X,gradientUnits:`userSpaceOnUse`},[...n[0]||=[h(`stop`,{offset:`0%`,style:{"stop-color":`var(--cheek)`}},null,-1),h(`stop`,{offset:`50%`,style:{"stop-color":`var(--accent)`}},null,-1),h(`stop`,{offset:`100%`,style:{"stop-color":`var(--orb-c)`}},null,-1)]]),n[1]||=h(`linearGradient`,{id:`ribbon-fill`,x1:`0`,y1:`0`,x2:`0`,y2:`1`},[h(`stop`,{offset:`0%`,style:{"stop-color":`var(--accent)`,"stop-opacity":`0.22`}}),h(`stop`,{offset:`100%`,style:{"stop-color":`var(--accent)`,"stop-opacity":`0`}})],-1)]),h(`line`,{class:`base`,x1:`0`,x2:a.value,y1:v.value,y2:v.value},null,8,J),h(`path`,{class:`area`,d:_.value},null,8,Y),h(`path`,{class:`line-glow`,d:m.value},null,8,ce),h(`path`,{class:`line`,d:m.value},null,8,le),(b(!0),u(o,null,S(p.value,t=>(b(),u(`g`,{key:t.id,class:f([`pt`,t.valence>=0?`warm`:`cool`])},[h(`circle`,{cx:t.x,cy:t.y,r:4+t.intensity*6},[h(`title`,null,r(j(z)(t.created,e.timeZone))+` `+r(t.feeling)+`:`+r(t.cause||j(c)(`说不清为什么`)),1)],8,ue)],2))),128))],8,q)):(b(),u(`p`,de,r(j(c)(`心情还没有被什么事牵动过。每一次经历都可能推动 TA 的心情,随后又会慢慢回到平常。`)),1))],512),y.value.length?(b(),u(`ul`,{key:0,class:`recent`,"aria-label":j(c)(`最近的心情`)},[(b(!0),u(o,null,S(y.value,t=>(b(),u(`li`,{key:t.id,class:f(t.valence>=0?`warm`:`cool`)},[h(`b`,null,r(t.feeling),1),h(`span`,null,r(t.cause||j(c)(`说不清为什么`)),1),h(`small`,null,r(j(z)(t.created,e.timeZone))+` · `+r(j(I)[t.origin]||t.origin),1)],2))),128))],8,fe)):g(``,!0)]))}}),[[`__scopeId`,`data-v-fef06576`]]),me=[`aria-label`],he={class:`self-scene`},ge={class:`scene-copy`},_e={class:`eyebrow`},ve={class:`scene-stats`},ye={class:`self-lens`,"aria-hidden":`true`},be={class:`orb-seat`},xe={class:`currents-head`},Se={class:`current-grid`},Ce={class:`current-head`},we={class:`current-number`},Te={class:`flecks`},Ee=[`data-thread`,`onClick`],De={class:`fleck-copy`},Oe=[`aria-expanded`,`onClick`],ke={"aria-hidden":`true`},Q=6,Ae=w(T({__name:`StarMap`,props:{threads:{},kinds:{},activity:{}},emits:[`open`],setup(t,{emit:n}){let i=t,s=n,p=d({}),m=l(()=>[...new Set(i.threads.map(e=>e.kind))].map(e=>({kind:e,label:i.kinds[e]||e,color:N[e]||`var(--accent)`,items:i.threads.filter(t=>t.kind===e).sort((e,t)=>(t.salience??0)-(e.salience??0))}))),_=l(()=>i.threads.filter(e=>!e.faded&&(e.salience??0)>=.5).length);function v(e){return e.faded?c(`暂时放远`):e.core?c(`慢慢笃定`):c(`还在生长`)}function y(e){return p.value[e.kind]?e.items:e.items.slice(0,Q)}function C(e){p.value={...p.value,[e]:!p.value[e]}}return(n,i)=>(b(),u(`section`,{class:`starmap`,"aria-label":j(c)(`心灵星图:TA 关于自己的线索`)},[h(`div`,he,[h(`div`,ge,[h(`span`,_e,r(j(c)(`SELF / 正在形成的自己`)),1),h(`h2`,null,[x(r(j(c)(`不是一张固定的画像,`)),1),i[0]||=h(`br`,null,null,-1),h(`em`,null,r(j(c)(`而是一直在生长。`)),1)]),h(`p`,null,r(j(c)(`一件经历可能留下一点痕迹。点开一枚光片,可以看到它从哪里来、后来又怎么改变。`)),1),h(`div`,ve,[h(`span`,null,[h(`b`,null,r(t.threads.length),1),x(` `+r(j(c)(`枚线索`)),1)]),h(`span`,null,[h(`b`,null,r(_.value),1),x(` `+r(j(c)(`枚此刻贴近`)),1)])])]),h(`div`,ye,[i[1]||=e(`<span class="lens-halo halo-one" data-v-f3560fca></span><span class="lens-halo halo-two" data-v-f3560fca></span><span class="lens-droplet droplet-one" data-v-f3560fca></span><span class="lens-droplet droplet-two" data-v-f3560fca></span><span class="lens-droplet droplet-three" data-v-f3560fca></span>`,5),h(`div`,be,[k(B,{mood:j(V),activity:t.activity||`idle`,size:106},null,8,[`mood`,`activity`])])])]),h(`div`,xe,[h(`div`,null,[i[2]||=h(`span`,{class:`eyebrow`},`INNER CURRENTS`,-1),h(`h3`,null,r(j(c)(`她留下的光片`)),1)]),h(`p`,null,r(j(c)(`越亮的线索,越贴近此刻。淡去的仍然保留在这里。`)),1)]),h(`div`,Se,[(b(!0),u(o,null,S(m.value,(e,t)=>(b(),u(`section`,{key:e.kind,class:`current`,style:a({"--tone":e.color,"--order":t})},[h(`header`,Ce,[i[3]||=h(`span`,{class:`current-icon`,"aria-hidden":`true`},[h(`i`)],-1),h(`div`,null,[h(`h4`,null,r(e.label),1),h(`small`,null,r(j(c)(`{length} 枚正在留存`,{length:e.items.length})),1)]),h(`span`,we,r(String(t+1).padStart(2,`0`)),1)]),h(`div`,Te,[(b(!0),u(o,null,S(y(e),e=>(b(),u(`button`,{key:e.thread,type:`button`,class:f([`fleck star`,{faded:e.faded,core:e.core}]),"data-thread":e.thread,onClick:t=>s(`open`,e)},[i[4]||=h(`span`,{class:`fleck-light`,"aria-hidden":`true`},null,-1),h(`span`,De,[h(`small`,null,r(v(e)),1),h(`b`,null,r(e.content),1)]),i[5]||=h(`span`,{class:`fleck-arrow`,"aria-hidden":`true`},`↗`,-1)],10,Ee))),128)),e.items.length>Q?(b(),u(`button`,{key:0,type:`button`,class:`fleck-more`,"aria-expanded":!!p.value[e.kind],onClick:t=>C(e.kind)},[x(r(p.value[e.kind]?j(c)(`收起线索`):j(c)(`再看 {v} 枚光片`,{v:e.items.length-Q}))+` `,1),h(`span`,ke,r(p.value[e.kind]?`↑`:`↓`),1)],8,Oe)):g(``,!0)])],4))),128))])],8,me))}}),[[`__scopeId`,`data-v-f3560fca`]]),je={class:`notes-wall`},Me={class:`wall-head`},Ne={class:`eyebrow`},Pe={class:`muted`},Fe=[`placeholder`,`aria-label`],Ie={key:0,class:`wall`},Le=[`data-kind`],Re={key:0,class:`stamp`},ze={key:1,class:`outreach`},Be=[`onClick`],Ve=[`onClick`],He=[`onClick`],Ue=w(T({__name:`NotesWall`,props:{thoughts:{},kinds:{},query:{}},emits:[`changed`,`search`],setup(e,{emit:t}){let n=e,a=t,l=d(n.query);async function _(e,t){try{await F.thought(e.id,t),a(`changed`)}catch(e){A(e.message,!0)}}async function v(e){if(await i(c(`删除这张便签?有后续修正的便签只能隐藏。`),{title:c(`删除便签`),confirmText:c(`删除`),danger:!0}))try{await F.removeThought(e.id),a(`changed`)}catch(e){A(e.message,!0)}}return(t,n)=>(b(),u(`section`,je,[h(`div`,Me,[h(`div`,null,[h(`span`,Ne,r(j(c)(`THOUGHTS / 留给自己的话`)),1),h(`h2`,null,r(j(c)(`有些念头,先放在心上。`)),1),h(`p`,Pe,r(j(c)(`有些是聊天之后的理解,有些是还没想好要告诉谁的念头。可以分享,也可以先留给自己。`)),1)]),h(`form`,{class:`wall-search`,role:`search`,onSubmit:n[2]||=p(e=>a(`search`,l.value),[`prevent`])},[y(h(`input`,{id:`noteSearch`,"onUpdate:modelValue":n[0]||=e=>l.value=e,placeholder:j(c)(`搜索某件事、某个人`),"aria-label":j(c)(`搜索便签`),onChange:n[1]||=e=>a(`search`,l.value)},null,40,Fe),[[s,l.value]])],32)]),e.thoughts.length?(b(),u(`div`,Ie,[(b(!0),u(o,null,S(e.thoughts,t=>(b(),u(`article`,{key:t.id,class:f([`note-card`,{muted:t.hidden,resolved:t.status===`resolved`}]),"data-kind":t.kind},[h(`header`,null,[h(`b`,null,[x(r(e.kinds[t.kind]||t.kind),1),t.parent_id?(b(),u(o,{key:0},[x(r(j(c)(`· 修正了以前的想法`)),1)],64)):g(``,!0)]),h(`time`,null,r(j(L)(t.created)),1)]),h(`p`,null,r(t.content),1),t.status===`resolved`?(b(),u(`p`,Re,r(t.resolution?j(c)(`放下了:{resolution}`,{resolution:t.resolution}):j(c)(`放下了`)),1)):g(``,!0),t.outreach?(b(),u(`p`,ze,[x(r(j(c)(`想主动说:{outreach} · {v}`,{outreach:t.outreach,v:j(P)[t.outreach_status]||t.outreach_status}))+` `,1),t.outreach_reason?(b(),u(o,{key:0},[n[3]||=h(`br`,null,null,-1),x(r(j(c)(`因为:{outreach_reason}`,{outreach_reason:t.outreach_reason})),1)],64)):g(``,!0),t.outreach_wait_reason?(b(),u(o,{key:1},[n[4]||=h(`br`,null,null,-1),x(r(j(c)(`{outreach_wait_reason};稍后重新决定。`,{outreach_wait_reason:t.outreach_wait_reason})),1)],64)):g(``,!0)])):g(``,!0),h(`footer`,null,[h(`button`,{class:`small`,onClick:e=>_(t,{status:t.status===`open`?`resolved`:`open`})},r(t.status===`open`?j(c)(`放下这件事`):j(c)(`重新关注`)),9,Be),h(`button`,{class:`small`,onClick:e=>_(t,{hidden:!t.hidden})},r(t.hidden?j(c)(`恢复`):j(c)(`不再参与 TA 的思考`)),9,Ve),h(`button`,{class:`text-button`,onClick:e=>v(t)},r(j(c)(`删除`)),9,He)])],10,Le))),128))])):(b(),m(O,{key:1,title:j(c)(`这面墙还空着`),text:j(c)(`独处时有了新的理解、放不下的事或久别想起的人,TA 会写一张便签贴在这里。`)},null,8,[`title`,`text`]))]))}}),[[`__scopeId`,`data-v-a56a9d02`]]),We={class:`page heart`},Ge={class:`heart-bar`},Ke={key:0,class:`muted`},qe={key:1,class:`thread-groups`},Je=[`data-thread-kind`],Ye=[`data-thread`,`onClick`],Xe=[`data-tone`],Ze={class:`thread-text`},Qe=[`aria-expanded`,`onClick`],$e={"aria-hidden":`true`},et={key:3,class:`grid-2 set-aside`},tt={class:`card fold`},nt={class:`eyebrow`},rt={key:0,class:`list`},it={class:`chip`,"data-tone":`quiet`},at={class:`grow`},ot={class:`faint`},st={key:1,class:`muted`},ct={class:`card fold`},lt={class:`eyebrow`},ut={key:0,class:`list`},dt={class:`chip`,"data-tone":`danger`},ft={class:`grow`},pt={key:0,class:`faint`},mt={class:`faint`},ht={key:1,class:`muted`},gt={key:0,class:`thread-sheet stack`},_t={class:`thread-content`},vt={class:`row`},yt={class:`chip`},bt={class:`chip`,"data-tone":`quiet`},xt={class:`stack tight`},St={class:`kv`},Ct={class:`sheet-title`},wt={class:`versions`},Tt={class:`faint`},$=4,Et=w(T({__name:`HeartPage`,setup(e){let i=se(`(max-width: 760px)`),s=t(_(`/mind/self`)||null),p=t(_(`/mind/mood`)||null),y=t(_(`/mind/thoughts?q=`)||[]),C=d(``),w=d(E.sub===`notes`?`notes`:E.sub===`list`||i.value?`list`:`stars`),T=d(null),D=d([]),P=l(()=>(s.value?.threads||[]).filter(e=>e.status!==`closed`)),z=d({}),B=l(()=>(s.value?.threads||[]).filter(e=>e.status===`closed`)),V=l(()=>{let e=s.value?.kinds||{};return Object.entries(e).map(([e,t])=>({kind:e,label:t,threads:P.value.filter(t=>t.kind===e)})).filter(e=>e.threads.length)});function H(e){return e.core?c(`TA 最核心的部分`):e.faded?c(`在远方:很久没被触及`):e.fading?c(`慢慢淡出`):ne[e.status]||e.status}function U(e){return z.value[e.kind]?e.threads:e.threads.slice(0,$)}function W(e){z.value={...z.value,[e]:!z.value[e]}}let G=0;async function K(){let e=++G,[t,n,r]=await Promise.all([F.self(),F.mood(),F.notes(C.value)]);e===G&&(s.value=t,p.value=n,y.value=r,T.value&&=t.threads.find(e=>e.thread===T.value.thread)||null)}async function q(e){let t=++G;C.value=e;let n=await F.notes(e);t===G&&(y.value=n)}async function J(e){T.value=e,D.value=[],D.value=await F.thread(e.thread)}async function Y(e){let t=await n(c(`撤销「{content}」?撤销后 TA 不会再这样认为,之后的独处也不会把它写回来。可以写下原因(可不填):`,{content:e.content}),{title:c(`撤销这条线索`),confirmText:c(`撤销`),danger:!0,placeholder:c(`原因`)});if(t!==null)try{await F.revoke(`self`,e.thread,t),A(c(`已撤销`)),T.value=null,await K()}catch(e){A(e.message,!0)}}return v(w,e=>{E.page===`heart`&&ee(e===`stars`?``:e)}),v(()=>[E.page,E.sub],([e,t])=>{e===`heart`&&(t===`notes`||t===`list`?w.value=t:!t&&w.value===`notes`&&(w.value=`stars`))}),re(`heart`,K),(e,t)=>(b(),u(`div`,We,[p.value?(b(),m(pe,{key:0,moods:p.value.moods,baseline:p.value.affect.baseline,"time-zone":p.value.clock.timeZone},null,8,[`moods`,`baseline`,`time-zone`])):g(``,!0),h(`div`,Ge,[k(ae,{modelValue:w.value,"onUpdate:modelValue":t[0]||=e=>w.value=e,label:j(c)(`内心的分区`),items:[{key:`stars`,label:j(c)(`心灵星图`),count:P.value.length},{key:`list`,label:j(c)(`线索清单`)},{key:`notes`,label:j(c)(`放在心上`),count:y.value.length}]},null,8,[`modelValue`,`label`,`items`]),w.value===`notes`?g(``,!0):(b(),u(`p`,Ke,r(j(c)(`她从真实经历里慢慢认识自己。点开一条线索,可以看到它如何变化。`)),1))]),s.value?(b(),u(o,{key:1},[w.value===`stars`?(b(),u(o,{key:0},[P.value.length?(b(),m(Ae,{key:0,threads:P.value,kinds:s.value.kinds,activity:j(ie).data?.activity?.kind,onOpen:J},null,8,[`threads`,`kinds`,`activity`])):(b(),m(O,{key:1,title:j(c)(`TA 还没有长出关于自己的东西`),text:j(c)(`聊得多了、独处过、写过日记,TA 会慢慢发现自己喜欢什么、怎么看事情、想做什么。天性只是种子。`)},null,8,[`title`,`text`]))],64)):w.value===`list`?(b(),u(`section`,qe,[(b(!0),u(o,null,S(V.value,e=>(b(),u(`div`,{key:e.kind,class:`card thread-group`,"data-thread-kind":e.kind},[h(`h2`,null,[h(`i`,{style:a({background:j(N)[e.kind]})},null,4),x(r(e.label)+` · `+r(e.threads.length),1)]),(b(!0),u(o,null,S(U(e),e=>(b(),u(`button`,{key:e.thread,class:f([`thread`,{faded:e.faded,core:e.core}]),"data-thread":e.thread,onClick:t=>J(e)},[h(`span`,{class:`chip`,"data-tone":e.faded||e.fading?`quiet`:void 0},r(H(e)),9,Xe),h(`span`,Ze,r(e.content),1),h(`small`,null,[x(r(j(c)(`强度 {v}`,{v:j(M)(e.strength)})),1),e.salience===null?g(``,!0):(b(),u(o,{key:0},[x(r(j(c)(`· 此刻的分量 {v}`,{v:j(M)(e.salience)})),1)],64))])],10,Ye))),128)),e.threads.length>$?(b(),u(`button`,{key:0,type:`button`,class:`thread-more`,"aria-expanded":!!z.value[e.kind],onClick:t=>W(e.kind)},[x(r(z.value[e.kind]?j(c)(`收起线索`):j(c)(`再看 {v} 条线索`,{v:e.threads.length-$}))+` `,1),h(`span`,$e,r(z.value[e.kind]?`↑`:`↓`),1)],8,Qe)):g(``,!0)],8,Je))),128)),V.value.length?g(``,!0):(b(),m(O,{key:0,title:j(c)(`TA 还没有长出关于自己的东西`),text:j(c)(`天性只是种子,其余的都从经历里来。`)},null,8,[`title`,`text`]))])):(b(),m(Ue,{key:2,thoughts:y.value,kinds:p.value?.kinds?.thoughts||{},query:C.value,onChanged:K,onSearch:q},null,8,[`thoughts`,`kinds`,`query`])),w.value===`notes`?g(``,!0):(b(),u(`div`,et,[h(`details`,tt,[h(`summary`,null,[h(`span`,nt,r(j(c)(`放下`)),1),h(`b`,null,r(j(c)(`TA 自己放下的 · {length}`,{length:B.value.length})),1)]),B.value.length?(b(),u(`ul`,rt,[(b(!0),u(o,null,S(B.value,e=>(b(),u(`li`,{key:e.thread,class:`list-row`},[h(`span`,it,r(j(c)(`放下`)),1),h(`span`,at,r(e.content),1),h(`time`,ot,r(j(L)(e.created)),1)]))),128))])):(b(),u(`p`,st,r(j(c)(`没有。`)),1))]),h(`details`,ct,[h(`summary`,null,[h(`span`,lt,r(j(c)(`撤销`)),1),h(`b`,null,r(j(c)(`你撤销过的 · {length}`,{length:s.value.revoked.length})),1)]),s.value.revoked.length?(b(),u(`ul`,ut,[(b(!0),u(o,null,S(s.value.revoked,e=>(b(),u(`li`,{key:e.id,class:`list-row`},[h(`span`,dt,r(j(c)(`撤销`)),1),h(`span`,ft,[x(r(e.content),1),e.reason?(b(),u(`small`,pt,` · `+r(e.reason),1)):g(``,!0)]),h(`time`,mt,r(j(L)(e.created)),1)]))),128))])):(b(),u(`p`,ht,r(j(c)(`没有。`)),1))])]))],64)):g(``,!0),k(oe,{open:!!T.value,title:T.value?s.value?.kinds?.[T.value.kind]||T.value.kind:``,eyebrow:j(c)(`一颗星`),onClose:t[2]||=e=>T.value=null},{default:te(()=>[T.value?(b(),u(`div`,gt,[h(`p`,_t,r(T.value.content),1),h(`div`,vt,[h(`span`,yt,r(H(T.value)),1),h(`span`,bt,r(j(I)[T.value.origin]||T.value.origin),1)]),h(`div`,xt,[k(R,{label:j(c)(`强度`),value:T.value.strength},null,8,[`label`,`value`]),T.value.salience===null?g(``,!0):(b(),m(R,{key:0,label:j(c)(`此刻的分量`),value:T.value.salience},null,8,[`label`,`value`]))]),h(`dl`,St,[h(`dt`,null,r(j(c)(`经历的天数`)),1),h(`dd`,null,r(j(c)(`{length} 天`,{length:T.value.days.length})),1),h(`dt`,null,r(j(c)(`来源`)),1),h(`dd`,null,r(j(c)(`{length} 处`,{length:T.value.sources.length})),1),h(`dt`,null,r(j(c)(`最近一次被触及`)),1),h(`dd`,null,r(j(L)(T.value.created)),1)]),h(`section`,null,[h(`h3`,Ct,r(j(c)(`怎么变成现在这样的 · {versions} 个版本`,{versions:T.value.versions})),1),h(`ol`,wt,[(b(!0),u(o,null,S(D.value,e=>(b(),u(`li`,{key:e.id},[h(`time`,null,r(j(L)(e.created)),1),h(`b`,null,r(j(M)(e.strength)),1),h(`span`,null,r(e.content),1)]))),128))])]),h(`button`,{class:`danger`,"data-revoke-thread":``,onClick:t[1]||=e=>Y(T.value)},r(j(c)(`撤销这条线索`)),1),h(`p`,Tt,r(j(c)(`撤销后 TA 不会再这样认为;这只是拿走一段理解,不会改写发生过的事。`)),1)])):g(``,!0)]),_:1},8,[`open`,`title`,`eyebrow`])]))}}),[[`__scopeId`,`data-v-f49fd383`]]);export{Et as default};