chatlab-cli 0.0.0 → 0.1.0

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