chatlab-cli 0.23.1 → 0.24.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 (80) hide show
  1. package/dist/{chunk-643S2N2U.mjs → chunk-36W2HWQN.mjs} +2 -2
  2. package/dist/{chunk-AHQQQ5HZ.mjs → chunk-A6JCL4PT.mjs} +26 -14
  3. package/dist/chunk-A6JCL4PT.mjs.map +1 -0
  4. package/dist/{chunk-WTQ7LAZR.mjs → chunk-KCBIBRHL.mjs} +2 -2
  5. package/dist/{chunk-L37WJ33K.mjs → chunk-NE4A6PND.mjs} +1905 -1914
  6. package/dist/chunk-NE4A6PND.mjs.map +1 -0
  7. package/dist/{chunk-3MX7VF2B.mjs → chunk-RLCFXTTS.mjs} +2 -2
  8. package/dist/{chunk-G7Q7EQ3V.mjs → chunk-XJJXSOLO.mjs} +6199 -6988
  9. package/dist/chunk-XJJXSOLO.mjs.map +1 -0
  10. package/dist/cli.mjs +2 -2
  11. package/dist/{http-JDORENHT.mjs → http-OPQ352FZ.mjs} +6 -14
  12. package/dist/{import-LCBSBLYF.mjs → import-GZMDLK2E.mjs} +4 -4
  13. package/dist/index.mjs +8 -8
  14. package/dist/{mcp-AR7HBIK4.mjs → mcp-SHZ2CVXY.mjs} +3 -3
  15. package/dist/{service-WCGGRZTA.mjs → service-KZTHBDQ5.mjs} +3 -1
  16. package/dist/service-KZTHBDQ5.mjs.map +1 -0
  17. package/dist/{stream-import-QM5R5FDG.mjs → stream-import-RDDB3N7B.mjs} +3 -3
  18. package/dist-web/assets/{DebugToolsPanel-Yzfz2ntn.css → DebugToolsPanel-DaSNc1o-.css} +1 -1
  19. package/dist-web/assets/DebugToolsPanel.vue_vue_type_script_setup_true_lang-_gbTn1ha.js +161 -0
  20. package/dist-web/assets/{EChartWordcloud-Cenbzcyj.js → EChartWordcloud-VpNJDurs.js} +1 -1
  21. package/dist-web/assets/{MessageExportModal-DMHA4X0b.js → MessageExportModal-n4JgNg_F.js} +1 -1
  22. package/dist-web/assets/electron-BIWBW8k_.js +1 -0
  23. package/dist-web/assets/electron-Bt9dK4Sr.js +1 -0
  24. package/dist-web/assets/fetch-BOevuRK_.js +1 -0
  25. package/dist-web/assets/fetch-BhsrPRzn.js +3 -0
  26. package/dist-web/assets/fetch-ChECfwh7.js +1 -0
  27. package/dist-web/assets/fetch-ClDGP9DK.js +1 -0
  28. package/dist-web/assets/{fetch-CvfdN4gM.js → fetch-CnyupWWc.js} +1 -1
  29. package/dist-web/assets/fetch-CvaXKzA-.js +1 -0
  30. package/dist-web/assets/fetch-D1_jL6PO.js +1 -0
  31. package/dist-web/assets/fetch-DFWZ48Xw.js +1 -0
  32. package/dist-web/assets/fetch-DKD1bKt2.js +1 -0
  33. package/dist-web/assets/fetch-DwPB8NOH.js +1 -0
  34. package/dist-web/assets/index-BN79SJSr.js +173 -0
  35. package/dist-web/assets/index-BVQsKKwg.js +1 -0
  36. package/dist-web/assets/{index-C2439065.js → index-Bzg8BftZ.js} +6 -6
  37. package/dist-web/assets/{index-DC0LjKV6.js → index-C-5bP7_E.js} +2 -2
  38. package/dist-web/assets/index-C3A_Rjh-.css +1 -0
  39. package/dist-web/assets/{index-BUfibEyG.css → index-CGNgUIT-.css} +1 -1
  40. package/dist-web/assets/index-D6ga3hbF.js +163 -0
  41. package/dist-web/assets/index-ESCx9pRR.js +8 -0
  42. package/dist-web/assets/index-JbR9Q-bb.css +1 -0
  43. package/dist-web/assets/vendor-nuxt-ui-BLMzqeB8.js +15 -0
  44. package/dist-web/assets/{vendor-reka-ui-BrTYuIfH.js → vendor-reka-ui-CnB0mMNX.js} +1 -1
  45. package/dist-web/assets/web-DtlveK3q.js +1 -0
  46. package/dist-web/index.html +4 -4
  47. package/package.json +2 -1
  48. package/dist/chunk-AHQQQ5HZ.mjs.map +0 -1
  49. package/dist/chunk-G7Q7EQ3V.mjs.map +0 -1
  50. package/dist/chunk-L37WJ33K.mjs.map +0 -1
  51. package/dist/service-WCGGRZTA.mjs.map +0 -1
  52. package/dist/src-EGXULYFQ.mjs +0 -340
  53. package/dist/stream-import-QM5R5FDG.mjs.map +0 -1
  54. package/dist-web/assets/DebugToolsPanel.vue_vue_type_script_setup_true_lang-DXxg55kL.js +0 -161
  55. package/dist-web/assets/electron-DPsrYwTt.js +0 -1
  56. package/dist-web/assets/electron-DfpXsXsS.js +0 -1
  57. package/dist-web/assets/electron-DpE69lO4.js +0 -1
  58. package/dist-web/assets/electron-HoVzpIVc.js +0 -1
  59. package/dist-web/assets/electron-SyPZX9OU.js +0 -1
  60. package/dist-web/assets/fetch-B7OBmANJ.js +0 -1
  61. package/dist-web/assets/fetch-BikSxuED.js +0 -3
  62. package/dist-web/assets/fetch-BwMZFwbF.js +0 -1
  63. package/dist-web/assets/fetch-BxE8RQYE.js +0 -1
  64. package/dist-web/assets/fetch-LBHcBBkv.js +0 -1
  65. package/dist-web/assets/http-C741fIwb.js +0 -1
  66. package/dist-web/assets/index-CD5Fz-JY.js +0 -7
  67. package/dist-web/assets/index-Dl-X-izi.css +0 -1
  68. package/dist-web/assets/index-DywUneNT.css +0 -1
  69. package/dist-web/assets/index-JvmxkAGk.js +0 -173
  70. package/dist-web/assets/index-vEVpfw-R.js +0 -163
  71. package/dist-web/assets/sse-CACcGGWv.js +0 -2
  72. package/dist-web/assets/vendor-nuxt-ui-3XQAyMd0.js +0 -15
  73. package/dist-web/assets/web-Bm94rL5B.js +0 -1
  74. /package/dist/{chunk-643S2N2U.mjs.map → chunk-36W2HWQN.mjs.map} +0 -0
  75. /package/dist/{chunk-WTQ7LAZR.mjs.map → chunk-KCBIBRHL.mjs.map} +0 -0
  76. /package/dist/{chunk-3MX7VF2B.mjs.map → chunk-RLCFXTTS.mjs.map} +0 -0
  77. /package/dist/{http-JDORENHT.mjs.map → http-OPQ352FZ.mjs.map} +0 -0
  78. /package/dist/{import-LCBSBLYF.mjs.map → import-GZMDLK2E.mjs.map} +0 -0
  79. /package/dist/{mcp-AR7HBIK4.mjs.map → mcp-SHZ2CVXY.mjs.map} +0 -0
  80. /package/dist/{src-EGXULYFQ.mjs.map → stream-import-RDDB3N7B.mjs.map} +0 -0
@@ -7,17 +7,17 @@ const __dirname = __pathDirname(__filename);
7
7
  import {
8
8
  importData,
9
9
  parseFile
10
- } from "./chunk-3MX7VF2B.mjs";
10
+ } from "./chunk-RLCFXTTS.mjs";
11
11
  import {
12
12
  analyzeIncrementalImport,
13
13
  analyzeNewImport,
14
14
  incrementalImport,
15
15
  streamImport
16
- } from "./chunk-643S2N2U.mjs";
16
+ } from "./chunk-36W2HWQN.mjs";
17
17
  import {
18
18
  AGENT_TOOL_REGISTRY,
19
19
  CoreDataProvider
20
- } from "./chunk-WTQ7LAZR.mjs";
20
+ } from "./chunk-KCBIBRHL.mjs";
21
21
  import {
22
22
  getVersion
23
23
  } from "./chunk-5ADKYYNA.mjs";
