chatlab-cli 0.0.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +115 -3
  2. package/bin/chatlab.mjs +2 -0
  3. package/dist/chunk-2AIOU3PW.mjs +26 -0
  4. package/dist/chunk-2AIOU3PW.mjs.map +1 -0
  5. package/dist/chunk-3DIWXYMJ.mjs +1344 -0
  6. package/dist/chunk-3DIWXYMJ.mjs.map +1 -0
  7. package/dist/chunk-EIRLEPXP.mjs +19145 -0
  8. package/dist/chunk-EIRLEPXP.mjs.map +1 -0
  9. package/dist/chunk-EWCSGYVT.mjs +144 -0
  10. package/dist/chunk-EWCSGYVT.mjs.map +1 -0
  11. package/dist/chunk-FBTHZU55.mjs +3400 -0
  12. package/dist/chunk-FBTHZU55.mjs.map +1 -0
  13. package/dist/chunk-NB4BIAYM.mjs +411 -0
  14. package/dist/chunk-NB4BIAYM.mjs.map +1 -0
  15. package/dist/chunk-XUHPCRVT.mjs +395 -0
  16. package/dist/chunk-XUHPCRVT.mjs.map +1 -0
  17. package/dist/cli.mjs +15 -0
  18. package/dist/cli.mjs.map +1 -0
  19. package/dist/http-QVYJ5HER.mjs +32 -0
  20. package/dist/http-QVYJ5HER.mjs.map +1 -0
  21. package/dist/import-AJKL6JPR.mjs +41 -0
  22. package/dist/import-AJKL6JPR.mjs.map +1 -0
  23. package/dist/index.mjs +24 -0
  24. package/dist/index.mjs.map +1 -0
  25. package/dist/mcp-FRF3YAN5.mjs +405 -0
  26. package/dist/mcp-FRF3YAN5.mjs.map +1 -0
  27. package/dist/src-KOIGBZJX.mjs +339 -0
  28. package/dist/src-KOIGBZJX.mjs.map +1 -0
  29. package/dist/stream-import-URGOCXUH.mjs +33 -0
  30. package/dist/stream-import-URGOCXUH.mjs.map +1 -0
  31. package/dist-web/assets/DebugToolsPanel-dUV6pVqD.css +1 -0
  32. package/dist-web/assets/DebugToolsPanel.vue_vue_type_script_setup_true_lang-DFN1Co1H.js +155 -0
  33. package/dist-web/assets/EChartWordcloud-BYLFxPAX.js +1 -0
  34. package/dist-web/assets/MessageExportModal-BAf68-wB.js +1 -0
  35. package/dist-web/assets/electron-2m4Vdk2z.js +1 -0
  36. package/dist-web/assets/electron-8qi0d92j.js +1 -0
  37. package/dist-web/assets/electron-BHjOx6B6.js +1 -0
  38. package/dist-web/assets/electron-CTK4NJag.js +1 -0
  39. package/dist-web/assets/electron-DPsrYwTt.js +1 -0
  40. package/dist-web/assets/electron-DfpXsXsS.js +1 -0
  41. package/dist-web/assets/electron-HoVzpIVc.js +1 -0
  42. package/dist-web/assets/fetch-Ayp337Ng.js +17 -0
  43. package/dist-web/assets/fetch-BS4SlQjA.js +1 -0
  44. package/dist-web/assets/fetch-BWtpQwBZ.js +1 -0
  45. package/dist-web/assets/fetch-CclRWOHf.js +1 -0
  46. package/dist-web/assets/fetch-W43zfemj.js +1 -0
  47. package/dist-web/assets/fetch-o5NJ2uWE.js +3 -0
  48. package/dist-web/assets/http-CeXMt-Fv.js +1 -0
  49. package/dist-web/assets/index-BKFGuwJu.css +1 -0
  50. package/dist-web/assets/index-Bd5r2oFG.js +163 -0
  51. package/dist-web/assets/index-BfZM-q0T.js +7 -0
  52. package/dist-web/assets/index-CLGv8Den.css +1 -0
  53. package/dist-web/assets/index-CLYHD_f4.js +2 -0
  54. package/dist-web/assets/index-CaTRpNBK.js +16 -0
  55. package/dist-web/assets/index-CmU6x-uJ.css +1 -0
  56. package/dist-web/assets/index-CzR1sElx.css +1 -0
  57. package/dist-web/assets/index-Ksa--o23.js +173 -0
  58. package/dist-web/assets/sse-CACcGGWv.js +2 -0
  59. package/dist-web/assets/vendor-echarts-C13vDgZW.js +11 -0
  60. package/dist-web/assets/vendor-echarts-wordcloud-uVdwYsYa.js +1 -0
  61. package/dist-web/assets/vendor-nuxt-ui-CYMNNdeD.js +15 -0
  62. package/dist-web/assets/vendor-reka-ui-CW2cIgMu.js +11 -0
  63. package/dist-web/assets/vendor-snapdom-XnBoh1gd.js +8 -0
  64. package/dist-web/assets/vendor-zrender-5DcYjSZk.js +8 -0
  65. package/dist-web/assets/web-C5yfcwjn.js +1 -0
  66. package/dist-web/index.html +18 -0
  67. package/package.json +70 -15
  68. package/cli.js +0 -3
