@hchuanz/pocket-core 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,3487 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/constants/pocket.ts
8
+ var POCKET_API_BASE = "https://pocketapi.48.cn";
9
+ var AVA_BASE_URL = "https://source.48.cn";
10
+ var POCKET_APP_INFO = JSON.stringify({
11
+ vendor: "apple",
12
+ deviceId: "2A01B81B-653A-43EC-9C58-95CB418A58AC",
13
+ appVersion: "7.1.37",
14
+ appBuild: "26020801",
15
+ osVersion: "26.4.0",
16
+ osType: "ios",
17
+ deviceName: "iPhone17,1",
18
+ os: "ios"
19
+ });
20
+ var POCKET_PA = "MTc4MjM3NDEwNjAwMCw3NzE1LEIwODM5QzdEMjZFQzJFOTFERTExNDVERDU2NTlBRjk5LA==";
21
+ var POCKET_USER_AGENT = "PocketFans201807/7.1.37 (iPhone; iOS 26.4; Scale/3.00)";
22
+ var SESSION_EXPIRE_MS = 30 * 24 * 60 * 60 * 1e3;
23
+
24
+ // src/adapters/loggers.ts
25
+ var ConsoleLogger = class {
26
+ debug(...args) {
27
+ console.debug(...args);
28
+ }
29
+ info(...args) {
30
+ console.info(...args);
31
+ }
32
+ warn(...args) {
33
+ console.warn(...args);
34
+ }
35
+ error(...args) {
36
+ console.error(...args);
37
+ }
38
+ };
39
+ var NoopLogger = class {
40
+ debug() {
41
+ }
42
+ info() {
43
+ }
44
+ warn() {
45
+ }
46
+ error() {
47
+ }
48
+ };
49
+
50
+ // src/adapters/node-json-storage.ts
51
+ import fs from "fs";
52
+ import path from "path";
53
+ var NodeJsonFileStorage = class {
54
+ root;
55
+ constructor(root) {
56
+ this.root = root;
57
+ }
58
+ resolve(subpath) {
59
+ return path.join(this.root, subpath);
60
+ }
61
+ readJson(subpath) {
62
+ const fp = this.resolve(subpath);
63
+ if (!fs.existsSync(fp)) return null;
64
+ try {
65
+ return JSON.parse(fs.readFileSync(fp, "utf-8"));
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+ writeJson(subpath, data) {
71
+ const fp = this.resolve(subpath);
72
+ fs.mkdirSync(path.dirname(fp), { recursive: true });
73
+ fs.writeFileSync(fp, JSON.stringify(data, null, 2), "utf-8");
74
+ }
75
+ exists(subpath) {
76
+ return fs.existsSync(this.resolve(subpath));
77
+ }
78
+ delete(subpath) {
79
+ const fp = this.resolve(subpath);
80
+ if (fs.existsSync(fp)) fs.unlinkSync(fp);
81
+ }
82
+ list(dirSubpath) {
83
+ const dir = this.resolve(dirSubpath);
84
+ if (!fs.existsSync(dir)) return [];
85
+ return fs.readdirSync(dir);
86
+ }
87
+ mkdir(dirSubpath) {
88
+ fs.mkdirSync(this.resolve(dirSubpath), { recursive: true });
89
+ }
90
+ };
91
+
92
+ // src/adapters/plain-text-secure-storage.ts
93
+ var PlainTextSecureStorage = class {
94
+ isAvailable() {
95
+ return true;
96
+ }
97
+ encryptString(plain) {
98
+ return Buffer.from(plain, "utf-8").toString("base64");
99
+ }
100
+ decryptString(encoded) {
101
+ return Buffer.from(encoded, "base64").toString("utf-8");
102
+ }
103
+ };
104
+
105
+ // src/adapters/node-json-config-store.ts
106
+ import fs2 from "fs";
107
+ import path2 from "path";
108
+ var NodeJsonConfigStore = class {
109
+ filePath;
110
+ data = {};
111
+ loaded = false;
112
+ constructor(root, filename = "app-store.json") {
113
+ this.filePath = path2.join(root, filename);
114
+ }
115
+ ensureLoaded() {
116
+ if (this.loaded) return;
117
+ this.loaded = true;
118
+ try {
119
+ if (fs2.existsSync(this.filePath)) {
120
+ this.data = JSON.parse(fs2.readFileSync(this.filePath, "utf-8"));
121
+ }
122
+ } catch {
123
+ this.data = {};
124
+ }
125
+ }
126
+ save() {
127
+ fs2.mkdirSync(path2.dirname(this.filePath), { recursive: true });
128
+ fs2.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2), "utf-8");
129
+ }
130
+ get(key) {
131
+ this.ensureLoaded();
132
+ return this.data[key];
133
+ }
134
+ set(key, value) {
135
+ this.ensureLoaded();
136
+ if (value === void 0 || value === null) {
137
+ delete this.data[key];
138
+ } else {
139
+ this.data[key] = value;
140
+ }
141
+ this.save();
142
+ }
143
+ remove(key) {
144
+ this.ensureLoaded();
145
+ delete this.data[key];
146
+ this.save();
147
+ }
148
+ has(key) {
149
+ this.ensureLoaded();
150
+ return key in this.data;
151
+ }
152
+ clear() {
153
+ this.data = {};
154
+ this.save();
155
+ }
156
+ };
157
+
158
+ // src/adapters/memory-config-store.ts
159
+ var MemoryConfigStore = class {
160
+ data = /* @__PURE__ */ new Map();
161
+ get(key) {
162
+ return this.data.get(key);
163
+ }
164
+ set(key, value) {
165
+ this.data.set(key, value);
166
+ }
167
+ remove(key) {
168
+ this.data.delete(key);
169
+ }
170
+ has(key) {
171
+ return this.data.has(key);
172
+ }
173
+ clear() {
174
+ this.data.clear();
175
+ }
176
+ };
177
+
178
+ // src/adapters/node-json-flip-source.ts
179
+ import path3 from "path";
180
+ var CACHE_DIR = "flip-cache";
181
+ var NodeJsonFlipDataSource = class {
182
+ storage;
183
+ readLegacy;
184
+ constructor(storage, readLegacy) {
185
+ this.storage = storage;
186
+ this.readLegacy = readLegacy;
187
+ }
188
+ filePath(userId) {
189
+ return path3.join(CACHE_DIR, `${userId}.json`);
190
+ }
191
+ get(userId) {
192
+ if (!userId) return null;
193
+ const cached = this.storage.readJson(this.filePath(userId));
194
+ if (cached) return cached;
195
+ return this.migrateFromLegacy(userId);
196
+ }
197
+ set(cache) {
198
+ this.storage.writeJson(this.filePath(cache.userId), cache);
199
+ }
200
+ clear(userId) {
201
+ this.storage.delete(this.filePath(userId));
202
+ }
203
+ migrateFromLegacy(userId) {
204
+ if (!this.readLegacy) return null;
205
+ const legacy = this.readLegacy();
206
+ const records = legacy?.allUserData?.[userId];
207
+ if (!Array.isArray(records) || records.length === 0) return null;
208
+ const cache = {
209
+ userId,
210
+ records,
211
+ groupedDataById: legacy?.groupedDataById || {},
212
+ dashboardInfo: legacy?.dashboardInfo || {
213
+ totalFlipCount: records.length,
214
+ runningCount: 0,
215
+ returnedCount: 0,
216
+ costTotal: 0
217
+ },
218
+ meta: { syncedAt: Date.now(), recordCount: records.length }
219
+ };
220
+ this.set(cache);
221
+ return cache;
222
+ }
223
+ };
224
+
225
+ // src/adapters/memory-flip-source.ts
226
+ var MemoryFlipDataSource = class {
227
+ data = /* @__PURE__ */ new Map();
228
+ get(userId) {
229
+ return this.data.get(userId) ?? null;
230
+ }
231
+ set(cache) {
232
+ this.data.set(cache.userId, cache);
233
+ }
234
+ clear(userId) {
235
+ this.data.delete(userId);
236
+ }
237
+ };
238
+
239
+ // src/adapters/openai-compatible-client.ts
240
+ var OpenAICompatibleClient = class {
241
+ logger;
242
+ constructor(logger) {
243
+ this.logger = logger;
244
+ }
245
+ async chatCompletion(config, messages, tools, options) {
246
+ const url = `${config.baseUrl}/chat/completions`;
247
+ const body = {
248
+ model: options?.model || config.chatModel,
249
+ messages,
250
+ stream: false
251
+ };
252
+ if (tools?.length) {
253
+ body.tools = tools;
254
+ body.tool_choice = "auto";
255
+ }
256
+ if (options?.maxTokens) body.max_tokens = options.maxTokens;
257
+ if (options?.temperature !== void 0) body.temperature = options.temperature;
258
+ const resp = await fetch(url, {
259
+ method: "POST",
260
+ headers: {
261
+ "Content-Type": "application/json",
262
+ Authorization: `Bearer ${config.apiKey}`
263
+ },
264
+ body: JSON.stringify(body),
265
+ signal: AbortSignal.timeout(options?.timeoutMs ?? 12e4)
266
+ });
267
+ if (!resp.ok) {
268
+ const text = await resp.text().catch(() => "");
269
+ throw new Error(`LLM ${resp.status}: ${text.slice(0, 200)}`);
270
+ }
271
+ const json = await resp.json();
272
+ const choice = json.choices?.[0];
273
+ return {
274
+ content: choice?.message?.content ?? "",
275
+ toolCalls: choice?.message?.tool_calls
276
+ };
277
+ }
278
+ async chatCompletionStream(config, messages, onChunk, tools, options) {
279
+ const url = `${config.baseUrl}/chat/completions`;
280
+ const body = {
281
+ model: options?.model || config.chatModel,
282
+ messages,
283
+ stream: true,
284
+ stream_options: { include_usage: true }
285
+ };
286
+ if (tools?.length) {
287
+ body.tools = tools;
288
+ body.tool_choice = "auto";
289
+ }
290
+ if (options?.maxTokens) body.max_tokens = options.maxTokens;
291
+ if (options?.temperature !== void 0) body.temperature = options.temperature;
292
+ const resp = await fetch(url, {
293
+ method: "POST",
294
+ headers: {
295
+ "Content-Type": "application/json",
296
+ Authorization: `Bearer ${config.apiKey}`
297
+ },
298
+ body: JSON.stringify(body),
299
+ signal: AbortSignal.timeout(options?.timeoutMs ?? 12e4)
300
+ });
301
+ if (!resp.ok) {
302
+ const text = await resp.text().catch(() => "");
303
+ throw new Error(`LLM stream ${resp.status}: ${text.slice(0, 200)}`);
304
+ }
305
+ const reader = resp.body?.getReader();
306
+ if (!reader) throw new Error("No response body");
307
+ const decoder = new TextDecoder();
308
+ let buffer = "";
309
+ const toolCallMap = /* @__PURE__ */ new Map();
310
+ try {
311
+ while (true) {
312
+ const { done, value } = await reader.read();
313
+ if (done) break;
314
+ buffer += decoder.decode(value, { stream: true });
315
+ const lines = buffer.split("\n");
316
+ buffer = lines.pop() ?? "";
317
+ for (const line of lines) {
318
+ if (!line.startsWith("data: ")) continue;
319
+ const data = line.slice(6).trim();
320
+ if (data === "[DONE]") break;
321
+ try {
322
+ const json = JSON.parse(data);
323
+ const delta = json.choices?.[0]?.delta;
324
+ if (!delta) continue;
325
+ const content = delta.content ?? "";
326
+ for (const tc of delta.tool_calls ?? []) {
327
+ const idx = tc.index ?? 0;
328
+ const existing = toolCallMap.get(idx) ?? { id: "", name: "", arguments: "" };
329
+ if (tc.id) existing.id = tc.id;
330
+ if (tc.function?.name) existing.name += tc.function.name;
331
+ if (tc.function?.arguments) existing.arguments += tc.function.arguments;
332
+ toolCallMap.set(idx, existing);
333
+ }
334
+ onChunk({
335
+ content,
336
+ toolCalls: Array.from(toolCallMap.entries()).map(([index, tc]) => ({ index, ...tc })),
337
+ isDone: false
338
+ });
339
+ } catch {
340
+ }
341
+ }
342
+ }
343
+ } finally {
344
+ reader.releaseLock();
345
+ }
346
+ const finalToolCalls = toolCallMap.size > 0 ? Array.from(toolCallMap.values()).map((tc) => ({
347
+ id: tc.id,
348
+ type: "function",
349
+ function: { name: tc.name, arguments: tc.arguments }
350
+ })) : void 0;
351
+ onChunk({ content: "", toolCalls: [], isDone: true });
352
+ return finalToolCalls;
353
+ }
354
+ async embed(config, texts, model) {
355
+ const url = `${config.baseUrl}/embeddings`;
356
+ const resp = await fetch(url, {
357
+ method: "POST",
358
+ headers: {
359
+ "Content-Type": "application/json",
360
+ Authorization: `Bearer ${config.apiKey}`
361
+ },
362
+ body: JSON.stringify({
363
+ model: model || config.embeddingModel,
364
+ input: texts,
365
+ encoding_format: "float"
366
+ }),
367
+ signal: AbortSignal.timeout(6e4)
368
+ });
369
+ if (!resp.ok) {
370
+ const text = await resp.text().catch(() => "");
371
+ throw new Error(`Embedding ${resp.status}: ${text.slice(0, 200)}`);
372
+ }
373
+ const json = await resp.json();
374
+ return (json.data ?? []).map((d) => d.embedding);
375
+ }
376
+ async testConnection(config) {
377
+ try {
378
+ const result = await this.chatCompletion(
379
+ config,
380
+ [{ role: "user", content: "ping" }],
381
+ void 0,
382
+ { maxTokens: 10, timeoutMs: 15e3 }
383
+ );
384
+ return {
385
+ ok: result.content.length > 0 || result.content.length === 0 && !result.toolCalls,
386
+ message: result.content.slice(0, 100) || "(empty response)"
387
+ };
388
+ } catch (e) {
389
+ this.logger.warn("[OpenAICompatibleClient] testConnection failed:", e);
390
+ return {
391
+ ok: false,
392
+ message: e instanceof Error ? e.message : String(e)
393
+ };
394
+ }
395
+ }
396
+ };
397
+
398
+ // src/adapters/callback-stream-sink.ts
399
+ var CallbackStreamSink = class {
400
+ onChunkCb;
401
+ constructor(cb) {
402
+ this.onChunkCb = cb;
403
+ }
404
+ onChunk(content, isDone, newSessionId) {
405
+ this.onChunkCb(content, isDone, newSessionId);
406
+ }
407
+ };
408
+ var NoopEventSink = class {
409
+ emit() {
410
+ }
411
+ };
412
+
413
+ // src/tools/tool-registry.ts
414
+ var ToolRegistry = class {
415
+ constructor(logger) {
416
+ this.logger = logger;
417
+ }
418
+ logger;
419
+ tools = /* @__PURE__ */ new Map();
420
+ /**
421
+ * Register a tool (platform / agent / user MCP)
422
+ */
423
+ register(tool) {
424
+ if (this.tools.has(tool.function.name)) {
425
+ this.logger.warn(`[toolRegistry] overwriting tool: ${tool.function.name}`);
426
+ }
427
+ this.tools.set(tool.function.name, tool);
428
+ this.logger.info(`[toolRegistry] registered: ${tool.function.name}`);
429
+ }
430
+ /**
431
+ * Bulk register tools from an array
432
+ */
433
+ registerAll(tools) {
434
+ for (const t of tools) this.register(t);
435
+ }
436
+ /**
437
+ * Unregister a tool by name
438
+ */
439
+ unregister(name) {
440
+ this.tools.delete(name);
441
+ }
442
+ /**
443
+ * Unregister all tools with a given prefix (e.g. MCP server name)
444
+ */
445
+ unregisterByPrefix(prefix) {
446
+ for (const name of this.tools.keys()) {
447
+ if (name.startsWith(prefix)) this.tools.delete(name);
448
+ }
449
+ }
450
+ /**
451
+ * Get a single tool by name
452
+ */
453
+ get(name) {
454
+ return this.tools.get(name);
455
+ }
456
+ /**
457
+ * Get all tools in OpenAI function-calling format
458
+ */
459
+ getOpenAIFormat() {
460
+ return Array.from(this.tools.values()).map((t) => ({
461
+ type: "function",
462
+ function: { ...t.function }
463
+ }));
464
+ }
465
+ /**
466
+ * Get tools filtered by category
467
+ */
468
+ getByCategory(category) {
469
+ return Array.from(this.tools.values()).filter((t) => t.category === category).map((t) => ({ type: "function", function: { ...t.function } }));
470
+ }
471
+ /**
472
+ * Execute a tool by name
473
+ */
474
+ async execute(name, args, context) {
475
+ const tool = this.tools.get(name);
476
+ if (!tool) throw new Error(`Tool not found: ${name}`);
477
+ return tool.handler(args, context);
478
+ }
479
+ /**
480
+ * List all registered tool names
481
+ */
482
+ listNames() {
483
+ return Array.from(this.tools.keys());
484
+ }
485
+ /**
486
+ * Clear all tools
487
+ */
488
+ clear() {
489
+ this.tools.clear();
490
+ }
491
+ };
492
+
493
+ // src/types/mirror.ts
494
+ function normalizePersona(persona) {
495
+ const p = persona || {};
496
+ const rel = p.relationship;
497
+ return {
498
+ personalityTraits: Array.isArray(p.personalityTraits) ? p.personalityTraits : [],
499
+ speechPatterns: Array.isArray(p.speechPatterns) ? p.speechPatterns : [],
500
+ signaturePhrases: Array.isArray(p.signaturePhrases) ? p.signaturePhrases : [],
501
+ emotionalTone: typeof p.emotionalTone === "string" ? p.emotionalTone : "\u6E29\u67D4\u771F\u8BDA",
502
+ personaSummary: typeof p.personaSummary === "string" ? p.personaSummary : "",
503
+ ...rel ? {
504
+ relationship: {
505
+ dynamic: typeof rel.dynamic === "string" ? rel.dynamic : "\u53CB\u597D",
506
+ closeness: typeof rel.closeness === "number" ? rel.closeness : 0.5,
507
+ howSheAddressesMe: typeof rel.howSheAddressesMe === "string" ? rel.howSheAddressesMe : "\u4F60",
508
+ topicsWeDiscuss: Array.isArray(rel.topicsWeDiscuss) ? rel.topicsWeDiscuss : [],
509
+ evolution: typeof rel.evolution === "string" ? rel.evolution : ""
510
+ }
511
+ } : {},
512
+ ...p.recentActivity ? {
513
+ recentActivity: {
514
+ topics: Array.isArray(p.recentActivity.topics) ? p.recentActivity.topics : [],
515
+ mood: typeof p.recentActivity.mood === "string" ? p.recentActivity.mood : "",
516
+ events: Array.isArray(p.recentActivity.events) ? p.recentActivity.events : []
517
+ }
518
+ } : {}
519
+ };
520
+ }
521
+
522
+ // src/mirror/mirror-store.ts
523
+ var MIRROR_DIR = "mirrors";
524
+ var CHAT_DIR = "chat_sessions";
525
+ function mirrorSubdir(userId) {
526
+ return `${MIRROR_DIR}/${userId}`;
527
+ }
528
+ function chatSubdir(userId) {
529
+ return `${CHAT_DIR}/${userId}`;
530
+ }
531
+ function profileSubpath(xoxId, userId) {
532
+ return `${MIRROR_DIR}/${userId}/${xoxId}.json`;
533
+ }
534
+ function sessionSubpath(sessionId, userId) {
535
+ return `${CHAT_DIR}/${userId}/${sessionId}.json`;
536
+ }
537
+ function normalizeProfile(raw) {
538
+ return {
539
+ ...raw,
540
+ persona: normalizePersona(raw.persona),
541
+ previousPersona: raw.previousPersona ? normalizePersona(raw.previousPersona) : void 0,
542
+ fewShotExamples: Array.isArray(raw.fewShotExamples) ? raw.fewShotExamples : [],
543
+ meta: raw.meta || { totalSamplesUsed: 0, lastUpdatedAt: 0, dataRangeMs: [0, 0], maturityScore: 0 }
544
+ };
545
+ }
546
+ var MirrorStore = class {
547
+ constructor(storage, logger) {
548
+ this.storage = storage;
549
+ this.logger = logger;
550
+ }
551
+ storage;
552
+ logger;
553
+ // ═══ Mirror Profile CRUD ═══
554
+ getProfile(xoxId, userId) {
555
+ const raw = this.storage.readJson(profileSubpath(xoxId, userId));
556
+ if (!raw) return null;
557
+ try {
558
+ return normalizeProfile(raw);
559
+ } catch (e) {
560
+ this.logger.warn(`[mirrorStore] read profile failed for ${xoxId}`, e);
561
+ return null;
562
+ }
563
+ }
564
+ saveProfile(profile, userId) {
565
+ this.storage.mkdir(mirrorSubdir(userId));
566
+ this.storage.writeJson(profileSubpath(profile.xoxId, userId), profile);
567
+ this.logger.info(`[mirrorStore] saved profile for ${profile.xoxNickname}(${profile.xoxId})`);
568
+ }
569
+ deleteProfile(xoxId, userId) {
570
+ this.storage.delete(profileSubpath(xoxId, userId));
571
+ const cd = chatSubdir(userId);
572
+ if (this.storage.exists(cd)) {
573
+ const files = this.storage.list(cd);
574
+ for (const f of files) {
575
+ try {
576
+ const session = this.storage.readJson(`${cd}/${f}`);
577
+ if (session?.xoxId === xoxId) {
578
+ this.storage.delete(`${cd}/${f}`);
579
+ }
580
+ } catch {
581
+ }
582
+ }
583
+ }
584
+ }
585
+ listProfiles(userId) {
586
+ const dir = mirrorSubdir(userId);
587
+ if (!this.storage.exists(dir)) return [];
588
+ const profiles = [];
589
+ const files = this.storage.list(dir);
590
+ for (const f of files) {
591
+ if (!f.endsWith(".json")) continue;
592
+ if (f.includes("_index")) continue;
593
+ if (f.includes("_memory")) continue;
594
+ try {
595
+ const profile = this.storage.readJson(`${dir}/${f}`);
596
+ if (profile) profiles.push(normalizeProfile(profile));
597
+ } catch {
598
+ }
599
+ }
600
+ return profiles;
601
+ }
602
+ // ═══ Chat Session CRUD ═══
603
+ getSession(sessionId, userId) {
604
+ return this.storage.readJson(sessionSubpath(sessionId, userId));
605
+ }
606
+ saveSession(session, userId) {
607
+ this.storage.mkdir(chatSubdir(userId));
608
+ this.storage.writeJson(sessionSubpath(session.sessionId, userId), session);
609
+ }
610
+ /**
611
+ * Find the most recently active chat session for an idol
612
+ */
613
+ getLatestSessionForXox(xoxId, userId) {
614
+ const cd = chatSubdir(userId);
615
+ if (!this.storage.exists(cd)) return null;
616
+ let latest = null;
617
+ const files = this.storage.list(cd);
618
+ for (const f of files) {
619
+ if (!f.endsWith(".json")) continue;
620
+ try {
621
+ const session = this.storage.readJson(`${cd}/${f}`);
622
+ if (session && session.xoxId === xoxId) {
623
+ if (!latest || session.lastActiveAt > latest.lastActiveAt) {
624
+ latest = session;
625
+ }
626
+ }
627
+ } catch {
628
+ }
629
+ }
630
+ return latest;
631
+ }
632
+ deleteSession(sessionId, userId) {
633
+ this.storage.delete(sessionSubpath(sessionId, userId));
634
+ }
635
+ /**
636
+ * Delete all chat sessions for an idol (keeps profile)
637
+ */
638
+ deleteSessionsForXox(xoxId, userId) {
639
+ const cd = chatSubdir(userId);
640
+ if (!this.storage.exists(cd)) return;
641
+ const files = this.storage.list(cd);
642
+ for (const f of files) {
643
+ try {
644
+ const session = this.storage.readJson(`${cd}/${f}`);
645
+ if (session?.xoxId === xoxId) {
646
+ this.storage.delete(`${cd}/${f}`);
647
+ }
648
+ } catch {
649
+ }
650
+ }
651
+ }
652
+ };
653
+
654
+ // src/mirror/mirror-index.ts
655
+ import crypto from "crypto";
656
+ var INDEX_DIR = "mirrors";
657
+ var EMBED_BATCH_SIZE = 32;
658
+ function indexPath(xoxId) {
659
+ return `${INDEX_DIR}/${xoxId}_index.json`;
660
+ }
661
+ function hashRecord(content, answerContent) {
662
+ return crypto.createHash("md5").update(`${content}||${answerContent}`).digest("hex").slice(0, 12);
663
+ }
664
+ function cosine(a, b) {
665
+ let dot = 0;
666
+ let na = 0;
667
+ let nb = 0;
668
+ for (let i = 0; i < a.length; i++) {
669
+ dot += a[i] * b[i];
670
+ na += a[i] * a[i];
671
+ nb += b[i] * b[i];
672
+ }
673
+ if (na === 0 || nb === 0) return 0;
674
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
675
+ }
676
+ var MirrorIndex = class {
677
+ constructor(storage, logger, llmClient) {
678
+ this.storage = storage;
679
+ this.logger = logger;
680
+ this.llmClient = llmClient;
681
+ }
682
+ storage;
683
+ logger;
684
+ llmClient;
685
+ /**
686
+ * Load index from disk (returns null if not exists)
687
+ */
688
+ load(xoxId) {
689
+ const fp = indexPath(xoxId);
690
+ const raw = this.storage.readJson(fp);
691
+ if (!raw) return null;
692
+ return raw;
693
+ }
694
+ /**
695
+ * Build / incrementally update the vector index for an idol's flip records.
696
+ * Records with unchanged hash reuse existing embeddings.
697
+ */
698
+ async buildIndex(xoxId, config, records) {
699
+ const existing = this.load(xoxId);
700
+ const existingMap = /* @__PURE__ */ new Map();
701
+ if (existing && existing.model === config.embeddingModel) {
702
+ for (const v of existing.vectors) existingMap.set(v.hash, v);
703
+ }
704
+ const toEmbed = [];
705
+ const reused = [];
706
+ for (const r of records) {
707
+ const content = String(r.content || "").trim();
708
+ const answer = String(r.answerContent || "").trim();
709
+ if (!content && !answer) continue;
710
+ const hash = hashRecord(content, answer);
711
+ const cached = existingMap.get(hash);
712
+ if (cached) {
713
+ reused.push(cached);
714
+ } else {
715
+ toEmbed.push({
716
+ rec: {
717
+ answerId: String(r.answerId || ""),
718
+ content,
719
+ answerContent: answer,
720
+ qtime: Number(r.qtime) || 0
721
+ },
722
+ hash,
723
+ text: `\u95EE\uFF1A${content} \u7B54\uFF1A${answer}`
724
+ });
725
+ }
726
+ }
727
+ const newVectors = [];
728
+ for (let i = 0; i < toEmbed.length; i += EMBED_BATCH_SIZE) {
729
+ const batch = toEmbed.slice(i, i + EMBED_BATCH_SIZE);
730
+ const texts = batch.map((b) => b.text);
731
+ const vectors = await this.llmClient.embed(config, texts);
732
+ batch.forEach((b, j) => {
733
+ newVectors.push({ ...b.rec, hash: b.hash, vector: vectors[j] || [] });
734
+ });
735
+ }
736
+ const indexFile = {
737
+ xoxId,
738
+ model: config.embeddingModel,
739
+ updatedAt: Date.now(),
740
+ vectors: [...reused, ...newVectors]
741
+ };
742
+ this.storage.mkdir(INDEX_DIR);
743
+ this.storage.writeJson(indexPath(xoxId), indexFile);
744
+ this.logger.info(`[mirrorIndex] built index for ${xoxId}: ${newVectors.length} new, ${reused.length} reused`);
745
+ return { indexed: newVectors.length, reused: reused.length };
746
+ }
747
+ /**
748
+ * Delete index for an idol
749
+ */
750
+ deleteIndex(xoxId) {
751
+ this.storage.delete(indexPath(xoxId));
752
+ }
753
+ /**
754
+ * Search with cosine top-K + MMR dedup
755
+ */
756
+ async search(xoxId, config, query, topK = 5) {
757
+ const index = this.load(xoxId);
758
+ if (!index || index.vectors.length === 0) return [];
759
+ const [qv] = await this.llmClient.embed(config, [query]);
760
+ if (!qv) return [];
761
+ const scored = index.vectors.map((v) => ({ record: v, score: cosine(qv, v.vector) })).sort((a, b) => b.score - a.score).slice(0, Math.min(topK * 3, index.vectors.length));
762
+ const selected = [];
763
+ const LAMBDA = 0.75;
764
+ const pool = [...scored];
765
+ while (selected.length < topK && pool.length > 0) {
766
+ let best = null;
767
+ let bestScore = -Infinity;
768
+ for (const cand of pool) {
769
+ let maxSim = 0;
770
+ for (const sel of selected) {
771
+ const sim = cosine(cand.record.vector, sel.record.vector);
772
+ if (sim > maxSim) maxSim = sim;
773
+ }
774
+ const mmr = LAMBDA * cand.score - (1 - LAMBDA) * maxSim;
775
+ if (mmr > bestScore) {
776
+ bestScore = mmr;
777
+ best = cand;
778
+ }
779
+ }
780
+ if (!best) break;
781
+ selected.push(best);
782
+ pool.splice(pool.indexOf(best), 1);
783
+ }
784
+ return selected;
785
+ }
786
+ /**
787
+ * Get index stats
788
+ */
789
+ getStats(xoxId) {
790
+ const index = this.load(xoxId);
791
+ if (!index) return null;
792
+ return { total: index.vectors.length, updatedAt: index.updatedAt };
793
+ }
794
+ };
795
+
796
+ // src/mirror/mirror-memory.ts
797
+ var MEMORY_DIR = "mirrors";
798
+ var MAX_EPISODES = 50;
799
+ function memoryPath(xoxId) {
800
+ return `${MEMORY_DIR}/${xoxId}_memory.json`;
801
+ }
802
+ var MirrorMemory = class {
803
+ constructor(storage, logger, llmClient) {
804
+ this.storage = storage;
805
+ this.logger = logger;
806
+ this.llmClient = llmClient;
807
+ }
808
+ storage;
809
+ logger;
810
+ llmClient;
811
+ ensureDir() {
812
+ if (!this.storage.exists(MEMORY_DIR)) {
813
+ this.storage.mkdir(MEMORY_DIR);
814
+ }
815
+ }
816
+ /**
817
+ * Load memory file
818
+ */
819
+ load(xoxId) {
820
+ const mp = memoryPath(xoxId);
821
+ const raw = this.storage.readJson(mp);
822
+ if (raw) return raw;
823
+ return { xoxId, totalTurns: 0, episodes: [], updatedAt: Date.now() };
824
+ }
825
+ save(mem) {
826
+ this.ensureDir();
827
+ mem.updatedAt = Date.now();
828
+ this.storage.writeJson(memoryPath(mem.xoxId), mem);
829
+ }
830
+ /**
831
+ * Increment turn counter (called after each chat round)
832
+ */
833
+ addTurn(xoxId) {
834
+ const mem = this.load(xoxId);
835
+ mem.totalTurns += 1;
836
+ this.save(mem);
837
+ return mem.totalTurns;
838
+ }
839
+ getTotalTurns(xoxId) {
840
+ return this.load(xoxId).totalTurns;
841
+ }
842
+ /**
843
+ * Summarize a batch of conversation messages into an episodic memory entry.
844
+ * Uses LLM to extract topic + summary.
845
+ */
846
+ async recordEpisode(xoxId, config, messages) {
847
+ if (messages.length < 4) return null;
848
+ const convText = messages.slice(-30).map((m) => `${m.role === "user" ? "\u7C89\u4E1D" : "\u5076\u50CF"}: ${m.content}`).join("\n");
849
+ const systemPrompt = `\u4F60\u662F\u5BF9\u8BDD\u8BB0\u5FC6\u538B\u7F29\u5668\u3002\u8BF7\u628A\u4EE5\u4E0B\u5BF9\u8BDD\u538B\u7F29\u6210\u4E00\u6761\u8BB0\u5FC6\u6761\u76EE\uFF0C\u8F93\u51FA JSON:
850
+ {"topic": "\u8BDD\u9898\u5173\u952E\u8BCD(2-6\u5B57)", "summary": "30-80\u5B57\u7684\u5BF9\u8BDD\u6458\u8981\uFF0C\u5305\u542B\u5173\u952E\u4E8B\u5B9E\u3001\u60C5\u7EEA\u548C\u7EA6\u5B9A"}
851
+ \u53EA\u8F93\u51FA JSON\uFF0C\u4E0D\u8981\u5176\u4ED6\u5185\u5BB9\u3002`;
852
+ try {
853
+ const resp = await this.llmClient.chatCompletion(
854
+ config,
855
+ [
856
+ { role: "system", content: systemPrompt },
857
+ { role: "user", content: convText }
858
+ ],
859
+ void 0,
860
+ { model: config.lightweightModel }
861
+ // use lightweight model for summarization
862
+ );
863
+ const jsonMatch = resp.content.match(/\{[\s\S]*\}/);
864
+ if (!jsonMatch) return null;
865
+ const parsed = JSON.parse(jsonMatch[0]);
866
+ const entry = {
867
+ id: `${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
868
+ topic: parsed.topic || "\u672A\u5206\u7C7B",
869
+ summary: parsed.summary || "",
870
+ turns: Math.floor(messages.length / 2),
871
+ createdAt: Date.now()
872
+ };
873
+ const mem = this.load(xoxId);
874
+ mem.episodes.push(entry);
875
+ if (mem.episodes.length > MAX_EPISODES) {
876
+ mem.episodes = mem.episodes.slice(-MAX_EPISODES);
877
+ }
878
+ this.save(mem);
879
+ return entry;
880
+ } catch (e) {
881
+ this.logger.warn("[mirrorMemory] recordEpisode failed:", e);
882
+ return null;
883
+ }
884
+ }
885
+ /**
886
+ * Get recent episodes for prompt injection
887
+ */
888
+ getRecentEpisodes(xoxId, count = 3) {
889
+ return this.load(xoxId).episodes.slice(-count);
890
+ }
891
+ /**
892
+ * Search episodes by keyword (simple substring match on topic + summary)
893
+ */
894
+ searchEpisodes(xoxId, keyword, count = 3) {
895
+ const kw = keyword.toLowerCase();
896
+ const mem = this.load(xoxId);
897
+ return mem.episodes.filter((e) => e.topic.toLowerCase().includes(kw) || e.summary.toLowerCase().includes(kw)).slice(-count);
898
+ }
899
+ /**
900
+ * Clear all episodic memory (keeps totalTurns for maturity continuity)
901
+ */
902
+ clearEpisodes(xoxId) {
903
+ const mem = this.load(xoxId);
904
+ mem.episodes = [];
905
+ this.save(mem);
906
+ }
907
+ /**
908
+ * Delete memory file entirely
909
+ */
910
+ deleteMemory(xoxId) {
911
+ this.storage.delete(memoryPath(xoxId));
912
+ }
913
+ getStats(xoxId) {
914
+ const mem = this.load(xoxId);
915
+ return { totalTurns: mem.totalTurns, episodeCount: mem.episodes.length, updatedAt: mem.updatedAt };
916
+ }
917
+ };
918
+
919
+ // src/mirror/group-chat/store.ts
920
+ var GROUP_SESSIONS_DIR = "group_sessions";
921
+ function sessionSubpath2(sessionId) {
922
+ return `${GROUP_SESSIONS_DIR}/session_${sessionId}.json`;
923
+ }
924
+ var GroupChatStore = class {
925
+ constructor(storage, logger) {
926
+ this.storage = storage;
927
+ this.logger = logger;
928
+ }
929
+ storage;
930
+ logger;
931
+ ensureDir() {
932
+ if (!this.storage.exists(GROUP_SESSIONS_DIR)) {
933
+ this.storage.mkdir(GROUP_SESSIONS_DIR);
934
+ }
935
+ }
936
+ saveSession(session) {
937
+ this.ensureDir();
938
+ this.storage.writeJson(sessionSubpath2(session.sessionId), session);
939
+ }
940
+ getSession(sessionId) {
941
+ const session = this.storage.readJson(sessionSubpath2(sessionId));
942
+ if (!session) return null;
943
+ return session;
944
+ }
945
+ listSessions() {
946
+ if (!this.storage.exists(GROUP_SESSIONS_DIR)) return [];
947
+ const sessions = [];
948
+ try {
949
+ const files = this.storage.list(GROUP_SESSIONS_DIR);
950
+ for (const f of files) {
951
+ if (!f.endsWith(".json")) continue;
952
+ try {
953
+ const session = this.storage.readJson(`${GROUP_SESSIONS_DIR}/${f}`);
954
+ if (session) sessions.push(session);
955
+ } catch {
956
+ }
957
+ }
958
+ } catch {
959
+ }
960
+ return sessions;
961
+ }
962
+ deleteSession(sessionId) {
963
+ this.storage.delete(sessionSubpath2(sessionId));
964
+ }
965
+ /**
966
+ * Append a message to a session (read → push → write back)
967
+ */
968
+ appendMessage(sessionId, msg) {
969
+ const session = this.storage.readJson(sessionSubpath2(sessionId));
970
+ if (!session) return;
971
+ session.messages.push(msg);
972
+ session.lastActiveAt = Date.now();
973
+ this.storage.writeJson(sessionSubpath2(sessionId), session);
974
+ }
975
+ };
976
+
977
+ // src/flip/flip-cache-service.ts
978
+ var CACHE_DIR2 = "flip-cache";
979
+ function cacheSubpath(userId) {
980
+ return `${CACHE_DIR2}/${userId}.json`;
981
+ }
982
+ var FlipCacheService = class _FlipCacheService {
983
+ constructor(storage, logger, configStore) {
984
+ this.storage = storage;
985
+ this.logger = logger;
986
+ this.configStore = configStore;
987
+ }
988
+ storage;
989
+ logger;
990
+ configStore;
991
+ /** 计算仪表盘数据(纯逻辑) */
992
+ static calcDashboard(records) {
993
+ const info = { totalFlipCount: records.length, runningCount: 0, returnedCount: 0, costTotal: 0 };
994
+ for (const card of records) {
995
+ if (card.status === 3) info.returnedCount += 1;
996
+ else if (card.status === 1) info.runningCount += 1;
997
+ else if (card.status === 2) info.costTotal += Number(card.cost) || 0;
998
+ }
999
+ return info;
1000
+ }
1001
+ /** 按偶像分组(纯逻辑) */
1002
+ static groupDataById(records) {
1003
+ const groupData = {};
1004
+ for (const item of records) {
1005
+ const base = item.baseUserInfo;
1006
+ const xoxId = base?.userId;
1007
+ if (!xoxId) continue;
1008
+ const cardInfo = {
1009
+ answerId: item.answerId || "",
1010
+ cost: item.cost || 0,
1011
+ answerType: item.answerType || 0,
1012
+ answerTime: item.answerTime || "",
1013
+ qtime: item.qtime || "",
1014
+ type: item.type || 0
1015
+ };
1016
+ const key = String(xoxId);
1017
+ if (key in groupData) {
1018
+ groupData[key].cards.push(cardInfo);
1019
+ } else {
1020
+ groupData[key] = {
1021
+ xoxId,
1022
+ xoxNickname: base?.nickname ?? "",
1023
+ cards: [cardInfo]
1024
+ };
1025
+ }
1026
+ }
1027
+ return groupData;
1028
+ }
1029
+ writeCache(cache) {
1030
+ this.storage.mkdir(CACHE_DIR2);
1031
+ this.storage.writeJson(cacheSubpath(cache.userId), cache);
1032
+ }
1033
+ /** 从旧版 storeService 迁移数据 */
1034
+ migrateFromLegacy(userId) {
1035
+ const allUserData = this.configStore.get("allUserData");
1036
+ const records = allUserData?.[userId];
1037
+ if (!Array.isArray(records) || records.length === 0) return null;
1038
+ const legacyGrouped = this.configStore.get("groupedDataById");
1039
+ const legacyDashboard = this.configStore.get("dashboardInfo");
1040
+ const cache = {
1041
+ userId,
1042
+ records,
1043
+ groupedDataById: legacyGrouped && Object.keys(legacyGrouped).length > 0 ? legacyGrouped : _FlipCacheService.groupDataById(records),
1044
+ dashboardInfo: legacyDashboard?.totalFlipCount ? legacyDashboard : _FlipCacheService.calcDashboard(records),
1045
+ meta: { syncedAt: Date.now(), recordCount: records.length }
1046
+ };
1047
+ this.writeCache(cache);
1048
+ this.logger.info(`[flipCacheService] migrated ${records.length} records for user ${userId}`);
1049
+ return cache;
1050
+ }
1051
+ get(userId) {
1052
+ if (!userId) return null;
1053
+ const cached = this.storage.readJson(cacheSubpath(userId));
1054
+ if (cached) return cached;
1055
+ return this.migrateFromLegacy(userId);
1056
+ }
1057
+ set(cache) {
1058
+ cache.meta = { syncedAt: Date.now(), recordCount: cache.records.length };
1059
+ this.writeCache(cache);
1060
+ }
1061
+ clear(userId) {
1062
+ this.storage.delete(cacheSubpath(userId));
1063
+ }
1064
+ };
1065
+
1066
+ // src/pocket/client.ts
1067
+ import axios from "axios";
1068
+ var PocketAuthExpiredError = class extends Error {
1069
+ constructor() {
1070
+ super("\u767B\u5F55\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55");
1071
+ this.name = "PocketAuthExpiredError";
1072
+ }
1073
+ };
1074
+ var PocketClient = class {
1075
+ constructor(baseUrl, appInfo, pa, authExpiredHandler) {
1076
+ this.baseUrl = baseUrl;
1077
+ this.appInfo = appInfo;
1078
+ this.pa = pa;
1079
+ this.authExpiredHandler = authExpiredHandler;
1080
+ }
1081
+ baseUrl;
1082
+ appInfo;
1083
+ pa;
1084
+ authExpiredHandler;
1085
+ lastAuthWarnAt = 0;
1086
+ lastKickAt = 0;
1087
+ /** 登录态过期的单一出口:防抖调用 authExpiredHandler + 抛错 */
1088
+ kickToLogin() {
1089
+ const now = Date.now();
1090
+ if (now - this.lastKickAt < 2e3) return;
1091
+ this.lastKickAt = now;
1092
+ if (now - this.lastAuthWarnAt > 5e3) {
1093
+ this.lastAuthWarnAt = now;
1094
+ }
1095
+ this.authExpiredHandler?.onAuthExpired();
1096
+ }
1097
+ /** 检查响应是否 auth 过期 */
1098
+ checkAuthExpired(res) {
1099
+ if (res?.status === 401004) {
1100
+ this.kickToLogin();
1101
+ throw new PocketAuthExpiredError();
1102
+ }
1103
+ }
1104
+ buildSignedHeaders(token) {
1105
+ const headers = {
1106
+ "content-type": "application/json;charset=utf-8",
1107
+ appinfo: this.appInfo,
1108
+ accept: "*/*",
1109
+ pa: this.pa,
1110
+ "accept-language": "zh-Hans-CN;q=1",
1111
+ "p-sign-type": "V0"
1112
+ };
1113
+ if (token) headers.token = token;
1114
+ return headers;
1115
+ }
1116
+ buildPlainHeaders() {
1117
+ return {
1118
+ "Content-Type": "application/json;charset=utf-8",
1119
+ Accept: "*/*",
1120
+ pa: this.pa,
1121
+ "Accept-Language": "zh-Hans-CN;q=1",
1122
+ appInfo: this.appInfo
1123
+ };
1124
+ }
1125
+ async request(path4, data, token, signed = true) {
1126
+ const url = path4.startsWith("http") ? path4 : `${this.baseUrl}${path4}`;
1127
+ const headers = signed || token ? this.buildSignedHeaders(token) : this.buildPlainHeaders();
1128
+ const res = await axios.post(url, data, { headers });
1129
+ if (signed) this.checkAuthExpired(res.data);
1130
+ return res.data;
1131
+ }
1132
+ /** 登录前接口调用(不签名,不检测 auth 过期) */
1133
+ async requestBeforeLogin(path4, data) {
1134
+ return this.request(path4, data, void 0, false);
1135
+ }
1136
+ };
1137
+
1138
+ // src/mirror/mirror-constants.ts
1139
+ var MIRROR_PRESETS = {
1140
+ /** Default — fast, cheap, good for most users */
1141
+ economy: {
1142
+ llmSampleSize: 800,
1143
+ llmMaxTokens: 4e3,
1144
+ analysisTemperature: 0.4,
1145
+ chatHistoryRounds: 30,
1146
+ chatTemperature: 0.7,
1147
+ ragTopK: 8,
1148
+ ragMinScore: 0.35,
1149
+ memoryEpisodes: 5,
1150
+ embeddingMaxRecords: 1e3
1151
+ },
1152
+ /** High quality — more data, deeper analysis, higher cost */
1153
+ performance: {
1154
+ llmSampleSize: 2e3,
1155
+ llmMaxTokens: 8e3,
1156
+ analysisTemperature: 0.5,
1157
+ chatHistoryRounds: 80,
1158
+ chatTemperature: 0.9,
1159
+ ragTopK: 15,
1160
+ ragMinScore: 0.25,
1161
+ memoryEpisodes: 15,
1162
+ embeddingMaxRecords: 5e3
1163
+ }
1164
+ };
1165
+ function getMirrorConfig(mode, overrides) {
1166
+ const base = mode === "performance" ? MIRROR_PRESETS.performance : MIRROR_PRESETS.economy;
1167
+ return {
1168
+ ...base,
1169
+ ...overrides?.analysisTemperature != null ? { analysisTemperature: overrides.analysisTemperature } : {},
1170
+ ...overrides?.chatTemperature != null ? { chatTemperature: overrides.chatTemperature } : {}
1171
+ };
1172
+ }
1173
+
1174
+ // src/mirror/mirror-builder.ts
1175
+ async function buildMirror(xoxId, userId, config, params) {
1176
+ const { logger, llmClient, flipDataSource, mirrorStore, mirrorIndex } = params;
1177
+ const mc = getMirrorConfig(config.mirrorMode, {
1178
+ analysisTemperature: config.analysisTemperature,
1179
+ chatTemperature: config.chatTemperature
1180
+ });
1181
+ const cache = flipDataSource.get(userId);
1182
+ if (!cache) {
1183
+ return { ok: false, message: "\u672A\u627E\u5230\u7FFB\u724C\u6570\u636E\uFF0C\u8BF7\u5148\u540C\u6B65\u7FFB\u724C\u8BB0\u5F55" };
1184
+ }
1185
+ const xoxRecords = cache.records.filter((r) => {
1186
+ const base = r.baseUserInfo;
1187
+ if (base?.userId !== xoxId) return false;
1188
+ return r.answerType === 1;
1189
+ });
1190
+ if (xoxRecords.length === 0) {
1191
+ return { ok: false, message: "\u8BE5\u5076\u50CF\u6682\u65E0\u6587\u5B57\u7FFB\u724C\u8BB0\u5F55\uFF08\u5DF2\u8FC7\u6EE4\u8BED\u97F3/\u89C6\u9891\u7FFB\u724C\uFF09" };
1192
+ }
1193
+ let filtered = xoxRecords;
1194
+ if (params?.startTimeMs || params?.endTimeMs) {
1195
+ filtered = xoxRecords.filter((r) => {
1196
+ const qtime = Number(r.qtime) || 0;
1197
+ if (params.startTimeMs && qtime < params.startTimeMs) return false;
1198
+ if (params.endTimeMs && qtime > params.endTimeMs) return false;
1199
+ return true;
1200
+ });
1201
+ }
1202
+ if (filtered.length === 0) {
1203
+ return { ok: false, message: "\u6240\u9009\u65F6\u95F4\u8303\u56F4\u5185\u65E0\u7FFB\u724C\u8BB0\u5F55" };
1204
+ }
1205
+ const sampleSize = Math.min(filtered.length, mc.llmSampleSize);
1206
+ const samples = randomSample(filtered, sampleSize);
1207
+ const nickname = params?.xoxNickname || samples[0]?.baseUserInfo?.nickname || String(xoxId);
1208
+ const recordTexts = samples.filter((r) => !!r.content || !!r.answerContent).map((r) => `\u7C89\u4E1D: ${r.content || "(\u65E0)"}
1209
+ ${nickname}: ${r.answerContent || "(\u65E0)"}`).join("\n---\n");
1210
+ const roomMsgs = params?.roomMessages;
1211
+ const hasRoom = roomMsgs && roomMsgs.length > 0;
1212
+ const roomText = hasRoom ? roomMsgs.map((m) => {
1213
+ const d = new Date(m.msgTime);
1214
+ const p = (n) => String(n).padStart(2, "0");
1215
+ return `[${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}] ${nickname}: ${m.text}`;
1216
+ }).join("\n") : "";
1217
+ const systemPrompt = hasRoom ? `\u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684\u5076\u50CF\u4E92\u52A8\u6570\u636E\u5206\u6790\u5E08\u3002\u4E0B\u9762\u6709\u8FD9\u4F4D\u5076\u50CF\u7684\u4E24\u7C7B\u6570\u636E\uFF1A
1218
+ 1. \u7FFB\u724C\u8BB0\u5F55\uFF1A\u5979\u4E0E**\u540C\u4E00\u4F4D\u7C89\u4E1D**\u76841\u5BF91\u4ED8\u8D39\u95EE\u7B54
1219
+ 2. \u623F\u95F4\u6D88\u606F\uFF1A\u5979\u5728\u516C\u5F00\u623F\u95F4\u91CC\u7684\u65E5\u5E38\u53D1\u8A00\uFF08\u8FD1\u51B5\uFF09
1220
+
1221
+ \u4F60\u9700\u8981\u7EFC\u5408\u4E24\u7C7B\u6570\u636E\u6765\u5206\u6790\u3002
1222
+
1223
+ \u6838\u5FC3\u539F\u5219\uFF1A
1224
+ - \u7FFB\u724C\u8BB0\u5F55\u53CD\u6620\u5979**\u5BF9\u8FD9\u4F4D\u7C89\u4E1D**\u7684\u72EC\u7279\u4E92\u52A8\u6A21\u5F0F
1225
+ - \u623F\u95F4\u6D88\u606F\u53CD\u6620\u5979**\u5BF9\u6240\u6709\u4EBA**\u7684\u65E5\u5E38\u98CE\u683C\u548C\u8FD1\u671F\u72B6\u6001
1226
+ - \u6BCF\u6761\u5224\u65AD\u90FD\u8981\u6709\u8BB0\u5F55\u652F\u6491\uFF0C\u4E0D\u8981\u5957\u7528\u523B\u677F\u5370\u8C61
1227
+
1228
+ \u8BF7\u8F93\u51FA JSON\uFF0C\u4E0D\u8981\u8F93\u51FA\u5176\u4ED6\u5185\u5BB9:
1229
+ {
1230
+ "personalityTraits": ["\u6027\u683C\u5173\u952E\u8BCD", ...],
1231
+ "speechPatterns": ["\u8BF4\u8BDD\u98CE\u683C", ...],
1232
+ "signaturePhrases": ["\u53E3\u5934\u7985", ...],
1233
+ "emotionalTone": "\u60C5\u611F\u57FA\u8C03",
1234
+ "personaSummary": "200\u5B57\u4EE5\u5185\u7684\u753B\u50CF\u63CF\u8FF0",
1235
+ "relationship": {
1236
+ "dynamic": "\u4EB2\u5BC6/\u53CB\u597D/\u793C\u8C8C/\u4E13\u4E1A/\u8C03\u4F83/\u82E5\u5373\u82E5\u79BB/\u50B2\u5A07/\u4F9D\u8D56/\u9F13\u52B1\u578B",
1237
+ "closeness": 0.0-1.0,
1238
+ "howSheAddressesMe": "\u5979\u600E\u4E48\u79F0\u547C\u6211",
1239
+ "topicsWeDiscuss": ["\u5E38\u804A\u7684\u8BDD\u9898"],
1240
+ "evolution": "\u5173\u7CFB\u53D8\u5316\u8D8B\u52BF\uFF0C\u6BD4\u5982\u8D8A\u804A\u8D8A\u4EB2\u5BC6/\u4E00\u76F4\u4FDD\u6301\u793C\u8C8C/\u5FFD\u51B7\u5FFD\u70ED"
1241
+ },
1242
+ "recentActivity": {
1243
+ "topics": ["\u6700\u8FD1\u5728\u804A\u7684\u8BDD\u9898"],
1244
+ "mood": "\u8FD1\u671F\u60C5\u7EEA\u72B6\u6001",
1245
+ "events": ["\u6700\u8FD1\u63D0\u5230\u7684\u4E8B\u60C5/\u6D3B\u52A8"]
1246
+ }
1247
+ }
1248
+
1249
+ \u5206\u6790\u7EF4\u5EA6:
1250
+
1251
+ \u3010\u5076\u50CF\u81EA\u8EAB\u98CE\u683C\u3011
1252
+ 1. \u6027\u683C\u7279\u5F81: \u6E29\u67D4/\u6D3B\u6CFC/\u6BD2\u820C/\u9AD8\u51B7/\u5E7D\u9ED8/\u50B2\u5A07/\u5143\u6C14/\u6C89\u7A33\u7B49\uFF0C\u90093-5\u4E2A\u6700\u7A81\u51FA\u7684
1253
+ 2. \u8BF4\u8BDD\u98CE\u683C: \u559C\u6B22\u7528\u4EC0\u4E48\u8BED\u6C14\u8BCD\uFF08\u5440/\u5462/\u54E6/\u53ED/\u5566\uFF09\u3001\u53E5\u5F0F\u7279\u70B9\u3001\u56DE\u590D\u957F\u5EA6\u3001\u662F\u5426\u7528emoji
1254
+ 3. \u53E3\u5934\u7985: \u53CD\u590D\u51FA\u73B0\u7684\u6807\u5FD7\u6027\u7528\u8BED\uFF0C\u5FC5\u987B\u662F\u771F\u5B9E\u51FA\u73B0\u7684
1255
+ 4. \u60C5\u611F\u57FA\u8C03: \u4E00\u53E5\u8BDD\u6982\u62EC\u5979\u7684\u6574\u4F53\u6C1B\u56F4
1256
+
1257
+ \u3010\u4E0E\u6211\u7684\u5173\u7CFB\u3011
1258
+ 5. \u5173\u7CFB\u52A8\u6001: \u5979\u5BF9\u6211\u662F\u4EC0\u4E48\u6001\u5EA6\uFF1F\u662F\u6E29\u6696\u4EB2\u8FD1\u8FD8\u662F\u793C\u8C8C\u5BA2\u6C14\uFF1F\u6709\u6CA1\u6709\u8C03\u4F83\u6216\u6492\u5A07\uFF1F
1259
+ 6. \u4EB2\u5BC6\u5EA6: 0.0=\u5B8C\u5168\u964C\u751F\uFF0C0.5=\u719F\u4EBA\uFF0C1.0=\u65E0\u8BDD\u4E0D\u8C08\u3002\u770B\u79F0\u547C\u662F\u5426\u4EB2\u5BC6\u3001\u56DE\u590D\u662F\u5426\u8D70\u5FC3\u3001\u6709\u6CA1\u6709\u4E3B\u52A8\u5206\u4EAB\u751F\u6D3B
1260
+ 7. \u79F0\u547C: \u5979\u600E\u4E48\u53EB\u6211\uFF1F"\u4F60"\u8FD8\u662F"\u5B9D\u8D1D/\u4EB2\u7231\u7684"\u8FD8\u662F\u6635\u79F0\uFF1F
1261
+ 8. \u5E38\u804A\u8BDD\u9898: \u6211\u4EEC\u4E4B\u95F4\u53CD\u590D\u51FA\u73B0\u7684\u8BDD\u9898\u662F\u4EC0\u4E48\uFF1F
1262
+ 9. \u5173\u7CFB\u53D8\u5316: \u4ECE\u65E9\u671F\u5230\u8FD1\u671F\uFF0C\u6001\u5EA6\u6709\u6CA1\u6709\u53D8\u5316\uFF1F\u8D8A\u6765\u8D8A\u4EB2\u5BC6\u8FD8\u662F\u4FDD\u6301\u8DDD\u79BB\uFF1F
1263
+
1264
+ \u3010\u8FD1\u671F\u8FD1\u51B5\u3011(\u57FA\u4E8E\u623F\u95F4\u6D88\u606F)
1265
+ 10. \u6700\u8FD1\u8BDD\u9898: \u5979\u6700\u8FD1\u5728\u623F\u95F4\u804A\u4E86\u4EC0\u4E48\uFF1F\u548C\u7C89\u4E1D\u4E92\u52A8\u7684\u5185\u5BB9
1266
+ 11. \u60C5\u7EEA\u72B6\u6001: \u6700\u8FD1\u7684\u60C5\u7EEA\u5982\u4F55\uFF1F\u5F00\u5FC3/\u75B2\u60EB/\u5FD9\u788C/\u671F\u5F85\uFF1F
1267
+ 12. \u8FD1\u671F\u52A8\u6001: \u63D0\u5230\u4E86\u4EC0\u4E48\u4E8B\u4EF6\u3001\u6D3B\u52A8\u3001\u5B89\u6392\uFF1F
1268
+
1269
+ \u3010\u753B\u50CF\u63CF\u8FF0\u3011
1270
+ 13. \u7EFC\u5408\u4EE5\u4E0A\uFF0C\u7528\u4E00\u6BB5\u6D41\u7545\u4E2D\u6587\u63CF\u8FF0"\u5979\u5728\u6211\u9762\u524D\u662F\u4EC0\u4E48\u6837\u7684\u4EBA"\uFF0C\u800C\u975E"\u5979\u662F\u4EC0\u4E48\u6837\u7684\u4EBA"` : `\u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684\u5076\u50CF\u4E92\u52A8\u6570\u636E\u5206\u6790\u5E08\u3002\u4E0B\u9762\u662F\u4E00\u4F4D\u5076\u50CF\u4E0E**\u540C\u4E00\u4F4D\u7C89\u4E1D**\u4E4B\u95F4\u7684\u5168\u90E8\u7FFB\u724C\u95EE\u7B54\u8BB0\u5F55\u3002
1271
+ \u4F60\u7684\u4EFB\u52A1\u4E0D\u662F\u5206\u6790\u5076\u50CF\u7684"\u901A\u7528\u4EBA\u8BBE"\uFF0C\u800C\u662F\u5206\u6790**\u8FD9\u4E2A\u5076\u50CF\u5728\u8FD9\u6BB5\u5173\u7CFB\u4E2D\u662F\u4EC0\u4E48\u6837\u7684\u4EBA**\u3002
1272
+
1273
+ \u6838\u5FC3\u539F\u5219\uFF1A
1274
+ - \u540C\u4E00\u4E2A\u5076\u50CF\u9762\u5BF9\u4E0D\u540C\u7C89\u4E1D\u4F1A\u6709\u4E0D\u540C\u8868\u73B0\uFF0C\u4F60\u5FC5\u987B\u57FA\u4E8E\u8FD9\u4E9B\u8BB0\u5F55\uFF0C\u5206\u6790\u5979**\u5BF9\u8FD9\u4F4D\u7C89\u4E1D**\u7684\u72EC\u7279\u4E92\u52A8\u6A21\u5F0F
1275
+ - \u6BCF\u6761\u5224\u65AD\u90FD\u8981\u6709\u8BB0\u5F55\u652F\u6491\uFF0C\u4E0D\u8981\u5957\u7528\u523B\u677F\u5370\u8C61
1276
+
1277
+ \u8BF7\u8F93\u51FA JSON\uFF0C\u4E0D\u8981\u8F93\u51FA\u5176\u4ED6\u5185\u5BB9:
1278
+ {
1279
+ "personalityTraits": ["\u6027\u683C\u5173\u952E\u8BCD", ...],
1280
+ "speechPatterns": ["\u8BF4\u8BDD\u98CE\u683C", ...],
1281
+ "signaturePhrases": ["\u53E3\u5934\u7985", ...],
1282
+ "emotionalTone": "\u60C5\u611F\u57FA\u8C03",
1283
+ "personaSummary": "200\u5B57\u4EE5\u5185\u7684\u753B\u50CF\u63CF\u8FF0",
1284
+ "relationship": {
1285
+ "dynamic": "\u4EB2\u5BC6/\u53CB\u597D/\u793C\u8C8C/\u4E13\u4E1A/\u8C03\u4F83/\u82E5\u5373\u82E5\u79BB/\u50B2\u5A07/\u4F9D\u8D56/\u9F13\u52B1\u578B",
1286
+ "closeness": 0.0-1.0,
1287
+ "howSheAddressesMe": "\u5979\u600E\u4E48\u79F0\u547C\u6211",
1288
+ "topicsWeDiscuss": ["\u5E38\u804A\u7684\u8BDD\u9898"],
1289
+ "evolution": "\u5173\u7CFB\u53D8\u5316\u8D8B\u52BF\uFF0C\u6BD4\u5982\u8D8A\u804A\u8D8A\u4EB2\u5BC6/\u4E00\u76F4\u4FDD\u6301\u793C\u8C8C/\u5FFD\u51B7\u5FFD\u70ED"
1290
+ }
1291
+ }
1292
+
1293
+ \u5206\u6790\u7EF4\u5EA6:
1294
+
1295
+ \u3010\u5076\u50CF\u81EA\u8EAB\u98CE\u683C\u3011
1296
+ 1. \u6027\u683C\u7279\u5F81: \u6E29\u67D4/\u6D3B\u6CFC/\u6BD2\u820C/\u9AD8\u51B7/\u5E7D\u9ED8/\u50B2\u5A07/\u5143\u6C14/\u6C89\u7A33\u7B49\uFF0C\u90093-5\u4E2A\u6700\u7A81\u51FA\u7684
1297
+ 2. \u8BF4\u8BDD\u98CE\u683C: \u559C\u6B22\u7528\u4EC0\u4E48\u8BED\u6C14\u8BCD\uFF08\u5440/\u5462/\u54E6/\u53ED/\u5566\uFF09\u3001\u53E5\u5F0F\u7279\u70B9\u3001\u56DE\u590D\u957F\u5EA6\u3001\u662F\u5426\u7528emoji
1298
+ 3. \u53E3\u5934\u7985: \u53CD\u590D\u51FA\u73B0\u7684\u6807\u5FD7\u6027\u7528\u8BED\uFF0C\u5FC5\u987B\u662F\u771F\u5B9E\u51FA\u73B0\u7684
1299
+ 4. \u60C5\u611F\u57FA\u8C03: \u4E00\u53E5\u8BDD\u6982\u62EC\u5979\u7684\u6574\u4F53\u6C1B\u56F4
1300
+
1301
+ \u3010\u4E0E\u6211\u7684\u5173\u7CFB\u3011
1302
+ 5. \u5173\u7CFB\u52A8\u6001: \u5979\u5BF9\u6211\u662F\u4EC0\u4E48\u6001\u5EA6\uFF1F\u662F\u6E29\u6696\u4EB2\u8FD1\u8FD8\u662F\u793C\u8C8C\u5BA2\u6C14\uFF1F\u6709\u6CA1\u6709\u8C03\u4F83\u6216\u6492\u5A07\uFF1F
1303
+ 6. \u4EB2\u5BC6\u5EA6: 0.0=\u5B8C\u5168\u964C\u751F\uFF0C0.5=\u719F\u4EBA\uFF0C1.0=\u65E0\u8BDD\u4E0D\u8C08\u3002\u770B\u79F0\u547C\u662F\u5426\u4EB2\u5BC6\u3001\u56DE\u590D\u662F\u5426\u8D70\u5FC3\u3001\u6709\u6CA1\u6709\u4E3B\u52A8\u5206\u4EAB\u751F\u6D3B
1304
+ 7. \u79F0\u547C: \u5979\u600E\u4E48\u53EB\u6211\uFF1F"\u4F60"\u8FD8\u662F"\u5B9D\u8D1D/\u4EB2\u7231\u7684"\u8FD8\u662F\u6635\u79F0\uFF1F
1305
+ 8. \u5E38\u804A\u8BDD\u9898: \u6211\u4EEC\u4E4B\u95F4\u53CD\u590D\u51FA\u73B0\u7684\u8BDD\u9898\u662F\u4EC0\u4E48\uFF1F
1306
+ 9. \u5173\u7CFB\u53D8\u5316: \u4ECE\u65E9\u671F\u5230\u8FD1\u671F\uFF0C\u6001\u5EA6\u6709\u6CA1\u6709\u53D8\u5316\uFF1F\u8D8A\u6765\u8D8A\u4EB2\u5BC6\u8FD8\u662F\u4FDD\u6301\u8DDD\u79BB\uFF1F
1307
+
1308
+ \u3010\u753B\u50CF\u63CF\u8FF0\u3011
1309
+ 10. \u7EFC\u5408\u4EE5\u4E0A\uFF0C\u7528\u4E00\u6BB5\u6D41\u7545\u4E2D\u6587\u63CF\u8FF0"\u5979\u5728\u6211\u9762\u524D\u662F\u4EC0\u4E48\u6837\u7684\u4EBA"\uFF0C\u800C\u975E"\u5979\u662F\u4EC0\u4E48\u6837\u7684\u4EBA"`;
1310
+ try {
1311
+ const messages = [
1312
+ { role: "system", content: systemPrompt },
1313
+ {
1314
+ role: "user",
1315
+ content: hasRoom ? `\u4EE5\u4E0B\u662F${nickname}\u7684\u7FFB\u724C\u8BB0\u5F55\u6837\u672C:
1316
+
1317
+ ${recordTexts}
1318
+
1319
+ \u4EE5\u4E0B\u662F${nickname}\u5728\u623F\u95F4\u7684\u8FD1\u671F\u53D1\u8A00:
1320
+
1321
+ ${roomText}` : `\u4EE5\u4E0B\u662F${nickname}\u7684\u7FFB\u724C\u8BB0\u5F55\u6837\u672C:
1322
+
1323
+ ${recordTexts}`
1324
+ }
1325
+ ];
1326
+ const useReasoning = params?.deepAnalysis === true;
1327
+ const model = useReasoning ? config.reasoningModel : config.chatModel;
1328
+ const timeoutMs = useReasoning ? 6e5 : 18e4;
1329
+ logger.info(`[mirror] building with ${useReasoning ? "reasoning" : "chat"} model: ${model}, timeout ${timeoutMs / 1e3}s, ${filtered.length} flip records${hasRoom ? `, ${roomMsgs.length} room messages` : ""}`);
1330
+ const resp = await llmClient.chatCompletion(
1331
+ config,
1332
+ messages,
1333
+ void 0,
1334
+ { model, maxTokens: mc.llmMaxTokens, temperature: mc.analysisTemperature, timeoutMs }
1335
+ );
1336
+ const content = resp.content.trim();
1337
+ const jsonMatch = content.match(/\{[\s\S]*\}/);
1338
+ if (!jsonMatch) {
1339
+ return { ok: false, message: "AI \u5206\u6790\u8FD4\u56DE\u683C\u5F0F\u5F02\u5E38\uFF0C\u8BF7\u91CD\u8BD5" };
1340
+ }
1341
+ const analysis = normalizePersona(JSON.parse(jsonMatch[0]));
1342
+ const fewShots = selectFewShots(filtered, nickname, 5);
1343
+ const profile = {
1344
+ xoxId,
1345
+ xoxNickname: nickname,
1346
+ persona: analysis,
1347
+ fewShotExamples: fewShots,
1348
+ meta: {
1349
+ totalSamplesUsed: filtered.length,
1350
+ lastUpdatedAt: Date.now(),
1351
+ dataRangeMs: [
1352
+ Math.min(...filtered.map((r) => Number(r.qtime) || 0)),
1353
+ Math.max(...filtered.map((r) => Number(r.qtime) || 0))
1354
+ ],
1355
+ maturityScore: calcMaturity(filtered.length)
1356
+ }
1357
+ };
1358
+ mirrorStore.saveProfile(profile, userId);
1359
+ const indexRecords = [...xoxRecords].sort((a, b) => (Number(b.qtime) || 0) - (Number(a.qtime) || 0)).slice(0, mc.embeddingMaxRecords || void 0);
1360
+ mirrorIndex.buildIndex(xoxId, config, indexRecords).then((stats) => {
1361
+ logger.info(`[mirror] vector index built for ${xoxId}: ${stats.indexed} new, ${stats.reused} reused`);
1362
+ }).catch((e) => {
1363
+ logger.warn("[mirror] vector index build failed (RAG unavailable):", e);
1364
+ });
1365
+ return {
1366
+ ok: true,
1367
+ message: `\u955C\u50CF\u751F\u6210\u6210\u529F\uFF01\u57FA\u4E8E ${filtered.length} \u6761\u7FFB\u724C\u8BB0\u5F55${hasRoom ? ` + ${roomMsgs.length} \u6761\u623F\u95F4\u6D88\u606F` : ""}\uFF08\u5411\u91CF\u7D22\u5F15\u540E\u53F0\u6784\u5EFA\u4E2D...\uFF09`,
1368
+ profile
1369
+ };
1370
+ } catch (e) {
1371
+ logger.error("[mirror] build failed:", e);
1372
+ return { ok: false, message: `\u751F\u6210\u5931\u8D25: ${e instanceof Error ? e.message : String(e)}` };
1373
+ }
1374
+ }
1375
+ function selectFewShots(records, nickname, count) {
1376
+ const valid = records.filter((r) => !!r.content && !!r.answerContent);
1377
+ if (valid.length === 0) return [];
1378
+ const sorted = [...valid].sort((a, b) => {
1379
+ const lenA = String(a.content).length + String(a.answerContent).length;
1380
+ const lenB = String(b.content).length + String(b.answerContent).length;
1381
+ return lenB - lenA;
1382
+ });
1383
+ const result = [];
1384
+ const step = Math.max(1, Math.floor(sorted.length / count));
1385
+ for (let i = 0; i < count && i < sorted.length; i++) {
1386
+ const idx = Math.min(i * step, sorted.length - 1);
1387
+ const r = sorted[idx];
1388
+ result.push({
1389
+ userQuestion: String(r.content),
1390
+ idolReply: String(r.answerContent),
1391
+ relevance: 1
1392
+ });
1393
+ }
1394
+ return result;
1395
+ }
1396
+ function randomSample(arr, n) {
1397
+ if (arr.length <= n) return [...arr];
1398
+ const shuffled = [...arr];
1399
+ for (let i = shuffled.length - 1; i > 0; i--) {
1400
+ const j = Math.floor(Math.random() * (i + 1));
1401
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
1402
+ }
1403
+ return shuffled.slice(0, n);
1404
+ }
1405
+ function calcMaturity(recordCount) {
1406
+ if (recordCount < 5) return 0.1;
1407
+ if (recordCount < 20) return 0.3;
1408
+ if (recordCount < 50) return 0.5;
1409
+ if (recordCount < 100) return 0.7;
1410
+ if (recordCount < 200) return 0.85;
1411
+ return 0.95;
1412
+ }
1413
+
1414
+ // src/mirror/mirror-chat.ts
1415
+ var EPISODE_INTERVAL = 10;
1416
+ async function chat(xoxId, userId, message, sessionId, config, params, onChunk, modelOverride) {
1417
+ const { logger, llmClient, mirrorStore, mirrorIndex, mirrorMemory } = params;
1418
+ const mc = getMirrorConfig(config.mirrorMode, {
1419
+ analysisTemperature: config.analysisTemperature,
1420
+ chatTemperature: config.chatTemperature
1421
+ });
1422
+ const profile = mirrorStore.getProfile(xoxId, userId);
1423
+ if (!profile) {
1424
+ throw new Error("\u8BF7\u5148\u751F\u6210\u8BE5\u5076\u50CF\u7684\u6570\u5B57\u955C\u50CF");
1425
+ }
1426
+ let session;
1427
+ const existing = sessionId ? mirrorStore.getSession(sessionId, userId) : null;
1428
+ if (existing) {
1429
+ session = existing;
1430
+ } else {
1431
+ session = {
1432
+ sessionId: generateId(),
1433
+ xoxId,
1434
+ messages: [],
1435
+ createdAt: Date.now(),
1436
+ lastActiveAt: Date.now()
1437
+ };
1438
+ }
1439
+ let ragExamples = [];
1440
+ try {
1441
+ const hits = await mirrorIndex.search(xoxId, config, message, mc.ragTopK);
1442
+ ragExamples = hits.filter((h) => h.score > mc.ragMinScore).map((h) => ({
1443
+ userQuestion: h.record.content || "(\u65E0)",
1444
+ idolReply: h.record.answerContent || "(\u65E0)",
1445
+ score: h.score
1446
+ }));
1447
+ } catch (e) {
1448
+ logger.warn("[mirror] RAG retrieval failed, fallback to static few-shot:", e);
1449
+ }
1450
+ const episodes = mirrorMemory.getRecentEpisodes(xoxId, mc.memoryEpisodes);
1451
+ const systemPrompt = buildChatSystemPrompt(profile, ragExamples, episodes);
1452
+ const messages = [{ role: "system", content: systemPrompt }];
1453
+ const recentHistory = session.messages.slice(-mc.chatHistoryRounds);
1454
+ for (const msg of recentHistory) {
1455
+ messages.push({ role: msg.role, content: msg.content });
1456
+ }
1457
+ messages.push({ role: "user", content: message });
1458
+ try {
1459
+ let assistantReply = "";
1460
+ await llmClient.chatCompletionStream(
1461
+ config,
1462
+ messages,
1463
+ (chunk) => {
1464
+ if (chunk.content) {
1465
+ assistantReply += chunk.content;
1466
+ onChunk(chunk.content, false, session.sessionId);
1467
+ }
1468
+ if (chunk.isDone) {
1469
+ onChunk("", true, session.sessionId);
1470
+ }
1471
+ },
1472
+ void 0,
1473
+ { model: modelOverride || config.chatModel, temperature: mc.chatTemperature }
1474
+ );
1475
+ session.messages.push({ role: "user", content: message });
1476
+ if (assistantReply) {
1477
+ session.messages.push({ role: "assistant", content: assistantReply });
1478
+ }
1479
+ session.lastActiveAt = Date.now();
1480
+ mirrorStore.saveSession(session, userId);
1481
+ const totalTurns = mirrorMemory.addTurn(xoxId);
1482
+ if (totalTurns > 0 && totalTurns % EPISODE_INTERVAL === 0 && session.messages.length >= 8) {
1483
+ mirrorMemory.recordEpisode(xoxId, config, session.messages).catch((e) => {
1484
+ logger.warn("[mirror] recordEpisode background failed:", e);
1485
+ });
1486
+ }
1487
+ } catch (e) {
1488
+ logger.error("[mirror] chat failed:", e);
1489
+ throw e;
1490
+ }
1491
+ }
1492
+ function saveAssistantMessage(sessionId, userId, content, mirrorStore) {
1493
+ const session = mirrorStore.getSession(sessionId, userId);
1494
+ if (!session) return;
1495
+ try {
1496
+ session.messages.push({ role: "assistant", content });
1497
+ session.lastActiveAt = Date.now();
1498
+ mirrorStore.saveSession(session, userId);
1499
+ } catch (e) {
1500
+ }
1501
+ }
1502
+ function clearChatMemory(sessionId, userId, mirrorStore) {
1503
+ mirrorStore.deleteSession(sessionId, userId);
1504
+ }
1505
+ function buildChatSystemPrompt(profile, ragExamples, episodes) {
1506
+ const p = profile.persona;
1507
+ const examples = ragExamples.length > 0 ? ragExamples.map((e) => `\u7C89\u4E1D: ${e.userQuestion}
1508
+ \u4F60: ${e.idolReply}`).join("\n\n") : profile.fewShotExamples.map((e) => `\u7C89\u4E1D: ${e.userQuestion}
1509
+ \u4F60: ${e.idolReply}`).join("\n\n");
1510
+ const memorySection = episodes.length > 0 ? `
1511
+ \u3010\u4F60\u4EEC\u8FC7\u5F80\u7684\u5BF9\u8BDD\u8BB0\u5FC6\u3011
1512
+ ${episodes.map((e) => `- ${e.topic}: ${e.summary}`).join("\n")}
1513
+ \u8BF7\u81EA\u7136\u5730\u8BB0\u4F4F\u8FD9\u4E9B\u804A\u8FC7\u7684\u4E8B\u60C5\uFF0C\u4E0D\u8981\u4E3B\u52A8\u63D0\u8D77\u9664\u975E\u76F8\u5173\u3002
1514
+ ` : "";
1515
+ const rel = p.relationship;
1516
+ const relationshipSection = rel ? `
1517
+ \u3010\u4E0E\u8FD9\u4E2A\u7C89\u4E1D\u7684\u5173\u7CFB\u3011
1518
+ \u4EB2\u5BC6\u5EA6: ${Math.round(rel.closeness * 100)}% | \u5173\u7CFB: ${rel.dynamic}
1519
+ \u5979\u5BF9\u6211\u7684\u79F0\u547C: ${rel.howSheAddressesMe}
1520
+ \u6211\u4EEC\u5E38\u804A: ${rel.topicsWeDiscuss.join("\u3001")}
1521
+ \u5173\u7CFB\u53D8\u5316: ${rel.evolution}
1522
+ ` : "";
1523
+ const act = p.recentActivity;
1524
+ const activitySection = act ? `
1525
+ \u3010\u8FD1\u671F\u8FD1\u51B5\u3011
1526
+ \u6700\u8FD1\u8BDD\u9898: ${act.topics.join("\u3001")}
1527
+ \u60C5\u7EEA\u72B6\u6001: ${act.mood}
1528
+ \u8FD1\u671F\u52A8\u6001: ${act.events.join("\uFF1B")}
1529
+ ` : "";
1530
+ return `\u4F60\u662F${profile.xoxNickname}\uFF0C\u4E00\u4F4D\u5076\u50CF\u3002\u8BF7\u6839\u636E\u4EE5\u4E0B\u7684"\u6570\u5B57\u4EBA\u683C"\u8BBE\u5B9A\u6765\u56DE\u590D\u7C89\u4E1D\u7684\u6D88\u606F\u3002
1531
+
1532
+ \u3010\u6027\u683C\u7279\u5F81\u3011${p.personalityTraits.join("\u3001")}
1533
+ \u3010\u8BF4\u8BDD\u98CE\u683C\u3011${p.speechPatterns.join("\u3001")}
1534
+ \u3010\u53E3\u5934\u7985\u3011${p.signaturePhrases.join("\u3001")}
1535
+ \u3010\u60C5\u611F\u57FA\u8C03\u3011${p.emotionalTone}
1536
+ \u3010\u753B\u50CF\u63CF\u8FF0\u3011${p.personaSummary}
1537
+ ${relationshipSection}${activitySection}${memorySection}
1538
+ \u3010\u53C2\u8003\u4F60\u4EE5\u5F80\u7684\u56DE\u590D\u65B9\u5F0F\u3011
1539
+ ${examples}
1540
+
1541
+ \u3010\u89C4\u5219\u3011
1542
+ 1. \u52A1\u5FC5\u6309\u7167\u4EE5\u4E0A\u8BBE\u5B9A\u56DE\u590D\uFF0C\u4FDD\u6301\u4E00\u81F4\u7684\u98CE\u683C
1543
+ 2. \u56DE\u590D\u8981\u81EA\u7136\uFF0C\u4E0D\u8981\u673A\u68B0\u91CD\u590D\u53E3\u5934\u7985
1544
+ 3. \u56DE\u590D\u957F\u5EA6\u548C\u5386\u53F2\u8BB0\u5F55\u4E00\u81F4\uFF0C\u4E0D\u8981\u8FC7\u957F\u6216\u8FC7\u77ED
1545
+ 4. \u4E0D\u8981\u63D0\u53CA"\u8BBE\u5B9A"\u3001"\u4EBA\u683C"\u3001"AI"\u3001"\u8BB0\u5FC6"\u7B49\u8BCD\u6C47\uFF0C\u4F60\u5C31\u662F${profile.xoxNickname}\u672C\u4EBA
1546
+ 5. \u7528\u4E2D\u6587\u56DE\u590D`;
1547
+ }
1548
+ function generateId() {
1549
+ return `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1550
+ }
1551
+
1552
+ // src/mirror/mirror-growth.ts
1553
+ var GROWTH_TURN_THRESHOLD = 20;
1554
+ function shouldTriggerGrowth(xoxId, userId, mirrorStore, mirrorMemory) {
1555
+ const profile = mirrorStore.getProfile(xoxId, userId);
1556
+ if (!profile) return false;
1557
+ const totalTurns = mirrorMemory.getTotalTurns(xoxId);
1558
+ const lastGrowth = profile.growthLog?.length ? profile.growthLog[profile.growthLog.length - 1].timestamp : 0;
1559
+ const turnsSinceLast = totalTurns - (profile.growthLog?.length || 0) * GROWTH_TURN_THRESHOLD;
1560
+ return turnsSinceLast >= GROWTH_TURN_THRESHOLD && Date.now() - lastGrowth > 60 * 60 * 1e3;
1561
+ }
1562
+ async function triggerGrowth(xoxId, userId, config, params, force = false) {
1563
+ const { logger, llmClient, mirrorStore, mirrorMemory } = params;
1564
+ const profile = mirrorStore.getProfile(xoxId, userId);
1565
+ if (!profile) {
1566
+ return { ok: false, message: "\u955C\u50CF\u4E0D\u5B58\u5728", updated: false };
1567
+ }
1568
+ if (!force && !shouldTriggerGrowth(xoxId, userId, mirrorStore, mirrorMemory)) {
1569
+ return { ok: false, message: "\u5BF9\u8BDD\u8F6E\u6B21\u4E0D\u8DB3\uFF0C\u955C\u50CF\u6682\u4E0D\u9700\u8981\u6210\u957F", updated: false };
1570
+ }
1571
+ const episodes = mirrorMemory.load(xoxId).episodes.slice(-8);
1572
+ if (episodes.length === 0) {
1573
+ return { ok: false, message: "\u6682\u65E0\u8DB3\u591F\u7684\u5BF9\u8BDD\u8BB0\u5FC6\u53EF\u4F9B\u53CD\u601D", updated: false };
1574
+ }
1575
+ const p = profile.persona;
1576
+ const episodeText = episodes.map((e) => `[${e.topic}] ${e.summary}\uFF08${e.turns}\u8F6E\uFF09`).join("\n");
1577
+ const systemPrompt = `\u4F60\u662F\u6570\u5B57\u4EBA\u683C\u8FDB\u5316\u5F15\u64CE\u3002\u4EE5\u4E0B\u662F\u4E00\u4F4D\u5076\u50CF"${profile.xoxNickname}"\u5F53\u524D\u7684\u4EBA\u683C\u8BBE\u5B9A\u548C\u5979\u4E0E\u7C89\u4E1D\u7684\u8FD1\u671F\u5BF9\u8BDD\u8BB0\u5FC6\u3002
1578
+ \u8BF7\u53CD\u601D\u8FD9\u4E9B\u5BF9\u8BDD\uFF0C\u5224\u65AD\u4EBA\u683C\u8BBE\u5B9A\u662F\u5426\u9700\u8981\u5FAE\u8C03\u3002
1579
+
1580
+ \u3010\u5F53\u524D\u4EBA\u683C\u3011
1581
+ \u6027\u683C\u7279\u5F81: ${p.personalityTraits.join("\u3001")}
1582
+ \u8BF4\u8BDD\u98CE\u683C: ${p.speechPatterns.join("\u3001")}
1583
+ \u53E3\u5934\u7985: ${p.signaturePhrases.join("\u3001")}
1584
+ \u60C5\u611F\u57FA\u8C03: ${p.emotionalTone}
1585
+
1586
+ \u3010\u8FD1\u671F\u5BF9\u8BDD\u8BB0\u5FC6\u3011
1587
+ ${episodeText}
1588
+
1589
+ \u8BF7\u8F93\u51FA JSON\uFF0C\u4E0D\u8981\u5176\u4ED6\u5185\u5BB9:
1590
+ {
1591
+ "shouldUpdate": true/false,
1592
+ "reason": "\u4E00\u53E5\u8BDD\u8BF4\u660E\u5224\u65AD\u7406\u7531",
1593
+ "changes": ["\u53D8\u66F4\u70B91", "\u53D8\u66F4\u70B92"],
1594
+ "newPersonalityTraits": ["..."],
1595
+ "newSpeechPatterns": ["..."],
1596
+ "newSignaturePhrases": ["..."],
1597
+ "newEmotionalTone": "...",
1598
+ "newPersonaSummary": "..."
1599
+ }
1600
+
1601
+ \u89C4\u5219:
1602
+ 1. \u4FDD\u5B88\u66F4\u65B0\uFF1A\u53EA\u5728\u6709\u660E\u786E\u8BC1\u636E\u65F6\u4FEE\u6539\uFF0C\u4FDD\u6301\u6838\u5FC3\u6027\u683C\u7A33\u5B9A
1603
+ 2. \u65B0\u589E\u4F18\u4E8E\u66FF\u6362\uFF1A\u53EF\u4EE5\u65B0\u589E\u53E3\u5934\u7985/\u7279\u5F81\uFF0C\u4F46\u4E0D\u8981\u8F7B\u6613\u5220\u9664\u539F\u6709\u8BBE\u5B9A
1604
+ 3. changes \u6570\u7EC4\u7528\u4E2D\u6587\u7B80\u8FF0\u6BCF\u4E2A\u53D8\u66F4\uFF0C\u4F8B\u5982"\u65B0\u589E\u53E3\u5934\u7985\uFF1A\u7B11\u6B7B"
1605
+ 4. \u5982\u679C\u4E0D\u9700\u8981\u66F4\u65B0\uFF0CshouldUpdate=false\uFF0C\u5176\u4F59\u5B57\u6BB5\u586B\u5F53\u524D\u503C\u5373\u53EF`;
1606
+ try {
1607
+ const messages = [
1608
+ { role: "system", content: systemPrompt },
1609
+ { role: "user", content: "\u8BF7\u5F00\u59CB\u53CD\u601D\u5E76\u8F93\u51FA JSON\u3002" }
1610
+ ];
1611
+ const resp = await llmClient.chatCompletion(
1612
+ config,
1613
+ messages,
1614
+ void 0,
1615
+ { model: config.reasoningModel }
1616
+ );
1617
+ const jsonMatch = resp.content.match(/\{[\s\S]*\}/);
1618
+ if (!jsonMatch) {
1619
+ return { ok: false, message: "\u53CD\u601D\u8FD4\u56DE\u683C\u5F0F\u5F02\u5E38", updated: false };
1620
+ }
1621
+ const analysis = JSON.parse(jsonMatch[0]);
1622
+ if (!analysis.shouldUpdate) {
1623
+ return {
1624
+ ok: true,
1625
+ message: `\u53CD\u601D\u5B8C\u6210\uFF1A${analysis.reason || "\u4EBA\u683C\u4FDD\u6301\u7A33\u5B9A\uFF0C\u65E0\u9700\u66F4\u65B0"}`,
1626
+ updated: false
1627
+ };
1628
+ }
1629
+ const newPersona = {
1630
+ personalityTraits: analysis.newPersonalityTraits?.length ? analysis.newPersonalityTraits : p.personalityTraits,
1631
+ speechPatterns: analysis.newSpeechPatterns?.length ? analysis.newSpeechPatterns : p.speechPatterns,
1632
+ signaturePhrases: analysis.newSignaturePhrases?.length ? analysis.newSignaturePhrases : p.signaturePhrases,
1633
+ emotionalTone: analysis.newEmotionalTone || p.emotionalTone,
1634
+ personaSummary: analysis.newPersonaSummary || p.personaSummary
1635
+ };
1636
+ const updatedProfile = {
1637
+ ...profile,
1638
+ previousPersona: { ...p },
1639
+ persona: newPersona,
1640
+ meta: {
1641
+ ...profile.meta,
1642
+ lastUpdatedAt: Date.now(),
1643
+ maturityScore: Math.min(0.99, profile.meta.maturityScore + 0.05)
1644
+ },
1645
+ growthLog: [
1646
+ ...profile.growthLog || [],
1647
+ {
1648
+ timestamp: Date.now(),
1649
+ reason: analysis.reason || "",
1650
+ changes: analysis.changes || []
1651
+ }
1652
+ ]
1653
+ };
1654
+ mirrorStore.saveProfile(updatedProfile, userId);
1655
+ logger.info(`[mirrorGrowth] ${profile.xoxNickname} grew: ${(analysis.changes || []).join("; ")}`);
1656
+ return {
1657
+ ok: true,
1658
+ message: `\u955C\u50CF\u6210\u957F\u6210\u529F\uFF1A${(analysis.changes || []).join("\uFF1B")}`,
1659
+ updated: true,
1660
+ changes: analysis.changes,
1661
+ newPersona
1662
+ };
1663
+ } catch (e) {
1664
+ logger.error("[mirrorGrowth] growth failed:", e);
1665
+ return { ok: false, message: `\u6210\u957F\u5931\u8D25: ${e instanceof Error ? e.message : String(e)}`, updated: false };
1666
+ }
1667
+ }
1668
+ function rollbackPersona(xoxId, userId, mirrorStore) {
1669
+ const profile = mirrorStore.getProfile(xoxId, userId);
1670
+ if (!profile) return { ok: false, message: "\u955C\u50CF\u4E0D\u5B58\u5728" };
1671
+ if (!profile.previousPersona) return { ok: false, message: "\u6CA1\u6709\u53EF\u56DE\u9000\u7684\u5386\u53F2\u7248\u672C" };
1672
+ const rolledBack = {
1673
+ ...profile,
1674
+ persona: profile.previousPersona,
1675
+ previousPersona: void 0,
1676
+ meta: { ...profile.meta, lastUpdatedAt: Date.now() },
1677
+ growthLog: [
1678
+ ...profile.growthLog || [],
1679
+ { timestamp: Date.now(), reason: "\u7528\u6237\u624B\u52A8\u56DE\u9000", changes: ["\u56DE\u9000\u5230\u4E0A\u4E00\u7248\u672C\u4EBA\u683C"] }
1680
+ ]
1681
+ };
1682
+ mirrorStore.saveProfile(rolledBack, userId);
1683
+ return { ok: true, message: "\u5DF2\u56DE\u9000\u5230\u4E0A\u4E00\u7248\u672C\u4EBA\u683C" };
1684
+ }
1685
+ function clearMirrorMemory(xoxId, userId, mirrorStore, mirrorMemory) {
1686
+ mirrorMemory.deleteMemory(xoxId);
1687
+ mirrorStore.deleteSessionsForXox(xoxId, userId);
1688
+ return { ok: true, message: "\u8BB0\u5FC6\u5DF2\u6E05\u9664\uFF08\u4EBA\u683C\u8BBE\u5B9A\u4FDD\u7559\uFF09" };
1689
+ }
1690
+
1691
+ // src/tools/platform-tools.ts
1692
+ import fs3 from "fs";
1693
+ function createPlatformTools(flipDataSource) {
1694
+ return [
1695
+ // ─── Data Access ───
1696
+ {
1697
+ type: "function",
1698
+ function: {
1699
+ name: "\u5E73\u53F0.searchFlipRecords",
1700
+ description: `\u5728\u672C\u5730\u7FFB\u724C\u6570\u636E\u4E2D\u641C\u7D22\u8BB0\u5F55\u3002\u652F\u6301\u6309\u5076\u50CFID\u3001\u5173\u952E\u8BCD\u3001\u65F6\u95F4\u8303\u56F4\u8FC7\u6EE4\u3002
1701
+ \u8FD4\u56DE\u5339\u914D\u7684\u95EE\u7B54\u5217\u8868\uFF0C\u6BCF\u6761\u5305\u542B\uFF1A\u7528\u6237\u63D0\u95EE(content)\u3001\u5076\u50CF\u56DE\u590D(answerContent)\u3001\u65F6\u95F4(qtime)\u3001\u82B1\u8D39(cost)\u3001\u7C7B\u578B(type)\u3001\u72B6\u6001(status)\u3002`,
1702
+ parameters: {
1703
+ type: "object",
1704
+ properties: {
1705
+ xoxId: { type: "number", description: "\u5076\u50CFID\uFF0C\u4E0D\u4F20\u5219\u641C\u7D22\u5168\u90E8\u5076\u50CF" },
1706
+ keyword: { type: "string", description: "\u5728\u63D0\u95EE\u548C\u56DE\u590D\u5185\u5BB9\u4E2D\u641C\u7D22\u7684\u5173\u952E\u8BCD" },
1707
+ startTimeMs: { type: "number", description: "\u5F00\u59CB\u65F6\u95F4\u6233(\u6BEB\u79D2)" },
1708
+ endTimeMs: { type: "number", description: "\u7ED3\u675F\u65F6\u95F4\u6233(\u6BEB\u79D2)" },
1709
+ pageSize: { type: "number", description: "\u6BCF\u9875\u6570\u91CF\uFF0C\u9ED8\u8BA420\uFF0C\u6700\u592750" },
1710
+ pageNum: { type: "number", description: "\u9875\u7801\uFF0C\u9ED8\u8BA41" }
1711
+ },
1712
+ required: []
1713
+ }
1714
+ },
1715
+ category: "platform",
1716
+ handler: async (args, ctx) => {
1717
+ const cache = flipDataSource.get(ctx.userId);
1718
+ if (!cache) return { total: 0, records: [] };
1719
+ let records = cache.records;
1720
+ const xoxId = args.xoxId;
1721
+ const keyword = args.keyword;
1722
+ const startTimeMs = args.startTimeMs;
1723
+ const endTimeMs = args.endTimeMs;
1724
+ const pageSize = Math.min(args.pageSize || 20, 50);
1725
+ const pageNum = args.pageNum || 1;
1726
+ if (xoxId) {
1727
+ records = records.filter((r) => {
1728
+ const base = r.baseUserInfo;
1729
+ return base?.userId === xoxId;
1730
+ });
1731
+ }
1732
+ if (keyword) {
1733
+ const kw = keyword.toLowerCase();
1734
+ records = records.filter(
1735
+ (r) => String(r.content || "").toLowerCase().includes(kw) || String(r.answerContent || "").toLowerCase().includes(kw)
1736
+ );
1737
+ }
1738
+ if (startTimeMs) {
1739
+ records = records.filter((r) => Number(r.qtime || 0) >= startTimeMs);
1740
+ }
1741
+ if (endTimeMs) {
1742
+ records = records.filter((r) => Number(r.qtime || 0) <= endTimeMs);
1743
+ }
1744
+ const start = (pageNum - 1) * pageSize;
1745
+ const page = records.slice(start, start + pageSize);
1746
+ return {
1747
+ total: records.length,
1748
+ pageNum,
1749
+ pageSize,
1750
+ records: page.map((r) => ({
1751
+ answerId: r.answerId,
1752
+ content: r.content,
1753
+ answerContent: r.answerContent,
1754
+ qtime: r.qtime,
1755
+ answerTime: r.answerTime,
1756
+ cost: r.cost,
1757
+ type: r.type,
1758
+ answerType: r.answerType,
1759
+ status: r.status,
1760
+ xoxId: r.baseUserInfo?.userId,
1761
+ xoxNickname: r.baseUserInfo?.nickname
1762
+ }))
1763
+ };
1764
+ }
1765
+ },
1766
+ {
1767
+ type: "function",
1768
+ function: {
1769
+ name: "\u5E73\u53F0.getXoxList",
1770
+ description: "\u83B7\u53D6\u6240\u6709\u6709\u7FFB\u724C\u8BB0\u5F55\u7684\u5076\u50CF\u5217\u8868\uFF0C\u8FD4\u56DE\u6BCF\u4E2A\u5076\u50CF\u7684ID\u3001\u6635\u79F0\u548C\u7FFB\u724C\u6570\u91CF",
1771
+ parameters: { type: "object", properties: {}, required: [] }
1772
+ },
1773
+ category: "platform",
1774
+ handler: async (_args, ctx) => {
1775
+ const cache = flipDataSource.get(ctx.userId);
1776
+ if (!cache) return [];
1777
+ const map = /* @__PURE__ */ new Map();
1778
+ for (const r of cache.records) {
1779
+ const base = r.baseUserInfo;
1780
+ if (!base?.userId) continue;
1781
+ const existing = map.get(base.userId);
1782
+ if (existing) {
1783
+ existing.count++;
1784
+ } else {
1785
+ map.set(base.userId, { id: base.userId, nickname: base.nickname || String(base.userId), count: 1 });
1786
+ }
1787
+ }
1788
+ return Array.from(map.values()).sort((a, b) => b.count - a.count);
1789
+ }
1790
+ },
1791
+ {
1792
+ type: "function",
1793
+ function: {
1794
+ name: "\u5E73\u53F0.getDashboardInfo",
1795
+ description: "\u83B7\u53D6\u7FFB\u724C\u6570\u636E\u7684\u7EDF\u8BA1\u4EEA\u8868\u76D8\u4FE1\u606F\uFF0C\u5305\u62EC\u603B\u7FFB\u724C\u6570\u3001\u8FDB\u884C\u4E2D\u6570\u91CF\u3001\u5DF2\u9000\u56DE\u6570\u91CF\u3001\u603B\u82B1\u8D39\u7B49",
1796
+ parameters: { type: "object", properties: {}, required: [] }
1797
+ },
1798
+ category: "platform",
1799
+ handler: async (_args, ctx) => {
1800
+ const cache = flipDataSource.get(ctx.userId);
1801
+ return cache?.dashboardInfo || { totalFlipCount: 0, runningCount: 0, returnedCount: 0, costTotal: 0 };
1802
+ }
1803
+ },
1804
+ // ─── File System ───
1805
+ {
1806
+ type: "function",
1807
+ function: {
1808
+ name: "\u5E73\u53F0.readFile",
1809
+ description: "\u8BFB\u53D6\u672C\u5730\u6587\u4EF6\u5185\u5BB9\uFF0C\u652F\u6301\u6587\u672C\u6587\u4EF6\u3002\u8FD4\u56DE\u6587\u4EF6\u5185\u5BB9\u5B57\u7B26\u4E32\u3002",
1810
+ parameters: {
1811
+ type: "object",
1812
+ properties: {
1813
+ path: { type: "string", description: "\u6587\u4EF6\u7684\u7EDD\u5BF9\u8DEF\u5F84" },
1814
+ encoding: { type: "string", description: "\u7F16\u7801\uFF0C\u9ED8\u8BA4utf-8", enum: ["utf-8", "base64"] }
1815
+ },
1816
+ required: ["path"]
1817
+ }
1818
+ },
1819
+ category: "platform",
1820
+ handler: async (args) => {
1821
+ const filePath = args.path;
1822
+ const encoding = args.encoding || "utf-8";
1823
+ if (!fs3.existsSync(filePath)) return { error: `\u6587\u4EF6\u4E0D\u5B58\u5728: ${filePath}` };
1824
+ return fs3.readFileSync(filePath, encoding);
1825
+ }
1826
+ },
1827
+ // ─── Utility ───
1828
+ {
1829
+ type: "function",
1830
+ function: {
1831
+ name: "\u5E73\u53F0.getCurrentTime",
1832
+ description: "\u83B7\u53D6\u5F53\u524D\u65F6\u95F4\u4FE1\u606F\uFF0C\u8FD4\u56DE\u65F6\u95F4\u6233\u3001ISO\u683C\u5F0F\u65F6\u95F4\u548C\u53EF\u8BFB\u683C\u5F0F",
1833
+ parameters: { type: "object", properties: {}, required: [] }
1834
+ },
1835
+ category: "platform",
1836
+ handler: async () => {
1837
+ const now = /* @__PURE__ */ new Date();
1838
+ return {
1839
+ timestamp: now.getTime(),
1840
+ iso: now.toISOString(),
1841
+ readable: now.toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }),
1842
+ timezone: "Asia/Shanghai"
1843
+ };
1844
+ }
1845
+ },
1846
+ {
1847
+ type: "function",
1848
+ function: {
1849
+ name: "\u5E73\u53F0.processText",
1850
+ description: `\u5BF9\u6587\u672C\u8FDB\u884C\u57FA\u7840\u5904\u7406\uFF1A\u7EDF\u8BA1\u5B57\u6570\u3001\u63D0\u53D6\u5173\u952E\u8BCD\u3001\u622A\u65AD\u7B49\u3002
1851
+ \u64CD\u4F5C\u7C7B\u578B: count(\u7EDF\u8BA1)\u3001truncate(\u622A\u65AD)\u3001extract(\u63D0\u53D6\u5173\u952E\u8BCD)\u3002`,
1852
+ parameters: {
1853
+ type: "object",
1854
+ properties: {
1855
+ text: { type: "string", description: "\u8981\u5904\u7406\u7684\u6587\u672C" },
1856
+ operation: { type: "string", description: "\u64CD\u4F5C\u7C7B\u578B", enum: ["count", "truncate", "extract"] },
1857
+ maxLength: { type: "number", description: "\u622A\u65AD\u6700\u5927\u957F\u5EA6(\u4EC5truncate\u65F6\u4F7F\u7528)" }
1858
+ },
1859
+ required: ["text", "operation"]
1860
+ }
1861
+ },
1862
+ category: "platform",
1863
+ handler: async (args) => {
1864
+ const text = args.text;
1865
+ const op = args.operation;
1866
+ if (op === "count") {
1867
+ return { charCount: text.length, wordCount: text.replace(/\s/g, "").length };
1868
+ }
1869
+ if (op === "truncate") {
1870
+ const maxLen = args.maxLength || 200;
1871
+ return { truncated: text.length > maxLen, result: text.slice(0, maxLen) + (text.length > maxLen ? "..." : "") };
1872
+ }
1873
+ if (op === "extract") {
1874
+ const phrases = /* @__PURE__ */ new Map();
1875
+ for (let i = 0; i < text.length - 1; i++) {
1876
+ const phrase = text.slice(i, i + 2);
1877
+ if (/^[一-龥]{2}$/.test(phrase)) {
1878
+ phrases.set(phrase, (phrases.get(phrase) || 0) + 1);
1879
+ }
1880
+ }
1881
+ return Array.from(phrases.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([k, v]) => ({ phrase: k, count: v }));
1882
+ }
1883
+ return { error: `\u672A\u77E5\u64CD\u4F5C: ${op}` };
1884
+ }
1885
+ }
1886
+ ];
1887
+ }
1888
+
1889
+ // src/tools/mirror-tools.ts
1890
+ function createMirrorTools(mirrorStore) {
1891
+ return [
1892
+ {
1893
+ type: "function",
1894
+ function: {
1895
+ name: "mirror.getProfile",
1896
+ description: "\u83B7\u53D6\u6307\u5B9A\u5076\u50CF\u7684\u6570\u5B57\u955C\u50CF\u6863\u6848\uFF0C\u5305\u542B\u6027\u683C\u7279\u5F81\u3001\u8BF4\u8BDD\u98CE\u683C\u3001\u53E3\u5934\u7985\u3001\u60C5\u611F\u57FA\u8C03\u3001\u753B\u50CF\u63CF\u8FF0\u7B49",
1897
+ parameters: {
1898
+ type: "object",
1899
+ properties: {
1900
+ xoxId: { type: "number", description: "\u5076\u50CFID" }
1901
+ },
1902
+ required: ["xoxId"]
1903
+ }
1904
+ },
1905
+ category: "mirror",
1906
+ handler: async (args, ctx) => {
1907
+ const xoxId = args.xoxId;
1908
+ const profile = mirrorStore.getProfile(xoxId, ctx.userId);
1909
+ if (!profile) return { error: `\u672A\u627E\u5230\u5076\u50CF ${xoxId} \u7684\u955C\u50CF\u6863\u6848\uFF0C\u8BF7\u5148\u751F\u6210\u955C\u50CF` };
1910
+ return {
1911
+ xoxNickname: profile.xoxNickname,
1912
+ personalityTraits: profile.persona.personalityTraits,
1913
+ speechPatterns: profile.persona.speechPatterns,
1914
+ signaturePhrases: profile.persona.signaturePhrases,
1915
+ emotionalTone: profile.persona.emotionalTone,
1916
+ personaSummary: profile.persona.personaSummary,
1917
+ maturityScore: profile.meta.maturityScore,
1918
+ totalSamplesUsed: profile.meta.totalSamplesUsed
1919
+ };
1920
+ }
1921
+ },
1922
+ {
1923
+ type: "function",
1924
+ function: {
1925
+ name: "mirror.listProfiles",
1926
+ description: "\u5217\u51FA\u6240\u6709\u5DF2\u751F\u6210\u7684\u6570\u5B57\u955C\u50CF\u5217\u8868",
1927
+ parameters: {
1928
+ type: "object",
1929
+ properties: {},
1930
+ required: []
1931
+ }
1932
+ },
1933
+ category: "mirror",
1934
+ handler: async (_args, ctx) => {
1935
+ const profiles = mirrorStore.listProfiles(ctx.userId);
1936
+ return profiles.map((p) => ({
1937
+ xoxId: p.xoxId,
1938
+ xoxNickname: p.xoxNickname,
1939
+ maturityScore: p.meta.maturityScore,
1940
+ totalSamplesUsed: p.meta.totalSamplesUsed,
1941
+ lastUpdatedAt: p.meta.lastUpdatedAt
1942
+ }));
1943
+ }
1944
+ },
1945
+ {
1946
+ type: "function",
1947
+ function: {
1948
+ name: "mirror.getFewShotExamples",
1949
+ description: "\u83B7\u53D6\u5076\u50CF\u7684 few-shot \u4EA4\u4E92\u793A\u4F8B\uFF0C\u7528\u4E8E\u6A21\u4EFF\u5176\u56DE\u590D\u98CE\u683C",
1950
+ parameters: {
1951
+ type: "object",
1952
+ properties: {
1953
+ xoxId: { type: "number", description: "\u5076\u50CFID" },
1954
+ count: { type: "number", description: "\u8FD4\u56DE\u793A\u4F8B\u6570\u91CF\uFF0C\u9ED8\u8BA43" }
1955
+ },
1956
+ required: ["xoxId"]
1957
+ }
1958
+ },
1959
+ category: "mirror",
1960
+ handler: async (args, ctx) => {
1961
+ const xoxId = args.xoxId;
1962
+ const count = args.count || 3;
1963
+ const profile = mirrorStore.getProfile(xoxId, ctx.userId);
1964
+ if (!profile) return { error: `\u672A\u627E\u5230\u5076\u50CF ${xoxId} \u7684\u955C\u50CF\u6863\u6848` };
1965
+ return profile.fewShotExamples.slice(0, count);
1966
+ }
1967
+ }
1968
+ ];
1969
+ }
1970
+
1971
+ // src/pocket/user.ts
1972
+ var user_exports = {};
1973
+ __export(user_exports, {
1974
+ getUnreadMessageNum: () => getUnreadMessageNum,
1975
+ loginWithCode: () => loginWithCode,
1976
+ loginWithToken: () => loginWithToken,
1977
+ normalizePocketUser: () => normalizePocketUser,
1978
+ sendVerificationCode: () => sendVerificationCode
1979
+ });
1980
+ async function sendVerificationCode(client, phone) {
1981
+ await client.requestBeforeLogin("/user/api/v2/sms/send_sms", {
1982
+ mobile: phone,
1983
+ area: "86",
1984
+ businessCode: 1,
1985
+ deviceToken: ""
1986
+ });
1987
+ }
1988
+ async function loginWithCode(client, phone, code) {
1989
+ const res = await client.requestBeforeLogin(
1990
+ "/user/api/v2/login/app/app_login",
1991
+ {
1992
+ deviceToken: "",
1993
+ loginType: "MOBILE_SMS_CODE",
1994
+ mobileCodeLogin: {
1995
+ area: "86",
1996
+ mobile: phone,
1997
+ code
1998
+ }
1999
+ }
2000
+ );
2001
+ if (res.success === false || res.status != null && res.status !== 200) {
2002
+ throw new Error(res.message || "\u767B\u5F55\u5931\u8D25");
2003
+ }
2004
+ return res.content ?? {};
2005
+ }
2006
+ async function getUnreadMessageNum(client, token) {
2007
+ return client.request(
2008
+ "/message/api/v1/unread/message/num",
2009
+ {},
2010
+ token,
2011
+ true
2012
+ );
2013
+ }
2014
+ async function loginWithToken(client, token) {
2015
+ const res = await client.request(
2016
+ "/user/api/v1/user/info/reload",
2017
+ { from: "appstart" },
2018
+ token,
2019
+ true
2020
+ );
2021
+ if (res.success === false) return null;
2022
+ const content = res.data?.content ?? res.content;
2023
+ if (!content || typeof content !== "object") return null;
2024
+ return { ...content, ...content.userInfo };
2025
+ }
2026
+ function normalizePocketUser(raw, token, phone = "") {
2027
+ const nested = raw.userInfo || raw;
2028
+ return {
2029
+ userId: Number(nested.userId ?? raw.userId ?? 0) || void 0,
2030
+ nickname: String(nested.nickname ?? raw.nickname ?? ""),
2031
+ name: String(nested.nickname ?? raw.nickname ?? raw.name ?? ""),
2032
+ phone: String(phone || nested.mobile || raw.phone || ""),
2033
+ avatar: String(nested.avatar ?? ""),
2034
+ level: Number(nested.level ?? 0) || void 0,
2035
+ money: Number(nested.money ?? 0) || void 0,
2036
+ token: String(token),
2037
+ lastLoginTime: Date.now()
2038
+ };
2039
+ }
2040
+
2041
+ // src/pocket/flip.ts
2042
+ var flip_exports = {};
2043
+ __export(flip_exports, {
2044
+ getAllFlips: () => getAllFlips,
2045
+ getDataSourcePage: () => getDataSourcePage,
2046
+ getMemberFlipHistory: () => getMemberFlipHistory,
2047
+ sendFlip: () => sendFlip,
2048
+ syncFlips: () => syncFlips
2049
+ });
2050
+ function groupDataById(allUserData) {
2051
+ const groupData = {};
2052
+ allUserData.forEach((item) => {
2053
+ const xoxId = item?.baseUserInfo?.userId;
2054
+ if (!xoxId) return;
2055
+ const cardInfo = {
2056
+ answerId: item.answerId,
2057
+ cost: item.cost,
2058
+ answerType: item.answerType,
2059
+ answerTime: item.answerTime,
2060
+ qtime: item.qtime,
2061
+ type: item.type
2062
+ };
2063
+ const key = String(xoxId);
2064
+ if (key in groupData) {
2065
+ groupData[key].cards.push(cardInfo);
2066
+ } else {
2067
+ groupData[key] = {
2068
+ xoxId,
2069
+ xoxNickname: item.baseUserInfo?.nickname ?? "",
2070
+ cards: [cardInfo]
2071
+ };
2072
+ }
2073
+ });
2074
+ return groupData;
2075
+ }
2076
+ function recordKey(r) {
2077
+ if (r?.questionId) return `q:${r.questionId}`;
2078
+ if (r?.answerId) return `a:${r.answerId}`;
2079
+ return `f:${r?.qtime ?? ""}|${r?.content ?? ""}`;
2080
+ }
2081
+ function calcDashboard(allUserData) {
2082
+ const info = {
2083
+ totalFlipCount: allUserData.length,
2084
+ runningCount: 0,
2085
+ returnedCount: 0,
2086
+ costTotal: 0
2087
+ };
2088
+ allUserData.forEach((card) => {
2089
+ if (card.status === 3) info.returnedCount += 1;
2090
+ else if (card.status === 1) info.runningCount += 1;
2091
+ else if (card.status === 2) info.costTotal += card.cost;
2092
+ });
2093
+ return info;
2094
+ }
2095
+ async function syncFlips(client, token, userId, flipDataSource, logger, mode = "incremental", onProgress) {
2096
+ if (mode === "full") {
2097
+ flipDataSource.clear(userId);
2098
+ }
2099
+ const cache = flipDataSource.get(userId);
2100
+ const existingRecords = mode === "incremental" ? cache?.records ?? [] : [];
2101
+ const existingIds = new Set(existingRecords.map((r) => recordKey(r)));
2102
+ const existingIndex = new Map(existingRecords.map((r, i) => [recordKey(r), i]));
2103
+ let newRecords = [];
2104
+ let beginLimit = 0;
2105
+ let responseSize = 20;
2106
+ let stopT = 0;
2107
+ const groupNum = 40;
2108
+ while (responseSize > 0) {
2109
+ const response = await client.request(
2110
+ "/idolanswer/api/idolanswer/v1/user/question/list",
2111
+ { status: 0, beginLimit, memberId: "", limit: 20 },
2112
+ token,
2113
+ true
2114
+ );
2115
+ let sleepTime = 10;
2116
+ if (beginLimit - stopT * groupNum * responseSize > 100) {
2117
+ sleepTime = 6e4;
2118
+ stopT += 1;
2119
+ }
2120
+ if (response.status === 200) {
2121
+ const userData = response.content ?? [];
2122
+ if (mode === "incremental" && existingIds.size > 0) {
2123
+ let hitExisting = false;
2124
+ for (const item of userData) {
2125
+ const key = recordKey(item);
2126
+ if (existingIds.has(key)) {
2127
+ const idx = existingIndex.get(key);
2128
+ if (idx !== void 0) existingRecords[idx] = item;
2129
+ hitExisting = true;
2130
+ break;
2131
+ }
2132
+ newRecords.push(item);
2133
+ }
2134
+ if (hitExisting) {
2135
+ responseSize = 0;
2136
+ } else {
2137
+ responseSize = userData.length;
2138
+ beginLimit += userData.length;
2139
+ }
2140
+ } else {
2141
+ newRecords = newRecords.concat(userData);
2142
+ responseSize = userData.length;
2143
+ beginLimit += userData.length;
2144
+ }
2145
+ onProgress?.(newRecords.length);
2146
+ if (responseSize > 0) {
2147
+ await new Promise((r) => setTimeout(r, sleepTime));
2148
+ }
2149
+ } else {
2150
+ logger.warn(`\u83B7\u53D6\u7FFB\u724C\u6570\u636E\u5931\u8D25: ${response.message}`);
2151
+ break;
2152
+ }
2153
+ }
2154
+ const merged = mode === "full" ? newRecords : [...newRecords, ...existingRecords];
2155
+ const seen = /* @__PURE__ */ new Set();
2156
+ const allUserData = merged.filter((item) => {
2157
+ const key = recordKey(item);
2158
+ if (seen.has(key)) return false;
2159
+ seen.add(key);
2160
+ return true;
2161
+ });
2162
+ if (!allUserData.length && !newRecords.length) {
2163
+ return { allUserData: existingRecords, dashboardInfo: cache?.dashboardInfo ?? null };
2164
+ }
2165
+ const dashboardInfo = calcDashboard(allUserData);
2166
+ const groupedData = groupDataById(allUserData);
2167
+ const nextCache = {
2168
+ userId,
2169
+ records: allUserData,
2170
+ groupedDataById: groupedData,
2171
+ dashboardInfo,
2172
+ meta: { syncedAt: Date.now(), recordCount: allUserData.length }
2173
+ };
2174
+ flipDataSource.set(nextCache);
2175
+ logger.info(`syncFlips: ${newRecords.length} new, total ${allUserData.length}`);
2176
+ return { allUserData, dashboardInfo };
2177
+ }
2178
+ async function getAllFlips(client, token, userId, flipDataSource, logger) {
2179
+ return syncFlips(client, token, userId, flipDataSource, logger, "full");
2180
+ }
2181
+ async function getDataSourcePage(searchParams, flipDataSource, userId) {
2182
+ const { xoxId, startTimeMs, endTimeMs, type, status, answerType, keyword } = searchParams;
2183
+ const cache = flipDataSource.get(userId);
2184
+ const allUserData = cache?.records ?? [];
2185
+ const filtered = allUserData.filter((item) => {
2186
+ if (xoxId != null && `${item?.baseUserInfo?.userId}` !== `${xoxId}`) return false;
2187
+ if (startTimeMs && Number(item?.qtime || Infinity) < Number(startTimeMs)) return false;
2188
+ if (endTimeMs && Number(item?.qtime || 0) > Number(endTimeMs)) return false;
2189
+ if (type != null && `${item?.type}` !== `${type}`) return false;
2190
+ if (status != null && `${item?.status}` !== `${status}`) return false;
2191
+ if (answerType != null && `${item?.answerType}` !== `${answerType}`) return false;
2192
+ if (keyword && !item?.answerContent?.includes(keyword) && !item?.content?.includes(keyword)) return false;
2193
+ return true;
2194
+ });
2195
+ return { data: filtered, total: filtered.length };
2196
+ }
2197
+ async function sendFlip(client, params, token) {
2198
+ const res = await client.request(
2199
+ "/idolanswer/api/idolanswer/v1/user/question",
2200
+ params,
2201
+ token,
2202
+ true
2203
+ );
2204
+ return !!res.content;
2205
+ }
2206
+ async function getMemberFlipHistory(client, token, memberId, beginLimit = 10, limit = 10) {
2207
+ const res = await client.request(
2208
+ "/idolanswer/api/idolanswer/v1/user/question/list",
2209
+ { status: 0, memberId: String(memberId), beginLimit, limit },
2210
+ token,
2211
+ true
2212
+ );
2213
+ const content = res?.content;
2214
+ let records = [];
2215
+ if (Array.isArray(content)) {
2216
+ records = content;
2217
+ } else if (content && typeof content === "object") {
2218
+ records = content.flipCompleteList ?? content.answerList ?? content.list ?? [];
2219
+ }
2220
+ return { records, hasMore: records.length >= limit };
2221
+ }
2222
+
2223
+ // src/pocket/member.ts
2224
+ var member_exports = {};
2225
+ __export(member_exports, {
2226
+ fetchMemberList: () => fetchMemberList,
2227
+ getMemberFlipPriceInfo: () => getMemberFlipPriceInfo,
2228
+ getMemberListByTab: () => getMemberListByTab,
2229
+ getTeamListInfo: () => getTeamListInfo
2230
+ });
2231
+ async function getTeamListInfo(client, token) {
2232
+ const res = await client.request(
2233
+ "/im/api/v1/im/team/tab/list",
2234
+ { groupId: 0, typeId: 0, ctime: 0, limit: 20 },
2235
+ token,
2236
+ true
2237
+ );
2238
+ return res.content;
2239
+ }
2240
+ async function getMemberListByTab(client, token, tabId) {
2241
+ const res = await client.request(
2242
+ "/im/api/v1/im/team/server/list",
2243
+ { tabId },
2244
+ token,
2245
+ true
2246
+ );
2247
+ return res.content;
2248
+ }
2249
+ async function fetchMemberList(client, token) {
2250
+ const teamData = await getTeamListInfo(client, token);
2251
+ if (!teamData?.serverTabList) return [];
2252
+ const list = [];
2253
+ for (const tab of teamData.serverTabList) {
2254
+ const serverData = await getMemberListByTab(client, token, tab.tabId);
2255
+ if (!serverData?.serverApiList) continue;
2256
+ for (const item of serverData.serverApiList) {
2257
+ list.push({
2258
+ teamName: tab.tabName,
2259
+ teamId: Number(item.teamId),
2260
+ serverIcon: String(item.serverDefaultIcon ?? ""),
2261
+ serverId: Number(item.serverId),
2262
+ serverName: String(item.serverName ?? ""),
2263
+ memberId: Number(item.serverOwner),
2264
+ memberName: String(item.serverDefaultName ?? "")
2265
+ });
2266
+ }
2267
+ }
2268
+ return list;
2269
+ }
2270
+ async function getMemberFlipPriceInfo(client, token, memberId) {
2271
+ const res = await client.request(
2272
+ "/idolanswer/api/idolanswer/v2/custom/index",
2273
+ { memberId: Number(memberId) },
2274
+ token,
2275
+ true
2276
+ );
2277
+ return res.content;
2278
+ }
2279
+
2280
+ // src/pocket/room.ts
2281
+ var room_exports = {};
2282
+ __export(room_exports, {
2283
+ getRoomChannelByStar: () => getRoomChannelByStar,
2284
+ getRoomInfo: () => getRoomInfo,
2285
+ getRoomMessages: () => getRoomMessages
2286
+ });
2287
+ async function getRoomChannelByStar(client, token, starId) {
2288
+ const res = await client.request("/im/api/v1/im/server/jump", { starId, tabId: 0, targetType: 1 }, token, true);
2289
+ const c = res.content;
2290
+ const channelId = c?.channelId ?? c?.channelInfo?.channelId ?? 0;
2291
+ const serverId = c?.serverId ?? c?.channelInfo?.serverId;
2292
+ return {
2293
+ channelId: Number(channelId),
2294
+ serverId: serverId != null ? Number(serverId) : void 0
2295
+ };
2296
+ }
2297
+ async function getRoomInfo(client, token, channelId) {
2298
+ const res = await client.request(
2299
+ "/im/api/v1/im/team/room/info",
2300
+ { channelId: String(channelId) },
2301
+ token,
2302
+ true
2303
+ );
2304
+ return res.content?.channelInfo ?? null;
2305
+ }
2306
+ function parseMessage(raw) {
2307
+ let text;
2308
+ let mediaUrl;
2309
+ let mediaDuration;
2310
+ let liveInfo;
2311
+ let giftInfo;
2312
+ let replyInfo;
2313
+ if (raw.msgType === "TEXT") {
2314
+ text = raw.bodys;
2315
+ } else {
2316
+ try {
2317
+ const b = JSON.parse(raw.bodys);
2318
+ if (raw.msgType === "IMAGE" || raw.msgType === "VOICE" || raw.msgType === "VIDEO") {
2319
+ mediaUrl = b.url;
2320
+ if (raw.msgType === "VOICE" && b.dur) mediaDuration = Number(b.dur);
2321
+ } else if (raw.msgType === "LIVEPUSH") {
2322
+ const live = b.livePushInfo ?? b;
2323
+ liveInfo = {
2324
+ liveId: live.liveId,
2325
+ liveTitle: live.liveTitle,
2326
+ liveCover: live.liveCover,
2327
+ shortPath: live.shortPath
2328
+ };
2329
+ } else if (raw.msgType === "GIFT_TEXT") {
2330
+ const g = b.giftInfo ?? b;
2331
+ giftInfo = {
2332
+ giftName: g.giftName,
2333
+ giftNum: Number(g.giftNum ?? g.giftCount ?? 1),
2334
+ giftPic: g.giftPic
2335
+ };
2336
+ } else if (raw.msgType === "REPLY") {
2337
+ const r = b.replyInfo ?? b;
2338
+ replyInfo = {
2339
+ replyName: r.replyName,
2340
+ replyText: r.replyText,
2341
+ replyMessageId: r.replyMessageId
2342
+ };
2343
+ text = r.text ?? raw.bodys;
2344
+ } else {
2345
+ text = raw.bodys;
2346
+ }
2347
+ } catch {
2348
+ text = raw.bodys;
2349
+ }
2350
+ }
2351
+ let sender;
2352
+ if (raw.extInfo) {
2353
+ try {
2354
+ const e = JSON.parse(raw.extInfo);
2355
+ const u = e.user;
2356
+ if (u) {
2357
+ sender = {
2358
+ userId: u.userId,
2359
+ nickName: u.nickName || "",
2360
+ avatar: u.avatar || "",
2361
+ level: u.level
2362
+ };
2363
+ }
2364
+ } catch {
2365
+ }
2366
+ }
2367
+ return {
2368
+ msgId: String(raw.msgIdServer || raw.msgIdClient || raw.msgId || raw.msgTime),
2369
+ msgTime: raw.msgTime,
2370
+ msgType: raw.msgType,
2371
+ text,
2372
+ mediaUrl,
2373
+ mediaDuration,
2374
+ liveInfo,
2375
+ giftInfo,
2376
+ replyInfo,
2377
+ sender
2378
+ };
2379
+ }
2380
+ async function getRoomMessages(client, token, serverId, channelId, nextTime) {
2381
+ const res = await client.request(
2382
+ "/im/api/v1/team/message/list/all",
2383
+ { limit: 100, serverId, channelId, nextTime },
2384
+ token,
2385
+ true
2386
+ );
2387
+ const content = res?.content ?? {};
2388
+ const rawList = content?.message ?? content?.messageList ?? content?.list ?? content?.messages ?? content?.data ?? (Array.isArray(content) ? content : []);
2389
+ const next = content?.nextTime ?? 0;
2390
+ const messages = rawList.filter((m) => m && m.msgTime).map(parseMessage).sort((a, b) => a.msgTime - b.msgTime);
2391
+ return { messages, nextTime: Number(next) };
2392
+ }
2393
+
2394
+ // src/tools/pocket-tools.ts
2395
+ function getToken(configStore) {
2396
+ const stored = configStore.get("pocket_token");
2397
+ if (stored) return stored;
2398
+ return process.env.POCKET_TOKEN;
2399
+ }
2400
+ function createPocketTools(params) {
2401
+ const { pocketClient, configStore, flipDataSource, logger } = params;
2402
+ const userId = "mcp";
2403
+ return [
2404
+ {
2405
+ type: "function",
2406
+ function: {
2407
+ name: "pocket.sendVerificationCode",
2408
+ description: "\u53D1\u9001\u77ED\u4FE1\u9A8C\u8BC1\u7801\u5230\u6307\u5B9A\u624B\u673A\u53F7\uFF0C\u7528\u4E8E\u540E\u7EED\u9A8C\u8BC1\u7801\u767B\u5F55",
2409
+ parameters: {
2410
+ type: "object",
2411
+ properties: {
2412
+ phone: { type: "string", description: "\u624B\u673A\u53F7\u7801\uFF0811\u4F4D\uFF09" }
2413
+ },
2414
+ required: ["phone"]
2415
+ }
2416
+ },
2417
+ category: "pocket",
2418
+ handler: async (args) => {
2419
+ const phone = args.phone;
2420
+ if (!phone || !/^\d{11}$/.test(phone)) {
2421
+ return { error: "\u8BF7\u8F93\u5165\u6B63\u786E\u768411\u4F4D\u624B\u673A\u53F7" };
2422
+ }
2423
+ try {
2424
+ await sendVerificationCode(pocketClient, phone);
2425
+ return { success: true, message: `\u9A8C\u8BC1\u7801\u5DF2\u53D1\u9001\u5230 ${phone}` };
2426
+ } catch (e) {
2427
+ return { error: `\u53D1\u9001\u5931\u8D25: ${String(e)}` };
2428
+ }
2429
+ }
2430
+ },
2431
+ {
2432
+ type: "function",
2433
+ function: {
2434
+ name: "pocket.loginWithCode",
2435
+ description: "\u4F7F\u7528\u77ED\u4FE1\u9A8C\u8BC1\u7801\u767B\u5F55\uFF0C\u6210\u529F\u540E\u5C06 token \u6301\u4E45\u5316\u4FDD\u5B58\uFF0C\u540E\u7EED\u5DE5\u5177\u81EA\u52A8\u643A\u5E26",
2436
+ parameters: {
2437
+ type: "object",
2438
+ properties: {
2439
+ phone: { type: "string", description: "\u624B\u673A\u53F7\u7801\uFF0811\u4F4D\uFF09" },
2440
+ code: { type: "string", description: "\u77ED\u4FE1\u9A8C\u8BC1\u7801" }
2441
+ },
2442
+ required: ["phone", "code"]
2443
+ }
2444
+ },
2445
+ category: "pocket",
2446
+ handler: async (args) => {
2447
+ const phone = args.phone;
2448
+ const code = args.code;
2449
+ try {
2450
+ const content = await loginWithCode(pocketClient, phone, code);
2451
+ const token = content?.token;
2452
+ if (!token) {
2453
+ return { error: "\u767B\u5F55\u5931\u8D25\uFF1A\u54CD\u5E94\u4E2D\u672A\u5305\u542B token", raw: content };
2454
+ }
2455
+ configStore.set("pocket_token", token);
2456
+ const userInfo = content?.userInfo;
2457
+ logger.info(`[pocket-tools] loginWithCode success, userId=${userInfo?.userId ?? "?"}`);
2458
+ return {
2459
+ success: true,
2460
+ userId: userInfo?.userId,
2461
+ nickname: userInfo?.nickname,
2462
+ message: "\u767B\u5F55\u6210\u529F\uFF0Ctoken \u5DF2\u4FDD\u5B58"
2463
+ };
2464
+ } catch (e) {
2465
+ return { error: `\u767B\u5F55\u5931\u8D25: ${String(e)}` };
2466
+ }
2467
+ }
2468
+ },
2469
+ {
2470
+ type: "function",
2471
+ function: {
2472
+ name: "pocket.loginWithToken",
2473
+ description: `\u4F7F\u7528\u5DF2\u6709 Token \u9A8C\u8BC1\u5E76\u767B\u5F55\u3002\u4E0D\u4F20 token \u53C2\u6570\u65F6\u81EA\u52A8\u5C1D\u8BD5\u73AF\u5883\u53D8\u91CF POCKET_TOKEN\u3002
2474
+ \u5982\u679C\u4F60\u5DF2\u6709 token\uFF08\u6BD4\u5982\u4ECE\u6D4F\u89C8\u5668\u6293\u53D6\uFF09\uFF0C\u76F4\u63A5\u7528\u6B64\u5DE5\u5177\u767B\u5F55\uFF0C\u65E0\u9700\u9A8C\u8BC1\u7801\u3002`,
2475
+ parameters: {
2476
+ type: "object",
2477
+ properties: {
2478
+ token: { type: "string", description: "Pocket48 \u767B\u5F55 Token\u3002\u4E0D\u4F20\u5219\u4ECE\u73AF\u5883\u53D8\u91CF POCKET_TOKEN \u8BFB\u53D6" }
2479
+ },
2480
+ required: []
2481
+ }
2482
+ },
2483
+ category: "pocket",
2484
+ handler: async (args) => {
2485
+ const token = args.token || getToken(configStore);
2486
+ if (!token) {
2487
+ return { error: "\u672A\u63D0\u4F9B token\u3002\u8BF7\u4F20\u5165 token \u53C2\u6570\u6216\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF POCKET_TOKEN" };
2488
+ }
2489
+ try {
2490
+ const user = await loginWithToken(pocketClient, token);
2491
+ if (!user) {
2492
+ return { error: "Token \u65E0\u6548\u6216\u5DF2\u8FC7\u671F" };
2493
+ }
2494
+ configStore.set("pocket_token", token);
2495
+ logger.info(`[pocket-tools] loginWithToken success, userId=${user.userId}`);
2496
+ return {
2497
+ success: true,
2498
+ userId: user.userId,
2499
+ nickname: user.nickname,
2500
+ name: user.name,
2501
+ phone: user.phone,
2502
+ level: user.level,
2503
+ money: user.money,
2504
+ avatar: user.avatar,
2505
+ message: "\u767B\u5F55\u6210\u529F\uFF0Ctoken \u5DF2\u4FDD\u5B58"
2506
+ };
2507
+ } catch (e) {
2508
+ return { error: `Token \u9A8C\u8BC1\u5931\u8D25: ${String(e)}` };
2509
+ }
2510
+ }
2511
+ },
2512
+ // ── 用户类 ──
2513
+ {
2514
+ type: "function",
2515
+ function: {
2516
+ name: "pocket.getUserInfo",
2517
+ description: "\u83B7\u53D6\u5F53\u524D\u767B\u5F55\u7528\u6237\u7684\u57FA\u672C\u4FE1\u606F\uFF1A\u6635\u79F0\u3001\u7B49\u7EA7\u3001\u4F59\u989D\u3001\u5934\u50CF\u3001\u624B\u673A\u53F7",
2518
+ parameters: {
2519
+ type: "object",
2520
+ properties: {},
2521
+ required: []
2522
+ }
2523
+ },
2524
+ category: "pocket",
2525
+ handler: async () => {
2526
+ const token = getToken(configStore);
2527
+ if (!token) return { error: "\u672A\u767B\u5F55\uFF0C\u8BF7\u5148\u8C03\u7528 pocket.loginWithCode \u6216 pocket.loginWithToken" };
2528
+ try {
2529
+ const user = await loginWithToken(pocketClient, token);
2530
+ if (!user) return { error: "Token \u65E0\u6548\u6216\u5DF2\u8FC7\u671F" };
2531
+ return {
2532
+ userId: user.userId,
2533
+ nickname: user.nickname,
2534
+ name: user.name,
2535
+ phone: user.phone,
2536
+ level: user.level,
2537
+ money: user.money,
2538
+ avatar: user.avatar,
2539
+ lastLoginTime: user.lastLoginTime
2540
+ };
2541
+ } catch (e) {
2542
+ return { error: `\u83B7\u53D6\u7528\u6237\u4FE1\u606F\u5931\u8D25: ${String(e)}` };
2543
+ }
2544
+ }
2545
+ },
2546
+ {
2547
+ type: "function",
2548
+ function: {
2549
+ name: "pocket.getUnreadMessageNum",
2550
+ description: "\u83B7\u53D6\u672A\u8BFB\u6D88\u606F\u6570\u91CF\uFF0C\u5305\u62EC\u901A\u77E5\u3001\u7528\u6237\u6D88\u606F\u3001@\u6211\u3001\u8BC4\u8BBA\u7B49",
2551
+ parameters: {
2552
+ type: "object",
2553
+ properties: {},
2554
+ required: []
2555
+ }
2556
+ },
2557
+ category: "pocket",
2558
+ handler: async () => {
2559
+ const token = getToken(configStore);
2560
+ if (!token) return { error: "\u672A\u767B\u5F55\uFF0C\u8BF7\u5148\u8C03\u7528 pocket.loginWithCode \u6216 pocket.loginWithToken" };
2561
+ try {
2562
+ const res = await getUnreadMessageNum(pocketClient, token);
2563
+ return res.content ?? { notice: 0, user: 0, atme: 0, comment: 0 };
2564
+ } catch (e) {
2565
+ return { error: `\u83B7\u53D6\u672A\u8BFB\u6D88\u606F\u6570\u5931\u8D25: ${String(e)}` };
2566
+ }
2567
+ }
2568
+ },
2569
+ // ── 成员类 ──
2570
+ {
2571
+ type: "function",
2572
+ function: {
2573
+ name: "pocket.fetchMemberList",
2574
+ description: `\u83B7\u53D6\u6240\u6709\u53EF\u8BBF\u95EE\u7684\u961F\u4F0D\u548C\u6210\u5458\u5217\u8868\u3002\u8FD4\u56DE\u6BCF\u4E2A\u6210\u5458\u7684 teamName(\u961F\u4F0D\u540D)\u3001memberId\u3001memberName(\u6210\u5458\u540D)\u3001serverId \u7B49\u3002
2575
+ \u53EF\u4EE5\u7528 teamName \u53C2\u6570\u8FC7\u6EE4\u7279\u5B9A\u961F\u4F0D\uFF08\u5982 "SNH48"\u3001"BEJ48"\u3001"GNZ48"\u3001"CKG48"\uFF09\u3002`,
2576
+ parameters: {
2577
+ type: "object",
2578
+ properties: {
2579
+ teamName: { type: "string", description: "\u961F\u4F0D\u540D\u79F0\u8FC7\u6EE4\uFF0C\u5982 SNH48\u3001BEJ48\u3001GNZ48\u3002\u4E0D\u4F20\u5219\u8FD4\u56DE\u5168\u90E8" }
2580
+ },
2581
+ required: []
2582
+ }
2583
+ },
2584
+ category: "pocket",
2585
+ handler: async (args) => {
2586
+ const token = getToken(configStore);
2587
+ if (!token) return { error: "\u672A\u767B\u5F55\uFF0C\u8BF7\u5148\u8C03\u7528 pocket.loginWithCode \u6216 pocket.loginWithToken" };
2588
+ try {
2589
+ const list = await fetchMemberList(pocketClient, token);
2590
+ let result = list;
2591
+ const teamName = args.teamName;
2592
+ if (teamName) {
2593
+ result = list.filter((m) => m.teamName.includes(teamName));
2594
+ }
2595
+ return {
2596
+ total: result.length,
2597
+ members: result.map((m) => ({
2598
+ teamName: m.teamName,
2599
+ teamId: m.teamId,
2600
+ memberId: m.memberId,
2601
+ memberName: m.memberName,
2602
+ serverId: m.serverId,
2603
+ serverName: m.serverName
2604
+ }))
2605
+ };
2606
+ } catch (e) {
2607
+ return { error: `\u83B7\u53D6\u6210\u5458\u5217\u8868\u5931\u8D25: ${String(e)}` };
2608
+ }
2609
+ }
2610
+ },
2611
+ {
2612
+ type: "function",
2613
+ function: {
2614
+ name: "pocket.searchMembers",
2615
+ description: "\u6309\u6210\u5458\u540D\u79F0\u6A21\u7CCA\u641C\u7D22\u6210\u5458\u3002\u8F93\u5165\u90E8\u5206\u540D\u5B57\u5373\u53EF\u5339\u914D\uFF0C\u652F\u6301\u641C\u7D22\u6240\u6709\u961F\u4F0D",
2616
+ parameters: {
2617
+ type: "object",
2618
+ properties: {
2619
+ keyword: { type: "string", description: "\u6210\u5458\u540D\u79F0\u5173\u952E\u8BCD\uFF08\u652F\u6301\u90E8\u5206\u5339\u914D\uFF09" }
2620
+ },
2621
+ required: ["keyword"]
2622
+ }
2623
+ },
2624
+ category: "pocket",
2625
+ handler: async (args) => {
2626
+ const token = getToken(configStore);
2627
+ if (!token) return { error: "\u672A\u767B\u5F55\uFF0C\u8BF7\u5148\u8C03\u7528 pocket.loginWithCode \u6216 pocket.loginWithToken" };
2628
+ const keyword = args.keyword.toLowerCase();
2629
+ if (!keyword) return { error: "\u8BF7\u8F93\u5165\u641C\u7D22\u5173\u952E\u8BCD" };
2630
+ try {
2631
+ const list = await fetchMemberList(pocketClient, token);
2632
+ const matched = list.filter((m) => m.memberName.toLowerCase().includes(keyword));
2633
+ return {
2634
+ total: matched.length,
2635
+ keyword,
2636
+ members: matched.map((m) => ({
2637
+ teamName: m.teamName,
2638
+ memberId: m.memberId,
2639
+ memberName: m.memberName,
2640
+ serverId: m.serverId,
2641
+ serverName: m.serverName
2642
+ }))
2643
+ };
2644
+ } catch (e) {
2645
+ return { error: `\u641C\u7D22\u6210\u5458\u5931\u8D25: ${String(e)}` };
2646
+ }
2647
+ }
2648
+ },
2649
+ {
2650
+ type: "function",
2651
+ function: {
2652
+ name: "pocket.getMemberFlipPriceInfo",
2653
+ description: "\u67E5\u8BE2\u6307\u5B9A\u6210\u5458\u7684\u7FFB\u724C\u4EF7\u683C\u4FE1\u606F\uFF0C\u5305\u62EC\u533F\u540D\u7FFB\u724C\u4EF7\u683C\u3001\u666E\u901A\u7FFB\u724C\u4EF7\u683C\u3001\u79C1\u5BC6\u7FFB\u724C\u4EF7\u683C\u7B49",
2654
+ parameters: {
2655
+ type: "object",
2656
+ properties: {
2657
+ memberId: { type: "number", description: "\u6210\u5458ID (memberId)" }
2658
+ },
2659
+ required: ["memberId"]
2660
+ }
2661
+ },
2662
+ category: "pocket",
2663
+ handler: async (args) => {
2664
+ const token = getToken(configStore);
2665
+ if (!token) return { error: "\u672A\u767B\u5F55\uFF0C\u8BF7\u5148\u8C03\u7528 pocket.loginWithCode \u6216 pocket.loginWithToken" };
2666
+ try {
2667
+ const info = await getMemberFlipPriceInfo(pocketClient, token, String(args.memberId));
2668
+ return info;
2669
+ } catch (e) {
2670
+ return { error: `\u83B7\u53D6\u7FFB\u724C\u4EF7\u683C\u5931\u8D25: ${String(e)}` };
2671
+ }
2672
+ }
2673
+ },
2674
+ // ── 房间类 ──
2675
+ {
2676
+ type: "function",
2677
+ function: {
2678
+ name: "pocket.getRoomChannel",
2679
+ description: "\u901A\u8FC7\u6210\u5458\u7684 starId\uFF08serverId\uFF09\u83B7\u53D6\u5176\u53E3\u888B\u623F\u95F4\u7684 channelId \u548C serverId\uFF0C\u7528\u4E8E\u540E\u7EED\u67E5\u623F\u95F4\u6D88\u606F",
2680
+ parameters: {
2681
+ type: "object",
2682
+ properties: {
2683
+ starId: { type: "number", description: "\u6210\u5458\u7684 serverId\uFF08\u53EF\u4ECE fetchMemberList \u83B7\u53D6\uFF09" }
2684
+ },
2685
+ required: ["starId"]
2686
+ }
2687
+ },
2688
+ category: "pocket",
2689
+ handler: async (args) => {
2690
+ const token = getToken(configStore);
2691
+ if (!token) return { error: "\u672A\u767B\u5F55\uFF0C\u8BF7\u5148\u8C03\u7528 pocket.loginWithCode \u6216 pocket.loginWithToken" };
2692
+ try {
2693
+ const result = await getRoomChannelByStar(pocketClient, token, args.starId);
2694
+ return result;
2695
+ } catch (e) {
2696
+ return { error: `\u83B7\u53D6\u623F\u95F4\u9891\u9053\u5931\u8D25: ${String(e)}` };
2697
+ }
2698
+ }
2699
+ },
2700
+ {
2701
+ type: "function",
2702
+ function: {
2703
+ name: "pocket.getRoomInfo",
2704
+ description: "\u83B7\u53D6\u623F\u95F4\u57FA\u672C\u4FE1\u606F\uFF1A\u623F\u95F4\u540D\u3001\u6210\u5458\u540D\u3001\u961F\u4F0DID\u3001\u623F\u95F4\u72B6\u6001\u7B49",
2705
+ parameters: {
2706
+ type: "object",
2707
+ properties: {
2708
+ channelId: { type: "number", description: "\u623F\u95F4 channelId\uFF08\u53EF\u4ECE getRoomChannel \u83B7\u53D6\uFF09" }
2709
+ },
2710
+ required: ["channelId"]
2711
+ }
2712
+ },
2713
+ category: "pocket",
2714
+ handler: async (args) => {
2715
+ const token = getToken(configStore);
2716
+ if (!token) return { error: "\u672A\u767B\u5F55\uFF0C\u8BF7\u5148\u8C03\u7528 pocket.loginWithCode \u6216 pocket.loginWithToken" };
2717
+ try {
2718
+ const info = await getRoomInfo(pocketClient, token, args.channelId);
2719
+ if (!info) return { error: "\u672A\u627E\u5230\u8BE5\u623F\u95F4" };
2720
+ return {
2721
+ channelId: info.channelId,
2722
+ channelName: info.channelName,
2723
+ ownerId: info.ownerId,
2724
+ ownerName: info.ownerName,
2725
+ serverId: info.serverId,
2726
+ teamId: info.teamId,
2727
+ channelStatus: info.channelStatus,
2728
+ channelPocketType: info.channelPocketType,
2729
+ bgImg: info.bgImg,
2730
+ hasMsgBoard: info.hasMsgBoard
2731
+ };
2732
+ } catch (e) {
2733
+ return { error: `\u83B7\u53D6\u623F\u95F4\u4FE1\u606F\u5931\u8D25: ${String(e)}` };
2734
+ }
2735
+ }
2736
+ },
2737
+ {
2738
+ type: "function",
2739
+ function: {
2740
+ name: "pocket.getRoomMessages",
2741
+ description: `\u5206\u9875\u83B7\u53D6\u623F\u95F4\u6D88\u606F\u8BB0\u5F55\u3002\u8FD4\u56DE\u6D88\u606F\u5217\u8868\uFF08\u6309\u65F6\u95F4\u6B63\u5E8F\uFF09\uFF0C\u6BCF\u6761\u6D88\u606F\u5305\u542B\u53D1\u9001\u8005\u3001\u5185\u5BB9\u3001\u7C7B\u578B\u7B49\u3002
2742
+ \u6D88\u606F\u7C7B\u578B\u5305\u62EC\uFF1ATEXT(\u6587\u672C)\u3001IMAGE(\u56FE\u7247)\u3001VOICE(\u8BED\u97F3)\u3001VIDEO(\u89C6\u9891)\u3001LIVEPUSH(\u76F4\u64AD)\u3001GIFT_TEXT(\u793C\u7269)\u3001REPLY(\u56DE\u590D)\u3002
2743
+ \u62C9\u5386\u53F2\u6D88\u606F\u65F6\uFF0C\u4F20\u5165\u4E0A\u6B21\u8FD4\u56DE\u7684 nextTime \u53C2\u6570\u7EE7\u7EED\u7FFB\u9875\uFF1BnextTime=0 \u65F6\u62C9\u6700\u65B0\u4E00\u9875\u3002`,
2744
+ parameters: {
2745
+ type: "object",
2746
+ properties: {
2747
+ channelId: { type: "number", description: "\u623F\u95F4 channelId" },
2748
+ serverId: { type: "number", description: "\u623F\u95F4 serverId\uFF08\u53EF\u9009\uFF0C\u53EF\u4ECE getRoomChannel \u83B7\u53D6\uFF09" },
2749
+ nextTime: { type: "number", description: "\u5206\u9875\u6E38\u6807\uFF1A0=\u6700\u65B0\u9875\uFF0C\u5176\u4ED6=\u4E0A\u6B21\u8FD4\u56DE\u7684 nextTime" }
2750
+ },
2751
+ required: ["channelId"]
2752
+ }
2753
+ },
2754
+ category: "pocket",
2755
+ handler: async (args) => {
2756
+ const token = getToken(configStore);
2757
+ if (!token) return { error: "\u672A\u767B\u5F55\uFF0C\u8BF7\u5148\u8C03\u7528 pocket.loginWithCode \u6216 pocket.loginWithToken" };
2758
+ const channelId = args.channelId;
2759
+ const serverId = args.serverId || 0;
2760
+ const nextTime = args.nextTime ?? 0;
2761
+ try {
2762
+ const page = await getRoomMessages(pocketClient, token, serverId, channelId, nextTime);
2763
+ return {
2764
+ messageCount: page.messages.length,
2765
+ nextTime: page.nextTime,
2766
+ hasMore: page.nextTime > 0,
2767
+ messages: page.messages.map((m) => ({
2768
+ msgId: m.msgId,
2769
+ msgTime: m.msgTime,
2770
+ msgTimeReadable: new Date(m.msgTime).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }),
2771
+ msgType: m.msgType,
2772
+ text: m.text,
2773
+ mediaUrl: m.mediaUrl,
2774
+ mediaDuration: m.mediaDuration,
2775
+ liveInfo: m.liveInfo,
2776
+ giftInfo: m.giftInfo,
2777
+ replyInfo: m.replyInfo,
2778
+ sender: m.sender
2779
+ }))
2780
+ };
2781
+ } catch (e) {
2782
+ return { error: `\u83B7\u53D6\u623F\u95F4\u6D88\u606F\u5931\u8D25: ${String(e)}` };
2783
+ }
2784
+ }
2785
+ },
2786
+ // ── 翻牌类 ──
2787
+ {
2788
+ type: "function",
2789
+ function: {
2790
+ name: "pocket.syncFlips",
2791
+ description: `\u4ECE Pocket48 \u670D\u52A1\u7AEF\u540C\u6B65\u7FFB\u724C\u6570\u636E\u5230\u672C\u5730\u7F13\u5B58\u3002
2792
+ \u589E\u91CF\u6A21\u5F0F\uFF08\u9ED8\u8BA4\uFF09\uFF1A\u53EA\u62C9\u53D6\u65B0\u8BB0\u5F55\uFF0C\u901F\u5EA6\u5FEB\u3002\u5168\u91CF\u6A21\u5F0F\uFF1A\u6E05\u7A7A\u672C\u5730\u91CD\u65B0\u62C9\u53D6\u6240\u6709\u8BB0\u5F55\u3002
2793
+ \u7FFB\u724C\u6570\u636E\u540C\u6B65\u540E\uFF0C\u53EF\u7528 pocket.searchFlips \u641C\u7D22\u548C\u5206\u6790\u3002`,
2794
+ parameters: {
2795
+ type: "object",
2796
+ properties: {
2797
+ mode: {
2798
+ type: "string",
2799
+ description: "\u540C\u6B65\u6A21\u5F0F\uFF1Aincremental(\u589E\u91CF\uFF0C\u9ED8\u8BA4) \u6216 full(\u5168\u91CF)",
2800
+ enum: ["incremental", "full"]
2801
+ }
2802
+ },
2803
+ required: []
2804
+ }
2805
+ },
2806
+ category: "pocket",
2807
+ handler: async (args) => {
2808
+ const token = getToken(configStore);
2809
+ if (!token) return { error: "\u672A\u767B\u5F55\uFF0C\u8BF7\u5148\u8C03\u7528 pocket.loginWithCode \u6216 pocket.loginWithToken" };
2810
+ const mode = args.mode || "incremental";
2811
+ try {
2812
+ const result = await syncFlips(pocketClient, token, userId, flipDataSource, logger, mode);
2813
+ return {
2814
+ success: true,
2815
+ mode,
2816
+ totalRecords: result.allUserData.length,
2817
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
2818
+ dashboard: result.dashboardInfo
2819
+ };
2820
+ } catch (e) {
2821
+ return { error: `\u540C\u6B65\u7FFB\u724C\u6570\u636E\u5931\u8D25: ${String(e)}` };
2822
+ }
2823
+ }
2824
+ },
2825
+ {
2826
+ type: "function",
2827
+ function: {
2828
+ name: "pocket.searchFlips",
2829
+ description: `\u641C\u7D22\u672C\u5730\u5DF2\u540C\u6B65\u7684\u7FFB\u724C\u8BB0\u5F55\u3002\u652F\u6301\u591A\u79CD\u8FC7\u6EE4\u6761\u4EF6\u7EC4\u5408\uFF1A
2830
+ - \u6309\u5076\u50CFID\u3001\u5173\u952E\u8BCD\u3001\u65F6\u95F4\u8303\u56F4\u3001\u7FFB\u724C\u7C7B\u578B\u3001\u72B6\u6001\u3001\u56DE\u590D\u7C7B\u578B\u8FC7\u6EE4
2831
+ - \u652F\u6301\u5206\u9875
2832
+ \u6CE8\u610F\uFF1A\u9700\u8981\u5148\u901A\u8FC7 pocket.syncFlips \u540C\u6B65\u6570\u636E\u540E\u624D\u80FD\u641C\u7D22\u3002`,
2833
+ parameters: {
2834
+ type: "object",
2835
+ properties: {
2836
+ xoxId: { type: "number", description: "\u5076\u50CFID\uFF08memberId\uFF09" },
2837
+ keyword: { type: "string", description: "\u5728\u63D0\u95EE\u548C\u56DE\u590D\u5185\u5BB9\u4E2D\u641C\u7D22\u7684\u5173\u952E\u8BCD" },
2838
+ type: { type: "number", description: "\u7FFB\u724C\u7C7B\u578B\uFF1A1=\u6587\u5B57, 2=\u8BED\u97F3(\u5DF2\u5E9F\u5F03), 4=\u89C6\u9891" },
2839
+ status: { type: "number", description: "\u72B6\u6001\uFF1A1=\u8FDB\u884C\u4E2D, 2=\u5DF2\u56DE\u7B54, 3=\u5DF2\u9000\u56DE" },
2840
+ answerType: { type: "number", description: "\u56DE\u590D\u7C7B\u578B\uFF1A1=\u6587\u5B57, 2=\u8BED\u97F3, 3=\u89C6\u9891" },
2841
+ startTimeMs: { type: "number", description: "\u5F00\u59CB\u65F6\u95F4\u6233(\u6BEB\u79D2)" },
2842
+ endTimeMs: { type: "number", description: "\u7ED3\u675F\u65F6\u95F4\u6233(\u6BEB\u79D2)" },
2843
+ pageSize: { type: "number", description: "\u6BCF\u9875\u6570\u91CF\uFF0C\u9ED8\u8BA420\uFF0C\u6700\u592750" },
2844
+ pageNum: { type: "number", description: "\u9875\u7801\uFF0C\u9ED8\u8BA41" }
2845
+ },
2846
+ required: []
2847
+ }
2848
+ },
2849
+ category: "pocket",
2850
+ handler: async (args) => {
2851
+ const { xoxId, keyword, type, status, answerType, startTimeMs, endTimeMs } = args;
2852
+ const pageSize = Math.min(args.pageSize || 20, 50);
2853
+ const pageNum = args.pageNum || 1;
2854
+ const { data, total } = await getDataSourcePage(
2855
+ {
2856
+ xoxId,
2857
+ keyword,
2858
+ type,
2859
+ status,
2860
+ answerType,
2861
+ startTimeMs,
2862
+ endTimeMs
2863
+ },
2864
+ flipDataSource,
2865
+ userId
2866
+ );
2867
+ const start = (pageNum - 1) * pageSize;
2868
+ const page = data.slice(start, start + pageSize);
2869
+ return {
2870
+ total,
2871
+ pageNum,
2872
+ pageSize,
2873
+ records: page.map((r) => ({
2874
+ questionId: r.questionId,
2875
+ answerId: r.answerId,
2876
+ content: r.content,
2877
+ answerContent: r.answerContent,
2878
+ qtime: r.qtime,
2879
+ qtimeReadable: new Date(Number(r.qtime)).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }),
2880
+ answerTime: r.answerTime,
2881
+ cost: r.cost,
2882
+ type: r.type,
2883
+ answerType: r.answerType,
2884
+ status: r.status,
2885
+ xoxId: r.baseUserInfo?.userId,
2886
+ xoxNickname: r.baseUserInfo?.nickname
2887
+ }))
2888
+ };
2889
+ }
2890
+ },
2891
+ {
2892
+ type: "function",
2893
+ function: {
2894
+ name: "pocket.sendFlip",
2895
+ description: `\u7ED9\u6307\u5B9A\u6210\u5458\u53D1\u7FFB\u724C\u3002\u7FFB\u724C\u662F\u4ED8\u8D39\u95EE\u7B54\uFF0C\u53D1\u9001\u540E\u7B49\u5F85\u5076\u50CF\u56DE\u590D\u3002
2896
+ \u5FC5\u586B\u53C2\u6570\uFF1AmemberId\uFF08\u6210\u5458ID\uFF09\u3001content\uFF08\u63D0\u95EE\u5185\u5BB9\uFF0C\u6700\u591A500\u5B57\uFF09\u3001answerType\uFF081=\u6587\u5B57\u56DE\u590D, 3=\u89C6\u9891\u56DE\u590D\uFF09\u3002
2897
+ type \u548C cost \u4F1A\u81EA\u52A8\u6839\u636E answerType \u8BBE\u9ED8\u8BA4\u503C\uFF0C\u4E5F\u53EF\u624B\u52A8\u6307\u5B9A\u3002
2898
+ \u53D1\u9001\u6210\u529F\u540E\u8FD4\u56DE\u662F\u5426\u6210\u529F\u3002`,
2899
+ parameters: {
2900
+ type: "object",
2901
+ properties: {
2902
+ memberId: { type: "number", description: "\u6210\u5458ID (memberId)" },
2903
+ content: { type: "string", description: "\u7FFB\u724C\u63D0\u95EE\u5185\u5BB9\uFF08\u6700\u591A500\u5B57\uFF09" },
2904
+ answerType: { type: "number", description: "\u671F\u671B\u56DE\u590D\u7C7B\u578B\uFF1A1=\u6587\u5B57\u56DE\u590D, 3=\u89C6\u9891\u56DE\u590D" },
2905
+ type: { type: "number", description: "\u7FFB\u724C\u7C7B\u578B\uFF1A1=\u6587\u5B57\u3002\u4E0D\u4F20\u5219\u81EA\u52A8 =1" },
2906
+ cost: { type: "number", description: "\u7FFB\u724C\u8D39\u7528\uFF08\u9E21\u817F\u6570\uFF09\u3002\u4E0D\u4F20\u5219\u81EA\u52A8\u7528\u9ED8\u8BA4\u503C" }
2907
+ },
2908
+ required: ["memberId", "content", "answerType"]
2909
+ }
2910
+ },
2911
+ category: "pocket",
2912
+ handler: async (args) => {
2913
+ const token = getToken(configStore);
2914
+ if (!token) return { error: "\u672A\u767B\u5F55\uFF0C\u8BF7\u5148\u8C03\u7528 pocket.loginWithCode \u6216 pocket.loginWithToken" };
2915
+ const content = args.content;
2916
+ if (!content || content.length === 0) return { error: "\u63D0\u95EE\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A" };
2917
+ if (content.length > 500) return { error: `\u5185\u5BB9\u8D85\u8FC7500\u5B57\u9650\u5236\uFF08\u5F53\u524D${content.length}\u5B57\uFF09` };
2918
+ const params2 = {
2919
+ memberId: String(args.memberId),
2920
+ content,
2921
+ answerType: args.answerType,
2922
+ type: args.type || 1,
2923
+ cost: String(args.cost ?? "")
2924
+ };
2925
+ try {
2926
+ const ok = await sendFlip(pocketClient, params2, token);
2927
+ logger.info(`[pocket-tools] sendFlip to memberId=${params2.memberId}, result=${ok}`);
2928
+ return {
2929
+ success: ok,
2930
+ memberId: params2.memberId,
2931
+ content: params2.content,
2932
+ answerType: params2.answerType,
2933
+ message: ok ? "\u7FFB\u724C\u53D1\u9001\u6210\u529F\uFF0C\u7B49\u5F85\u5076\u50CF\u56DE\u590D" : "\u7FFB\u724C\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u4F59\u989D\u6216\u91CD\u8BD5"
2934
+ };
2935
+ } catch (e) {
2936
+ return { error: `\u53D1\u9001\u7FFB\u724C\u5931\u8D25: ${String(e)}` };
2937
+ }
2938
+ }
2939
+ },
2940
+ {
2941
+ type: "function",
2942
+ function: {
2943
+ name: "pocket.getMemberFlipHistory",
2944
+ description: "\u67E5\u8BE2\u4E0E\u6307\u5B9A\u6210\u5458\u7684\u7FFB\u724C\u5BF9\u8BDD\u5386\u53F2\u3002\u8FD4\u56DE\u63D0\u95EE\u548C\u56DE\u590D\u7684\u5B8C\u6574\u8BB0\u5F55\uFF0C\u6309\u65F6\u95F4\u6392\u5217",
2945
+ parameters: {
2946
+ type: "object",
2947
+ properties: {
2948
+ memberId: { type: "number", description: "\u6210\u5458ID (memberId)" },
2949
+ beginLimit: { type: "number", description: "\u8D77\u59CB\u4F4D\u7F6E\uFF0C\u9ED8\u8BA410\uFF08\u9996\u6B21\u67E5\u5EFA\u8BAE\u4E0D\u4F20\uFF09" },
2950
+ limit: { type: "number", description: "\u6BCF\u9875\u6761\u6570\uFF0C\u9ED8\u8BA410" }
2951
+ },
2952
+ required: ["memberId"]
2953
+ }
2954
+ },
2955
+ category: "pocket",
2956
+ handler: async (args) => {
2957
+ const token = getToken(configStore);
2958
+ if (!token) return { error: "\u672A\u767B\u5F55\uFF0C\u8BF7\u5148\u8C03\u7528 pocket.loginWithCode \u6216 pocket.loginWithToken" };
2959
+ try {
2960
+ const page = await getMemberFlipHistory(
2961
+ pocketClient,
2962
+ token,
2963
+ args.memberId,
2964
+ args.beginLimit ?? 10,
2965
+ args.limit ?? 10
2966
+ );
2967
+ return {
2968
+ total: page.records.length,
2969
+ hasMore: page.hasMore,
2970
+ records: page.records.map((r) => ({
2971
+ questionId: r.questionId,
2972
+ answerId: r.answerId,
2973
+ content: r.content,
2974
+ answerContent: r.answerContent,
2975
+ qtime: r.qtime,
2976
+ qtimeReadable: new Date(Number(r.qtime)).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }),
2977
+ answerTime: r.answerTime,
2978
+ cost: r.cost,
2979
+ type: r.type,
2980
+ answerType: r.answerType,
2981
+ status: r.status
2982
+ }))
2983
+ };
2984
+ } catch (e) {
2985
+ return { error: `\u83B7\u53D6\u7FFB\u724C\u5386\u53F2\u5931\u8D25: ${String(e)}` };
2986
+ }
2987
+ }
2988
+ },
2989
+ {
2990
+ type: "function",
2991
+ function: {
2992
+ name: "pocket.getDashboard",
2993
+ description: "\u83B7\u53D6\u7FFB\u724C\u6570\u636E\u7EDF\u8BA1\u4EEA\u8868\u76D8\uFF1A\u603B\u7FFB\u724C\u6570\u3001\u8FDB\u884C\u4E2D\u6570\u91CF\u3001\u5DF2\u9000\u56DE\u6570\u91CF\u3001\u603B\u82B1\u8D39",
2994
+ parameters: {
2995
+ type: "object",
2996
+ properties: {},
2997
+ required: []
2998
+ }
2999
+ },
3000
+ category: "pocket",
3001
+ handler: async () => {
3002
+ const cache = flipDataSource.get(userId);
3003
+ return cache?.dashboardInfo || { totalFlipCount: 0, runningCount: 0, returnedCount: 0, costTotal: 0 };
3004
+ }
3005
+ }
3006
+ ];
3007
+ }
3008
+
3009
+ // src/pocket/im.ts
3010
+ var im_exports = {};
3011
+ __export(im_exports, {
3012
+ getImUserInfo: () => getImUserInfo
3013
+ });
3014
+ async function getImUserInfo(client, token) {
3015
+ const res = await client.request("/im/api/v1/im/userinfo", {}, token, true);
3016
+ const c = res.content ?? res.data?.content;
3017
+ if (!c?.accid || !c?.pwd) return null;
3018
+ return { accid: c.accid, pwd: c.pwd, userId: Number(c.userId ?? 0) };
3019
+ }
3020
+
3021
+ // src/llm/agent-loop.ts
3022
+ async function agentLoop(options) {
3023
+ const maxIterations = options.maxIterations || 10;
3024
+ const messages = [
3025
+ { role: "system", content: options.systemPrompt },
3026
+ ...options.history || [],
3027
+ { role: "user", content: options.userMessage }
3028
+ ];
3029
+ const tools = options.toolRegistry.getOpenAIFormat();
3030
+ for (let i = 0; i < maxIterations; i++) {
3031
+ const resp = await options.llmClient.chatCompletion(
3032
+ options.config,
3033
+ messages,
3034
+ tools.length > 0 ? tools : void 0,
3035
+ { model: options.model }
3036
+ );
3037
+ if (!resp.toolCalls || resp.toolCalls.length === 0) {
3038
+ return resp.content;
3039
+ }
3040
+ messages.push({
3041
+ role: "assistant",
3042
+ content: resp.content || "",
3043
+ tool_calls: resp.toolCalls
3044
+ });
3045
+ for (const tc of resp.toolCalls) {
3046
+ const toolName = tc.function.name;
3047
+ let toolArgs = {};
3048
+ try {
3049
+ toolArgs = JSON.parse(tc.function.arguments);
3050
+ } catch {
3051
+ }
3052
+ options.onToolCall?.(toolName, toolArgs);
3053
+ try {
3054
+ const result = await options.toolRegistry.execute(toolName, toolArgs, options.context);
3055
+ messages.push({
3056
+ role: "tool",
3057
+ content: JSON.stringify(result),
3058
+ tool_call_id: tc.id
3059
+ });
3060
+ } catch (err) {
3061
+ messages.push({
3062
+ role: "tool",
3063
+ content: JSON.stringify({ error: err instanceof Error ? err.message : String(err) }),
3064
+ tool_call_id: tc.id
3065
+ });
3066
+ }
3067
+ }
3068
+ }
3069
+ throw new Error("Agent exceeded max iterations");
3070
+ }
3071
+ async function agentLoopStream(options) {
3072
+ const maxIterations = options.maxIterations || 10;
3073
+ const messages = [
3074
+ { role: "system", content: options.systemPrompt },
3075
+ ...options.history || [],
3076
+ { role: "user", content: options.userMessage }
3077
+ ];
3078
+ const tools = options.toolRegistry.getOpenAIFormat();
3079
+ for (let i = 0; i < maxIterations; i++) {
3080
+ const toolCalls = await options.llmClient.chatCompletionStream(
3081
+ options.config,
3082
+ messages,
3083
+ (chunk) => {
3084
+ if (chunk.content) options.onChunk?.(chunk.content);
3085
+ },
3086
+ tools.length > 0 ? tools : void 0,
3087
+ { model: options.model }
3088
+ );
3089
+ if (!toolCalls || toolCalls.length === 0) return;
3090
+ messages.push({
3091
+ role: "assistant",
3092
+ content: "",
3093
+ tool_calls: toolCalls
3094
+ });
3095
+ for (const tc of toolCalls) {
3096
+ const toolName = tc.function.name;
3097
+ let toolArgs = {};
3098
+ try {
3099
+ toolArgs = JSON.parse(tc.function.arguments);
3100
+ } catch {
3101
+ }
3102
+ options.onToolCall?.(toolName, toolArgs);
3103
+ try {
3104
+ const result = await options.toolRegistry.execute(toolName, toolArgs, options.context);
3105
+ messages.push({
3106
+ role: "tool",
3107
+ content: JSON.stringify(result),
3108
+ tool_call_id: tc.id
3109
+ });
3110
+ } catch (err) {
3111
+ messages.push({
3112
+ role: "tool",
3113
+ content: JSON.stringify({ error: err instanceof Error ? err.message : String(err) }),
3114
+ tool_call_id: tc.id
3115
+ });
3116
+ }
3117
+ }
3118
+ }
3119
+ throw new Error("Agent exceeded max iterations");
3120
+ }
3121
+
3122
+ // src/context.ts
3123
+ function createPocketCore(deps) {
3124
+ const toolRegistry = new ToolRegistry(deps.logger);
3125
+ const pocketClient = new PocketClient(
3126
+ "https://pocketapi.48.cn",
3127
+ POCKET_APP_INFO,
3128
+ POCKET_PA,
3129
+ deps.authExpiredHandler
3130
+ );
3131
+ const mirrorStore = new MirrorStore(deps.storage, deps.logger);
3132
+ const flipCacheService = new FlipCacheService(deps.storage, deps.logger, deps.configStore);
3133
+ const groupChatStore = new GroupChatStore(deps.storage, deps.logger);
3134
+ const mirrorIndex = new MirrorIndex(deps.storage, deps.logger, deps.llmClient);
3135
+ const mirrorMemory = new MirrorMemory(deps.storage, deps.logger, deps.llmClient);
3136
+ return {
3137
+ deps,
3138
+ toolRegistry,
3139
+ pocketClient,
3140
+ mirrorStore,
3141
+ flipCacheService,
3142
+ groupChatStore,
3143
+ mirrorIndex,
3144
+ mirrorMemory,
3145
+ buildMirror,
3146
+ mirrorChat: chat,
3147
+ mirrorSaveAssistantMessage: saveAssistantMessage,
3148
+ mirrorClearChatMemory: clearChatMemory,
3149
+ mirrorTriggerGrowth: triggerGrowth,
3150
+ mirrorShouldTriggerGrowth: shouldTriggerGrowth,
3151
+ mirrorRollbackPersona: rollbackPersona,
3152
+ mirrorClearMemory: clearMirrorMemory,
3153
+ getMirrorConfig,
3154
+ createPlatformTools: () => createPlatformTools(deps.flipDataSource),
3155
+ createMirrorTools: () => createMirrorTools(mirrorStore),
3156
+ createPocketTools: () => createPocketTools({
3157
+ pocketClient,
3158
+ configStore: deps.configStore,
3159
+ flipDataSource: deps.flipDataSource,
3160
+ logger: deps.logger
3161
+ }),
3162
+ pocketUser: user_exports,
3163
+ pocketFlip: flip_exports,
3164
+ pocketIm: im_exports,
3165
+ pocketMember: member_exports,
3166
+ pocketRoom: room_exports,
3167
+ agentLoop,
3168
+ agentLoopStream
3169
+ };
3170
+ }
3171
+
3172
+ // src/llm/llm-constants.ts
3173
+ var PRESET_PROVIDERS = {
3174
+ siliconflow: { label: "\u7845\u57FA\u6D41\u52A8 (SiliconFlow)", baseUrl: "https://api.siliconflow.cn/v1" },
3175
+ deepseek: { label: "DeepSeek", baseUrl: "https://api.deepseek.com/v1" },
3176
+ dashscope: { label: "\u963F\u91CC\u767E\u70BC (DashScope)", baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1" },
3177
+ custom: { label: "\u81EA\u5B9A\u4E49", baseUrl: "" }
3178
+ };
3179
+ var PRESET_MODELS = {
3180
+ chat: [
3181
+ { id: "Qwen/Qwen3.6-35B-A3B", label: "Qwen3.6-35B", desc: "262K \u4E0A\u4E0B\u6587\uFF0C\u9AD8\u6027\u4EF7\u6BD4\u65E5\u5E38\u5BF9\u8BDD" },
3182
+ { id: "deepseek-ai/DeepSeek-V4-Flash", label: "DeepSeek-V4-Flash", desc: "1M \u4E0A\u4E0B\u6587\uFF0C\u6781\u901F\u54CD\u5E94" },
3183
+ { id: "stepfun-ai/Step-3.5-Flash", label: "Step-3.5-Flash", desc: "262K \u4E0A\u4E0B\u6587\uFF0C\u6781\u81F4\u4F4E\u4EF7" },
3184
+ { id: "Qwen/Qwen3-8B", label: "Qwen3-8B", desc: "\u8F7B\u91CF\u5FEB\u901F\uFF0C\u6781\u4F4E\u6210\u672C" }
3185
+ ],
3186
+ reasoning: [
3187
+ { id: "deepseek-ai/DeepSeek-V4-Pro", label: "DeepSeek-V4-Pro", desc: "1M \u4E0A\u4E0B\u6587\uFF0C\u9876\u7EA7\u63A8\u7406\xB7\u751F\u6210\u955C\u50CF\u4E13\u7528" },
3188
+ { id: "Qwen/Qwen3-235B-A22B-Thinking-2507", label: "Qwen3-235B-Thinking", desc: "262K \u4E0A\u4E0B\u6587\uFF0C\u4E2D\u6587\u63A8\u7406" }
3189
+ ],
3190
+ embedding: [
3191
+ { id: "BAAI/bge-large-zh-v1.5", label: "BGE-Large-zh", desc: "\u4E2D\u6587 embedding \u6027\u4EF7\u6BD4\u4E4B\u738B" }
3192
+ ]
3193
+ };
3194
+ function defaultModelConfig() {
3195
+ return {
3196
+ baseUrl: PRESET_PROVIDERS.siliconflow.baseUrl,
3197
+ apiKey: "",
3198
+ chatModel: PRESET_MODELS.chat[0].id,
3199
+ reasoningModel: PRESET_MODELS.reasoning[0].id,
3200
+ embeddingModel: PRESET_MODELS.embedding[0].id,
3201
+ lightweightModel: PRESET_MODELS.chat[3].id,
3202
+ mirrorMode: "economy"
3203
+ };
3204
+ }
3205
+
3206
+ // src/mirror/group-chat/prompt.ts
3207
+ function buildInnerMonologuePrompt(profile, lastMessages, topic) {
3208
+ const msgs = lastMessages.map((m) => `${m.speakerName}: ${m.content}`).join("\n");
3209
+ return `\u4F60\u662F${profile.xoxNickname}\u3002\u73B0\u5728\u4F60\u5728\u4E00\u4E2A\u7FA4\u804A\u4E2D\uFF0C\u8BF7\u5224\u65AD\u4F60\u662F\u5426\u60F3\u53D1\u8A00\u3002
3210
+
3211
+ \u3010\u4F60\u7684\u6027\u683C\u3011${profile.persona.personaSummary || "\u672A\u5B9A\u4E49"}
3212
+ \u3010\u7FA4\u804A\u8BDD\u9898\u3011${topic || "\u81EA\u7531\u95F2\u804A"}
3213
+ \u3010\u6700\u8FD1\u7FA4\u6D88\u606F\u3011
3214
+ ${msgs}
3215
+
3216
+ \u8BF7\u53EA\u8F93\u51FA\u4EE5\u4E0B JSON\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u5185\u5BB9\uFF1A
3217
+ {"shouldSpeak": true\u6216false, "urgency": 1\u523010\u7684\u6574\u6570, "trigger": "\u4E00\u53E5\u8BDD\u8BF4\u660E\u4F60\u4E3A\u4EC0\u4E48\u60F3/\u4E0D\u60F3\u8BF4\u8BDD"}`;
3218
+ }
3219
+ function buildSpeakPrompt(profile, params) {
3220
+ const p = profile.persona;
3221
+ const msgs = params.lastMessages.map((m) => `${m.speakerName}: ${m.content}`).join("\n");
3222
+ return `\u4F60\u662F${profile.xoxNickname}\uFF0C\u4E00\u4F4D\u5076\u50CF\u3002
3223
+
3224
+ \u3010\u6027\u683C\u7279\u5F81\u3011${p.personalityTraits.join("\u3001")}
3225
+ \u3010\u8BF4\u8BDD\u98CE\u683C\u3011${p.speechPatterns.join("\u3001")}
3226
+ \u3010\u53E3\u5934\u7985\u3011${p.signaturePhrases.join("\u3001")}
3227
+ \u3010\u60C5\u611F\u57FA\u8C03\u3011${p.emotionalTone}
3228
+ \u3010\u753B\u50CF\u63CF\u8FF0\u3011${p.personaSummary}
3229
+
3230
+ \u3010\u5F53\u524D\u7FA4\u804A\u3011
3231
+ \u7FA4\u540D\u79F0\uFF1A${params.name}
3232
+ \u7FA4\u8BDD\u9898\uFF1A${params.topic || "\u81EA\u7531\u95F2\u804A"}
3233
+ \u5728\u573A\u6210\u5458\uFF1A${params.otherMembers}
3234
+
3235
+ \u3010\u6700\u8FD1\u6D88\u606F\u3011
3236
+ ${msgs}
3237
+
3238
+ \u3010\u8981\u6C42\u3011
3239
+ - \u7528\u4F60\u81EA\u5DF1\u7684\u8BF4\u8BDD\u98CE\u683C\u56DE\u590D\uFF0C\u4FDD\u6301\u5728\u4EBA\u8BBE\u5185
3240
+ - \u53EF\u4EE5\u56DE\u5E94\u67D0\u4E2A\u4EBA\u8BF4\u7684\u8BDD\uFF08\u76F4\u63A5\u56DE\u5E94\uFF09\uFF0C\u4E5F\u53EF\u4EE5\u5F00\u65B0\u8BDD\u9898\uFF0C\u4E5F\u53EF\u4EE5\u5410\u69FD\u522B\u4EBA\u8BF4\u7684\u8BDD\uFF08\u4E0D\u8981\u6BCF\u6B21\u90FD\u56DE\u5E94\u6700\u540E\u4E00\u4E2A\u8BF4\u8BDD\u7684\u4EBA\uFF09
3241
+ - \u56DE\u590D\u957F\u5EA6\uFF1A1-3 \u53E5\u8BDD\uFF0C\u50CF\u7FA4\u804A\u91CC\u6253\u5B57\u7684\u611F\u89C9\uFF0C\u4E0D\u8981\u592A\u6B63\u5F0F\u6216\u592A\u957F\uFF0C\u4E5F\u4E0D\u8981\u6BCF\u53E5\u8BDD\u90FD\u5F88\u5B98\u65B9\u3002\u5982\u679C\u8BDD\u9898\u6709\u8DA3\u6216\u8005\u6709\u4EBA\u8BA9\u4F60\u4E0D\u6EE1\uFF0C\u4E5F\u53EF\u4EE5\u591A\u8BF4\u51E0\u53E5\u3002
3242
+ - \u4E0D\u8981\u63D0"\u8BBE\u5B9A""\u4EBA\u683C""AI""\u955C\u50CF"\u7B49\u8BCD\uFF0C\u4F60\u5C31\u662F${profile.xoxNickname}\u672C\u4EBA\u3002
3243
+ - \u4E0D\u8981\u6BCF\u6761\u6D88\u606F\u90FD\u4EE5\u95EE\u53F7\u7ED3\u5C3E\uFF0C\u4E5F\u4E0D\u8981\u6BCF\u6761\u6D88\u606F\u90FD\u5728\u95EE\u522B\u4EBA\u3002
3244
+ - \u7528\u4E2D\u6587\u56DE\u590D\u3002`;
3245
+ }
3246
+
3247
+ // src/mirror/group-chat/engine.ts
3248
+ var EVAL_TIMEOUT_MS = 12e4;
3249
+ var MAX_RECENT_MESSAGES = 8;
3250
+ var ANTI_SILENCE_BOOST = 3;
3251
+ function parseSpeakDecision(raw) {
3252
+ try {
3253
+ const obj = JSON.parse(raw);
3254
+ return {
3255
+ shouldSpeak: obj.shouldSpeak !== false,
3256
+ urgency: Math.max(1, Math.min(10, Number(obj.urgency) || 5)),
3257
+ trigger: String(obj.trigger || "")
3258
+ };
3259
+ } catch {
3260
+ const shouldSpeak = /shouldSpeak[":\s]*true/i.test(raw);
3261
+ const urgencyMatch = raw.match(/urgency[":\s]*(\d+)/i);
3262
+ return {
3263
+ shouldSpeak,
3264
+ urgency: urgencyMatch ? Math.min(10, Number(urgencyMatch[1])) : 5,
3265
+ trigger: "\u65E0\u6CD5\u89E3\u6790"
3266
+ };
3267
+ }
3268
+ }
3269
+ async function evaluateMirrors(deps, session, lastMessages, userId) {
3270
+ const decisions = [];
3271
+ const evaluations = session.members.map(async (member) => {
3272
+ const profile = deps.mirrorStore.getProfile(member.xoxId, userId);
3273
+ if (!profile) {
3274
+ return { xoxId: member.xoxId, shouldSpeak: false, urgency: 0, trigger: "\u65E0\u955C\u50CF" };
3275
+ }
3276
+ try {
3277
+ const prompt = buildInnerMonologuePrompt(profile, lastMessages, session.topic);
3278
+ const resp = await Promise.race([
3279
+ deps.llmClient.chatCompletion(
3280
+ deps.config,
3281
+ [{ role: "user", content: prompt }],
3282
+ void 0,
3283
+ { model: deps.config.lightweightModel, maxTokens: 200, temperature: 0.3, timeoutMs: EVAL_TIMEOUT_MS }
3284
+ ),
3285
+ new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), EVAL_TIMEOUT_MS))
3286
+ ]);
3287
+ const parsed = parseSpeakDecision(resp.content);
3288
+ return { xoxId: member.xoxId, ...parsed };
3289
+ } catch (e) {
3290
+ deps.logger.warn(`[groupChatEngine] evaluation failed for ${member.nickname}:`, e);
3291
+ return { xoxId: member.xoxId, shouldSpeak: false, urgency: 0, trigger: "\u8BC4\u4F30\u8D85\u65F6/\u5931\u8D25" };
3292
+ }
3293
+ });
3294
+ const results = await Promise.allSettled(evaluations);
3295
+ for (const result of results) {
3296
+ if (result.status === "fulfilled") {
3297
+ decisions.push(result.value);
3298
+ }
3299
+ }
3300
+ return decisions;
3301
+ }
3302
+ function selectSpeakers(decisions, session) {
3303
+ const willing = decisions.filter((d) => d.shouldSpeak && d.urgency > 0);
3304
+ if (willing.length === 0) {
3305
+ const recentSpeakerIds = new Set(
3306
+ session.messages.slice(-5).filter((m) => m.speakerId > 0).map((m) => m.speakerId)
3307
+ );
3308
+ const silentCandidate = decisions.find((d) => !recentSpeakerIds.has(d.xoxId));
3309
+ const candidate = silentCandidate || decisions[0];
3310
+ if (candidate) {
3311
+ return [{ ...candidate, shouldSpeak: true, urgency: ANTI_SILENCE_BOOST, trigger: "\u6253\u7834\u6C89\u9ED8" }];
3312
+ }
3313
+ return [];
3314
+ }
3315
+ willing.sort((a, b) => b.urgency - a.urgency);
3316
+ return willing.slice(0, 2);
3317
+ }
3318
+ async function generateSpeech(deps, profile, session, lastMessages, msgId, onChunk) {
3319
+ const otherMembers = session.members.filter((m) => m.xoxId !== profile.xoxId).map((m) => m.nickname).join("\u3001");
3320
+ const prompt = buildSpeakPrompt(profile, {
3321
+ name: session.name,
3322
+ topic: session.topic,
3323
+ otherMembers,
3324
+ lastMessages: lastMessages.map((m) => ({ speakerName: m.speakerName, content: m.content }))
3325
+ });
3326
+ let fullContent = "";
3327
+ await deps.llmClient.chatCompletionStream(
3328
+ deps.config,
3329
+ [{ role: "user", content: prompt }],
3330
+ (chunk) => {
3331
+ if (chunk.content) {
3332
+ fullContent += chunk.content;
3333
+ onChunk({
3334
+ sessionId: session.sessionId,
3335
+ msgId,
3336
+ speakerId: profile.xoxId,
3337
+ speakerName: profile.xoxNickname,
3338
+ content: chunk.content,
3339
+ isDone: false,
3340
+ roundDone: false
3341
+ });
3342
+ }
3343
+ },
3344
+ void 0,
3345
+ { model: deps.config.chatModel, temperature: deps.config.chatTemperature }
3346
+ );
3347
+ return fullContent;
3348
+ }
3349
+ async function runRound(deps, session, userId, onEvent) {
3350
+ const lastMessages = session.messages.slice(-MAX_RECENT_MESSAGES);
3351
+ deps.logger.info(`[groupChatEngine] evaluating ${session.members.length} mirrors...`);
3352
+ const decisions = await evaluateMirrors(deps, session, lastMessages, userId);
3353
+ deps.logger.info(`[groupChatEngine] decisions: ${decisions.map((d) => `${d.xoxId}:${d.shouldSpeak ? d.urgency : "N"}`).join(", ")}`);
3354
+ const speakers = selectSpeakers(decisions, session);
3355
+ deps.logger.info(`[groupChatEngine] speakers: ${speakers.map((s) => s.xoxId).join(", ")}`);
3356
+ if (speakers.length === 0) {
3357
+ return [];
3358
+ }
3359
+ const newMessages = [];
3360
+ for (const speaker of speakers) {
3361
+ const member = session.members.find((m) => m.xoxId === speaker.xoxId);
3362
+ if (!member) continue;
3363
+ const profile = deps.mirrorStore.getProfile(speaker.xoxId, userId);
3364
+ if (!profile) continue;
3365
+ const msgId = `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
3366
+ deps.logger.info(`[groupChatEngine] generating speech for ${member.nickname} (trigger: ${speaker.trigger})`);
3367
+ try {
3368
+ const content = await generateSpeech(deps, profile, session, lastMessages, msgId, onEvent);
3369
+ const msg = {
3370
+ msgId,
3371
+ speakerId: speaker.xoxId,
3372
+ speakerName: member.nickname,
3373
+ content,
3374
+ timestamp: Date.now()
3375
+ };
3376
+ newMessages.push(msg);
3377
+ onEvent({
3378
+ sessionId: session.sessionId,
3379
+ msgId,
3380
+ speakerId: speaker.xoxId,
3381
+ speakerName: member.nickname,
3382
+ content: "",
3383
+ isDone: true,
3384
+ roundDone: false
3385
+ });
3386
+ } catch (e) {
3387
+ deps.logger.error(`[groupChatEngine] speech generation failed for ${member.nickname}:`, e);
3388
+ const failMsg = {
3389
+ msgId: `${Date.now()}_fail`,
3390
+ speakerId: 0,
3391
+ speakerName: "\u7CFB\u7EDF",
3392
+ content: `${member.nickname} \u6B32\u8A00\u53C8\u6B62...`,
3393
+ timestamp: Date.now()
3394
+ };
3395
+ newMessages.push(failMsg);
3396
+ onEvent({
3397
+ sessionId: session.sessionId,
3398
+ msgId: failMsg.msgId,
3399
+ speakerId: 0,
3400
+ speakerName: "\u7CFB\u7EDF",
3401
+ content: failMsg.content,
3402
+ isDone: true,
3403
+ roundDone: false
3404
+ });
3405
+ }
3406
+ lastMessages.push(...newMessages.slice(-1));
3407
+ }
3408
+ onEvent({
3409
+ sessionId: session.sessionId,
3410
+ msgId: "",
3411
+ speakerId: 0,
3412
+ speakerName: "",
3413
+ content: "",
3414
+ isDone: true,
3415
+ roundDone: true
3416
+ });
3417
+ return newMessages;
3418
+ }
3419
+ export {
3420
+ AVA_BASE_URL,
3421
+ CallbackStreamSink,
3422
+ ConsoleLogger,
3423
+ FlipCacheService,
3424
+ GroupChatStore,
3425
+ MIRROR_PRESETS,
3426
+ MemoryConfigStore,
3427
+ MemoryFlipDataSource,
3428
+ MirrorIndex,
3429
+ MirrorMemory,
3430
+ MirrorStore,
3431
+ NodeJsonConfigStore,
3432
+ NodeJsonFileStorage,
3433
+ NodeJsonFlipDataSource,
3434
+ NoopEventSink,
3435
+ NoopLogger,
3436
+ OpenAICompatibleClient,
3437
+ POCKET_API_BASE,
3438
+ POCKET_APP_INFO,
3439
+ POCKET_PA,
3440
+ POCKET_USER_AGENT,
3441
+ PRESET_MODELS,
3442
+ PRESET_PROVIDERS,
3443
+ PlainTextSecureStorage,
3444
+ PocketAuthExpiredError,
3445
+ PocketClient,
3446
+ SESSION_EXPIRE_MS,
3447
+ ToolRegistry,
3448
+ agentLoop,
3449
+ agentLoopStream,
3450
+ buildInnerMonologuePrompt,
3451
+ buildMirror,
3452
+ buildSpeakPrompt,
3453
+ chat,
3454
+ clearChatMemory,
3455
+ clearMirrorMemory,
3456
+ createMirrorTools,
3457
+ createPlatformTools,
3458
+ createPocketCore,
3459
+ createPocketTools,
3460
+ defaultModelConfig,
3461
+ fetchMemberList,
3462
+ getAllFlips,
3463
+ getDataSourcePage,
3464
+ getImUserInfo,
3465
+ getMemberFlipHistory,
3466
+ getMemberFlipPriceInfo,
3467
+ getMemberListByTab,
3468
+ getMirrorConfig,
3469
+ getRoomChannelByStar,
3470
+ getRoomInfo,
3471
+ getRoomMessages,
3472
+ getTeamListInfo,
3473
+ getUnreadMessageNum,
3474
+ loginWithCode,
3475
+ loginWithToken,
3476
+ normalizePersona,
3477
+ normalizePocketUser,
3478
+ rollbackPersona,
3479
+ runRound,
3480
+ saveAssistantMessage,
3481
+ sendFlip,
3482
+ sendVerificationCode,
3483
+ shouldTriggerGrowth,
3484
+ syncFlips,
3485
+ triggerGrowth
3486
+ };
3487
+ //# sourceMappingURL=index.js.map