@@ -30,14 +30,19 @@ import {
30
30
  BUILTIN_MODELS,
31
31
  BUILTIN_PROVIDERS,
32
32
  BUILTIN_TOOL_CATALOG,
33
+ CustomModelStore,
34
+ CustomProviderStore,
33
35
  DatabaseManager,
36
+ LLMConfigStore,
37
+ MergeSessionCache,
34
38
  MigrationRunner,
35
39
  NodePathProvider,
36
40
  POS_TAG_DEFINITIONS,
37
41
  PreferencesManager,
38
42
  SkillManager,
39
43
  SkillManagerCore,
40
- TempDbReader,
44
+ TempDbWriter,
45
+ applyPendingNodeDataDirMigrationIfNeeded,
41
46
  applyPreprocessingPipeline,
42
47
  batchSegmentWithFrequency,
43
48
  buildMergedOutput,
@@ -45,15 +50,15 @@ import {
45
50
  buildSystemPrompt,
46
51
  checkAndCompress,
47
52
  checkConflictsFromSources,
48
- cleanupTempDatabases,
49
53
  computeWordFrequency,
54
+ countMessagesTokens,
50
55
  createActivateSkillTool,
51
56
  createAiTranslate,
52
57
  createCompressionLlmAdapter,
53
58
  createDatabaseManagerAdapter,
54
59
  createJiebaNlpProvider,
60
+ createNodeDataDirSwitch,
55
61
  deleteDict,
56
- deleteTempDatabase,
57
62
  detectAllFormats,
58
63
  detectFormat,
59
64
  downloadDict,
@@ -63,6 +68,7 @@ import {
63
68
  exportSessionToJson,
64
69
  export_service_exports,
65
70
  fetchRemoteModels,
71
+ filterMessagesWithContext,
66
72
  findEntryFileInDirectory,
67
73
  formatAIError,
68
74
  getAvailableYears,
@@ -71,6 +77,8 @@ import {
71
77
  getChatOverview,
72
78
  getClusterGraph,
73
79
  getDailyActivity,
80
+ getDefaultNodeUserDataDir,
81
+ getDefaultRulesForLocale,
74
82
  getDictList,
75
83
  getHourlyActivity,
76
84
  getLanguagePreferenceAnalysis,
@@ -83,6 +91,8 @@ import {
83
91
  getMessageLengthDistribution,
84
92
  getMessageTypeStats,
85
93
  getMonthlyActivity,
94
+ getMultipleSessionsMessages,
95
+ getPendingNodeDataDirMigration,
86
96
  getRelationshipStats,
87
97
  getSchemaDetailed,
88
98
  getSessionInfo,
@@ -94,9 +104,11 @@ import {
94
104
  getYearlyActivity,
95
105
  hasPendingElectronDataWarning,
96
106
  initNlpDir,
107
+ initTokenizer,
97
108
  isDictDownloaded,
98
109
  loadConfig,
99
110
  member_service_exports,
111
+ mergeRulesForLocale,
100
112
  openBetterSqliteDatabase,
101
113
  parseAssistantFile,
102
114
  queryMessages,
@@ -109,27 +121,25 @@ import {
109
121
  session_index_service_exports,
110
122
  session_service_exports,
111
123
  streamParseFileInfo,
124
+ stripAvatarFields,
112
125
  summary_service_exports,
113
126
  validateApiKey,
114
127
  verifyCliDataPath,
115
128
  writeAuthProfile,
116
129
  writeConfigField
117
- } from "./chunk-G7Q7EQ3V.mjs";
130
+ } from "./chunk-XJJXSOLO.mjs";
118
131
  import {
119
132
  resolveCliPath
120
133
  } from "./chunk-FJ35V3TE.mjs";
121
134
 
122
135
  // src/http/index.ts
123
- import * as fs14 from "fs";
124
- import * as crypto5 from "crypto";
136
+ import * as fs11 from "fs";
137
+ import * as crypto4 from "crypto";
125
138
 
126
139
  // src/http/server.ts
127
140
  import Fastify from "fastify";
128
141
 
129
- // src/http/auth.ts
130
- import { timingSafeEqual } from "crypto";
131
-
132
- // src/http/errors.ts
142
+ // ../../packages/http-routes/src/errors.ts
133
143
  var HTTP_STATUS = {
134
144
  ["UNAUTHORIZED" /* UNAUTHORIZED */]: 401,
135
145
  ["SESSION_NOT_FOUND" /* SESSION_NOT_FOUND */]: 404,
@@ -190,83 +200,17 @@ function errorResponse(error) {
190
200
  };
191
201
  }
192
202
 
193
- // src/http/auth.ts
194
- var cachedToken = null;
195
- var webModeEnabled = false;
196
- function setAuthToken(token) {
197
- cachedToken = token;
198
- }
199
- function setWebMode(enabled) {
200
- webModeEnabled = enabled;
201
- }
202
- function safeTokenCompare(a, b) {
203
- const bufA = Buffer.from(a);
204
- const bufB = Buffer.from(b);
205
- if (bufA.length !== bufB.length) return false;
206
- return timingSafeEqual(bufA, bufB);
207
- }
208
- async function authHook(request, reply) {
209
- if (!cachedToken) return;
210
- if (request.url.startsWith("/_web/")) return;
211
- if (webModeEnabled && !request.url.startsWith("/api/")) return;
212
- return requireBearerToken(request, reply);
213
- }
214
- function requireBearerToken(request, reply) {
215
- if (!cachedToken) return;
216
- const authHeader = request.headers.authorization;
217
- if (!authHeader || !authHeader.startsWith("Bearer ")) {
218
- const err = unauthorized();
219
- reply.code(err.statusCode).send(errorResponse(err));
220
- return;
221
- }
222
- const token = authHeader.slice(7);
223
- if (!safeTokenCompare(token, cachedToken)) {
224
- const err = unauthorized();
225
- reply.code(err.statusCode).send(errorResponse(err));
226
- return;
227
- }
228
- }
229
-
230
- // src/http/server.ts
231
- var JSON_BODY_LIMIT = 50 * 1024 * 1024;
232
- function createServer() {
233
- const server2 = Fastify({
234
- logger: false,
235
- bodyLimit: JSON_BODY_LIMIT
236
- });
237
- server2.addHook("onRequest", authHook);
238
- server2.setErrorHandler((error, _request, reply) => {
239
- if (error instanceof ApiError) {
240
- reply.code(error.statusCode).send(errorResponse(error));
241
- return;
242
- }
243
- if (error.statusCode === 413) {
244
- const bodyErr = new ApiError("BODY_TOO_LARGE" /* BODY_TOO_LARGE */, "Request body exceeds 50MB limit");
245
- reply.code(413).send(errorResponse(bodyErr));
246
- return;
247
- }
248
- const statusCode = error.statusCode;
249
- if (statusCode && statusCode >= 400 && statusCode < 600) {
250
- reply.code(statusCode).send({ success: false, error: { code: "CLIENT_ERROR", message: error.message } });
251
- return;
252
- }
253
- const err = serverError(error.message);
254
- reply.code(err.statusCode).send(errorResponse(err));
255
- });
256
- return server2;
257
- }
258
-
259
- // src/http/routes/system.ts
260
- function registerSystemRoutes(server2, dbManager2) {
203
+ // ../../packages/http-routes/src/routes/system.ts
204
+ function registerSystemRoutes(server2, ctx) {
261
205
  server2.get("/api/v1/status", async () => {
262
206
  let sessionCount = 0;
263
207
  try {
264
- sessionCount = dbManager2.listSessionIds().length;
208
+ sessionCount = ctx.dbManager.listSessionIds().length;
265
209
  } catch {
266
210
  }
267
211
  return successResponse({
268
212
  name: "ChatLab API",
269
- version: getVersion(),
213
+ version: ctx.getVersion(),
270
214
  uptime: Math.floor(process.uptime()),
271
215
  sessionCount
272
216
  });
@@ -333,18 +277,18 @@ function registerSystemRoutes(server2, dbManager2) {
333
277
  });
334
278
  }
335
279
 
336
- // src/http/routes/sessions.ts
280
+ // ../../packages/http-routes/src/routes/sessions.ts
337
281
  var EXPORT_MESSAGE_LIMIT = 1e5;
338
- function ensureDb(dbManager2, sessionId) {
339
- const db = dbManager2.open(sessionId);
282
+ function ensureDb(ctx, sessionId) {
283
+ const db = ctx.dbManager.open(sessionId);
340
284
  if (!db) throw sessionNotFound(sessionId);
341
285
  return db;
342
286
  }
343
- function registerSessionRoutes(server2, dbManager2) {
287
+ function registerRestSessionRoutes(server2, ctx) {
344
288
  server2.get("/api/v1/sessions", async () => {
345
- const sessionIds = dbManager2.listSessionIds();
289
+ const sessionIds = ctx.dbManager.listSessionIds();
346
290
  const sessions = sessionIds.map((id) => {
347
- const db = dbManager2.open(id);
291
+ const db = ctx.dbManager.open(id);
348
292
  if (!db) return null;
349
293
  const info = getSessionInfo(db);
350
294
  if (!info) return null;
@@ -363,7 +307,7 @@ function registerSessionRoutes(server2, dbManager2) {
363
307
  return successResponse(sessions);
364
308
  });
365
309
  server2.get("/api/v1/sessions/:id", async (request) => {
366
- const db = ensureDb(dbManager2, request.params.id);
310
+ const db = ensureDb(ctx, request.params.id);
367
311
  const info = getSessionInfo(db);
368
312
  if (!info) throw sessionNotFound(request.params.id);
369
313
  return successResponse({
@@ -380,7 +324,7 @@ function registerSessionRoutes(server2, dbManager2) {
380
324
  });
381
325
  server2.get("/api/v1/sessions/:id/messages", async (request) => {
382
326
  const { id } = request.params;
383
- const db = ensureDb(dbManager2, id);
327
+ const db = ensureDb(ctx, id);
384
328
  const page = Math.max(1, parseInt(request.query.page || "1", 10) || 1);
385
329
  const limit = Math.min(1e3, Math.max(1, parseInt(request.query.limit || "100", 10) || 100));
386
330
  const offset = (page - 1) * limit;
@@ -402,13 +346,13 @@ function registerSessionRoutes(server2, dbManager2) {
402
346
  });
403
347
  });
404
348
  server2.get("/api/v1/sessions/:id/members", async (request) => {
405
- const db = ensureDb(dbManager2, request.params.id);
349
+ const db = ensureDb(ctx, request.params.id);
406
350
  const members = getMembers(db);
407
351
  return successResponse(members);
408
352
  });
409
353
  server2.get("/api/v1/sessions/:id/stats/overview", async (request) => {
410
354
  const { id } = request.params;
411
- const db = ensureDb(dbManager2, id);
355
+ const db = ensureDb(ctx, id);
412
356
  const overview = getSessionOverview(db);
413
357
  const memberActivity = getMemberActivity(db);
414
358
  const typeDistribution = getMessageTypeStats(db);
@@ -435,7 +379,7 @@ function registerSessionRoutes(server2, dbManager2) {
435
379
  });
436
380
  server2.post("/api/v1/sessions/:id/sql", async (request, reply) => {
437
381
  const { id } = request.params;
438
- const db = ensureDb(dbManager2, id);
382
+ const db = ensureDb(ctx, id);
439
383
  const { sql } = request.body || {};
440
384
  if (!sql || typeof sql !== "string") {
441
385
  const err = sqlExecutionError("Missing sql parameter");
@@ -457,7 +401,7 @@ function registerSessionRoutes(server2, dbManager2) {
457
401
  });
458
402
  server2.get("/api/v1/sessions/:id/export", async (request, reply) => {
459
403
  const { id } = request.params;
460
- const db = ensureDb(dbManager2, id);
404
+ const db = ensureDb(ctx, id);
461
405
  const meta = getSessionMeta(db);
462
406
  if (!meta) throw sessionNotFound(id);
463
407
  const overview = getSessionOverview(db);
@@ -496,86 +440,9 @@ function registerSessionRoutes(server2, dbManager2) {
496
440
  });
497
441
  }
498
442
 
499
- // src/http/routes/web/index.ts
500
- import * as os3 from "os";
501
- import * as path6 from "path";
502
-
503
- // src/merger/merge-cache.ts
504
- import * as fs from "fs";
505
- import * as path from "path";
506
- import * as crypto from "crypto";
507
- var MergeSessionCache = class {
508
- cache = /* @__PURE__ */ new Map();
509
- tempDir;
510
- nativeBinding;
511
- constructor(pathProvider, options) {
512
- this.tempDir = pathProvider.getTempDir();
513
- this.nativeBinding = options?.nativeBinding;
514
- if (!fs.existsSync(this.tempDir)) {
515
- fs.mkdirSync(this.tempDir, { recursive: true });
516
- }
517
- }
518
- generateTempDbPath(filename) {
519
- const ts = Date.now();
520
- const rand = Math.random().toString(36).slice(2, 8);
521
- const safeName = filename.replace(/[/\\?%*:|"<>]/g, "_").slice(0, 40);
522
- return path.join(this.tempDir, `merge_${safeName}_${ts}_${rand}.db`);
523
- }
524
- /** Store a parsed file's temp DB and return a handle for subsequent requests. */
525
- store(filename, tempDbPath) {
526
- const handle = crypto.randomUUID();
527
- this.cache.set(handle, { tempDbPath, filename, createdAt: Date.now() });
528
- return handle;
529
- }
530
- /** Open a TempDbReader for a given handle. Caller must close the reader. */
531
- openReader(handle) {
532
- const entry = this.cache.get(handle);
533
- if (!entry || !fs.existsSync(entry.tempDbPath)) return null;
534
- const adapter = openBetterSqliteDatabase(entry.tempDbPath, {
535
- readonly: true,
536
- nativeBinding: this.nativeBinding
537
- });
538
- return { reader: new TempDbReader(adapter), filename: entry.filename };
539
- }
540
- /** Create a writable temp DB for streaming parse. */
541
- createTempDatabase(filename) {
542
- const tempDbPath = this.generateTempDbPath(filename);
543
- const adapter = openBetterSqliteDatabase(tempDbPath, { nativeBinding: this.nativeBinding });
544
- return { db: adapter, tempDbPath };
545
- }
546
- /** Remove a single entry and its temp DB file. */
547
- delete(handle) {
548
- const entry = this.cache.get(handle);
549
- if (entry) {
550
- deleteTempDatabase(entry.tempDbPath);
551
- this.cache.delete(handle);
552
- }
553
- }
554
- /** Clear all entries and delete their temp DB files. */
555
- clear() {
556
- for (const entry of this.cache.values()) {
557
- deleteTempDatabase(entry.tempDbPath);
558
- }
559
- this.cache.clear();
560
- }
561
- /** Cleanup orphan temp DBs from previous runs. */
562
- cleanupOrphans() {
563
- cleanupTempDatabases(this.tempDir);
564
- }
565
- /** Auto-cleanup entries older than maxAge (default 1 hour). */
566
- cleanupExpired(maxAgeMs = 36e5) {
567
- const now = Date.now();
568
- for (const [handle, entry] of this.cache) {
569
- if (now - entry.createdAt > maxAgeMs) {
570
- deleteTempDatabase(entry.tempDbPath);
571
- this.cache.delete(handle);
572
- }
573
- }
574
- }
575
- };
576
-
577
- // src/http/routes/web/sessions.ts
578
- function registerSessionRoutes2(server2, adapter) {
443
+ // ../../packages/http-routes/src/routes/web/sessions.ts
444
+ function registerSessionRoutes(server2, ctx) {
445
+ const { sessionAdapter: adapter } = ctx;
579
446
  server2.get("/_web/sessions", async () => {
580
447
  return session_service_exports.listAnalysisSessions(adapter);
581
448
  });
@@ -611,8 +478,9 @@ function registerSessionRoutes2(server2, adapter) {
611
478
  );
612
479
  }
613
480
 
614
- // src/http/routes/web/members.ts
615
- function registerMemberRoutes(server2, adapter) {
481
+ // ../../packages/http-routes/src/routes/web/members.ts
482
+ function registerMemberRoutes(server2, ctx) {
483
+ const { sessionAdapter: adapter } = ctx;
616
484
  server2.get("/_web/sessions/:id/members", async (request) => {
617
485
  return member_service_exports.getMembers(adapter, request.params.id);
618
486
  });
@@ -657,8 +525,46 @@ function registerMemberRoutes(server2, adapter) {
657
525
  );
658
526
  }
659
527
 
660
- // src/http/routes/web/helpers.ts
661
- import * as fs2 from "fs";
528
+ // ../../packages/http-routes/src/routes/web/preferences.ts
529
+ function registerPreferencesRoutes(server2, ctx) {
530
+ const prefManager = ctx.preferencesManager ?? new PreferencesManager(ctx.pathProvider.getSystemDir());
531
+ server2.get("/_web/preferences", async () => {
532
+ return prefManager.load();
533
+ });
534
+ server2.patch("/_web/preferences", async (request) => {
535
+ return prefManager.save(request.body);
536
+ });
537
+ server2.get("/_web/preferences/ui-config", async () => {
538
+ const config = loadConfig();
539
+ return config.ui;
540
+ });
541
+ server2.patch("/_web/preferences/ui-config", async (request) => {
542
+ try {
543
+ for (const [key, value] of Object.entries(request.body)) {
544
+ if (value !== void 0) {
545
+ writeConfigField("ui", key, value);
546
+ }
547
+ }
548
+ return { success: true };
549
+ } catch (err) {
550
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
551
+ }
552
+ });
553
+ server2.get("/_web/preferences/locale", async () => {
554
+ const config = loadConfig();
555
+ return { lang: config.locale.lang };
556
+ });
557
+ server2.patch("/_web/preferences/locale", async (request) => {
558
+ try {
559
+ writeConfigField("locale", "lang", request.body.lang);
560
+ return { success: true };
561
+ } catch (err) {
562
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
563
+ }
564
+ });
565
+ }
566
+
567
+ // ../../packages/http-routes/src/helpers.ts
662
568
  function parseTimeFilter(query) {
663
569
  const { startTs, endTs, memberId } = query;
664
570
  if (!startTs && !endTs && !memberId) return void 0;
@@ -668,29 +574,10 @@ function parseTimeFilter(query) {
668
574
  if (memberId) filter.memberId = parseInt(memberId, 10);
669
575
  return filter;
670
576
  }
671
- function resolveNativeBinding() {
672
- if (process.versions.electron) return void 0;
673
- const nativePath = resolveCliPath("native/better_sqlite3.node");
674
- if (fs2.existsSync(nativePath)) return nativePath;
675
- return void 0;
676
- }
677
- function getAiDataDir(dbManager2) {
678
- const pathProvider = dbManager2["pathProvider"];
679
- if (!pathProvider) {
680
- throw Object.assign(new Error("PathProvider not available"), { statusCode: 500 });
681
- }
682
- return pathProvider.getAiDataDir();
683
- }
684
- function ensureDb2(dbManager2, sessionId) {
685
- const db = dbManager2.open(sessionId);
686
- if (!db) {
687
- throw Object.assign(new Error(`Session not found: ${sessionId}`), { statusCode: 404 });
688
- }
689
- return db;
690
- }
691
577
 
692
- // src/http/routes/web/analytics.ts
693
- function registerAnalyticsRoutes(server2, _dbManager, adapter) {
578
+ // ../../packages/http-routes/src/routes/web/analytics.ts
579
+ function registerAnalyticsRoutes(server2, ctx) {
580
+ const { sessionAdapter: adapter } = ctx;
694
581
  server2.get("/_web/sessions/:id/years", async (request) => {
695
582
  const db = adapter.ensureReadonly(request.params.id);
696
583
  return getAvailableYears(db);
@@ -811,8 +698,9 @@ function registerAnalyticsRoutes(server2, _dbManager, adapter) {
811
698
  );
812
699
  }
813
700
 
814
- // src/http/routes/web/sql.ts
815
- function registerSqlRoutes(server2, adapter) {
701
+ // ../../packages/http-routes/src/routes/web/sql.ts
702
+ function registerSqlRoutes(server2, ctx) {
703
+ const { sessionAdapter: adapter } = ctx;
816
704
  server2.post("/_web/sessions/:id/sql", async (request, reply) => {
817
705
  const db = adapter.ensureReadonly(request.params.id);
818
706
  const { sql } = request.body || {};
@@ -847,8 +735,9 @@ function registerSqlRoutes(server2, adapter) {
847
735
  });
848
736
  }
849
737
 
850
- // src/http/routes/web/session-index.ts
851
- function registerSessionIndexRoutes(server2, adapter) {
738
+ // ../../packages/http-routes/src/routes/web/session-index.ts
739
+ function registerSessionIndexRoutes(server2, ctx) {
740
+ const { sessionAdapter: adapter } = ctx;
852
741
  server2.get("/_web/sessions/index-stats", async () => {
853
742
  return session_index_service_exports.getAllIndexStats(adapter);
854
743
  });
@@ -885,586 +774,715 @@ function registerSessionIndexRoutes(server2, adapter) {
885
774
  });
886
775
  }
887
776
 
888
- // src/ai/llm-config.ts
889
- import * as fs3 from "fs";
890
- import * as path2 from "path";
891
- import { randomUUID as randomUUID2 } from "crypto";
892
- function loadLlmConfig(aiDataDir) {
893
- const configPath = path2.join(aiDataDir, "llm-config.json");
894
- if (!fs3.existsSync(configPath)) {
895
- return { configs: [], defaultAssistant: null, fastModel: null };
896
- }
897
- try {
898
- const data = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
899
- const configs = (data.configs || []).map((c) => {
900
- const provider = c.provider || "";
901
- const authProfile = c.authProfile;
902
- const profileKey = resolveApiKey(provider, authProfile);
903
- const rawKey = c.apiKey || "";
904
- const fallbackKey = rawKey.startsWith("enc:") ? "" : rawKey;
905
- return {
906
- ...c,
907
- apiKey: profileKey || fallbackKey
908
- };
909
- });
910
- return {
911
- configs,
912
- defaultAssistant: data.defaultAssistant ?? null,
913
- fastModel: data.fastModel ?? null
914
- };
915
- } catch {
916
- return { configs: [], defaultAssistant: null, fastModel: null };
917
- }
918
- }
919
- function resolveSlot(slot, configs) {
920
- if (slot && configs.some((c) => c.id === slot.configId)) return slot;
921
- const fallback = configs[0];
922
- if (!fallback) return null;
923
- return { configId: fallback.id, modelId: fallback.model };
924
- }
925
- function getDefaultAssistantConfig(aiDataDir) {
926
- const store = loadLlmConfig(aiDataDir);
927
- const slot = resolveSlot(store.defaultAssistant, store.configs);
928
- if (!slot) return null;
929
- const config = store.configs.find((c) => c.id === slot.configId);
930
- if (!config) return null;
931
- return { ...config, model: slot.modelId || config.model };
932
- }
933
- var MAX_CONFIG_COUNT = 99;
934
- function saveLlmConfig(aiDataDir, store) {
935
- const configPath = path2.join(aiDataDir, "llm-config.json");
936
- const toSave = {
937
- ...store,
938
- configs: store.configs.map((c) => {
939
- const { apiKey: _k, ...rest } = c;
940
- return rest;
941
- }),
942
- schemaVersion: 3
943
- };
944
- fs3.writeFileSync(configPath, JSON.stringify(toSave, null, 2), "utf-8");
945
- }
946
- function addLlmConfig(aiDataDir, config) {
947
- const store = loadLlmConfig(aiDataDir);
948
- if (store.configs.length >= MAX_CONFIG_COUNT) {
949
- return { success: false, error: `Maximum ${MAX_CONFIG_COUNT} configs reached` };
950
- }
951
- const newConfig = {
952
- ...config,
953
- id: randomUUID2()
954
- };
955
- const storeForSave = loadRawConfigStore(aiDataDir);
956
- storeForSave.configs.push({
957
- ...newConfig,
958
- apiKey: ""
959
- });
960
- if (storeForSave.configs.length === 1) {
961
- storeForSave.defaultAssistant = { configId: newConfig.id, modelId: newConfig.model || "" };
962
- }
963
- if (config.apiKey) {
964
- const profileName = config.name?.toLowerCase().replace(/\s+/g, "-") || config.provider;
965
- writeAuthProfile(profileName, {
966
- type: "api_key",
967
- provider: config.provider,
968
- key: config.apiKey
969
- });
970
- storeForSave.configs[storeForSave.configs.length - 1].authProfile = profileName;
971
- }
972
- saveLlmConfig(aiDataDir, storeForSave);
973
- return { success: true, config: { ...newConfig, apiKey: "" } };
974
- }
975
- function updateLlmConfig(aiDataDir, id, updates) {
976
- const storeForSave = loadRawConfigStore(aiDataDir);
977
- const index = storeForSave.configs.findIndex((c) => c.id === id);
978
- if (index === -1) {
979
- return { success: false, error: "Config not found" };
980
- }
981
- const { apiKey: newApiKey, ...restUpdates } = updates;
982
- const updated = {
983
- ...storeForSave.configs[index],
984
- ...restUpdates
985
- };
986
- storeForSave.configs[index] = updated;
987
- if (newApiKey) {
988
- const profileName = updated.name?.toLowerCase().replace(/\s+/g, "-") || updated.provider;
989
- writeAuthProfile(profileName, {
990
- type: "api_key",
991
- provider: updated.provider,
992
- key: newApiKey
777
+ // ../../packages/http-routes/src/routes/web/export.ts
778
+ function registerExportRoutes(server2, ctx) {
779
+ const { sessionAdapter: adapter } = ctx;
780
+ server2.post("/_web/sessions/:id/export/markdown", async (request, reply) => {
781
+ const { id } = request.params;
782
+ const body = request.body;
783
+ const sessionName = body?.sessionName || id;
784
+ const { result, content } = export_service_exports.exportMarkdown(adapter, {
785
+ sessionId: id,
786
+ sessionName,
787
+ filterMode: body.filterMode || "condition",
788
+ keywords: body.keywords,
789
+ timeFilter: body.timeFilter,
790
+ senderIds: body.senderIds,
791
+ contextSize: body.contextSize,
792
+ chatSessionIds: body.chatSessionIds
993
793
  });
994
- storeForSave.configs[index].authProfile = profileName;
995
- }
996
- saveLlmConfig(aiDataDir, storeForSave);
997
- return { success: true };
998
- }
999
- function deleteLlmConfig(aiDataDir, id) {
1000
- const storeForSave = loadRawConfigStore(aiDataDir);
1001
- const index = storeForSave.configs.findIndex((c) => c.id === id);
1002
- if (index === -1) {
1003
- return { success: false, error: "Config not found" };
1004
- }
1005
- storeForSave.configs.splice(index, 1);
1006
- const fallback = storeForSave.configs[0];
1007
- if (storeForSave.defaultAssistant?.configId === id) {
1008
- storeForSave.defaultAssistant = fallback ? { configId: fallback.id, modelId: fallback.model || "" } : null;
1009
- }
1010
- if (storeForSave.fastModel?.configId === id) {
1011
- storeForSave.fastModel = fallback ? { configId: fallback.id, modelId: fallback.model || "" } : null;
1012
- }
1013
- saveLlmConfig(aiDataDir, storeForSave);
1014
- return { success: true };
1015
- }
1016
- function setDefaultAssistantSlot(aiDataDir, configId, modelId) {
1017
- const storeForSave = loadRawConfigStore(aiDataDir);
1018
- const config = storeForSave.configs.find((c) => c.id === configId);
1019
- if (!config) {
1020
- return { success: false, error: "Config not found" };
1021
- }
1022
- storeForSave.defaultAssistant = { configId, modelId };
1023
- saveLlmConfig(aiDataDir, storeForSave);
1024
- return { success: true };
1025
- }
1026
- function setFastModelSlot(aiDataDir, slot) {
1027
- const storeForSave = loadRawConfigStore(aiDataDir);
1028
- if (slot !== null) {
1029
- const config = storeForSave.configs.find((c) => c.id === slot.configId);
1030
- if (!config) {
1031
- return { success: false, error: "Config not found" };
794
+ if (!result.success) {
795
+ return reply.code(500).send({ error: result.error });
1032
796
  }
1033
- }
1034
- storeForSave.fastModel = slot;
1035
- saveLlmConfig(aiDataDir, storeForSave);
1036
- return { success: true };
1037
- }
1038
- function loadRawConfigStore(aiDataDir) {
1039
- const configPath = path2.join(aiDataDir, "llm-config.json");
1040
- if (!fs3.existsSync(configPath)) {
1041
- return { configs: [], defaultAssistant: null, fastModel: null };
1042
- }
1043
- try {
1044
- const data = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
1045
- return {
1046
- configs: data.configs || [],
1047
- defaultAssistant: data.defaultAssistant ?? null,
1048
- fastModel: data.fastModel ?? null
1049
- };
1050
- } catch {
1051
- return { configs: [], defaultAssistant: null, fastModel: null };
1052
- }
1053
- }
1054
- function buildPiModel2(config) {
1055
- return buildPiModel(config);
797
+ reply.header("Content-Type", "text/markdown; charset=utf-8");
798
+ reply.header("Content-Disposition", `attachment; filename="${encodeURIComponent(sessionName)}_export.md"`);
799
+ return reply.send(content);
800
+ });
1056
801
  }
1057
802
 
1058
- // src/http/routes/web/summaries.ts
1059
- function createSummaryDeps(dbManager2) {
1060
- const aiDataDir = getAiDataDir(dbManager2);
1061
- return {
1062
- getLlmConfig() {
1063
- return getDefaultAssistantConfig(aiDataDir);
1064
- },
1065
- buildPiModel(config) {
1066
- return buildPiModel2(config);
1067
- }
1068
- };
1069
- }
1070
- function registerSummaryRoutes(server2, dbManager2, adapter) {
1071
- const deps = createSummaryDeps(dbManager2);
1072
- server2.post("/_web/sessions/:id/summaries/generate", async (request, reply) => {
1073
- const { chatSessionId, locale, forceRegenerate, strategy } = request.body;
1074
- const result = await summary_service_exports.generateSummary(adapter, request.params.id, chatSessionId, deps, {
1075
- locale,
1076
- forceRegenerate,
1077
- strategy
1078
- });
1079
- if ("error" in result && !result.success) {
1080
- return reply.code(400).send({ error: result.error });
1081
- }
1082
- return result;
803
+ // ../../packages/http-routes/src/routes/web/nlp.ts
804
+ import * as path from "path";
805
+ function registerNlpRoutes(server2, ctx) {
806
+ const nlpDir = path.join(ctx.pathProvider.getSystemDir(), "nlp");
807
+ initNlpDir(nlpDir);
808
+ ensureDefaultDict(nlpDir).catch((err) => console.warn("[NLP] Auto-download zh-CN dict failed:", err));
809
+ server2.get("/_web/nlp/pos-tags", async () => {
810
+ return POS_TAG_DEFINITIONS;
1083
811
  });
1084
- server2.post("/_web/sessions/:id/summaries/generate-all", async (request, reply) => {
1085
- const { locale, forceRegenerate } = request.body;
1086
- const result = await summary_service_exports.generateAllSummaries(adapter, request.params.id, deps, {
1087
- locale,
1088
- forceRegenerate
1089
- });
1090
- if (result.error) {
1091
- return reply.code(400).send({ error: result.error });
1092
- }
1093
- return result;
812
+ server2.get("/_web/nlp/dicts", async () => {
813
+ return getDictList(nlpDir);
1094
814
  });
1095
- server2.post("/_web/sessions/:id/summaries/check-can-generate", async (request) => {
1096
- const { chatSessionIds } = request.body;
1097
- return summary_service_exports.checkCanGenerate(adapter, request.params.id, chatSessionIds);
815
+ server2.get("/_web/nlp/dicts/:id/status", async (request) => {
816
+ return isDictDownloaded(nlpDir, request.params.id);
1098
817
  });
1099
- }
1100
-
1101
- // src/http/routes/web/import.ts
1102
- import * as fs4 from "fs";
1103
- import * as os from "os";
1104
- import * as path3 from "path";
1105
- var DEMO_BASE_URL = "https://chatlab.fun/assets/demo";
1106
- function cleanupTemp(...paths) {
1107
- for (const p of paths) {
1108
- try {
1109
- const stat2 = fs4.statSync(p);
1110
- if (stat2.isDirectory()) {
1111
- fs4.rmSync(p, { recursive: true, force: true });
1112
- } else {
1113
- fs4.unlinkSync(p);
1114
- }
1115
- } catch {
818
+ server2.post("/_web/nlp/dicts/:id/download", async (request) => {
819
+ return downloadDict(nlpDir, request.params.id);
820
+ });
821
+ server2.delete("/_web/nlp/dicts/:id", async (request) => {
822
+ return deleteDict(nlpDir, request.params.id);
823
+ });
824
+ server2.post("/_web/nlp/word-frequency", async (request) => {
825
+ const params = request.body;
826
+ const db = ctx.dbManager.open(params.sessionId);
827
+ if (!db) {
828
+ throw Object.assign(new Error(`Session not found: ${params.sessionId}`), { statusCode: 404 });
1116
829
  }
1117
- }
1118
- }
1119
- function registerImportRoutes(server2, dbManager2) {
1120
- server2.get("/_web/supported-formats", async () => {
1121
- return getSupportedFormats();
830
+ return computeWordFrequency(db, params);
1122
831
  });
1123
- server2.post("/_web/detect-format", async (request, reply) => {
1124
- const data = await request.file();
1125
- if (!data) return reply.code(400).send({ error: "No file uploaded" });
1126
- const tmpDir = fs4.mkdtempSync(path3.join(os.tmpdir(), "chatlab-detect-"));
1127
- const tmpPath = path3.join(tmpDir, data.filename || "upload");
1128
- try {
1129
- const chunks = [];
1130
- for await (const chunk of data.file) {
1131
- chunks.push(chunk);
1132
- }
1133
- fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
1134
- const format = detectFormat(tmpPath);
1135
- const allFormats = detectAllFormats(tmpPath);
1136
- return { format, allFormats };
1137
- } finally {
1138
- cleanupTemp(tmpPath, tmpDir);
832
+ server2.post(
833
+ "/_web/nlp/segment",
834
+ async (request) => {
835
+ const { text, locale, minLength } = request.body;
836
+ return segmentText(text, locale, minLength);
1139
837
  }
838
+ );
839
+ }
840
+
841
+ // ../../packages/http-routes/src/routes/web/ai-assistants.ts
842
+ function registerAiAssistantRoutes(server2, ctx) {
843
+ server2.get("/_web/ai/tools/catalog", async () => {
844
+ return BUILTIN_TOOL_CATALOG;
1140
845
  });
1141
- server2.post("/_web/scan-multi-chat", async (request, reply) => {
1142
- const data = await request.file();
1143
- if (!data) return reply.code(400).send({ error: "No file uploaded" });
1144
- const tmpDir = fs4.mkdtempSync(path3.join(os.tmpdir(), "chatlab-scan-"));
1145
- const tmpPath = path3.join(tmpDir, data.filename || "upload");
1146
- try {
1147
- const chunks = [];
1148
- for await (const chunk of data.file) {
1149
- chunks.push(chunk);
1150
- }
1151
- fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
1152
- const chats = await scanMultiChatFile(tmpPath);
1153
- return { chats };
1154
- } finally {
1155
- cleanupTemp(tmpPath, tmpDir);
846
+ const mgr = ctx.assistantManager;
847
+ if (!mgr) return;
848
+ server2.get("/_web/ai/assistants", async () => {
849
+ return mgr.getAllAssistants();
850
+ });
851
+ server2.get("/_web/ai/assistants/:id", async (request, reply) => {
852
+ const config = mgr.getAssistantConfig(request.params.id);
853
+ if (!config) return reply.code(404).send({ error: "Not found" });
854
+ return config;
855
+ });
856
+ server2.post("/_web/ai/assistants", async (request) => {
857
+ return mgr.createAssistant(request.body);
858
+ });
859
+ server2.put("/_web/ai/assistants/:id", async (request, reply) => {
860
+ const result = mgr.updateAssistant(request.params.id, request.body);
861
+ if (!result.success) return reply.code(404).send(result);
862
+ return result;
863
+ });
864
+ server2.delete("/_web/ai/assistants/:id", async (request, reply) => {
865
+ const result = mgr.deleteAssistant(request.params.id);
866
+ if (!result.success) return reply.code(400).send(result);
867
+ return result;
868
+ });
869
+ server2.post("/_web/ai/assistants/:id/reset", async (request, reply) => {
870
+ const result = mgr.resetAssistant(request.params.id);
871
+ if (!result.success) return reply.code(400).send(result);
872
+ return result;
873
+ });
874
+ server2.post("/_web/ai/assistants/import", async (request) => {
875
+ return mgr.importAssistantFromMd(request.body.rawMd);
876
+ });
877
+ server2.post("/_web/ai/assistants/import-builtin", async (request, reply) => {
878
+ const result = mgr.importAssistant(request.body.builtinId);
879
+ if (!result.success) return reply.code(400).send(result);
880
+ return result;
881
+ });
882
+ server2.post("/_web/ai/assistants/:id/reimport", async (request, reply) => {
883
+ const result = mgr.reimportAssistant(request.params.id);
884
+ if (!result.success) return reply.code(400).send(result);
885
+ return result;
886
+ });
887
+ }
888
+
889
+ // ../../packages/http-routes/src/routes/web/ai-skills.ts
890
+ function registerAiSkillRoutes(server2, ctx) {
891
+ const mgr = ctx.skillManagerCore;
892
+ if (!mgr) return;
893
+ server2.get("/_web/ai/skills", async () => {
894
+ return mgr.getAllSkills();
895
+ });
896
+ server2.get("/_web/ai/skills/:id", async (request, reply) => {
897
+ const config = mgr.getSkillConfig(request.params.id);
898
+ if (!config) return reply.code(404).send({ error: "Not found" });
899
+ return config;
900
+ });
901
+ server2.post("/_web/ai/skills", async (request) => {
902
+ return mgr.createSkill(request.body.rawMd);
903
+ });
904
+ server2.put("/_web/ai/skills/:id", async (request, reply) => {
905
+ const result = mgr.updateSkill(request.params.id, request.body.rawMd);
906
+ if (!result.success) return reply.code(404).send(result);
907
+ return result;
908
+ });
909
+ server2.delete("/_web/ai/skills/:id", async (request, reply) => {
910
+ const result = mgr.deleteSkill(request.params.id);
911
+ if (!result.success) return reply.code(400).send(result);
912
+ return result;
913
+ });
914
+ server2.post("/_web/ai/skills/import", async (request) => {
915
+ return mgr.importSkillFromMd(request.body.rawMd);
916
+ });
917
+ server2.get("/_web/ai/skills/builtin-catalog", async () => {
918
+ return mgr.getBuiltinCatalog();
919
+ });
920
+ server2.post("/_web/ai/skills/import-builtin", async (request, reply) => {
921
+ const result = mgr.importSkill(request.body.builtinId);
922
+ if (!result.success) return reply.code(400).send(result);
923
+ return result;
924
+ });
925
+ server2.post("/_web/ai/skills/:id/reimport", async (request, reply) => {
926
+ const result = mgr.reimportSkill(request.params.id);
927
+ if (!result.success) return reply.code(400).send(result);
928
+ return result;
929
+ });
930
+ }
931
+
932
+ // ../../packages/http-routes/src/routes/web/ai-llm.ts
933
+ function hasRealApiKey(apiKey) {
934
+ return !!apiKey && apiKey !== "sk-no-key-required";
935
+ }
936
+ function toConfigDisplay(config) {
937
+ const { apiKey: _raw, ...rest } = config;
938
+ return { ...rest, apiKey: "", apiKeySet: hasRealApiKey(String(_raw || "")) };
939
+ }
940
+ function registerAiLlmRoutes(server2, ctx) {
941
+ const store = ctx.llmConfigStore;
942
+ const aiDataDir = ctx.aiDataDir;
943
+ if (!store || !aiDataDir) return;
944
+ server2.get("/_web/ai/llm/has-config", async () => {
945
+ return store.hasActiveConfig();
946
+ });
947
+ server2.get("/_web/ai/llm/configs", async () => {
948
+ const data = store.loadStore();
949
+ return {
950
+ configs: data.configs.map((c) => toConfigDisplay(c)),
951
+ defaultAssistant: data.defaultAssistant,
952
+ fastModel: data.fastModel
953
+ };
954
+ });
955
+ server2.post("/_web/ai/llm/configs", async (request) => {
956
+ const result = store.addConfig(request.body);
957
+ if (result.success && result.config) {
958
+ return { ...result, config: toConfigDisplay(result.config) };
1156
959
  }
960
+ return result;
1157
961
  });
1158
- server2.post("/_web/import", async (request, reply) => {
1159
- const data = await request.file();
1160
- if (!data) return reply.code(400).send({ error: "No file uploaded" });
1161
- const tmpDir = fs4.mkdtempSync(path3.join(os.tmpdir(), "chatlab-import-"));
1162
- const tmpPath = path3.join(tmpDir, data.filename || "upload");
1163
- const chunks = [];
1164
- for await (const chunk of data.file) {
1165
- chunks.push(chunk);
962
+ server2.put("/_web/ai/llm/configs/:id", async (request) => {
963
+ return store.updateConfig(request.params.id, request.body);
964
+ });
965
+ server2.delete("/_web/ai/llm/configs/:id", async (request) => {
966
+ return store.deleteConfig(request.params.id);
967
+ });
968
+ server2.get("/_web/ai/llm/default-assistant-slot", async () => {
969
+ return store.getDefaultAssistantSlot();
970
+ });
971
+ server2.put("/_web/ai/llm/default-assistant-slot", async (request) => {
972
+ return store.setDefaultAssistantModel(request.body.configId, request.body.modelId);
973
+ });
974
+ server2.get("/_web/ai/llm/fast-model-slot", async () => {
975
+ return store.getFastModelSlot();
976
+ });
977
+ server2.put("/_web/ai/llm/fast-model-slot", async (request) => {
978
+ return store.setFastModel(request.body);
979
+ });
980
+ server2.get("/_web/ai/llm/providers", async () => {
981
+ return BUILTIN_PROVIDERS.map((p) => {
982
+ const models = getBuiltinModelsByProvider(p.id);
983
+ return {
984
+ id: p.id,
985
+ name: p.name,
986
+ defaultBaseUrl: p.defaultBaseUrl,
987
+ models: models.filter((m) => !m.capabilities.includes("embedding") && !m.capabilities.includes("ranking")).map((m) => ({ id: m.id, name: m.name, description: m.description }))
988
+ };
989
+ });
990
+ });
991
+ server2.get("/_web/ai/llm/provider-registry", async () => {
992
+ const custom = ctx.customProviderStore?.getAll() ?? [];
993
+ return [...BUILTIN_PROVIDERS, ...custom];
994
+ });
995
+ server2.get("/_web/ai/llm/model-catalog", async () => {
996
+ const custom = ctx.customModelStore?.getAll() ?? [];
997
+ return [...BUILTIN_MODELS, ...custom];
998
+ });
999
+ server2.post("/_web/ai/llm/custom-providers", async (request, reply) => {
1000
+ if (!ctx.customProviderStore)
1001
+ return reply.code(501).send({ success: false, error: "Custom providers not available" });
1002
+ return ctx.customProviderStore.add(request.body);
1003
+ });
1004
+ server2.put("/_web/ai/llm/custom-providers/:id", async (request, reply) => {
1005
+ if (!ctx.customProviderStore)
1006
+ return reply.code(501).send({ success: false, error: "Custom providers not available" });
1007
+ const result = ctx.customProviderStore.update(request.params.id, request.body);
1008
+ if (!result.success) return reply.code(404).send(result);
1009
+ return result;
1010
+ });
1011
+ server2.delete("/_web/ai/llm/custom-providers/:id", async (request, reply) => {
1012
+ if (!ctx.customProviderStore)
1013
+ return reply.code(501).send({ success: false, error: "Custom providers not available" });
1014
+ const result = ctx.customProviderStore.delete(request.params.id);
1015
+ if (!result.success) return reply.code(404).send(result);
1016
+ return result;
1017
+ });
1018
+ server2.post("/_web/ai/llm/custom-models", async (request, reply) => {
1019
+ if (!ctx.customModelStore) return reply.code(501).send({ success: false, error: "Custom models not available" });
1020
+ const result = ctx.customModelStore.add(request.body);
1021
+ if (!result.success) return reply.code(409).send(result);
1022
+ return result;
1023
+ });
1024
+ server2.put("/_web/ai/llm/custom-models/:providerId/:modelId", async (request, reply) => {
1025
+ if (!ctx.customModelStore) return reply.code(501).send({ success: false, error: "Custom models not available" });
1026
+ const result = ctx.customModelStore.update(request.params.providerId, request.params.modelId, request.body);
1027
+ if (!result.success) return reply.code(404).send(result);
1028
+ return result;
1029
+ });
1030
+ server2.delete("/_web/ai/llm/custom-models/:providerId/:modelId", async (request, reply) => {
1031
+ if (!ctx.customModelStore) return reply.code(501).send({ success: false, error: "Custom models not available" });
1032
+ const result = ctx.customModelStore.delete(request.params.providerId, request.params.modelId);
1033
+ if (!result.success) return reply.code(404).send(result);
1034
+ return result;
1035
+ });
1036
+ server2.post("/_web/ai/llm/validate-key", async (request) => {
1037
+ const { provider, apiKey, baseUrl, model, apiFormat, configId } = request.body;
1038
+ const resolvedKey = apiKey?.trim() ? apiKey : configId ? store.getConfigById(configId)?.apiKey || "" : "";
1039
+ return validateApiKey(provider, resolvedKey, baseUrl, model, apiFormat);
1040
+ });
1041
+ server2.post("/_web/ai/llm/remote-models", async (request) => {
1042
+ const { provider, apiKey, baseUrl, apiFormat, configId } = request.body;
1043
+ const resolvedKey = apiKey?.trim() ? apiKey : configId ? store.getConfigById(configId)?.apiKey || "" : "";
1044
+ return fetchRemoteModels(provider, resolvedKey, baseUrl, apiFormat);
1045
+ });
1046
+ server2.get("/_web/ai/desensitize-rules/defaults", async (request) => {
1047
+ return getDefaultRulesForLocale(request.query.locale ?? "zh-CN");
1048
+ });
1049
+ server2.post("/_web/ai/desensitize-rules/merge", async (request) => {
1050
+ return mergeRulesForLocale(request.body.existingRules, request.body.locale);
1051
+ });
1052
+ }
1053
+
1054
+ // ../../packages/http-routes/src/routes/web/ai-llm-stream.ts
1055
+ function registerAiLlmStreamRoutes(server2, ctx) {
1056
+ const store = ctx.llmConfigStore;
1057
+ if (!store) return;
1058
+ server2.post("/_web/ai/llm/chat-stream", async (request, reply) => {
1059
+ const { messages, options } = request.body;
1060
+ const llmConfig = store.getDefaultAssistantConfig();
1061
+ if (!llmConfig) {
1062
+ return reply.code(400).send({ success: false, error: "LLM service not configured" });
1166
1063
  }
1167
- fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
1168
- const formatId = data.fields?.formatId?.value;
1169
- const chatIndexStr = data.fields?.chatIndex?.value;
1170
- const chatIndex = chatIndexStr !== void 0 ? parseInt(chatIndexStr, 10) : void 0;
1064
+ const piModel = buildPiModel(llmConfig);
1065
+ const abortController = new AbortController();
1171
1066
  reply.raw.writeHead(200, {
1172
1067
  "Content-Type": "text/event-stream",
1173
1068
  "Cache-Control": "no-cache",
1174
1069
  Connection: "keep-alive"
1175
1070
  });
1176
- function sendEvent(event, eventData) {
1177
- reply.raw.write(`event: ${event}
1178
- data: ${JSON.stringify(eventData)}
1071
+ reply.raw.on("close", () => {
1072
+ if (!abortController.signal.aborted) abortController.abort();
1073
+ });
1074
+ const sendChunk = (data) => {
1075
+ if (reply.raw.writableEnded || reply.raw.destroyed) return;
1076
+ reply.raw.write(`event: chunk
1077
+ data: ${JSON.stringify(data)}
1179
1078
 
1180
1079
  `);
1181
- }
1080
+ };
1182
1081
  try {
1183
- const nativeBinding = resolveNativeBinding();
1184
- const result = await streamImport(dbManager2, tmpPath, {
1185
- formatId,
1186
- chatIndex,
1187
- nativeBinding,
1188
- onProgress: (p) => sendEvent("progress", p)
1082
+ await runSimpleLlmStream({
1083
+ messages,
1084
+ apiKey: llmConfig.apiKey,
1085
+ piModel,
1086
+ temperature: options?.temperature,
1087
+ maxTokens: options?.maxTokens,
1088
+ onChunk: sendChunk,
1089
+ abortSignal: abortController.signal
1189
1090
  });
1190
- if (result.success) {
1191
- sendEvent("done", {
1192
- success: true,
1193
- sessionId: result.sessionId,
1194
- messageCount: result.diagnostics?.messagesWritten ?? 0,
1195
- memberCount: 0
1196
- });
1197
- } else {
1198
- sendEvent("error", { success: false, error: result.error });
1199
- }
1200
- } catch (err) {
1201
- sendEvent("error", { success: false, error: err instanceof Error ? err.message : String(err) });
1091
+ } catch (error) {
1092
+ if (abortController.signal.aborted) return;
1093
+ const msg = error instanceof Error ? error.message : String(error);
1094
+ sendChunk({ content: "", isFinished: true, finishReason: "error", error: msg });
1202
1095
  } finally {
1203
- reply.raw.end();
1204
- cleanupTemp(tmpPath, tmpDir);
1096
+ if (!reply.raw.writableEnded && !reply.raw.destroyed) reply.raw.end();
1205
1097
  }
1206
1098
  });
1207
- server2.post("/_web/import-directory", async (request, reply) => {
1208
- const parts = request.parts();
1209
- if (!parts) return reply.code(400).send({ error: "No files uploaded" });
1210
- const tmpDir = fs4.mkdtempSync(path3.join(os.tmpdir(), "chatlab-dir-import-"));
1211
- const relativePaths = [];
1212
- const fileBuffers = [];
1213
- try {
1214
- let sendEvent2 = function(event, eventData) {
1215
- reply.raw.write(`event: ${event}
1216
- data: ${JSON.stringify(eventData)}
1217
-
1218
- `);
1219
- };
1220
- var sendEvent = sendEvent2;
1221
- for await (const part of parts) {
1222
- if (part.type === "field" && part.fieldname === "relativePaths") {
1223
- relativePaths.push(String(part.value));
1224
- } else if (part.type === "file") {
1225
- const chunks = [];
1226
- for await (const chunk of part.file) {
1227
- chunks.push(chunk);
1228
- }
1229
- fileBuffers.push({ data: Buffer.concat(chunks), filename: part.filename || "" });
1230
- }
1231
- }
1232
- for (let i = 0; i < fileBuffers.length; i++) {
1233
- let relPath = relativePaths[i] || fileBuffers[i].filename || `file_${i}`;
1234
- const segments = relPath.split("/");
1235
- if (segments.length > 1) {
1236
- relPath = segments.slice(1).join("/");
1237
- }
1238
- const targetPath = path3.resolve(tmpDir, relPath);
1239
- if (!targetPath.startsWith(tmpDir + path3.sep)) continue;
1240
- fs4.mkdirSync(path3.dirname(targetPath), { recursive: true });
1241
- fs4.writeFileSync(targetPath, fileBuffers[i].data);
1242
- }
1243
- const entryPath = findEntryFileInDirectory(tmpDir);
1244
- if (!entryPath) {
1245
- return reply.code(400).send({ error: "No recognizable import format found in directory" });
1246
- }
1247
- reply.raw.writeHead(200, {
1248
- "Content-Type": "text/event-stream",
1249
- "Cache-Control": "no-cache",
1250
- Connection: "keep-alive"
1251
- });
1252
- const nativeBinding = resolveNativeBinding();
1253
- const result = await streamImport(dbManager2, entryPath, {
1254
- nativeBinding,
1255
- onProgress: (p) => sendEvent2("progress", p)
1256
- });
1257
- if (result.success) {
1258
- sendEvent2("done", {
1259
- success: true,
1260
- sessionId: result.sessionId,
1261
- messageCount: result.diagnostics?.messagesWritten ?? 0,
1262
- memberCount: 0
1263
- });
1264
- } else {
1265
- sendEvent2("error", { success: false, error: result.error });
1266
- }
1267
- } catch (err) {
1268
- if (!reply.raw.headersSent) {
1269
- return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
1270
- }
1271
- reply.raw.write(
1272
- `event: error
1273
- data: ${JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) })}
1099
+ }
1274
1100
 
1275
- `
1276
- );
1277
- } finally {
1278
- reply.raw.end();
1279
- cleanupTemp(tmpDir);
1280
- }
1281
- });
1282
- server2.post("/_web/sessions/:id/import/incremental", async (request, reply) => {
1283
- const sessionId = request.params.id;
1284
- const data = await request.file();
1285
- if (!data) return reply.code(400).send({ error: "No file uploaded" });
1286
- const tmpDir = fs4.mkdtempSync(path3.join(os.tmpdir(), "chatlab-inc-"));
1287
- const tmpPath = path3.join(tmpDir, data.filename || "upload");
1288
- const chunks = [];
1289
- for await (const chunk of data.file) {
1290
- chunks.push(chunk);
1291
- }
1292
- fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
1101
+ // ../../packages/http-routes/src/routes/web/ai-agent-stream.ts
1102
+ var activeAgentAborts = /* @__PURE__ */ new Map();
1103
+ function registerAiAgentStreamRoutes(server2, ctx) {
1104
+ if (!ctx.runAgentStream) return;
1105
+ const runAgentStream = ctx.runAgentStream;
1106
+ server2.post("/_web/ai/agent/stream", async (request, reply) => {
1107
+ const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1108
+ const abortController = new AbortController();
1109
+ activeAgentAborts.set(requestId, abortController);
1293
1110
  reply.raw.writeHead(200, {
1294
1111
  "Content-Type": "text/event-stream",
1295
1112
  "Cache-Control": "no-cache",
1296
- Connection: "keep-alive"
1113
+ Connection: "keep-alive",
1114
+ "X-Request-Id": requestId
1297
1115
  });
1298
- function sendEvent(event, eventData) {
1116
+ let emittedDone = false;
1117
+ const safeSendSSE = (event, data) => {
1118
+ if (reply.raw.writableEnded || reply.raw.destroyed) return;
1299
1119
  reply.raw.write(`event: ${event}
1300
- data: ${JSON.stringify(eventData)}
1120
+ data: ${JSON.stringify(data)}
1301
1121
 
1302
1122
  `);
1303
- }
1304
- try {
1305
- const result = await incrementalImport(dbManager2, sessionId, tmpPath, {
1306
- onProgress: (p) => sendEvent("progress", p)
1307
- });
1308
- if (result.success) {
1309
- sendEvent("done", result);
1310
- } else {
1311
- sendEvent("error", { success: false, error: result.error });
1312
- }
1313
- } catch (err) {
1314
- sendEvent("error", { success: false, error: err instanceof Error ? err.message : String(err) });
1315
- } finally {
1123
+ };
1124
+ const safeEnd = () => {
1125
+ if (reply.raw.writableEnded || reply.raw.destroyed) return;
1316
1126
  reply.raw.end();
1317
- cleanupTemp(tmpPath, tmpDir);
1318
- }
1319
- });
1320
- server2.post("/_web/sessions/:id/import/incremental/analyze", async (request, reply) => {
1321
- const sessionId = request.params.id;
1322
- const data = await request.file();
1323
- if (!data) return reply.code(400).send({ error: "No file uploaded" });
1324
- const tmpDir = fs4.mkdtempSync(path3.join(os.tmpdir(), "chatlab-analyze-"));
1325
- const tmpPath = path3.join(tmpDir, data.filename || "upload");
1326
- const chunks = [];
1327
- for await (const chunk of data.file) {
1328
- chunks.push(chunk);
1329
- }
1330
- fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
1127
+ };
1128
+ safeSendSSE("meta", { requestId });
1129
+ reply.raw.on("close", () => {
1130
+ if (!abortController.signal.aborted) {
1131
+ abortController.abort();
1132
+ }
1133
+ activeAgentAborts.delete(requestId);
1134
+ });
1331
1135
  try {
1332
- return await analyzeIncrementalImport(dbManager2, sessionId, tmpPath);
1333
- } catch (err) {
1334
- return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
1136
+ await runAgentStream(
1137
+ request.body,
1138
+ (chunk) => {
1139
+ if (chunk.type === "done") emittedDone = true;
1140
+ safeSendSSE(chunk.type, chunk);
1141
+ },
1142
+ abortController.signal
1143
+ );
1144
+ } catch (error) {
1145
+ const msg = error instanceof Error ? error.message : String(error);
1146
+ safeSendSSE("error", { type: "error", error: { name: "ServerError", message: msg } });
1335
1147
  } finally {
1336
- cleanupTemp(tmpPath, tmpDir);
1148
+ if (!emittedDone) safeSendSSE("done", { type: "done", isFinished: true });
1149
+ activeAgentAborts.delete(requestId);
1150
+ safeEnd();
1337
1151
  }
1338
1152
  });
1339
- server2.post("/_web/import/analyze", async (request, reply) => {
1340
- const data = await request.file();
1341
- if (!data) return reply.code(400).send({ error: "No file uploaded" });
1342
- const tmpDir = fs4.mkdtempSync(path3.join(os.tmpdir(), "chatlab-analyze-"));
1343
- const tmpPath = path3.join(tmpDir, data.filename || "upload");
1344
- const chunks = [];
1345
- for await (const chunk of data.file) {
1346
- chunks.push(chunk);
1347
- }
1348
- fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
1349
- try {
1350
- return await analyzeNewImport(tmpPath);
1351
- } catch (err) {
1352
- return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
1353
- } finally {
1354
- cleanupTemp(tmpPath, tmpDir);
1153
+ server2.post("/_web/ai/agent/abort", async (request) => {
1154
+ const { requestId } = request.body;
1155
+ const controller = activeAgentAborts.get(requestId);
1156
+ if (controller) {
1157
+ controller.abort();
1158
+ activeAgentAborts.delete(requestId);
1159
+ return { success: true };
1355
1160
  }
1161
+ return { success: false };
1356
1162
  });
1357
- const DEMO_FILES = [
1358
- "demo-group.json",
1359
- "demo-private-A-cuilan.json",
1360
- "demo-private-B-wukong.json",
1361
- "demo-private-C-spider.json"
1362
- ];
1363
- server2.post("/_web/demo/import", async (request, reply) => {
1364
- const locale = request.body?.locale || "en";
1365
- const nativeBinding = resolveNativeBinding();
1366
- const total = DEMO_FILES.length;
1367
- reply.raw.writeHead(200, {
1368
- "Content-Type": "text/event-stream",
1369
- "Cache-Control": "no-cache",
1370
- Connection: "keep-alive"
1371
- });
1372
- function sendEvent(event, eventData) {
1373
- reply.raw.write(`event: ${event}
1374
- data: ${JSON.stringify(eventData)}
1163
+ }
1375
1164
 
1376
- `);
1377
- }
1378
- const tmpDir = fs4.mkdtempSync(path3.join(os.tmpdir(), "chatlab-demo-"));
1379
- try {
1380
- const localPaths = [];
1381
- for (let i = 0; i < total; i++) {
1382
- sendEvent("progress", { stage: "downloading", current: i + 1, total });
1383
- const localPath = path3.join(tmpDir, DEMO_FILES[i]);
1384
- const resp = await fetch(`${DEMO_BASE_URL}/${locale}/${DEMO_FILES[i]}`, {
1385
- signal: AbortSignal.timeout(6e4)
1386
- });
1387
- if (!resp.ok) throw new Error(`Download demo failed (${DEMO_FILES[i]}): ${resp.status}`);
1388
- fs4.writeFileSync(localPath, Buffer.from(await resp.arrayBuffer()));
1389
- localPaths.push(localPath);
1390
- }
1391
- sendEvent("progress", { stage: "importing", current: 1, total });
1392
- const groupResult = await streamImport(dbManager2, localPaths[0], { nativeBinding });
1393
- if (!groupResult.success) throw new Error(groupResult.error || "Failed to import group demo");
1394
- const privateSessionIds = [];
1395
- for (let i = 1; i < localPaths.length; i++) {
1396
- sendEvent("progress", { stage: "importing", current: i + 1, total });
1397
- const result = await streamImport(dbManager2, localPaths[i], { nativeBinding });
1398
- if (!result.success) throw new Error(result.error || `Failed to import private demo: ${DEMO_FILES[i]}`);
1399
- if (result.sessionId) privateSessionIds.push(result.sessionId);
1400
- }
1401
- sendEvent("progress", { stage: "done", current: total, total });
1402
- sendEvent("result", {
1403
- success: true,
1404
- groupSessionId: groupResult.sessionId,
1405
- privateSessionIds
1406
- });
1407
- } catch (err) {
1408
- const message = err instanceof Error ? err.message : String(err);
1409
- sendEvent("progress", { stage: "error", current: 0, total, message });
1410
- sendEvent("result", { success: false, error: message });
1411
- } finally {
1412
- reply.raw.end();
1413
- cleanupTemp(tmpDir);
1414
- }
1165
+ // ../../packages/http-routes/src/routes/web/ai-filter.ts
1166
+ function registerAiFilterRoutes(server2, ctx) {
1167
+ server2.post("/_web/ai/filter-messages", async (request, reply) => {
1168
+ const { sessionId, keywords, timeFilter, senderIds, contextSize, page, pageSize } = request.body;
1169
+ const db = ctx.dbManager.open(sessionId);
1170
+ if (!db) return reply.code(404).send({ error: `Session not found: ${sessionId}` });
1171
+ return filterMessagesWithContext(db, { keywords, timeFilter, senderIds, contextSize, page, pageSize });
1172
+ });
1173
+ server2.post("/_web/ai/multiple-sessions-messages", async (request, reply) => {
1174
+ const { sessionId, chatSessionIds, page, pageSize } = request.body;
1175
+ const db = ctx.dbManager.open(sessionId);
1176
+ if (!db) return reply.code(404).send({ error: `Session not found: ${sessionId}` });
1177
+ return getMultipleSessionsMessages(db, chatSessionIds, page, pageSize);
1415
1178
  });
1416
1179
  }
1417
1180
 
1418
- // src/http/routes/web/merge.ts
1419
- import * as fs5 from "fs";
1420
- import * as os2 from "os";
1421
- import * as path4 from "path";
1422
- function registerMergeRoutes(server2, dbManager2, mergeCache) {
1423
- server2.post("/_web/merge/parse", async (request, reply) => {
1424
- const data = await request.file();
1425
- if (!data) return reply.code(400).send({ error: "No file uploaded" });
1426
- const tmpDir = fs5.mkdtempSync(path4.join(os2.tmpdir(), "chatlab-merge-"));
1427
- const tmpPath = path4.join(tmpDir, data.filename || "upload");
1428
- const chunks = [];
1429
- for await (const chunk of data.file) {
1430
- chunks.push(chunk);
1181
+ // ../../packages/http-routes/src/routes/web/ai-tools.ts
1182
+ var MAX_RESULT_CHARS = 5e5;
1183
+ var activeToolTests = /* @__PURE__ */ new Map();
1184
+ function registerAiToolRoutes(server2, ctx) {
1185
+ server2.get("/_web/ai/tools/full-catalog", async () => {
1186
+ return AGENT_TOOL_REGISTRY.map((tool) => ({
1187
+ name: tool.name,
1188
+ category: tool.category ?? "core",
1189
+ description: tool.description,
1190
+ parameters: tool.inputSchema ?? {}
1191
+ }));
1192
+ });
1193
+ server2.post("/_web/ai/tools/execute", async (request, reply) => {
1194
+ const { testId, toolName, params, sessionId } = request.body;
1195
+ const entry = AGENT_TOOL_REGISTRY.find((t) => t.name === toolName);
1196
+ if (!entry) {
1197
+ return reply.code(404).send({ success: false, error: `Tool not found: ${toolName}` });
1198
+ }
1199
+ const db = ctx.dbManager.open(sessionId);
1200
+ if (!db) {
1201
+ return reply.code(404).send({ success: false, error: `Session not found: ${sessionId}` });
1431
1202
  }
1432
- fs5.writeFileSync(tmpPath, Buffer.concat(chunks));
1203
+ const abortController = new AbortController();
1204
+ activeToolTests.set(testId, abortController);
1433
1205
  try {
1434
- const result = await streamParseFileInfo(tmpPath, {
1435
- createTempDatabase(sourceFilePath) {
1436
- return mergeCache.createTempDatabase(path4.basename(sourceFilePath));
1437
- },
1438
- onProgress() {
1206
+ const execCtx = {
1207
+ sessionId,
1208
+ db,
1209
+ dataProvider: new CoreDataProvider(db)
1210
+ };
1211
+ const startTime = Date.now();
1212
+ const result = await entry.handler(params, execCtx);
1213
+ const elapsed = Date.now() - startTime;
1214
+ if (abortController.signal.aborted) {
1215
+ return { success: false, error: "cancelled" };
1216
+ }
1217
+ let details = result.data ?? void 0;
1218
+ let truncated = false;
1219
+ if (details) {
1220
+ stripAvatarFields(details);
1221
+ const raw = JSON.stringify(details);
1222
+ if (raw.length > MAX_RESULT_CHARS) {
1223
+ truncated = true;
1224
+ details = { _truncated: true, _originalSize: raw.length, _preview: raw.slice(0, MAX_RESULT_CHARS) };
1439
1225
  }
1440
- });
1441
- const handle = mergeCache.store(data.filename || "upload", result.tempDbPath);
1226
+ }
1442
1227
  return {
1443
- handle,
1444
- name: result.name,
1445
- format: result.format,
1446
- platform: result.platform,
1447
- messageCount: result.messageCount,
1448
- memberCount: result.memberCount,
1449
- fileSize: result.fileSize
1228
+ success: true,
1229
+ elapsed,
1230
+ content: [{ type: "text", text: result.content }],
1231
+ details,
1232
+ truncated
1450
1233
  };
1451
- } finally {
1452
- try {
1453
- fs5.unlinkSync(tmpPath);
1454
- } catch {
1455
- }
1456
- try {
1457
- fs5.rmdirSync(tmpDir);
1458
- } catch {
1234
+ } catch (error) {
1235
+ if (abortController.signal.aborted) {
1236
+ return { success: false, error: "cancelled" };
1459
1237
  }
1238
+ console.error(`Failed to execute tool ${toolName}:`, error);
1239
+ return { success: false, error: String(error) };
1240
+ } finally {
1241
+ activeToolTests.delete(testId);
1460
1242
  }
1461
1243
  });
1462
- server2.post("/_web/merge/conflicts", async (request, reply) => {
1463
- const { handles } = request.body;
1464
- if (!handles || !Array.isArray(handles) || handles.length === 0) {
1465
- return reply.code(400).send({ error: "Missing or empty handles array" });
1466
- }
1467
- const readers = [];
1244
+ server2.post("/_web/ai/tools/cancel", async (request) => {
1245
+ const { testId } = request.body;
1246
+ const controller = activeToolTests.get(testId);
1247
+ if (controller) {
1248
+ controller.abort();
1249
+ activeToolTests.delete(testId);
1250
+ return { success: true };
1251
+ }
1252
+ return { success: false };
1253
+ });
1254
+ }
1255
+
1256
+ // ../../packages/http-routes/src/routes/web/ai-conversations.ts
1257
+ function registerAiConversationRoutes(server2, ctx) {
1258
+ const cm = ctx.conversationManager;
1259
+ if (!cm) return;
1260
+ server2.post("/_web/ai/conversations", async (request) => {
1261
+ const { sessionId, title, assistantId } = request.body;
1262
+ return cm.createConversation(sessionId, title, assistantId);
1263
+ });
1264
+ server2.get("/_web/ai/conversations", async (request) => {
1265
+ const { sessionId } = request.query;
1266
+ if (!sessionId) return [];
1267
+ return cm.getConversations(sessionId);
1268
+ });
1269
+ server2.get("/_web/ai/conversations/:id", async (request, reply) => {
1270
+ const conv = cm.getConversation(request.params.id);
1271
+ if (!conv) return reply.code(404).send({ error: "Conversation not found" });
1272
+ return conv;
1273
+ });
1274
+ server2.put("/_web/ai/conversations/:id/title", async (request) => {
1275
+ return cm.updateConversationTitle(request.params.id, request.body.title);
1276
+ });
1277
+ server2.delete("/_web/ai/conversations/:id", async (request) => {
1278
+ return cm.deleteConversation(request.params.id);
1279
+ });
1280
+ server2.post("/_web/ai/conversations/:id/messages", async (request) => {
1281
+ const { role, content, dataKeywords, dataMessageCount, contentBlocks, tokenUsage } = request.body;
1282
+ return cm.addMessage(
1283
+ request.params.id,
1284
+ role,
1285
+ content,
1286
+ dataKeywords,
1287
+ dataMessageCount,
1288
+ contentBlocks,
1289
+ tokenUsage
1290
+ );
1291
+ });
1292
+ server2.get("/_web/ai/conversations/:id/messages", async (request) => {
1293
+ return cm.getMessages(request.params.id);
1294
+ });
1295
+ server2.post("/_web/ai/conversations/:id/messages/:messageId/delete-from", async (request, reply) => {
1296
+ const { id, messageId } = request.params;
1297
+ if (!id || typeof id !== "string" || !messageId || typeof messageId !== "string") {
1298
+ return reply.code(400).send({ error: "conversation id and messageId are required" });
1299
+ }
1300
+ cm.deleteMessagesFrom(id, messageId);
1301
+ return { success: true };
1302
+ });
1303
+ server2.post("/_web/ai/conversations/:id/fork", async (request, reply) => {
1304
+ const { upToMessageId, title } = request.body;
1305
+ if (!upToMessageId || typeof upToMessageId !== "string") {
1306
+ return reply.code(400).send({ error: "upToMessageId is required" });
1307
+ }
1308
+ return cm.forkConversation(request.params.id, upToMessageId, title);
1309
+ });
1310
+ server2.put("/_web/ai/messages/:messageId/content", async (request, reply) => {
1311
+ const { content } = request.body;
1312
+ if (!content || typeof content !== "string") {
1313
+ return reply.code(400).send({ error: "content is required" });
1314
+ }
1315
+ cm.updateMessageContent(request.params.messageId, content);
1316
+ return { success: true };
1317
+ });
1318
+ server2.post("/_web/ai/conversations/:id/messages/:messageId/delete-relink", async (request, reply) => {
1319
+ const { id, messageId } = request.params;
1320
+ if (!id || !messageId) {
1321
+ return reply.code(400).send({ error: "conversation id and messageId are required" });
1322
+ }
1323
+ cm.deleteAndRelinkMessage(id, messageId);
1324
+ return { success: true };
1325
+ });
1326
+ server2.post("/_web/ai/conversations/:id/messages/insert-after", async (request, reply) => {
1327
+ const { afterMessageId, role, content, contentBlocks, tokenUsage } = request.body;
1328
+ if (!afterMessageId || typeof afterMessageId !== "string") {
1329
+ return reply.code(400).send({ error: "afterMessageId is required" });
1330
+ }
1331
+ return cm.insertMessageAfter(request.params.id, afterMessageId, role, content, contentBlocks, tokenUsage);
1332
+ });
1333
+ server2.get("/_web/ai/conversations/:id/token-usage", async (request) => {
1334
+ return cm.getConversationTokenUsage(request.params.id);
1335
+ });
1336
+ server2.get("/_web/ai/debug/schema", async () => {
1337
+ return cm.getAiSchema();
1338
+ });
1339
+ server2.post("/_web/ai/debug/execute-sql", async (request, reply) => {
1340
+ const { sql } = request.body;
1341
+ if (!sql || typeof sql !== "string") {
1342
+ return reply.code(400).send({ error: "sql is required" });
1343
+ }
1344
+ try {
1345
+ return cm.executeAiSQL(sql);
1346
+ } catch (e) {
1347
+ const msg = e instanceof Error ? e.message : String(e);
1348
+ return reply.code(400).send({ error: msg });
1349
+ }
1350
+ });
1351
+ server2.post("/_web/ai/debug/clear-debug-context", async () => {
1352
+ const cleared = cm.clearAllDebugContext();
1353
+ return { success: true, cleared };
1354
+ });
1355
+ server2.get("/_web/ai/conversations/:id/estimate-tokens", async (request) => {
1356
+ const history = cm.getHistoryForAgent(request.params.id);
1357
+ const tokens = countMessagesTokens(history.map((m) => ({ role: m.role, content: m.content })));
1358
+ return { success: true, tokens, messageCount: history.length };
1359
+ });
1360
+ }
1361
+
1362
+ // ../../packages/http-routes/src/routes/web/ai-summaries.ts
1363
+ function createSummaryDeps(ctx) {
1364
+ const store = ctx.llmConfigStore;
1365
+ if (!store) return null;
1366
+ return {
1367
+ getLlmConfig() {
1368
+ const config = store.getDefaultAssistantConfig();
1369
+ if (!config) return null;
1370
+ return config;
1371
+ },
1372
+ buildPiModel(config) {
1373
+ return buildPiModel(config);
1374
+ }
1375
+ };
1376
+ }
1377
+ function registerAiSummaryRoutes(server2, ctx) {
1378
+ const deps = createSummaryDeps(ctx);
1379
+ if (!deps) return;
1380
+ const { sessionAdapter: adapter } = ctx;
1381
+ server2.post("/_web/sessions/:id/summaries/generate", async (request, reply) => {
1382
+ const { chatSessionId, locale, forceRegenerate, strategy } = request.body;
1383
+ const result = await summary_service_exports.generateSummary(adapter, request.params.id, chatSessionId, deps, {
1384
+ locale,
1385
+ forceRegenerate,
1386
+ strategy
1387
+ });
1388
+ if ("error" in result && !result.success) {
1389
+ return reply.code(400).send({ error: result.error });
1390
+ }
1391
+ return result;
1392
+ });
1393
+ server2.post("/_web/sessions/:id/summaries/generate-all", async (request, reply) => {
1394
+ const { locale, forceRegenerate } = request.body;
1395
+ const result = await summary_service_exports.generateAllSummaries(adapter, request.params.id, deps, {
1396
+ locale,
1397
+ forceRegenerate
1398
+ });
1399
+ if (result.error) {
1400
+ return reply.code(400).send({ error: result.error });
1401
+ }
1402
+ return result;
1403
+ });
1404
+ server2.post("/_web/sessions/:id/summaries/check-can-generate", async (request) => {
1405
+ const { chatSessionIds } = request.body;
1406
+ return summary_service_exports.checkCanGenerate(adapter, request.params.id, chatSessionIds);
1407
+ });
1408
+ }
1409
+
1410
+ // ../../packages/http-routes/src/routes/web/merge.ts
1411
+ import * as fs from "fs";
1412
+ import * as os from "os";
1413
+ import * as path2 from "path";
1414
+ function ensureDb2(ctx, sessionId) {
1415
+ const db = ctx.dbManager.open(sessionId);
1416
+ if (!db) throw sessionNotFound(sessionId);
1417
+ return db;
1418
+ }
1419
+ function registerMergeRoutes(server2, ctx) {
1420
+ const { dbManager: dbManager2, mergeSessionCache: mergeCache, streamImport: streamImport2 } = ctx;
1421
+ if (!mergeCache) return;
1422
+ server2.post("/_web/merge/parse", async (request, reply) => {
1423
+ const contentType = request.headers["content-type"] || "";
1424
+ let filePath;
1425
+ let cleanupFile = false;
1426
+ let cleanupDir;
1427
+ if (contentType.includes("multipart/form-data")) {
1428
+ const data = await request.file();
1429
+ if (!data) return reply.code(400).send({ error: "No file uploaded" });
1430
+ const tmpDir = fs.mkdtempSync(path2.join(os.tmpdir(), "chatlab-merge-"));
1431
+ const tmpPath = path2.join(tmpDir, data.filename || "upload");
1432
+ const chunks = [];
1433
+ for await (const chunk of data.file) {
1434
+ chunks.push(chunk);
1435
+ }
1436
+ fs.writeFileSync(tmpPath, Buffer.concat(chunks));
1437
+ filePath = tmpPath;
1438
+ cleanupFile = true;
1439
+ cleanupDir = tmpDir;
1440
+ } else {
1441
+ const body = request.body;
1442
+ if (!body?.filePath) return reply.code(400).send({ error: "Missing filePath in body" });
1443
+ if (!fs.existsSync(body.filePath)) return reply.code(400).send({ error: "File not found" });
1444
+ filePath = body.filePath;
1445
+ }
1446
+ try {
1447
+ const result = await streamParseFileInfo(filePath, {
1448
+ createTempDatabase(sourceFilePath) {
1449
+ return mergeCache.createTempDatabase(path2.basename(sourceFilePath));
1450
+ },
1451
+ onProgress() {
1452
+ }
1453
+ });
1454
+ const filename = path2.basename(filePath);
1455
+ const handle = mergeCache.store(filename, result.tempDbPath);
1456
+ return {
1457
+ handle,
1458
+ name: result.name,
1459
+ format: result.format,
1460
+ platform: result.platform,
1461
+ messageCount: result.messageCount,
1462
+ memberCount: result.memberCount,
1463
+ fileSize: result.fileSize
1464
+ };
1465
+ } finally {
1466
+ if (cleanupFile) {
1467
+ try {
1468
+ fs.unlinkSync(filePath);
1469
+ } catch {
1470
+ }
1471
+ }
1472
+ if (cleanupDir) {
1473
+ try {
1474
+ fs.rmdirSync(cleanupDir);
1475
+ } catch {
1476
+ }
1477
+ }
1478
+ }
1479
+ });
1480
+ server2.post("/_web/merge/conflicts", async (request, reply) => {
1481
+ const { handles } = request.body;
1482
+ if (!handles || !Array.isArray(handles) || handles.length === 0) {
1483
+ return reply.code(400).send({ error: "Missing or empty handles array" });
1484
+ }
1485
+ const readers = [];
1468
1486
  try {
1469
1487
  const dataSources = [];
1470
1488
  for (const handle of handles) {
@@ -1498,21 +1516,24 @@ function registerMergeRoutes(server2, dbManager2, mergeCache) {
1498
1516
  const merged = buildMergedOutput(dataSources, outputName);
1499
1517
  let sessionId;
1500
1518
  if (andImport) {
1501
- const { streamImport: serverStreamImport } = await import("./stream-import-QM5R5FDG.mjs");
1519
+ if (!streamImport2) {
1520
+ return reply.code(501).send({ error: "Import not available on this server" });
1521
+ }
1502
1522
  const jsonData = JSON.stringify(merged.chatLabData);
1503
- const tmpDir = fs5.mkdtempSync(path4.join(os2.tmpdir(), "chatlab-merged-"));
1504
- const tmpPath = path4.join(tmpDir, `${outputName}.json`);
1505
- fs5.writeFileSync(tmpPath, jsonData, "utf-8");
1523
+ const tmpDir = fs.mkdtempSync(path2.join(os.tmpdir(), "chatlab-merged-"));
1524
+ const safeName = path2.basename(outputName).replace(/[/\\?%*:|"<>]/g, "_") || "merged";
1525
+ const tmpPath = path2.join(tmpDir, `${safeName}.json`);
1526
+ fs.writeFileSync(tmpPath, jsonData, "utf-8");
1506
1527
  try {
1507
- const importResult = await serverStreamImport(dbManager2, tmpPath);
1528
+ const importResult = await streamImport2(dbManager2, tmpPath);
1508
1529
  sessionId = importResult.sessionId;
1509
1530
  } finally {
1510
1531
  try {
1511
- fs5.unlinkSync(tmpPath);
1532
+ fs.unlinkSync(tmpPath);
1512
1533
  } catch {
1513
1534
  }
1514
1535
  try {
1515
- fs5.rmdirSync(tmpDir);
1536
+ fs.rmdirSync(tmpDir);
1516
1537
  } catch {
1517
1538
  }
1518
1539
  }
@@ -1548,10 +1569,9 @@ function registerMergeRoutes(server2, dbManager2, mergeCache) {
1548
1569
  }
1549
1570
  const handles = [];
1550
1571
  for (const sid of sessionIds) {
1551
- const db = ensureDb2(dbManager2, sid);
1572
+ const db = ensureDb2(ctx, sid);
1552
1573
  const exported = exportSessionToJson(db);
1553
1574
  const { db: tempDb, tempDbPath } = mergeCache.createTempDatabase(exported.meta.name);
1554
- const { TempDbWriter } = await import("./src-EGXULYFQ.mjs");
1555
1575
  const writer = new TempDbWriter(tempDb);
1556
1576
  writer.writeMeta({
1557
1577
  name: exported.meta.name,
@@ -1586,72 +1606,17 @@ function registerMergeRoutes(server2, dbManager2, mergeCache) {
1586
1606
  });
1587
1607
  }
1588
1608
 
1589
- // src/http/routes/web/export.ts
1590
- function registerExportRoutes(server2, adapter) {
1591
- server2.post("/_web/sessions/:id/export/markdown", async (request, reply) => {
1592
- const { id } = request.params;
1593
- const body = request.body;
1594
- const sessionName = body?.sessionName || id;
1595
- const { result, content } = export_service_exports.exportMarkdown(adapter, {
1596
- sessionId: id,
1597
- sessionName,
1598
- filterMode: body.filterMode || "condition",
1599
- keywords: body.keywords,
1600
- timeFilter: body.timeFilter,
1601
- senderIds: body.senderIds,
1602
- contextSize: body.contextSize,
1603
- chatSessionIds: body.chatSessionIds
1604
- });
1605
- if (!result.success) {
1606
- return reply.code(500).send({ error: result.error });
1607
- }
1608
- reply.header("Content-Type", "text/markdown; charset=utf-8");
1609
- reply.header("Content-Disposition", `attachment; filename="${encodeURIComponent(sessionName)}_export.md"`);
1610
- return reply.send(content);
1611
- });
1612
- }
1613
-
1614
- // src/http/routes/web/cache.ts
1615
- import * as fs6 from "fs";
1609
+ // ../../packages/http-routes/src/routes/web/cache.ts
1610
+ import * as fs2 from "fs";
1616
1611
  import * as fsp from "fs/promises";
1617
- import * as path5 from "path";
1618
- import { execFile } from "child_process";
1619
- function runExecFile(file, args, runner = execFile) {
1620
- return new Promise((resolve2, reject) => {
1621
- runner(file, args, (error) => {
1622
- if (error) {
1623
- reject(error);
1624
- return;
1625
- }
1626
- resolve2();
1627
- });
1628
- });
1629
- }
1630
- async function openDirectoryPath(dirPath, platform = process.platform, runner) {
1631
- if (platform === "darwin") {
1632
- await runExecFile("open", [dirPath], runner);
1633
- } else if (platform === "win32") {
1634
- await runExecFile("explorer.exe", [dirPath], runner);
1635
- } else {
1636
- await runExecFile("xdg-open", [dirPath], runner);
1637
- }
1638
- }
1639
- async function showPathInFolder(filePath, platform = process.platform, runner) {
1640
- if (platform === "darwin") {
1641
- await runExecFile("open", ["-R", filePath], runner);
1642
- } else if (platform === "win32") {
1643
- await runExecFile("explorer.exe", [`/select,${filePath}`], runner);
1644
- } else {
1645
- await runExecFile("xdg-open", [path5.dirname(filePath)], runner);
1646
- }
1647
- }
1612
+ import * as path3 from "path";
1648
1613
  async function getDirSize(dirPath) {
1649
1614
  let totalSize = 0;
1650
1615
  try {
1651
- if (!fs6.existsSync(dirPath)) return 0;
1616
+ if (!fs2.existsSync(dirPath)) return 0;
1652
1617
  const files = await fsp.readdir(dirPath, { withFileTypes: true });
1653
1618
  for (const file of files) {
1654
- const filePath = path5.join(dirPath, file.name);
1619
+ const filePath = path3.join(dirPath, file.name);
1655
1620
  if (file.isDirectory()) {
1656
1621
  totalSize += await getDirSize(filePath);
1657
1622
  } else {
@@ -1666,10 +1631,10 @@ async function getDirSize(dirPath) {
1666
1631
  async function getFileCount(dirPath) {
1667
1632
  let count = 0;
1668
1633
  try {
1669
- if (!fs6.existsSync(dirPath)) return 0;
1634
+ if (!fs2.existsSync(dirPath)) return 0;
1670
1635
  const files = await fsp.readdir(dirPath, { withFileTypes: true });
1671
1636
  for (const file of files) {
1672
- const filePath = path5.join(dirPath, file.name);
1637
+ const filePath = path3.join(dirPath, file.name);
1673
1638
  if (file.isDirectory()) {
1674
1639
  count += await getFileCount(filePath);
1675
1640
  } else {
@@ -1680,15 +1645,26 @@ async function getFileCount(dirPath) {
1680
1645
  }
1681
1646
  return count;
1682
1647
  }
1683
- function registerCacheRoutes(server2, pathProvider) {
1684
- const downloadsDir = pathProvider.getDownloadsDir();
1648
+ function hasChatLabDatabases(dirPath) {
1649
+ try {
1650
+ const markerPath = path3.join(dirPath, ".chatlab");
1651
+ const dbDir = path3.join(dirPath, "databases");
1652
+ if (!fs2.existsSync(markerPath) || !fs2.existsSync(dbDir)) return false;
1653
+ return fs2.readdirSync(dbDir).some((file) => file.endsWith(".db"));
1654
+ } catch {
1655
+ return false;
1656
+ }
1657
+ }
1658
+ function registerCacheRoutes(server2, ctx) {
1659
+ const pp = ctx.pathProvider;
1660
+ const downloadsDir = ctx.downloadsDir ?? pp.getDownloadsDir();
1685
1661
  server2.get("/_web/cache/info", async () => {
1686
1662
  const directories = [
1687
1663
  {
1688
1664
  id: "databases",
1689
1665
  name: "settings.storage.cache.databases.name",
1690
1666
  description: "settings.storage.cache.databases.description",
1691
- path: pathProvider.getDatabaseDir(),
1667
+ path: pp.getDatabaseDir(),
1692
1668
  icon: "i-heroicons-circle-stack",
1693
1669
  canClear: false
1694
1670
  },
@@ -1696,7 +1672,7 @@ function registerCacheRoutes(server2, pathProvider) {
1696
1672
  id: "ai",
1697
1673
  name: "settings.storage.cache.ai.name",
1698
1674
  description: "settings.storage.cache.ai.description",
1699
- path: pathProvider.getAiDataDir(),
1675
+ path: pp.getAiDataDir(),
1700
1676
  icon: "i-heroicons-sparkles",
1701
1677
  canClear: false
1702
1678
  },
@@ -1704,7 +1680,7 @@ function registerCacheRoutes(server2, pathProvider) {
1704
1680
  id: "cache",
1705
1681
  name: "settings.storage.cache.statsCache.name",
1706
1682
  description: "settings.storage.cache.statsCache.description",
1707
- path: pathProvider.getCacheDir(),
1683
+ path: pp.getCacheDir(),
1708
1684
  icon: "i-heroicons-bolt",
1709
1685
  canClear: true
1710
1686
  },
@@ -1712,7 +1688,7 @@ function registerCacheRoutes(server2, pathProvider) {
1712
1688
  id: "logs",
1713
1689
  name: "settings.storage.cache.logs.name",
1714
1690
  description: "settings.storage.cache.logs.description",
1715
- path: pathProvider.getLogsDir(),
1691
+ path: pp.getLogsDir(),
1716
1692
  icon: "i-heroicons-document-text",
1717
1693
  canClear: true
1718
1694
  }
@@ -1720,12 +1696,11 @@ function registerCacheRoutes(server2, pathProvider) {
1720
1696
  const results = await Promise.all(
1721
1697
  directories.map(async (dir) => {
1722
1698
  const [size, fileCount] = await Promise.all([getDirSize(dir.path), getFileCount(dir.path)]);
1723
- return { ...dir, size, fileCount, exists: fs6.existsSync(dir.path) };
1699
+ return { ...dir, size, fileCount, exists: fs2.existsSync(dir.path) };
1724
1700
  })
1725
1701
  );
1726
1702
  return {
1727
- baseDir: pathProvider.getUserDataDir(),
1728
- systemDir: pathProvider.getSystemDir(),
1703
+ baseDir: pp.getUserDataDir(),
1729
1704
  directories: results,
1730
1705
  totalSize: results.reduce((sum, d) => sum + d.size, 0)
1731
1706
  };
@@ -1733,15 +1708,15 @@ function registerCacheRoutes(server2, pathProvider) {
1733
1708
  server2.post("/_web/cache/clear", async (request) => {
1734
1709
  const { cacheId } = request.body;
1735
1710
  const allowedDirs = {
1736
- cache: pathProvider.getCacheDir(),
1737
- logs: pathProvider.getLogsDir()
1711
+ cache: pp.getCacheDir(),
1712
+ logs: pp.getLogsDir()
1738
1713
  };
1739
1714
  const dirPath = allowedDirs[cacheId];
1740
1715
  if (!dirPath) return { success: false, error: "Not allowed to clear this directory" };
1741
- if (!fs6.existsSync(dirPath)) return { success: true };
1716
+ if (!fs2.existsSync(dirPath)) return { success: true };
1742
1717
  const files = await fsp.readdir(dirPath);
1743
1718
  for (const file of files) {
1744
- const filePath = path5.join(dirPath, file);
1719
+ const filePath = path3.join(dirPath, file);
1745
1720
  const stat2 = await fsp.stat(filePath);
1746
1721
  if (stat2.isDirectory()) {
1747
1722
  await fsp.rm(filePath, { recursive: true });
@@ -1751,37 +1726,32 @@ function registerCacheRoutes(server2, pathProvider) {
1751
1726
  }
1752
1727
  return { success: true };
1753
1728
  });
1754
- server2.post("/_web/cache/open-dir", async (request) => {
1755
- const { cacheId } = request.body;
1756
- const dirPaths = {
1757
- base: pathProvider.getUserDataDir(),
1758
- system: pathProvider.getSystemDir(),
1759
- databases: pathProvider.getDatabaseDir(),
1760
- cache: pathProvider.getCacheDir(),
1761
- ai: pathProvider.getAiDataDir(),
1762
- logs: pathProvider.getLogsDir(),
1763
- downloads: downloadsDir
1729
+ server2.get("/_web/cache/data-dir", async () => {
1730
+ const pending = ctx.getPendingDataDirMigration?.();
1731
+ return {
1732
+ path: pp.getUserDataDir(),
1733
+ defaultPath: ctx.defaultUserDataDir,
1734
+ isCustom: ctx.isCustomDataDir ?? false,
1735
+ canSetDataDir: ctx.canSetDataDir ?? Boolean(ctx.setDataDir),
1736
+ hasLegacyDataAtDefaultDir: Boolean(ctx.defaultUserDataDir) && path3.resolve(pp.getUserDataDir()) !== path3.resolve(ctx.defaultUserDataDir ?? "") && hasChatLabDatabases(ctx.defaultUserDataDir ?? ""),
1737
+ pendingMigration: pending ? {
1738
+ from: pending.from,
1739
+ to: pending.to,
1740
+ createdAt: pending.createdAt
1741
+ } : void 0
1764
1742
  };
1765
- const dirPath = dirPaths[cacheId];
1766
- if (!dirPath) return { success: false, error: "Unknown directory" };
1767
- if (!fs6.existsSync(dirPath)) {
1768
- await fsp.mkdir(dirPath, { recursive: true });
1769
- }
1770
- try {
1771
- await openDirectoryPath(dirPath);
1772
- } catch (error) {
1773
- return { success: false, error: error instanceof Error ? error.message : String(error) };
1774
- }
1775
- return { success: true };
1776
1743
  });
1777
- server2.get("/_web/cache/data-dir", async () => {
1778
- const config = loadConfig();
1779
- const isCustom = Boolean(config.data.user_data_dir || process.env.CHATLAB_DATA_DIR);
1780
- return { path: pathProvider.getUserDataDir(), isCustom };
1744
+ server2.post("/_web/cache/data-dir", async (request, reply) => {
1745
+ if (!ctx.setDataDir) {
1746
+ return reply.code(501).send({ success: false, error: "Data directory changes are not supported" });
1747
+ }
1748
+ const targetPath = typeof request.body?.path === "string" ? request.body.path : null;
1749
+ const migrate = request.body?.migrate !== false;
1750
+ return ctx.setDataDir(targetPath, migrate);
1781
1751
  });
1782
1752
  server2.get("/_web/cache/latest-import-log", async () => {
1783
- const importLogDir = path5.join(pathProvider.getLogsDir(), "import");
1784
- if (!fs6.existsSync(importLogDir)) {
1753
+ const importLogDir = path3.join(pp.getLogsDir(), "import");
1754
+ if (!fs2.existsSync(importLogDir)) {
1785
1755
  return { success: false, error: "Log directory not found" };
1786
1756
  }
1787
1757
  const files = await fsp.readdir(importLogDir);
@@ -1791,7 +1761,7 @@ function registerCacheRoutes(server2, pathProvider) {
1791
1761
  }
1792
1762
  const fileStats = await Promise.all(
1793
1763
  logFiles.map(async (f) => {
1794
- const filePath = path5.join(importLogDir, f);
1764
+ const filePath = path3.join(importLogDir, f);
1795
1765
  const stat2 = await fsp.stat(filePath);
1796
1766
  return { name: f, path: filePath, mtime: stat2.mtime.getTime() };
1797
1767
  })
@@ -1804,898 +1774,1012 @@ function registerCacheRoutes(server2, pathProvider) {
1804
1774
  if (!filename || !dataUrl) {
1805
1775
  return { success: false, error: "filename and dataUrl are required" };
1806
1776
  }
1807
- const base64Prefix = dataUrl.indexOf(",");
1808
- const base64Data = base64Prefix >= 0 ? dataUrl.slice(base64Prefix + 1) : dataUrl;
1809
- const buffer = Buffer.from(base64Data, "base64");
1810
- if (!fs6.existsSync(downloadsDir)) {
1811
- fs6.mkdirSync(downloadsDir, { recursive: true });
1777
+ let buffer;
1778
+ if (dataUrl.includes(";base64,")) {
1779
+ const base64Data = dataUrl.split(";base64,")[1];
1780
+ buffer = Buffer.from(base64Data, "base64");
1781
+ } else if (dataUrl.includes("charset=utf-8,")) {
1782
+ const textData = dataUrl.split("charset=utf-8,")[1];
1783
+ buffer = Buffer.from(decodeURIComponent(textData), "utf-8");
1784
+ } else {
1785
+ const base64Data = dataUrl.replace(/^data:[^,]+,/, "");
1786
+ buffer = Buffer.from(base64Data, "base64");
1787
+ }
1788
+ if (!fs2.existsSync(downloadsDir)) {
1789
+ fs2.mkdirSync(downloadsDir, { recursive: true });
1812
1790
  }
1813
- const filePath = path5.join(downloadsDir, filename);
1814
- fs6.writeFileSync(filePath, buffer);
1791
+ const filePath = path3.join(downloadsDir, filename);
1792
+ fs2.writeFileSync(filePath, buffer);
1815
1793
  return { success: true, filePath };
1816
1794
  });
1817
- server2.post("/_web/cache/show-in-folder", async (request) => {
1818
- const { filePath } = request.body;
1819
- if (!filePath) {
1820
- return { success: false, error: "filePath is required" };
1821
- }
1822
- try {
1823
- await showPathInFolder(filePath);
1824
- } catch (error) {
1825
- return { success: false, error: error instanceof Error ? error.message : String(error) };
1826
- }
1795
+ server2.post("/_web/cache/open-dir", async (request, reply) => {
1796
+ if (!ctx.openDirectory) return reply.code(501).send({ success: false, error: "Not supported" });
1797
+ const { cacheId } = request.body;
1798
+ const dirPaths = {
1799
+ base: pp.getUserDataDir(),
1800
+ databases: pp.getDatabaseDir(),
1801
+ cache: pp.getCacheDir(),
1802
+ ai: pp.getAiDataDir(),
1803
+ logs: pp.getLogsDir(),
1804
+ downloads: downloadsDir
1805
+ };
1806
+ const dirPath = dirPaths[cacheId];
1807
+ if (!dirPath) return { success: false, error: "Unknown directory" };
1808
+ if (!fs2.existsSync(dirPath)) {
1809
+ await fsp.mkdir(dirPath, { recursive: true });
1810
+ }
1811
+ try {
1812
+ await ctx.openDirectory(dirPath);
1813
+ } catch (error) {
1814
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
1815
+ }
1827
1816
  return { success: true };
1828
1817
  });
1829
- }
1830
-
1831
- // src/http/routes/web/index.ts
1832
- function isNewerVersion(latest, current) {
1833
- const parse = (v) => {
1834
- const [core, pre] = v.split("-", 2);
1835
- const parts = core.split(".").map(Number);
1836
- return { major: parts[0] || 0, minor: parts[1] || 0, patch: parts[2] || 0, pre };
1837
- };
1838
- const l = parse(latest);
1839
- const c = parse(current);
1840
- if (l.major !== c.major) return l.major > c.major;
1841
- if (l.minor !== c.minor) return l.minor > c.minor;
1842
- if (l.patch !== c.patch) return l.patch > c.patch;
1843
- if (c.pre && !l.pre) return true;
1844
- return false;
1845
- }
1846
- function registerWebRoutes(server2, dbManager2, options) {
1847
- const adapter = createDatabaseManagerAdapter(dbManager2);
1848
- const mergeCache = options?.pathProvider ? new MergeSessionCache(options.pathProvider, { nativeBinding: options.nativeBinding }) : null;
1849
- mergeCache?.cleanupOrphans();
1850
- const fallbackPathProvider = {
1851
- getSystemDir: () => path6.join(os3.homedir(), ".chatlab"),
1852
- getUserDataDir: () => path6.join(os3.homedir(), ".chatlab", "data"),
1853
- getDatabaseDir: () => path6.join(os3.homedir(), ".chatlab", "data", "databases"),
1854
- getAiDataDir: () => path6.join(os3.homedir(), ".chatlab", "ai"),
1855
- getSettingsDir: () => path6.join(os3.homedir(), ".chatlab", "settings"),
1856
- getCacheDir: () => path6.join(os3.homedir(), ".chatlab", "cache"),
1857
- getTempDir: () => path6.join(os3.homedir(), ".chatlab", "temp"),
1858
- getLogsDir: () => path6.join(os3.homedir(), ".chatlab", "logs"),
1859
- getDownloadsDir: () => path6.join(os3.homedir(), "Downloads")
1860
- };
1861
- const resolvedPathProvider = options?.pathProvider ?? fallbackPathProvider;
1862
- registerSessionRoutes2(server2, adapter);
1863
- registerMemberRoutes(server2, adapter);
1864
- registerAnalyticsRoutes(server2, dbManager2, adapter);
1865
- registerSqlRoutes(server2, adapter);
1866
- registerSessionIndexRoutes(server2, adapter);
1867
- registerSummaryRoutes(server2, dbManager2, adapter);
1868
- registerImportRoutes(server2, dbManager2);
1869
- if (mergeCache) {
1870
- registerMergeRoutes(server2, dbManager2, mergeCache);
1871
- }
1872
- registerExportRoutes(server2, adapter);
1873
- registerCacheRoutes(server2, resolvedPathProvider);
1874
- server2.get("/_web/system/check-update", async () => {
1875
- const currentVersion = getVersion();
1876
- const packageName = "chatlab-cli";
1818
+ server2.post("/_web/cache/show-in-folder", async (request, reply) => {
1819
+ if (!ctx.showInFolder) return reply.code(501).send({ success: false, error: "Not supported" });
1820
+ const { filePath } = request.body;
1821
+ if (!filePath) return { success: false, error: "filePath is required" };
1877
1822
  try {
1878
- const resp = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
1879
- headers: { Accept: "application/json" },
1880
- signal: AbortSignal.timeout(1e4)
1881
- });
1882
- if (!resp.ok) {
1883
- return { hasUpdate: false, currentVersion, error: `npm registry HTTP ${resp.status}` };
1884
- }
1885
- const data = await resp.json();
1886
- const latestVersion = data.version || currentVersion;
1887
- const hasUpdate = isNewerVersion(latestVersion, currentVersion);
1888
- return { hasUpdate, currentVersion, latestVersion };
1889
- } catch (err) {
1890
- return {
1891
- hasUpdate: false,
1892
- currentVersion,
1893
- error: err instanceof Error ? err.message : String(err)
1894
- };
1823
+ await ctx.showInFolder(filePath);
1824
+ } catch (error) {
1825
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
1895
1826
  }
1827
+ return { success: true };
1896
1828
  });
1897
1829
  }
1898
1830
 
1899
- // src/http/routes/nlp.ts
1900
- import * as path7 from "path";
1901
- function ensureDb3(dbManager2, sessionId) {
1902
- const db = dbManager2.open(sessionId);
1903
- if (!db) {
1904
- throw Object.assign(new Error(`Session not found: ${sessionId}`), { statusCode: 404 });
1905
- }
1906
- return db;
1907
- }
1908
- function registerNlpRoutes(server2, dbManager2) {
1909
- const pathProvider = dbManager2["pathProvider"];
1910
- if (pathProvider) {
1911
- const nlpDir = path7.join(pathProvider.getSystemDir(), "nlp");
1912
- initNlpDir(nlpDir);
1913
- ensureDefaultDict(nlpDir).catch((err) => console.warn("[NLP] Auto-download zh-CN dict failed:", err));
1914
- }
1915
- server2.get("/_web/nlp/pos-tags", async () => {
1916
- return POS_TAG_DEFINITIONS;
1917
- });
1918
- server2.get("/_web/nlp/dicts", async () => {
1919
- const nlpDir = resolveNlpDir(dbManager2);
1920
- return getDictList(nlpDir);
1921
- });
1922
- server2.get("/_web/nlp/dicts/:id/status", async (request) => {
1923
- const nlpDir = resolveNlpDir(dbManager2);
1924
- return isDictDownloaded(nlpDir, request.params.id);
1925
- });
1926
- server2.post("/_web/nlp/dicts/:id/download", async (request) => {
1927
- const nlpDir = resolveNlpDir(dbManager2);
1928
- return downloadDict(nlpDir, request.params.id);
1929
- });
1930
- server2.delete("/_web/nlp/dicts/:id", async (request) => {
1931
- const nlpDir = resolveNlpDir(dbManager2);
1932
- return deleteDict(nlpDir, request.params.id);
1933
- });
1934
- server2.post("/_web/nlp/word-frequency", async (request) => {
1935
- const params = request.body;
1936
- const db = ensureDb3(dbManager2, params.sessionId);
1937
- return computeWordFrequency(db, params);
1938
- });
1939
- server2.post(
1940
- "/_web/nlp/segment",
1941
- async (request) => {
1942
- const { text, locale, minLength } = request.body;
1943
- return segmentText(text, locale, minLength);
1944
- }
1945
- );
1946
- }
1947
- function resolveNlpDir(dbManager2) {
1948
- const pathProvider = dbManager2["pathProvider"];
1949
- return path7.join(pathProvider.getSystemDir(), "nlp");
1831
+ // ../../packages/http-routes/src/register.ts
1832
+ function registerSharedRoutes(server2, ctx, options) {
1833
+ registerSystemRoutes(server2, ctx);
1834
+ registerRestSessionRoutes(server2, ctx);
1835
+ registerSessionRoutes(server2, ctx);
1836
+ registerMemberRoutes(server2, ctx);
1837
+ registerPreferencesRoutes(server2, ctx);
1838
+ registerAnalyticsRoutes(server2, ctx);
1839
+ registerSqlRoutes(server2, ctx);
1840
+ registerSessionIndexRoutes(server2, ctx);
1841
+ registerExportRoutes(server2, ctx);
1842
+ registerNlpRoutes(server2, ctx);
1843
+ if (options?.requireAi) {
1844
+ const missing = [];
1845
+ if (!ctx.aiDataDir) missing.push("aiDataDir");
1846
+ if (!ctx.conversationManager) missing.push("conversationManager");
1847
+ if (!ctx.assistantManager) missing.push("assistantManager");
1848
+ if (!ctx.skillManagerCore) missing.push("skillManagerCore");
1849
+ if (!ctx.llmConfigStore) missing.push("llmConfigStore");
1850
+ if (!ctx.customProviderStore) missing.push("customProviderStore");
1851
+ if (!ctx.customModelStore) missing.push("customModelStore");
1852
+ if (!ctx.runAgentStream) missing.push("runAgentStream");
1853
+ if (missing.length > 0) {
1854
+ throw new Error(`[http-routes] requireAi is set but missing AI dependencies: ${missing.join(", ")}`);
1855
+ }
1856
+ }
1857
+ registerAiAssistantRoutes(server2, ctx);
1858
+ registerAiSkillRoutes(server2, ctx);
1859
+ registerAiLlmRoutes(server2, ctx);
1860
+ registerAiLlmStreamRoutes(server2, ctx);
1861
+ registerAiAgentStreamRoutes(server2, ctx);
1862
+ registerAiFilterRoutes(server2, ctx);
1863
+ registerAiToolRoutes(server2, ctx);
1864
+ registerAiConversationRoutes(server2, ctx);
1865
+ registerAiSummaryRoutes(server2, ctx);
1866
+ registerMergeRoutes(server2, ctx);
1867
+ registerCacheRoutes(server2, ctx);
1950
1868
  }
1951
1869
 
1952
- // src/http/routes/ai.ts
1953
- import * as fs10 from "fs";
1954
- import * as path11 from "path";
1955
-
1956
- // src/ai/logger.ts
1957
- var logger = null;
1958
- function initServerAiLogger(logsDir) {
1959
- if (!logger) {
1960
- logger = new AiLogger(logsDir);
1961
- }
1962
- return logger;
1870
+ // ../../packages/http-routes/src/auth.ts
1871
+ import { timingSafeEqual, createHmac, randomBytes } from "crypto";
1872
+ var cachedToken = null;
1873
+ var requireAuthEnabled = false;
1874
+ function setAuthToken(token) {
1875
+ cachedToken = token;
1963
1876
  }
1964
- function getServerAiLogger() {
1965
- return logger;
1877
+ function setRequireAuth(enabled) {
1878
+ requireAuthEnabled = enabled;
1966
1879
  }
1967
- function closeServerAiLogger() {
1968
- if (logger) {
1969
- logger.close();
1970
- logger = null;
1880
+ var hmacKey = randomBytes(32);
1881
+ function safeTokenCompare(a, b) {
1882
+ const hashA = createHmac("sha256", hmacKey).update(a).digest();
1883
+ const hashB = createHmac("sha256", hmacKey).update(b).digest();
1884
+ return timingSafeEqual(hashA, hashB);
1885
+ }
1886
+ async function authHook(request, reply) {
1887
+ if (!cachedToken) return;
1888
+ const url = request.url;
1889
+ if (url.startsWith("/api/")) {
1890
+ return requireBearerToken(request, reply);
1891
+ }
1892
+ if (url.startsWith("/_web/")) {
1893
+ if (requireAuthEnabled) return requireBearerToken(request, reply);
1894
+ return;
1971
1895
  }
1972
1896
  }
1973
-
1974
- // src/ai/tool-adapter.ts
1975
- var DEFAULT_MAX_TOOL_RESULT_TOKENS = 8e3;
1976
- var TOOL_TRUNCATION_STRATEGY = {
1977
- search_messages: "keep_first",
1978
- deep_search_messages: "keep_first",
1979
- get_recent_messages: "keep_last",
1980
- get_message_context: "keep_last",
1981
- get_session_messages: "keep_last",
1982
- get_conversation_between: "keep_last"
1983
- };
1984
- function convertJsonSchemaToParameters(schema) {
1985
- const properties = {};
1986
- for (const [key, prop] of Object.entries(schema.properties)) {
1987
- properties[key] = { ...prop };
1897
+ function requireBearerToken(request, reply) {
1898
+ if (!cachedToken) return;
1899
+ const authHeader = request.headers.authorization;
1900
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
1901
+ const err = unauthorized();
1902
+ reply.code(err.statusCode).send(errorResponse(err));
1903
+ return;
1904
+ }
1905
+ const token = authHeader.slice(7);
1906
+ if (!safeTokenCompare(token, cachedToken)) {
1907
+ const err = unauthorized();
1908
+ reply.code(err.statusCode).send(errorResponse(err));
1909
+ return;
1988
1910
  }
1989
- return {
1990
- type: "object",
1991
- properties,
1992
- required: schema.required || []
1993
- };
1994
1911
  }
1995
- function adaptToolsForAgent(tools, getContext, options) {
1996
- const tokenBudget = options?.maxToolResultTokens ?? DEFAULT_MAX_TOOL_RESULT_TOKENS;
1997
- return tools.map((tool) => ({
1998
- name: tool.name,
1999
- label: tool.name,
2000
- description: tool.description,
2001
- parameters: convertJsonSchemaToParameters(tool.inputSchema),
2002
- async execute(_toolCallId, params) {
2003
- const toolParams = params && typeof params === "object" ? params : {};
2004
- const ctx = getContext();
2005
- const execCtx = {
2006
- db: ctx.db,
2007
- dataProvider: new CoreDataProvider(ctx.db),
2008
- sessionId: ctx.sessionId,
2009
- locale: ctx.locale,
2010
- segmentText: (texts, locale, options2) => batchSegmentWithFrequency(texts, locale, options2)
2011
- };
2012
- try {
2013
- const result = await tool.handler(toolParams, execCtx);
2014
- if (result.rawMessages && result.rawMessages.length > 0) {
2015
- const pipelineResult = applyPreprocessingPipeline({
2016
- rawMessages: result.rawMessages,
2017
- locale: ctx.locale,
2018
- maxToolResultTokens: tokenBudget,
2019
- truncationStrategy: TOOL_TRUNCATION_STRATEGY[tool.name] ?? "keep_last",
2020
- extraDetails: result.data ?? {},
2021
- logger: getServerAiLogger() ?? void 0
2022
- });
2023
- return { content: [{ type: "text", text: pipelineResult.text }], details: null };
2024
- }
2025
- return { content: [{ type: "text", text: result.content }], details: null };
2026
- } catch (error) {
2027
- const msg = error instanceof Error ? error.message : String(error);
2028
- return { content: [{ type: "text", text: `Error: ${msg}` }], details: null };
2029
- }
1912
+
1913
+ // src/http/server.ts
1914
+ var JSON_BODY_LIMIT = 50 * 1024 * 1024;
1915
+ function createServer() {
1916
+ const server2 = Fastify({
1917
+ logger: false,
1918
+ bodyLimit: JSON_BODY_LIMIT
1919
+ });
1920
+ server2.addHook("onRequest", authHook);
1921
+ server2.setErrorHandler((error, _request, reply) => {
1922
+ if (error instanceof ApiError) {
1923
+ reply.code(error.statusCode).send(errorResponse(error));
1924
+ return;
2030
1925
  }
2031
- }));
1926
+ if (error.statusCode === 413) {
1927
+ const bodyErr = new ApiError("BODY_TOO_LARGE" /* BODY_TOO_LARGE */, "Request body exceeds 50MB limit");
1928
+ reply.code(413).send(errorResponse(bodyErr));
1929
+ return;
1930
+ }
1931
+ const statusCode = error.statusCode;
1932
+ if (statusCode && statusCode >= 400 && statusCode < 600) {
1933
+ reply.code(statusCode).send({ success: false, error: { code: "CLIENT_ERROR", message: error.message } });
1934
+ return;
1935
+ }
1936
+ const err = serverError(error.message);
1937
+ reply.code(err.statusCode).send(errorResponse(err));
1938
+ });
1939
+ return server2;
2032
1940
  }
2033
1941
 
2034
- // src/ai/assistant-loader.ts
2035
- import * as fs7 from "fs";
2036
- import * as path8 from "path";
2037
- function loadAssistantConfig(aiDataDir, assistantId) {
2038
- const filePath = path8.join(aiDataDir, "assistants", `${assistantId}.md`);
2039
- if (!fs7.existsSync(filePath)) return null;
2040
- try {
2041
- const content = fs7.readFileSync(filePath, "utf-8");
2042
- return parseAssistantFile(content, filePath);
2043
- } catch {
2044
- return null;
2045
- }
1942
+ // src/http/routes/web/index.ts
1943
+ import * as os3 from "os";
1944
+ import * as path6 from "path";
1945
+
1946
+ // src/http/routes/web/import.ts
1947
+ import * as fs4 from "fs";
1948
+ import * as os2 from "os";
1949
+ import * as path4 from "path";
1950
+
1951
+ // src/http/routes/web/helpers.ts
1952
+ import * as fs3 from "fs";
1953
+ function resolveNativeBinding() {
1954
+ if (process.versions.electron) return void 0;
1955
+ const nativePath = resolveCliPath("native/better_sqlite3.node");
1956
+ if (fs3.existsSync(nativePath)) return nativePath;
1957
+ return void 0;
2046
1958
  }
2047
1959
 
2048
- // src/ai/manager-factory.ts
2049
- import * as fs8 from "fs";
2050
- import * as path9 from "path";
2051
- import { createHash, randomUUID as randomUUID3 } from "crypto";
2052
- var nodeFs = {
2053
- ensureDir(dir) {
2054
- if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
2055
- },
2056
- listFiles(dir, ext) {
2057
- if (!fs8.existsSync(dir)) return [];
2058
- return fs8.readdirSync(dir).filter((f) => f.endsWith(ext));
2059
- },
2060
- readFile(filePath) {
2061
- return fs8.readFileSync(filePath, "utf-8");
2062
- },
2063
- writeFile(filePath, content) {
2064
- fs8.writeFileSync(filePath, content, "utf-8");
2065
- },
2066
- deleteFile(filePath) {
2067
- if (fs8.existsSync(filePath)) fs8.unlinkSync(filePath);
2068
- },
2069
- fileExists(filePath) {
2070
- return fs8.existsSync(filePath);
2071
- },
2072
- joinPath(...parts) {
2073
- return path9.join(...parts);
2074
- }
2075
- };
2076
- var managerCache = /* @__PURE__ */ new Map();
2077
- function getAssistantManager(aiDataDir) {
2078
- let cached = managerCache.get(aiDataDir);
2079
- if (!cached) {
2080
- cached = {
2081
- assistant: new AssistantManager({
2082
- fs: nodeFs,
2083
- assistantsDir: path9.join(aiDataDir, "assistants"),
2084
- builtinRawConfigs: [],
2085
- generateId: () => `custom_${randomUUID3().replace(/-/g, "").slice(0, 12)}`
2086
- }),
2087
- skill: new SkillManagerCore({
2088
- fs: nodeFs,
2089
- skillsDir: path9.join(aiDataDir, "skills"),
2090
- builtinRawSkills: [],
2091
- contentHash: (content) => createHash("md5").update(content).digest("hex")
2092
- })
2093
- };
2094
- managerCache.set(aiDataDir, cached);
2095
- }
2096
- return cached.assistant;
2097
- }
2098
- function getSkillManagerCore(aiDataDir) {
2099
- let cached = managerCache.get(aiDataDir);
2100
- if (!cached) {
2101
- getAssistantManager(aiDataDir);
2102
- cached = managerCache.get(aiDataDir);
1960
+ // src/http/routes/web/import.ts
1961
+ var DEMO_BASE_URL = "https://chatlab.fun/assets/demo";
1962
+ function cleanupTemp(...paths) {
1963
+ for (const p of paths) {
1964
+ try {
1965
+ const stat2 = fs4.statSync(p);
1966
+ if (stat2.isDirectory()) {
1967
+ fs4.rmSync(p, { recursive: true, force: true });
1968
+ } else {
1969
+ fs4.unlinkSync(p);
1970
+ }
1971
+ } catch {
1972
+ }
2103
1973
  }
2104
- return cached.skill;
2105
1974
  }
2106
-
2107
- // src/ai/agent.ts
2108
- async function runServerAgent(options) {
2109
- const {
2110
- userMessage,
2111
- conversationId,
2112
- historyLeafMessageId,
2113
- chatType = "group",
2114
- locale = "zh-CN",
2115
- assistantSystemPrompt,
2116
- skillMenu,
2117
- compressionConfig,
2118
- tools = [],
2119
- aiDataDir,
2120
- convManager: convManager2,
2121
- onEvent,
2122
- abortSignal,
2123
- ownerInfo,
2124
- mentionedMembers,
2125
- dataSnapshot,
2126
- thinkingLevel
2127
- } = options;
2128
- const aiLogger = getServerAiLogger();
2129
- const llmConfig = getDefaultAssistantConfig(aiDataDir);
2130
- if (!llmConfig) {
2131
- onEvent({ type: "error", error: { name: "ConfigError", message: "LLM service not configured" } });
2132
- onEvent({ type: "done", isFinished: true });
2133
- return;
2134
- }
2135
- const piModel = buildPiModel2(llmConfig);
2136
- const t = createAiTranslate(locale);
2137
- let skillCtx;
2138
- if (skillMenu) {
2139
- skillCtx = { skillMenu };
2140
- }
2141
- const systemPrompt = buildSystemPrompt({
2142
- t,
2143
- chatType,
2144
- assistantSystemPrompt,
2145
- ownerInfo,
2146
- locale,
2147
- skillCtx,
2148
- mentionedMembers,
2149
- dataSnapshot
1975
+ function registerImportRoutes(server2, dbManager2) {
1976
+ server2.get("/_web/supported-formats", async () => {
1977
+ return getSupportedFormats();
2150
1978
  });
2151
- const handler = new AgentEventHandler({
2152
- onChunk: onEvent,
2153
- context: {},
2154
- systemPrompt
1979
+ server2.post("/_web/detect-format", async (request, reply) => {
1980
+ const data = await request.file();
1981
+ if (!data) return reply.code(400).send({ error: "No file uploaded" });
1982
+ const tmpDir = fs4.mkdtempSync(path4.join(os2.tmpdir(), "chatlab-detect-"));
1983
+ const tmpPath = path4.join(tmpDir, data.filename || "upload");
1984
+ try {
1985
+ const chunks = [];
1986
+ for await (const chunk of data.file) {
1987
+ chunks.push(chunk);
1988
+ }
1989
+ fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
1990
+ const format = detectFormat(tmpPath);
1991
+ const allFormats = detectAllFormats(tmpPath);
1992
+ return { format, allFormats };
1993
+ } finally {
1994
+ cleanupTemp(tmpPath, tmpDir);
1995
+ }
2155
1996
  });
2156
- if (compressionConfig?.enabled && historyLeafMessageId === void 0) {
2157
- const llmAdapter = createCompressionLlmAdapter({
2158
- piModel,
2159
- apiKey: llmConfig.apiKey,
2160
- onCompressing: () => handler.emitStatus("compressing", [])
2161
- });
2162
- const compressionResult = await checkAndCompress(
2163
- conversationId,
2164
- compressionConfig,
2165
- systemPrompt,
2166
- llmAdapter,
2167
- convManager2,
2168
- aiLogger ?? void 0
2169
- );
2170
- if (compressionResult.compressed) {
2171
- onEvent({
2172
- type: "compression_done",
2173
- compressionResult: {
2174
- summaryContent: compressionResult.summaryContent ?? "",
2175
- tokensBefore: compressionResult.tokensBefore ?? 0,
2176
- tokensAfter: compressionResult.tokensAfter ?? 0,
2177
- timestamp: Date.now()
2178
- }
2179
- });
1997
+ server2.post("/_web/scan-multi-chat", async (request, reply) => {
1998
+ const data = await request.file();
1999
+ if (!data) return reply.code(400).send({ error: "No file uploaded" });
2000
+ const tmpDir = fs4.mkdtempSync(path4.join(os2.tmpdir(), "chatlab-scan-"));
2001
+ const tmpPath = path4.join(tmpDir, data.filename || "upload");
2002
+ try {
2003
+ const chunks = [];
2004
+ for await (const chunk of data.file) {
2005
+ chunks.push(chunk);
2006
+ }
2007
+ fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
2008
+ const chats = await scanMultiChatFile(tmpPath);
2009
+ return { chats };
2010
+ } finally {
2011
+ cleanupTemp(tmpPath, tmpDir);
2180
2012
  }
2181
- } else if (compressionConfig?.enabled && historyLeafMessageId !== void 0) {
2182
- aiLogger?.info?.("Compression", "Skipping compression for edited branch request", {
2183
- conversationId,
2184
- historyLeafMessageId
2013
+ });
2014
+ server2.post("/_web/import", async (request, reply) => {
2015
+ const data = await request.file();
2016
+ if (!data) return reply.code(400).send({ error: "No file uploaded" });
2017
+ const tmpDir = fs4.mkdtempSync(path4.join(os2.tmpdir(), "chatlab-import-"));
2018
+ const tmpPath = path4.join(tmpDir, data.filename || "upload");
2019
+ const chunks = [];
2020
+ for await (const chunk of data.file) {
2021
+ chunks.push(chunk);
2022
+ }
2023
+ fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
2024
+ const formatId = data.fields?.formatId?.value;
2025
+ const chatIndexStr = data.fields?.chatIndex?.value;
2026
+ const chatIndex = chatIndexStr !== void 0 ? parseInt(chatIndexStr, 10) : void 0;
2027
+ reply.raw.writeHead(200, {
2028
+ "Content-Type": "text/event-stream",
2029
+ "Cache-Control": "no-cache",
2030
+ Connection: "keep-alive"
2185
2031
  });
2186
- }
2187
- if (abortSignal?.aborted) {
2188
- handler.emitStatus("aborted", [], { force: true });
2189
- onEvent({ type: "done", isFinished: true, usage: handler.cloneUsage() });
2190
- return;
2191
- }
2192
- let history = [];
2193
- try {
2194
- history = convManager2.getHistoryForAgent(conversationId, void 0, historyLeafMessageId);
2195
- } catch {
2196
- }
2197
- handler.emitStatus("preparing", [], { pendingUserMessage: userMessage, force: true });
2198
- const steerMessage = t("ai.agent.answerWithoutTools");
2199
- let cachedMessages = [];
2200
- try {
2201
- const result = await runAgentCore({
2202
- piModel,
2203
- apiKey: llmConfig.apiKey,
2204
- systemPrompt,
2205
- tools,
2206
- history,
2207
- userMessage,
2208
- maxToolRounds: 5,
2209
- abortSignal,
2210
- steerMessage,
2211
- thinkingLevel,
2212
- onConvertToLlm: (filteredMessages) => {
2213
- cachedMessages = filteredMessages;
2214
- },
2215
- onEvent: (coreEvent) => handler.handleCoreEvent(coreEvent, cachedMessages),
2216
- onDebugContext: (messages) => {
2217
- try {
2218
- convManager2.setPendingDebugContext(conversationId, JSON.stringify(messages, null, 2));
2219
- } catch {
2220
- }
2032
+ function sendEvent(event, eventData) {
2033
+ reply.raw.write(`event: ${event}
2034
+ data: ${JSON.stringify(eventData)}
2035
+
2036
+ `);
2037
+ }
2038
+ try {
2039
+ const nativeBinding = resolveNativeBinding();
2040
+ const result = await streamImport(dbManager2, tmpPath, {
2041
+ formatId,
2042
+ chatIndex,
2043
+ nativeBinding,
2044
+ onProgress: (p) => sendEvent("progress", p)
2045
+ });
2046
+ if (result.success) {
2047
+ sendEvent("done", {
2048
+ success: true,
2049
+ sessionId: result.sessionId,
2050
+ messageCount: result.diagnostics?.messagesWritten ?? 0,
2051
+ memberCount: 0
2052
+ });
2053
+ } else {
2054
+ sendEvent("error", { success: false, error: result.error });
2221
2055
  }
2222
- });
2223
- if (result.error) {
2224
- const friendlyMessage = formatAIError(result.error);
2225
- onEvent({ type: "error", error: { name: "AgentError", message: friendlyMessage } });
2226
- }
2227
- handler.emitStatus("completed", cachedMessages, { force: true });
2228
- onEvent({ type: "done", isFinished: true, usage: result.usage });
2229
- } catch (error) {
2230
- const friendlyMessage = formatAIError(error);
2231
- aiLogger?.error("ServerAgent", "Agent execution error", { error: String(error) });
2232
- handler.emitStatus("error", cachedMessages, { force: true });
2233
- onEvent({ type: "error", error: { name: "AgentError", message: friendlyMessage } });
2234
- onEvent({ type: "done", isFinished: true, usage: handler.cloneUsage() });
2235
- }
2236
- }
2237
-
2238
- // src/ai/custom-store.ts
2239
- import * as fs9 from "fs";
2240
- import * as path10 from "path";
2241
- import { randomUUID as randomUUID4 } from "crypto";
2242
- function readProviders(aiDataDir) {
2243
- const storePath = path10.join(aiDataDir, "custom-providers.json");
2244
- if (!fs9.existsSync(storePath)) return [];
2245
- try {
2246
- const content = fs9.readFileSync(storePath, "utf-8");
2247
- return JSON.parse(content);
2248
- } catch {
2249
- return [];
2250
- }
2251
- }
2252
- function writeProviders(aiDataDir, providers) {
2253
- const storePath = path10.join(aiDataDir, "custom-providers.json");
2254
- const dir = path10.dirname(storePath);
2255
- if (!fs9.existsSync(dir)) fs9.mkdirSync(dir, { recursive: true });
2256
- fs9.writeFileSync(storePath, JSON.stringify(providers, null, 2), "utf-8");
2257
- }
2258
- function addCustomProvider(aiDataDir, input) {
2259
- const providers = readProviders(aiDataDir);
2260
- const newProvider = {
2261
- ...input,
2262
- id: `custom:${randomUUID4()}`,
2263
- builtin: false,
2264
- enabledByDefault: false
2265
- };
2266
- providers.push(newProvider);
2267
- writeProviders(aiDataDir, providers);
2268
- return newProvider;
2269
- }
2270
- function updateCustomProvider(aiDataDir, id, updates) {
2271
- const providers = readProviders(aiDataDir);
2272
- const index = providers.findIndex((p) => p.id === id);
2273
- if (index === -1) return { success: false, error: "Custom provider not found" };
2274
- providers[index] = { ...providers[index], ...updates };
2275
- writeProviders(aiDataDir, providers);
2276
- return { success: true };
2277
- }
2278
- function deleteCustomProvider(aiDataDir, id) {
2279
- const providers = readProviders(aiDataDir);
2280
- const index = providers.findIndex((p) => p.id === id);
2281
- if (index === -1) return { success: false, error: "Custom provider not found" };
2282
- providers.splice(index, 1);
2283
- writeProviders(aiDataDir, providers);
2284
- return { success: true };
2285
- }
2286
- function readModels(aiDataDir) {
2287
- const storePath = path10.join(aiDataDir, "custom-models.json");
2288
- if (!fs9.existsSync(storePath)) return [];
2289
- try {
2290
- const content = fs9.readFileSync(storePath, "utf-8");
2291
- return JSON.parse(content);
2292
- } catch {
2293
- return [];
2294
- }
2295
- }
2296
- function writeModels(aiDataDir, models) {
2297
- const storePath = path10.join(aiDataDir, "custom-models.json");
2298
- const dir = path10.dirname(storePath);
2299
- if (!fs9.existsSync(dir)) fs9.mkdirSync(dir, { recursive: true });
2300
- fs9.writeFileSync(storePath, JSON.stringify(models, null, 2), "utf-8");
2301
- }
2302
- function addCustomModel(aiDataDir, input) {
2303
- const models = readModels(aiDataDir);
2304
- const existing = models.find((m) => m.id === input.id && m.providerId === input.providerId);
2305
- if (existing) {
2306
- return { success: false, error: `Model "${input.id}" already exists under provider "${input.providerId}"` };
2307
- }
2308
- const newModel = {
2309
- ...input,
2310
- builtin: false,
2311
- editable: true
2312
- };
2313
- models.push(newModel);
2314
- writeModels(aiDataDir, models);
2315
- return { success: true, model: newModel };
2316
- }
2317
- function updateCustomModel(aiDataDir, providerId, modelId, updates) {
2318
- const models = readModels(aiDataDir);
2319
- const index = models.findIndex((m) => m.id === modelId && m.providerId === providerId);
2320
- if (index === -1) return { success: false, error: "Custom model not found" };
2321
- models[index] = { ...models[index], ...updates };
2322
- writeModels(aiDataDir, models);
2323
- return { success: true };
2324
- }
2325
- function deleteCustomModel(aiDataDir, providerId, modelId) {
2326
- const models = readModels(aiDataDir);
2327
- const index = models.findIndex((m) => m.id === modelId && m.providerId === providerId);
2328
- if (index === -1) return { success: false, error: "Custom model not found" };
2329
- models.splice(index, 1);
2330
- writeModels(aiDataDir, models);
2331
- return { success: true };
2332
- }
2333
-
2334
- // src/http/routes/ai-config-display.ts
2335
- var NO_KEY_PLACEHOLDER = "sk-no-key-required";
2336
- function hasRealApiKey(apiKey) {
2337
- return !!apiKey && apiKey !== NO_KEY_PLACEHOLDER;
2338
- }
2339
- function toLlmConfigDisplay(config, resolveApiKey2) {
2340
- const { apiKey: _rawApiKey, ...rest } = config;
2341
- const provider = typeof config.provider === "string" ? config.provider : "";
2342
- const authProfile = typeof config.authProfile === "string" ? config.authProfile : void 0;
2343
- const resolvedApiKey = provider ? resolveApiKey2(provider, authProfile) : "";
2344
- return {
2345
- ...rest,
2346
- apiKey: "",
2347
- apiKeySet: hasRealApiKey(resolvedApiKey || "")
2348
- };
2349
- }
2350
-
2351
- // src/http/routes/ai.ts
2352
- function getAiDir(dbManager2) {
2353
- const pathProvider = dbManager2["pathProvider"];
2354
- if (!pathProvider) {
2355
- throw Object.assign(new Error("PathProvider not available"), { statusCode: 500 });
2356
- }
2357
- return pathProvider.getAiDataDir();
2358
- }
2359
- function registerAiRoutes(server2, dbManager2, convManager2) {
2360
- server2.get("/_web/ai/assistants", async () => {
2361
- const mgr = getAssistantManager(getAiDir(dbManager2));
2362
- return mgr.getAllAssistants();
2363
- });
2364
- server2.get("/_web/ai/assistants/:id", async (request, reply) => {
2365
- const mgr = getAssistantManager(getAiDir(dbManager2));
2366
- const config = mgr.getAssistantConfig(request.params.id);
2367
- if (!config) return reply.code(404).send({ error: "Not found" });
2368
- return config;
2369
- });
2370
- server2.post("/_web/ai/assistants", async (request) => {
2371
- const mgr = getAssistantManager(getAiDir(dbManager2));
2372
- return mgr.createAssistant(request.body);
2373
- });
2374
- server2.put("/_web/ai/assistants/:id", async (request, reply) => {
2375
- const mgr = getAssistantManager(getAiDir(dbManager2));
2376
- const result = mgr.updateAssistant(request.params.id, request.body);
2377
- if (!result.success) return reply.code(404).send(result);
2378
- return result;
2379
- });
2380
- server2.delete("/_web/ai/assistants/:id", async (request, reply) => {
2381
- const mgr = getAssistantManager(getAiDir(dbManager2));
2382
- const result = mgr.deleteAssistant(request.params.id);
2383
- if (!result.success) return reply.code(400).send(result);
2384
- return result;
2385
- });
2386
- server2.post("/_web/ai/assistants/:id/reset", async (request, reply) => {
2387
- const mgr = getAssistantManager(getAiDir(dbManager2));
2388
- const result = mgr.resetAssistant(request.params.id);
2389
- if (!result.success) return reply.code(400).send(result);
2390
- return result;
2391
- });
2392
- server2.post("/_web/ai/assistants/import", async (request) => {
2393
- const mgr = getAssistantManager(getAiDir(dbManager2));
2394
- return mgr.importAssistantFromMd(request.body.rawMd);
2395
- });
2396
- server2.get("/_web/ai/skills", async () => {
2397
- const mgr = getSkillManagerCore(getAiDir(dbManager2));
2398
- return mgr.getAllSkills();
2399
- });
2400
- server2.get("/_web/ai/skills/:id", async (request, reply) => {
2401
- const mgr = getSkillManagerCore(getAiDir(dbManager2));
2402
- const config = mgr.getSkillConfig(request.params.id);
2403
- if (!config) return reply.code(404).send({ error: "Not found" });
2404
- return config;
2405
- });
2406
- server2.post("/_web/ai/skills", async (request) => {
2407
- const mgr = getSkillManagerCore(getAiDir(dbManager2));
2408
- return mgr.createSkill(request.body.rawMd);
2409
- });
2410
- server2.put("/_web/ai/skills/:id", async (request, reply) => {
2411
- const mgr = getSkillManagerCore(getAiDir(dbManager2));
2412
- const result = mgr.updateSkill(request.params.id, request.body.rawMd);
2413
- if (!result.success) return reply.code(404).send(result);
2414
- return result;
2415
- });
2416
- server2.delete("/_web/ai/skills/:id", async (request, reply) => {
2417
- const mgr = getSkillManagerCore(getAiDir(dbManager2));
2418
- const result = mgr.deleteSkill(request.params.id);
2419
- if (!result.success) return reply.code(400).send(result);
2420
- return result;
2421
- });
2422
- server2.post("/_web/ai/skills/import", async (request) => {
2423
- const mgr = getSkillManagerCore(getAiDir(dbManager2));
2424
- return mgr.importSkillFromMd(request.body.rawMd);
2425
- });
2426
- server2.get("/_web/ai/skills/builtin-catalog", async () => {
2427
- const mgr = getSkillManagerCore(getAiDir(dbManager2));
2428
- return mgr.getBuiltinCatalog();
2429
- });
2430
- server2.get("/_web/ai/llm/has-config", async () => {
2431
- const configPath = path11.join(getAiDir(dbManager2), "llm-config.json");
2432
- if (!fs10.existsSync(configPath)) return false;
2433
- try {
2434
- const data = JSON.parse(fs10.readFileSync(configPath, "utf-8"));
2435
- if (!data.configs || !Array.isArray(data.configs) || data.configs.length === 0) return false;
2436
- return data.defaultAssistant != null;
2437
- } catch {
2438
- return false;
2056
+ } catch (err) {
2057
+ sendEvent("error", { success: false, error: err instanceof Error ? err.message : String(err) });
2058
+ } finally {
2059
+ reply.raw.end();
2060
+ cleanupTemp(tmpPath, tmpDir);
2439
2061
  }
2440
2062
  });
2441
- server2.get("/_web/ai/llm/configs", async () => {
2442
- const configPath = path11.join(getAiDir(dbManager2), "llm-config.json");
2443
- if (!fs10.existsSync(configPath)) return { configs: [], defaultAssistant: null, fastModel: null };
2063
+ server2.post("/_web/import-directory", async (request, reply) => {
2064
+ const parts = request.parts();
2065
+ if (!parts) return reply.code(400).send({ error: "No files uploaded" });
2066
+ const tmpDir = fs4.mkdtempSync(path4.join(os2.tmpdir(), "chatlab-dir-import-"));
2067
+ const relativePaths = [];
2068
+ const fileBuffers = [];
2444
2069
  try {
2445
- const data = JSON.parse(fs10.readFileSync(configPath, "utf-8"));
2446
- const configs = (data.configs || []).map((c) => toLlmConfigDisplay(c, resolveApiKey));
2447
- return {
2448
- configs,
2449
- defaultAssistant: data.defaultAssistant ?? null,
2450
- fastModel: data.fastModel ?? null
2451
- };
2452
- } catch {
2453
- return { configs: [], defaultAssistant: null, fastModel: null };
2454
- }
2455
- });
2456
- server2.get("/_web/ai/llm/providers", async () => {
2457
- return BUILTIN_PROVIDERS.map((p) => {
2458
- const models = getBuiltinModelsByProvider(p.id);
2459
- return {
2460
- id: p.id,
2461
- name: p.name,
2462
- defaultBaseUrl: p.defaultBaseUrl,
2463
- models: models.filter((m) => !m.capabilities.includes("embedding") && !m.capabilities.includes("ranking")).map((m) => ({ id: m.id, name: m.name, description: m.description }))
2070
+ let sendEvent2 = function(event, eventData) {
2071
+ reply.raw.write(`event: ${event}
2072
+ data: ${JSON.stringify(eventData)}
2073
+
2074
+ `);
2464
2075
  };
2465
- });
2466
- });
2467
- server2.get("/_web/ai/llm/provider-registry", async () => {
2468
- const customPath = path11.join(getAiDir(dbManager2), "custom-providers.json");
2469
- let custom = [];
2470
- if (fs10.existsSync(customPath)) {
2471
- try {
2472
- custom = JSON.parse(fs10.readFileSync(customPath, "utf-8"));
2473
- if (!Array.isArray(custom)) custom = [];
2474
- } catch {
2475
- custom = [];
2076
+ var sendEvent = sendEvent2;
2077
+ for await (const part of parts) {
2078
+ if (part.type === "field" && part.fieldname === "relativePaths") {
2079
+ relativePaths.push(String(part.value));
2080
+ } else if (part.type === "file") {
2081
+ const chunks = [];
2082
+ for await (const chunk of part.file) {
2083
+ chunks.push(chunk);
2084
+ }
2085
+ fileBuffers.push({ data: Buffer.concat(chunks), filename: part.filename || "" });
2086
+ }
2476
2087
  }
2477
- }
2478
- return [...BUILTIN_PROVIDERS, ...custom];
2479
- });
2480
- server2.get("/_web/ai/llm/model-catalog", async () => {
2481
- const customPath = path11.join(getAiDir(dbManager2), "custom-models.json");
2482
- let custom = [];
2483
- if (fs10.existsSync(customPath)) {
2484
- try {
2485
- custom = JSON.parse(fs10.readFileSync(customPath, "utf-8"));
2486
- if (!Array.isArray(custom)) custom = [];
2487
- } catch {
2488
- custom = [];
2088
+ for (let i = 0; i < fileBuffers.length; i++) {
2089
+ let relPath = relativePaths[i] || fileBuffers[i].filename || `file_${i}`;
2090
+ const segments = relPath.split("/");
2091
+ if (segments.length > 1) {
2092
+ relPath = segments.slice(1).join("/");
2093
+ }
2094
+ const targetPath = path4.resolve(tmpDir, relPath);
2095
+ if (!targetPath.startsWith(tmpDir + path4.sep)) continue;
2096
+ fs4.mkdirSync(path4.dirname(targetPath), { recursive: true });
2097
+ fs4.writeFileSync(targetPath, fileBuffers[i].data);
2489
2098
  }
2099
+ const entryPath = findEntryFileInDirectory(tmpDir);
2100
+ if (!entryPath) {
2101
+ return reply.code(400).send({ error: "No recognizable import format found in directory" });
2102
+ }
2103
+ reply.raw.writeHead(200, {
2104
+ "Content-Type": "text/event-stream",
2105
+ "Cache-Control": "no-cache",
2106
+ Connection: "keep-alive"
2107
+ });
2108
+ const nativeBinding = resolveNativeBinding();
2109
+ const result = await streamImport(dbManager2, entryPath, {
2110
+ nativeBinding,
2111
+ onProgress: (p) => sendEvent2("progress", p)
2112
+ });
2113
+ if (result.success) {
2114
+ sendEvent2("done", {
2115
+ success: true,
2116
+ sessionId: result.sessionId,
2117
+ messageCount: result.diagnostics?.messagesWritten ?? 0,
2118
+ memberCount: 0
2119
+ });
2120
+ } else {
2121
+ sendEvent2("error", { success: false, error: result.error });
2122
+ }
2123
+ } catch (err) {
2124
+ if (!reply.raw.headersSent) {
2125
+ return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
2126
+ }
2127
+ reply.raw.write(
2128
+ `event: error
2129
+ data: ${JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) })}
2130
+
2131
+ `
2132
+ );
2133
+ } finally {
2134
+ reply.raw.end();
2135
+ cleanupTemp(tmpDir);
2490
2136
  }
2491
- return [...BUILTIN_MODELS, ...custom];
2492
- });
2493
- server2.get("/_web/ai/llm/default-assistant-slot", async () => {
2494
- const configPath = path11.join(getAiDir(dbManager2), "llm-config.json");
2495
- if (!fs10.existsSync(configPath)) return null;
2496
- try {
2497
- const data = JSON.parse(fs10.readFileSync(configPath, "utf-8"));
2498
- return data.defaultAssistant ?? null;
2499
- } catch {
2500
- return null;
2501
- }
2502
- });
2503
- server2.get("/_web/ai/llm/fast-model-slot", async () => {
2504
- const configPath = path11.join(getAiDir(dbManager2), "llm-config.json");
2505
- if (!fs10.existsSync(configPath)) return null;
2506
- try {
2507
- const data = JSON.parse(fs10.readFileSync(configPath, "utf-8"));
2508
- return data.fastModel ?? null;
2509
- } catch {
2510
- return null;
2511
- }
2512
- });
2513
- server2.post("/_web/ai/llm/configs", async (request) => {
2514
- const aiDataDir = getAiDir(dbManager2);
2515
- return addLlmConfig(aiDataDir, request.body);
2516
- });
2517
- server2.put("/_web/ai/llm/configs/:id", async (request) => {
2518
- const aiDataDir = getAiDir(dbManager2);
2519
- return updateLlmConfig(aiDataDir, request.params.id, request.body);
2520
- });
2521
- server2.delete("/_web/ai/llm/configs/:id", async (request) => {
2522
- const aiDataDir = getAiDir(dbManager2);
2523
- return deleteLlmConfig(aiDataDir, request.params.id);
2524
- });
2525
- server2.put("/_web/ai/llm/default-assistant-slot", async (request) => {
2526
- const aiDataDir = getAiDir(dbManager2);
2527
- return setDefaultAssistantSlot(aiDataDir, request.body.configId, request.body.modelId);
2528
- });
2529
- server2.put("/_web/ai/llm/fast-model-slot", async (request) => {
2530
- const aiDataDir = getAiDir(dbManager2);
2531
- return setFastModelSlot(aiDataDir, request.body);
2532
- });
2533
- server2.post("/_web/ai/llm/custom-providers", async (request) => {
2534
- const aiDataDir = getAiDir(dbManager2);
2535
- return addCustomProvider(aiDataDir, {
2536
- ...request.body,
2537
- kind: request.body.kind || "openai-compatible",
2538
- authMode: "api-key",
2539
- supportsCustomModels: request.body.supportsCustomModels ?? true,
2540
- modelIds: request.body.modelIds ?? []
2541
- });
2542
- });
2543
- server2.put("/_web/ai/llm/custom-providers/:id", async (request) => {
2544
- const aiDataDir = getAiDir(dbManager2);
2545
- return updateCustomProvider(aiDataDir, request.params.id, request.body);
2546
- });
2547
- server2.delete("/_web/ai/llm/custom-providers/:id", async (request) => {
2548
- const aiDataDir = getAiDir(dbManager2);
2549
- return deleteCustomProvider(aiDataDir, request.params.id);
2550
- });
2551
- server2.post("/_web/ai/llm/custom-models", async (request) => {
2552
- const aiDataDir = getAiDir(dbManager2);
2553
- return addCustomModel(aiDataDir, {
2554
- ...request.body,
2555
- capabilities: request.body.capabilities ?? ["chat"],
2556
- recommendedFor: request.body.recommendedFor ?? ["chat"],
2557
- status: request.body.status ?? "stable"
2558
- });
2559
- });
2560
- server2.put("/_web/ai/llm/custom-models/:providerId/:modelId", async (request) => {
2561
- const aiDataDir = getAiDir(dbManager2);
2562
- return updateCustomModel(aiDataDir, request.params.providerId, request.params.modelId, request.body);
2563
- });
2564
- server2.delete("/_web/ai/llm/custom-models/:providerId/:modelId", async (request) => {
2565
- const aiDataDir = getAiDir(dbManager2);
2566
- return deleteCustomModel(aiDataDir, request.params.providerId, request.params.modelId);
2567
- });
2568
- server2.post("/_web/ai/llm/validate-key", async (request) => {
2569
- const { provider, apiKey, baseUrl, model, apiFormat } = request.body;
2570
- return validateApiKey(provider, apiKey, baseUrl, model, apiFormat);
2571
- });
2572
- server2.post("/_web/ai/llm/remote-models", async (request) => {
2573
- const { provider, apiKey, baseUrl, apiFormat } = request.body;
2574
- return fetchRemoteModels(provider, apiKey, baseUrl, apiFormat);
2575
2137
  });
2576
- server2.post("/_web/ai/llm/chat-stream", async (request, reply) => {
2577
- const { messages, options } = request.body;
2578
- const aiDataDir = getAiDir(dbManager2);
2579
- const llmConfig = getDefaultAssistantConfig(aiDataDir);
2580
- if (!llmConfig) {
2581
- return reply.code(400).send({ success: false, error: "LLM service not configured" });
2138
+ server2.post("/_web/sessions/:id/import/incremental", async (request, reply) => {
2139
+ const sessionId = request.params.id;
2140
+ const data = await request.file();
2141
+ if (!data) return reply.code(400).send({ error: "No file uploaded" });
2142
+ const tmpDir = fs4.mkdtempSync(path4.join(os2.tmpdir(), "chatlab-inc-"));
2143
+ const tmpPath = path4.join(tmpDir, data.filename || "upload");
2144
+ const chunks = [];
2145
+ for await (const chunk of data.file) {
2146
+ chunks.push(chunk);
2582
2147
  }
2583
- const piModel = buildPiModel2(llmConfig);
2148
+ fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
2584
2149
  reply.raw.writeHead(200, {
2585
2150
  "Content-Type": "text/event-stream",
2586
2151
  "Cache-Control": "no-cache",
2587
2152
  Connection: "keep-alive"
2588
2153
  });
2589
- const sendChunk = (data) => {
2590
- reply.raw.write(`event: chunk
2591
- data: ${JSON.stringify(data)}
2154
+ function sendEvent(event, eventData) {
2155
+ reply.raw.write(`event: ${event}
2156
+ data: ${JSON.stringify(eventData)}
2592
2157
 
2593
2158
  `);
2594
- };
2159
+ }
2595
2160
  try {
2596
- await runSimpleLlmStream({
2597
- messages,
2598
- apiKey: llmConfig.apiKey,
2599
- piModel,
2600
- temperature: options?.temperature,
2601
- maxTokens: options?.maxTokens,
2602
- onChunk: sendChunk
2161
+ const result = await incrementalImport(dbManager2, sessionId, tmpPath, {
2162
+ onProgress: (p) => sendEvent("progress", p)
2603
2163
  });
2604
- } catch (error) {
2605
- const msg = error instanceof Error ? error.message : String(error);
2606
- sendChunk({ content: "", isFinished: true, finishReason: "error", error: msg });
2164
+ if (result.success) {
2165
+ sendEvent("done", result);
2166
+ } else {
2167
+ sendEvent("error", { success: false, error: result.error });
2168
+ }
2169
+ } catch (err) {
2170
+ sendEvent("error", { success: false, error: err instanceof Error ? err.message : String(err) });
2171
+ } finally {
2172
+ reply.raw.end();
2173
+ cleanupTemp(tmpPath, tmpDir);
2607
2174
  }
2608
- reply.raw.end();
2609
- });
2610
- server2.get("/_web/ai/tools/catalog", async () => {
2611
- return BUILTIN_TOOL_CATALOG;
2612
- });
2613
- if (!convManager2) return;
2614
- server2.post("/_web/ai/conversations", async (request) => {
2615
- const { sessionId, title, assistantId } = request.body;
2616
- return convManager2.createConversation(sessionId, title, assistantId);
2617
- });
2618
- server2.get("/_web/ai/conversations", async (request) => {
2619
- const { sessionId } = request.query;
2620
- if (!sessionId) return [];
2621
- return convManager2.getConversations(sessionId);
2622
- });
2623
- server2.get("/_web/ai/conversations/:id", async (request, reply) => {
2624
- const conv = convManager2.getConversation(request.params.id);
2625
- if (!conv) return reply.code(404).send({ error: "Conversation not found" });
2626
- return conv;
2627
2175
  });
2628
- server2.put("/_web/ai/conversations/:id/title", async (request) => {
2629
- return convManager2.updateConversationTitle(request.params.id, request.body.title);
2176
+ server2.post("/_web/sessions/:id/import/incremental/analyze", async (request, reply) => {
2177
+ const sessionId = request.params.id;
2178
+ const data = await request.file();
2179
+ if (!data) return reply.code(400).send({ error: "No file uploaded" });
2180
+ const tmpDir = fs4.mkdtempSync(path4.join(os2.tmpdir(), "chatlab-analyze-"));
2181
+ const tmpPath = path4.join(tmpDir, data.filename || "upload");
2182
+ const chunks = [];
2183
+ for await (const chunk of data.file) {
2184
+ chunks.push(chunk);
2185
+ }
2186
+ fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
2187
+ try {
2188
+ return await analyzeIncrementalImport(dbManager2, sessionId, tmpPath);
2189
+ } catch (err) {
2190
+ return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
2191
+ } finally {
2192
+ cleanupTemp(tmpPath, tmpDir);
2193
+ }
2630
2194
  });
2631
- server2.delete("/_web/ai/conversations/:id", async (request) => {
2632
- return convManager2.deleteConversation(request.params.id);
2195
+ server2.post("/_web/import/analyze", async (request, reply) => {
2196
+ const data = await request.file();
2197
+ if (!data) return reply.code(400).send({ error: "No file uploaded" });
2198
+ const tmpDir = fs4.mkdtempSync(path4.join(os2.tmpdir(), "chatlab-analyze-"));
2199
+ const tmpPath = path4.join(tmpDir, data.filename || "upload");
2200
+ const chunks = [];
2201
+ for await (const chunk of data.file) {
2202
+ chunks.push(chunk);
2203
+ }
2204
+ fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
2205
+ try {
2206
+ return await analyzeNewImport(tmpPath);
2207
+ } catch (err) {
2208
+ return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
2209
+ } finally {
2210
+ cleanupTemp(tmpPath, tmpDir);
2211
+ }
2633
2212
  });
2634
- server2.post("/_web/ai/conversations/:id/messages", async (request) => {
2635
- const { role, content, dataKeywords, dataMessageCount, contentBlocks, tokenUsage } = request.body;
2636
- return convManager2.addMessage(
2637
- request.params.id,
2638
- role,
2639
- content,
2640
- dataKeywords,
2641
- dataMessageCount,
2642
- contentBlocks,
2643
- tokenUsage
2644
- );
2213
+ const DEMO_FILES = [
2214
+ "demo-group.json",
2215
+ "demo-private-A-cuilan.json",
2216
+ "demo-private-B-wukong.json",
2217
+ "demo-private-C-spider.json"
2218
+ ];
2219
+ server2.post("/_web/demo/import", async (request, reply) => {
2220
+ const locale = request.body?.locale || "en";
2221
+ const nativeBinding = resolveNativeBinding();
2222
+ const total = DEMO_FILES.length;
2223
+ reply.raw.writeHead(200, {
2224
+ "Content-Type": "text/event-stream",
2225
+ "Cache-Control": "no-cache",
2226
+ Connection: "keep-alive"
2227
+ });
2228
+ function sendEvent(event, eventData) {
2229
+ reply.raw.write(`event: ${event}
2230
+ data: ${JSON.stringify(eventData)}
2231
+
2232
+ `);
2233
+ }
2234
+ const tmpDir = fs4.mkdtempSync(path4.join(os2.tmpdir(), "chatlab-demo-"));
2235
+ try {
2236
+ const localPaths = [];
2237
+ for (let i = 0; i < total; i++) {
2238
+ sendEvent("progress", { stage: "downloading", current: i + 1, total });
2239
+ const localPath = path4.join(tmpDir, DEMO_FILES[i]);
2240
+ const resp = await fetch(`${DEMO_BASE_URL}/${locale}/${DEMO_FILES[i]}`, {
2241
+ signal: AbortSignal.timeout(6e4)
2242
+ });
2243
+ if (!resp.ok) throw new Error(`Download demo failed (${DEMO_FILES[i]}): ${resp.status}`);
2244
+ fs4.writeFileSync(localPath, Buffer.from(await resp.arrayBuffer()));
2245
+ localPaths.push(localPath);
2246
+ }
2247
+ sendEvent("progress", { stage: "importing", current: 1, total });
2248
+ const groupResult = await streamImport(dbManager2, localPaths[0], { nativeBinding });
2249
+ if (!groupResult.success) throw new Error(groupResult.error || "Failed to import group demo");
2250
+ const privateSessionIds = [];
2251
+ for (let i = 1; i < localPaths.length; i++) {
2252
+ sendEvent("progress", { stage: "importing", current: i + 1, total });
2253
+ const result = await streamImport(dbManager2, localPaths[i], { nativeBinding });
2254
+ if (!result.success) throw new Error(result.error || `Failed to import private demo: ${DEMO_FILES[i]}`);
2255
+ if (result.sessionId) privateSessionIds.push(result.sessionId);
2256
+ }
2257
+ sendEvent("progress", { stage: "done", current: total, total });
2258
+ sendEvent("result", {
2259
+ success: true,
2260
+ groupSessionId: groupResult.sessionId,
2261
+ privateSessionIds
2262
+ });
2263
+ } catch (err) {
2264
+ const message = err instanceof Error ? err.message : String(err);
2265
+ sendEvent("progress", { stage: "error", current: 0, total, message });
2266
+ sendEvent("result", { success: false, error: message });
2267
+ } finally {
2268
+ reply.raw.end();
2269
+ cleanupTemp(tmpDir);
2270
+ }
2645
2271
  });
2646
- server2.get("/_web/ai/conversations/:id/messages", async (request) => {
2647
- return convManager2.getMessages(request.params.id);
2272
+ }
2273
+
2274
+ // src/http/routes/web/cache.ts
2275
+ import * as path5 from "path";
2276
+ import { execFile } from "child_process";
2277
+ function runExecFile(file, args, runner = execFile) {
2278
+ return new Promise((resolve4, reject) => {
2279
+ runner(file, args, (error) => {
2280
+ if (error) {
2281
+ reject(error);
2282
+ return;
2283
+ }
2284
+ resolve4();
2285
+ });
2648
2286
  });
2649
- server2.post("/_web/ai/conversations/:id/messages/:messageId/delete-from", async (request, reply) => {
2650
- const { id, messageId } = request.params;
2651
- if (!id || typeof id !== "string" || !messageId || typeof messageId !== "string") {
2652
- return reply.code(400).send({ error: "conversation id and messageId are required" });
2287
+ }
2288
+ async function openDirectoryPath(dirPath, platform = process.platform, runner) {
2289
+ if (platform === "darwin") {
2290
+ await runExecFile("open", [dirPath], runner);
2291
+ } else if (platform === "win32") {
2292
+ await runExecFile("explorer.exe", [dirPath], runner);
2293
+ } else {
2294
+ await runExecFile("xdg-open", [dirPath], runner);
2295
+ }
2296
+ }
2297
+ async function showPathInFolder(filePath, platform = process.platform, runner) {
2298
+ if (platform === "darwin") {
2299
+ await runExecFile("open", ["-R", filePath], runner);
2300
+ } else if (platform === "win32") {
2301
+ await runExecFile("explorer.exe", [`/select,${filePath}`], runner);
2302
+ } else {
2303
+ await runExecFile("xdg-open", [path5.dirname(filePath)], runner);
2304
+ }
2305
+ }
2306
+
2307
+ // src/http/routes/web/index.ts
2308
+ function isNewerVersion(latest, current) {
2309
+ const parse = (v) => {
2310
+ const [core, pre] = v.split("-", 2);
2311
+ const parts = core.split(".").map(Number);
2312
+ return { major: parts[0] || 0, minor: parts[1] || 0, patch: parts[2] || 0, pre };
2313
+ };
2314
+ const l = parse(latest);
2315
+ const c = parse(current);
2316
+ if (l.major !== c.major) return l.major > c.major;
2317
+ if (l.minor !== c.minor) return l.minor > c.minor;
2318
+ if (l.patch !== c.patch) return l.patch > c.patch;
2319
+ if (c.pre && !l.pre) return true;
2320
+ return false;
2321
+ }
2322
+ function registerWebRoutes(server2, dbManager2, options) {
2323
+ const adapter = createDatabaseManagerAdapter(dbManager2);
2324
+ const mergeCache = options?.pathProvider ? new MergeSessionCache(options.pathProvider, { nativeBinding: options.nativeBinding }) : null;
2325
+ mergeCache?.cleanupOrphans();
2326
+ const fallbackPathProvider = {
2327
+ getSystemDir: () => path6.join(os3.homedir(), ".chatlab"),
2328
+ getUserDataDir: () => path6.join(os3.homedir(), ".chatlab", "data"),
2329
+ getDatabaseDir: () => path6.join(os3.homedir(), ".chatlab", "data", "databases"),
2330
+ getAiDataDir: () => path6.join(os3.homedir(), ".chatlab", "ai"),
2331
+ getSettingsDir: () => path6.join(os3.homedir(), ".chatlab", "settings"),
2332
+ getCacheDir: () => path6.join(os3.homedir(), ".chatlab", "cache"),
2333
+ getTempDir: () => path6.join(os3.homedir(), ".chatlab", "temp"),
2334
+ getLogsDir: () => path6.join(os3.homedir(), ".chatlab", "logs"),
2335
+ getDownloadsDir: () => path6.join(os3.homedir(), "Downloads")
2336
+ };
2337
+ const resolvedPathProvider = options?.pathProvider ?? fallbackPathProvider;
2338
+ const ai = options?.aiContext;
2339
+ const cliStreamImport = async (dm, filePath) => {
2340
+ const { streamImport: streamImport2 } = await import("./stream-import-RDDB3N7B.mjs");
2341
+ const result = await streamImport2(dm, filePath);
2342
+ if (!result.sessionId) throw new Error("Import succeeded but no sessionId returned");
2343
+ return { sessionId: result.sessionId };
2344
+ };
2345
+ const defaultUserDataDir = getDefaultNodeUserDataDir();
2346
+ const isCustom = path6.resolve(resolvedPathProvider.getUserDataDir()) !== path6.resolve(defaultUserDataDir);
2347
+ registerSharedRoutes(
2348
+ server2,
2349
+ {
2350
+ dbManager: dbManager2,
2351
+ sessionAdapter: adapter,
2352
+ pathProvider: resolvedPathProvider,
2353
+ getVersion,
2354
+ nativeBinding: options?.nativeBinding,
2355
+ openDirectory: openDirectoryPath,
2356
+ showInFolder: showPathInFolder,
2357
+ downloadsDir: resolvedPathProvider.getDownloadsDir(),
2358
+ defaultUserDataDir,
2359
+ isCustomDataDir: isCustom,
2360
+ canSetDataDir: !process.env.CHATLAB_DATA_DIR,
2361
+ getPendingDataDirMigration: () => getPendingNodeDataDirMigration(resolvedPathProvider.getSystemDir()),
2362
+ setDataDir: (dirPath, migrate) => createNodeDataDirSwitch({
2363
+ systemDir: resolvedPathProvider.getSystemDir(),
2364
+ currentDir: resolvedPathProvider.getUserDataDir(),
2365
+ targetDir: dirPath,
2366
+ defaultDir: defaultUserDataDir,
2367
+ migrate,
2368
+ envDataDir: process.env.CHATLAB_DATA_DIR
2369
+ }),
2370
+ ...mergeCache && {
2371
+ mergeSessionCache: mergeCache,
2372
+ streamImport: cliStreamImport
2373
+ },
2374
+ ...ai && {
2375
+ aiDataDir: ai.aiDataDir,
2376
+ conversationManager: ai.convManager,
2377
+ assistantManager: ai.assistantManager,
2378
+ skillManagerCore: ai.skillManagerCore,
2379
+ llmConfigStore: ai.llmConfigStore,
2380
+ customProviderStore: ai.customProviderStore,
2381
+ customModelStore: ai.customModelStore,
2382
+ runAgentStream: ai.runAgentStream
2383
+ }
2384
+ },
2385
+ ai ? { requireAi: true } : void 0
2386
+ );
2387
+ registerImportRoutes(server2, dbManager2);
2388
+ server2.get("/_web/system/check-update", async () => {
2389
+ const currentVersion = getVersion();
2390
+ const packageName = "chatlab-cli";
2391
+ try {
2392
+ const resp = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
2393
+ headers: { Accept: "application/json" },
2394
+ signal: AbortSignal.timeout(1e4)
2395
+ });
2396
+ if (!resp.ok) {
2397
+ return { hasUpdate: false, currentVersion, error: `npm registry HTTP ${resp.status}` };
2398
+ }
2399
+ const data = await resp.json();
2400
+ const latestVersion = data.version || currentVersion;
2401
+ const hasUpdate = isNewerVersion(latestVersion, currentVersion);
2402
+ return { hasUpdate, currentVersion, latestVersion };
2403
+ } catch (err) {
2404
+ return {
2405
+ hasUpdate: false,
2406
+ currentVersion,
2407
+ error: err instanceof Error ? err.message : String(err)
2408
+ };
2653
2409
  }
2654
- convManager2.deleteMessagesFrom(id, messageId);
2655
- return { success: true };
2656
2410
  });
2657
- server2.post("/_web/ai/conversations/:id/fork", async (request, reply) => {
2658
- const { upToMessageId, title } = request.body;
2659
- if (!upToMessageId || typeof upToMessageId !== "string") {
2660
- return reply.code(400).send({ error: "upToMessageId is required" });
2411
+ }
2412
+
2413
+ // src/http/routes/proxy.ts
2414
+ var PROXY_TARGET = "https://chatlab.fun";
2415
+ function registerProxyRoutes(server2) {
2416
+ server2.get("/_proxy/chatlab.fun/*", async (request, reply) => {
2417
+ const subPath = request.params["*"] ?? "";
2418
+ const queryString = new URLSearchParams(
2419
+ request.query
2420
+ ).toString();
2421
+ const targetUrl = queryString ? `${PROXY_TARGET}/${subPath}?${queryString}` : `${PROXY_TARGET}/${subPath}`;
2422
+ try {
2423
+ const upstream = await fetch(targetUrl, {
2424
+ headers: { Accept: request.headers.accept ?? "*/*" },
2425
+ signal: AbortSignal.timeout(1e4)
2426
+ });
2427
+ const contentType = upstream.headers.get("content-type") ?? "application/octet-stream";
2428
+ const body = Buffer.from(await upstream.arrayBuffer());
2429
+ reply.code(upstream.status).header("content-type", contentType).send(body);
2430
+ } catch (err) {
2431
+ reply.code(502).send({
2432
+ error: "Proxy error",
2433
+ message: err instanceof Error ? err.message : String(err)
2434
+ });
2661
2435
  }
2662
- return convManager2.forkConversation(request.params.id, upToMessageId, title);
2663
2436
  });
2664
- server2.put("/_web/ai/messages/:messageId/content", async (request, reply) => {
2665
- const { content } = request.body;
2666
- if (!content || typeof content !== "string") {
2667
- return reply.code(400).send({ error: "content is required" });
2437
+ }
2438
+
2439
+ // src/ai/logger.ts
2440
+ var logger = null;
2441
+ function initServerAiLogger(logsDir) {
2442
+ if (!logger) {
2443
+ logger = new AiLogger(logsDir);
2444
+ }
2445
+ return logger;
2446
+ }
2447
+ function getServerAiLogger() {
2448
+ return logger;
2449
+ }
2450
+ function closeServerAiLogger() {
2451
+ if (logger) {
2452
+ logger.close();
2453
+ logger = null;
2454
+ }
2455
+ }
2456
+
2457
+ // src/ai/manager-factory.ts
2458
+ import * as fs5 from "fs";
2459
+ import * as path7 from "path";
2460
+ import { createHash, randomUUID } from "crypto";
2461
+ var nodeFs = {
2462
+ ensureDir(dir) {
2463
+ if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
2464
+ },
2465
+ listFiles(dir, ext) {
2466
+ if (!fs5.existsSync(dir)) return [];
2467
+ return fs5.readdirSync(dir).filter((f) => f.endsWith(ext));
2468
+ },
2469
+ readFile(filePath) {
2470
+ return fs5.readFileSync(filePath, "utf-8");
2471
+ },
2472
+ writeFile(filePath, content) {
2473
+ fs5.writeFileSync(filePath, content, "utf-8");
2474
+ },
2475
+ deleteFile(filePath) {
2476
+ if (fs5.existsSync(filePath)) fs5.unlinkSync(filePath);
2477
+ },
2478
+ fileExists(filePath) {
2479
+ return fs5.existsSync(filePath);
2480
+ },
2481
+ joinPath(...parts) {
2482
+ return path7.join(...parts);
2483
+ }
2484
+ };
2485
+ var managerCache = /* @__PURE__ */ new Map();
2486
+ function getAssistantManager(aiDataDir) {
2487
+ let cached = managerCache.get(aiDataDir);
2488
+ if (!cached) {
2489
+ cached = {
2490
+ assistant: new AssistantManager({
2491
+ fs: nodeFs,
2492
+ assistantsDir: path7.join(aiDataDir, "assistants"),
2493
+ builtinRawConfigs: [],
2494
+ generateId: () => `custom_${randomUUID().replace(/-/g, "").slice(0, 12)}`
2495
+ }),
2496
+ skill: new SkillManagerCore({
2497
+ fs: nodeFs,
2498
+ skillsDir: path7.join(aiDataDir, "skills"),
2499
+ builtinRawSkills: [],
2500
+ contentHash: (content) => createHash("md5").update(content).digest("hex")
2501
+ })
2502
+ };
2503
+ managerCache.set(aiDataDir, cached);
2504
+ }
2505
+ return cached.assistant;
2506
+ }
2507
+ function getSkillManagerCore(aiDataDir) {
2508
+ let cached = managerCache.get(aiDataDir);
2509
+ if (!cached) {
2510
+ getAssistantManager(aiDataDir);
2511
+ cached = managerCache.get(aiDataDir);
2512
+ }
2513
+ return cached.skill;
2514
+ }
2515
+
2516
+ // src/ai/tool-adapter.ts
2517
+ var DEFAULT_MAX_TOOL_RESULT_TOKENS = 8e3;
2518
+ var TOOL_TRUNCATION_STRATEGY = {
2519
+ search_messages: "keep_first",
2520
+ deep_search_messages: "keep_first",
2521
+ get_recent_messages: "keep_last",
2522
+ get_message_context: "keep_last",
2523
+ get_session_messages: "keep_last",
2524
+ get_conversation_between: "keep_last"
2525
+ };
2526
+ function convertJsonSchemaToParameters(schema) {
2527
+ const properties = {};
2528
+ for (const [key, prop] of Object.entries(schema.properties)) {
2529
+ properties[key] = { ...prop };
2530
+ }
2531
+ return {
2532
+ type: "object",
2533
+ properties,
2534
+ required: schema.required || []
2535
+ };
2536
+ }
2537
+ function adaptToolsForAgent(tools, getContext, options) {
2538
+ const tokenBudget = options?.maxToolResultTokens ?? DEFAULT_MAX_TOOL_RESULT_TOKENS;
2539
+ return tools.map((tool) => ({
2540
+ name: tool.name,
2541
+ label: tool.name,
2542
+ description: tool.description,
2543
+ parameters: convertJsonSchemaToParameters(tool.inputSchema),
2544
+ async execute(_toolCallId, params) {
2545
+ const toolParams = params && typeof params === "object" ? params : {};
2546
+ const ctx = getContext();
2547
+ const execCtx = {
2548
+ db: ctx.db,
2549
+ dataProvider: new CoreDataProvider(ctx.db),
2550
+ sessionId: ctx.sessionId,
2551
+ locale: ctx.locale,
2552
+ segmentText: (texts, locale, options2) => batchSegmentWithFrequency(texts, locale, options2)
2553
+ };
2554
+ try {
2555
+ const result = await tool.handler(toolParams, execCtx);
2556
+ if (result.rawMessages && result.rawMessages.length > 0) {
2557
+ const pipelineResult = applyPreprocessingPipeline({
2558
+ rawMessages: result.rawMessages,
2559
+ locale: ctx.locale,
2560
+ maxToolResultTokens: tokenBudget,
2561
+ truncationStrategy: TOOL_TRUNCATION_STRATEGY[tool.name] ?? "keep_last",
2562
+ extraDetails: result.data ?? {},
2563
+ logger: getServerAiLogger() ?? void 0
2564
+ });
2565
+ return { content: [{ type: "text", text: pipelineResult.text }], details: null };
2566
+ }
2567
+ return { content: [{ type: "text", text: result.content }], details: null };
2568
+ } catch (error) {
2569
+ const msg = error instanceof Error ? error.message : String(error);
2570
+ return { content: [{ type: "text", text: `Error: ${msg}` }], details: null };
2571
+ }
2668
2572
  }
2669
- convManager2.updateMessageContent(request.params.messageId, content);
2670
- return { success: true };
2573
+ }));
2574
+ }
2575
+
2576
+ // src/ai/llm-config.ts
2577
+ import * as fs6 from "fs";
2578
+ import * as path8 from "path";
2579
+ function loadLlmConfig(aiDataDir) {
2580
+ const configPath = path8.join(aiDataDir, "llm-config.json");
2581
+ if (!fs6.existsSync(configPath)) {
2582
+ return { configs: [], defaultAssistant: null, fastModel: null };
2583
+ }
2584
+ try {
2585
+ const data = JSON.parse(fs6.readFileSync(configPath, "utf-8"));
2586
+ const configs = (data.configs || []).map((c) => {
2587
+ const provider = c.provider || "";
2588
+ const authProfile = c.authProfile;
2589
+ const profileKey = resolveApiKey(provider, authProfile);
2590
+ const rawKey = c.apiKey || "";
2591
+ const fallbackKey = rawKey.startsWith("enc:") ? "" : rawKey;
2592
+ return {
2593
+ ...c,
2594
+ apiKey: profileKey || fallbackKey
2595
+ };
2596
+ });
2597
+ return {
2598
+ configs,
2599
+ defaultAssistant: data.defaultAssistant ?? null,
2600
+ fastModel: data.fastModel ?? null
2601
+ };
2602
+ } catch {
2603
+ return { configs: [], defaultAssistant: null, fastModel: null };
2604
+ }
2605
+ }
2606
+ function resolveSlot(slot, configs) {
2607
+ if (slot && configs.some((c) => c.id === slot.configId)) return slot;
2608
+ const fallback = configs[0];
2609
+ if (!fallback) return null;
2610
+ return { configId: fallback.id, modelId: fallback.model };
2611
+ }
2612
+ function getDefaultAssistantConfig(aiDataDir) {
2613
+ const store = loadLlmConfig(aiDataDir);
2614
+ const slot = resolveSlot(store.defaultAssistant, store.configs);
2615
+ if (!slot) return null;
2616
+ const config = store.configs.find((c) => c.id === slot.configId);
2617
+ if (!config) return null;
2618
+ return { ...config, model: slot.modelId || config.model };
2619
+ }
2620
+ function buildPiModel2(config) {
2621
+ return buildPiModel(config);
2622
+ }
2623
+
2624
+ // src/ai/assistant-loader.ts
2625
+ import * as fs7 from "fs";
2626
+ import * as path9 from "path";
2627
+ function loadAssistantConfig(aiDataDir, assistantId) {
2628
+ const filePath = path9.join(aiDataDir, "assistants", `${assistantId}.md`);
2629
+ if (!fs7.existsSync(filePath)) return null;
2630
+ try {
2631
+ const content = fs7.readFileSync(filePath, "utf-8");
2632
+ return parseAssistantFile(content, filePath);
2633
+ } catch {
2634
+ return null;
2635
+ }
2636
+ }
2637
+
2638
+ // src/ai/agent.ts
2639
+ async function runServerAgent(options) {
2640
+ const {
2641
+ userMessage,
2642
+ conversationId,
2643
+ historyLeafMessageId,
2644
+ chatType = "group",
2645
+ locale = "zh-CN",
2646
+ assistantSystemPrompt,
2647
+ skillMenu,
2648
+ skillDef,
2649
+ compressionConfig,
2650
+ tools = [],
2651
+ aiDataDir,
2652
+ convManager: convManager2,
2653
+ onEvent,
2654
+ abortSignal,
2655
+ ownerInfo,
2656
+ mentionedMembers,
2657
+ dataSnapshot,
2658
+ thinkingLevel
2659
+ } = options;
2660
+ const aiLogger = getServerAiLogger();
2661
+ await initTokenizer();
2662
+ const llmConfig = getDefaultAssistantConfig(aiDataDir);
2663
+ if (!llmConfig) {
2664
+ onEvent({ type: "error", error: { name: "ConfigError", message: "LLM service not configured" } });
2665
+ onEvent({ type: "done", isFinished: true });
2666
+ return;
2667
+ }
2668
+ const piModel = buildPiModel2(llmConfig);
2669
+ const t = createAiTranslate(locale);
2670
+ let skillCtx;
2671
+ if (skillDef) {
2672
+ skillCtx = { skillDef };
2673
+ } else if (skillMenu) {
2674
+ skillCtx = { skillMenu };
2675
+ }
2676
+ const systemPrompt = buildSystemPrompt({
2677
+ t,
2678
+ chatType,
2679
+ assistantSystemPrompt,
2680
+ ownerInfo,
2681
+ locale,
2682
+ skillCtx,
2683
+ mentionedMembers,
2684
+ dataSnapshot
2671
2685
  });
2672
- server2.post("/_web/ai/conversations/:id/messages/:messageId/delete-relink", async (request, reply) => {
2673
- const { id, messageId } = request.params;
2674
- if (!id || !messageId) {
2675
- return reply.code(400).send({ error: "conversation id and messageId are required" });
2686
+ const handler = new AgentEventHandler({
2687
+ onChunk: onEvent,
2688
+ context: {},
2689
+ systemPrompt
2690
+ });
2691
+ if (compressionConfig?.enabled && historyLeafMessageId === void 0) {
2692
+ const llmAdapter = createCompressionLlmAdapter({
2693
+ piModel,
2694
+ apiKey: llmConfig.apiKey,
2695
+ onCompressing: () => handler.emitStatus("compressing", [])
2696
+ });
2697
+ const compressionResult = await checkAndCompress(
2698
+ conversationId,
2699
+ compressionConfig,
2700
+ systemPrompt,
2701
+ llmAdapter,
2702
+ convManager2,
2703
+ aiLogger ?? void 0
2704
+ );
2705
+ if (compressionResult.compressed) {
2706
+ onEvent({
2707
+ type: "compression_done",
2708
+ compressionResult: {
2709
+ summaryContent: compressionResult.summaryContent ?? "",
2710
+ tokensBefore: compressionResult.tokensBefore ?? 0,
2711
+ tokensAfter: compressionResult.tokensAfter ?? 0,
2712
+ timestamp: Date.now()
2713
+ }
2714
+ });
2676
2715
  }
2677
- convManager2.deleteAndRelinkMessage(id, messageId);
2678
- return { success: true };
2679
- });
2680
- server2.post("/_web/ai/conversations/:id/messages/insert-after", async (request, reply) => {
2681
- const { afterMessageId, role, content, contentBlocks, tokenUsage } = request.body;
2682
- if (!afterMessageId || typeof afterMessageId !== "string") {
2683
- return reply.code(400).send({ error: "afterMessageId is required" });
2716
+ } else if (compressionConfig?.enabled && historyLeafMessageId !== void 0) {
2717
+ aiLogger?.info?.("Compression", "Skipping compression for edited branch request", {
2718
+ conversationId,
2719
+ historyLeafMessageId
2720
+ });
2721
+ }
2722
+ if (abortSignal?.aborted) {
2723
+ handler.emitStatus("aborted", [], { force: true });
2724
+ onEvent({ type: "done", isFinished: true, usage: handler.cloneUsage() });
2725
+ return;
2726
+ }
2727
+ let history = [];
2728
+ try {
2729
+ history = convManager2.getHistoryForAgent(conversationId, void 0, historyLeafMessageId);
2730
+ } catch {
2731
+ }
2732
+ handler.emitStatus("preparing", [], { pendingUserMessage: userMessage, force: true });
2733
+ const steerMessage = t("ai.agent.answerWithoutTools");
2734
+ let cachedMessages = [];
2735
+ try {
2736
+ const result = await runAgentCore({
2737
+ piModel,
2738
+ apiKey: llmConfig.apiKey,
2739
+ systemPrompt,
2740
+ tools,
2741
+ history,
2742
+ userMessage,
2743
+ maxToolRounds: 5,
2744
+ abortSignal,
2745
+ steerMessage,
2746
+ thinkingLevel,
2747
+ onConvertToLlm: (filteredMessages) => {
2748
+ cachedMessages = filteredMessages;
2749
+ },
2750
+ onEvent: (coreEvent) => handler.handleCoreEvent(coreEvent, cachedMessages),
2751
+ onDebugContext: (messages) => {
2752
+ try {
2753
+ convManager2.setPendingDebugContext(conversationId, JSON.stringify(messages, null, 2));
2754
+ } catch {
2755
+ }
2756
+ }
2757
+ });
2758
+ if (result.error) {
2759
+ const friendlyMessage = formatAIError(result.error);
2760
+ onEvent({ type: "error", error: { name: "AgentError", message: friendlyMessage } });
2684
2761
  }
2685
- return convManager2.insertMessageAfter(
2686
- request.params.id,
2687
- afterMessageId,
2688
- role,
2689
- content,
2690
- contentBlocks,
2691
- tokenUsage
2692
- );
2693
- });
2694
- server2.get("/_web/ai/conversations/:id/token-usage", async (request) => {
2695
- return convManager2.getConversationTokenUsage(request.params.id);
2696
- });
2697
- const activeAgentAborts = /* @__PURE__ */ new Map();
2698
- server2.post("/_web/ai/agent/stream", async (request, reply) => {
2762
+ handler.emitStatus("completed", cachedMessages, { force: true });
2763
+ onEvent({ type: "done", isFinished: true, usage: result.usage });
2764
+ } catch (error) {
2765
+ const friendlyMessage = formatAIError(error);
2766
+ aiLogger?.error("ServerAgent", "Agent execution error", { error: String(error) });
2767
+ handler.emitStatus("error", cachedMessages, { force: true });
2768
+ onEvent({ type: "error", error: { name: "AgentError", message: friendlyMessage } });
2769
+ onEvent({ type: "done", isFinished: true, usage: handler.cloneUsage() });
2770
+ }
2771
+ }
2772
+
2773
+ // src/ai/agent-stream-runner.ts
2774
+ function getAiDir(dbManager2) {
2775
+ const pathProvider = dbManager2["pathProvider"];
2776
+ if (!pathProvider) {
2777
+ throw Object.assign(new Error("PathProvider not available"), { statusCode: 500 });
2778
+ }
2779
+ return pathProvider.getAiDataDir();
2780
+ }
2781
+ function createCliRunAgentStream(dbManager2, convManager2) {
2782
+ return async (params, onEvent, abortSignal) => {
2699
2783
  const {
2700
2784
  userMessage,
2701
2785
  conversationId,
@@ -2704,27 +2788,13 @@ data: ${JSON.stringify(data)}
2704
2788
  chatType,
2705
2789
  locale,
2706
2790
  assistantId,
2791
+ skillId,
2792
+ enableAutoSkill,
2707
2793
  compressionConfig,
2708
2794
  ownerInfo,
2709
2795
  mentionedMembers,
2710
2796
  thinkingLevel
2711
- } = request.body;
2712
- const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
2713
- const abortController = new AbortController();
2714
- activeAgentAborts.set(requestId, abortController);
2715
- reply.raw.writeHead(200, {
2716
- "Content-Type": "text/event-stream",
2717
- "Cache-Control": "no-cache",
2718
- Connection: "keep-alive",
2719
- "X-Request-Id": requestId
2720
- });
2721
- const sendSSE = (event, data) => {
2722
- reply.raw.write(`event: ${event}
2723
- data: ${JSON.stringify(data)}
2724
-
2725
- `);
2726
- };
2727
- sendSSE("meta", { requestId });
2797
+ } = params;
2728
2798
  const aiDataDir = getAiDir(dbManager2);
2729
2799
  let assistantSystemPrompt;
2730
2800
  if (assistantId) {
@@ -2739,191 +2809,73 @@ data: ${JSON.stringify(data)}
2739
2809
  const maxToolResultTokens = Math.floor(contextWindow * (maxToolResultPercent / 100));
2740
2810
  const db = dbManager2.open?.(sessionId);
2741
2811
  const agentTools = db ? adaptToolsForAgent(AGENT_TOOL_REGISTRY, () => ({ db, sessionId, locale }), { maxToolResultTokens }) : [];
2742
- const skillManager = new SkillManager(aiDataDir);
2743
- skillManager.init();
2812
+ const skillMgr = new SkillManager(aiDataDir);
2813
+ skillMgr.init();
2744
2814
  const toolNames = agentTools.map((t) => t.name);
2745
- const skillMenu = skillManager.getSkillMenu(chatType ?? "group", toolNames);
2746
- if (skillMenu) {
2815
+ let resolvedSkillDef;
2816
+ let resolvedSkillMenu;
2817
+ if (skillId) {
2818
+ const def = skillMgr.getSkillConfig(skillId);
2819
+ if (def) resolvedSkillDef = { name: def.name, prompt: def.prompt };
2820
+ } else if (enableAutoSkill) {
2821
+ const menu = skillMgr.getSkillMenu(chatType ?? "group", toolNames);
2822
+ if (menu) resolvedSkillMenu = menu;
2823
+ }
2824
+ if (resolvedSkillMenu) {
2747
2825
  const activateSkillTool = createActivateSkillTool({
2748
2826
  chatType: chatType ?? "group",
2749
2827
  allowedTools: toolNames,
2750
2828
  locale,
2751
- getSkillConfig: (id) => skillManager.getSkillConfig(id)
2829
+ getSkillConfig: (id) => skillMgr.getSkillConfig(id)
2752
2830
  });
2753
2831
  agentTools.push(activateSkillTool);
2754
2832
  }
2755
- const onEvent = (event) => {
2756
- sendSSE(event.type, event);
2757
- if (event.type === "done") {
2758
- activeAgentAborts.delete(requestId);
2759
- reply.raw.end();
2760
- }
2761
- };
2762
- reply.raw.on("close", () => {
2763
- if (!abortController.signal.aborted) {
2764
- abortController.abort();
2765
- }
2766
- activeAgentAborts.delete(requestId);
2767
- });
2768
- try {
2769
- const resolvedCompression = compressionConfig?.enabled ? {
2770
- enabled: true,
2771
- tokenThresholdPercent: compressionConfig.tokenThresholdPercent ?? 75,
2772
- bufferSizePercent: compressionConfig.bufferSizePercent ?? 20,
2773
- maxToolResultPercent: compressionConfig.maxToolResultPercent
2774
- } : void 0;
2775
- let dataSnapshot;
2776
- if (db) {
2777
- try {
2778
- const overview = getChatOverview(db, 5);
2779
- if (overview) {
2780
- dataSnapshot = {
2781
- name: overview.name,
2782
- platform: overview.platform,
2783
- type: overview.type,
2784
- totalMessages: overview.totalMessages,
2785
- totalMembers: overview.totalMembers,
2786
- firstMessageTs: overview.firstMessageTs,
2787
- lastMessageTs: overview.lastMessageTs,
2788
- capturedAt: Math.floor(Date.now() / 1e3)
2789
- };
2790
- }
2791
- } catch {
2792
- }
2793
- }
2794
- await runServerAgent({
2795
- userMessage,
2796
- conversationId,
2797
- historyLeafMessageId,
2798
- chatType,
2799
- locale,
2800
- assistantSystemPrompt,
2801
- skillMenu,
2802
- compressionConfig: resolvedCompression,
2803
- tools: agentTools,
2804
- aiDataDir,
2805
- convManager: convManager2,
2806
- onEvent,
2807
- abortSignal: abortController.signal,
2808
- ownerInfo,
2809
- mentionedMembers,
2810
- dataSnapshot,
2811
- thinkingLevel
2812
- });
2813
- } catch (error) {
2814
- const msg = error instanceof Error ? error.message : String(error);
2815
- sendSSE("error", { type: "error", error: { name: "ServerError", message: msg } });
2816
- sendSSE("done", { type: "done", isFinished: true });
2817
- activeAgentAborts.delete(requestId);
2818
- reply.raw.end();
2819
- }
2820
- });
2821
- server2.post("/_web/ai/agent/abort", async (request) => {
2822
- const { requestId } = request.body;
2823
- const controller = activeAgentAborts.get(requestId);
2824
- if (controller) {
2825
- controller.abort();
2826
- activeAgentAborts.delete(requestId);
2827
- return { success: true };
2828
- }
2829
- return { success: false };
2830
- });
2831
- server2.get("/_web/ai/debug/schema", async (_request, reply) => {
2832
- if (!convManager2) {
2833
- return reply.code(503).send({ error: "AI conversation manager not available" });
2834
- }
2835
- return convManager2.getAiSchema();
2836
- });
2837
- server2.post("/_web/ai/debug/execute-sql", async (request, reply) => {
2838
- if (!convManager2) {
2839
- return reply.code(503).send({ error: "AI conversation manager not available" });
2840
- }
2841
- const { sql } = request.body;
2842
- if (!sql || typeof sql !== "string") {
2843
- return reply.code(400).send({ error: "sql is required" });
2844
- }
2845
- try {
2846
- return convManager2.executeAiSQL(sql);
2847
- } catch (e) {
2848
- const msg = e instanceof Error ? e.message : String(e);
2849
- return reply.code(400).send({ error: msg });
2850
- }
2851
- });
2852
- server2.post("/_web/ai/debug/clear-debug-context", async (_request, reply) => {
2853
- if (!convManager2) {
2854
- return reply.code(503).send({ error: "AI conversation manager not available" });
2855
- }
2856
- const cleared = convManager2.clearAllDebugContext();
2857
- return { success: true, cleared };
2858
- });
2859
- }
2860
-
2861
- // src/http/routes/preferences.ts
2862
- var prefManager = null;
2863
- function registerPreferencesRoutes(server2, pathProvider) {
2864
- if (!prefManager) {
2865
- prefManager = new PreferencesManager(pathProvider.getSystemDir());
2866
- }
2867
- server2.get("/_web/preferences", async () => {
2868
- return prefManager.load();
2869
- });
2870
- server2.patch("/_web/preferences", async (request) => {
2871
- return prefManager.save(request.body);
2872
- });
2873
- server2.get("/_web/preferences/ui-config", async () => {
2874
- const config = loadConfig();
2875
- return config.ui;
2876
- });
2877
- server2.patch("/_web/preferences/ui-config", async (request) => {
2878
- try {
2879
- for (const [key, value] of Object.entries(request.body)) {
2880
- if (value !== void 0) {
2881
- writeConfigField("ui", key, value);
2833
+ const resolvedCompression = compressionConfig?.enabled ? {
2834
+ enabled: true,
2835
+ tokenThresholdPercent: compressionConfig.tokenThresholdPercent ?? 75,
2836
+ bufferSizePercent: compressionConfig.bufferSizePercent ?? 20,
2837
+ maxToolResultPercent: compressionConfig.maxToolResultPercent
2838
+ } : void 0;
2839
+ let dataSnapshot;
2840
+ if (db) {
2841
+ try {
2842
+ const overview = getChatOverview(db, 5);
2843
+ if (overview) {
2844
+ dataSnapshot = {
2845
+ name: overview.name,
2846
+ platform: overview.platform,
2847
+ type: overview.type,
2848
+ totalMessages: overview.totalMessages,
2849
+ totalMembers: overview.totalMembers,
2850
+ firstMessageTs: overview.firstMessageTs,
2851
+ lastMessageTs: overview.lastMessageTs,
2852
+ capturedAt: Math.floor(Date.now() / 1e3)
2853
+ };
2882
2854
  }
2855
+ } catch {
2883
2856
  }
2884
- return { success: true };
2885
- } catch (err) {
2886
- return { success: false, error: err instanceof Error ? err.message : String(err) };
2887
- }
2888
- });
2889
- server2.get("/_web/preferences/locale", async () => {
2890
- const config = loadConfig();
2891
- return { lang: config.locale.lang };
2892
- });
2893
- server2.patch("/_web/preferences/locale", async (request) => {
2894
- try {
2895
- writeConfigField("locale", "lang", request.body.lang);
2896
- return { success: true };
2897
- } catch (err) {
2898
- return { success: false, error: err instanceof Error ? err.message : String(err) };
2899
- }
2900
- });
2901
- }
2902
-
2903
- // src/http/routes/proxy.ts
2904
- var PROXY_TARGET = "https://chatlab.fun";
2905
- function registerProxyRoutes(server2) {
2906
- server2.get("/_proxy/chatlab.fun/*", async (request, reply) => {
2907
- const subPath = request.params["*"] ?? "";
2908
- const queryString = new URLSearchParams(
2909
- request.query
2910
- ).toString();
2911
- const targetUrl = queryString ? `${PROXY_TARGET}/${subPath}?${queryString}` : `${PROXY_TARGET}/${subPath}`;
2912
- try {
2913
- const upstream = await fetch(targetUrl, {
2914
- headers: { Accept: request.headers.accept ?? "*/*" },
2915
- signal: AbortSignal.timeout(1e4)
2916
- });
2917
- const contentType = upstream.headers.get("content-type") ?? "application/octet-stream";
2918
- const body = Buffer.from(await upstream.arrayBuffer());
2919
- reply.code(upstream.status).header("content-type", contentType).send(body);
2920
- } catch (err) {
2921
- reply.code(502).send({
2922
- error: "Proxy error",
2923
- message: err instanceof Error ? err.message : String(err)
2924
- });
2925
2857
  }
2926
- });
2858
+ await runServerAgent({
2859
+ userMessage,
2860
+ conversationId,
2861
+ historyLeafMessageId,
2862
+ chatType,
2863
+ locale,
2864
+ assistantSystemPrompt,
2865
+ skillMenu: resolvedSkillMenu,
2866
+ skillDef: resolvedSkillDef,
2867
+ compressionConfig: resolvedCompression,
2868
+ tools: agentTools,
2869
+ aiDataDir,
2870
+ convManager: convManager2,
2871
+ onEvent,
2872
+ abortSignal,
2873
+ ownerInfo,
2874
+ mentionedMembers,
2875
+ dataSnapshot,
2876
+ thinkingLevel
2877
+ });
2878
+ };
2927
2879
  }
2928
2880
 
2929
2881
  // ../../packages/sync/src/types.ts
@@ -2932,12 +2884,12 @@ var noop = () => {
2932
2884
  var NOOP_LOGGER = { info: noop, warn: noop, error: noop };
2933
2885
 
2934
2886
  // ../../packages/sync/src/data-source-manager.ts
2935
- import * as fs11 from "fs";
2936
- import * as path12 from "path";
2937
- import * as crypto2 from "crypto";
2887
+ import * as fs8 from "fs";
2888
+ import * as path10 from "path";
2889
+ import * as crypto from "crypto";
2938
2890
  var CONFIG_FILE = "data-sources.json";
2939
2891
  function generateId(prefix = "ds") {
2940
- return `${prefix}_${crypto2.randomBytes(6).toString("hex")}`;
2892
+ return `${prefix}_${crypto.randomBytes(6).toString("hex")}`;
2941
2893
  }
2942
2894
  function normalizeBaseUrl(input) {
2943
2895
  let url = input.trim().replace(/\/+$/, "");
@@ -2961,17 +2913,17 @@ var DataSourceManager = class {
2961
2913
  this.logger = logger2 ?? NOOP_LOGGER;
2962
2914
  }
2963
2915
  getConfigPath() {
2964
- return path12.join(this.configDir, CONFIG_FILE);
2916
+ return path10.join(this.configDir, CONFIG_FILE);
2965
2917
  }
2966
2918
  ensureDir() {
2967
- fs11.mkdirSync(this.configDir, { recursive: true });
2919
+ fs8.mkdirSync(this.configDir, { recursive: true });
2968
2920
  }
2969
2921
  // ==================== Load / Save ====================
2970
2922
  loadAll() {
2971
2923
  try {
2972
2924
  const filePath = this.getConfigPath();
2973
- if (fs11.existsSync(filePath)) {
2974
- const raw = fs11.readFileSync(filePath, "utf-8");
2925
+ if (fs8.existsSync(filePath)) {
2926
+ const raw = fs8.readFileSync(filePath, "utf-8");
2975
2927
  const parsed = JSON.parse(raw);
2976
2928
  if (!isValidDataSourceArray(parsed)) {
2977
2929
  this.logger.warn("[DataSource] Incompatible config format detected, returning empty.");
@@ -2990,7 +2942,7 @@ var DataSourceManager = class {
2990
2942
  saveAll(sources) {
2991
2943
  try {
2992
2944
  this.ensureDir();
2993
- fs11.writeFileSync(this.getConfigPath(), JSON.stringify(sources, null, 2), "utf-8");
2945
+ fs8.writeFileSync(this.getConfigPath(), JSON.stringify(sources, null, 2), "utf-8");
2994
2946
  } catch (err) {
2995
2947
  this.logger.error("[DataSource] Failed to save config", err);
2996
2948
  }
@@ -3115,8 +3067,8 @@ function parseRemoteSessionsResponse(body) {
3115
3067
  }
3116
3068
 
3117
3069
  // ../../packages/sync/src/pull-engine.ts
3118
- import * as fs12 from "fs";
3119
- import * as crypto3 from "crypto";
3070
+ import * as fs9 from "fs";
3071
+ import * as crypto2 from "crypto";
3120
3072
  var MAX_PAGES_PER_PULL = 5e3;
3121
3073
  var PULL_OVERLAP_SECONDS = 60;
3122
3074
  function buildPullUrl(baseUrl, remoteSessionId, params) {
@@ -3128,14 +3080,14 @@ function buildPullUrl(baseUrl, remoteSessionId, params) {
3128
3080
  return base + "?" + qs.join("&");
3129
3081
  }
3130
3082
  function deriveLocalSessionId(baseUrl, remoteSessionId) {
3131
- const hash = crypto3.createHash("sha256").update(`${baseUrl}\0${remoteSessionId}`).digest("hex").slice(0, 12);
3083
+ const hash = crypto2.createHash("sha256").update(`${baseUrl}\0${remoteSessionId}`).digest("hex").slice(0, 12);
3132
3084
  return `remote_${hash}`;
3133
3085
  }
3134
3086
  function parseSyncFromFile(filePath) {
3135
3087
  try {
3136
3088
  const isJsonl = filePath.endsWith(".jsonl");
3137
3089
  if (isJsonl) {
3138
- const content = fs12.readFileSync(filePath, "utf-8");
3090
+ const content = fs9.readFileSync(filePath, "utf-8");
3139
3091
  const lines = content.trimEnd().split("\n");
3140
3092
  for (let i = lines.length - 1; i >= Math.max(0, lines.length - 5); i--) {
3141
3093
  try {
@@ -3154,7 +3106,7 @@ function parseSyncFromFile(filePath) {
3154
3106
  }
3155
3107
  return null;
3156
3108
  }
3157
- const raw = fs12.readFileSync(filePath, "utf-8");
3109
+ const raw = fs9.readFileSync(filePath, "utf-8");
3158
3110
  const parsed = JSON.parse(raw);
3159
3111
  if (parsed.sync && typeof parsed.sync === "object") {
3160
3112
  const s = parsed.sync;
@@ -3168,7 +3120,7 @@ function parseSyncFromFile(filePath) {
3168
3120
  function fileContainsMessages(filePath) {
3169
3121
  try {
3170
3122
  if (filePath.endsWith(".jsonl")) {
3171
- const content = fs12.readFileSync(filePath, "utf-8");
3123
+ const content = fs9.readFileSync(filePath, "utf-8");
3172
3124
  for (const line of content.split("\n")) {
3173
3125
  const trimmed = line.trim();
3174
3126
  if (!trimmed) continue;
@@ -3181,7 +3133,7 @@ function fileContainsMessages(filePath) {
3181
3133
  }
3182
3134
  return false;
3183
3135
  }
3184
- const raw = fs12.readFileSync(filePath, "utf-8");
3136
+ const raw = fs9.readFileSync(filePath, "utf-8");
3185
3137
  const parsed = JSON.parse(raw);
3186
3138
  return Array.isArray(parsed.messages) && parsed.messages.length > 0;
3187
3139
  } catch {
@@ -3190,7 +3142,7 @@ function fileContainsMessages(filePath) {
3190
3142
  }
3191
3143
  function cleanupTempFile(filePath) {
3192
3144
  try {
3193
- if (fs12.existsSync(filePath)) fs12.unlinkSync(filePath);
3145
+ if (fs9.existsSync(filePath)) fs9.unlinkSync(filePath);
3194
3146
  } catch {
3195
3147
  }
3196
3148
  }
@@ -3250,7 +3202,7 @@ var PullEngine = class {
3250
3202
  limit: ds.pullLimit
3251
3203
  });
3252
3204
  try {
3253
- const stat2 = fs12.statSync(tempFile);
3205
+ const stat2 = fs9.statSync(tempFile);
3254
3206
  this.logger.info(`[Pull] "${sess.name}" page ${pageCount}: fetched ${stat2.size} bytes`);
3255
3207
  const sync0 = parseSyncFromFile(tempFile);
3256
3208
  if (stat2.size < 1024) {
@@ -3268,7 +3220,7 @@ var PullEngine = class {
3268
3220
  since,
3269
3221
  limit: ds.pullLimit
3270
3222
  });
3271
- const retryStat = fs12.statSync(retryFile);
3223
+ const retryStat = fs9.statSync(retryFile);
3272
3224
  this.logger.info(`[Pull] "${sess.name}" retry ${ri + 1}: fetched ${retryStat.size} bytes`);
3273
3225
  const retrySync = parseSyncFromFile(retryFile);
3274
3226
  if (retryStat.size < 1024 && !fileContainsMessages(retryFile) && retrySync?.hasMore === false) {
@@ -3542,13 +3494,13 @@ function reloadTimer(dsId, skipInitialPull = false) {
3542
3494
  }
3543
3495
 
3544
3496
  // src/sync/adapters.ts
3545
- import * as fs13 from "fs";
3497
+ import * as fs10 from "fs";
3546
3498
  import * as os4 from "os";
3547
- import * as path13 from "path";
3548
- import * as crypto4 from "crypto";
3499
+ import * as path11 from "path";
3500
+ import * as crypto3 from "crypto";
3549
3501
  function getTempFilePath(ext) {
3550
- const id = crypto4.randomBytes(8).toString("hex");
3551
- return path13.join(os4.tmpdir(), `chatlab-pull-${id}${ext}`);
3502
+ const id = crypto3.randomBytes(8).toString("hex");
3503
+ return path11.join(os4.tmpdir(), `chatlab-pull-${id}${ext}`);
3552
3504
  }
3553
3505
  var NodeFetcher = class {
3554
3506
  async fetchToTempFile(baseUrl, remoteSessionId, token, params) {
@@ -3565,14 +3517,14 @@ var NodeFetcher = class {
3565
3517
  const isJsonl = contentType.includes("ndjson") || contentType.includes("jsonl");
3566
3518
  const tempFile = getTempFilePath(isJsonl ? ".jsonl" : ".json");
3567
3519
  const buffer = Buffer.from(await response.arrayBuffer());
3568
- fs13.writeFileSync(tempFile, buffer);
3520
+ fs10.writeFileSync(tempFile, buffer);
3569
3521
  return tempFile;
3570
3522
  }
3571
3523
  };
3572
3524
  function resolveNativeBinding2() {
3573
3525
  if (process.versions.electron) return void 0;
3574
3526
  const nativePath = resolveCliPath("native/better_sqlite3.node");
3575
- if (fs13.existsSync(nativePath)) return nativePath;
3527
+ if (fs10.existsSync(nativePath)) return nativePath;
3576
3528
  return void 0;
3577
3529
  }
3578
3530
  var DirectImporter = class {
@@ -3586,7 +3538,7 @@ var DirectImporter = class {
3586
3538
  }
3587
3539
  sessionExists(sessionId) {
3588
3540
  const dbPath = this.dbManager.getDbPath(sessionId);
3589
- if (!fs13.existsSync(dbPath)) return false;
3541
+ if (!fs10.existsSync(dbPath)) return false;
3590
3542
  try {
3591
3543
  const db = openBetterSqliteDatabase(dbPath, { readonly: true, nativeBinding: this.nativeBinding });
3592
3544
  const row = db.prepare("SELECT COUNT(*) as cnt FROM sqlite_master WHERE type='table' AND name='message'").get();
@@ -3594,7 +3546,7 @@ var DirectImporter = class {
3594
3546
  if (row.cnt === 0) {
3595
3547
  this.logger.warn(`[DirectImporter] DB file exists but has no message table: ${sessionId}, removing`);
3596
3548
  try {
3597
- fs13.unlinkSync(dbPath);
3549
+ fs10.unlinkSync(dbPath);
3598
3550
  } catch {
3599
3551
  }
3600
3552
  return false;
@@ -3603,7 +3555,7 @@ var DirectImporter = class {
3603
3555
  } catch {
3604
3556
  this.logger.warn(`[DirectImporter] Cannot validate DB file: ${sessionId}, removing`);
3605
3557
  try {
3606
- fs13.unlinkSync(dbPath);
3558
+ fs10.unlinkSync(dbPath);
3607
3559
  } catch {
3608
3560
  }
3609
3561
  return false;
@@ -3723,8 +3675,8 @@ function registerAutomationRoutes(server2, ctx) {
3723
3675
  if (request.query.deleteData === "true" && removed.targetSessionId) {
3724
3676
  dbManager2.close(removed.targetSessionId);
3725
3677
  const dbPath = dbManager2.getDbPath(removed.targetSessionId);
3726
- const fs15 = await import("fs");
3727
- if (fs15.existsSync(dbPath)) fs15.unlinkSync(dbPath);
3678
+ const fs12 = await import("fs");
3679
+ if (fs12.existsSync(dbPath)) fs12.unlinkSync(dbPath);
3728
3680
  }
3729
3681
  return { success: true };
3730
3682
  });
@@ -3800,15 +3752,30 @@ function cleanupSync() {
3800
3752
  var server = null;
3801
3753
  var dbManager = null;
3802
3754
  var convManager = null;
3755
+ function createFileConfigStorage(aiDataDir) {
3756
+ return {
3757
+ readJson(key) {
3758
+ try {
3759
+ return JSON.parse(fs11.readFileSync(`${aiDataDir}/${key}.json`, "utf-8"));
3760
+ } catch {
3761
+ return null;
3762
+ }
3763
+ },
3764
+ writeJson(key, data) {
3765
+ if (!fs11.existsSync(aiDataDir)) fs11.mkdirSync(aiDataDir, { recursive: true });
3766
+ fs11.writeFileSync(`${aiDataDir}/${key}.json`, JSON.stringify(data, null, 2), "utf-8");
3767
+ }
3768
+ };
3769
+ }
3803
3770
  function resolveNativeBinding3() {
3804
3771
  if (process.versions.electron) return void 0;
3805
3772
  const nativePath = resolveCliPath("native/better_sqlite3.node");
3806
- if (fs14.existsSync(nativePath)) return nativePath;
3773
+ if (fs11.existsSync(nativePath)) return nativePath;
3807
3774
  return void 0;
3808
3775
  }
3809
3776
  function ensureToken(config) {
3810
3777
  if (config.api.token) return config.api.token;
3811
- const token = `clb_${crypto5.randomBytes(32).toString("hex")}`;
3778
+ const token = `clb_${crypto4.randomBytes(32).toString("hex")}`;
3812
3779
  try {
3813
3780
  writeConfigField("api", "token", token);
3814
3781
  } catch {
@@ -3819,10 +3786,19 @@ async function startHttpServer(options) {
3819
3786
  if (server) {
3820
3787
  throw new Error("HTTP server is already running");
3821
3788
  }
3822
- const config = loadConfig();
3789
+ let config = loadConfig();
3823
3790
  const port = options?.port ?? config.api.port;
3824
3791
  const host = options?.host ?? config.api.host;
3825
3792
  const token = options?.token ?? ensureToken(config);
3793
+ const pendingMigration = applyPendingNodeDataDirMigrationIfNeeded();
3794
+ if (!pendingMigration.skipped) {
3795
+ if (pendingMigration.success) {
3796
+ console.log("[Migration] Pending data directory migration completed");
3797
+ config = loadConfig();
3798
+ } else {
3799
+ console.error("[Migration] Pending data directory migration failed:", pendingMigration.error);
3800
+ }
3801
+ }
3826
3802
  const userDataDir = config.data.user_data_dir || void 0;
3827
3803
  const pathProvider = new NodePathProvider(userDataDir);
3828
3804
  pathProvider.ensureAllDirs();
@@ -3843,24 +3819,43 @@ async function startHttpServer(options) {
3843
3819
  await migrationRunner.run();
3844
3820
  const nativeBinding = resolveNativeBinding3();
3845
3821
  dbManager = new DatabaseManager(pathProvider, { nativeBinding });
3846
- convManager = new AIConversationManager(pathProvider.getAiDataDir(), { nativeBinding });
3822
+ const aiDataDir = pathProvider.getAiDataDir();
3823
+ convManager = new AIConversationManager(aiDataDir, { nativeBinding });
3824
+ const assistantManager = getAssistantManager(aiDataDir);
3825
+ const skillManagerCore = getSkillManagerCore(aiDataDir);
3826
+ const llmConfigStore = new LLMConfigStore(createFileConfigStorage(aiDataDir), {
3827
+ resolveApiKey: (provider, authProfile) => resolveApiKey(provider, authProfile) || void 0,
3828
+ onApiKeyCreated: (config2, apiKey) => {
3829
+ const profileName = config2.name?.toLowerCase().replace(/\s+/g, "-") || config2.provider;
3830
+ writeAuthProfile(profileName, { type: "api_key", provider: config2.provider, key: apiKey });
3831
+ return profileName;
3832
+ }
3833
+ });
3847
3834
  initServerAiLogger(pathProvider.getLogsDir());
3848
3835
  setAuthToken(token);
3836
+ setRequireAuth(!!(options?.requireAuth ?? config.api.require_auth));
3849
3837
  server = createServer();
3850
3838
  const multipart = await import("@fastify/multipart");
3851
3839
  await server.register(multipart.default, {
3852
3840
  limits: { fileSize: 1024 * 1024 * 1024 }
3853
3841
  // 1GB
3854
3842
  });
3855
- registerSystemRoutes(server, dbManager);
3856
- registerSessionRoutes(server, dbManager);
3857
- registerNlpRoutes(server, dbManager);
3858
- registerAiRoutes(server, dbManager, convManager);
3859
- registerWebRoutes(server, dbManager, { pathProvider, nativeBinding });
3860
- registerPreferencesRoutes(server, pathProvider);
3843
+ registerWebRoutes(server, dbManager, {
3844
+ pathProvider,
3845
+ nativeBinding,
3846
+ aiContext: {
3847
+ aiDataDir,
3848
+ convManager,
3849
+ assistantManager,
3850
+ skillManagerCore,
3851
+ llmConfigStore,
3852
+ customProviderStore: new CustomProviderStore(createFileConfigStorage(aiDataDir)),
3853
+ customModelStore: new CustomModelStore(createFileConfigStorage(aiDataDir)),
3854
+ runAgentStream: createCliRunAgentStream(dbManager, convManager)
3855
+ }
3856
+ });
3861
3857
  initSync(server, dbManager, pathProvider, { port, host, token });
3862
- if (options?.webRoot && fs14.existsSync(options.webRoot)) {
3863
- setWebMode(true);
3858
+ if (options?.webRoot && fs11.existsSync(options.webRoot)) {
3864
3859
  registerProxyRoutes(server);
3865
3860
  const fastifyStatic = await import("@fastify/static");
3866
3861
  await server.register(fastifyStatic.default, {
@@ -3890,19 +3885,15 @@ async function stopHttpServer() {
3890
3885
  dbManager = null;
3891
3886
  }
3892
3887
  closeServerAiLogger();
3893
- setWebMode(false);
3888
+ setRequireAuth(false);
3894
3889
  server = null;
3895
3890
  }
3896
3891
  }
3897
3892
 
3898
3893
  export {
3899
3894
  createServer,
3900
- registerSystemRoutes,
3901
- registerSessionRoutes,
3902
3895
  registerWebRoutes,
3903
- registerNlpRoutes,
3904
- registerAiRoutes,
3905
3896
  startHttpServer,
3906
3897
  stopHttpServer
3907
3898
  };
3908
- //# sourceMappingURL=chunk-L37WJ33K.mjs.map
3899
+ //# sourceMappingURL=chunk-NE4A6PND.mjs.map