@@ -0,0 +1,3400 @@
1
+ import { createRequire as __createRequire } from 'module';
2
+ import { dirname as __pathDirname } from 'path';
3
+ import { fileURLToPath as __fileURLToPath } from 'url';
4
+ const require = __createRequire(import.meta.url);
5
+ const __filename = __fileURLToPath(import.meta.url);
6
+ const __dirname = __pathDirname(__filename);
7
+ import {
8
+ importData,
9
+ parseFile
10
+ } from "./chunk-XUHPCRVT.mjs";
11
+ import {
12
+ analyzeIncrementalImport,
13
+ analyzeNewImport,
14
+ incrementalImport,
15
+ streamImport
16
+ } from "./chunk-EWCSGYVT.mjs";
17
+ import {
18
+ AGENT_TOOL_REGISTRY,
19
+ CoreDataProvider
20
+ } from "./chunk-3DIWXYMJ.mjs";
21
+ import {
22
+ getVersion
23
+ } from "./chunk-2AIOU3PW.mjs";
24
+ import {
25
+ AIConversationManager,
26
+ ALL_MIGRATIONS,
27
+ AgentEventHandler,
28
+ AiLogger,
29
+ AssistantManager,
30
+ BUILTIN_MODELS,
31
+ BUILTIN_PROVIDERS,
32
+ BUILTIN_TOOL_CATALOG,
33
+ DatabaseManager,
34
+ MigrationRunner,
35
+ NodePathProvider,
36
+ POS_TAG_DEFINITIONS,
37
+ PreferencesManager,
38
+ SkillManager,
39
+ SkillManagerCore,
40
+ TempDbReader,
41
+ applyPreprocessingPipeline,
42
+ batchSegmentWithFrequency,
43
+ buildMergedOutput,
44
+ buildPiModel,
45
+ buildSystemPrompt,
46
+ checkAndCompress,
47
+ checkConflictsFromSources,
48
+ cleanupTempDatabases,
49
+ computeWordFrequency,
50
+ createActivateSkillTool,
51
+ createAiTranslate,
52
+ createCompressionLlmAdapter,
53
+ createDatabaseManagerAdapter,
54
+ createJiebaNlpProvider,
55
+ deleteDict,
56
+ deleteTempDatabase,
57
+ detectAllFormats,
58
+ detectFormat,
59
+ downloadDict,
60
+ ensureDefaultDict,
61
+ executeReadonlySql,
62
+ executeSql,
63
+ exportSessionToJson,
64
+ export_service_exports,
65
+ fetchRemoteModels,
66
+ findEntryFileInDirectory,
67
+ formatAIError,
68
+ getAvailableYears,
69
+ getBuiltinModelsByProvider,
70
+ getCatchphraseAnalysis,
71
+ getChatOverview,
72
+ getClusterGraph,
73
+ getDailyActivity,
74
+ getDictList,
75
+ getHourlyActivity,
76
+ getLanguagePreferenceAnalysis,
77
+ getLaughAnalysis,
78
+ getMemberActivity,
79
+ getMembers,
80
+ getMembersDetailed,
81
+ getMentionAnalysis,
82
+ getMentionGraph,
83
+ getMessageLengthDistribution,
84
+ getMessageTypeStats,
85
+ getMonthlyActivity,
86
+ getRelationshipStats,
87
+ getSchemaDetailed,
88
+ getSessionInfo,
89
+ getSessionMeta,
90
+ getSessionOverview,
91
+ getSupportedFormats,
92
+ getTimeRange,
93
+ getWeekdayActivity,
94
+ getYearlyActivity,
95
+ hasPendingElectronDataWarning,
96
+ initNlpDir,
97
+ isDictDownloaded,
98
+ loadConfig,
99
+ member_service_exports,
100
+ openBetterSqliteDatabase,
101
+ parseAssistantFile,
102
+ queryMessages,
103
+ resolveApiKey,
104
+ runAgentCore,
105
+ runSimpleLlmStream,
106
+ scanMultiChatFile,
107
+ segmentText,
108
+ serializeChatLabToJsonl,
109
+ session_index_service_exports,
110
+ session_service_exports,
111
+ streamParseFileInfo,
112
+ summary_service_exports,
113
+ validateApiKey,
114
+ verifyCliDataPath,
115
+ writeAuthProfile,
116
+ writeConfigField
117
+ } from "./chunk-EIRLEPXP.mjs";
118
+
119
+ // src/http/index.ts
120
+ import * as fs14 from "fs";
121
+ import * as path15 from "path";
122
+ import * as crypto5 from "crypto";
123
+
124
+ // src/http/server.ts
125
+ import Fastify from "fastify";
126
+
127
+ // src/http/auth.ts
128
+ import { timingSafeEqual } from "crypto";
129
+
130
+ // src/http/errors.ts
131
+ var HTTP_STATUS = {
132
+ ["UNAUTHORIZED" /* UNAUTHORIZED */]: 401,
133
+ ["SESSION_NOT_FOUND" /* SESSION_NOT_FOUND */]: 404,
134
+ ["INVALID_FORMAT" /* INVALID_FORMAT */]: 400,
135
+ ["INVALID_PAYLOAD" /* INVALID_PAYLOAD */]: 400,
136
+ ["SQL_READONLY_VIOLATION" /* SQL_READONLY_VIOLATION */]: 400,
137
+ ["SQL_EXECUTION_ERROR" /* SQL_EXECUTION_ERROR */]: 400,
138
+ ["EXPORT_TOO_LARGE" /* EXPORT_TOO_LARGE */]: 400,
139
+ ["BODY_TOO_LARGE" /* BODY_TOO_LARGE */]: 413,
140
+ ["SERVER_ERROR" /* SERVER_ERROR */]: 500
141
+ };
142
+ var ApiError = class extends Error {
143
+ code;
144
+ statusCode;
145
+ constructor(code, message) {
146
+ super(message);
147
+ this.name = "ApiError";
148
+ this.code = code;
149
+ this.statusCode = HTTP_STATUS[code];
150
+ }
151
+ };
152
+ function unauthorized(message = "Invalid or missing token") {
153
+ return new ApiError("UNAUTHORIZED" /* UNAUTHORIZED */, message);
154
+ }
155
+ function sessionNotFound(id) {
156
+ return new ApiError("SESSION_NOT_FOUND" /* SESSION_NOT_FOUND */, `Session not found: ${id}`);
157
+ }
158
+ function sqlExecutionError(message) {
159
+ return new ApiError("SQL_EXECUTION_ERROR" /* SQL_EXECUTION_ERROR */, message);
160
+ }
161
+ function exportTooLarge(count, limit) {
162
+ return new ApiError(
163
+ "EXPORT_TOO_LARGE" /* EXPORT_TOO_LARGE */,
164
+ `Message count ${count} exceeds export limit ${limit}. Use paginated /messages API instead.`
165
+ );
166
+ }
167
+ function serverError(message = "Internal server error") {
168
+ return new ApiError("SERVER_ERROR" /* SERVER_ERROR */, message);
169
+ }
170
+ function successResponse(data, meta) {
171
+ return {
172
+ success: true,
173
+ data,
174
+ meta: {
175
+ timestamp: Math.floor(Date.now() / 1e3),
176
+ version: "0.0.2",
177
+ ...meta
178
+ }
179
+ };
180
+ }
181
+ function errorResponse(error) {
182
+ return {
183
+ success: false,
184
+ error: {
185
+ code: error.code,
186
+ message: error.message
187
+ }
188
+ };
189
+ }
190
+
191
+ // src/http/auth.ts
192
+ var cachedToken = null;
193
+ var webModeEnabled = false;
194
+ function setAuthToken(token) {
195
+ cachedToken = token;
196
+ }
197
+ function setWebMode(enabled) {
198
+ webModeEnabled = enabled;
199
+ }
200
+ function safeTokenCompare(a, b) {
201
+ const bufA = Buffer.from(a);
202
+ const bufB = Buffer.from(b);
203
+ if (bufA.length !== bufB.length) return false;
204
+ return timingSafeEqual(bufA, bufB);
205
+ }
206
+ async function authHook(request, reply) {
207
+ if (!cachedToken) return;
208
+ if (request.url.startsWith("/_web/")) return;
209
+ if (webModeEnabled && !request.url.startsWith("/api/")) return;
210
+ const authHeader = request.headers.authorization;
211
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
212
+ const err = unauthorized();
213
+ reply.code(err.statusCode).send(errorResponse(err));
214
+ return;
215
+ }
216
+ const token = authHeader.slice(7);
217
+ if (!safeTokenCompare(token, cachedToken)) {
218
+ const err = unauthorized();
219
+ reply.code(err.statusCode).send(errorResponse(err));
220
+ return;
221
+ }
222
+ }
223
+
224
+ // src/http/server.ts
225
+ var JSON_BODY_LIMIT = 50 * 1024 * 1024;
226
+ function createServer() {
227
+ const server2 = Fastify({
228
+ logger: false,
229
+ bodyLimit: JSON_BODY_LIMIT
230
+ });
231
+ server2.addHook("onRequest", authHook);
232
+ server2.setErrorHandler((error, _request, reply) => {
233
+ if (error instanceof ApiError) {
234
+ reply.code(error.statusCode).send(errorResponse(error));
235
+ return;
236
+ }
237
+ if (error.statusCode === 413) {
238
+ const bodyErr = new ApiError("BODY_TOO_LARGE" /* BODY_TOO_LARGE */, "Request body exceeds 50MB limit");
239
+ reply.code(413).send(errorResponse(bodyErr));
240
+ return;
241
+ }
242
+ const statusCode = error.statusCode;
243
+ if (statusCode && statusCode >= 400 && statusCode < 600) {
244
+ reply.code(statusCode).send({ success: false, error: { code: "CLIENT_ERROR", message: error.message } });
245
+ return;
246
+ }
247
+ const err = serverError(error.message);
248
+ reply.code(err.statusCode).send(errorResponse(err));
249
+ });
250
+ return server2;
251
+ }
252
+
253
+ // src/http/routes/system.ts
254
+ function registerSystemRoutes(server2, dbManager2) {
255
+ server2.get("/api/v1/status", async () => {
256
+ let sessionCount = 0;
257
+ try {
258
+ sessionCount = dbManager2.listSessionIds().length;
259
+ } catch {
260
+ }
261
+ return successResponse({
262
+ name: "ChatLab API",
263
+ version: getVersion(),
264
+ uptime: Math.floor(process.uptime()),
265
+ sessionCount
266
+ });
267
+ });
268
+ server2.get("/api/v1/schema", async () => {
269
+ return successResponse({
270
+ format: "ChatLab Format",
271
+ version: "0.0.2",
272
+ spec: {
273
+ chatlab: {
274
+ type: "object",
275
+ required: ["version"],
276
+ properties: {
277
+ version: { type: "string" },
278
+ exportedAt: { type: "number" },
279
+ generator: { type: "string" }
280
+ }
281
+ },
282
+ meta: {
283
+ type: "object",
284
+ required: ["name", "platform", "type"],
285
+ properties: {
286
+ name: { type: "string" },
287
+ platform: {
288
+ type: "string",
289
+ enum: ["qq", "wechat", "telegram", "discord", "line", "whatsapp", "instagram", "unknown"]
290
+ },
291
+ type: { type: "string", enum: ["group", "private"] },
292
+ groupId: { type: "string" }
293
+ }
294
+ },
295
+ members: {
296
+ type: "array",
297
+ items: {
298
+ type: "object",
299
+ required: ["platformId", "accountName"],
300
+ properties: {
301
+ platformId: { type: "string" },
302
+ accountName: { type: "string" },
303
+ groupNickname: { type: "string" },
304
+ avatar: { type: "string" }
305
+ }
306
+ }
307
+ },
308
+ messages: {
309
+ type: "array",
310
+ items: {
311
+ type: "object",
312
+ required: ["sender", "timestamp", "type"],
313
+ properties: {
314
+ platformMessageId: { type: "string" },
315
+ sender: { type: "string" },
316
+ accountName: { type: "string" },
317
+ groupNickname: { type: "string" },
318
+ timestamp: { type: "number" },
319
+ type: { type: "number" },
320
+ content: { type: ["string", "null"] },
321
+ replyToMessageId: { type: "string" }
322
+ }
323
+ }
324
+ }
325
+ }
326
+ });
327
+ });
328
+ }
329
+
330
+ // src/http/routes/sessions.ts
331
+ var EXPORT_MESSAGE_LIMIT = 1e5;
332
+ function ensureDb(dbManager2, sessionId) {
333
+ const db = dbManager2.open(sessionId);
334
+ if (!db) throw sessionNotFound(sessionId);
335
+ return db;
336
+ }
337
+ function registerSessionRoutes(server2, dbManager2) {
338
+ server2.get("/api/v1/sessions", async () => {
339
+ const sessionIds = dbManager2.listSessionIds();
340
+ const sessions = sessionIds.map((id) => {
341
+ const db = dbManager2.open(id);
342
+ if (!db) return null;
343
+ const info = getSessionInfo(db);
344
+ if (!info) return null;
345
+ return {
346
+ id,
347
+ name: info.name,
348
+ platform: info.platform,
349
+ type: info.type,
350
+ groupId: info.groupId || void 0,
351
+ messageCount: info.messageCount,
352
+ memberCount: info.memberCount,
353
+ firstTimestamp: info.firstMessageTs,
354
+ lastTimestamp: info.lastMessageTs
355
+ };
356
+ }).filter(Boolean);
357
+ return successResponse(sessions);
358
+ });
359
+ server2.get("/api/v1/sessions/:id", async (request) => {
360
+ const db = ensureDb(dbManager2, request.params.id);
361
+ const info = getSessionInfo(db);
362
+ if (!info) throw sessionNotFound(request.params.id);
363
+ return successResponse({
364
+ id: request.params.id,
365
+ name: info.name,
366
+ platform: info.platform,
367
+ type: info.type,
368
+ groupId: info.groupId || void 0,
369
+ messageCount: info.messageCount,
370
+ memberCount: info.memberCount,
371
+ firstTimestamp: info.firstMessageTs,
372
+ lastTimestamp: info.lastMessageTs
373
+ });
374
+ });
375
+ server2.get("/api/v1/sessions/:id/messages", async (request) => {
376
+ const { id } = request.params;
377
+ const db = ensureDb(dbManager2, id);
378
+ const page = Math.max(1, parseInt(request.query.page || "1", 10) || 1);
379
+ const limit = Math.min(1e3, Math.max(1, parseInt(request.query.limit || "100", 10) || 100));
380
+ const offset = (page - 1) * limit;
381
+ const { startTime, endTime, keyword, senderId } = request.query;
382
+ const result = queryMessages(db, {
383
+ keyword: keyword || void 0,
384
+ startTs: startTime ? parseInt(startTime, 10) : void 0,
385
+ endTs: endTime ? parseInt(endTime, 10) : void 0,
386
+ senderId: senderId ? parseInt(senderId, 10) : void 0,
387
+ limit,
388
+ offset
389
+ });
390
+ return successResponse({
391
+ messages: result.messages,
392
+ total: result.total,
393
+ page,
394
+ limit,
395
+ totalPages: result.totalPages
396
+ });
397
+ });
398
+ server2.get("/api/v1/sessions/:id/members", async (request) => {
399
+ const db = ensureDb(dbManager2, request.params.id);
400
+ const members = getMembers(db);
401
+ return successResponse(members);
402
+ });
403
+ server2.get("/api/v1/sessions/:id/stats/overview", async (request) => {
404
+ const { id } = request.params;
405
+ const db = ensureDb(dbManager2, id);
406
+ const overview = getSessionOverview(db);
407
+ const memberActivity = getMemberActivity(db);
408
+ const typeDistribution = getMessageTypeStats(db);
409
+ const typeMap = {};
410
+ for (const item of typeDistribution) {
411
+ typeMap[String(item.type)] = item.count;
412
+ }
413
+ const topMembers = memberActivity.slice(0, 10).map((m) => ({
414
+ platformId: m.platformId,
415
+ name: m.name,
416
+ messageCount: m.messageCount,
417
+ percentage: m.percentage
418
+ }));
419
+ return successResponse({
420
+ messageCount: overview.totalMessages,
421
+ memberCount: overview.totalMembers,
422
+ timeRange: {
423
+ start: overview.firstMessageTs ?? 0,
424
+ end: overview.lastMessageTs ?? 0
425
+ },
426
+ messageTypeDistribution: typeMap,
427
+ topMembers
428
+ });
429
+ });
430
+ server2.post("/api/v1/sessions/:id/sql", async (request, reply) => {
431
+ const { id } = request.params;
432
+ const db = ensureDb(dbManager2, id);
433
+ const { sql } = request.body || {};
434
+ if (!sql || typeof sql !== "string") {
435
+ const err = sqlExecutionError("Missing sql parameter");
436
+ return reply.code(err.statusCode).send(errorResponse(err));
437
+ }
438
+ try {
439
+ const result = executeReadonlySql(db, sql);
440
+ return successResponse(result);
441
+ } catch (err) {
442
+ const message = err instanceof Error ? err.message : "SQL execution error";
443
+ if (message.includes("SELECT") || message.includes("\u53EA\u8BFB") || message.includes("readonly")) {
444
+ const apiErr2 = new ApiError("SQL_READONLY_VIOLATION", message);
445
+ apiErr2.statusCode = 400;
446
+ return reply.code(400).send(errorResponse(apiErr2));
447
+ }
448
+ const apiErr = sqlExecutionError(message);
449
+ return reply.code(apiErr.statusCode).send(errorResponse(apiErr));
450
+ }
451
+ });
452
+ server2.get("/api/v1/sessions/:id/export", async (request, reply) => {
453
+ const { id } = request.params;
454
+ const db = ensureDb(dbManager2, id);
455
+ const meta = getSessionMeta(db);
456
+ if (!meta) throw sessionNotFound(id);
457
+ const overview = getSessionOverview(db);
458
+ if (overview.totalMessages > EXPORT_MESSAGE_LIMIT) {
459
+ const err = exportTooLarge(overview.totalMessages, EXPORT_MESSAGE_LIMIT);
460
+ return reply.code(err.statusCode).send(errorResponse(err));
461
+ }
462
+ const members = getMembersDetailed(db);
463
+ const allMessages = queryMessages(db, { limit: EXPORT_MESSAGE_LIMIT, offset: 0 });
464
+ const chatLabFormat = {
465
+ chatlab: {
466
+ version: "0.0.2",
467
+ exportedAt: Math.floor(Date.now() / 1e3),
468
+ generator: "ChatLab API"
469
+ },
470
+ meta: {
471
+ name: meta.name,
472
+ platform: meta.platform,
473
+ type: meta.type,
474
+ groupId: meta.groupId || void 0
475
+ },
476
+ members: members.map((m) => ({
477
+ platformId: m.platformId,
478
+ accountName: m.accountName || m.platformId,
479
+ groupNickname: m.groupNickname || void 0
480
+ })),
481
+ messages: allMessages.messages.map((msg) => ({
482
+ sender: msg.senderPlatformId,
483
+ accountName: msg.senderName || void 0,
484
+ timestamp: msg.timestamp,
485
+ type: msg.type,
486
+ content: msg.content || null
487
+ }))
488
+ };
489
+ return successResponse(chatLabFormat);
490
+ });
491
+ }
492
+
493
+ // src/http/routes/web/index.ts
494
+ import * as os3 from "os";
495
+ import * as path7 from "path";
496
+
497
+ // src/merger/merge-cache.ts
498
+ import * as fs from "fs";
499
+ import * as path from "path";
500
+ import * as crypto from "crypto";
501
+ var MergeSessionCache = class {
502
+ cache = /* @__PURE__ */ new Map();
503
+ tempDir;
504
+ nativeBinding;
505
+ constructor(pathProvider, options) {
506
+ this.tempDir = pathProvider.getTempDir();
507
+ this.nativeBinding = options?.nativeBinding;
508
+ if (!fs.existsSync(this.tempDir)) {
509
+ fs.mkdirSync(this.tempDir, { recursive: true });
510
+ }
511
+ }
512
+ generateTempDbPath(filename) {
513
+ const ts = Date.now();
514
+ const rand = Math.random().toString(36).slice(2, 8);
515
+ const safeName = filename.replace(/[/\\?%*:|"<>]/g, "_").slice(0, 40);
516
+ return path.join(this.tempDir, `merge_${safeName}_${ts}_${rand}.db`);
517
+ }
518
+ /** Store a parsed file's temp DB and return a handle for subsequent requests. */
519
+ store(filename, tempDbPath) {
520
+ const handle = crypto.randomUUID();
521
+ this.cache.set(handle, { tempDbPath, filename, createdAt: Date.now() });
522
+ return handle;
523
+ }
524
+ /** Open a TempDbReader for a given handle. Caller must close the reader. */
525
+ openReader(handle) {
526
+ const entry = this.cache.get(handle);
527
+ if (!entry || !fs.existsSync(entry.tempDbPath)) return null;
528
+ const adapter = openBetterSqliteDatabase(entry.tempDbPath, {
529
+ readonly: true,
530
+ nativeBinding: this.nativeBinding
531
+ });
532
+ return { reader: new TempDbReader(adapter), filename: entry.filename };
533
+ }
534
+ /** Create a writable temp DB for streaming parse. */
535
+ createTempDatabase(filename) {
536
+ const tempDbPath = this.generateTempDbPath(filename);
537
+ const adapter = openBetterSqliteDatabase(tempDbPath, { nativeBinding: this.nativeBinding });
538
+ return { db: adapter, tempDbPath };
539
+ }
540
+ /** Remove a single entry and its temp DB file. */
541
+ delete(handle) {
542
+ const entry = this.cache.get(handle);
543
+ if (entry) {
544
+ deleteTempDatabase(entry.tempDbPath);
545
+ this.cache.delete(handle);
546
+ }
547
+ }
548
+ /** Clear all entries and delete their temp DB files. */
549
+ clear() {
550
+ for (const entry of this.cache.values()) {
551
+ deleteTempDatabase(entry.tempDbPath);
552
+ }
553
+ this.cache.clear();
554
+ }
555
+ /** Cleanup orphan temp DBs from previous runs. */
556
+ cleanupOrphans() {
557
+ cleanupTempDatabases(this.tempDir);
558
+ }
559
+ /** Auto-cleanup entries older than maxAge (default 1 hour). */
560
+ cleanupExpired(maxAgeMs = 36e5) {
561
+ const now = Date.now();
562
+ for (const [handle, entry] of this.cache) {
563
+ if (now - entry.createdAt > maxAgeMs) {
564
+ deleteTempDatabase(entry.tempDbPath);
565
+ this.cache.delete(handle);
566
+ }
567
+ }
568
+ }
569
+ };
570
+
571
+ // src/http/routes/web/sessions.ts
572
+ function registerSessionRoutes2(server2, adapter) {
573
+ server2.get("/_web/sessions", async () => {
574
+ return session_service_exports.listAnalysisSessions(adapter);
575
+ });
576
+ server2.get("/_web/sessions/:id", async (request) => {
577
+ const session = session_service_exports.getAnalysisSession(adapter, request.params.id);
578
+ if (!session) {
579
+ throw Object.assign(new Error(`Session not found: ${request.params.id}`), { statusCode: 404 });
580
+ }
581
+ return session;
582
+ });
583
+ server2.delete("/_web/sessions/:id", async (request, reply) => {
584
+ const { id } = request.params;
585
+ try {
586
+ const deleted = session_service_exports.deleteSession(adapter, id);
587
+ if (!deleted) {
588
+ return reply.code(404).send({ success: false, error: "File not found" });
589
+ }
590
+ return { success: true };
591
+ } catch (err) {
592
+ return reply.code(500).send({ success: false, error: String(err) });
593
+ }
594
+ });
595
+ server2.patch("/_web/sessions/:id/name", async (request) => {
596
+ session_service_exports.renameSession(adapter, request.params.id, request.body.name);
597
+ return { success: true };
598
+ });
599
+ server2.patch(
600
+ "/_web/sessions/:id/owner",
601
+ async (request) => {
602
+ session_service_exports.updateSessionOwnerId(adapter, request.params.id, request.body.ownerId ?? null);
603
+ return { success: true };
604
+ }
605
+ );
606
+ }
607
+
608
+ // src/http/routes/web/members.ts
609
+ function registerMemberRoutes(server2, adapter) {
610
+ server2.get("/_web/sessions/:id/members", async (request) => {
611
+ return member_service_exports.getMembers(adapter, request.params.id);
612
+ });
613
+ server2.get("/_web/sessions/:id/members/paginated", async (request) => {
614
+ return member_service_exports.getMembersPaginated(adapter, request.params.id, {
615
+ page: parseInt(request.query.page || "1", 10),
616
+ pageSize: parseInt(request.query.pageSize || "20", 10),
617
+ search: request.query.search,
618
+ sortOrder: request.query.sortOrder === "asc" ? "asc" : "desc"
619
+ });
620
+ });
621
+ server2.patch(
622
+ "/_web/sessions/:id/members/:memberId/aliases",
623
+ async (request) => {
624
+ const memberId = parseInt(request.params.memberId, 10);
625
+ member_service_exports.updateMemberAliases(adapter, request.params.id, memberId, request.body.aliases);
626
+ return { success: true };
627
+ }
628
+ );
629
+ server2.delete(
630
+ "/_web/sessions/:id/members/:memberId",
631
+ async (request) => {
632
+ const memberId = parseInt(request.params.memberId, 10);
633
+ member_service_exports.deleteMember(adapter, request.params.id, memberId);
634
+ return { success: true };
635
+ }
636
+ );
637
+ server2.post(
638
+ "/_web/sessions/:id/members/merge",
639
+ async (request) => {
640
+ const { memberId1, memberId2 } = request.body;
641
+ member_service_exports.mergeMembers(adapter, request.params.id, memberId1, memberId2);
642
+ return { success: true };
643
+ }
644
+ );
645
+ server2.get(
646
+ "/_web/sessions/:id/members/:memberId/history",
647
+ async (request) => {
648
+ const memberId = parseInt(request.params.memberId, 10);
649
+ return member_service_exports.getMemberNameHistory(adapter, request.params.id, memberId);
650
+ }
651
+ );
652
+ }
653
+
654
+ // src/http/routes/web/helpers.ts
655
+ import * as fs2 from "fs";
656
+ import * as path2 from "path";
657
+ function parseTimeFilter(query) {
658
+ const { startTs, endTs, memberId } = query;
659
+ if (!startTs && !endTs && !memberId) return void 0;
660
+ const filter = {};
661
+ if (startTs) filter.startTs = parseInt(startTs, 10);
662
+ if (endTs) filter.endTs = parseInt(endTs, 10);
663
+ if (memberId) filter.memberId = parseInt(memberId, 10);
664
+ return filter;
665
+ }
666
+ function resolveNativeBinding() {
667
+ if (process.versions.electron) return void 0;
668
+ const nativePath = path2.resolve(__dirname, "../../../../native/better_sqlite3.node");
669
+ if (fs2.existsSync(nativePath)) return nativePath;
670
+ return void 0;
671
+ }
672
+ function getAiDataDir(dbManager2) {
673
+ const pathProvider = dbManager2["pathProvider"];
674
+ if (!pathProvider) {
675
+ throw Object.assign(new Error("PathProvider not available"), { statusCode: 500 });
676
+ }
677
+ return pathProvider.getAiDataDir();
678
+ }
679
+ function ensureDb2(dbManager2, sessionId) {
680
+ const db = dbManager2.open(sessionId);
681
+ if (!db) {
682
+ throw Object.assign(new Error(`Session not found: ${sessionId}`), { statusCode: 404 });
683
+ }
684
+ return db;
685
+ }
686
+
687
+ // src/http/routes/web/analytics.ts
688
+ function registerAnalyticsRoutes(server2, _dbManager, adapter) {
689
+ server2.get("/_web/sessions/:id/years", async (request) => {
690
+ const db = adapter.ensureReadonly(request.params.id);
691
+ return getAvailableYears(db);
692
+ });
693
+ server2.get("/_web/sessions/:id/time-range", async (request) => {
694
+ const db = adapter.ensureReadonly(request.params.id);
695
+ return getTimeRange(db);
696
+ });
697
+ server2.get(
698
+ "/_web/sessions/:id/stats/member-activity",
699
+ async (request) => {
700
+ const db = adapter.ensureReadonly(request.params.id);
701
+ return getMemberActivity(db, parseTimeFilter(request.query));
702
+ }
703
+ );
704
+ server2.get(
705
+ "/_web/sessions/:id/stats/hourly",
706
+ async (request) => {
707
+ const db = adapter.ensureReadonly(request.params.id);
708
+ return getHourlyActivity(db, parseTimeFilter(request.query));
709
+ }
710
+ );
711
+ server2.get(
712
+ "/_web/sessions/:id/stats/daily",
713
+ async (request) => {
714
+ const db = adapter.ensureReadonly(request.params.id);
715
+ return getDailyActivity(db, parseTimeFilter(request.query));
716
+ }
717
+ );
718
+ server2.get(
719
+ "/_web/sessions/:id/stats/weekday",
720
+ async (request) => {
721
+ const db = adapter.ensureReadonly(request.params.id);
722
+ return getWeekdayActivity(db, parseTimeFilter(request.query));
723
+ }
724
+ );
725
+ server2.get(
726
+ "/_web/sessions/:id/stats/message-types",
727
+ async (request) => {
728
+ const db = adapter.ensureReadonly(request.params.id);
729
+ return getMessageTypeStats(db, parseTimeFilter(request.query));
730
+ }
731
+ );
732
+ server2.get(
733
+ "/_web/sessions/:id/analytics/relationship",
734
+ async (request) => {
735
+ const db = adapter.ensureReadonly(request.params.id);
736
+ return getRelationshipStats(db, parseTimeFilter(request.query));
737
+ }
738
+ );
739
+ server2.get(
740
+ "/_web/sessions/:id/analytics/catchphrase",
741
+ async (request) => {
742
+ const db = adapter.ensureReadonly(request.params.id);
743
+ return getCatchphraseAnalysis(db, parseTimeFilter(request.query));
744
+ }
745
+ );
746
+ server2.get(
747
+ "/_web/sessions/:id/analytics/mention",
748
+ async (request) => {
749
+ const db = adapter.ensureReadonly(request.params.id);
750
+ return getMentionAnalysis(db, parseTimeFilter(request.query));
751
+ }
752
+ );
753
+ server2.get(
754
+ "/_web/sessions/:id/analytics/mention-graph",
755
+ async (request) => {
756
+ const db = adapter.ensureReadonly(request.params.id);
757
+ return getMentionGraph(db, parseTimeFilter(request.query));
758
+ }
759
+ );
760
+ server2.get(
761
+ "/_web/sessions/:id/analytics/laugh",
762
+ async (request) => {
763
+ const db = adapter.ensureReadonly(request.params.id);
764
+ return getLaughAnalysis(db, parseTimeFilter(request.query));
765
+ }
766
+ );
767
+ server2.get(
768
+ "/_web/sessions/:id/analytics/cluster",
769
+ async (request) => {
770
+ const db = adapter.ensureReadonly(request.params.id);
771
+ const filter = parseTimeFilter(request.query);
772
+ const topEdges = request.query.topEdges ? parseInt(request.query.topEdges, 10) : void 0;
773
+ return getClusterGraph(db, filter, topEdges ? { topEdges } : void 0);
774
+ }
775
+ );
776
+ server2.get(
777
+ "/_web/sessions/:id/analytics/language-preference",
778
+ async (request) => {
779
+ const db = adapter.ensureReadonly(request.params.id);
780
+ const filter = parseTimeFilter(request.query);
781
+ const locale = request.query.locale || "zh-CN";
782
+ const nlpProvider = createJiebaNlpProvider();
783
+ return getLanguagePreferenceAnalysis(db, { locale, timeFilter: filter, nlpProvider });
784
+ }
785
+ );
786
+ server2.get(
787
+ "/_web/sessions/:id/analytics/monthly-activity",
788
+ async (request) => {
789
+ const db = adapter.ensureReadonly(request.params.id);
790
+ return getMonthlyActivity(db, parseTimeFilter(request.query));
791
+ }
792
+ );
793
+ server2.get(
794
+ "/_web/sessions/:id/analytics/yearly-activity",
795
+ async (request) => {
796
+ const db = adapter.ensureReadonly(request.params.id);
797
+ return getYearlyActivity(db, parseTimeFilter(request.query));
798
+ }
799
+ );
800
+ server2.get(
801
+ "/_web/sessions/:id/analytics/message-length-distribution",
802
+ async (request) => {
803
+ const db = adapter.ensureReadonly(request.params.id);
804
+ return getMessageLengthDistribution(db, parseTimeFilter(request.query));
805
+ }
806
+ );
807
+ }
808
+
809
+ // src/http/routes/web/sql.ts
810
+ function registerSqlRoutes(server2, adapter) {
811
+ server2.post("/_web/sessions/:id/sql", async (request, reply) => {
812
+ const db = adapter.ensureReadonly(request.params.id);
813
+ const { sql } = request.body || {};
814
+ if (!sql || typeof sql !== "string") {
815
+ return reply.code(400).send({ error: "Missing sql parameter" });
816
+ }
817
+ try {
818
+ return executeSql(db, sql, { timing: true });
819
+ } catch (err) {
820
+ const message = err instanceof Error ? err.message : "SQL execution error";
821
+ return reply.code(400).send({ error: message });
822
+ }
823
+ });
824
+ server2.get("/_web/sessions/:id/schema", async (request) => {
825
+ const db = adapter.ensureReadonly(request.params.id);
826
+ return getSchemaDetailed(db);
827
+ });
828
+ server2.post("/_web/sessions/:id/query", async (request) => {
829
+ const db = adapter.ensureReadonly(request.params.id);
830
+ const { sql, params = [] } = request.body;
831
+ if (!sql || typeof sql !== "string") {
832
+ throw Object.assign(new Error('Missing or invalid "sql" field'), { statusCode: 400 });
833
+ }
834
+ const stmt = db.prepare(sql.trim());
835
+ if (!stmt.readonly) {
836
+ throw Object.assign(new Error("Only READ-ONLY statements are allowed"), { statusCode: 403 });
837
+ }
838
+ if (Array.isArray(params)) {
839
+ return stmt.all(...params);
840
+ }
841
+ return stmt.all(params);
842
+ });
843
+ }
844
+
845
+ // src/http/routes/web/session-index.ts
846
+ function registerSessionIndexRoutes(server2, adapter) {
847
+ server2.post("/_web/sessions/:id/generate-index", async (request) => {
848
+ const gapThreshold = request.body?.gapThreshold ?? 1800;
849
+ const sessionCount = session_index_service_exports.generateIndex(adapter, request.params.id, gapThreshold);
850
+ return { sessionCount };
851
+ });
852
+ server2.post("/_web/sessions/:id/clear-index", async (request) => {
853
+ session_index_service_exports.clearIndex(adapter, request.params.id);
854
+ return { success: true };
855
+ });
856
+ server2.get("/_web/sessions/:id/search/fts", async (request, reply) => {
857
+ if (!session_index_service_exports.getFtsStatus(adapter, request.params.id)) {
858
+ return reply.code(400).send({ error: "FTS index not built for this session" });
859
+ }
860
+ const keywords = request.query.keywords.split(/\s+/).filter(Boolean);
861
+ if (keywords.length === 0) return { rowids: [], total: 0 };
862
+ const limit = parseInt(request.query.limit || "100", 10);
863
+ const offset = parseInt(request.query.offset || "0", 10);
864
+ return session_index_service_exports.searchFts(adapter, request.params.id, keywords, limit, offset);
865
+ });
866
+ server2.get("/_web/sessions/:id/fts/status", async (request) => {
867
+ return { hasFtsIndex: session_index_service_exports.getFtsStatus(adapter, request.params.id) };
868
+ });
869
+ server2.post("/_web/sessions/:id/fts/rebuild", async (request) => {
870
+ const result = session_index_service_exports.rebuildFts(adapter, request.params.id);
871
+ return { success: true, indexed: result.indexed };
872
+ });
873
+ }
874
+
875
+ // src/ai/llm-config.ts
876
+ import * as fs3 from "fs";
877
+ import * as path3 from "path";
878
+ import { randomUUID as randomUUID2 } from "crypto";
879
+ function loadLlmConfig(aiDataDir) {
880
+ const configPath = path3.join(aiDataDir, "llm-config.json");
881
+ if (!fs3.existsSync(configPath)) {
882
+ return { configs: [], defaultAssistant: null, fastModel: null };
883
+ }
884
+ try {
885
+ const data = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
886
+ const configs = (data.configs || []).map((c) => {
887
+ const provider = c.provider || "";
888
+ const authProfile = c.authProfile;
889
+ const profileKey = resolveApiKey(provider, authProfile);
890
+ const rawKey = c.apiKey || "";
891
+ const fallbackKey = rawKey.startsWith("enc:") ? "" : rawKey;
892
+ return {
893
+ ...c,
894
+ apiKey: profileKey || fallbackKey
895
+ };
896
+ });
897
+ return {
898
+ configs,
899
+ defaultAssistant: data.defaultAssistant ?? null,
900
+ fastModel: data.fastModel ?? null
901
+ };
902
+ } catch {
903
+ return { configs: [], defaultAssistant: null, fastModel: null };
904
+ }
905
+ }
906
+ function resolveSlot(slot, configs) {
907
+ if (slot && configs.some((c) => c.id === slot.configId)) return slot;
908
+ const fallback = configs[0];
909
+ if (!fallback) return null;
910
+ return { configId: fallback.id, modelId: fallback.model };
911
+ }
912
+ function getDefaultAssistantConfig(aiDataDir) {
913
+ const store = loadLlmConfig(aiDataDir);
914
+ const slot = resolveSlot(store.defaultAssistant, store.configs);
915
+ if (!slot) return null;
916
+ const config = store.configs.find((c) => c.id === slot.configId);
917
+ if (!config) return null;
918
+ return { ...config, model: slot.modelId || config.model };
919
+ }
920
+ var MAX_CONFIG_COUNT = 99;
921
+ function saveLlmConfig(aiDataDir, store) {
922
+ const configPath = path3.join(aiDataDir, "llm-config.json");
923
+ const toSave = {
924
+ ...store,
925
+ configs: store.configs.map((c) => {
926
+ const { apiKey: _k, ...rest } = c;
927
+ return rest;
928
+ }),
929
+ schemaVersion: 3
930
+ };
931
+ fs3.writeFileSync(configPath, JSON.stringify(toSave, null, 2), "utf-8");
932
+ }
933
+ function addLlmConfig(aiDataDir, config) {
934
+ const store = loadLlmConfig(aiDataDir);
935
+ if (store.configs.length >= MAX_CONFIG_COUNT) {
936
+ return { success: false, error: `Maximum ${MAX_CONFIG_COUNT} configs reached` };
937
+ }
938
+ const newConfig = {
939
+ ...config,
940
+ id: randomUUID2()
941
+ };
942
+ const storeForSave = loadRawConfigStore(aiDataDir);
943
+ storeForSave.configs.push({
944
+ ...newConfig,
945
+ apiKey: ""
946
+ });
947
+ if (storeForSave.configs.length === 1) {
948
+ storeForSave.defaultAssistant = { configId: newConfig.id, modelId: newConfig.model || "" };
949
+ }
950
+ if (config.apiKey) {
951
+ const profileName = config.name?.toLowerCase().replace(/\s+/g, "-") || config.provider;
952
+ writeAuthProfile(profileName, {
953
+ type: "api_key",
954
+ provider: config.provider,
955
+ key: config.apiKey
956
+ });
957
+ storeForSave.configs[storeForSave.configs.length - 1].authProfile = profileName;
958
+ }
959
+ saveLlmConfig(aiDataDir, storeForSave);
960
+ return { success: true, config: { ...newConfig, apiKey: "" } };
961
+ }
962
+ function updateLlmConfig(aiDataDir, id, updates) {
963
+ const storeForSave = loadRawConfigStore(aiDataDir);
964
+ const index = storeForSave.configs.findIndex((c) => c.id === id);
965
+ if (index === -1) {
966
+ return { success: false, error: "Config not found" };
967
+ }
968
+ const { apiKey: newApiKey, ...restUpdates } = updates;
969
+ const updated = {
970
+ ...storeForSave.configs[index],
971
+ ...restUpdates
972
+ };
973
+ storeForSave.configs[index] = updated;
974
+ if (newApiKey) {
975
+ const profileName = updated.name?.toLowerCase().replace(/\s+/g, "-") || updated.provider;
976
+ writeAuthProfile(profileName, {
977
+ type: "api_key",
978
+ provider: updated.provider,
979
+ key: newApiKey
980
+ });
981
+ storeForSave.configs[index].authProfile = profileName;
982
+ }
983
+ saveLlmConfig(aiDataDir, storeForSave);
984
+ return { success: true };
985
+ }
986
+ function deleteLlmConfig(aiDataDir, id) {
987
+ const storeForSave = loadRawConfigStore(aiDataDir);
988
+ const index = storeForSave.configs.findIndex((c) => c.id === id);
989
+ if (index === -1) {
990
+ return { success: false, error: "Config not found" };
991
+ }
992
+ storeForSave.configs.splice(index, 1);
993
+ const fallback = storeForSave.configs[0];
994
+ if (storeForSave.defaultAssistant?.configId === id) {
995
+ storeForSave.defaultAssistant = fallback ? { configId: fallback.id, modelId: fallback.model || "" } : null;
996
+ }
997
+ if (storeForSave.fastModel?.configId === id) {
998
+ storeForSave.fastModel = fallback ? { configId: fallback.id, modelId: fallback.model || "" } : null;
999
+ }
1000
+ saveLlmConfig(aiDataDir, storeForSave);
1001
+ return { success: true };
1002
+ }
1003
+ function setDefaultAssistantSlot(aiDataDir, configId, modelId) {
1004
+ const storeForSave = loadRawConfigStore(aiDataDir);
1005
+ const config = storeForSave.configs.find((c) => c.id === configId);
1006
+ if (!config) {
1007
+ return { success: false, error: "Config not found" };
1008
+ }
1009
+ storeForSave.defaultAssistant = { configId, modelId };
1010
+ saveLlmConfig(aiDataDir, storeForSave);
1011
+ return { success: true };
1012
+ }
1013
+ function setFastModelSlot(aiDataDir, slot) {
1014
+ const storeForSave = loadRawConfigStore(aiDataDir);
1015
+ if (slot !== null) {
1016
+ const config = storeForSave.configs.find((c) => c.id === slot.configId);
1017
+ if (!config) {
1018
+ return { success: false, error: "Config not found" };
1019
+ }
1020
+ }
1021
+ storeForSave.fastModel = slot;
1022
+ saveLlmConfig(aiDataDir, storeForSave);
1023
+ return { success: true };
1024
+ }
1025
+ function loadRawConfigStore(aiDataDir) {
1026
+ const configPath = path3.join(aiDataDir, "llm-config.json");
1027
+ if (!fs3.existsSync(configPath)) {
1028
+ return { configs: [], defaultAssistant: null, fastModel: null };
1029
+ }
1030
+ try {
1031
+ const data = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
1032
+ return {
1033
+ configs: data.configs || [],
1034
+ defaultAssistant: data.defaultAssistant ?? null,
1035
+ fastModel: data.fastModel ?? null
1036
+ };
1037
+ } catch {
1038
+ return { configs: [], defaultAssistant: null, fastModel: null };
1039
+ }
1040
+ }
1041
+ function buildPiModel2(config) {
1042
+ return buildPiModel(config);
1043
+ }
1044
+
1045
+ // src/http/routes/web/summaries.ts
1046
+ function createSummaryDeps(dbManager2) {
1047
+ const aiDataDir = getAiDataDir(dbManager2);
1048
+ return {
1049
+ getLlmConfig() {
1050
+ return getDefaultAssistantConfig(aiDataDir);
1051
+ },
1052
+ buildPiModel(config) {
1053
+ return buildPiModel2(config);
1054
+ }
1055
+ };
1056
+ }
1057
+ function registerSummaryRoutes(server2, dbManager2, adapter) {
1058
+ const deps = createSummaryDeps(dbManager2);
1059
+ server2.post("/_web/sessions/:id/summaries/generate", async (request, reply) => {
1060
+ const { chatSessionId, locale, forceRegenerate } = request.body;
1061
+ const result = await summary_service_exports.generateSummary(adapter, request.params.id, chatSessionId, deps, {
1062
+ locale,
1063
+ forceRegenerate
1064
+ });
1065
+ if ("error" in result && !result.success) {
1066
+ return reply.code(400).send({ error: result.error });
1067
+ }
1068
+ return result;
1069
+ });
1070
+ server2.post("/_web/sessions/:id/summaries/generate-all", async (request, reply) => {
1071
+ const { locale, forceRegenerate } = request.body;
1072
+ const result = await summary_service_exports.generateAllSummaries(adapter, request.params.id, deps, {
1073
+ locale,
1074
+ forceRegenerate
1075
+ });
1076
+ if (result.error) {
1077
+ return reply.code(400).send({ error: result.error });
1078
+ }
1079
+ return result;
1080
+ });
1081
+ }
1082
+
1083
+ // src/http/routes/web/import.ts
1084
+ import * as fs4 from "fs";
1085
+ import * as os from "os";
1086
+ import * as path4 from "path";
1087
+ var DEMO_BASE_URL = "https://chatlab.fun/assets/demo";
1088
+ function cleanupTemp(...paths) {
1089
+ for (const p of paths) {
1090
+ try {
1091
+ const stat = fs4.statSync(p);
1092
+ if (stat.isDirectory()) {
1093
+ fs4.rmSync(p, { recursive: true, force: true });
1094
+ } else {
1095
+ fs4.unlinkSync(p);
1096
+ }
1097
+ } catch {
1098
+ }
1099
+ }
1100
+ }
1101
+ function registerImportRoutes(server2, dbManager2) {
1102
+ server2.get("/_web/supported-formats", async () => {
1103
+ return getSupportedFormats();
1104
+ });
1105
+ server2.post("/_web/detect-format", async (request, reply) => {
1106
+ const data = await request.file();
1107
+ if (!data) return reply.code(400).send({ error: "No file uploaded" });
1108
+ const tmpDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "chatlab-detect-"));
1109
+ const tmpPath = path4.join(tmpDir, data.filename || "upload");
1110
+ try {
1111
+ const chunks = [];
1112
+ for await (const chunk of data.file) {
1113
+ chunks.push(chunk);
1114
+ }
1115
+ fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
1116
+ const format = detectFormat(tmpPath);
1117
+ const allFormats = detectAllFormats(tmpPath);
1118
+ return { format, allFormats };
1119
+ } finally {
1120
+ cleanupTemp(tmpPath, tmpDir);
1121
+ }
1122
+ });
1123
+ server2.post("/_web/scan-multi-chat", async (request, reply) => {
1124
+ const data = await request.file();
1125
+ if (!data) return reply.code(400).send({ error: "No file uploaded" });
1126
+ const tmpDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "chatlab-scan-"));
1127
+ const tmpPath = path4.join(tmpDir, data.filename || "upload");
1128
+ try {
1129
+ const chunks = [];
1130
+ for await (const chunk of data.file) {
1131
+ chunks.push(chunk);
1132
+ }
1133
+ fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
1134
+ const chats = await scanMultiChatFile(tmpPath);
1135
+ return { chats };
1136
+ } finally {
1137
+ cleanupTemp(tmpPath, tmpDir);
1138
+ }
1139
+ });
1140
+ server2.post("/_web/import", async (request, reply) => {
1141
+ const data = await request.file();
1142
+ if (!data) return reply.code(400).send({ error: "No file uploaded" });
1143
+ const tmpDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "chatlab-import-"));
1144
+ const tmpPath = path4.join(tmpDir, data.filename || "upload");
1145
+ const chunks = [];
1146
+ for await (const chunk of data.file) {
1147
+ chunks.push(chunk);
1148
+ }
1149
+ fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
1150
+ const formatId = data.fields?.formatId?.value;
1151
+ const chatIndexStr = data.fields?.chatIndex?.value;
1152
+ const chatIndex = chatIndexStr !== void 0 ? parseInt(chatIndexStr, 10) : void 0;
1153
+ reply.raw.writeHead(200, {
1154
+ "Content-Type": "text/event-stream",
1155
+ "Cache-Control": "no-cache",
1156
+ Connection: "keep-alive"
1157
+ });
1158
+ function sendEvent(event, eventData) {
1159
+ reply.raw.write(`event: ${event}
1160
+ data: ${JSON.stringify(eventData)}
1161
+
1162
+ `);
1163
+ }
1164
+ try {
1165
+ const nativeBinding = resolveNativeBinding();
1166
+ const result = await streamImport(dbManager2, tmpPath, {
1167
+ formatId,
1168
+ chatIndex,
1169
+ nativeBinding,
1170
+ onProgress: (p) => sendEvent("progress", p)
1171
+ });
1172
+ if (result.success) {
1173
+ sendEvent("done", {
1174
+ success: true,
1175
+ sessionId: result.sessionId,
1176
+ messageCount: result.diagnostics?.messagesWritten ?? 0,
1177
+ memberCount: 0
1178
+ });
1179
+ } else {
1180
+ sendEvent("error", { success: false, error: result.error });
1181
+ }
1182
+ } catch (err) {
1183
+ sendEvent("error", { success: false, error: err instanceof Error ? err.message : String(err) });
1184
+ } finally {
1185
+ reply.raw.end();
1186
+ cleanupTemp(tmpPath, tmpDir);
1187
+ }
1188
+ });
1189
+ server2.post("/_web/import-directory", async (request, reply) => {
1190
+ const parts = request.parts();
1191
+ if (!parts) return reply.code(400).send({ error: "No files uploaded" });
1192
+ const tmpDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "chatlab-dir-import-"));
1193
+ const relativePaths = [];
1194
+ const fileBuffers = [];
1195
+ try {
1196
+ let sendEvent2 = function(event, eventData) {
1197
+ reply.raw.write(`event: ${event}
1198
+ data: ${JSON.stringify(eventData)}
1199
+
1200
+ `);
1201
+ };
1202
+ var sendEvent = sendEvent2;
1203
+ for await (const part of parts) {
1204
+ if (part.type === "field" && part.fieldname === "relativePaths") {
1205
+ relativePaths.push(String(part.value));
1206
+ } else if (part.type === "file") {
1207
+ const chunks = [];
1208
+ for await (const chunk of part.file) {
1209
+ chunks.push(chunk);
1210
+ }
1211
+ fileBuffers.push({ data: Buffer.concat(chunks), filename: part.filename || "" });
1212
+ }
1213
+ }
1214
+ for (let i = 0; i < fileBuffers.length; i++) {
1215
+ let relPath = relativePaths[i] || fileBuffers[i].filename || `file_${i}`;
1216
+ const segments = relPath.split("/");
1217
+ if (segments.length > 1) {
1218
+ relPath = segments.slice(1).join("/");
1219
+ }
1220
+ const targetPath = path4.resolve(tmpDir, relPath);
1221
+ if (!targetPath.startsWith(tmpDir + path4.sep)) continue;
1222
+ fs4.mkdirSync(path4.dirname(targetPath), { recursive: true });
1223
+ fs4.writeFileSync(targetPath, fileBuffers[i].data);
1224
+ }
1225
+ const entryPath = findEntryFileInDirectory(tmpDir);
1226
+ if (!entryPath) {
1227
+ return reply.code(400).send({ error: "No recognizable import format found in directory" });
1228
+ }
1229
+ reply.raw.writeHead(200, {
1230
+ "Content-Type": "text/event-stream",
1231
+ "Cache-Control": "no-cache",
1232
+ Connection: "keep-alive"
1233
+ });
1234
+ const nativeBinding = resolveNativeBinding();
1235
+ const result = await streamImport(dbManager2, entryPath, {
1236
+ nativeBinding,
1237
+ onProgress: (p) => sendEvent2("progress", p)
1238
+ });
1239
+ if (result.success) {
1240
+ sendEvent2("done", {
1241
+ success: true,
1242
+ sessionId: result.sessionId,
1243
+ messageCount: result.diagnostics?.messagesWritten ?? 0,
1244
+ memberCount: 0
1245
+ });
1246
+ } else {
1247
+ sendEvent2("error", { success: false, error: result.error });
1248
+ }
1249
+ } catch (err) {
1250
+ if (!reply.raw.headersSent) {
1251
+ return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
1252
+ }
1253
+ reply.raw.write(
1254
+ `event: error
1255
+ data: ${JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) })}
1256
+
1257
+ `
1258
+ );
1259
+ } finally {
1260
+ reply.raw.end();
1261
+ cleanupTemp(tmpDir);
1262
+ }
1263
+ });
1264
+ server2.post("/_web/sessions/:id/import/incremental", async (request, reply) => {
1265
+ const sessionId = request.params.id;
1266
+ const data = await request.file();
1267
+ if (!data) return reply.code(400).send({ error: "No file uploaded" });
1268
+ const tmpDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "chatlab-inc-"));
1269
+ const tmpPath = path4.join(tmpDir, data.filename || "upload");
1270
+ const chunks = [];
1271
+ for await (const chunk of data.file) {
1272
+ chunks.push(chunk);
1273
+ }
1274
+ fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
1275
+ reply.raw.writeHead(200, {
1276
+ "Content-Type": "text/event-stream",
1277
+ "Cache-Control": "no-cache",
1278
+ Connection: "keep-alive"
1279
+ });
1280
+ function sendEvent(event, eventData) {
1281
+ reply.raw.write(`event: ${event}
1282
+ data: ${JSON.stringify(eventData)}
1283
+
1284
+ `);
1285
+ }
1286
+ try {
1287
+ const result = await incrementalImport(dbManager2, sessionId, tmpPath, {
1288
+ onProgress: (p) => sendEvent("progress", p)
1289
+ });
1290
+ if (result.success) {
1291
+ sendEvent("done", result);
1292
+ } else {
1293
+ sendEvent("error", { success: false, error: result.error });
1294
+ }
1295
+ } catch (err) {
1296
+ sendEvent("error", { success: false, error: err instanceof Error ? err.message : String(err) });
1297
+ } finally {
1298
+ reply.raw.end();
1299
+ cleanupTemp(tmpPath, tmpDir);
1300
+ }
1301
+ });
1302
+ server2.post("/_web/sessions/:id/import/incremental/analyze", async (request, reply) => {
1303
+ const sessionId = request.params.id;
1304
+ const data = await request.file();
1305
+ if (!data) return reply.code(400).send({ error: "No file uploaded" });
1306
+ const tmpDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "chatlab-analyze-"));
1307
+ const tmpPath = path4.join(tmpDir, data.filename || "upload");
1308
+ const chunks = [];
1309
+ for await (const chunk of data.file) {
1310
+ chunks.push(chunk);
1311
+ }
1312
+ fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
1313
+ try {
1314
+ return await analyzeIncrementalImport(dbManager2, sessionId, tmpPath);
1315
+ } catch (err) {
1316
+ return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
1317
+ } finally {
1318
+ cleanupTemp(tmpPath, tmpDir);
1319
+ }
1320
+ });
1321
+ server2.post("/_web/import/analyze", async (request, reply) => {
1322
+ const data = await request.file();
1323
+ if (!data) return reply.code(400).send({ error: "No file uploaded" });
1324
+ const tmpDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "chatlab-analyze-"));
1325
+ const tmpPath = path4.join(tmpDir, data.filename || "upload");
1326
+ const chunks = [];
1327
+ for await (const chunk of data.file) {
1328
+ chunks.push(chunk);
1329
+ }
1330
+ fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
1331
+ try {
1332
+ return await analyzeNewImport(tmpPath);
1333
+ } catch (err) {
1334
+ return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
1335
+ } finally {
1336
+ cleanupTemp(tmpPath, tmpDir);
1337
+ }
1338
+ });
1339
+ server2.post("/_web/demo/import", async (request, reply) => {
1340
+ const locale = request.body?.locale || "en";
1341
+ const nativeBinding = resolveNativeBinding();
1342
+ reply.raw.writeHead(200, {
1343
+ "Content-Type": "text/event-stream",
1344
+ "Cache-Control": "no-cache",
1345
+ Connection: "keep-alive"
1346
+ });
1347
+ function sendEvent(event, eventData) {
1348
+ reply.raw.write(`event: ${event}
1349
+ data: ${JSON.stringify(eventData)}
1350
+
1351
+ `);
1352
+ }
1353
+ const tmpDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "chatlab-demo-"));
1354
+ const groupPath = path4.join(tmpDir, "demo-group.json");
1355
+ const privatePath = path4.join(tmpDir, "demo-private.json");
1356
+ try {
1357
+ sendEvent("progress", { stage: "downloading", current: 1, total: 2 });
1358
+ const groupResp = await fetch(`${DEMO_BASE_URL}/${locale}/demo-group.json`, {
1359
+ signal: AbortSignal.timeout(6e4)
1360
+ });
1361
+ if (!groupResp.ok) throw new Error(`Download group demo failed: ${groupResp.status}`);
1362
+ fs4.writeFileSync(groupPath, Buffer.from(await groupResp.arrayBuffer()));
1363
+ sendEvent("progress", { stage: "downloading", current: 2, total: 2 });
1364
+ const privateResp = await fetch(`${DEMO_BASE_URL}/${locale}/demo-private.json`, {
1365
+ signal: AbortSignal.timeout(6e4)
1366
+ });
1367
+ if (!privateResp.ok) throw new Error(`Download private demo failed: ${privateResp.status}`);
1368
+ fs4.writeFileSync(privatePath, Buffer.from(await privateResp.arrayBuffer()));
1369
+ sendEvent("progress", { stage: "importing", current: 1, total: 2 });
1370
+ const groupResult = await streamImport(dbManager2, groupPath, { nativeBinding });
1371
+ if (!groupResult.success) throw new Error(groupResult.error || "Failed to import group demo");
1372
+ sendEvent("progress", { stage: "importing", current: 2, total: 2 });
1373
+ const privateResult = await streamImport(dbManager2, privatePath, { nativeBinding });
1374
+ if (!privateResult.success) throw new Error(privateResult.error || "Failed to import private demo");
1375
+ sendEvent("progress", { stage: "done", current: 2, total: 2 });
1376
+ sendEvent("result", {
1377
+ success: true,
1378
+ groupSessionId: groupResult.sessionId,
1379
+ privateSessionId: privateResult.sessionId
1380
+ });
1381
+ } catch (err) {
1382
+ const message = err instanceof Error ? err.message : String(err);
1383
+ sendEvent("progress", { stage: "error", current: 0, total: 2, message });
1384
+ sendEvent("result", { success: false, error: message });
1385
+ } finally {
1386
+ reply.raw.end();
1387
+ cleanupTemp(tmpDir);
1388
+ }
1389
+ });
1390
+ }
1391
+
1392
+ // src/http/routes/web/merge.ts
1393
+ import * as fs5 from "fs";
1394
+ import * as os2 from "os";
1395
+ import * as path5 from "path";
1396
+ function registerMergeRoutes(server2, dbManager2, mergeCache) {
1397
+ server2.post("/_web/merge/parse", async (request, reply) => {
1398
+ const data = await request.file();
1399
+ if (!data) return reply.code(400).send({ error: "No file uploaded" });
1400
+ const tmpDir = fs5.mkdtempSync(path5.join(os2.tmpdir(), "chatlab-merge-"));
1401
+ const tmpPath = path5.join(tmpDir, data.filename || "upload");
1402
+ const chunks = [];
1403
+ for await (const chunk of data.file) {
1404
+ chunks.push(chunk);
1405
+ }
1406
+ fs5.writeFileSync(tmpPath, Buffer.concat(chunks));
1407
+ try {
1408
+ const result = await streamParseFileInfo(tmpPath, {
1409
+ createTempDatabase(sourceFilePath) {
1410
+ return mergeCache.createTempDatabase(path5.basename(sourceFilePath));
1411
+ },
1412
+ onProgress() {
1413
+ }
1414
+ });
1415
+ const handle = mergeCache.store(data.filename || "upload", result.tempDbPath);
1416
+ return {
1417
+ handle,
1418
+ name: result.name,
1419
+ format: result.format,
1420
+ platform: result.platform,
1421
+ messageCount: result.messageCount,
1422
+ memberCount: result.memberCount,
1423
+ fileSize: result.fileSize
1424
+ };
1425
+ } finally {
1426
+ try {
1427
+ fs5.unlinkSync(tmpPath);
1428
+ } catch {
1429
+ }
1430
+ try {
1431
+ fs5.rmdirSync(tmpDir);
1432
+ } catch {
1433
+ }
1434
+ }
1435
+ });
1436
+ server2.post("/_web/merge/conflicts", async (request, reply) => {
1437
+ const { handles } = request.body;
1438
+ if (!handles || !Array.isArray(handles) || handles.length === 0) {
1439
+ return reply.code(400).send({ error: "Missing or empty handles array" });
1440
+ }
1441
+ const readers = [];
1442
+ try {
1443
+ const dataSources = [];
1444
+ for (const handle of handles) {
1445
+ const entry = mergeCache.openReader(handle);
1446
+ if (!entry) return reply.code(404).send({ error: `Handle not found: ${handle}` });
1447
+ readers.push(entry.reader);
1448
+ dataSources.push({ source: entry.reader.toDataSource(), filename: entry.filename });
1449
+ }
1450
+ return checkConflictsFromSources(dataSources);
1451
+ } finally {
1452
+ for (const r of readers) r.close();
1453
+ }
1454
+ });
1455
+ server2.post("/_web/merge/execute", async (request, reply) => {
1456
+ const { handles, outputName, format = "json", andImport } = request.body;
1457
+ if (!handles || !Array.isArray(handles) || handles.length === 0) {
1458
+ return reply.code(400).send({ error: "Missing handles" });
1459
+ }
1460
+ if (!outputName) {
1461
+ return reply.code(400).send({ error: "Missing outputName" });
1462
+ }
1463
+ const readers = [];
1464
+ try {
1465
+ const dataSources = [];
1466
+ for (const handle of handles) {
1467
+ const entry = mergeCache.openReader(handle);
1468
+ if (!entry) return reply.code(404).send({ error: `Handle not found: ${handle}` });
1469
+ readers.push(entry.reader);
1470
+ dataSources.push({ source: entry.reader.toDataSource(), filename: entry.filename });
1471
+ }
1472
+ const merged = buildMergedOutput(dataSources, outputName);
1473
+ let sessionId;
1474
+ if (andImport) {
1475
+ const { streamImport: serverStreamImport } = await import("./stream-import-URGOCXUH.mjs");
1476
+ const jsonData = JSON.stringify(merged.chatLabData);
1477
+ const tmpDir = fs5.mkdtempSync(path5.join(os2.tmpdir(), "chatlab-merged-"));
1478
+ const tmpPath = path5.join(tmpDir, `${outputName}.json`);
1479
+ fs5.writeFileSync(tmpPath, jsonData, "utf-8");
1480
+ try {
1481
+ const importResult = await serverStreamImport(dbManager2, tmpPath);
1482
+ sessionId = importResult.sessionId;
1483
+ } finally {
1484
+ try {
1485
+ fs5.unlinkSync(tmpPath);
1486
+ } catch {
1487
+ }
1488
+ try {
1489
+ fs5.rmdirSync(tmpDir);
1490
+ } catch {
1491
+ }
1492
+ }
1493
+ }
1494
+ for (const handle of handles) {
1495
+ mergeCache.delete(handle);
1496
+ }
1497
+ if (format === "jsonl") {
1498
+ const lines = [];
1499
+ for (const line of serializeChatLabToJsonl(merged.chatLabData)) {
1500
+ lines.push(line);
1501
+ }
1502
+ return { success: true, sessionId, data: lines.join("\n") };
1503
+ }
1504
+ return { success: true, sessionId, data: merged.chatLabData };
1505
+ } finally {
1506
+ for (const r of readers) r.close();
1507
+ }
1508
+ });
1509
+ server2.post("/_web/merge/clear", async (request) => {
1510
+ const { handle } = request.body || {};
1511
+ if (handle) {
1512
+ mergeCache.delete(handle);
1513
+ } else {
1514
+ mergeCache.clear();
1515
+ }
1516
+ return { success: true };
1517
+ });
1518
+ server2.post("/_web/sessions/export-for-merge", async (request, reply) => {
1519
+ const { sessionIds } = request.body;
1520
+ if (!sessionIds || !Array.isArray(sessionIds) || sessionIds.length === 0) {
1521
+ return reply.code(400).send({ error: "Missing sessionIds" });
1522
+ }
1523
+ const handles = [];
1524
+ for (const sid of sessionIds) {
1525
+ const db = ensureDb2(dbManager2, sid);
1526
+ const exported = exportSessionToJson(db);
1527
+ const { db: tempDb, tempDbPath } = mergeCache.createTempDatabase(exported.meta.name);
1528
+ const { TempDbWriter } = await import("./src-KOIGBZJX.mjs");
1529
+ const writer = new TempDbWriter(tempDb);
1530
+ writer.writeMeta({
1531
+ name: exported.meta.name,
1532
+ platform: exported.meta.platform,
1533
+ type: exported.meta.type,
1534
+ groupId: exported.meta.groupId,
1535
+ groupAvatar: exported.meta.groupAvatar
1536
+ });
1537
+ writer.writeMembers(
1538
+ exported.members.map((m) => ({
1539
+ platformId: m.platformId,
1540
+ accountName: m.accountName,
1541
+ groupNickname: m.groupNickname,
1542
+ avatar: m.avatar
1543
+ }))
1544
+ );
1545
+ writer.writeMessages(
1546
+ exported.messages.map((msg) => ({
1547
+ senderPlatformId: msg.sender,
1548
+ senderAccountName: msg.accountName,
1549
+ senderGroupNickname: msg.groupNickname,
1550
+ timestamp: msg.timestamp,
1551
+ type: msg.type,
1552
+ content: msg.content
1553
+ }))
1554
+ );
1555
+ writer.finish();
1556
+ const handle = mergeCache.store(exported.meta.name, tempDbPath);
1557
+ handles.push({ sessionId: sid, handle });
1558
+ }
1559
+ return { success: true, handles };
1560
+ });
1561
+ }
1562
+
1563
+ // src/http/routes/web/export.ts
1564
+ function registerExportRoutes(server2, adapter) {
1565
+ server2.post("/_web/sessions/:id/export/markdown", async (request, reply) => {
1566
+ const { id } = request.params;
1567
+ const body = request.body;
1568
+ const sessionName = body?.sessionName || id;
1569
+ const { result, content } = export_service_exports.exportMarkdown(adapter, {
1570
+ sessionId: id,
1571
+ sessionName,
1572
+ filterMode: body.filterMode || "condition",
1573
+ keywords: body.keywords,
1574
+ timeFilter: body.timeFilter,
1575
+ senderIds: body.senderIds,
1576
+ contextSize: body.contextSize,
1577
+ chatSessionIds: body.chatSessionIds
1578
+ });
1579
+ if (!result.success) {
1580
+ return reply.code(500).send({ error: result.error });
1581
+ }
1582
+ reply.header("Content-Type", "text/markdown; charset=utf-8");
1583
+ reply.header("Content-Disposition", `attachment; filename="${encodeURIComponent(sessionName)}_export.md"`);
1584
+ return reply.send(content);
1585
+ });
1586
+ }
1587
+
1588
+ // src/http/routes/web/cache.ts
1589
+ import * as fs6 from "fs";
1590
+ import * as path6 from "path";
1591
+ function registerCacheRoutes(server2, downloadsDir) {
1592
+ server2.post("/_web/cache/save-to-downloads", async (request) => {
1593
+ const { filename, dataUrl } = request.body;
1594
+ if (!filename || !dataUrl) {
1595
+ return { success: false, error: "filename and dataUrl are required" };
1596
+ }
1597
+ const base64Prefix = dataUrl.indexOf(",");
1598
+ const base64Data = base64Prefix >= 0 ? dataUrl.slice(base64Prefix + 1) : dataUrl;
1599
+ const buffer = Buffer.from(base64Data, "base64");
1600
+ if (!fs6.existsSync(downloadsDir)) {
1601
+ fs6.mkdirSync(downloadsDir, { recursive: true });
1602
+ }
1603
+ const filePath = path6.join(downloadsDir, filename);
1604
+ fs6.writeFileSync(filePath, buffer);
1605
+ return { success: true, filePath };
1606
+ });
1607
+ server2.post("/_web/cache/show-in-folder", async (request) => {
1608
+ const { filePath } = request.body;
1609
+ if (!filePath) {
1610
+ return { success: false, error: "filePath is required" };
1611
+ }
1612
+ const { exec } = await import("child_process");
1613
+ const platform = process.platform;
1614
+ const dir = path6.dirname(filePath);
1615
+ if (platform === "darwin") {
1616
+ exec(`open -R "${filePath}"`);
1617
+ } else if (platform === "win32") {
1618
+ exec(`explorer /select,"${filePath}"`);
1619
+ } else {
1620
+ exec(`xdg-open "${dir}"`);
1621
+ }
1622
+ return { success: true };
1623
+ });
1624
+ }
1625
+
1626
+ // src/http/routes/web/index.ts
1627
+ function registerWebRoutes(server2, dbManager2, options) {
1628
+ const adapter = createDatabaseManagerAdapter(dbManager2);
1629
+ const mergeCache = options?.pathProvider ? new MergeSessionCache(options.pathProvider, { nativeBinding: options.nativeBinding }) : null;
1630
+ mergeCache?.cleanupOrphans();
1631
+ const downloadsDir = options?.pathProvider?.getDownloadsDir() || path7.join(os3.homedir(), "Downloads");
1632
+ registerSessionRoutes2(server2, adapter);
1633
+ registerMemberRoutes(server2, adapter);
1634
+ registerAnalyticsRoutes(server2, dbManager2, adapter);
1635
+ registerSqlRoutes(server2, adapter);
1636
+ registerSessionIndexRoutes(server2, adapter);
1637
+ registerSummaryRoutes(server2, dbManager2, adapter);
1638
+ registerImportRoutes(server2, dbManager2);
1639
+ if (mergeCache) {
1640
+ registerMergeRoutes(server2, dbManager2, mergeCache);
1641
+ }
1642
+ registerExportRoutes(server2, adapter);
1643
+ registerCacheRoutes(server2, downloadsDir);
1644
+ }
1645
+
1646
+ // src/http/routes/nlp.ts
1647
+ import * as path8 from "path";
1648
+ function ensureDb3(dbManager2, sessionId) {
1649
+ const db = dbManager2.open(sessionId);
1650
+ if (!db) {
1651
+ throw Object.assign(new Error(`Session not found: ${sessionId}`), { statusCode: 404 });
1652
+ }
1653
+ return db;
1654
+ }
1655
+ function registerNlpRoutes(server2, dbManager2) {
1656
+ const pathProvider = dbManager2["pathProvider"];
1657
+ if (pathProvider) {
1658
+ const nlpDir = path8.join(pathProvider.getUserDataDir(), "nlp");
1659
+ initNlpDir(nlpDir);
1660
+ ensureDefaultDict(nlpDir).catch((err) => console.warn("[NLP] Auto-download zh-CN dict failed:", err));
1661
+ }
1662
+ server2.get("/_web/nlp/pos-tags", async () => {
1663
+ return POS_TAG_DEFINITIONS;
1664
+ });
1665
+ server2.get("/_web/nlp/dicts", async () => {
1666
+ const nlpDir = resolveNlpDir(dbManager2);
1667
+ return getDictList(nlpDir);
1668
+ });
1669
+ server2.get("/_web/nlp/dicts/:id/status", async (request) => {
1670
+ const nlpDir = resolveNlpDir(dbManager2);
1671
+ return isDictDownloaded(nlpDir, request.params.id);
1672
+ });
1673
+ server2.post("/_web/nlp/dicts/:id/download", async (request) => {
1674
+ const nlpDir = resolveNlpDir(dbManager2);
1675
+ return downloadDict(nlpDir, request.params.id);
1676
+ });
1677
+ server2.delete("/_web/nlp/dicts/:id", async (request) => {
1678
+ const nlpDir = resolveNlpDir(dbManager2);
1679
+ return deleteDict(nlpDir, request.params.id);
1680
+ });
1681
+ server2.post("/_web/nlp/word-frequency", async (request) => {
1682
+ const params = request.body;
1683
+ const db = ensureDb3(dbManager2, params.sessionId);
1684
+ return computeWordFrequency(db, params);
1685
+ });
1686
+ server2.post(
1687
+ "/_web/nlp/segment",
1688
+ async (request) => {
1689
+ const { text, locale, minLength } = request.body;
1690
+ return segmentText(text, locale, minLength);
1691
+ }
1692
+ );
1693
+ }
1694
+ function resolveNlpDir(dbManager2) {
1695
+ const pathProvider = dbManager2["pathProvider"];
1696
+ return path8.join(pathProvider.getUserDataDir(), "nlp");
1697
+ }
1698
+
1699
+ // src/http/routes/ai.ts
1700
+ import * as fs10 from "fs";
1701
+ import * as path12 from "path";
1702
+
1703
+ // src/ai/logger.ts
1704
+ var logger = null;
1705
+ function initServerAiLogger(logsDir) {
1706
+ if (!logger) {
1707
+ logger = new AiLogger(logsDir);
1708
+ }
1709
+ return logger;
1710
+ }
1711
+ function getServerAiLogger() {
1712
+ return logger;
1713
+ }
1714
+ function closeServerAiLogger() {
1715
+ if (logger) {
1716
+ logger.close();
1717
+ logger = null;
1718
+ }
1719
+ }
1720
+
1721
+ // src/ai/tool-adapter.ts
1722
+ var DEFAULT_MAX_TOOL_RESULT_TOKENS = 8e3;
1723
+ var TOOL_TRUNCATION_STRATEGY = {
1724
+ search_messages: "keep_first",
1725
+ deep_search_messages: "keep_first",
1726
+ get_recent_messages: "keep_last",
1727
+ get_message_context: "keep_last",
1728
+ get_session_messages: "keep_last",
1729
+ get_conversation_between: "keep_last"
1730
+ };
1731
+ function convertJsonSchemaToParameters(schema) {
1732
+ const properties = {};
1733
+ for (const [key, prop] of Object.entries(schema.properties)) {
1734
+ properties[key] = { ...prop };
1735
+ }
1736
+ return {
1737
+ type: "object",
1738
+ properties,
1739
+ required: schema.required || []
1740
+ };
1741
+ }
1742
+ function adaptToolsForAgent(tools, getContext, options) {
1743
+ const tokenBudget = options?.maxToolResultTokens ?? DEFAULT_MAX_TOOL_RESULT_TOKENS;
1744
+ return tools.map((tool) => ({
1745
+ name: tool.name,
1746
+ label: tool.name,
1747
+ description: tool.description,
1748
+ parameters: convertJsonSchemaToParameters(tool.inputSchema),
1749
+ async execute(_toolCallId, params) {
1750
+ const ctx = getContext();
1751
+ const execCtx = {
1752
+ db: ctx.db,
1753
+ dataProvider: new CoreDataProvider(ctx.db),
1754
+ sessionId: ctx.sessionId,
1755
+ locale: ctx.locale,
1756
+ segmentText: (texts, locale, options2) => batchSegmentWithFrequency(texts, locale, options2)
1757
+ };
1758
+ try {
1759
+ const result = await tool.handler(params, execCtx);
1760
+ if (result.rawMessages && result.rawMessages.length > 0) {
1761
+ const pipelineResult = applyPreprocessingPipeline({
1762
+ rawMessages: result.rawMessages,
1763
+ locale: ctx.locale,
1764
+ maxToolResultTokens: tokenBudget,
1765
+ truncationStrategy: TOOL_TRUNCATION_STRATEGY[tool.name] ?? "keep_last",
1766
+ extraDetails: result.data ?? {},
1767
+ logger: getServerAiLogger() ?? void 0
1768
+ });
1769
+ return { content: [{ type: "text", text: pipelineResult.text }], details: null };
1770
+ }
1771
+ return { content: [{ type: "text", text: result.content }], details: null };
1772
+ } catch (error) {
1773
+ const msg = error instanceof Error ? error.message : String(error);
1774
+ return { content: [{ type: "text", text: `Error: ${msg}` }], details: null };
1775
+ }
1776
+ }
1777
+ }));
1778
+ }
1779
+
1780
+ // src/ai/assistant-loader.ts
1781
+ import * as fs7 from "fs";
1782
+ import * as path9 from "path";
1783
+ function loadAssistantConfig(aiDataDir, assistantId) {
1784
+ const filePath = path9.join(aiDataDir, "assistants", `${assistantId}.md`);
1785
+ if (!fs7.existsSync(filePath)) return null;
1786
+ try {
1787
+ const content = fs7.readFileSync(filePath, "utf-8");
1788
+ return parseAssistantFile(content, filePath);
1789
+ } catch {
1790
+ return null;
1791
+ }
1792
+ }
1793
+
1794
+ // src/ai/manager-factory.ts
1795
+ import * as fs8 from "fs";
1796
+ import * as path10 from "path";
1797
+ import { createHash, randomUUID as randomUUID3 } from "crypto";
1798
+ var nodeFs = {
1799
+ ensureDir(dir) {
1800
+ if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
1801
+ },
1802
+ listFiles(dir, ext) {
1803
+ if (!fs8.existsSync(dir)) return [];
1804
+ return fs8.readdirSync(dir).filter((f) => f.endsWith(ext));
1805
+ },
1806
+ readFile(filePath) {
1807
+ return fs8.readFileSync(filePath, "utf-8");
1808
+ },
1809
+ writeFile(filePath, content) {
1810
+ fs8.writeFileSync(filePath, content, "utf-8");
1811
+ },
1812
+ deleteFile(filePath) {
1813
+ if (fs8.existsSync(filePath)) fs8.unlinkSync(filePath);
1814
+ },
1815
+ fileExists(filePath) {
1816
+ return fs8.existsSync(filePath);
1817
+ },
1818
+ joinPath(...parts) {
1819
+ return path10.join(...parts);
1820
+ }
1821
+ };
1822
+ var managerCache = /* @__PURE__ */ new Map();
1823
+ function getAssistantManager(aiDataDir) {
1824
+ let cached = managerCache.get(aiDataDir);
1825
+ if (!cached) {
1826
+ cached = {
1827
+ assistant: new AssistantManager({
1828
+ fs: nodeFs,
1829
+ assistantsDir: path10.join(aiDataDir, "assistants"),
1830
+ builtinRawConfigs: [],
1831
+ generateId: () => `custom_${randomUUID3().replace(/-/g, "").slice(0, 12)}`
1832
+ }),
1833
+ skill: new SkillManagerCore({
1834
+ fs: nodeFs,
1835
+ skillsDir: path10.join(aiDataDir, "skills"),
1836
+ builtinRawSkills: [],
1837
+ contentHash: (content) => createHash("md5").update(content).digest("hex")
1838
+ })
1839
+ };
1840
+ managerCache.set(aiDataDir, cached);
1841
+ }
1842
+ return cached.assistant;
1843
+ }
1844
+ function getSkillManagerCore(aiDataDir) {
1845
+ let cached = managerCache.get(aiDataDir);
1846
+ if (!cached) {
1847
+ getAssistantManager(aiDataDir);
1848
+ cached = managerCache.get(aiDataDir);
1849
+ }
1850
+ return cached.skill;
1851
+ }
1852
+
1853
+ // src/ai/agent.ts
1854
+ async function runServerAgent(options) {
1855
+ const {
1856
+ userMessage,
1857
+ conversationId,
1858
+ chatType = "group",
1859
+ locale = "zh-CN",
1860
+ assistantSystemPrompt,
1861
+ skillMenu,
1862
+ compressionConfig,
1863
+ tools = [],
1864
+ aiDataDir,
1865
+ convManager: convManager2,
1866
+ onEvent,
1867
+ abortSignal,
1868
+ ownerInfo,
1869
+ mentionedMembers,
1870
+ dataSnapshot
1871
+ } = options;
1872
+ const aiLogger = getServerAiLogger();
1873
+ const llmConfig = getDefaultAssistantConfig(aiDataDir);
1874
+ if (!llmConfig) {
1875
+ onEvent({ type: "error", error: { name: "ConfigError", message: "LLM service not configured" } });
1876
+ onEvent({ type: "done", isFinished: true });
1877
+ return;
1878
+ }
1879
+ const piModel = buildPiModel2(llmConfig);
1880
+ const t = createAiTranslate(locale);
1881
+ let skillCtx;
1882
+ if (skillMenu) {
1883
+ skillCtx = { skillMenu };
1884
+ }
1885
+ const systemPrompt = buildSystemPrompt({
1886
+ t,
1887
+ chatType,
1888
+ assistantSystemPrompt,
1889
+ ownerInfo,
1890
+ locale,
1891
+ skillCtx,
1892
+ mentionedMembers,
1893
+ dataSnapshot
1894
+ });
1895
+ const handler = new AgentEventHandler({
1896
+ onChunk: onEvent,
1897
+ context: {},
1898
+ systemPrompt
1899
+ });
1900
+ if (compressionConfig?.enabled) {
1901
+ const llmAdapter = createCompressionLlmAdapter({
1902
+ piModel,
1903
+ apiKey: llmConfig.apiKey,
1904
+ onCompressing: () => handler.emitStatus("compressing", [])
1905
+ });
1906
+ const compressionResult = await checkAndCompress(
1907
+ conversationId,
1908
+ compressionConfig,
1909
+ systemPrompt,
1910
+ llmAdapter,
1911
+ convManager2,
1912
+ aiLogger ?? void 0
1913
+ );
1914
+ if (compressionResult.compressed) {
1915
+ onEvent({
1916
+ type: "compression_done",
1917
+ compressionResult: {
1918
+ summaryContent: compressionResult.summaryContent ?? "",
1919
+ tokensBefore: compressionResult.tokensBefore ?? 0,
1920
+ tokensAfter: compressionResult.tokensAfter ?? 0,
1921
+ timestamp: Date.now()
1922
+ }
1923
+ });
1924
+ }
1925
+ }
1926
+ if (abortSignal?.aborted) {
1927
+ handler.emitStatus("aborted", [], { force: true });
1928
+ onEvent({ type: "done", isFinished: true, usage: handler.cloneUsage() });
1929
+ return;
1930
+ }
1931
+ let history = [];
1932
+ try {
1933
+ history = convManager2.getHistoryForAgent(conversationId);
1934
+ } catch {
1935
+ }
1936
+ handler.emitStatus("preparing", [], { pendingUserMessage: userMessage, force: true });
1937
+ const steerMessage = t("ai.agent.answerWithoutTools");
1938
+ let cachedMessages = [];
1939
+ try {
1940
+ const result = await runAgentCore({
1941
+ piModel,
1942
+ apiKey: llmConfig.apiKey,
1943
+ systemPrompt,
1944
+ tools,
1945
+ history,
1946
+ userMessage,
1947
+ maxToolRounds: 5,
1948
+ abortSignal,
1949
+ steerMessage,
1950
+ onConvertToLlm: (filteredMessages) => {
1951
+ cachedMessages = filteredMessages;
1952
+ },
1953
+ onEvent: (coreEvent) => handler.handleCoreEvent(coreEvent, cachedMessages),
1954
+ onDebugContext: (messages) => {
1955
+ try {
1956
+ convManager2.setPendingDebugContext(conversationId, JSON.stringify(messages, null, 2));
1957
+ } catch {
1958
+ }
1959
+ }
1960
+ });
1961
+ if (result.error) {
1962
+ const friendlyMessage = formatAIError(result.error);
1963
+ onEvent({ type: "error", error: { name: "AgentError", message: friendlyMessage } });
1964
+ }
1965
+ handler.emitStatus("completed", cachedMessages, { force: true });
1966
+ onEvent({ type: "done", isFinished: true, usage: result.usage });
1967
+ } catch (error) {
1968
+ const friendlyMessage = formatAIError(error);
1969
+ aiLogger?.error("ServerAgent", "Agent execution error", { error: String(error) });
1970
+ handler.emitStatus("error", cachedMessages, { force: true });
1971
+ onEvent({ type: "error", error: { name: "AgentError", message: friendlyMessage } });
1972
+ onEvent({ type: "done", isFinished: true, usage: handler.cloneUsage() });
1973
+ }
1974
+ }
1975
+
1976
+ // src/ai/custom-store.ts
1977
+ import * as fs9 from "fs";
1978
+ import * as path11 from "path";
1979
+ import { randomUUID as randomUUID4 } from "crypto";
1980
+ function readProviders(aiDataDir) {
1981
+ const storePath = path11.join(aiDataDir, "custom-providers.json");
1982
+ if (!fs9.existsSync(storePath)) return [];
1983
+ try {
1984
+ const content = fs9.readFileSync(storePath, "utf-8");
1985
+ return JSON.parse(content);
1986
+ } catch {
1987
+ return [];
1988
+ }
1989
+ }
1990
+ function writeProviders(aiDataDir, providers) {
1991
+ const storePath = path11.join(aiDataDir, "custom-providers.json");
1992
+ const dir = path11.dirname(storePath);
1993
+ if (!fs9.existsSync(dir)) fs9.mkdirSync(dir, { recursive: true });
1994
+ fs9.writeFileSync(storePath, JSON.stringify(providers, null, 2), "utf-8");
1995
+ }
1996
+ function addCustomProvider(aiDataDir, input) {
1997
+ const providers = readProviders(aiDataDir);
1998
+ const newProvider = {
1999
+ ...input,
2000
+ id: `custom:${randomUUID4()}`,
2001
+ builtin: false,
2002
+ enabledByDefault: false
2003
+ };
2004
+ providers.push(newProvider);
2005
+ writeProviders(aiDataDir, providers);
2006
+ return newProvider;
2007
+ }
2008
+ function updateCustomProvider(aiDataDir, id, updates) {
2009
+ const providers = readProviders(aiDataDir);
2010
+ const index = providers.findIndex((p) => p.id === id);
2011
+ if (index === -1) return { success: false, error: "Custom provider not found" };
2012
+ providers[index] = { ...providers[index], ...updates };
2013
+ writeProviders(aiDataDir, providers);
2014
+ return { success: true };
2015
+ }
2016
+ function deleteCustomProvider(aiDataDir, id) {
2017
+ const providers = readProviders(aiDataDir);
2018
+ const index = providers.findIndex((p) => p.id === id);
2019
+ if (index === -1) return { success: false, error: "Custom provider not found" };
2020
+ providers.splice(index, 1);
2021
+ writeProviders(aiDataDir, providers);
2022
+ return { success: true };
2023
+ }
2024
+ function readModels(aiDataDir) {
2025
+ const storePath = path11.join(aiDataDir, "custom-models.json");
2026
+ if (!fs9.existsSync(storePath)) return [];
2027
+ try {
2028
+ const content = fs9.readFileSync(storePath, "utf-8");
2029
+ return JSON.parse(content);
2030
+ } catch {
2031
+ return [];
2032
+ }
2033
+ }
2034
+ function writeModels(aiDataDir, models) {
2035
+ const storePath = path11.join(aiDataDir, "custom-models.json");
2036
+ const dir = path11.dirname(storePath);
2037
+ if (!fs9.existsSync(dir)) fs9.mkdirSync(dir, { recursive: true });
2038
+ fs9.writeFileSync(storePath, JSON.stringify(models, null, 2), "utf-8");
2039
+ }
2040
+ function addCustomModel(aiDataDir, input) {
2041
+ const models = readModels(aiDataDir);
2042
+ const existing = models.find((m) => m.id === input.id && m.providerId === input.providerId);
2043
+ if (existing) {
2044
+ return { success: false, error: `Model "${input.id}" already exists under provider "${input.providerId}"` };
2045
+ }
2046
+ const newModel = {
2047
+ ...input,
2048
+ builtin: false,
2049
+ editable: true
2050
+ };
2051
+ models.push(newModel);
2052
+ writeModels(aiDataDir, models);
2053
+ return { success: true, model: newModel };
2054
+ }
2055
+ function updateCustomModel(aiDataDir, providerId, modelId, updates) {
2056
+ const models = readModels(aiDataDir);
2057
+ const index = models.findIndex((m) => m.id === modelId && m.providerId === providerId);
2058
+ if (index === -1) return { success: false, error: "Custom model not found" };
2059
+ models[index] = { ...models[index], ...updates };
2060
+ writeModels(aiDataDir, models);
2061
+ return { success: true };
2062
+ }
2063
+ function deleteCustomModel(aiDataDir, providerId, modelId) {
2064
+ const models = readModels(aiDataDir);
2065
+ const index = models.findIndex((m) => m.id === modelId && m.providerId === providerId);
2066
+ if (index === -1) return { success: false, error: "Custom model not found" };
2067
+ models.splice(index, 1);
2068
+ writeModels(aiDataDir, models);
2069
+ return { success: true };
2070
+ }
2071
+
2072
+ // src/http/routes/ai.ts
2073
+ function getAiDir(dbManager2) {
2074
+ const pathProvider = dbManager2["pathProvider"];
2075
+ if (!pathProvider) {
2076
+ throw Object.assign(new Error("PathProvider not available"), { statusCode: 500 });
2077
+ }
2078
+ return pathProvider.getAiDataDir();
2079
+ }
2080
+ function registerAiRoutes(server2, dbManager2, convManager2) {
2081
+ server2.get("/_web/ai/assistants", async () => {
2082
+ const mgr = getAssistantManager(getAiDir(dbManager2));
2083
+ return mgr.getAllAssistants();
2084
+ });
2085
+ server2.get("/_web/ai/assistants/:id", async (request, reply) => {
2086
+ const mgr = getAssistantManager(getAiDir(dbManager2));
2087
+ const config = mgr.getAssistantConfig(request.params.id);
2088
+ if (!config) return reply.code(404).send({ error: "Not found" });
2089
+ return config;
2090
+ });
2091
+ server2.post("/_web/ai/assistants", async (request) => {
2092
+ const mgr = getAssistantManager(getAiDir(dbManager2));
2093
+ return mgr.createAssistant(request.body);
2094
+ });
2095
+ server2.put("/_web/ai/assistants/:id", async (request, reply) => {
2096
+ const mgr = getAssistantManager(getAiDir(dbManager2));
2097
+ const result = mgr.updateAssistant(request.params.id, request.body);
2098
+ if (!result.success) return reply.code(404).send(result);
2099
+ return result;
2100
+ });
2101
+ server2.delete("/_web/ai/assistants/:id", async (request, reply) => {
2102
+ const mgr = getAssistantManager(getAiDir(dbManager2));
2103
+ const result = mgr.deleteAssistant(request.params.id);
2104
+ if (!result.success) return reply.code(400).send(result);
2105
+ return result;
2106
+ });
2107
+ server2.post("/_web/ai/assistants/:id/reset", async (request, reply) => {
2108
+ const mgr = getAssistantManager(getAiDir(dbManager2));
2109
+ const result = mgr.resetAssistant(request.params.id);
2110
+ if (!result.success) return reply.code(400).send(result);
2111
+ return result;
2112
+ });
2113
+ server2.post("/_web/ai/assistants/import", async (request) => {
2114
+ const mgr = getAssistantManager(getAiDir(dbManager2));
2115
+ return mgr.importAssistantFromMd(request.body.rawMd);
2116
+ });
2117
+ server2.get("/_web/ai/skills", async () => {
2118
+ const mgr = getSkillManagerCore(getAiDir(dbManager2));
2119
+ return mgr.getAllSkills();
2120
+ });
2121
+ server2.get("/_web/ai/skills/:id", async (request, reply) => {
2122
+ const mgr = getSkillManagerCore(getAiDir(dbManager2));
2123
+ const config = mgr.getSkillConfig(request.params.id);
2124
+ if (!config) return reply.code(404).send({ error: "Not found" });
2125
+ return config;
2126
+ });
2127
+ server2.post("/_web/ai/skills", async (request) => {
2128
+ const mgr = getSkillManagerCore(getAiDir(dbManager2));
2129
+ return mgr.createSkill(request.body.rawMd);
2130
+ });
2131
+ server2.put("/_web/ai/skills/:id", async (request, reply) => {
2132
+ const mgr = getSkillManagerCore(getAiDir(dbManager2));
2133
+ const result = mgr.updateSkill(request.params.id, request.body.rawMd);
2134
+ if (!result.success) return reply.code(404).send(result);
2135
+ return result;
2136
+ });
2137
+ server2.delete("/_web/ai/skills/:id", async (request, reply) => {
2138
+ const mgr = getSkillManagerCore(getAiDir(dbManager2));
2139
+ const result = mgr.deleteSkill(request.params.id);
2140
+ if (!result.success) return reply.code(400).send(result);
2141
+ return result;
2142
+ });
2143
+ server2.post("/_web/ai/skills/import", async (request) => {
2144
+ const mgr = getSkillManagerCore(getAiDir(dbManager2));
2145
+ return mgr.importSkillFromMd(request.body.rawMd);
2146
+ });
2147
+ server2.get("/_web/ai/skills/builtin-catalog", async () => {
2148
+ const mgr = getSkillManagerCore(getAiDir(dbManager2));
2149
+ return mgr.getBuiltinCatalog();
2150
+ });
2151
+ server2.get("/_web/ai/llm/has-config", async () => {
2152
+ const configPath = path12.join(getAiDir(dbManager2), "llm-config.json");
2153
+ if (!fs10.existsSync(configPath)) return false;
2154
+ try {
2155
+ const data = JSON.parse(fs10.readFileSync(configPath, "utf-8"));
2156
+ if (!data.configs || !Array.isArray(data.configs) || data.configs.length === 0) return false;
2157
+ return data.defaultAssistant != null;
2158
+ } catch {
2159
+ return false;
2160
+ }
2161
+ });
2162
+ server2.get("/_web/ai/llm/configs", async () => {
2163
+ const configPath = path12.join(getAiDir(dbManager2), "llm-config.json");
2164
+ if (!fs10.existsSync(configPath)) return { configs: [], defaultAssistant: null, fastModel: null };
2165
+ try {
2166
+ const data = JSON.parse(fs10.readFileSync(configPath, "utf-8"));
2167
+ const configs = (data.configs || []).map((c) => {
2168
+ const { apiKey: _k, ...rest } = c;
2169
+ return { ...rest, apiKey: "" };
2170
+ });
2171
+ return {
2172
+ configs,
2173
+ defaultAssistant: data.defaultAssistant ?? null,
2174
+ fastModel: data.fastModel ?? null
2175
+ };
2176
+ } catch {
2177
+ return { configs: [], defaultAssistant: null, fastModel: null };
2178
+ }
2179
+ });
2180
+ server2.get("/_web/ai/llm/providers", async () => {
2181
+ return BUILTIN_PROVIDERS.map((p) => {
2182
+ const models = getBuiltinModelsByProvider(p.id);
2183
+ return {
2184
+ id: p.id,
2185
+ name: p.name,
2186
+ defaultBaseUrl: p.defaultBaseUrl,
2187
+ models: models.filter((m) => !m.capabilities.includes("embedding") && !m.capabilities.includes("ranking")).map((m) => ({ id: m.id, name: m.name, description: m.description }))
2188
+ };
2189
+ });
2190
+ });
2191
+ server2.get("/_web/ai/llm/provider-registry", async () => {
2192
+ const customPath = path12.join(getAiDir(dbManager2), "custom-providers.json");
2193
+ let custom = [];
2194
+ if (fs10.existsSync(customPath)) {
2195
+ try {
2196
+ custom = JSON.parse(fs10.readFileSync(customPath, "utf-8"));
2197
+ if (!Array.isArray(custom)) custom = [];
2198
+ } catch {
2199
+ custom = [];
2200
+ }
2201
+ }
2202
+ return [...BUILTIN_PROVIDERS, ...custom];
2203
+ });
2204
+ server2.get("/_web/ai/llm/model-catalog", async () => {
2205
+ const customPath = path12.join(getAiDir(dbManager2), "custom-models.json");
2206
+ let custom = [];
2207
+ if (fs10.existsSync(customPath)) {
2208
+ try {
2209
+ custom = JSON.parse(fs10.readFileSync(customPath, "utf-8"));
2210
+ if (!Array.isArray(custom)) custom = [];
2211
+ } catch {
2212
+ custom = [];
2213
+ }
2214
+ }
2215
+ return [...BUILTIN_MODELS, ...custom];
2216
+ });
2217
+ server2.get("/_web/ai/llm/default-assistant-slot", async () => {
2218
+ const configPath = path12.join(getAiDir(dbManager2), "llm-config.json");
2219
+ if (!fs10.existsSync(configPath)) return null;
2220
+ try {
2221
+ const data = JSON.parse(fs10.readFileSync(configPath, "utf-8"));
2222
+ return data.defaultAssistant ?? null;
2223
+ } catch {
2224
+ return null;
2225
+ }
2226
+ });
2227
+ server2.get("/_web/ai/llm/fast-model-slot", async () => {
2228
+ const configPath = path12.join(getAiDir(dbManager2), "llm-config.json");
2229
+ if (!fs10.existsSync(configPath)) return null;
2230
+ try {
2231
+ const data = JSON.parse(fs10.readFileSync(configPath, "utf-8"));
2232
+ return data.fastModel ?? null;
2233
+ } catch {
2234
+ return null;
2235
+ }
2236
+ });
2237
+ server2.post("/_web/ai/llm/configs", async (request) => {
2238
+ const aiDataDir = getAiDir(dbManager2);
2239
+ return addLlmConfig(aiDataDir, request.body);
2240
+ });
2241
+ server2.put("/_web/ai/llm/configs/:id", async (request) => {
2242
+ const aiDataDir = getAiDir(dbManager2);
2243
+ return updateLlmConfig(aiDataDir, request.params.id, request.body);
2244
+ });
2245
+ server2.delete("/_web/ai/llm/configs/:id", async (request) => {
2246
+ const aiDataDir = getAiDir(dbManager2);
2247
+ return deleteLlmConfig(aiDataDir, request.params.id);
2248
+ });
2249
+ server2.put("/_web/ai/llm/default-assistant-slot", async (request) => {
2250
+ const aiDataDir = getAiDir(dbManager2);
2251
+ return setDefaultAssistantSlot(aiDataDir, request.body.configId, request.body.modelId);
2252
+ });
2253
+ server2.put("/_web/ai/llm/fast-model-slot", async (request) => {
2254
+ const aiDataDir = getAiDir(dbManager2);
2255
+ return setFastModelSlot(aiDataDir, request.body);
2256
+ });
2257
+ server2.post("/_web/ai/llm/custom-providers", async (request) => {
2258
+ const aiDataDir = getAiDir(dbManager2);
2259
+ return addCustomProvider(aiDataDir, {
2260
+ ...request.body,
2261
+ kind: request.body.kind || "openai-compatible",
2262
+ authMode: "api-key",
2263
+ supportsCustomModels: request.body.supportsCustomModels ?? true,
2264
+ modelIds: request.body.modelIds ?? []
2265
+ });
2266
+ });
2267
+ server2.put("/_web/ai/llm/custom-providers/:id", async (request) => {
2268
+ const aiDataDir = getAiDir(dbManager2);
2269
+ return updateCustomProvider(aiDataDir, request.params.id, request.body);
2270
+ });
2271
+ server2.delete("/_web/ai/llm/custom-providers/:id", async (request) => {
2272
+ const aiDataDir = getAiDir(dbManager2);
2273
+ return deleteCustomProvider(aiDataDir, request.params.id);
2274
+ });
2275
+ server2.post("/_web/ai/llm/custom-models", async (request) => {
2276
+ const aiDataDir = getAiDir(dbManager2);
2277
+ return addCustomModel(aiDataDir, {
2278
+ ...request.body,
2279
+ capabilities: request.body.capabilities ?? ["chat"],
2280
+ recommendedFor: request.body.recommendedFor ?? ["chat"],
2281
+ status: request.body.status ?? "stable"
2282
+ });
2283
+ });
2284
+ server2.put("/_web/ai/llm/custom-models/:providerId/:modelId", async (request) => {
2285
+ const aiDataDir = getAiDir(dbManager2);
2286
+ return updateCustomModel(aiDataDir, request.params.providerId, request.params.modelId, request.body);
2287
+ });
2288
+ server2.delete("/_web/ai/llm/custom-models/:providerId/:modelId", async (request) => {
2289
+ const aiDataDir = getAiDir(dbManager2);
2290
+ return deleteCustomModel(aiDataDir, request.params.providerId, request.params.modelId);
2291
+ });
2292
+ server2.post("/_web/ai/llm/validate-key", async (request) => {
2293
+ const { provider, apiKey, baseUrl, model, apiFormat } = request.body;
2294
+ return validateApiKey(provider, apiKey, baseUrl, model, apiFormat);
2295
+ });
2296
+ server2.post("/_web/ai/llm/remote-models", async (request) => {
2297
+ const { provider, apiKey, baseUrl, apiFormat } = request.body;
2298
+ return fetchRemoteModels(provider, apiKey, baseUrl, apiFormat);
2299
+ });
2300
+ server2.post("/_web/ai/llm/chat-stream", async (request, reply) => {
2301
+ const { messages, options } = request.body;
2302
+ const aiDataDir = getAiDir(dbManager2);
2303
+ const llmConfig = getDefaultAssistantConfig(aiDataDir);
2304
+ if (!llmConfig) {
2305
+ return reply.code(400).send({ success: false, error: "LLM service not configured" });
2306
+ }
2307
+ const piModel = buildPiModel2(llmConfig);
2308
+ reply.raw.writeHead(200, {
2309
+ "Content-Type": "text/event-stream",
2310
+ "Cache-Control": "no-cache",
2311
+ Connection: "keep-alive"
2312
+ });
2313
+ const sendChunk = (data) => {
2314
+ reply.raw.write(`event: chunk
2315
+ data: ${JSON.stringify(data)}
2316
+
2317
+ `);
2318
+ };
2319
+ try {
2320
+ await runSimpleLlmStream({
2321
+ messages,
2322
+ apiKey: llmConfig.apiKey,
2323
+ piModel,
2324
+ temperature: options?.temperature,
2325
+ maxTokens: options?.maxTokens,
2326
+ onChunk: sendChunk
2327
+ });
2328
+ } catch (error) {
2329
+ const msg = error instanceof Error ? error.message : String(error);
2330
+ sendChunk({ content: "", isFinished: true, finishReason: "error", error: msg });
2331
+ }
2332
+ reply.raw.end();
2333
+ });
2334
+ server2.get("/_web/ai/tools/catalog", async () => {
2335
+ return BUILTIN_TOOL_CATALOG;
2336
+ });
2337
+ if (!convManager2) return;
2338
+ server2.post("/_web/ai/conversations", async (request) => {
2339
+ const { sessionId, title, assistantId } = request.body;
2340
+ return convManager2.createConversation(sessionId, title, assistantId);
2341
+ });
2342
+ server2.get("/_web/ai/conversations", async (request) => {
2343
+ const { sessionId } = request.query;
2344
+ if (!sessionId) return [];
2345
+ return convManager2.getConversations(sessionId);
2346
+ });
2347
+ server2.get("/_web/ai/conversations/:id", async (request, reply) => {
2348
+ const conv = convManager2.getConversation(request.params.id);
2349
+ if (!conv) return reply.code(404).send({ error: "Conversation not found" });
2350
+ return conv;
2351
+ });
2352
+ server2.put("/_web/ai/conversations/:id/title", async (request) => {
2353
+ return convManager2.updateConversationTitle(request.params.id, request.body.title);
2354
+ });
2355
+ server2.delete("/_web/ai/conversations/:id", async (request) => {
2356
+ return convManager2.deleteConversation(request.params.id);
2357
+ });
2358
+ server2.post("/_web/ai/conversations/:id/messages", async (request) => {
2359
+ const { role, content, dataKeywords, dataMessageCount, contentBlocks, tokenUsage } = request.body;
2360
+ return convManager2.addMessage(
2361
+ request.params.id,
2362
+ role,
2363
+ content,
2364
+ dataKeywords,
2365
+ dataMessageCount,
2366
+ contentBlocks,
2367
+ tokenUsage
2368
+ );
2369
+ });
2370
+ server2.get("/_web/ai/conversations/:id/messages", async (request) => {
2371
+ return convManager2.getMessages(request.params.id);
2372
+ });
2373
+ server2.get("/_web/ai/conversations/:id/token-usage", async (request) => {
2374
+ return convManager2.getConversationTokenUsage(request.params.id);
2375
+ });
2376
+ const activeAgentAborts = /* @__PURE__ */ new Map();
2377
+ server2.post("/_web/ai/agent/stream", async (request, reply) => {
2378
+ const {
2379
+ userMessage,
2380
+ conversationId,
2381
+ sessionId,
2382
+ chatType,
2383
+ locale,
2384
+ assistantId,
2385
+ compressionConfig,
2386
+ ownerInfo,
2387
+ mentionedMembers
2388
+ } = request.body;
2389
+ const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
2390
+ const abortController = new AbortController();
2391
+ activeAgentAborts.set(requestId, abortController);
2392
+ reply.raw.writeHead(200, {
2393
+ "Content-Type": "text/event-stream",
2394
+ "Cache-Control": "no-cache",
2395
+ Connection: "keep-alive",
2396
+ "X-Request-Id": requestId
2397
+ });
2398
+ const sendSSE = (event, data) => {
2399
+ reply.raw.write(`event: ${event}
2400
+ data: ${JSON.stringify(data)}
2401
+
2402
+ `);
2403
+ };
2404
+ sendSSE("meta", { requestId });
2405
+ const aiDataDir = getAiDir(dbManager2);
2406
+ let assistantSystemPrompt;
2407
+ if (assistantId) {
2408
+ const assistantConfig = loadAssistantConfig(aiDataDir, assistantId);
2409
+ if (assistantConfig?.systemPrompt) {
2410
+ assistantSystemPrompt = assistantConfig.systemPrompt;
2411
+ }
2412
+ }
2413
+ const llmConfig = getDefaultAssistantConfig(aiDataDir);
2414
+ const maxToolResultPercent = compressionConfig?.maxToolResultPercent ?? 50;
2415
+ const contextWindow = llmConfig ? buildPiModel2(llmConfig).contextWindow ?? 128e3 : 128e3;
2416
+ const maxToolResultTokens = Math.floor(contextWindow * (maxToolResultPercent / 100));
2417
+ const db = dbManager2.open?.(sessionId);
2418
+ const agentTools = db ? adaptToolsForAgent(AGENT_TOOL_REGISTRY, () => ({ db, sessionId, locale }), { maxToolResultTokens }) : [];
2419
+ const skillManager = new SkillManager(aiDataDir);
2420
+ skillManager.init();
2421
+ const toolNames = agentTools.map((t) => t.name);
2422
+ const skillMenu = skillManager.getSkillMenu(chatType ?? "group", toolNames);
2423
+ if (skillMenu) {
2424
+ const activateSkillTool = createActivateSkillTool({
2425
+ chatType: chatType ?? "group",
2426
+ allowedTools: toolNames,
2427
+ locale,
2428
+ getSkillConfig: (id) => skillManager.getSkillConfig(id)
2429
+ });
2430
+ agentTools.push(activateSkillTool);
2431
+ }
2432
+ const onEvent = (event) => {
2433
+ sendSSE(event.type, event);
2434
+ if (event.type === "done") {
2435
+ activeAgentAborts.delete(requestId);
2436
+ reply.raw.end();
2437
+ }
2438
+ };
2439
+ reply.raw.on("close", () => {
2440
+ if (!abortController.signal.aborted) {
2441
+ abortController.abort();
2442
+ }
2443
+ activeAgentAborts.delete(requestId);
2444
+ });
2445
+ try {
2446
+ const resolvedCompression = compressionConfig?.enabled ? {
2447
+ enabled: true,
2448
+ tokenThresholdPercent: compressionConfig.tokenThresholdPercent ?? 75,
2449
+ bufferSizePercent: compressionConfig.bufferSizePercent ?? 20,
2450
+ maxToolResultPercent: compressionConfig.maxToolResultPercent
2451
+ } : void 0;
2452
+ let dataSnapshot;
2453
+ if (db) {
2454
+ try {
2455
+ const overview = getChatOverview(db, 5);
2456
+ if (overview) {
2457
+ dataSnapshot = {
2458
+ name: overview.name,
2459
+ platform: overview.platform,
2460
+ type: overview.type,
2461
+ totalMessages: overview.totalMessages,
2462
+ totalMembers: overview.totalMembers,
2463
+ firstMessageTs: overview.firstMessageTs,
2464
+ lastMessageTs: overview.lastMessageTs,
2465
+ capturedAt: Math.floor(Date.now() / 1e3)
2466
+ };
2467
+ }
2468
+ } catch {
2469
+ }
2470
+ }
2471
+ await runServerAgent({
2472
+ userMessage,
2473
+ conversationId,
2474
+ chatType,
2475
+ locale,
2476
+ assistantSystemPrompt,
2477
+ skillMenu,
2478
+ compressionConfig: resolvedCompression,
2479
+ tools: agentTools,
2480
+ aiDataDir,
2481
+ convManager: convManager2,
2482
+ onEvent,
2483
+ abortSignal: abortController.signal,
2484
+ ownerInfo,
2485
+ mentionedMembers,
2486
+ dataSnapshot
2487
+ });
2488
+ } catch (error) {
2489
+ const msg = error instanceof Error ? error.message : String(error);
2490
+ sendSSE("error", { type: "error", error: { name: "ServerError", message: msg } });
2491
+ sendSSE("done", { type: "done", isFinished: true });
2492
+ activeAgentAborts.delete(requestId);
2493
+ reply.raw.end();
2494
+ }
2495
+ });
2496
+ server2.post("/_web/ai/agent/abort", async (request) => {
2497
+ const { requestId } = request.body;
2498
+ const controller = activeAgentAborts.get(requestId);
2499
+ if (controller) {
2500
+ controller.abort();
2501
+ activeAgentAborts.delete(requestId);
2502
+ return { success: true };
2503
+ }
2504
+ return { success: false };
2505
+ });
2506
+ server2.get("/_web/ai/debug/schema", async (_request, reply) => {
2507
+ if (!convManager2) {
2508
+ return reply.code(503).send({ error: "AI conversation manager not available" });
2509
+ }
2510
+ return convManager2.getAiSchema();
2511
+ });
2512
+ server2.post("/_web/ai/debug/execute-sql", async (request, reply) => {
2513
+ if (!convManager2) {
2514
+ return reply.code(503).send({ error: "AI conversation manager not available" });
2515
+ }
2516
+ const { sql } = request.body;
2517
+ if (!sql || typeof sql !== "string") {
2518
+ return reply.code(400).send({ error: "sql is required" });
2519
+ }
2520
+ try {
2521
+ return convManager2.executeAiSQL(sql);
2522
+ } catch (e) {
2523
+ const msg = e instanceof Error ? e.message : String(e);
2524
+ return reply.code(400).send({ error: msg });
2525
+ }
2526
+ });
2527
+ server2.post("/_web/ai/debug/clear-debug-context", async (_request, reply) => {
2528
+ if (!convManager2) {
2529
+ return reply.code(503).send({ error: "AI conversation manager not available" });
2530
+ }
2531
+ const cleared = convManager2.clearAllDebugContext();
2532
+ return { success: true, cleared };
2533
+ });
2534
+ }
2535
+
2536
+ // src/http/routes/preferences.ts
2537
+ var prefManager = null;
2538
+ function registerPreferencesRoutes(server2, pathProvider) {
2539
+ if (!prefManager) {
2540
+ prefManager = new PreferencesManager(pathProvider.getSystemDir());
2541
+ }
2542
+ server2.get("/_web/preferences", async () => {
2543
+ return prefManager.load();
2544
+ });
2545
+ server2.patch("/_web/preferences", async (request) => {
2546
+ return prefManager.save(request.body);
2547
+ });
2548
+ server2.get("/_web/preferences/ui-config", async () => {
2549
+ const config = loadConfig();
2550
+ return config.ui;
2551
+ });
2552
+ server2.patch("/_web/preferences/ui-config", async (request) => {
2553
+ try {
2554
+ for (const [key, value] of Object.entries(request.body)) {
2555
+ if (value !== void 0) {
2556
+ writeConfigField("ui", key, value);
2557
+ }
2558
+ }
2559
+ return { success: true };
2560
+ } catch (err) {
2561
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
2562
+ }
2563
+ });
2564
+ server2.get("/_web/preferences/locale", async () => {
2565
+ const config = loadConfig();
2566
+ return { lang: config.locale.lang };
2567
+ });
2568
+ server2.patch("/_web/preferences/locale", async (request) => {
2569
+ try {
2570
+ writeConfigField("locale", "lang", request.body.lang);
2571
+ return { success: true };
2572
+ } catch (err) {
2573
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
2574
+ }
2575
+ });
2576
+ }
2577
+
2578
+ // ../../packages/sync/src/types.ts
2579
+ var noop = () => {
2580
+ };
2581
+ var NOOP_LOGGER = { info: noop, warn: noop, error: noop };
2582
+
2583
+ // ../../packages/sync/src/data-source-manager.ts
2584
+ import * as fs11 from "fs";
2585
+ import * as path13 from "path";
2586
+ import * as crypto2 from "crypto";
2587
+ var CONFIG_FILE = "data-sources.json";
2588
+ function generateId(prefix = "ds") {
2589
+ return `${prefix}_${crypto2.randomBytes(6).toString("hex")}`;
2590
+ }
2591
+ function normalizeBaseUrl(input) {
2592
+ let url = input.trim().replace(/\/+$/, "");
2593
+ if (url && !/^https?:\/\//i.test(url)) {
2594
+ url = `http://${url}`;
2595
+ }
2596
+ if (url && !url.endsWith("/api/v1")) {
2597
+ url = url.replace(/\/api\/v1$/, "") + "/api/v1";
2598
+ }
2599
+ return url;
2600
+ }
2601
+ function isValidDataSourceArray(data) {
2602
+ if (!Array.isArray(data)) return false;
2603
+ return data.every((item) => item && typeof item === "object" && Array.isArray(item.sessions));
2604
+ }
2605
+ var DataSourceManager = class {
2606
+ configDir;
2607
+ logger;
2608
+ constructor(configDir, logger2) {
2609
+ this.configDir = configDir;
2610
+ this.logger = logger2 ?? NOOP_LOGGER;
2611
+ }
2612
+ getConfigPath() {
2613
+ return path13.join(this.configDir, CONFIG_FILE);
2614
+ }
2615
+ ensureDir() {
2616
+ fs11.mkdirSync(this.configDir, { recursive: true });
2617
+ }
2618
+ // ==================== Load / Save ====================
2619
+ loadAll() {
2620
+ try {
2621
+ const filePath = this.getConfigPath();
2622
+ if (fs11.existsSync(filePath)) {
2623
+ const raw = fs11.readFileSync(filePath, "utf-8");
2624
+ const parsed = JSON.parse(raw);
2625
+ if (!isValidDataSourceArray(parsed)) {
2626
+ this.logger.warn("[DataSource] Incompatible config format detected, returning empty.");
2627
+ return [];
2628
+ }
2629
+ for (const ds of parsed) {
2630
+ if (!ds.pullLimit) ds.pullLimit = 1e3;
2631
+ }
2632
+ return parsed;
2633
+ }
2634
+ } catch (err) {
2635
+ this.logger.error("[DataSource] Failed to load config", err);
2636
+ }
2637
+ return [];
2638
+ }
2639
+ saveAll(sources) {
2640
+ try {
2641
+ this.ensureDir();
2642
+ fs11.writeFileSync(this.getConfigPath(), JSON.stringify(sources, null, 2), "utf-8");
2643
+ } catch (err) {
2644
+ this.logger.error("[DataSource] Failed to save config", err);
2645
+ }
2646
+ }
2647
+ // ==================== DataSource CRUD ====================
2648
+ get(id) {
2649
+ return this.loadAll().find((s) => s.id === id) || null;
2650
+ }
2651
+ add(partial) {
2652
+ const sources = this.loadAll();
2653
+ const ds = {
2654
+ id: generateId("src"),
2655
+ name: partial.name || "",
2656
+ baseUrl: normalizeBaseUrl(partial.baseUrl),
2657
+ token: partial.token,
2658
+ intervalMinutes: partial.intervalMinutes,
2659
+ pullLimit: partial.pullLimit || 1e3,
2660
+ enabled: true,
2661
+ createdAt: Math.floor(Date.now() / 1e3),
2662
+ sessions: []
2663
+ };
2664
+ sources.push(ds);
2665
+ this.saveAll(sources);
2666
+ return ds;
2667
+ }
2668
+ update(id, updates) {
2669
+ const sources = this.loadAll();
2670
+ const idx = sources.findIndex((s) => s.id === id);
2671
+ if (idx === -1) return null;
2672
+ const ds = sources[idx];
2673
+ if (updates.name !== void 0) ds.name = updates.name;
2674
+ if (updates.baseUrl !== void 0) ds.baseUrl = normalizeBaseUrl(updates.baseUrl);
2675
+ if (updates.token !== void 0) ds.token = updates.token;
2676
+ if (updates.intervalMinutes !== void 0) ds.intervalMinutes = updates.intervalMinutes;
2677
+ if (updates.pullLimit !== void 0) ds.pullLimit = updates.pullLimit;
2678
+ if (updates.enabled !== void 0) ds.enabled = updates.enabled;
2679
+ this.saveAll(sources);
2680
+ return ds;
2681
+ }
2682
+ delete(id) {
2683
+ const sources = this.loadAll();
2684
+ const filtered = sources.filter((s) => s.id !== id);
2685
+ if (filtered.length === sources.length) return false;
2686
+ this.saveAll(filtered);
2687
+ return true;
2688
+ }
2689
+ // ==================== ImportSession CRUD ====================
2690
+ addSessions(sourceId, sessions) {
2691
+ const sources = this.loadAll();
2692
+ const ds = sources.find((s) => s.id === sourceId);
2693
+ if (!ds) return [];
2694
+ const added = [];
2695
+ for (const sess of sessions) {
2696
+ if (ds.sessions.some((s) => s.remoteSessionId === sess.remoteSessionId)) continue;
2697
+ const imp = {
2698
+ id: generateId("sess"),
2699
+ name: sess.name,
2700
+ remoteSessionId: sess.remoteSessionId,
2701
+ targetSessionId: "",
2702
+ lastPullAt: 0,
2703
+ lastStatus: "idle",
2704
+ lastError: "",
2705
+ lastNewMessages: 0
2706
+ };
2707
+ ds.sessions.push(imp);
2708
+ added.push(imp);
2709
+ }
2710
+ this.saveAll(sources);
2711
+ return added;
2712
+ }
2713
+ removeSession(sourceId, sessionId) {
2714
+ const sources = this.loadAll();
2715
+ const ds = sources.find((s) => s.id === sourceId);
2716
+ if (!ds) return false;
2717
+ const before = ds.sessions.length;
2718
+ ds.sessions = ds.sessions.filter((s) => s.id !== sessionId);
2719
+ if (ds.sessions.length === before) return false;
2720
+ this.saveAll(sources);
2721
+ return true;
2722
+ }
2723
+ updateSession(sourceId, sessionId, updates) {
2724
+ const sources = this.loadAll();
2725
+ const ds = sources.find((s) => s.id === sourceId);
2726
+ if (!ds) return null;
2727
+ const sess = ds.sessions.find((s) => s.id === sessionId);
2728
+ if (!sess) return null;
2729
+ Object.assign(sess, updates, { id: sessionId });
2730
+ this.saveAll(sources);
2731
+ return sess;
2732
+ }
2733
+ };
2734
+
2735
+ // ../../packages/sync/src/discovery.ts
2736
+ function buildRemoteSessionsUrl(baseUrl, query = {}) {
2737
+ const searchParams = new URLSearchParams();
2738
+ searchParams.set("format", "chatlab");
2739
+ if (query.keyword?.trim()) searchParams.set("keyword", query.keyword.trim());
2740
+ if (query.limit && query.limit > 0) searchParams.set("limit", String(query.limit));
2741
+ if (query.cursor) searchParams.set("cursor", query.cursor);
2742
+ return `${baseUrl}/sessions?${searchParams.toString()}`;
2743
+ }
2744
+ function parseRemoteSessionsResponse(body) {
2745
+ const parsed = JSON.parse(body);
2746
+ let sessions;
2747
+ let pageSource;
2748
+ if (Array.isArray(parsed)) {
2749
+ sessions = parsed;
2750
+ } else if (parsed && typeof parsed === "object") {
2751
+ sessions = parsed.sessions ?? parsed.data?.sessions ?? parsed.data ?? [];
2752
+ if (!Array.isArray(sessions)) sessions = [];
2753
+ pageSource = parsed.page ?? parsed.data?.page;
2754
+ } else {
2755
+ sessions = [];
2756
+ }
2757
+ return {
2758
+ sessions,
2759
+ page: pageSource && typeof pageSource === "object" ? {
2760
+ hasMore: Boolean(pageSource.hasMore),
2761
+ nextCursor: typeof pageSource.nextCursor === "string" ? pageSource.nextCursor : void 0
2762
+ } : void 0
2763
+ };
2764
+ }
2765
+
2766
+ // ../../packages/sync/src/pull-engine.ts
2767
+ import * as fs12 from "fs";
2768
+ import * as crypto3 from "crypto";
2769
+ var MAX_PAGES_PER_PULL = 50;
2770
+ var PULL_OVERLAP_SECONDS = 60;
2771
+ function buildPullUrl(baseUrl, remoteSessionId, params) {
2772
+ const base = `${baseUrl}/sessions/${remoteSessionId}/messages`;
2773
+ const qs = ["format=chatlab"];
2774
+ if (params.since !== void 0 && params.since > 0) qs.push(`since=${params.since}`);
2775
+ if (params.offset !== void 0 && params.offset > 0) qs.push(`offset=${params.offset}`);
2776
+ if (params.end !== void 0 && params.end > 0) qs.push(`end=${params.end}`);
2777
+ if (params.limit !== void 0 && params.limit > 0) qs.push(`limit=${params.limit}`);
2778
+ return base + "?" + qs.join("&");
2779
+ }
2780
+ function deriveLocalSessionId(baseUrl, remoteSessionId) {
2781
+ const hash = crypto3.createHash("sha256").update(`${baseUrl}\0${remoteSessionId}`).digest("hex").slice(0, 12);
2782
+ return `remote_${hash}`;
2783
+ }
2784
+ function parseSyncFromFile(filePath) {
2785
+ try {
2786
+ const isJsonl = filePath.endsWith(".jsonl");
2787
+ if (isJsonl) {
2788
+ const content = fs12.readFileSync(filePath, "utf-8");
2789
+ const lines = content.trimEnd().split("\n");
2790
+ for (let i = lines.length - 1; i >= Math.max(0, lines.length - 5); i--) {
2791
+ try {
2792
+ const obj = JSON.parse(lines[i]);
2793
+ if (obj._type === "sync") {
2794
+ return {
2795
+ hasMore: !!obj.hasMore,
2796
+ nextSince: obj.nextSince,
2797
+ nextOffset: obj.nextOffset,
2798
+ watermark: obj.watermark
2799
+ };
2800
+ }
2801
+ } catch {
2802
+ continue;
2803
+ }
2804
+ }
2805
+ return null;
2806
+ }
2807
+ const raw = fs12.readFileSync(filePath, "utf-8");
2808
+ const parsed = JSON.parse(raw);
2809
+ if (parsed.sync && typeof parsed.sync === "object") {
2810
+ const s = parsed.sync;
2811
+ return { hasMore: !!s.hasMore, nextSince: s.nextSince, nextOffset: s.nextOffset, watermark: s.watermark };
2812
+ }
2813
+ return null;
2814
+ } catch {
2815
+ return null;
2816
+ }
2817
+ }
2818
+ function cleanupTempFile(filePath) {
2819
+ try {
2820
+ if (fs12.existsSync(filePath)) fs12.unlinkSync(filePath);
2821
+ } catch {
2822
+ }
2823
+ }
2824
+ var PullEngine = class {
2825
+ fetcher;
2826
+ importer;
2827
+ notifier;
2828
+ dsManager;
2829
+ logger;
2830
+ isImporting;
2831
+ pullingSourceIds = /* @__PURE__ */ new Set();
2832
+ constructor(options) {
2833
+ this.fetcher = options.fetcher;
2834
+ this.importer = options.importer;
2835
+ this.notifier = options.notifier;
2836
+ this.dsManager = options.dsManager;
2837
+ this.logger = options.logger ?? NOOP_LOGGER;
2838
+ this.isImporting = options.isImporting ?? (() => false);
2839
+ }
2840
+ async importTempFile(baseUrl, sess, tempFile) {
2841
+ let targetId = sess.targetSessionId;
2842
+ if (!targetId) {
2843
+ const derived = deriveLocalSessionId(baseUrl, sess.remoteSessionId);
2844
+ if (this.importer.sessionExists(derived)) {
2845
+ targetId = derived;
2846
+ this.logger.info(`[Pull] Reusing existing local session ${derived} for "${sess.name}"`);
2847
+ }
2848
+ }
2849
+ const externalId = deriveLocalSessionId(baseUrl, sess.remoteSessionId);
2850
+ return this.importer.importFile(tempFile, targetId || void 0, externalId);
2851
+ }
2852
+ async executePullSession(sourceId, ds, sess) {
2853
+ if (this.isImporting()) {
2854
+ this.logger.info(`[Pull] Skipping "${sess.name}": import in progress`);
2855
+ return { success: false, newMessageCount: 0, error: "Import in progress" };
2856
+ }
2857
+ this.logger.info(`[Pull] Pulling "${sess.name}" from ${ds.baseUrl}`);
2858
+ let totalNewMessages = 0;
2859
+ let since = sess.lastPullAt;
2860
+ let offset = 0;
2861
+ let end;
2862
+ let pageCount = 0;
2863
+ let resyncAttempted = false;
2864
+ try {
2865
+ while (pageCount < MAX_PAGES_PER_PULL) {
2866
+ pageCount++;
2867
+ const tempFile = await this.fetcher.fetchToTempFile(ds.baseUrl, sess.remoteSessionId, ds.token, {
2868
+ since,
2869
+ offset,
2870
+ end,
2871
+ limit: ds.pullLimit
2872
+ });
2873
+ try {
2874
+ const stat = fs12.statSync(tempFile);
2875
+ this.logger.info(`[Pull] "${sess.name}" page ${pageCount}: fetched ${stat.size} bytes`);
2876
+ if (stat.size === 0) {
2877
+ cleanupTempFile(tempFile);
2878
+ break;
2879
+ }
2880
+ const sync = parseSyncFromFile(tempFile);
2881
+ const result = await this.importTempFile(ds.baseUrl, sess, tempFile);
2882
+ cleanupTempFile(tempFile);
2883
+ if (result.needFullResync && !resyncAttempted) {
2884
+ resyncAttempted = true;
2885
+ this.logger.info(`[Pull] Resetting since=0 for "${sess.name}" full resync`);
2886
+ since = 0;
2887
+ offset = 0;
2888
+ pageCount = 0;
2889
+ sess.targetSessionId = "";
2890
+ sess.lastPullAt = 0;
2891
+ this.dsManager.updateSession(sourceId, sess.id, { targetSessionId: "", lastPullAt: 0 });
2892
+ continue;
2893
+ }
2894
+ if (result.needFullResync) {
2895
+ const errMsg = "Full resync failed";
2896
+ this.logger.error(`[Pull] Full resync already attempted for "${sess.name}", aborting`);
2897
+ this.dsManager.updateSession(sourceId, sess.id, {
2898
+ lastPullAt: Math.floor(Date.now() / 1e3),
2899
+ lastStatus: "error",
2900
+ lastError: errMsg
2901
+ });
2902
+ this.notifier.onPullResult(sourceId, sess.id, "error", errMsg);
2903
+ return { success: false, newMessageCount: 0, error: errMsg };
2904
+ }
2905
+ if (!result.success) {
2906
+ const errMsg = result.error || "Import failed";
2907
+ this.dsManager.updateSession(sourceId, sess.id, {
2908
+ lastPullAt: Math.floor(Date.now() / 1e3),
2909
+ lastStatus: "error",
2910
+ lastError: errMsg
2911
+ });
2912
+ this.notifier.onPullResult(sourceId, sess.id, "error", errMsg);
2913
+ return { success: false, newMessageCount: 0, error: errMsg };
2914
+ }
2915
+ if (!sess.targetSessionId && result.sessionId) {
2916
+ sess.targetSessionId = result.sessionId;
2917
+ this.dsManager.updateSession(sourceId, sess.id, { targetSessionId: result.sessionId });
2918
+ }
2919
+ totalNewMessages += result.newMessageCount;
2920
+ if (!sync || !sync.hasMore) break;
2921
+ if (sync.nextSince !== void 0) since = sync.nextSince;
2922
+ if (sync.nextOffset !== void 0) offset = sync.nextOffset;
2923
+ else offset = 0;
2924
+ if (sync.watermark !== void 0 && !end) end = sync.watermark;
2925
+ } catch (importErr) {
2926
+ cleanupTempFile(tempFile);
2927
+ throw importErr;
2928
+ }
2929
+ }
2930
+ if (pageCount >= MAX_PAGES_PER_PULL) {
2931
+ this.logger.warn(`[Pull] "${sess.name}" reached page limit (${MAX_PAGES_PER_PULL}), data may be incomplete`);
2932
+ }
2933
+ this.dsManager.updateSession(sourceId, sess.id, {
2934
+ lastPullAt: Math.floor(Date.now() / 1e3) - PULL_OVERLAP_SECONDS,
2935
+ lastStatus: "success",
2936
+ lastNewMessages: totalNewMessages,
2937
+ lastError: ""
2938
+ });
2939
+ if (totalNewMessages > 0) this.notifier.onSessionListChanged();
2940
+ this.notifier.onPullResult(sourceId, sess.id, "success", `+${totalNewMessages} messages`);
2941
+ return { success: true, newMessageCount: totalNewMessages };
2942
+ } catch (error) {
2943
+ const errMsg = error.message || "Pull failed";
2944
+ this.logger.error(`[Pull] Pull failed for "${sess.name}"`, error);
2945
+ this.dsManager.updateSession(sourceId, sess.id, {
2946
+ lastPullAt: Math.floor(Date.now() / 1e3),
2947
+ lastStatus: "error",
2948
+ lastError: errMsg
2949
+ });
2950
+ this.notifier.onPullResult(sourceId, sess.id, "error", errMsg);
2951
+ return { success: false, newMessageCount: 0, error: errMsg };
2952
+ }
2953
+ }
2954
+ async pullAllSessions(ds) {
2955
+ if (this.pullingSourceIds.has(ds.id)) {
2956
+ this.logger.info(`[Pull] Skipping pullAllSessions for "${ds.baseUrl}": pull already in progress`);
2957
+ return;
2958
+ }
2959
+ this.pullingSourceIds.add(ds.id);
2960
+ try {
2961
+ for (const sess of ds.sessions) {
2962
+ await this.executePullSession(ds.id, ds, sess);
2963
+ }
2964
+ } finally {
2965
+ this.pullingSourceIds.delete(ds.id);
2966
+ }
2967
+ }
2968
+ async triggerPull(sourceId, sessionId) {
2969
+ const ds = this.dsManager.get(sourceId);
2970
+ if (!ds) return { success: false, error: "Data source not found" };
2971
+ if (sessionId) {
2972
+ const sess = ds.sessions.find((s) => s.id === sessionId);
2973
+ if (!sess) return { success: false, error: "Session not found" };
2974
+ const result = await this.executePullSession(sourceId, ds, sess);
2975
+ return { success: result.success, error: result.error };
2976
+ }
2977
+ if (this.pullingSourceIds.has(sourceId)) {
2978
+ this.logger.info(`[Pull] Skipping triggerPull for source ${sourceId}: pull already in progress`);
2979
+ return { success: true };
2980
+ }
2981
+ this.pullingSourceIds.add(sourceId);
2982
+ try {
2983
+ const errors = [];
2984
+ for (const sess of ds.sessions) {
2985
+ const result = await this.executePullSession(sourceId, ds, sess);
2986
+ if (!result.success && result.error) errors.push(`${sess.name}: ${result.error}`);
2987
+ }
2988
+ if (errors.length > 0) {
2989
+ return { success: false, error: errors.join("; ") };
2990
+ }
2991
+ return { success: true };
2992
+ } finally {
2993
+ this.pullingSourceIds.delete(sourceId);
2994
+ }
2995
+ }
2996
+ async triggerPullAll(sourceId) {
2997
+ return this.triggerPull(sourceId);
2998
+ }
2999
+ };
3000
+
3001
+ // ../../packages/sync/src/scheduler.ts
3002
+ var timers = /* @__PURE__ */ new Map();
3003
+ var initialized = false;
3004
+ var _dsManager;
3005
+ var _pullEngine;
3006
+ var _logger;
3007
+ function startTimer(ds, skipInitialPull = false) {
3008
+ stopTimer(ds.id);
3009
+ if (!ds.enabled || ds.intervalMinutes < 1 || ds.sessions.length === 0) return;
3010
+ const intervalMs = ds.intervalMinutes * 60 * 1e3;
3011
+ if (!skipInitialPull) {
3012
+ _pullEngine.pullAllSessions(ds).catch((err) => {
3013
+ _logger.error("[Pull] Initial pull failed", err);
3014
+ });
3015
+ }
3016
+ const timer = setInterval(() => {
3017
+ const current = _dsManager.loadAll().find((s) => s.id === ds.id);
3018
+ if (!current || !current.enabled || current.sessions.length === 0) {
3019
+ stopTimer(ds.id);
3020
+ return;
3021
+ }
3022
+ _pullEngine.pullAllSessions(current).catch((err) => {
3023
+ _logger.error("[Pull] Scheduled pull failed", err);
3024
+ });
3025
+ }, intervalMs);
3026
+ timers.set(ds.id, timer);
3027
+ _logger.info(
3028
+ `[Pull] Timer started for source ${ds.baseUrl} (${ds.sessions.length} sessions, every ${ds.intervalMinutes}min)`
3029
+ );
3030
+ }
3031
+ function stopTimer(id) {
3032
+ const timer = timers.get(id);
3033
+ if (timer) {
3034
+ clearInterval(timer);
3035
+ timers.delete(id);
3036
+ }
3037
+ }
3038
+ function initScheduler(options) {
3039
+ if (initialized) return;
3040
+ initialized = true;
3041
+ _dsManager = options.dsManager;
3042
+ _pullEngine = options.pullEngine;
3043
+ _logger = options.logger ?? NOOP_LOGGER;
3044
+ const sources = _dsManager.loadAll();
3045
+ for (const ds of sources) {
3046
+ if (ds.enabled && ds.sessions.length > 0) {
3047
+ startTimer(ds);
3048
+ }
3049
+ }
3050
+ _logger.info(`[Pull] Initialized with ${sources.filter((s) => s.enabled).length} active sources`);
3051
+ }
3052
+ function stopAllTimers() {
3053
+ for (const [id] of timers) {
3054
+ stopTimer(id);
3055
+ }
3056
+ initialized = false;
3057
+ _logger?.info("[Pull] All timers stopped");
3058
+ }
3059
+ function reloadTimer(dsId, skipInitialPull = false) {
3060
+ stopTimer(dsId);
3061
+ const ds = _dsManager?.loadAll().find((s) => s.id === dsId);
3062
+ if (ds && ds.enabled) {
3063
+ startTimer(ds, skipInitialPull);
3064
+ }
3065
+ }
3066
+
3067
+ // src/sync/adapters.ts
3068
+ import * as fs13 from "fs";
3069
+ import * as os4 from "os";
3070
+ import * as path14 from "path";
3071
+ import * as crypto4 from "crypto";
3072
+ function getTempFilePath(ext) {
3073
+ const id = crypto4.randomBytes(8).toString("hex");
3074
+ return path14.join(os4.tmpdir(), `chatlab-pull-${id}${ext}`);
3075
+ }
3076
+ var NodeFetcher = class {
3077
+ async fetchToTempFile(baseUrl, remoteSessionId, token, params) {
3078
+ const url = buildPullUrl(baseUrl, remoteSessionId, params);
3079
+ const headers = {
3080
+ Accept: "application/json, application/x-ndjson"
3081
+ };
3082
+ if (token) headers["Authorization"] = `Bearer ${token}`;
3083
+ const response = await fetch(url, { headers, signal: AbortSignal.timeout(12e4) });
3084
+ if (!response.ok) {
3085
+ throw new Error(`HTTP ${response.status}`);
3086
+ }
3087
+ const contentType = response.headers.get("content-type") || "application/json";
3088
+ const isJsonl = contentType.includes("ndjson") || contentType.includes("jsonl");
3089
+ const tempFile = getTempFilePath(isJsonl ? ".jsonl" : ".json");
3090
+ const buffer = Buffer.from(await response.arrayBuffer());
3091
+ fs13.writeFileSync(tempFile, buffer);
3092
+ return tempFile;
3093
+ }
3094
+ };
3095
+ function resolveNativeBinding2() {
3096
+ if (process.versions.electron) return void 0;
3097
+ const nativePath = path14.resolve(__dirname, "../../native/better_sqlite3.node");
3098
+ if (fs13.existsSync(nativePath)) return nativePath;
3099
+ return void 0;
3100
+ }
3101
+ var DirectImporter = class {
3102
+ dbManager;
3103
+ nativeBinding;
3104
+ logger;
3105
+ constructor(dbManager2, logger2) {
3106
+ this.dbManager = dbManager2;
3107
+ this.nativeBinding = resolveNativeBinding2();
3108
+ this.logger = logger2 ?? NOOP_LOGGER;
3109
+ }
3110
+ sessionExists(sessionId) {
3111
+ const dbPath = this.dbManager.getDbPath(sessionId);
3112
+ return fs13.existsSync(dbPath);
3113
+ }
3114
+ async importFile(tempFile, targetSessionId, externalId) {
3115
+ if (targetSessionId && this.sessionExists(targetSessionId)) {
3116
+ return this.incrementalImportFile(targetSessionId, tempFile);
3117
+ }
3118
+ if (targetSessionId) {
3119
+ this.logger.info(`[DirectImporter] Session ${targetSessionId} not found locally, need full resync`);
3120
+ return { success: false, newMessageCount: 0, needFullResync: true };
3121
+ }
3122
+ return this.fullImportFile(tempFile, externalId);
3123
+ }
3124
+ async incrementalImportFile(sessionId, tempFile) {
3125
+ try {
3126
+ const data = await parseFile(tempFile);
3127
+ this.dbManager.close(sessionId);
3128
+ const result = await importData(this.dbManager, data, {
3129
+ sessionId,
3130
+ nativeBinding: this.nativeBinding
3131
+ });
3132
+ if (result.success) {
3133
+ this.logger.info(
3134
+ `[DirectImporter] Incremental OK: +${result.messageCount} messages (${result.duplicateCount} duplicates skipped)`
3135
+ );
3136
+ return {
3137
+ success: true,
3138
+ newMessageCount: result.messageCount,
3139
+ sessionId
3140
+ };
3141
+ }
3142
+ if (result.error?.includes("not found") || result.error?.includes("session_not_found")) {
3143
+ return { success: false, newMessageCount: 0, sessionId, needFullResync: true };
3144
+ }
3145
+ return { success: false, newMessageCount: 0, sessionId, error: result.error };
3146
+ } catch (err) {
3147
+ this.logger.error(`[DirectImporter] Incremental import failed`, err);
3148
+ return { success: false, newMessageCount: 0, sessionId, error: err.message };
3149
+ }
3150
+ }
3151
+ async fullImportFile(tempFile, externalId) {
3152
+ try {
3153
+ const data = await parseFile(tempFile);
3154
+ const result = await importData(this.dbManager, data, {
3155
+ sessionId: externalId,
3156
+ nativeBinding: this.nativeBinding
3157
+ });
3158
+ if (result.success) {
3159
+ this.logger.info(
3160
+ `[DirectImporter] Full import OK: ${result.messageCount} messages, ${result.memberCount} members`
3161
+ );
3162
+ return { success: true, newMessageCount: result.messageCount, sessionId: externalId };
3163
+ }
3164
+ return { success: false, newMessageCount: 0, error: result.error };
3165
+ } catch (err) {
3166
+ this.logger.error(`[DirectImporter] Full import failed`, err);
3167
+ return { success: false, newMessageCount: 0, error: err.message };
3168
+ }
3169
+ }
3170
+ };
3171
+ var noop2 = () => {
3172
+ };
3173
+ var NoopNotifier = class {
3174
+ onSessionListChanged = noop2;
3175
+ onPullResult = noop2;
3176
+ };
3177
+
3178
+ // src/sync/routes.ts
3179
+ function registerAutomationRoutes(server2, ctx) {
3180
+ const { dsManager: dsManager2, pullEngine: pullEngine2, serverInfo } = ctx;
3181
+ server2.get("/_web/automation/config", async () => {
3182
+ return {
3183
+ enabled: true,
3184
+ port: serverInfo.port,
3185
+ token: serverInfo.token,
3186
+ host: serverInfo.host
3187
+ };
3188
+ });
3189
+ server2.get("/_web/automation/data-sources", async () => {
3190
+ return dsManager2.loadAll();
3191
+ });
3192
+ server2.post("/_web/automation/data-sources", async (request) => {
3193
+ const ds = dsManager2.add(request.body);
3194
+ reloadTimer(ds.id);
3195
+ return ds;
3196
+ });
3197
+ server2.patch("/_web/automation/data-sources/:id", async (request, reply) => {
3198
+ const ds = dsManager2.update(request.params.id, request.body);
3199
+ if (!ds) return reply.code(404).send({ error: "Data source not found" });
3200
+ reloadTimer(ds.id);
3201
+ return ds;
3202
+ });
3203
+ server2.delete("/_web/automation/data-sources/:id", async (request, reply) => {
3204
+ stopTimer(request.params.id);
3205
+ const ok = dsManager2.delete(request.params.id);
3206
+ if (!ok) return reply.code(404).send({ error: "Data source not found" });
3207
+ return { success: true };
3208
+ });
3209
+ server2.post("/_web/automation/data-sources/:id/sessions", async (request, reply) => {
3210
+ const added = dsManager2.addSessions(request.params.id, request.body.sessions);
3211
+ if (added.length === 0 && !dsManager2.get(request.params.id)) {
3212
+ return reply.code(404).send({ error: "Data source not found" });
3213
+ }
3214
+ reloadTimer(request.params.id, true);
3215
+ for (const sess of added) {
3216
+ pullEngine2.triggerPull(request.params.id, sess.id).catch(() => {
3217
+ });
3218
+ }
3219
+ return added;
3220
+ });
3221
+ server2.delete("/_web/automation/data-sources/:id/sessions/:sessId", async (request, reply) => {
3222
+ const ok = dsManager2.removeSession(request.params.id, request.params.sessId);
3223
+ if (!ok) return reply.code(404).send({ error: "Session not found" });
3224
+ reloadTimer(request.params.id);
3225
+ return { success: true };
3226
+ });
3227
+ server2.post(
3228
+ "/_web/automation/data-sources/:id/pull",
3229
+ async (request) => {
3230
+ return pullEngine2.triggerPull(request.params.id, request.body?.sessionId);
3231
+ }
3232
+ );
3233
+ server2.post("/_web/automation/data-sources/:id/pull-all", async (request) => {
3234
+ return pullEngine2.triggerPullAll(request.params.id);
3235
+ });
3236
+ server2.get("/_web/automation/remote-sessions", async (request, reply) => {
3237
+ const { baseUrl, token, keyword, limit, cursor } = request.query;
3238
+ if (!baseUrl) return reply.code(400).send({ error: "baseUrl is required" });
3239
+ const normalizedUrl = normalizeBaseUrl(baseUrl);
3240
+ const url = buildRemoteSessionsUrl(normalizedUrl, {
3241
+ keyword,
3242
+ limit: limit ? parseInt(limit, 10) : void 0,
3243
+ cursor
3244
+ });
3245
+ const headers = { Accept: "application/json" };
3246
+ if (token) headers["Authorization"] = `Bearer ${token}`;
3247
+ try {
3248
+ const resp = await fetch(url, { headers, signal: AbortSignal.timeout(3e4) });
3249
+ if (!resp.ok) throw new Error(`Remote server returned HTTP ${resp.status}`);
3250
+ const body = await resp.text();
3251
+ return parseRemoteSessionsResponse(body);
3252
+ } catch (err) {
3253
+ return reply.code(502).send({ error: err.message || "Failed to fetch remote sessions" });
3254
+ }
3255
+ });
3256
+ }
3257
+
3258
+ // src/sync/index.ts
3259
+ var syncLogger = {
3260
+ info: (msg) => console.log(`[Sync] ${msg}`),
3261
+ warn: (msg) => console.warn(`[Sync] ${msg}`),
3262
+ error: (msg, err) => console.error(`[Sync] ${msg}`, err ?? "")
3263
+ };
3264
+ var dsManager = null;
3265
+ var pullEngine = null;
3266
+ function initSync(server2, dbManager2, pathProvider, serverInfo) {
3267
+ const settingsDir = pathProvider.getSettingsDir();
3268
+ dsManager = new DataSourceManager(settingsDir, syncLogger);
3269
+ const fetcher = new NodeFetcher();
3270
+ const importer = new DirectImporter(dbManager2, syncLogger);
3271
+ const notifier = new NoopNotifier();
3272
+ pullEngine = new PullEngine({
3273
+ fetcher,
3274
+ importer,
3275
+ notifier,
3276
+ dsManager,
3277
+ logger: syncLogger
3278
+ });
3279
+ registerAutomationRoutes(server2, { dsManager, pullEngine, serverInfo });
3280
+ initScheduler({
3281
+ dsManager,
3282
+ pullEngine,
3283
+ logger: syncLogger
3284
+ });
3285
+ }
3286
+ function cleanupSync() {
3287
+ stopAllTimers();
3288
+ dsManager = null;
3289
+ pullEngine = null;
3290
+ }
3291
+
3292
+ // src/http/index.ts
3293
+ var server = null;
3294
+ var dbManager = null;
3295
+ var convManager = null;
3296
+ function resolveNativeBinding3() {
3297
+ if (process.versions.electron) return void 0;
3298
+ const nativePath = path15.resolve(__dirname, "../../native/better_sqlite3.node");
3299
+ if (fs14.existsSync(nativePath)) return nativePath;
3300
+ return void 0;
3301
+ }
3302
+ function ensureToken(config) {
3303
+ if (config.api.token) return config.api.token;
3304
+ const token = `clb_${crypto5.randomBytes(32).toString("hex")}`;
3305
+ try {
3306
+ writeConfigField("api", "token", token);
3307
+ } catch {
3308
+ }
3309
+ return token;
3310
+ }
3311
+ async function startHttpServer(options) {
3312
+ if (server) {
3313
+ throw new Error("HTTP server is already running");
3314
+ }
3315
+ const config = loadConfig();
3316
+ const port = options?.port ?? config.api.port;
3317
+ const host = options?.host ?? config.api.host;
3318
+ const token = options?.token ?? ensureToken(config);
3319
+ const userDataDir = config.data.user_data_dir || void 0;
3320
+ const pathProvider = new NodePathProvider(userDataDir);
3321
+ pathProvider.ensureAllDirs();
3322
+ if (hasPendingElectronDataWarning() || !verifyCliDataPath(pathProvider.getDatabaseDir())) {
3323
+ throw new Error(
3324
+ "\n" + "=".repeat(68) + "\n ChatLab: Electron desktop data not found\n" + "=".repeat(68) + '\n\n Detected that ChatLab desktop app was installed on this machine,\n but could not locate your chat databases.\n\n This usually means you changed the data directory in desktop settings.\n\n To fix this, choose one of:\n\n 1. Open ChatLab desktop app \u2014 it will auto-migrate your data\n 2. Set the data directory manually:\n export CHATLAB_DATA_DIR="/path/to/your/data"\n 3. Edit ~/.chatlab/config.toml:\n [data]\n user_data_dir = "/path/to/your/data"\n\n' + "=".repeat(68) + "\n"
3325
+ );
3326
+ }
3327
+ const migrationRunner = new MigrationRunner(ALL_MIGRATIONS, {
3328
+ dataDir: pathProvider.getSystemDir(),
3329
+ aiDataDir: pathProvider.getAiDataDir(),
3330
+ logger: {
3331
+ info: (_cat, msg) => console.log(`[Migration] ${msg}`),
3332
+ warn: (_cat, msg) => console.warn(`[Migration] ${msg}`),
3333
+ error: (_cat, msg, ...args) => console.error(`[Migration] ${msg}`, ...args)
3334
+ }
3335
+ });
3336
+ await migrationRunner.run();
3337
+ const nativeBinding = resolveNativeBinding3();
3338
+ dbManager = new DatabaseManager(pathProvider, { nativeBinding });
3339
+ convManager = new AIConversationManager(pathProvider.getAiDataDir(), { nativeBinding });
3340
+ initServerAiLogger(pathProvider.getLogsDir());
3341
+ setAuthToken(token);
3342
+ server = createServer();
3343
+ const multipart = await import("@fastify/multipart");
3344
+ await server.register(multipart.default, {
3345
+ limits: { fileSize: 1024 * 1024 * 1024 }
3346
+ // 1GB
3347
+ });
3348
+ registerSystemRoutes(server, dbManager);
3349
+ registerSessionRoutes(server, dbManager);
3350
+ registerNlpRoutes(server, dbManager);
3351
+ registerAiRoutes(server, dbManager, convManager);
3352
+ registerWebRoutes(server, dbManager, { pathProvider, nativeBinding });
3353
+ registerPreferencesRoutes(server, pathProvider);
3354
+ initSync(server, dbManager, pathProvider, { port, host, token });
3355
+ if (options?.webRoot && fs14.existsSync(options.webRoot)) {
3356
+ setWebMode(true);
3357
+ const fastifyStatic = await import("@fastify/static");
3358
+ await server.register(fastifyStatic.default, {
3359
+ root: options.webRoot,
3360
+ prefix: "/",
3361
+ wildcard: false
3362
+ });
3363
+ server.setNotFoundHandler(async (_request, reply) => {
3364
+ return reply.sendFile("index.html");
3365
+ });
3366
+ }
3367
+ await server.listen({ port, host });
3368
+ return { port, host, token };
3369
+ }
3370
+ async function stopHttpServer() {
3371
+ if (!server) return;
3372
+ try {
3373
+ await server.close();
3374
+ } finally {
3375
+ cleanupSync();
3376
+ if (convManager) {
3377
+ convManager.close();
3378
+ convManager = null;
3379
+ }
3380
+ if (dbManager) {
3381
+ dbManager.closeAll();
3382
+ dbManager = null;
3383
+ }
3384
+ closeServerAiLogger();
3385
+ setWebMode(false);
3386
+ server = null;
3387
+ }
3388
+ }
3389
+
3390
+ export {
3391
+ createServer,
3392
+ registerSystemRoutes,
3393
+ registerSessionRoutes,
3394
+ registerWebRoutes,
3395
+ registerNlpRoutes,
3396
+ registerAiRoutes,
3397
+ startHttpServer,
3398
+ stopHttpServer
3399
+ };
3400
+ //# sourceMappingURL=chunk-FBTHZU55.mjs.map