chatlab-cli 0.31.2-beta.1 → 0.31.2

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 (92) hide show
  1. package/dist/{chat-command-V6NYTTPD.mjs → chat-command-QPL4GNHP.mjs} +8 -8
  2. package/dist/{chunk-7TDYBU5L.mjs → chunk-3SZWD7UM.mjs} +4 -4
  3. package/dist/{chunk-WL2ISRYH.mjs → chunk-64FKPYPJ.mjs} +11 -11
  4. package/dist/{chunk-LWC7JQ2W.mjs → chunk-6JKKHPXY.mjs} +2 -2
  5. package/dist/{chunk-67HNEIKY.mjs → chunk-BSSR4ZYW.mjs} +1878 -2038
  6. package/dist/chunk-BSSR4ZYW.mjs.map +1 -0
  7. package/dist/{chunk-ZF67S6RY.mjs → chunk-F2NZ7HAY.mjs} +2 -2
  8. package/dist/{chunk-5G2INROS.mjs → chunk-GYPBWKAU.mjs} +356 -483
  9. package/dist/chunk-GYPBWKAU.mjs.map +1 -0
  10. package/dist/{chunk-XZ5FFDLX.mjs → chunk-IK7GJTGY.mjs} +131 -117
  11. package/dist/chunk-IK7GJTGY.mjs.map +1 -0
  12. package/dist/{chunk-HUKZ2PBC.mjs → chunk-OAXVKNPN.mjs} +2 -2
  13. package/dist/{chunk-54GVCXHZ.mjs → chunk-OQ6N4PDD.mjs} +2 -2
  14. package/dist/{chunk-SJQVAKFA.mjs → chunk-S5PQM5J4.mjs} +4 -5
  15. package/dist/chunk-S5PQM5J4.mjs.map +1 -0
  16. package/dist/{chunk-OIZ64ET3.mjs → chunk-ZCXCHKQV.mjs} +3 -3
  17. package/dist/cli.mjs +8 -8
  18. package/dist/contacts-worker.mjs +2 -2
  19. package/dist/global-insight-worker.mjs +1 -1
  20. package/dist/{http-AMDWRKOH.mjs → http-NOEWVWCP.mjs} +11 -11
  21. package/dist/{import-WIJHB4LH.mjs → import-RS3HW7ZM.mjs} +7 -7
  22. package/dist/index.mjs +11 -11
  23. package/dist/{mcp-NC5YNTM5.mjs → mcp-7F3G47Z4.mjs} +8 -8
  24. package/dist/people-relationships-worker.mjs +2 -2
  25. package/dist/semantic-index-worker.mjs +1 -1
  26. package/dist/{src-UY4GCVNF.mjs → src-MNH5S423.mjs} +6 -22
  27. package/dist/{stream-import-JKQYRISL.mjs → stream-import-BPCHGRAN.mjs} +7 -7
  28. package/dist/{worker-runtime-KKUOJJTN.mjs → worker-runtime-TTAXGQ5R.mjs} +5 -5
  29. package/dist-web/assets/{ActionToolsPanel-BL0GQR6r.css → ActionToolsPanel-CbWhu_Uv.css} +1 -1
  30. package/dist-web/assets/ActionToolsPanel.vue_vue_type_script_setup_true_lang-CvvNqXuh.js +110 -0
  31. package/dist-web/assets/{BufferResource-i1amDJTV.js → BufferResource-Dh2kaMuP.js} +1 -1
  32. package/dist-web/assets/{CanvasRenderer-DGa7AjdC.js → CanvasRenderer-B8hHywwz.js} +1 -1
  33. package/dist-web/assets/{Filter-BTlq0yU-.js → Filter-Bap1KMW6.js} +1 -1
  34. package/dist-web/assets/{MessageExportModal-D9C5t_zF.js → MessageExportModal-H3-LAaXY.js} +1 -1
  35. package/dist-web/assets/{PageHeader-BEWr2vsg.js → PageHeader-CzTV_lyM.js} +1 -1
  36. package/dist-web/assets/{RelationshipGalaxyCanvas-CSgPNRif.js → RelationshipGalaxyCanvas-C6ZoUZNc.js} +3 -3
  37. package/dist-web/assets/{RelationshipGalaxyThreeCanvas-CBAnU8vN.js → RelationshipGalaxyThreeCanvas-DV6B-7XB.js} +1 -1
  38. package/dist-web/assets/{RenderTargetSystem-Ctlu222X.js → RenderTargetSystem-Dpgyoac_.js} +1 -1
  39. package/dist-web/assets/{TimeSelect.vue_vue_type_script_setup_true_lang-D5vov0eO.js → TimeSelect.vue_vue_type_script_setup_true_lang--Tbo7p-l.js} +1 -1
  40. package/dist-web/assets/{WebGLRenderer-D2Zvhmcw.js → WebGLRenderer-DICnsAm5.js} +1 -1
  41. package/dist-web/assets/{WebGPURenderer-BOtP6h9o.js → WebGPURenderer-Bf0gkRK9.js} +1 -1
  42. package/dist-web/assets/{base-BMjDSaj6.js → base-D5T8QZ8b.js} +1 -1
  43. package/dist-web/assets/{browserAll-BhwLvWQY.js → browserAll-DcoCy7xY.js} +1 -1
  44. package/dist-web/assets/{dateFormat-dteP1INW.js → dateFormat-BDOkdsCR.js} +1 -1
  45. package/dist-web/assets/{electron-DCgfulPE.js → electron-CmhzSjaH.js} +1 -1
  46. package/dist-web/assets/{fetch-BbU4MPPu.js → fetch-BJka-sCN.js} +1 -1
  47. package/dist-web/assets/{fetch-CubnKpLG.js → fetch-BddgpyKJ.js} +1 -1
  48. package/dist-web/assets/{fetch-DgXWr9nB.js → fetch-C1mlq5VY.js} +1 -1
  49. package/dist-web/assets/{fetch-rJA9qLvd.js → fetch-CNz28Lv5.js} +1 -1
  50. package/dist-web/assets/{fetch-D5BrGna5.js → fetch-D71bG6Fc.js} +1 -1
  51. package/dist-web/assets/{fetch-CpvfCafG.js → fetch-D8xv4lgE.js} +1 -1
  52. package/dist-web/assets/{fetch-6q7oz7ZO.js → fetch-DfTM4Dah.js} +1 -1
  53. package/dist-web/assets/{fetch-B_0HIxs5.js → fetch-DtcNaN3d.js} +1 -1
  54. package/dist-web/assets/{fetch-DvZuLuTE.js → fetch-TATpgXht.js} +1 -1
  55. package/dist-web/assets/{fetch-BxHdB6X1.js → fetch-dI5w8lYN.js} +1 -1
  56. package/dist-web/assets/{index-DXcFBsM1.js → index-Bs4ZX2-v.js} +1 -1
  57. package/dist-web/assets/index-CF59LLOG.js +25 -0
  58. package/dist-web/assets/{index-CLijvqy3.js → index-CJ3WtSLo.js} +1 -1
  59. package/dist-web/assets/{index-D7aKwT6w.js → index-CJS6FVuj.js} +3 -3
  60. package/dist-web/assets/{index-Cj2GU1SR.js → index-CY5yJhCJ.js} +1 -1
  61. package/dist-web/assets/{index-DaZnh1zP.js → index-DClxj4T2.js} +2 -2
  62. package/dist-web/assets/{index-BoalB6nB.js → index-De3ImZb9.js} +1 -1
  63. package/dist-web/assets/{index-CUzFFw5b.js → index-Dp3gCHpK.js} +1 -1
  64. package/dist-web/assets/{index-Bp5VKUF9.js → index-DphjBwbA.js} +1 -1
  65. package/dist-web/assets/{index-Bs15Yc7I.js → index-Dtbu8m1o.js} +1 -1
  66. package/dist-web/assets/{index-t6s0nOlb.js → index-a1ZrlsKo.js} +1 -1
  67. package/dist-web/assets/{index-BfGKlkx3.js → index-qZ7jnIna.js} +2 -2
  68. package/dist-web/assets/{index-WJCmqKnh.js → index-soX5gDgh.js} +1 -1
  69. package/dist-web/assets/{web-BoTDL3HM.js → web-H9T1dZys.js} +1 -1
  70. package/dist-web/assets/{webworkerAll-c0ZvpENM.js → webworkerAll-D1Kui8G8.js} +1 -1
  71. package/dist-web/index.html +1 -1
  72. package/package.json +1 -1
  73. package/dist/chunk-5G2INROS.mjs.map +0 -1
  74. package/dist/chunk-67HNEIKY.mjs.map +0 -1
  75. package/dist/chunk-SJQVAKFA.mjs.map +0 -1
  76. package/dist/chunk-XZ5FFDLX.mjs.map +0 -1
  77. package/dist-web/assets/ActionToolsPanel.vue_vue_type_script_setup_true_lang-DVdpZpfD.js +0 -110
  78. package/dist-web/assets/index-DeV8SAiM.js +0 -25
  79. /package/dist/{chat-command-V6NYTTPD.mjs.map → chat-command-QPL4GNHP.mjs.map} +0 -0
  80. /package/dist/{chunk-7TDYBU5L.mjs.map → chunk-3SZWD7UM.mjs.map} +0 -0
  81. /package/dist/{chunk-WL2ISRYH.mjs.map → chunk-64FKPYPJ.mjs.map} +0 -0
  82. /package/dist/{chunk-LWC7JQ2W.mjs.map → chunk-6JKKHPXY.mjs.map} +0 -0
  83. /package/dist/{chunk-ZF67S6RY.mjs.map → chunk-F2NZ7HAY.mjs.map} +0 -0
  84. /package/dist/{chunk-HUKZ2PBC.mjs.map → chunk-OAXVKNPN.mjs.map} +0 -0
  85. /package/dist/{chunk-54GVCXHZ.mjs.map → chunk-OQ6N4PDD.mjs.map} +0 -0
  86. /package/dist/{chunk-OIZ64ET3.mjs.map → chunk-ZCXCHKQV.mjs.map} +0 -0
  87. /package/dist/{http-AMDWRKOH.mjs.map → http-NOEWVWCP.mjs.map} +0 -0
  88. /package/dist/{import-WIJHB4LH.mjs.map → import-RS3HW7ZM.mjs.map} +0 -0
  89. /package/dist/{mcp-NC5YNTM5.mjs.map → mcp-7F3G47Z4.mjs.map} +0 -0
  90. /package/dist/{src-UY4GCVNF.mjs.map → src-MNH5S423.mjs.map} +0 -0
  91. /package/dist/{stream-import-JKQYRISL.mjs.map → stream-import-BPCHGRAN.mjs.map} +0 -0
  92. /package/dist/{worker-runtime-KKUOJJTN.mjs.map → worker-runtime-TTAXGQ5R.mjs.map} +0 -0
@@ -9,21 +9,21 @@ import {
9
9
  createCliRunAgentStream,
10
10
  getServerAiLogger,
11
11
  initServerAiLogger
12
- } from "./chunk-7TDYBU5L.mjs";
12
+ } from "./chunk-3SZWD7UM.mjs";
13
13
  import {
14
14
  AGENT_TOOL_REGISTRY,
15
15
  CoreDataProvider
16
- } from "./chunk-LWC7JQ2W.mjs";
16
+ } from "./chunk-6JKKHPXY.mjs";
17
17
  import {
18
18
  analyzeIncrementalImport,
19
19
  analyzeNewImport,
20
20
  autoImport,
21
21
  incrementalImport,
22
22
  streamImport
23
- } from "./chunk-OIZ64ET3.mjs";
23
+ } from "./chunk-ZCXCHKQV.mjs";
24
24
  import {
25
25
  assertCliDataDirCompatible
26
- } from "./chunk-54GVCXHZ.mjs";
26
+ } from "./chunk-OQ6N4PDD.mjs";
27
27
  import {
28
28
  getVersion
29
29
  } from "./chunk-5ADKYYNA.mjs";
@@ -41,7 +41,7 @@ import {
41
41
  getPendingNodeDataDirMigration,
42
42
  hasPendingElectronDataWarning,
43
43
  serializeChatLabToJsonl
44
- } from "./chunk-SJQVAKFA.mjs";
44
+ } from "./chunk-S5PQM5J4.mjs";
45
45
  import {
46
46
  AIChatManager,
47
47
  ArchiveImportError,
@@ -52,6 +52,7 @@ import {
52
52
  IMPORT_IN_PROGRESS_ERROR_KEY,
53
53
  ImportIdempotencyCache,
54
54
  ImportInProgressError,
55
+ LLMConfigStore,
55
56
  MergeSessionCache,
56
57
  SEMANTIC_INDEX_CONFIG_FILE,
57
58
  SemanticIndexConfigStore,
@@ -60,10 +61,7 @@ import {
60
61
  buildPiModel,
61
62
  computeWordFrequency,
62
63
  countMessagesTokens,
63
- createAuthProfileLlmConfigStore,
64
- createChatLabTempDir,
65
64
  createContactsService,
66
- createFileConfigStorage,
67
65
  createGlobalInsightService,
68
66
  createPeopleRelationshipsService,
69
67
  deleteDict,
@@ -75,14 +73,12 @@ import {
75
73
  export_service_exports,
76
74
  fetchRemoteModels,
77
75
  findEntryFileInDirectory,
78
- getChatLabTempScopeDir,
79
76
  getDefaultRulesForLocale,
80
77
  getDictList,
81
78
  getSupportedFormats,
82
79
  hashImportBody,
83
80
  isDictDownloaded,
84
81
  isSemanticIndexConfigured,
85
- isValidImportSessionId,
86
82
  logNativeParserStatus,
87
83
  member_service_exports,
88
84
  mergeRulesForLocale,
@@ -98,7 +94,7 @@ import {
98
94
  streamParseFileInfo,
99
95
  summary_service_exports,
100
96
  validateApiKey
101
- } from "./chunk-5G2INROS.mjs";
97
+ } from "./chunk-GYPBWKAU.mjs";
102
98
  import {
103
99
  resolveCliPath
104
100
  } from "./chunk-FJ35V3TE.mjs";
@@ -114,6 +110,7 @@ import {
114
110
  POS_TAG_DEFINITIONS,
115
111
  appLogger,
116
112
  createDatabaseManagerAdapter,
113
+ deleteAuthProfile,
117
114
  executeReadonlySql,
118
115
  executeSql,
119
116
  getAvailableYears,
@@ -132,7 +129,8 @@ import {
132
129
  getLongMessageCount,
133
130
  getMemberActivity,
134
131
  getMemberMonthlyTrend,
135
- getMembersWithAliases,
132
+ getMembers,
133
+ getMembersDetailed,
136
134
  getMemeBattleAnalysis,
137
135
  getMentionAnalysis,
138
136
  getMentionGraph,
@@ -157,16 +155,18 @@ import {
157
155
  isNewerStableVersion,
158
156
  loadConfig,
159
157
  queryMessages,
158
+ resolveApiKey,
160
159
  stripAvatarFields,
161
160
  verifyCliDataPath,
161
+ writeAuthProfile,
162
162
  writeConfigField
163
- } from "./chunk-XZ5FFDLX.mjs";
163
+ } from "./chunk-IK7GJTGY.mjs";
164
164
 
165
165
  // src/http/index.ts
166
166
  import * as fs12 from "fs";
167
167
  import * as crypto4 from "crypto";
168
168
 
169
- // ../../packages/http-routes/src/server.ts
169
+ // src/http/server.ts
170
170
  import Fastify from "fastify";
171
171
 
172
172
  // ../../packages/http-routes/src/errors.ts
@@ -217,8 +217,8 @@ function exportTooLarge(count, limit) {
217
217
  function serverError(message = "Internal server error") {
218
218
  return new ApiError("SERVER_ERROR" /* SERVER_ERROR */, message);
219
219
  }
220
- function importInProgress(message = "Another import is already in progress") {
221
- return new ApiError("IMPORT_IN_PROGRESS" /* IMPORT_IN_PROGRESS */, message);
220
+ function importInProgress() {
221
+ return new ApiError("IMPORT_IN_PROGRESS" /* IMPORT_IN_PROGRESS */, "Another import is already in progress");
222
222
  }
223
223
  function idempotencyConflict() {
224
224
  return new ApiError("IDEMPOTENCY_CONFLICT" /* IDEMPOTENCY_CONFLICT */, "Same Idempotency-Key with different request body hash");
@@ -238,18 +238,7 @@ function dataDirIncompatible(message) {
238
238
  function apiErrorFromUnknown(error) {
239
239
  if (error instanceof ApiError) return error;
240
240
  if (error instanceof ImportInProgressError) return importInProgress();
241
- const compatibilityError = findDataDirCompatibilityError(error);
242
- if (compatibilityError) return dataDirIncompatible(compatibilityError.message);
243
- return null;
244
- }
245
- function findDataDirCompatibilityError(error) {
246
- const seen = /* @__PURE__ */ new Set();
247
- let current = error;
248
- while (current instanceof Error && !seen.has(current)) {
249
- if (current instanceof DataDirCompatibilityError) return current;
250
- seen.add(current);
251
- current = current.cause;
252
- }
241
+ if (error instanceof DataDirCompatibilityError) return dataDirIncompatible(error.message);
253
242
  return null;
254
243
  }
255
244
  function successResponse(data, meta) {
@@ -273,378 +262,195 @@ function errorResponse(error) {
273
262
  };
274
263
  }
275
264
 
276
- // ../../packages/http-routes/src/server.ts
277
- var DEFAULT_JSON_BODY_LIMIT = 50 * 1024 * 1024;
278
- function configureApiErrorHandler(server2, options = {}) {
279
- server2.setErrorHandler((error, request, reply) => {
280
- const apiError = apiErrorFromUnknown(error);
281
- if (apiError) {
282
- reply.code(apiError.statusCode).send(errorResponse(apiError));
283
- return;
284
- }
285
- if (error.statusCode === 413) {
286
- const bodyError = new ApiError("BODY_TOO_LARGE" /* BODY_TOO_LARGE */, "Request body exceeds 50MB limit");
287
- reply.code(bodyError.statusCode).send(errorResponse(bodyError));
288
- return;
289
- }
290
- const statusCode = error.statusCode;
291
- if (statusCode && statusCode >= 400 && statusCode < 600) {
292
- reply.code(statusCode).send({ success: false, error: { code: "CLIENT_ERROR", message: error.message } });
293
- return;
294
- }
295
- options.onUnhandledError?.(request, error);
296
- const internalError = serverError(error.message);
297
- reply.code(internalError.statusCode).send(errorResponse(internalError));
298
- });
299
- }
300
- function createApiServer(options = {}) {
301
- const server2 = Fastify({
302
- logger: false,
303
- bodyLimit: options.bodyLimit ?? DEFAULT_JSON_BODY_LIMIT
304
- });
305
- if (options.authHook) server2.addHook("onRequest", options.authHook);
306
- configureApiErrorHandler(server2, options);
307
- return server2;
308
- }
309
-
310
- // ../../packages/http-routes/src/auth.ts
311
- import { timingSafeEqual, createHmac, randomBytes } from "crypto";
312
- function createBearerAuthHook(options) {
313
- const hmacKey = randomBytes(32);
314
- const safeTokenCompare = (provided, expected) => {
315
- const providedHash = createHmac("sha256", hmacKey).update(provided).digest();
316
- const expectedHash = createHmac("sha256", hmacKey).update(expected).digest();
317
- return timingSafeEqual(providedHash, expectedHash);
318
- };
319
- return async function bearerAuthHook(request, reply) {
320
- if (options.shouldAuthenticate && !options.shouldAuthenticate(request)) return;
321
- const expectedToken = options.getToken();
322
- if (!expectedToken && options.allowMissingToken !== false) return;
323
- const authHeader = request.headers.authorization;
324
- if (!authHeader || !authHeader.startsWith("Bearer ")) {
325
- const err = unauthorized(options.missingTokenMessage);
326
- reply.code(err.statusCode).send(errorResponse(err));
327
- return;
328
- }
329
- const providedToken = authHeader.slice(7);
330
- if (!expectedToken || !safeTokenCompare(providedToken, expectedToken)) {
331
- const err = unauthorized(options.invalidTokenMessage);
332
- reply.code(err.statusCode).send(errorResponse(err));
333
- }
334
- };
335
- }
336
-
337
- // src/http/auth.ts
338
- var cachedToken = null;
339
- var requireAuthEnabled = false;
340
- function setAuthToken(token) {
341
- cachedToken = token;
342
- }
343
- function setRequireAuth(enabled) {
344
- requireAuthEnabled = enabled;
345
- }
346
- var authHook = createBearerAuthHook({
347
- getToken: () => cachedToken,
348
- shouldAuthenticate: (request) => request.url.startsWith("/api/") || requireAuthEnabled && request.url.startsWith("/_web/")
349
- });
350
-
351
- // src/http/server.ts
352
- function createServer() {
353
- return createApiServer({
354
- authHook,
355
- onUnhandledError: (request, error) => {
356
- appLogger.error("http", `${request.method} ${request.url} -> 500`, error);
265
+ // ../../packages/http-routes/src/routes/system.ts
266
+ function registerSystemRoutes(server2, ctx) {
267
+ server2.get("/api/v1/status", async () => {
268
+ let sessionCount = 0;
269
+ try {
270
+ sessionCount = ctx.dbManager.listSessionIds().length;
271
+ } catch {
357
272
  }
273
+ return successResponse({
274
+ name: "ChatLab API",
275
+ version: ctx.getVersion(),
276
+ uptime: Math.floor(process.uptime()),
277
+ sessionCount
278
+ });
358
279
  });
359
- }
360
-
361
- // src/http/routes/web/index.ts
362
- import * as os from "os";
363
- import * as path10 from "path";
364
-
365
- // ../../packages/http-routes/src/import/json-push-handler.ts
366
- var INVALID_SESSION_ID_MESSAGE = "sessionId must be 1-128 safe characters, start with [A-Za-z0-9_@-], and not contain '..'";
367
- function buildImportIdempotencyCacheKey(idempotencyKey, sessionId, dryRun, includeDryRun) {
368
- if (!idempotencyKey) return null;
369
- return includeDryRun ? `${idempotencyKey}:${sessionId}:${dryRun}` : `${idempotencyKey}:${sessionId}`;
370
- }
371
- function errorResult(error) {
372
- return { statusCode: error.statusCode, response: errorResponse(error) };
373
- }
374
- function analysisResponse(sessionId, outcome) {
375
- const analysis = {
376
- totalInFile: outcome.result.totalInFile,
377
- newMessageCount: outcome.result.newMessageCount,
378
- duplicateCount: outcome.result.duplicateCount
379
- };
380
- return successResponse({
381
- sessionId,
382
- created: outcome.result.created,
383
- dryRun: true,
384
- analysis: outcome.result.created ? { ...analysis, newMemberCount: outcome.result.newMemberCount } : analysis
385
- });
386
- }
387
- function createJsonPushImportHandler(options) {
388
- const idempotencyCache = options.idempotencyCache ?? new ImportIdempotencyCache();
389
- return async (request) => {
390
- const contentType = request.contentType.toLowerCase();
391
- if (!contentType.includes("application/json")) {
392
- return errorResult(invalidPayload("Content-Type must be application/json (JSONL is not yet supported)"));
393
- }
394
- if (!isValidImportSessionId(request.sessionId)) {
395
- return errorResult(invalidPayload(INVALID_SESSION_ID_MESSAGE));
396
- }
397
- const dryRun = request.dryRun === true;
398
- if (dryRun && !options.analyze) {
399
- return errorResult(invalidPayload("X-Dry-Run is not yet supported"));
400
- }
401
- const cacheKey = buildImportIdempotencyCacheKey(
402
- request.idempotencyKey,
403
- request.sessionId,
404
- dryRun,
405
- options.includeDryRunInIdempotencyKey === true
406
- );
407
- if (cacheKey) {
408
- const start = idempotencyCache.start(cacheKey, hashImportBody(request.body));
409
- if (start.status === "conflict") return errorResult(idempotencyConflict());
410
- if (start.status === "pending") return errorResult(idempotencyPending());
411
- if (start.status === "success") return { statusCode: 200, response: start.response };
412
- }
413
- let acquired = false;
414
- try {
415
- if (options.acquire) {
416
- acquired = await options.acquire(request.sessionId);
417
- if (!acquired) {
418
- if (cacheKey) idempotencyCache.fail(cacheKey);
419
- return errorResult(importInProgress(options.importInProgressMessage));
420
- }
421
- }
422
- const payload = request.body ?? {};
423
- if (dryRun) {
424
- const outcome2 = await options.analyze(request.sessionId, payload);
425
- if (!outcome2.ok) {
426
- if (cacheKey) idempotencyCache.fail(cacheKey);
427
- const error = outcome2.reason === "invalid_payload" ? invalidPayload(outcome2.message) : importFailed(outcome2.message);
428
- return errorResult(error);
429
- }
430
- const response2 = analysisResponse(request.sessionId, outcome2);
431
- if (cacheKey) idempotencyCache.success(cacheKey, response2);
432
- return { statusCode: 200, response: response2 };
433
- }
434
- const outcome = await options.execute(request.sessionId, payload);
435
- if (!outcome.ok) {
436
- if (cacheKey) idempotencyCache.fail(cacheKey);
437
- const error = outcome.reason === "import_in_progress" ? importInProgress(options.importInProgressMessage) : outcome.reason === "invalid_payload" ? invalidPayload(outcome.message) : importFailed(outcome.message);
438
- return errorResult(error);
439
- }
440
- const response = successResponse(outcome.result);
441
- if (cacheKey) idempotencyCache.success(cacheKey, response);
442
- try {
443
- await options.onSuccess?.(outcome.result);
444
- } catch (error) {
445
- options.onError?.(error);
446
- }
447
- return { statusCode: 200, response };
448
- } catch (error) {
449
- if (cacheKey) idempotencyCache.fail(cacheKey);
450
- options.onError?.(error);
451
- const apiError = apiErrorFromUnknown(error);
452
- if (apiError) return errorResult(apiError);
453
- return errorResult(importFailed(error instanceof Error ? error.message : "Import process error"));
454
- } finally {
455
- if (acquired) {
456
- try {
457
- await options.release?.(request.sessionId);
458
- } catch (error) {
459
- options.onError?.(error);
280
+ server2.get("/api/v1/schema", async () => {
281
+ return successResponse({
282
+ format: "ChatLab Format",
283
+ version: "0.0.2",
284
+ spec: {
285
+ chatlab: {
286
+ type: "object",
287
+ required: ["version"],
288
+ properties: {
289
+ version: { type: "string" },
290
+ exportedAt: { type: "number" },
291
+ generator: { type: "string" }
292
+ }
293
+ },
294
+ meta: {
295
+ type: "object",
296
+ required: ["name", "platform", "type"],
297
+ properties: {
298
+ name: { type: "string" },
299
+ platform: {
300
+ type: "string",
301
+ enum: ["qq", "wechat", "telegram", "discord", "line", "whatsapp", "instagram", "unknown"]
302
+ },
303
+ type: { type: "string", enum: ["group", "private"] },
304
+ groupId: { type: "string" }
305
+ }
306
+ },
307
+ members: {
308
+ type: "array",
309
+ items: {
310
+ type: "object",
311
+ required: ["platformId", "accountName"],
312
+ properties: {
313
+ platformId: { type: "string" },
314
+ accountName: { type: "string" },
315
+ groupNickname: { type: "string" },
316
+ avatar: { type: "string" }
317
+ }
318
+ }
319
+ },
320
+ messages: {
321
+ type: "array",
322
+ items: {
323
+ type: "object",
324
+ required: ["sender", "timestamp", "type"],
325
+ properties: {
326
+ platformMessageId: { type: "string" },
327
+ sender: { type: "string" },
328
+ accountName: { type: "string" },
329
+ groupNickname: { type: "string" },
330
+ timestamp: { type: "number" },
331
+ type: { type: "number" },
332
+ content: { type: ["string", "null"] },
333
+ replyToMessageId: { type: "string" }
334
+ }
335
+ }
460
336
  }
461
337
  }
462
- }
463
- };
464
- }
465
-
466
- // ../../packages/http-routes/src/routes/rest/imports.ts
467
- function registerImportRoutes(server2, ctx) {
468
- const handleJsonPushImport = createJsonPushImportHandler({
469
- execute: (sessionId, payload) => pushImport(ctx.dbManager, sessionId, payload),
470
- onError: (error) => appLogger.error("http-import", "Push import request failed", error)
338
+ });
471
339
  });
472
- server2.post(
473
- "/api/v1/imports/:sessionId",
474
- async (request, reply) => {
475
- const idempotencyKey = request.headers["idempotency-key"];
476
- const result = await handleJsonPushImport({
477
- sessionId: request.params.sessionId,
478
- body: request.body,
479
- contentType: request.headers["content-type"] || "",
480
- idempotencyKey: typeof idempotencyKey === "string" ? idempotencyKey : void 0,
481
- dryRun: request.headers["x-dry-run"] === "true"
482
- });
483
- return reply.code(result.statusCode).send(result.response);
484
- }
485
- );
486
340
  }
487
341
 
488
- // ../../packages/http-routes/src/routes/rest/session-provider.ts
489
- function normalizeRestSessionMember(member) {
490
- return {
491
- ...member,
492
- name: member.name || member.groupNickname || member.accountName || member.platformId
493
- };
494
- }
495
- function getSummary(dbManager2, sessionId) {
496
- const db = dbManager2.open(sessionId);
497
- if (!db) return null;
498
- const info = getSessionInfo(db);
499
- if (!info) return null;
500
- return {
501
- id: sessionId,
502
- name: info.name,
503
- platform: info.platform,
504
- type: info.type,
505
- groupId: info.groupId || void 0,
506
- messageCount: info.messageCount,
507
- memberCount: info.memberCount,
508
- firstTimestamp: info.firstMessageTs,
509
- lastTimestamp: info.lastMessageTs,
510
- importedAt: info.importedAt
511
- };
512
- }
513
- function getDetail(dbManager2, sessionId) {
514
- const summary = getSummary(dbManager2, sessionId);
515
- if (!summary) return null;
516
- const db = dbManager2.open(sessionId);
517
- if (!db) return null;
518
- return {
519
- ...summary,
520
- lastPlatformMessageId: getLastPlatformMessageId(db),
521
- importedAt: summary.importedAt ?? 0
522
- };
342
+ // ../../packages/http-routes/src/routes/sessions.ts
343
+ var EXPORT_MESSAGE_LIMIT = 1e5;
344
+ function ensureDb(ctx, sessionId) {
345
+ const db = ctx.dbManager.open(sessionId);
346
+ if (!db) throw sessionNotFound(sessionId);
347
+ return db;
523
348
  }
524
- function createDatabaseRestSessionProvider(dbManager2) {
525
- return {
526
- countSessions: () => dbManager2.listSessionIds().length,
527
- listSessions: () => dbManager2.listSessionIds().map((sessionId) => getSummary(dbManager2, sessionId)).filter((session) => session !== null),
528
- getSession: (sessionId) => getDetail(dbManager2, sessionId),
529
- queryMessages: (sessionId, query) => {
530
- const db = dbManager2.open(sessionId);
531
- if (!db) return null;
532
- return queryMessages(db, {
533
- keyword: query.keyword,
534
- startTs: query.startTime,
535
- endTs: query.endTime,
536
- senderId: query.senderId,
537
- limit: query.limit,
538
- offset: query.offset
539
- });
540
- },
541
- getMembers: (sessionId) => {
542
- const db = dbManager2.open(sessionId);
543
- return db ? getMembersWithAliases(db).map(normalizeRestSessionMember) : null;
544
- },
545
- getOverview: (sessionId) => {
546
- const db = dbManager2.open(sessionId);
547
- if (!db) return null;
548
- const overview = getSessionOverview(db);
549
- const memberActivity = getMemberActivity(db);
550
- const typeDistribution = getMessageTypeStats(db);
551
- const messageTypeDistribution = {};
552
- for (const item of typeDistribution) {
553
- messageTypeDistribution[String(item.type)] = item.count;
554
- }
555
- return {
556
- messageCount: overview.totalMessages,
557
- memberCount: overview.totalMembers,
558
- timeRange: {
559
- start: overview.firstMessageTs ?? 0,
560
- end: overview.lastMessageTs ?? 0
561
- },
562
- messageTypeDistribution,
563
- topMembers: memberActivity.slice(0, 10).map((member) => ({
564
- platformId: member.platformId,
565
- name: member.name,
566
- messageCount: member.messageCount,
567
- percentage: member.percentage
568
- }))
569
- };
570
- },
571
- executeReadonlySql: (sessionId, sql) => {
572
- const db = dbManager2.open(sessionId);
573
- return db ? executeReadonlySql(db, sql) : null;
574
- },
575
- getExportData: (sessionId, limit) => {
576
- const db = dbManager2.open(sessionId);
349
+ function registerRestSessionRoutes(server2, ctx) {
350
+ server2.get("/api/v1/sessions", async () => {
351
+ const sessionIds = ctx.dbManager.listSessionIds();
352
+ const sessions = sessionIds.map((id) => {
353
+ const db = ctx.dbManager.open(id);
577
354
  if (!db) return null;
578
- const session = getDetail(dbManager2, sessionId);
579
- const meta = getSessionMeta(db);
580
- if (!session || !meta) return null;
581
- const members = getMembersWithAliases(db);
582
- const messages = queryMessages(db, { limit, offset: 0 }).messages;
355
+ const info = getSessionInfo(db);
356
+ if (!info) return null;
583
357
  return {
584
- members,
585
- messages
358
+ id,
359
+ name: info.name,
360
+ platform: info.platform,
361
+ type: info.type,
362
+ groupId: info.groupId || void 0,
363
+ messageCount: info.messageCount,
364
+ memberCount: info.memberCount,
365
+ firstTimestamp: info.firstMessageTs,
366
+ lastTimestamp: info.lastMessageTs
586
367
  };
587
- }
588
- };
589
- }
590
-
591
- // ../../packages/http-routes/src/routes/rest/sessions.ts
592
- var EXPORT_MESSAGE_LIMIT = 1e5;
593
- function registerRestSessionRoutes(server2, provider) {
594
- server2.get("/api/v1/sessions", async () => {
595
- return successResponse(await provider.listSessions());
368
+ }).filter(Boolean);
369
+ return successResponse(sessions);
596
370
  });
597
371
  server2.get("/api/v1/sessions/:id", async (request) => {
598
- const session = await provider.getSession(request.params.id);
599
- if (!session) throw sessionNotFound(request.params.id);
600
- return successResponse(session);
372
+ const db = ensureDb(ctx, request.params.id);
373
+ const info = getSessionInfo(db);
374
+ if (!info) throw sessionNotFound(request.params.id);
375
+ return successResponse({
376
+ id: request.params.id,
377
+ name: info.name,
378
+ platform: info.platform,
379
+ type: info.type,
380
+ groupId: info.groupId || void 0,
381
+ messageCount: info.messageCount,
382
+ memberCount: info.memberCount,
383
+ firstTimestamp: info.firstMessageTs,
384
+ lastTimestamp: info.lastMessageTs,
385
+ lastPlatformMessageId: getLastPlatformMessageId(db),
386
+ importedAt: info.importedAt
387
+ });
601
388
  });
602
389
  server2.get("/api/v1/sessions/:id/messages", async (request) => {
603
390
  const { id } = request.params;
391
+ const db = ensureDb(ctx, id);
604
392
  const page = Math.max(1, parseInt(request.query.page || "1", 10) || 1);
605
393
  const limit = Math.min(1e3, Math.max(1, parseInt(request.query.limit || "100", 10) || 100));
606
394
  const offset = (page - 1) * limit;
607
395
  const { startTime, endTime, keyword, senderId } = request.query;
608
- const result = await provider.queryMessages(id, {
396
+ const result = queryMessages(db, {
609
397
  keyword: keyword || void 0,
610
- startTime: startTime ? parseInt(startTime, 10) : void 0,
611
- endTime: endTime ? parseInt(endTime, 10) : void 0,
398
+ startTs: startTime ? parseInt(startTime, 10) : void 0,
399
+ endTs: endTime ? parseInt(endTime, 10) : void 0,
612
400
  senderId: senderId ? parseInt(senderId, 10) : void 0,
613
401
  limit,
614
402
  offset
615
403
  });
616
- if (!result) throw sessionNotFound(id);
617
404
  return successResponse({
618
405
  messages: result.messages,
619
406
  total: result.total,
620
407
  page,
621
408
  limit,
622
- totalPages: result.totalPages ?? Math.ceil(result.total / limit)
409
+ totalPages: result.totalPages
623
410
  });
624
411
  });
625
412
  server2.get("/api/v1/sessions/:id/members", async (request) => {
626
- const members = await provider.getMembers(request.params.id);
627
- if (!members) throw sessionNotFound(request.params.id);
413
+ const db = ensureDb(ctx, request.params.id);
414
+ const members = getMembers(db);
628
415
  return successResponse(members);
629
416
  });
630
417
  server2.get("/api/v1/sessions/:id/stats/overview", async (request) => {
631
- const overview = await provider.getOverview(request.params.id);
632
- if (!overview) throw sessionNotFound(request.params.id);
633
- return successResponse(overview);
418
+ const { id } = request.params;
419
+ const db = ensureDb(ctx, id);
420
+ const overview = getSessionOverview(db);
421
+ const memberActivity = getMemberActivity(db);
422
+ const typeDistribution = getMessageTypeStats(db);
423
+ const typeMap = {};
424
+ for (const item of typeDistribution) {
425
+ typeMap[String(item.type)] = item.count;
426
+ }
427
+ const topMembers = memberActivity.slice(0, 10).map((m) => ({
428
+ platformId: m.platformId,
429
+ name: m.name,
430
+ messageCount: m.messageCount,
431
+ percentage: m.percentage
432
+ }));
433
+ return successResponse({
434
+ messageCount: overview.totalMessages,
435
+ memberCount: overview.totalMembers,
436
+ timeRange: {
437
+ start: overview.firstMessageTs ?? 0,
438
+ end: overview.lastMessageTs ?? 0
439
+ },
440
+ messageTypeDistribution: typeMap,
441
+ topMembers
442
+ });
634
443
  });
635
444
  server2.post("/api/v1/sessions/:id/sql", async (request, reply) => {
636
445
  const { id } = request.params;
446
+ const db = ensureDb(ctx, id);
637
447
  const { sql } = request.body || {};
638
448
  if (!sql || typeof sql !== "string") {
639
449
  const err = sqlExecutionError("Missing sql parameter");
640
450
  return reply.code(err.statusCode).send(errorResponse(err));
641
451
  }
642
452
  try {
643
- const result = await provider.executeReadonlySql(id, sql);
644
- if (result === null) {
645
- const err = sessionNotFound(id);
646
- return reply.code(err.statusCode).send(errorResponse(err));
647
- }
453
+ const result = executeReadonlySql(db, sql);
648
454
  return successResponse(result);
649
455
  } catch (err) {
650
456
  const message = err instanceof Error ? err.message : "SQL execution error";
@@ -659,14 +465,16 @@ function registerRestSessionRoutes(server2, provider) {
659
465
  });
660
466
  server2.get("/api/v1/sessions/:id/export", async (request, reply) => {
661
467
  const { id } = request.params;
662
- const session = await provider.getSession(id);
663
- if (!session) throw sessionNotFound(id);
664
- if (session.messageCount > EXPORT_MESSAGE_LIMIT) {
665
- const err = exportTooLarge(session.messageCount, EXPORT_MESSAGE_LIMIT);
468
+ const db = ensureDb(ctx, id);
469
+ const meta = getSessionMeta(db);
470
+ if (!meta) throw sessionNotFound(id);
471
+ const overview = getSessionOverview(db);
472
+ if (overview.totalMessages > EXPORT_MESSAGE_LIMIT) {
473
+ const err = exportTooLarge(overview.totalMessages, EXPORT_MESSAGE_LIMIT);
666
474
  return reply.code(err.statusCode).send(errorResponse(err));
667
475
  }
668
- const exportData = await provider.getExportData(id, EXPORT_MESSAGE_LIMIT);
669
- if (!exportData) throw sessionNotFound(id);
476
+ const members = getMembersDetailed(db);
477
+ const allMessages = queryMessages(db, { limit: EXPORT_MESSAGE_LIMIT, offset: 0 });
670
478
  const chatLabFormat = {
671
479
  chatlab: {
672
480
  version: "0.0.2",
@@ -674,18 +482,17 @@ function registerRestSessionRoutes(server2, provider) {
674
482
  generator: "ChatLab API"
675
483
  },
676
484
  meta: {
677
- name: session.name,
678
- platform: session.platform,
679
- type: session.type,
680
- groupId: session.groupId || void 0
485
+ name: meta.name,
486
+ platform: meta.platform,
487
+ type: meta.type,
488
+ groupId: meta.groupId || void 0
681
489
  },
682
- members: exportData.members.map((m) => ({
490
+ members: members.map((m) => ({
683
491
  platformId: m.platformId,
684
492
  accountName: m.accountName || m.platformId,
685
- groupNickname: m.groupNickname || void 0,
686
- aliases: Array.isArray(m.aliases) && m.aliases.length > 0 ? m.aliases : void 0
493
+ groupNickname: m.groupNickname || void 0
687
494
  })),
688
- messages: exportData.messages.map((msg) => ({
495
+ messages: allMessages.messages.map((msg) => ({
689
496
  sender: msg.senderPlatformId,
690
497
  accountName: msg.senderName || void 0,
691
498
  timestamp: msg.timestamp,
@@ -697,898 +504,422 @@ function registerRestSessionRoutes(server2, provider) {
697
504
  });
698
505
  }
699
506
 
700
- // ../../packages/http-routes/src/routes/rest/system.ts
701
- function registerSystemRoutes(server2, ctx) {
702
- server2.get("/api/v1/status", async () => {
703
- let sessionCount = 0;
704
- try {
705
- sessionCount = await ctx.countSessions();
706
- } catch {
507
+ // ../../packages/http-routes/src/routes/imports.ts
508
+ function registerImportRoutes(server2, ctx) {
509
+ const idempotencyCache = new ImportIdempotencyCache();
510
+ server2.post(
511
+ "/api/v1/imports/:sessionId",
512
+ async (request, reply) => {
513
+ const { sessionId } = request.params;
514
+ const contentType = request.headers["content-type"] || "";
515
+ if (!contentType.includes("application/json")) {
516
+ const err = invalidPayload("Content-Type must be application/json (JSONL is not yet supported)");
517
+ return reply.code(err.statusCode).send(errorResponse(err));
518
+ }
519
+ if (request.headers["x-dry-run"] === "true") {
520
+ const err = invalidPayload("X-Dry-Run is not yet supported");
521
+ return reply.code(err.statusCode).send(errorResponse(err));
522
+ }
523
+ const idempotencyKey = request.headers["idempotency-key"];
524
+ const cacheKey = typeof idempotencyKey === "string" && idempotencyKey ? `${idempotencyKey}:${sessionId}` : null;
525
+ if (cacheKey) {
526
+ const start = idempotencyCache.start(cacheKey, hashImportBody(request.body));
527
+ if (start.status === "conflict") {
528
+ const err = idempotencyConflict();
529
+ return reply.code(err.statusCode).send(errorResponse(err));
530
+ }
531
+ if (start.status === "pending") {
532
+ const err = idempotencyPending();
533
+ return reply.code(err.statusCode).send(errorResponse(err));
534
+ }
535
+ if (start.status === "success") return start.response;
536
+ }
537
+ try {
538
+ const outcome = await pushImport(ctx.dbManager, sessionId, request.body ?? {});
539
+ if (!outcome.ok) {
540
+ if (cacheKey) idempotencyCache.fail(cacheKey);
541
+ if (outcome.reason === "import_in_progress") {
542
+ const err2 = importInProgress();
543
+ return reply.code(err2.statusCode).send(errorResponse(err2));
544
+ }
545
+ if (outcome.reason === "invalid_payload") {
546
+ const err2 = invalidPayload(outcome.message);
547
+ return reply.code(err2.statusCode).send(errorResponse(err2));
548
+ }
549
+ const err = importFailed(outcome.message);
550
+ return reply.code(err.statusCode).send(errorResponse(err));
551
+ }
552
+ const response = successResponse(outcome.result);
553
+ if (cacheKey) idempotencyCache.success(cacheKey, response);
554
+ return response;
555
+ } catch (error) {
556
+ if (cacheKey) idempotencyCache.fail(cacheKey);
557
+ throw error;
558
+ }
707
559
  }
708
- return successResponse({
709
- name: "ChatLab API",
710
- version: ctx.getVersion(),
711
- uptime: Math.floor(process.uptime()),
712
- sessionCount
560
+ );
561
+ }
562
+
563
+ // ../../packages/http-routes/src/routes/web/sessions.ts
564
+ function registerSessionRoutes(server2, ctx) {
565
+ const { sessionAdapter: adapter } = ctx;
566
+ let preferencesInstance = null;
567
+ const preferences = () => {
568
+ preferencesInstance ??= ctx.preferencesManager ?? new PreferencesManager(ctx.pathProvider.getSystemDir());
569
+ return preferencesInstance;
570
+ };
571
+ server2.get("/_web/sessions", async () => {
572
+ const aiChatCounts = ctx.aiChatManager?.getAIChatCountsBySession();
573
+ return session_service_exports.listAnalysisSessions(adapter, {
574
+ enrichSession: aiChatCounts?.size ? (dto) => ({ ...dto, aiConversationCount: aiChatCounts.get(dto.id) ?? 0 }) : void 0
713
575
  });
714
576
  });
715
- server2.get("/api/v1/schema", async () => {
716
- return successResponse({
717
- format: "ChatLab Format",
718
- version: "0.0.2",
719
- spec: {
720
- chatlab: {
721
- type: "object",
722
- required: ["version"],
723
- properties: {
724
- version: { type: "string" },
725
- exportedAt: { type: "number" },
726
- generator: { type: "string" }
727
- }
728
- },
729
- meta: {
730
- type: "object",
731
- required: ["name", "platform", "type"],
732
- properties: {
733
- name: { type: "string" },
734
- platform: {
735
- type: "string",
736
- enum: ["qq", "wechat", "telegram", "discord", "line", "whatsapp", "instagram", "unknown"]
737
- },
738
- type: { type: "string", enum: ["group", "private"] },
739
- groupId: { type: "string" }
740
- }
741
- },
742
- members: {
743
- type: "array",
744
- items: {
745
- type: "object",
746
- required: ["platformId", "accountName"],
747
- properties: {
748
- platformId: { type: "string" },
749
- accountName: { type: "string" },
750
- groupNickname: { type: "string" },
751
- avatar: { type: "string" }
752
- }
753
- }
754
- },
755
- messages: {
756
- type: "array",
757
- items: {
758
- type: "object",
759
- required: ["sender", "timestamp", "type"],
760
- properties: {
761
- platformMessageId: { type: "string" },
762
- sender: { type: "string" },
763
- accountName: { type: "string" },
764
- groupNickname: { type: "string" },
765
- timestamp: { type: "number" },
766
- type: { type: "number" },
767
- content: { type: ["string", "null"] },
768
- replyToMessageId: { type: "string" }
769
- }
770
- }
771
- }
772
- }
773
- });
577
+ server2.get("/_web/sessions/:id", async (request) => {
578
+ const session = session_service_exports.getAnalysisSession(adapter, request.params.id);
579
+ if (!session) {
580
+ throw Object.assign(new Error(`Session not found: ${request.params.id}`), { statusCode: 404 });
581
+ }
582
+ if (ctx.aiChatManager) {
583
+ session.aiConversationCount = ctx.aiChatManager.getAIChats(request.params.id).length;
584
+ }
585
+ return session;
774
586
  });
775
- }
776
-
777
- // ../../packages/http-routes/src/routes/rest/register.ts
778
- function registerRestRoutes(server2, ctx) {
779
- const sessionProvider = ctx.restSessionProvider ?? createDatabaseRestSessionProvider(ctx.dbManager);
780
- registerSystemRoutes(server2, {
781
- getVersion: ctx.getVersion,
782
- countSessions: sessionProvider.countSessions
783
- });
784
- registerRestSessionRoutes(server2, sessionProvider);
785
- registerImportRoutes(server2, ctx);
786
- }
787
-
788
- // ../../packages/http-routes/src/routes/web/ai-agent-stream.ts
789
- var activeAgentAborts = /* @__PURE__ */ new Map();
790
- function registerAiAgentStreamRoutes(server2, ctx) {
791
- if (!ctx.runAgentStream) return;
792
- const runAgentStream = ctx.runAgentStream;
793
- server2.post("/_web/ai/agent/stream", async (request, reply) => {
794
- const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
795
- const abortController = new AbortController();
796
- activeAgentAborts.set(requestId, abortController);
797
- reply.raw.writeHead(200, {
798
- "Content-Type": "text/event-stream",
799
- "Cache-Control": "no-cache",
800
- Connection: "keep-alive",
801
- "X-Request-Id": requestId
802
- });
803
- let emittedDone = false;
804
- const safeSendSSE = (event, data) => {
805
- if (reply.raw.writableEnded || reply.raw.destroyed) return;
806
- reply.raw.write(`event: ${event}
807
- data: ${JSON.stringify(data)}
808
-
809
- `);
810
- };
811
- const safeEnd = () => {
812
- if (reply.raw.writableEnded || reply.raw.destroyed) return;
813
- reply.raw.end();
814
- };
815
- safeSendSSE("meta", { requestId });
816
- reply.raw.on("close", () => {
817
- if (!abortController.signal.aborted) {
818
- abortController.abort();
819
- }
820
- activeAgentAborts.delete(requestId);
821
- });
587
+ server2.delete("/_web/sessions/:id", async (request, reply) => {
588
+ const { id } = request.params;
822
589
  try {
823
- await runAgentStream(
824
- request.body,
825
- (chunk) => {
826
- if (chunk.type === "done") emittedDone = true;
827
- safeSendSSE(chunk.type, chunk);
828
- },
829
- abortController.signal
830
- );
831
- } catch (error) {
832
- const msg = error instanceof Error ? error.message : String(error);
833
- safeSendSSE("error", { type: "error", error: { name: "ServerError", message: msg } });
834
- } finally {
835
- if (!emittedDone) safeSendSSE("done", { type: "done", isFinished: true });
836
- activeAgentAborts.delete(requestId);
837
- safeEnd();
590
+ const deleted = session_service_exports.deleteSession(adapter, id);
591
+ if (!deleted) {
592
+ return reply.code(404).send({ success: false, error: "File not found" });
593
+ }
594
+ return { success: true };
595
+ } catch (err) {
596
+ return reply.code(500).send({ success: false, error: String(err) });
838
597
  }
839
598
  });
840
- server2.post("/_web/ai/agent/abort", async (request) => {
841
- const { requestId } = request.body;
842
- const controller = activeAgentAborts.get(requestId);
843
- if (controller) {
844
- controller.abort();
845
- activeAgentAborts.delete(requestId);
599
+ server2.patch("/_web/sessions/:id/name", async (request) => {
600
+ session_service_exports.renameSession(adapter, request.params.id, request.body.name);
601
+ return { success: true };
602
+ });
603
+ server2.patch(
604
+ "/_web/sessions/:id/owner",
605
+ async (request) => {
606
+ session_service_exports.updateSessionOwnerId(adapter, request.params.id, request.body.ownerId ?? null);
846
607
  return { success: true };
847
608
  }
848
- return { success: false };
609
+ );
610
+ server2.post("/_web/sessions/:id/owner/apply-profile", async (request) => {
611
+ return owner_profile_service_exports.tryApplyOwnerProfile(adapter, preferences(), request.params.id);
612
+ });
613
+ server2.post(
614
+ "/_web/sessions/:id/owner/select",
615
+ async (request) => {
616
+ return owner_profile_service_exports.setOwnerAndApplyProfile(
617
+ adapter,
618
+ preferences(),
619
+ request.params.id,
620
+ request.body.ownerPlatformId
621
+ );
622
+ }
623
+ );
624
+ server2.post("/_web/sessions/:id/owner/dismiss-prompt", async (request) => {
625
+ owner_profile_service_exports.dismissOwnerPrompt(preferences(), request.params.id);
626
+ return { success: true };
849
627
  });
850
628
  }
851
629
 
852
- // ../../packages/http-routes/src/routes/web/ai-assistants.ts
853
- function registerAiAssistantRoutes(server2, ctx) {
854
- server2.get("/_web/ai/tools/catalog", async () => {
855
- return BUILTIN_TOOL_CATALOG;
856
- });
857
- const mgr = ctx.assistantManager;
858
- if (!mgr) return;
859
- server2.get("/_web/ai/assistants", async () => {
860
- return mgr.getAllAssistants();
630
+ // ../../packages/http-routes/src/routes/web/members.ts
631
+ function registerMemberRoutes(server2, ctx) {
632
+ const { sessionAdapter: adapter } = ctx;
633
+ server2.get("/_web/sessions/:id/members", async (request) => {
634
+ return member_service_exports.getMembers(adapter, request.params.id);
861
635
  });
862
- server2.get("/_web/ai/assistants/:id", async (request, reply) => {
863
- const config = mgr.getAssistantConfig(request.params.id);
864
- if (!config) return reply.code(404).send({ error: "Not found" });
865
- return config;
636
+ server2.get("/_web/sessions/:id/members/paginated", async (request) => {
637
+ return member_service_exports.getMembersPaginated(adapter, request.params.id, {
638
+ page: parseInt(request.query.page || "1", 10),
639
+ pageSize: parseInt(request.query.pageSize || "20", 10),
640
+ search: request.query.search,
641
+ sortOrder: request.query.sortOrder === "asc" ? "asc" : "desc"
642
+ });
866
643
  });
867
- server2.post("/_web/ai/assistants", async (request) => {
868
- return mgr.createAssistant(request.body);
644
+ server2.patch(
645
+ "/_web/sessions/:id/members/:memberId/aliases",
646
+ async (request) => {
647
+ const memberId = parseInt(request.params.memberId, 10);
648
+ member_service_exports.updateMemberAliases(adapter, request.params.id, memberId, request.body.aliases);
649
+ return { success: true };
650
+ }
651
+ );
652
+ server2.delete(
653
+ "/_web/sessions/:id/members/:memberId",
654
+ async (request) => {
655
+ const memberId = parseInt(request.params.memberId, 10);
656
+ member_service_exports.deleteMember(adapter, request.params.id, memberId);
657
+ return { success: true };
658
+ }
659
+ );
660
+ server2.post(
661
+ "/_web/sessions/:id/members/merge",
662
+ async (request) => {
663
+ const { memberId1, memberId2 } = request.body;
664
+ member_service_exports.mergeMembers(adapter, request.params.id, memberId1, memberId2);
665
+ return { success: true };
666
+ }
667
+ );
668
+ server2.get(
669
+ "/_web/sessions/:id/members/:memberId/history",
670
+ async (request) => {
671
+ const memberId = parseInt(request.params.memberId, 10);
672
+ return member_service_exports.getMemberNameHistory(adapter, request.params.id, memberId);
673
+ }
674
+ );
675
+ }
676
+
677
+ // ../../packages/http-routes/src/routes/web/contacts.ts
678
+ function registerContactsRoutes(server2, ctx) {
679
+ const service = ctx.contactsService ?? createContactsService({
680
+ adapter: ctx.sessionAdapter,
681
+ pathProvider: ctx.pathProvider,
682
+ runtimeIdentity: ctx.runtimeIdentity,
683
+ nativeBinding: ctx.nativeBinding
869
684
  });
870
- server2.put("/_web/ai/assistants/:id", async (request, reply) => {
871
- const result = mgr.updateAssistant(request.params.id, request.body);
872
- if (!result.success) return reply.code(404).send(result);
873
- return result;
685
+ server2.addHook("onClose", async () => {
686
+ await service.close();
874
687
  });
875
- server2.delete("/_web/ai/assistants/:id", async (request, reply) => {
876
- const result = mgr.deleteAssistant(request.params.id);
877
- if (!result.success) return reply.code(400).send(result);
878
- return result;
688
+ server2.get("/_web/contacts", async (request) => {
689
+ return service.getContactsPage({
690
+ acceptStale: isTruthy(request.query.acceptStale),
691
+ timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange),
692
+ pool: parseContactPool(request.query.pool),
693
+ page: parsePositiveInteger(request.query.page),
694
+ pageSize: parsePositiveInteger(request.query.pageSize),
695
+ query: request.query.q
696
+ });
879
697
  });
880
- server2.post("/_web/ai/assistants/:id/reset", async (request, reply) => {
881
- const result = mgr.resetAssistant(request.params.id);
882
- if (!result.success) return reply.code(400).send(result);
883
- return result;
698
+ server2.post("/_web/contacts/recompute", async (request) => {
699
+ return service.startRecompute({
700
+ timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange),
701
+ pool: parseContactPool(request.query.pool),
702
+ page: parsePositiveInteger(request.query.page),
703
+ pageSize: parsePositiveInteger(request.query.pageSize),
704
+ query: request.query.q
705
+ });
884
706
  });
885
- server2.post("/_web/ai/assistants/import", async (request) => {
886
- return mgr.importAssistantFromMd(request.body.rawMd);
887
- });
888
- server2.post("/_web/ai/assistants/import-builtin", async (request, reply) => {
889
- const result = mgr.importAssistant(request.body.builtinId);
890
- if (!result.success) return reply.code(400).send(result);
891
- return result;
892
- });
893
- server2.post("/_web/ai/assistants/:id/reimport", async (request, reply) => {
894
- const result = mgr.reimportAssistant(request.params.id);
895
- if (!result.success) return reply.code(400).send(result);
896
- return result;
707
+ server2.get("/_web/contacts/:key/detail", async (request) => {
708
+ return service.getContactDetail(request.params.key, {
709
+ acceptStale: isTruthy(request.query.acceptStale),
710
+ timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange)
711
+ });
897
712
  });
713
+ server2.put(
714
+ "/_web/contacts/:key/mark-friend",
715
+ async (request) => {
716
+ return service.markContactAsFriend(request.params.key, {
717
+ timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange)
718
+ });
719
+ }
720
+ );
721
+ server2.delete(
722
+ "/_web/contacts/:key/mark-friend",
723
+ async (request) => {
724
+ return service.unmarkContactAsFriend(request.params.key, {
725
+ timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange)
726
+ });
727
+ }
728
+ );
729
+ }
730
+ function isTruthy(value) {
731
+ return value === "1" || value === "true" || value === "yes";
732
+ }
733
+ function parseContactsTimeRangePreset(value) {
734
+ return CONTACTS_TIME_RANGE_PRESETS.includes(value) ? value : "1y";
735
+ }
736
+ function parseContactPool(value) {
737
+ return value === "friend" || value === "non_friend" ? value : void 0;
738
+ }
739
+ function parsePositiveInteger(value) {
740
+ if (!value) return void 0;
741
+ const parsed = Number(value);
742
+ if (!Number.isFinite(parsed)) return void 0;
743
+ return Math.max(1, Math.trunc(parsed));
898
744
  }
899
745
 
900
- // ../../packages/http-routes/src/routes/web/ai-chats.ts
901
- function registerAiChatRoutes(server2, ctx) {
902
- const cm = ctx.aiChatManager;
903
- if (!cm) return;
904
- server2.post("/_web/ai/chats", async (request) => {
905
- const { sessionId, title, assistantId } = request.body;
906
- return cm.createAIChat(sessionId, title, assistantId);
907
- });
908
- server2.get("/_web/ai/chats", async (request) => {
909
- const { sessionId } = request.query;
910
- if (!sessionId) return [];
911
- return cm.getAIChats(sessionId);
912
- });
913
- server2.get("/_web/ai/chats/:id", async (request, reply) => {
914
- const conv = cm.getAIChat(request.params.id);
915
- if (!conv) return reply.code(404).send({ error: "AI chat not found" });
916
- return conv;
917
- });
918
- server2.put("/_web/ai/chats/:id/title", async (request) => {
919
- return cm.updateAIChatTitle(request.params.id, request.body.title);
920
- });
921
- server2.delete("/_web/ai/chats/:id", async (request) => {
922
- return cm.deleteAIChat(request.params.id);
923
- });
924
- server2.post("/_web/ai/chats/:id/messages", async (request) => {
925
- const { role, content, dataKeywords, dataMessageCount, contentBlocks, tokenUsage } = request.body;
926
- return cm.addMessage(
927
- request.params.id,
928
- role,
929
- content,
930
- dataKeywords,
931
- dataMessageCount,
932
- contentBlocks,
933
- tokenUsage
934
- );
746
+ // ../../packages/http-routes/src/routes/web/people-relationships.ts
747
+ function registerPeopleRelationshipsRoutes(server2, ctx) {
748
+ const service = ctx.peopleRelationshipsService ?? createPeopleRelationshipsService({
749
+ adapter: ctx.sessionAdapter,
750
+ pathProvider: ctx.pathProvider,
751
+ runtimeIdentity: ctx.runtimeIdentity,
752
+ nativeBinding: ctx.nativeBinding
935
753
  });
936
- server2.get("/_web/ai/chats/:id/messages", async (request) => {
937
- return cm.getMessages(request.params.id);
754
+ server2.addHook("onClose", async () => {
755
+ await service.close();
938
756
  });
939
- server2.post("/_web/ai/chats/:id/messages/:messageId/delete-from", async (request, reply) => {
940
- const { id, messageId } = request.params;
941
- if (!id || typeof id !== "string" || !messageId || typeof messageId !== "string") {
942
- return reply.code(400).send({ error: "aiChatId and messageId are required" });
943
- }
944
- cm.deleteMessagesFrom(id, messageId);
945
- return { success: true };
757
+ server2.get("/_web/people/relationships", async (request) => {
758
+ return service.getGraph({
759
+ acceptStale: isTruthy2(request.query.acceptStale),
760
+ timeRangePreset: parseContactsTimeRangePreset2(request.query.timeRange),
761
+ graphScope: parsePeopleRelationshipsGraphScope(request.query.scope),
762
+ query: request.query.q
763
+ });
946
764
  });
947
- server2.post("/_web/ai/chats/:id/fork", async (request, reply) => {
948
- const { upToMessageId, title } = request.body;
949
- if (!upToMessageId || typeof upToMessageId !== "string") {
950
- return reply.code(400).send({ error: "upToMessageId is required" });
951
- }
952
- return cm.forkAIChat(request.params.id, upToMessageId, title);
765
+ server2.post("/_web/people/relationships/recompute", async (request) => {
766
+ return service.startRecompute({
767
+ timeRangePreset: parseContactsTimeRangePreset2(request.query.timeRange),
768
+ graphScope: parsePeopleRelationshipsGraphScope(request.query.scope),
769
+ query: request.query.q
770
+ });
953
771
  });
954
- server2.put("/_web/ai/messages/:messageId/content", async (request, reply) => {
955
- const { content } = request.body;
956
- if (!content || typeof content !== "string") {
957
- return reply.code(400).send({ error: "content is required" });
772
+ server2.get(
773
+ "/_web/people/relationships/:key/neighborhood",
774
+ async (request) => {
775
+ return service.getNeighborhood(request.params.key, {
776
+ acceptStale: isTruthy2(request.query.acceptStale),
777
+ timeRangePreset: parseContactsTimeRangePreset2(request.query.timeRange)
778
+ });
958
779
  }
959
- cm.updateMessageContent(request.params.messageId, content);
960
- return { success: true };
780
+ );
781
+ }
782
+ function isTruthy2(value) {
783
+ return value === "1" || value === "true" || value === "yes";
784
+ }
785
+ function parseContactsTimeRangePreset2(value) {
786
+ return CONTACTS_TIME_RANGE_PRESETS.includes(value) ? value : "1y";
787
+ }
788
+ function parsePeopleRelationshipsGraphScope(value) {
789
+ return value === "close" || value === "friends" ? value : "panorama";
790
+ }
791
+
792
+ // ../../packages/http-routes/src/routes/web/global-insight.ts
793
+ function registerGlobalInsightRoutes(server2, ctx) {
794
+ const service = ctx.globalInsightService ?? createGlobalInsightService({
795
+ adapter: ctx.sessionAdapter,
796
+ pathProvider: ctx.pathProvider,
797
+ runtimeIdentity: ctx.runtimeIdentity,
798
+ nativeBinding: ctx.nativeBinding
961
799
  });
962
- server2.post("/_web/ai/chats/:id/messages/:messageId/delete-relink", async (request, reply) => {
963
- const { id, messageId } = request.params;
964
- if (!id || !messageId) {
965
- return reply.code(400).send({ error: "aiChatId and messageId are required" });
966
- }
967
- cm.deleteAndRelinkMessage(id, messageId);
968
- return { success: true };
800
+ server2.addHook("onClose", async () => service.close());
801
+ server2.get("/_web/global-insight/annual-summary", async (request) => {
802
+ return service.getAnnualSummary({
803
+ ...parseRange(request.query),
804
+ acceptStale: isTruthy3(request.query.acceptStale)
805
+ });
969
806
  });
970
- server2.post("/_web/ai/chats/:id/messages/insert-after", async (request, reply) => {
971
- const { afterMessageId, role, content, contentBlocks, tokenUsage } = request.body;
972
- if (!afterMessageId || typeof afterMessageId !== "string") {
973
- return reply.code(400).send({ error: "afterMessageId is required" });
974
- }
975
- return cm.insertMessageAfter(request.params.id, afterMessageId, role, content, contentBlocks, tokenUsage);
807
+ server2.post(
808
+ "/_web/global-insight/annual-summary/recompute",
809
+ async (request) => service.startRecompute(parseRange(request.query))
810
+ );
811
+ }
812
+ function parseRange(query) {
813
+ const mode = query.mode === "recent" ? "recent" : "year";
814
+ return {
815
+ mode,
816
+ year: parseInteger(query.year),
817
+ days: query.days === "365" ? 365 : void 0
818
+ };
819
+ }
820
+ function parseInteger(value) {
821
+ if (!value) return void 0;
822
+ const parsed = Number(value);
823
+ return Number.isInteger(parsed) ? parsed : void 0;
824
+ }
825
+ function isTruthy3(value) {
826
+ return value === "1" || value === "true" || value === "yes";
827
+ }
828
+
829
+ // ../../packages/http-routes/src/routes/web/preferences.ts
830
+ function registerPreferencesRoutes(server2, ctx) {
831
+ const prefManager = ctx.preferencesManager ?? new PreferencesManager(ctx.pathProvider.getSystemDir());
832
+ server2.get("/_web/preferences", async () => {
833
+ return prefManager.load();
976
834
  });
977
- server2.get("/_web/ai/chats/:id/token-usage", async (request) => {
978
- return cm.getAIChatTokenUsage(request.params.id);
835
+ server2.patch("/_web/preferences", async (request) => {
836
+ return prefManager.save(request.body);
979
837
  });
980
- server2.get("/_web/ai/debug/schema", async () => {
981
- return cm.getAiSchema();
838
+ server2.get("/_web/preferences/ui-config", async () => {
839
+ const config = loadConfig();
840
+ return config.ui;
982
841
  });
983
- server2.post("/_web/ai/debug/execute-sql", async (request, reply) => {
984
- const { sql } = request.body;
985
- if (!sql || typeof sql !== "string") {
986
- return reply.code(400).send({ error: "sql is required" });
987
- }
842
+ server2.patch("/_web/preferences/ui-config", async (request) => {
988
843
  try {
989
- return cm.executeAiSQL(sql);
990
- } catch (e) {
991
- const msg = e instanceof Error ? e.message : String(e);
992
- return reply.code(400).send({ error: msg });
844
+ for (const [key, value] of Object.entries(request.body)) {
845
+ if (value !== void 0) {
846
+ writeConfigField("ui", key, value);
847
+ }
848
+ }
849
+ return { success: true };
850
+ } catch (err) {
851
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
993
852
  }
994
853
  });
995
- server2.post("/_web/ai/debug/clear-debug-context", async () => {
996
- const cleared = cm.clearAllDebugContext();
997
- return { success: true, cleared };
854
+ server2.get("/_web/preferences/locale", async () => {
855
+ const config = loadConfig();
856
+ return { lang: config.locale.lang };
998
857
  });
999
- server2.get("/_web/ai/chats/:id/estimate-tokens", async (request) => {
1000
- const history = cm.getHistoryForAgent(request.params.id);
1001
- const tokens = countMessagesTokens(history.map((m) => ({ role: m.role, content: m.content })));
1002
- return { success: true, tokens, messageCount: history.length };
858
+ server2.patch("/_web/preferences/locale", async (request) => {
859
+ try {
860
+ writeConfigField("locale", "lang", request.body.lang);
861
+ return { success: true };
862
+ } catch (err) {
863
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
864
+ }
1003
865
  });
1004
866
  }
1005
867
 
1006
- // ../../packages/http-routes/src/routes/web/ai-llm-stream.ts
1007
- function registerAiLlmStreamRoutes(server2, ctx) {
1008
- const store = ctx.llmConfigStore;
1009
- if (!store) return;
1010
- server2.post("/_web/ai/llm/chat-stream", async (request, reply) => {
1011
- const { messages, options } = request.body;
1012
- const llmConfig = store.getDefaultAssistantConfig();
1013
- if (!llmConfig) {
1014
- return reply.code(400).send({ success: false, error: "LLM service not configured" });
1015
- }
1016
- const piModel = buildPiModel(llmConfig);
1017
- const abortController = new AbortController();
1018
- reply.raw.writeHead(200, {
1019
- "Content-Type": "text/event-stream",
1020
- "Cache-Control": "no-cache",
1021
- Connection: "keep-alive"
1022
- });
1023
- reply.raw.on("close", () => {
1024
- if (!abortController.signal.aborted) abortController.abort();
1025
- });
1026
- const sendChunk = (data) => {
1027
- if (reply.raw.writableEnded || reply.raw.destroyed) return;
1028
- reply.raw.write(`event: chunk
1029
- data: ${JSON.stringify(data)}
868
+ // ../../packages/http-routes/src/routes/web/analytics.ts
869
+ import * as fs from "fs";
870
+ import * as path2 from "path";
1030
871
 
1031
- `);
1032
- };
1033
- try {
1034
- await runSimpleLlmStream({
1035
- messages,
1036
- apiKey: llmConfig.apiKey,
1037
- piModel,
1038
- temperature: options?.temperature,
1039
- maxTokens: options?.maxTokens,
1040
- onChunk: sendChunk,
1041
- abortSignal: abortController.signal
1042
- });
1043
- } catch (error) {
1044
- if (abortController.signal.aborted) return;
1045
- const msg = error instanceof Error ? error.message : String(error);
1046
- sendChunk({ content: "", isFinished: true, finishReason: "error", error: msg });
1047
- } finally {
1048
- if (!reply.raw.writableEnded && !reply.raw.destroyed) reply.raw.end();
1049
- }
1050
- });
872
+ // ../../packages/http-routes/src/helpers.ts
873
+ function parseTimeFilter(query) {
874
+ const { startTs, endTs, memberId } = query;
875
+ if (!startTs && !endTs && !memberId) return void 0;
876
+ const filter = {};
877
+ if (startTs) filter.startTs = parseInt(startTs, 10);
878
+ if (endTs) filter.endTs = parseInt(endTs, 10);
879
+ if (memberId) filter.memberId = parseInt(memberId, 10);
880
+ return filter;
1051
881
  }
1052
882
 
1053
- // ../../packages/http-routes/src/routes/web/ai-llm.ts
1054
- function hasRealApiKey(apiKey) {
1055
- return !!apiKey && apiKey !== "sk-no-key-required";
883
+ // ../../packages/http-routes/src/analytics-cache.ts
884
+ import * as path from "path";
885
+ function canonical(value) {
886
+ if (value === void 0) return "null";
887
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
888
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
889
+ const obj = value;
890
+ const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
891
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonical(obj[k])}`).join(",")}}`;
1056
892
  }
1057
- function toConfigDisplay(config) {
1058
- const { apiKey: _raw, ...rest } = config;
1059
- return { ...rest, apiKey: "", apiKeySet: hasRealApiKey(String(_raw || "")) };
893
+ function buildAnalyticsCacheKey(namespace, params) {
894
+ return `${namespace}:${canonical(params)}`;
1060
895
  }
1061
- function registerAiLlmRoutes(server2, ctx) {
1062
- const store = ctx.llmConfigStore;
1063
- const aiDataDir = ctx.aiDataDir;
1064
- if (!store || !aiDataDir) return;
1065
- server2.get("/_web/ai/llm/has-config", async () => {
1066
- return store.hasActiveConfig();
1067
- });
1068
- server2.get("/_web/ai/llm/configs", async () => {
1069
- const data = store.loadStore();
1070
- return {
1071
- configs: data.configs.map((c) => toConfigDisplay(c)),
1072
- defaultAssistant: data.defaultAssistant,
1073
- fastModel: data.fastModel
1074
- };
1075
- });
1076
- server2.post("/_web/ai/llm/configs", async (request) => {
1077
- const result = store.addConfig(request.body);
1078
- if (result.success && result.config) {
1079
- return { ...result, config: toConfigDisplay(result.config) };
1080
- }
1081
- return result;
1082
- });
1083
- server2.put("/_web/ai/llm/configs/:id", async (request) => {
1084
- return store.updateConfig(request.params.id, request.body);
1085
- });
1086
- server2.delete("/_web/ai/llm/configs/:id", async (request) => {
1087
- return store.deleteConfig(request.params.id);
1088
- });
1089
- server2.get("/_web/ai/llm/default-assistant-slot", async () => {
1090
- return store.getDefaultAssistantSlot();
1091
- });
1092
- server2.put("/_web/ai/llm/default-assistant-slot", async (request) => {
1093
- return store.setDefaultAssistantModel(request.body.configId, request.body.modelId);
1094
- });
1095
- server2.get("/_web/ai/llm/fast-model-slot", async () => {
1096
- return store.getFastModelSlot();
1097
- });
1098
- server2.put("/_web/ai/llm/fast-model-slot", async (request) => {
1099
- return store.setFastModel(request.body);
1100
- });
1101
- server2.get("/_web/ai/llm/providers", async () => {
1102
- return BUILTIN_PROVIDERS.map((p) => {
1103
- const models = getBuiltinModelsByProvider(p.id);
1104
- return {
1105
- id: p.id,
1106
- name: p.name,
1107
- defaultBaseUrl: p.defaultBaseUrl,
1108
- models: models.filter((m) => !m.capabilities.includes("embedding") && !m.capabilities.includes("ranking")).map((m) => ({ id: m.id, name: m.name, description: m.description }))
1109
- };
1110
- });
1111
- });
1112
- server2.get("/_web/ai/llm/provider-registry", async () => {
1113
- const custom = ctx.customProviderStore?.getAll() ?? [];
1114
- return [...BUILTIN_PROVIDERS, ...custom];
1115
- });
1116
- server2.get("/_web/ai/llm/model-catalog", async () => {
1117
- const custom = ctx.customModelStore?.getAll() ?? [];
1118
- return [...BUILTIN_MODELS, ...custom];
1119
- });
1120
- server2.post("/_web/ai/llm/custom-providers", async (request, reply) => {
1121
- if (!ctx.customProviderStore)
1122
- return reply.code(501).send({ success: false, error: "Custom providers not available" });
1123
- return ctx.customProviderStore.add(request.body);
1124
- });
1125
- server2.put("/_web/ai/llm/custom-providers/:id", async (request, reply) => {
1126
- if (!ctx.customProviderStore)
1127
- return reply.code(501).send({ success: false, error: "Custom providers not available" });
1128
- const result = ctx.customProviderStore.update(request.params.id, request.body);
1129
- if (!result.success) return reply.code(404).send(result);
1130
- return result;
1131
- });
1132
- server2.delete("/_web/ai/llm/custom-providers/:id", async (request, reply) => {
1133
- if (!ctx.customProviderStore)
1134
- return reply.code(501).send({ success: false, error: "Custom providers not available" });
1135
- const result = ctx.customProviderStore.delete(request.params.id);
1136
- if (!result.success) return reply.code(404).send(result);
1137
- return result;
1138
- });
1139
- server2.post("/_web/ai/llm/custom-models", async (request, reply) => {
1140
- if (!ctx.customModelStore) return reply.code(501).send({ success: false, error: "Custom models not available" });
1141
- const result = ctx.customModelStore.add(request.body);
1142
- if (!result.success) return reply.code(409).send(result);
1143
- return result;
1144
- });
1145
- server2.put("/_web/ai/llm/custom-models/:providerId/:modelId", async (request, reply) => {
1146
- if (!ctx.customModelStore) return reply.code(501).send({ success: false, error: "Custom models not available" });
1147
- const result = ctx.customModelStore.update(request.params.providerId, request.params.modelId, request.body);
1148
- if (!result.success) return reply.code(404).send(result);
1149
- return result;
1150
- });
1151
- server2.delete("/_web/ai/llm/custom-models/:providerId/:modelId", async (request, reply) => {
1152
- if (!ctx.customModelStore) return reply.code(501).send({ success: false, error: "Custom models not available" });
1153
- const result = ctx.customModelStore.delete(request.params.providerId, request.params.modelId);
1154
- if (!result.success) return reply.code(404).send(result);
1155
- return result;
1156
- });
1157
- server2.post("/_web/ai/llm/validate-key", async (request) => {
1158
- const { provider, apiKey, baseUrl, model, apiFormat, configId } = request.body;
1159
- const resolvedKey = apiKey?.trim() ? apiKey : configId ? store.getConfigById(configId)?.apiKey || "" : "";
1160
- return validateApiKey(provider, resolvedKey, baseUrl, model, apiFormat);
1161
- });
1162
- server2.post("/_web/ai/llm/remote-models", async (request) => {
1163
- const { provider, apiKey, baseUrl, apiFormat, configId } = request.body;
1164
- const resolvedKey = apiKey?.trim() ? apiKey : configId ? store.getConfigById(configId)?.apiKey || "" : "";
1165
- return fetchRemoteModels(provider, resolvedKey, baseUrl, apiFormat);
1166
- });
1167
- server2.get("/_web/ai/desensitize-rules/defaults", async (request) => {
1168
- return getDefaultRulesForLocale(request.query.locale ?? "zh-CN");
1169
- });
1170
- server2.post("/_web/ai/desensitize-rules/merge", async (request) => {
1171
- return mergeRulesForLocale(request.body.existingRules, request.body.locale, request.body.overrides ?? {});
1172
- });
896
+ function withAnalyticsCache(ctx, sessionId, namespace, params, compute, options) {
897
+ const queryCacheDir = path.join(ctx.pathProvider.getCacheDir(), "query");
898
+ const dateStr = options?.dailyInvalidate ? `|date:${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}` : "";
899
+ const extraStr = options?.extraVersion ? `|${options.extraVersion}` : "";
900
+ const version = `${ctx.getVersion()}|${getDbFileVersion(ctx.sessionAdapter.getDbPath(sessionId))}${dateStr}${extraStr}`;
901
+ const key = buildAnalyticsCacheKey(namespace, params);
902
+ return getOrComputeAnalysisCache(sessionId, key, queryCacheDir, version, compute);
1173
903
  }
1174
904
 
1175
- // ../../packages/http-routes/src/routes/web/ai-logs.ts
1176
- import fs from "fs";
1177
- import path from "path";
1178
- function findLatestAiLogPath(logsDir) {
1179
- const aiLogDir = path.join(logsDir, "ai");
1180
- if (!fs.existsSync(aiLogDir)) return null;
1181
- const latest = fs.readdirSync(aiLogDir).filter((name) => name.startsWith("ai_") && name.endsWith(".log")).map((name) => {
1182
- const filePath = path.join(aiLogDir, name);
1183
- const stat2 = fs.statSync(filePath);
1184
- return { path: filePath, mtimeMs: stat2.mtimeMs };
1185
- }).sort((a, b) => b.mtimeMs - a.mtimeMs)[0];
1186
- return latest?.path ?? null;
1187
- }
1188
- function resolveAiLogPath(ctx) {
1189
- const currentPath = ctx.getCurrentAiLogPath?.();
1190
- if (currentPath && fs.existsSync(currentPath)) return currentPath;
1191
- return findLatestAiLogPath(ctx.pathProvider.getLogsDir());
1192
- }
1193
- function isLoopbackAddress(address) {
1194
- if (!address) return true;
1195
- const normalized = address.replace(/^::ffff:/, "");
1196
- return normalized === "localhost" || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1" || normalized.startsWith("127.");
1197
- }
1198
- function registerAiLogRoutes(server2, ctx) {
1199
- server2.post("/_web/ai/logs/show", async (request, reply) => {
1200
- if (!ctx.showInFolder) return reply.code(501).send({ success: false, error: "Not supported" });
1201
- if (!isLoopbackAddress(request.ip)) {
1202
- return { success: false, error: "Opening AI logs is only supported on this device" };
1203
- }
1204
- const logPath = resolveAiLogPath(ctx);
1205
- if (!logPath) return { success: false, error: "No AI log files found" };
1206
- try {
1207
- await ctx.showInFolder(logPath);
1208
- return { success: true, path: logPath };
1209
- } catch (error) {
1210
- return { success: false, error: error instanceof Error ? error.message : String(error) };
1211
- }
905
+ // ../../packages/http-routes/src/routes/web/analytics.ts
906
+ function registerAnalyticsRoutes(server2, ctx) {
907
+ const { sessionAdapter: adapter } = ctx;
908
+ const cached = (name, sessionId, params, compute, options) => withAnalyticsCache(ctx, sessionId, `analytics.${name}`, params, compute, options);
909
+ server2.get("/_web/sessions/:id/years", async (request) => {
910
+ const db = adapter.ensureReadonly(request.params.id);
911
+ return getAvailableYears(db);
1212
912
  });
1213
- }
1214
-
1215
- // ../../packages/http-routes/src/routes/web/ai-semantic-index.ts
1216
- import path2 from "path";
1217
- function registerSemanticIndexRoutes(server2, ctx) {
1218
- const service = ctx.semanticIndexService;
1219
- if (!service) {
1220
- if (ctx.aiDataDir) {
1221
- const configStore = new SemanticIndexConfigStore(path2.join(ctx.aiDataDir, SEMANTIC_INDEX_CONFIG_FILE));
1222
- const configResponse = (config) => ({
1223
- config,
1224
- apiKeySet: resolveSemanticIndexApiKeySet(config, ctx.resolveApiKey),
1225
- configured: isSemanticIndexConfigured(config)
1226
- });
1227
- server2.get("/_web/ai/semantic-index/config", async () => ({
1228
- ...configResponse(configStore.get()),
1229
- modelStatus: "idle"
1230
- }));
1231
- server2.put(
1232
- "/_web/ai/semantic-index/config",
1233
- async (request) => {
1234
- const config = persistSemanticIndexConfig(configStore, request.body.config, {
1235
- apiKey: request.body.apiKey,
1236
- writeAuthProfile: ctx.writeAuthProfile
1237
- });
1238
- return { ...configResponse(config), modelStatus: "idle" };
1239
- }
1240
- );
1241
- }
1242
- server2.get("/_web/ai/semantic-index/enabled", async () => ({ sessions: [] }));
1243
- server2.get("/_web/ai/semantic-index/status", async () => ({ status: null }));
1244
- server2.post("/_web/ai/semantic-index/status", async () => ({ sessions: [] }));
1245
- return;
1246
- }
1247
- server2.get("/_web/ai/semantic-index/config", async () => {
1248
- return {
1249
- config: await service.getConfig(),
1250
- apiKeySet: await service.hasApiKey(),
1251
- configured: await service.isConfigured(),
1252
- modelStatus: await service.getModelStatus()
1253
- };
913
+ server2.get("/_web/sessions/:id/time-range", async (request) => {
914
+ const db = adapter.ensureReadonly(request.params.id);
915
+ return getTimeRange(db);
1254
916
  });
1255
- server2.put(
1256
- "/_web/ai/semantic-index/config",
917
+ server2.get(
918
+ "/_web/sessions/:id/stats/member-activity",
1257
919
  async (request) => {
1258
- const config = await service.setConfig(request.body.config, { apiKey: request.body.apiKey });
1259
- return {
1260
- config,
1261
- apiKeySet: await service.hasApiKey(),
1262
- configured: await service.isConfigured(),
1263
- modelStatus: await service.getModelStatus()
1264
- };
1265
- }
1266
- );
1267
- server2.get("/_web/ai/semantic-index/enabled", async () => {
1268
- return { sessions: await service.listEnabledStatuses() };
1269
- });
1270
- server2.get("/_web/ai/semantic-index/status", async (request) => {
1271
- return { status: await service.status(request.query.sessionId) };
1272
- });
1273
- server2.post("/_web/ai/semantic-index/status", async (request) => {
1274
- return { sessions: await service.statusForSessions(request.body.sessionIds ?? []) };
1275
- });
1276
- const sessionAction = (path14, action) => {
1277
- server2.post(path14, async (request, reply) => {
1278
- const { sessionId } = request.body;
1279
- if (!sessionId) return reply.code(400).send({ error: "sessionId is required" });
1280
- await action(sessionId);
1281
- return { status: await service.status(sessionId) };
1282
- });
1283
- };
1284
- sessionAction("/_web/ai/semantic-index/enable", (id) => service.enable(id));
1285
- sessionAction("/_web/ai/semantic-index/remove", (id) => service.remove(id));
1286
- sessionAction("/_web/ai/semantic-index/build", (id) => service.build(id));
1287
- sessionAction("/_web/ai/semantic-index/pause", (id) => service.pause(id));
1288
- sessionAction("/_web/ai/semantic-index/cancel", (id) => service.cancel(id));
1289
- sessionAction("/_web/ai/semantic-index/rebuild", (id) => service.rebuild(id));
1290
- server2.post("/_web/ai/semantic-index/build-pending", async () => {
1291
- await service.buildAllPending();
1292
- return { sessions: await service.listEnabledStatuses() };
1293
- });
1294
- server2.post("/_web/ai/semantic-index/cleanup", async () => {
1295
- return await service.cleanupUnused();
1296
- });
1297
- server2.post(
1298
- "/_web/ai/semantic-index/search",
1299
- async (request, reply) => {
1300
- const { sessionId, query, finalTopK } = request.body;
1301
- if (!sessionId || !query) return reply.code(400).send({ error: "sessionId and query are required" });
1302
- return service.search(sessionId, query, { finalTopK });
1303
- }
1304
- );
1305
- }
1306
-
1307
- // ../../packages/http-routes/src/routes/web/ai-skills.ts
1308
- function registerAiSkillRoutes(server2, ctx) {
1309
- const mgr = ctx.skillManagerCore;
1310
- if (!mgr) return;
1311
- server2.get("/_web/ai/skills", async () => {
1312
- return mgr.getAllSkills();
1313
- });
1314
- server2.get("/_web/ai/skills/:id", async (request, reply) => {
1315
- const config = mgr.getSkillConfig(request.params.id);
1316
- if (!config) return reply.code(404).send({ error: "Not found" });
1317
- return config;
1318
- });
1319
- server2.post("/_web/ai/skills", async (request) => {
1320
- return mgr.createSkill(request.body.rawMd);
1321
- });
1322
- server2.put("/_web/ai/skills/:id", async (request, reply) => {
1323
- const result = mgr.updateSkill(request.params.id, request.body.rawMd);
1324
- if (!result.success) return reply.code(404).send(result);
1325
- return result;
1326
- });
1327
- server2.delete("/_web/ai/skills/:id", async (request, reply) => {
1328
- const result = mgr.deleteSkill(request.params.id);
1329
- if (!result.success) return reply.code(400).send(result);
1330
- return result;
1331
- });
1332
- server2.post("/_web/ai/skills/import", async (request) => {
1333
- return mgr.importSkillFromMd(request.body.rawMd);
1334
- });
1335
- server2.get("/_web/ai/skills/builtin-catalog", async () => {
1336
- return mgr.getBuiltinCatalog();
1337
- });
1338
- server2.post("/_web/ai/skills/import-builtin", async (request, reply) => {
1339
- const result = mgr.importSkill(request.body.builtinId);
1340
- if (!result.success) return reply.code(400).send(result);
1341
- return result;
1342
- });
1343
- server2.post("/_web/ai/skills/:id/reimport", async (request, reply) => {
1344
- const result = mgr.reimportSkill(request.params.id);
1345
- if (!result.success) return reply.code(400).send(result);
1346
- return result;
1347
- });
1348
- }
1349
-
1350
- // ../../packages/http-routes/src/routes/web/ai-summaries.ts
1351
- function createSummaryDeps(ctx) {
1352
- const store = ctx.llmConfigStore;
1353
- if (!store) return null;
1354
- return {
1355
- getLlmConfig() {
1356
- const config = store.getDefaultAssistantConfig();
1357
- if (!config) return null;
1358
- return config;
1359
- },
1360
- buildPiModel(config) {
1361
- return buildPiModel(config);
1362
- }
1363
- };
1364
- }
1365
- function registerAiSummaryRoutes(server2, ctx) {
1366
- const deps = createSummaryDeps(ctx);
1367
- if (!deps) return;
1368
- const { sessionAdapter: adapter } = ctx;
1369
- server2.post("/_web/sessions/:id/summaries/generate", async (request, reply) => {
1370
- const { segmentId, locale, forceRegenerate, strategy } = request.body;
1371
- const result = await summary_service_exports.generateSummary(adapter, request.params.id, segmentId, deps, {
1372
- locale,
1373
- forceRegenerate,
1374
- strategy
1375
- });
1376
- if ("error" in result && !result.success) {
1377
- return reply.code(400).send({ error: result.error });
1378
- }
1379
- return result;
1380
- });
1381
- server2.post("/_web/sessions/:id/summaries/generate-all", async (request, reply) => {
1382
- const { locale, forceRegenerate } = request.body;
1383
- const result = await summary_service_exports.generateAllSummaries(adapter, request.params.id, deps, {
1384
- locale,
1385
- forceRegenerate
1386
- });
1387
- if (result.error) {
1388
- return reply.code(400).send({ error: result.error });
1389
- }
1390
- return result;
1391
- });
1392
- server2.post("/_web/sessions/:id/summaries/check-can-generate", async (request) => {
1393
- const { segmentIds } = request.body;
1394
- return summary_service_exports.checkCanGenerate(adapter, request.params.id, segmentIds);
1395
- });
1396
- }
1397
-
1398
- // ../../packages/http-routes/src/ai/tool-executor.ts
1399
- var MAX_RESULT_CHARS = 5e5;
1400
- async function executeRegistryTool(params, deps) {
1401
- const { toolName, params: toolParams, abortSignal } = params;
1402
- const entry = AGENT_TOOL_REGISTRY.find((tool) => tool.name === toolName);
1403
- if (!entry) {
1404
- return { success: false, error: `Tool not found: ${toolName}` };
1405
- }
1406
- try {
1407
- if (abortSignal.aborted) return { success: false, error: "cancelled" };
1408
- const execCtx = {
1409
- sessionId: params.sessionId,
1410
- abortSignal,
1411
- db: deps.db,
1412
- dataProvider: deps.dataProvider,
1413
- semanticIndexService: deps.semanticIndexService,
1414
- segmentText: deps.segmentText,
1415
- translateTemplate: deps.translateTemplate
1416
- };
1417
- const startTime = Date.now();
1418
- const result = await entry.handler(toolParams, execCtx);
1419
- const elapsed = Date.now() - startTime;
1420
- if (abortSignal.aborted) return { success: false, error: "cancelled" };
1421
- let details = result.data ?? void 0;
1422
- let truncated = false;
1423
- if (details) {
1424
- stripAvatarFields(details);
1425
- const raw = JSON.stringify(details);
1426
- if (raw.length > MAX_RESULT_CHARS) {
1427
- truncated = true;
1428
- details = { _truncated: true, _originalSize: raw.length, _preview: raw.slice(0, MAX_RESULT_CHARS) };
1429
- }
1430
- }
1431
- return {
1432
- success: true,
1433
- elapsed,
1434
- content: [{ type: "text", text: result.content }],
1435
- details,
1436
- truncated
1437
- };
1438
- } catch (error) {
1439
- if (abortSignal.aborted) {
1440
- return { success: false, error: "cancelled" };
1441
- }
1442
- return { success: false, error: error instanceof Error ? error.message : String(error) };
1443
- }
1444
- }
1445
-
1446
- // ../../packages/http-routes/src/routes/web/ai-tools.ts
1447
- var activeToolTests = /* @__PURE__ */ new Map();
1448
- function registerAiToolRoutes(server2, ctx) {
1449
- server2.get("/_web/ai/tools/full-catalog", async () => {
1450
- return AGENT_TOOL_REGISTRY.map((tool) => ({
1451
- name: tool.name,
1452
- category: tool.category ?? "core",
1453
- description: tool.description,
1454
- parameters: tool.inputSchema ?? {}
1455
- }));
1456
- });
1457
- server2.post("/_web/ai/tools/execute", async (request, reply) => {
1458
- const { testId, toolName, params, sessionId } = request.body;
1459
- const entry = AGENT_TOOL_REGISTRY.find((t) => t.name === toolName);
1460
- if (!entry) {
1461
- return reply.code(404).send({ success: false, error: `Tool not found: ${toolName}` });
1462
- }
1463
- const abortController = new AbortController();
1464
- activeToolTests.set(testId, abortController);
1465
- try {
1466
- if (ctx.executeAiTool) {
1467
- return await ctx.executeAiTool({ testId, toolName, params, sessionId, abortSignal: abortController.signal });
1468
- }
1469
- const db = ctx.dbManager.open(sessionId);
1470
- if (!db) {
1471
- return reply.code(404).send({ success: false, error: `Session not found: ${sessionId}` });
1472
- }
1473
- const result = await executeRegistryTool(
1474
- { testId, toolName, params, sessionId, abortSignal: abortController.signal },
1475
- {
1476
- db,
1477
- dataProvider: new CoreDataProvider(db)
1478
- }
1479
- );
1480
- if (!result.success && result.error !== "cancelled") {
1481
- appLogger.error("ai-tools", `Failed to execute tool ${toolName}`, { error: result.error });
1482
- }
1483
- return result;
1484
- } catch (error) {
1485
- if (abortController.signal.aborted) {
1486
- return { success: false, error: "cancelled" };
1487
- }
1488
- appLogger.error("ai-tools", `Failed to execute tool ${toolName}`, error);
1489
- return { success: false, error: String(error) };
1490
- } finally {
1491
- activeToolTests.delete(testId);
1492
- }
1493
- });
1494
- server2.post("/_web/ai/tools/cancel", async (request) => {
1495
- const { testId } = request.body;
1496
- const controller = activeToolTests.get(testId);
1497
- if (controller) {
1498
- controller.abort();
1499
- activeToolTests.delete(testId);
1500
- return { success: true };
1501
- }
1502
- return { success: false };
1503
- });
1504
- }
1505
-
1506
- // ../../packages/http-routes/src/routes/web/register-ai.ts
1507
- var REQUIRED_AI_DEPENDENCIES = [
1508
- "aiDataDir",
1509
- "aiChatManager",
1510
- "assistantManager",
1511
- "skillManagerCore",
1512
- "llmConfigStore",
1513
- "customProviderStore",
1514
- "customModelStore",
1515
- "runAgentStream"
1516
- ];
1517
- function assertRequiredAiContext(ctx) {
1518
- const missing = REQUIRED_AI_DEPENDENCIES.filter((key) => !ctx[key]);
1519
- if (missing.length > 0) {
1520
- throw new Error(`[http-routes] requireAi is set but missing AI dependencies: ${missing.join(", ")}`);
1521
- }
1522
- }
1523
- function registerAiRoutes(server2, ctx, options) {
1524
- if (options?.requireAi) assertRequiredAiContext(ctx);
1525
- registerAiAssistantRoutes(server2, ctx);
1526
- registerAiSkillRoutes(server2, ctx);
1527
- registerAiLlmRoutes(server2, ctx);
1528
- registerAiLlmStreamRoutes(server2, ctx);
1529
- registerAiAgentStreamRoutes(server2, ctx);
1530
- registerAiToolRoutes(server2, ctx);
1531
- registerAiChatRoutes(server2, ctx);
1532
- registerAiSummaryRoutes(server2, ctx);
1533
- registerAiLogRoutes(server2, ctx);
1534
- registerSemanticIndexRoutes(server2, ctx);
1535
- }
1536
-
1537
- // ../../packages/http-routes/src/routes/web/analytics.ts
1538
- import * as fs2 from "fs";
1539
- import * as path4 from "path";
1540
-
1541
- // ../../packages/http-routes/src/routes/web/time-filter.ts
1542
- function parseTimeFilter(query) {
1543
- const { startTs, endTs, memberId } = query;
1544
- if (!startTs && !endTs && !memberId) return void 0;
1545
- const filter = {};
1546
- if (startTs) filter.startTs = parseInt(startTs, 10);
1547
- if (endTs) filter.endTs = parseInt(endTs, 10);
1548
- if (memberId) filter.memberId = parseInt(memberId, 10);
1549
- return filter;
1550
- }
1551
-
1552
- // ../../packages/http-routes/src/routes/web/analytics-cache.ts
1553
- import * as path3 from "path";
1554
- function canonical(value) {
1555
- if (value === void 0) return "null";
1556
- if (value === null || typeof value !== "object") return JSON.stringify(value);
1557
- if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
1558
- const obj = value;
1559
- const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
1560
- return `{${keys.map((k) => `${JSON.stringify(k)}:${canonical(obj[k])}`).join(",")}}`;
1561
- }
1562
- function buildAnalyticsCacheKey(namespace, params) {
1563
- return `${namespace}:${canonical(params)}`;
1564
- }
1565
- function withAnalyticsCache(ctx, sessionId, namespace, params, compute, options) {
1566
- const queryCacheDir = path3.join(ctx.pathProvider.getCacheDir(), "query");
1567
- const dateStr = options?.dailyInvalidate ? `|date:${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}` : "";
1568
- const extraStr = options?.extraVersion ? `|${options.extraVersion}` : "";
1569
- const version = `${ctx.getVersion()}|${getDbFileVersion(ctx.sessionAdapter.getDbPath(sessionId))}${dateStr}${extraStr}`;
1570
- const key = buildAnalyticsCacheKey(namespace, params);
1571
- return getOrComputeAnalysisCache(sessionId, key, queryCacheDir, version, compute);
1572
- }
1573
-
1574
- // ../../packages/http-routes/src/routes/web/analytics.ts
1575
- function registerAnalyticsRoutes(server2, ctx) {
1576
- const { sessionAdapter: adapter } = ctx;
1577
- const cached = (name, sessionId, params, compute, options) => withAnalyticsCache(ctx, sessionId, `analytics.${name}`, params, compute, options);
1578
- server2.get("/_web/sessions/:id/years", async (request) => {
1579
- const db = adapter.ensureReadonly(request.params.id);
1580
- return getAvailableYears(db);
1581
- });
1582
- server2.get("/_web/sessions/:id/time-range", async (request) => {
1583
- const db = adapter.ensureReadonly(request.params.id);
1584
- return getTimeRange(db);
1585
- });
1586
- server2.get(
1587
- "/_web/sessions/:id/stats/member-activity",
1588
- async (request) => {
1589
- const id = request.params.id;
1590
- const filter = parseTimeFilter(request.query);
1591
- return cached("member-activity", id, { ...filter }, () => getMemberActivity(adapter.ensureReadonly(id), filter));
920
+ const id = request.params.id;
921
+ const filter = parseTimeFilter(request.query);
922
+ return cached("member-activity", id, { ...filter }, () => getMemberActivity(adapter.ensureReadonly(id), filter));
1592
923
  }
1593
924
  );
1594
925
  server2.get(
@@ -1684,10 +1015,10 @@ function registerAnalyticsRoutes(server2, ctx) {
1684
1015
  const id = request.params.id;
1685
1016
  const filter = parseTimeFilter(request.query);
1686
1017
  const locale = request.query.locale || "zh-CN";
1687
- const zhDictPath = path4.join(ctx.pathProvider.getSystemDir(), "nlp", "zh-CN.dict");
1018
+ const zhDictPath = path2.join(ctx.pathProvider.getSystemDir(), "nlp", "zh-CN.dict");
1688
1019
  let zhDictVersion;
1689
1020
  try {
1690
- const st = fs2.statSync(zhDictPath);
1021
+ const st = fs.statSync(zhDictPath);
1691
1022
  zhDictVersion = `${Math.floor(st.mtimeMs)}:${st.size}`;
1692
1023
  } catch {
1693
1024
  zhDictVersion = "-";
@@ -1815,484 +1146,907 @@ function registerAnalyticsRoutes(server2, ctx) {
1815
1146
  const filter = parseTimeFilter(request.query);
1816
1147
  return cached("meme-battle", id, { ...filter }, () => getMemeBattleAnalysis(adapter.ensureReadonly(id), filter));
1817
1148
  }
1818
- );
1819
- server2.get(
1820
- "/_web/sessions/:id/analytics/night-owl",
1821
- async (request) => {
1822
- const id = request.params.id;
1823
- const filter = parseTimeFilter(request.query);
1824
- return cached("night-owl", id, { ...filter }, () => getNightOwlAnalysis(adapter.ensureReadonly(id), filter), {
1825
- dailyInvalidate: true
1826
- });
1149
+ );
1150
+ server2.get(
1151
+ "/_web/sessions/:id/analytics/night-owl",
1152
+ async (request) => {
1153
+ const id = request.params.id;
1154
+ const filter = parseTimeFilter(request.query);
1155
+ return cached("night-owl", id, { ...filter }, () => getNightOwlAnalysis(adapter.ensureReadonly(id), filter), {
1156
+ dailyInvalidate: true
1157
+ });
1158
+ }
1159
+ );
1160
+ server2.get(
1161
+ "/_web/sessions/:id/analytics/repeat",
1162
+ async (request) => {
1163
+ const id = request.params.id;
1164
+ const filter = parseTimeFilter(request.query);
1165
+ return cached("repeat", id, { ...filter }, () => getRepeatAnalysis(adapter.ensureReadonly(id), filter));
1166
+ }
1167
+ );
1168
+ }
1169
+
1170
+ // ../../packages/http-routes/src/routes/web/sql.ts
1171
+ function registerSqlRoutes(server2, ctx) {
1172
+ const { sessionAdapter: adapter } = ctx;
1173
+ server2.post("/_web/sessions/:id/sql", async (request, reply) => {
1174
+ const db = adapter.ensureReadonly(request.params.id);
1175
+ const { sql } = request.body || {};
1176
+ if (!sql || typeof sql !== "string") {
1177
+ return reply.code(400).send({ error: "Missing sql parameter" });
1178
+ }
1179
+ try {
1180
+ const result = executeSql(db, sql, { columnar: true, timing: true, maxRows: 0 });
1181
+ return {
1182
+ columns: result.columns,
1183
+ rows: result.rows,
1184
+ rowCount: result.rowCount,
1185
+ duration: result.duration ?? 0,
1186
+ limited: result.truncated
1187
+ };
1188
+ } catch (err) {
1189
+ const message = err instanceof Error ? err.message : "SQL execution error";
1190
+ return reply.code(400).send({ error: message });
1191
+ }
1192
+ });
1193
+ server2.get("/_web/sessions/:id/schema", async (request) => {
1194
+ const db = adapter.ensureReadonly(request.params.id);
1195
+ return getSchemaDetailed(db);
1196
+ });
1197
+ server2.post("/_web/sessions/:id/query", async (request) => {
1198
+ const db = adapter.ensureReadonly(request.params.id);
1199
+ const { sql, params = [] } = request.body;
1200
+ if (!sql || typeof sql !== "string") {
1201
+ throw Object.assign(new Error('Missing or invalid "sql" field'), { statusCode: 400 });
1202
+ }
1203
+ const stmt = db.prepare(sql.trim());
1204
+ if (!stmt.readonly) {
1205
+ throw Object.assign(new Error("Only READ-ONLY statements are allowed"), { statusCode: 403 });
1206
+ }
1207
+ if (Array.isArray(params)) {
1208
+ return stmt.all(...params);
1209
+ }
1210
+ return stmt.all(params);
1211
+ });
1212
+ }
1213
+
1214
+ // ../../packages/http-routes/src/routes/web/session-index.ts
1215
+ function registerSessionIndexRoutes(server2, ctx) {
1216
+ const { sessionAdapter: adapter } = ctx;
1217
+ server2.get("/_web/sessions/index-stats", async () => {
1218
+ return session_index_service_exports.getAllIndexStats(adapter);
1219
+ });
1220
+ server2.post("/_web/sessions/:id/generate-index", async (request) => {
1221
+ const gapThreshold = request.body?.gapThreshold ?? 1800;
1222
+ const sessionCount = session_index_service_exports.generateIndex(adapter, request.params.id, gapThreshold);
1223
+ return { sessionCount };
1224
+ });
1225
+ server2.post("/_web/sessions/:id/generate-incremental-index", async (request) => {
1226
+ const gapThreshold = request.body?.gapThreshold ?? 1800;
1227
+ const sessionCount = session_index_service_exports.generateIncrementalIndex(adapter, request.params.id, gapThreshold);
1228
+ return { sessionCount };
1229
+ });
1230
+ server2.post("/_web/sessions/:id/clear-index", async (request) => {
1231
+ session_index_service_exports.clearIndex(adapter, request.params.id);
1232
+ return { success: true };
1233
+ });
1234
+ server2.get("/_web/sessions/:id/search/fts", async (request, reply) => {
1235
+ if (!session_index_service_exports.getFtsStatus(adapter, request.params.id)) {
1236
+ return reply.code(400).send({ error: "FTS index not built for this session" });
1237
+ }
1238
+ const keywords = request.query.keywords.split(/\s+/).filter(Boolean);
1239
+ if (keywords.length === 0) return { rowids: [], total: 0 };
1240
+ const limit = parseInt(request.query.limit || "100", 10);
1241
+ const offset = parseInt(request.query.offset || "0", 10);
1242
+ return session_index_service_exports.searchFts(adapter, request.params.id, keywords, limit, offset);
1243
+ });
1244
+ server2.get("/_web/sessions/:id/fts/status", async (request) => {
1245
+ return { hasFtsIndex: session_index_service_exports.getFtsStatus(adapter, request.params.id) };
1246
+ });
1247
+ server2.post("/_web/sessions/:id/fts/rebuild", async (request) => {
1248
+ const result = session_index_service_exports.rebuildFts(adapter, request.params.id);
1249
+ return { success: true, indexed: result.indexed };
1250
+ });
1251
+ }
1252
+
1253
+ // ../../packages/http-routes/src/routes/web/export.ts
1254
+ function registerExportRoutes(server2, ctx) {
1255
+ const { sessionAdapter: adapter } = ctx;
1256
+ server2.post("/_web/sessions/:id/export", async (request, reply) => {
1257
+ const { id } = request.params;
1258
+ const body = request.body;
1259
+ const sessionName = body?.sessionName || id;
1260
+ const format = body?.format || "txt";
1261
+ const result = export_service_exports.exportFormatted(adapter, {
1262
+ sessionId: id,
1263
+ sessionName,
1264
+ format,
1265
+ timeFilter: body.timeFilter
1266
+ });
1267
+ if (!result.success) {
1268
+ return reply.code(result.totalMessages === 0 ? 404 : 500).send({ error: result.error });
1269
+ }
1270
+ reply.header("Content-Type", result.mimeType);
1271
+ reply.header("Content-Disposition", `attachment; filename="${encodeURIComponent(result.filename)}"`);
1272
+ return reply.send(result.content);
1273
+ });
1274
+ }
1275
+
1276
+ // ../../packages/http-routes/src/routes/web/nlp.ts
1277
+ import * as fs2 from "fs";
1278
+ import * as path3 from "path";
1279
+ function registerNlpRoutes(server2, ctx) {
1280
+ const nlpDir = path3.join(ctx.pathProvider.getSystemDir(), "nlp");
1281
+ initNlpDir(nlpDir);
1282
+ ensureDefaultDict(nlpDir).catch((err) => console.warn("[NLP] Auto-download zh-CN dict failed:", err));
1283
+ server2.get("/_web/nlp/pos-tags", async () => {
1284
+ return POS_TAG_DEFINITIONS;
1285
+ });
1286
+ server2.get("/_web/nlp/dicts", async () => {
1287
+ return getDictList(nlpDir);
1288
+ });
1289
+ server2.get("/_web/nlp/dicts/:id/status", async (request) => {
1290
+ return isDictDownloaded(nlpDir, request.params.id);
1291
+ });
1292
+ server2.post("/_web/nlp/dicts/:id/download", async (request) => {
1293
+ return downloadDict(nlpDir, request.params.id);
1294
+ });
1295
+ server2.delete("/_web/nlp/dicts/:id", async (request) => {
1296
+ return deleteDict(nlpDir, request.params.id);
1297
+ });
1298
+ server2.post("/_web/nlp/word-frequency", async (request) => {
1299
+ const params = request.body;
1300
+ const db = ctx.dbManager.open(params.sessionId);
1301
+ if (!db) {
1302
+ throw Object.assign(new Error(`Session not found: ${params.sessionId}`), { statusCode: 404 });
1303
+ }
1304
+ const { sessionId, ...keyParams } = params;
1305
+ const effectiveDictId = !params.dictType || params.dictType === "default" ? "zh-CN" : String(params.dictType);
1306
+ const dictFilePath = path3.join(nlpDir, `${effectiveDictId}.dict`);
1307
+ let dictVersion;
1308
+ try {
1309
+ const st = fs2.statSync(dictFilePath);
1310
+ dictVersion = `${Math.floor(st.mtimeMs)}:${st.size}`;
1311
+ } catch {
1312
+ dictVersion = "-";
1827
1313
  }
1828
- );
1829
- server2.get(
1830
- "/_web/sessions/:id/analytics/repeat",
1314
+ return withAnalyticsCache(ctx, sessionId, "nlp.word-frequency", keyParams, () => computeWordFrequency(db, params), {
1315
+ extraVersion: `dict:${dictVersion}`
1316
+ });
1317
+ });
1318
+ server2.post(
1319
+ "/_web/nlp/segment",
1831
1320
  async (request) => {
1832
- const id = request.params.id;
1833
- const filter = parseTimeFilter(request.query);
1834
- return cached("repeat", id, { ...filter }, () => getRepeatAnalysis(adapter.ensureReadonly(id), filter));
1321
+ const { text, locale, minLength } = request.body;
1322
+ return segmentText(text, locale, minLength);
1835
1323
  }
1836
1324
  );
1837
1325
  }
1838
1326
 
1839
- // ../../packages/http-routes/src/routes/web/cache.ts
1840
- import * as fs3 from "fs";
1841
- import * as fsp from "fs/promises";
1842
- import * as path5 from "path";
1843
- async function getDirSize(dirPath) {
1844
- let totalSize = 0;
1845
- try {
1846
- if (!fs3.existsSync(dirPath)) return 0;
1847
- const files = await fsp.readdir(dirPath, { withFileTypes: true });
1848
- for (const file of files) {
1849
- const filePath = path5.join(dirPath, file.name);
1850
- if (file.isDirectory()) {
1851
- totalSize += await getDirSize(filePath);
1852
- } else {
1853
- const stat2 = await fsp.stat(filePath);
1854
- totalSize += stat2.size;
1855
- }
1856
- }
1857
- } catch {
1858
- }
1859
- return totalSize;
1860
- }
1861
- async function getFileCount(dirPath) {
1862
- let count = 0;
1863
- try {
1864
- if (!fs3.existsSync(dirPath)) return 0;
1865
- const files = await fsp.readdir(dirPath, { withFileTypes: true });
1866
- for (const file of files) {
1867
- const filePath = path5.join(dirPath, file.name);
1868
- if (file.isDirectory()) {
1869
- count += await getFileCount(filePath);
1870
- } else {
1871
- count++;
1872
- }
1873
- }
1874
- } catch {
1875
- }
1876
- return count;
1877
- }
1878
- function hasChatLabDatabases(dirPath) {
1879
- try {
1880
- const markerPath = path5.join(dirPath, ".chatlab");
1881
- const dbDir = path5.join(dirPath, "databases");
1882
- if (!fs3.existsSync(markerPath) || !fs3.existsSync(dbDir)) return false;
1883
- return fs3.readdirSync(dbDir).some((file) => file.endsWith(".db"));
1884
- } catch {
1885
- return false;
1886
- }
1327
+ // ../../packages/http-routes/src/routes/web/ai-assistants.ts
1328
+ function registerAiAssistantRoutes(server2, ctx) {
1329
+ server2.get("/_web/ai/tools/catalog", async () => {
1330
+ return BUILTIN_TOOL_CATALOG;
1331
+ });
1332
+ const mgr = ctx.assistantManager;
1333
+ if (!mgr) return;
1334
+ server2.get("/_web/ai/assistants", async () => {
1335
+ return mgr.getAllAssistants();
1336
+ });
1337
+ server2.get("/_web/ai/assistants/:id", async (request, reply) => {
1338
+ const config = mgr.getAssistantConfig(request.params.id);
1339
+ if (!config) return reply.code(404).send({ error: "Not found" });
1340
+ return config;
1341
+ });
1342
+ server2.post("/_web/ai/assistants", async (request) => {
1343
+ return mgr.createAssistant(request.body);
1344
+ });
1345
+ server2.put("/_web/ai/assistants/:id", async (request, reply) => {
1346
+ const result = mgr.updateAssistant(request.params.id, request.body);
1347
+ if (!result.success) return reply.code(404).send(result);
1348
+ return result;
1349
+ });
1350
+ server2.delete("/_web/ai/assistants/:id", async (request, reply) => {
1351
+ const result = mgr.deleteAssistant(request.params.id);
1352
+ if (!result.success) return reply.code(400).send(result);
1353
+ return result;
1354
+ });
1355
+ server2.post("/_web/ai/assistants/:id/reset", async (request, reply) => {
1356
+ const result = mgr.resetAssistant(request.params.id);
1357
+ if (!result.success) return reply.code(400).send(result);
1358
+ return result;
1359
+ });
1360
+ server2.post("/_web/ai/assistants/import", async (request) => {
1361
+ return mgr.importAssistantFromMd(request.body.rawMd);
1362
+ });
1363
+ server2.post("/_web/ai/assistants/import-builtin", async (request, reply) => {
1364
+ const result = mgr.importAssistant(request.body.builtinId);
1365
+ if (!result.success) return reply.code(400).send(result);
1366
+ return result;
1367
+ });
1368
+ server2.post("/_web/ai/assistants/:id/reimport", async (request, reply) => {
1369
+ const result = mgr.reimportAssistant(request.params.id);
1370
+ if (!result.success) return reply.code(400).send(result);
1371
+ return result;
1372
+ });
1887
1373
  }
1888
- function registerCacheRoutes(server2, ctx) {
1889
- const pp = ctx.pathProvider;
1890
- const downloadsDir = ctx.downloadsDir ?? pp.getDownloadsDir();
1891
- server2.get("/_web/cache/info", async () => {
1892
- const directories = [
1893
- {
1894
- id: "databases",
1895
- name: "settings.storage.cache.databases.name",
1896
- description: "settings.storage.cache.databases.description",
1897
- path: pp.getDatabaseDir(),
1898
- scope: "user-data",
1899
- rootPath: pp.getUserDataDir(),
1900
- icon: "i-heroicons-circle-stack",
1901
- canClear: false
1902
- },
1903
- {
1904
- id: "ai",
1905
- name: "settings.storage.cache.ai.name",
1906
- description: "settings.storage.cache.ai.description",
1907
- path: pp.getAiDataDir(),
1908
- scope: "system-data",
1909
- rootPath: pp.getSystemDir(),
1910
- icon: "i-heroicons-sparkles",
1911
- canClear: false
1912
- },
1913
- {
1914
- id: "cache",
1915
- name: "settings.storage.cache.statsCache.name",
1916
- description: "settings.storage.cache.statsCache.description",
1917
- path: pp.getCacheDir(),
1918
- scope: "system-data",
1919
- rootPath: pp.getSystemDir(),
1920
- icon: "i-heroicons-bolt",
1921
- canClear: true
1922
- },
1923
- {
1924
- id: "logs",
1925
- name: "settings.storage.cache.logs.name",
1926
- description: "settings.storage.cache.logs.description",
1927
- path: pp.getLogsDir(),
1928
- scope: "system-data",
1929
- rootPath: pp.getSystemDir(),
1930
- icon: "i-heroicons-document-text",
1931
- canClear: true
1932
- }
1933
- ];
1934
- const results = await Promise.all(
1935
- directories.map(async (dir) => {
1936
- const [size, fileCount] = await Promise.all([getDirSize(dir.path), getFileCount(dir.path)]);
1937
- return { ...dir, size, fileCount, exists: fs3.existsSync(dir.path) };
1938
- })
1939
- );
1940
- return {
1941
- baseDir: pp.getSystemDir(),
1942
- directories: results,
1943
- totalSize: results.reduce((sum, d) => sum + d.size, 0)
1944
- };
1374
+
1375
+ // ../../packages/http-routes/src/routes/web/ai-skills.ts
1376
+ function registerAiSkillRoutes(server2, ctx) {
1377
+ const mgr = ctx.skillManagerCore;
1378
+ if (!mgr) return;
1379
+ server2.get("/_web/ai/skills", async () => {
1380
+ return mgr.getAllSkills();
1945
1381
  });
1946
- server2.post("/_web/cache/clear", async (request) => {
1947
- const { cacheId } = request.body;
1948
- const allowedDirs = {
1949
- cache: pp.getCacheDir(),
1950
- logs: pp.getLogsDir()
1951
- };
1952
- const dirPath = allowedDirs[cacheId];
1953
- if (!dirPath) return { success: false, error: "Not allowed to clear this directory" };
1954
- if (!fs3.existsSync(dirPath)) return { success: true };
1955
- const files = await fsp.readdir(dirPath);
1956
- for (const file of files) {
1957
- const filePath = path5.join(dirPath, file);
1958
- const stat2 = await fsp.stat(filePath);
1959
- if (stat2.isDirectory()) {
1960
- await fsp.rm(filePath, { recursive: true });
1961
- } else {
1962
- await fsp.unlink(filePath);
1963
- }
1964
- }
1965
- return { success: true };
1382
+ server2.get("/_web/ai/skills/:id", async (request, reply) => {
1383
+ const config = mgr.getSkillConfig(request.params.id);
1384
+ if (!config) return reply.code(404).send({ error: "Not found" });
1385
+ return config;
1386
+ });
1387
+ server2.post("/_web/ai/skills", async (request) => {
1388
+ return mgr.createSkill(request.body.rawMd);
1389
+ });
1390
+ server2.put("/_web/ai/skills/:id", async (request, reply) => {
1391
+ const result = mgr.updateSkill(request.params.id, request.body.rawMd);
1392
+ if (!result.success) return reply.code(404).send(result);
1393
+ return result;
1394
+ });
1395
+ server2.delete("/_web/ai/skills/:id", async (request, reply) => {
1396
+ const result = mgr.deleteSkill(request.params.id);
1397
+ if (!result.success) return reply.code(400).send(result);
1398
+ return result;
1399
+ });
1400
+ server2.post("/_web/ai/skills/import", async (request) => {
1401
+ return mgr.importSkillFromMd(request.body.rawMd);
1402
+ });
1403
+ server2.get("/_web/ai/skills/builtin-catalog", async () => {
1404
+ return mgr.getBuiltinCatalog();
1405
+ });
1406
+ server2.post("/_web/ai/skills/import-builtin", async (request, reply) => {
1407
+ const result = mgr.importSkill(request.body.builtinId);
1408
+ if (!result.success) return reply.code(400).send(result);
1409
+ return result;
1410
+ });
1411
+ server2.post("/_web/ai/skills/:id/reimport", async (request, reply) => {
1412
+ const result = mgr.reimportSkill(request.params.id);
1413
+ if (!result.success) return reply.code(400).send(result);
1414
+ return result;
1415
+ });
1416
+ }
1417
+
1418
+ // ../../packages/http-routes/src/routes/web/ai-llm.ts
1419
+ function hasRealApiKey(apiKey) {
1420
+ return !!apiKey && apiKey !== "sk-no-key-required";
1421
+ }
1422
+ function toConfigDisplay(config) {
1423
+ const { apiKey: _raw, ...rest } = config;
1424
+ return { ...rest, apiKey: "", apiKeySet: hasRealApiKey(String(_raw || "")) };
1425
+ }
1426
+ function registerAiLlmRoutes(server2, ctx) {
1427
+ const store = ctx.llmConfigStore;
1428
+ const aiDataDir = ctx.aiDataDir;
1429
+ if (!store || !aiDataDir) return;
1430
+ server2.get("/_web/ai/llm/has-config", async () => {
1431
+ return store.hasActiveConfig();
1966
1432
  });
1967
- server2.get("/_web/cache/data-dir", async () => {
1968
- const pending = ctx.getPendingDataDirMigration?.();
1433
+ server2.get("/_web/ai/llm/configs", async () => {
1434
+ const data = store.loadStore();
1969
1435
  return {
1970
- path: pp.getUserDataDir(),
1971
- defaultPath: ctx.defaultUserDataDir,
1972
- isCustom: ctx.isCustomDataDir ?? false,
1973
- canSetDataDir: ctx.canSetDataDir ?? Boolean(ctx.setDataDir),
1974
- managedScope: "chat-databases",
1975
- managedDescription: "settings.storage.dataLocation.managedDescription",
1976
- hasLegacyDataAtDefaultDir: Boolean(ctx.defaultUserDataDir) && path5.resolve(pp.getUserDataDir()) !== path5.resolve(ctx.defaultUserDataDir ?? "") && hasChatLabDatabases(ctx.defaultUserDataDir ?? ""),
1977
- pendingMigration: pending ? {
1978
- from: pending.from,
1979
- to: pending.to,
1980
- createdAt: pending.createdAt
1981
- } : void 0
1436
+ configs: data.configs.map((c) => toConfigDisplay(c)),
1437
+ defaultAssistant: data.defaultAssistant,
1438
+ fastModel: data.fastModel
1982
1439
  };
1983
1440
  });
1984
- server2.post("/_web/cache/data-dir", async (request, reply) => {
1985
- if (!ctx.setDataDir) {
1986
- return reply.code(501).send({ success: false, error: "Data directory changes are not supported" });
1441
+ server2.post("/_web/ai/llm/configs", async (request) => {
1442
+ const result = store.addConfig(request.body);
1443
+ if (result.success && result.config) {
1444
+ return { ...result, config: toConfigDisplay(result.config) };
1987
1445
  }
1988
- const targetPath = typeof request.body?.path === "string" ? request.body.path : null;
1989
- const migrate = request.body?.migrate !== false;
1990
- return ctx.setDataDir(targetPath, migrate);
1446
+ return result;
1991
1447
  });
1992
- server2.get("/_web/cache/latest-import-log", async () => {
1993
- const importLogDir = path5.join(pp.getLogsDir(), "import");
1994
- if (!fs3.existsSync(importLogDir)) {
1995
- return { success: false, error: "Log directory not found" };
1448
+ server2.put("/_web/ai/llm/configs/:id", async (request) => {
1449
+ return store.updateConfig(request.params.id, request.body);
1450
+ });
1451
+ server2.delete("/_web/ai/llm/configs/:id", async (request) => {
1452
+ return store.deleteConfig(request.params.id);
1453
+ });
1454
+ server2.get("/_web/ai/llm/default-assistant-slot", async () => {
1455
+ return store.getDefaultAssistantSlot();
1456
+ });
1457
+ server2.put("/_web/ai/llm/default-assistant-slot", async (request) => {
1458
+ return store.setDefaultAssistantModel(request.body.configId, request.body.modelId);
1459
+ });
1460
+ server2.get("/_web/ai/llm/fast-model-slot", async () => {
1461
+ return store.getFastModelSlot();
1462
+ });
1463
+ server2.put("/_web/ai/llm/fast-model-slot", async (request) => {
1464
+ return store.setFastModel(request.body);
1465
+ });
1466
+ server2.get("/_web/ai/llm/providers", async () => {
1467
+ return BUILTIN_PROVIDERS.map((p) => {
1468
+ const models = getBuiltinModelsByProvider(p.id);
1469
+ return {
1470
+ id: p.id,
1471
+ name: p.name,
1472
+ defaultBaseUrl: p.defaultBaseUrl,
1473
+ models: models.filter((m) => !m.capabilities.includes("embedding") && !m.capabilities.includes("ranking")).map((m) => ({ id: m.id, name: m.name, description: m.description }))
1474
+ };
1475
+ });
1476
+ });
1477
+ server2.get("/_web/ai/llm/provider-registry", async () => {
1478
+ const custom = ctx.customProviderStore?.getAll() ?? [];
1479
+ return [...BUILTIN_PROVIDERS, ...custom];
1480
+ });
1481
+ server2.get("/_web/ai/llm/model-catalog", async () => {
1482
+ const custom = ctx.customModelStore?.getAll() ?? [];
1483
+ return [...BUILTIN_MODELS, ...custom];
1484
+ });
1485
+ server2.post("/_web/ai/llm/custom-providers", async (request, reply) => {
1486
+ if (!ctx.customProviderStore)
1487
+ return reply.code(501).send({ success: false, error: "Custom providers not available" });
1488
+ return ctx.customProviderStore.add(request.body);
1489
+ });
1490
+ server2.put("/_web/ai/llm/custom-providers/:id", async (request, reply) => {
1491
+ if (!ctx.customProviderStore)
1492
+ return reply.code(501).send({ success: false, error: "Custom providers not available" });
1493
+ const result = ctx.customProviderStore.update(request.params.id, request.body);
1494
+ if (!result.success) return reply.code(404).send(result);
1495
+ return result;
1496
+ });
1497
+ server2.delete("/_web/ai/llm/custom-providers/:id", async (request, reply) => {
1498
+ if (!ctx.customProviderStore)
1499
+ return reply.code(501).send({ success: false, error: "Custom providers not available" });
1500
+ const result = ctx.customProviderStore.delete(request.params.id);
1501
+ if (!result.success) return reply.code(404).send(result);
1502
+ return result;
1503
+ });
1504
+ server2.post("/_web/ai/llm/custom-models", async (request, reply) => {
1505
+ if (!ctx.customModelStore) return reply.code(501).send({ success: false, error: "Custom models not available" });
1506
+ const result = ctx.customModelStore.add(request.body);
1507
+ if (!result.success) return reply.code(409).send(result);
1508
+ return result;
1509
+ });
1510
+ server2.put("/_web/ai/llm/custom-models/:providerId/:modelId", async (request, reply) => {
1511
+ if (!ctx.customModelStore) return reply.code(501).send({ success: false, error: "Custom models not available" });
1512
+ const result = ctx.customModelStore.update(request.params.providerId, request.params.modelId, request.body);
1513
+ if (!result.success) return reply.code(404).send(result);
1514
+ return result;
1515
+ });
1516
+ server2.delete("/_web/ai/llm/custom-models/:providerId/:modelId", async (request, reply) => {
1517
+ if (!ctx.customModelStore) return reply.code(501).send({ success: false, error: "Custom models not available" });
1518
+ const result = ctx.customModelStore.delete(request.params.providerId, request.params.modelId);
1519
+ if (!result.success) return reply.code(404).send(result);
1520
+ return result;
1521
+ });
1522
+ server2.post("/_web/ai/llm/validate-key", async (request) => {
1523
+ const { provider, apiKey, baseUrl, model, apiFormat, configId } = request.body;
1524
+ const resolvedKey = apiKey?.trim() ? apiKey : configId ? store.getConfigById(configId)?.apiKey || "" : "";
1525
+ return validateApiKey(provider, resolvedKey, baseUrl, model, apiFormat);
1526
+ });
1527
+ server2.post("/_web/ai/llm/remote-models", async (request) => {
1528
+ const { provider, apiKey, baseUrl, apiFormat, configId } = request.body;
1529
+ const resolvedKey = apiKey?.trim() ? apiKey : configId ? store.getConfigById(configId)?.apiKey || "" : "";
1530
+ return fetchRemoteModels(provider, resolvedKey, baseUrl, apiFormat);
1531
+ });
1532
+ server2.get("/_web/ai/desensitize-rules/defaults", async (request) => {
1533
+ return getDefaultRulesForLocale(request.query.locale ?? "zh-CN");
1534
+ });
1535
+ server2.post("/_web/ai/desensitize-rules/merge", async (request) => {
1536
+ return mergeRulesForLocale(request.body.existingRules, request.body.locale, request.body.overrides ?? {});
1537
+ });
1538
+ }
1539
+
1540
+ // ../../packages/http-routes/src/routes/web/ai-llm-stream.ts
1541
+ function registerAiLlmStreamRoutes(server2, ctx) {
1542
+ const store = ctx.llmConfigStore;
1543
+ if (!store) return;
1544
+ server2.post("/_web/ai/llm/chat-stream", async (request, reply) => {
1545
+ const { messages, options } = request.body;
1546
+ const llmConfig = store.getDefaultAssistantConfig();
1547
+ if (!llmConfig) {
1548
+ return reply.code(400).send({ success: false, error: "LLM service not configured" });
1996
1549
  }
1997
- const files = await fsp.readdir(importLogDir);
1998
- const logFiles = files.filter((f) => f.startsWith("import_") && f.endsWith(".log"));
1999
- if (logFiles.length === 0) {
2000
- return { success: false, error: "No import logs found" };
1550
+ const piModel = buildPiModel(llmConfig);
1551
+ const abortController = new AbortController();
1552
+ reply.raw.writeHead(200, {
1553
+ "Content-Type": "text/event-stream",
1554
+ "Cache-Control": "no-cache",
1555
+ Connection: "keep-alive"
1556
+ });
1557
+ reply.raw.on("close", () => {
1558
+ if (!abortController.signal.aborted) abortController.abort();
1559
+ });
1560
+ const sendChunk = (data) => {
1561
+ if (reply.raw.writableEnded || reply.raw.destroyed) return;
1562
+ reply.raw.write(`event: chunk
1563
+ data: ${JSON.stringify(data)}
1564
+
1565
+ `);
1566
+ };
1567
+ try {
1568
+ await runSimpleLlmStream({
1569
+ messages,
1570
+ apiKey: llmConfig.apiKey,
1571
+ piModel,
1572
+ temperature: options?.temperature,
1573
+ maxTokens: options?.maxTokens,
1574
+ onChunk: sendChunk,
1575
+ abortSignal: abortController.signal
1576
+ });
1577
+ } catch (error) {
1578
+ if (abortController.signal.aborted) return;
1579
+ const msg = error instanceof Error ? error.message : String(error);
1580
+ sendChunk({ content: "", isFinished: true, finishReason: "error", error: msg });
1581
+ } finally {
1582
+ if (!reply.raw.writableEnded && !reply.raw.destroyed) reply.raw.end();
2001
1583
  }
2002
- const fileStats = await Promise.all(
2003
- logFiles.map(async (f) => {
2004
- const filePath = path5.join(importLogDir, f);
2005
- const stat2 = await fsp.stat(filePath);
2006
- return { name: f, path: filePath, mtime: stat2.mtime.getTime() };
2007
- })
2008
- );
2009
- fileStats.sort((a, b) => b.mtime - a.mtime);
2010
- return { success: true, path: fileStats[0].path, name: fileStats[0].name };
2011
1584
  });
2012
- server2.post("/_web/cache/save-to-downloads", async (request) => {
2013
- const { filename, dataUrl } = request.body;
2014
- if (!filename || !dataUrl) {
2015
- return { success: false, error: "filename and dataUrl are required" };
2016
- }
2017
- let buffer;
2018
- if (dataUrl.includes(";base64,")) {
2019
- const base64Data = dataUrl.split(";base64,")[1];
2020
- buffer = Buffer.from(base64Data, "base64");
2021
- } else if (dataUrl.includes("charset=utf-8,")) {
2022
- const textData = dataUrl.split("charset=utf-8,")[1];
2023
- buffer = Buffer.from(decodeURIComponent(textData), "utf-8");
2024
- } else {
2025
- const base64Data = dataUrl.replace(/^data:[^,]+,/, "");
2026
- buffer = Buffer.from(base64Data, "base64");
1585
+ }
1586
+
1587
+ // ../../packages/http-routes/src/routes/web/ai-agent-stream.ts
1588
+ var activeAgentAborts = /* @__PURE__ */ new Map();
1589
+ function registerAiAgentStreamRoutes(server2, ctx) {
1590
+ if (!ctx.runAgentStream) return;
1591
+ const runAgentStream = ctx.runAgentStream;
1592
+ server2.post("/_web/ai/agent/stream", async (request, reply) => {
1593
+ const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1594
+ const abortController = new AbortController();
1595
+ activeAgentAborts.set(requestId, abortController);
1596
+ reply.raw.writeHead(200, {
1597
+ "Content-Type": "text/event-stream",
1598
+ "Cache-Control": "no-cache",
1599
+ Connection: "keep-alive",
1600
+ "X-Request-Id": requestId
1601
+ });
1602
+ let emittedDone = false;
1603
+ const safeSendSSE = (event, data) => {
1604
+ if (reply.raw.writableEnded || reply.raw.destroyed) return;
1605
+ reply.raw.write(`event: ${event}
1606
+ data: ${JSON.stringify(data)}
1607
+
1608
+ `);
1609
+ };
1610
+ const safeEnd = () => {
1611
+ if (reply.raw.writableEnded || reply.raw.destroyed) return;
1612
+ reply.raw.end();
1613
+ };
1614
+ safeSendSSE("meta", { requestId });
1615
+ reply.raw.on("close", () => {
1616
+ if (!abortController.signal.aborted) {
1617
+ abortController.abort();
1618
+ }
1619
+ activeAgentAborts.delete(requestId);
1620
+ });
1621
+ try {
1622
+ await runAgentStream(
1623
+ request.body,
1624
+ (chunk) => {
1625
+ if (chunk.type === "done") emittedDone = true;
1626
+ safeSendSSE(chunk.type, chunk);
1627
+ },
1628
+ abortController.signal
1629
+ );
1630
+ } catch (error) {
1631
+ const msg = error instanceof Error ? error.message : String(error);
1632
+ safeSendSSE("error", { type: "error", error: { name: "ServerError", message: msg } });
1633
+ } finally {
1634
+ if (!emittedDone) safeSendSSE("done", { type: "done", isFinished: true });
1635
+ activeAgentAborts.delete(requestId);
1636
+ safeEnd();
2027
1637
  }
2028
- if (!fs3.existsSync(downloadsDir)) {
2029
- fs3.mkdirSync(downloadsDir, { recursive: true });
1638
+ });
1639
+ server2.post("/_web/ai/agent/abort", async (request) => {
1640
+ const { requestId } = request.body;
1641
+ const controller = activeAgentAborts.get(requestId);
1642
+ if (controller) {
1643
+ controller.abort();
1644
+ activeAgentAborts.delete(requestId);
1645
+ return { success: true };
2030
1646
  }
2031
- const filePath = path5.join(downloadsDir, filename);
2032
- fs3.writeFileSync(filePath, buffer);
2033
- return { success: true, filePath };
1647
+ return { success: false };
2034
1648
  });
2035
- server2.post("/_web/cache/open-dir", async (request, reply) => {
2036
- if (!ctx.openDirectory) return reply.code(501).send({ success: false, error: "Not supported" });
2037
- const { cacheId } = request.body;
2038
- const dirPaths = {
2039
- base: pp.getSystemDir(),
2040
- userData: pp.getUserDataDir(),
2041
- databases: pp.getDatabaseDir(),
2042
- cache: pp.getCacheDir(),
2043
- ai: pp.getAiDataDir(),
2044
- logs: pp.getLogsDir(),
2045
- downloads: downloadsDir
1649
+ }
1650
+
1651
+ // ../../packages/http-routes/src/ai/tool-executor.ts
1652
+ var MAX_RESULT_CHARS = 5e5;
1653
+ async function executeRegistryTool(params, deps) {
1654
+ const { toolName, params: toolParams, abortSignal } = params;
1655
+ const entry = AGENT_TOOL_REGISTRY.find((tool) => tool.name === toolName);
1656
+ if (!entry) {
1657
+ return { success: false, error: `Tool not found: ${toolName}` };
1658
+ }
1659
+ try {
1660
+ if (abortSignal.aborted) return { success: false, error: "cancelled" };
1661
+ const execCtx = {
1662
+ sessionId: params.sessionId,
1663
+ abortSignal,
1664
+ db: deps.db,
1665
+ dataProvider: deps.dataProvider,
1666
+ semanticIndexService: deps.semanticIndexService,
1667
+ segmentText: deps.segmentText,
1668
+ translateTemplate: deps.translateTemplate
2046
1669
  };
2047
- const dirPath = dirPaths[cacheId];
2048
- if (!dirPath) return { success: false, error: "Unknown directory" };
2049
- if (!fs3.existsSync(dirPath)) {
2050
- await fsp.mkdir(dirPath, { recursive: true });
1670
+ const startTime = Date.now();
1671
+ const result = await entry.handler(toolParams, execCtx);
1672
+ const elapsed = Date.now() - startTime;
1673
+ if (abortSignal.aborted) return { success: false, error: "cancelled" };
1674
+ let details = result.data ?? void 0;
1675
+ let truncated = false;
1676
+ if (details) {
1677
+ stripAvatarFields(details);
1678
+ const raw = JSON.stringify(details);
1679
+ if (raw.length > MAX_RESULT_CHARS) {
1680
+ truncated = true;
1681
+ details = { _truncated: true, _originalSize: raw.length, _preview: raw.slice(0, MAX_RESULT_CHARS) };
1682
+ }
2051
1683
  }
2052
- try {
2053
- await ctx.openDirectory(dirPath);
2054
- } catch (error) {
2055
- return { success: false, error: error instanceof Error ? error.message : String(error) };
1684
+ return {
1685
+ success: true,
1686
+ elapsed,
1687
+ content: [{ type: "text", text: result.content }],
1688
+ details,
1689
+ truncated
1690
+ };
1691
+ } catch (error) {
1692
+ if (abortSignal.aborted) {
1693
+ return { success: false, error: "cancelled" };
2056
1694
  }
2057
- return { success: true };
1695
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
1696
+ }
1697
+ }
1698
+
1699
+ // ../../packages/http-routes/src/routes/web/ai-tools.ts
1700
+ var activeToolTests = /* @__PURE__ */ new Map();
1701
+ function registerAiToolRoutes(server2, ctx) {
1702
+ server2.get("/_web/ai/tools/full-catalog", async () => {
1703
+ return AGENT_TOOL_REGISTRY.map((tool) => ({
1704
+ name: tool.name,
1705
+ category: tool.category ?? "core",
1706
+ description: tool.description,
1707
+ parameters: tool.inputSchema ?? {}
1708
+ }));
2058
1709
  });
2059
- server2.post("/_web/cache/show-in-folder", async (request, reply) => {
2060
- if (!ctx.showInFolder) return reply.code(501).send({ success: false, error: "Not supported" });
2061
- const { filePath } = request.body;
2062
- if (!filePath) return { success: false, error: "filePath is required" };
1710
+ server2.post("/_web/ai/tools/execute", async (request, reply) => {
1711
+ const { testId, toolName, params, sessionId } = request.body;
1712
+ const entry = AGENT_TOOL_REGISTRY.find((t) => t.name === toolName);
1713
+ if (!entry) {
1714
+ return reply.code(404).send({ success: false, error: `Tool not found: ${toolName}` });
1715
+ }
1716
+ const abortController = new AbortController();
1717
+ activeToolTests.set(testId, abortController);
2063
1718
  try {
2064
- await ctx.showInFolder(filePath);
1719
+ if (ctx.executeAiTool) {
1720
+ return await ctx.executeAiTool({ testId, toolName, params, sessionId, abortSignal: abortController.signal });
1721
+ }
1722
+ const db = ctx.dbManager.open(sessionId);
1723
+ if (!db) {
1724
+ return reply.code(404).send({ success: false, error: `Session not found: ${sessionId}` });
1725
+ }
1726
+ const result = await executeRegistryTool(
1727
+ { testId, toolName, params, sessionId, abortSignal: abortController.signal },
1728
+ {
1729
+ db,
1730
+ dataProvider: new CoreDataProvider(db)
1731
+ }
1732
+ );
1733
+ if (!result.success && result.error !== "cancelled") {
1734
+ appLogger.error("ai-tools", `Failed to execute tool ${toolName}`, { error: result.error });
1735
+ }
1736
+ return result;
2065
1737
  } catch (error) {
2066
- return { success: false, error: error instanceof Error ? error.message : String(error) };
1738
+ if (abortController.signal.aborted) {
1739
+ return { success: false, error: "cancelled" };
1740
+ }
1741
+ appLogger.error("ai-tools", `Failed to execute tool ${toolName}`, error);
1742
+ return { success: false, error: String(error) };
1743
+ } finally {
1744
+ activeToolTests.delete(testId);
2067
1745
  }
2068
- return { success: true };
1746
+ });
1747
+ server2.post("/_web/ai/tools/cancel", async (request) => {
1748
+ const { testId } = request.body;
1749
+ const controller = activeToolTests.get(testId);
1750
+ if (controller) {
1751
+ controller.abort();
1752
+ activeToolTests.delete(testId);
1753
+ return { success: true };
1754
+ }
1755
+ return { success: false };
2069
1756
  });
2070
1757
  }
2071
1758
 
2072
- // ../../packages/http-routes/src/routes/web/contacts.ts
2073
- function registerContactsRoutes(server2, ctx) {
2074
- const service = ctx.contactsService ?? createContactsService({
2075
- adapter: ctx.sessionAdapter,
2076
- pathProvider: ctx.pathProvider,
2077
- runtimeIdentity: ctx.runtimeIdentity,
2078
- nativeBinding: ctx.nativeBinding
1759
+ // ../../packages/http-routes/src/routes/web/ai-chats.ts
1760
+ function registerAiChatRoutes(server2, ctx) {
1761
+ const cm = ctx.aiChatManager;
1762
+ if (!cm) return;
1763
+ server2.post("/_web/ai/chats", async (request) => {
1764
+ const { sessionId, title, assistantId } = request.body;
1765
+ return cm.createAIChat(sessionId, title, assistantId);
2079
1766
  });
2080
- server2.addHook("onClose", async () => {
2081
- await service.close();
1767
+ server2.get("/_web/ai/chats", async (request) => {
1768
+ const { sessionId } = request.query;
1769
+ if (!sessionId) return [];
1770
+ return cm.getAIChats(sessionId);
2082
1771
  });
2083
- server2.get("/_web/contacts", async (request) => {
2084
- return service.getContactsPage({
2085
- acceptStale: isTruthy(request.query.acceptStale),
2086
- timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange),
2087
- pool: parseContactPool(request.query.pool),
2088
- page: parsePositiveInteger(request.query.page),
2089
- pageSize: parsePositiveInteger(request.query.pageSize),
2090
- query: request.query.q
2091
- });
1772
+ server2.get("/_web/ai/chats/:id", async (request, reply) => {
1773
+ const conv = cm.getAIChat(request.params.id);
1774
+ if (!conv) return reply.code(404).send({ error: "AI chat not found" });
1775
+ return conv;
2092
1776
  });
2093
- server2.post("/_web/contacts/recompute", async (request) => {
2094
- return service.startRecompute({
2095
- timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange),
2096
- pool: parseContactPool(request.query.pool),
2097
- page: parsePositiveInteger(request.query.page),
2098
- pageSize: parsePositiveInteger(request.query.pageSize),
2099
- query: request.query.q
2100
- });
1777
+ server2.put("/_web/ai/chats/:id/title", async (request) => {
1778
+ return cm.updateAIChatTitle(request.params.id, request.body.title);
2101
1779
  });
2102
- server2.get("/_web/contacts/:key/detail", async (request) => {
2103
- return service.getContactDetail(request.params.key, {
2104
- acceptStale: isTruthy(request.query.acceptStale),
2105
- timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange)
2106
- });
1780
+ server2.delete("/_web/ai/chats/:id", async (request) => {
1781
+ return cm.deleteAIChat(request.params.id);
2107
1782
  });
2108
- server2.put(
2109
- "/_web/contacts/:key/mark-friend",
2110
- async (request) => {
2111
- return service.markContactAsFriend(request.params.key, {
2112
- timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange)
2113
- });
1783
+ server2.post("/_web/ai/chats/:id/messages", async (request) => {
1784
+ const { role, content, dataKeywords, dataMessageCount, contentBlocks, tokenUsage } = request.body;
1785
+ return cm.addMessage(
1786
+ request.params.id,
1787
+ role,
1788
+ content,
1789
+ dataKeywords,
1790
+ dataMessageCount,
1791
+ contentBlocks,
1792
+ tokenUsage
1793
+ );
1794
+ });
1795
+ server2.get("/_web/ai/chats/:id/messages", async (request) => {
1796
+ return cm.getMessages(request.params.id);
1797
+ });
1798
+ server2.post("/_web/ai/chats/:id/messages/:messageId/delete-from", async (request, reply) => {
1799
+ const { id, messageId } = request.params;
1800
+ if (!id || typeof id !== "string" || !messageId || typeof messageId !== "string") {
1801
+ return reply.code(400).send({ error: "aiChatId and messageId are required" });
2114
1802
  }
2115
- );
2116
- server2.delete(
2117
- "/_web/contacts/:key/mark-friend",
2118
- async (request) => {
2119
- return service.unmarkContactAsFriend(request.params.key, {
2120
- timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange)
2121
- });
1803
+ cm.deleteMessagesFrom(id, messageId);
1804
+ return { success: true };
1805
+ });
1806
+ server2.post("/_web/ai/chats/:id/fork", async (request, reply) => {
1807
+ const { upToMessageId, title } = request.body;
1808
+ if (!upToMessageId || typeof upToMessageId !== "string") {
1809
+ return reply.code(400).send({ error: "upToMessageId is required" });
1810
+ }
1811
+ return cm.forkAIChat(request.params.id, upToMessageId, title);
1812
+ });
1813
+ server2.put("/_web/ai/messages/:messageId/content", async (request, reply) => {
1814
+ const { content } = request.body;
1815
+ if (!content || typeof content !== "string") {
1816
+ return reply.code(400).send({ error: "content is required" });
1817
+ }
1818
+ cm.updateMessageContent(request.params.messageId, content);
1819
+ return { success: true };
1820
+ });
1821
+ server2.post("/_web/ai/chats/:id/messages/:messageId/delete-relink", async (request, reply) => {
1822
+ const { id, messageId } = request.params;
1823
+ if (!id || !messageId) {
1824
+ return reply.code(400).send({ error: "aiChatId and messageId are required" });
1825
+ }
1826
+ cm.deleteAndRelinkMessage(id, messageId);
1827
+ return { success: true };
1828
+ });
1829
+ server2.post("/_web/ai/chats/:id/messages/insert-after", async (request, reply) => {
1830
+ const { afterMessageId, role, content, contentBlocks, tokenUsage } = request.body;
1831
+ if (!afterMessageId || typeof afterMessageId !== "string") {
1832
+ return reply.code(400).send({ error: "afterMessageId is required" });
1833
+ }
1834
+ return cm.insertMessageAfter(request.params.id, afterMessageId, role, content, contentBlocks, tokenUsage);
1835
+ });
1836
+ server2.get("/_web/ai/chats/:id/token-usage", async (request) => {
1837
+ return cm.getAIChatTokenUsage(request.params.id);
1838
+ });
1839
+ server2.get("/_web/ai/debug/schema", async () => {
1840
+ return cm.getAiSchema();
1841
+ });
1842
+ server2.post("/_web/ai/debug/execute-sql", async (request, reply) => {
1843
+ const { sql } = request.body;
1844
+ if (!sql || typeof sql !== "string") {
1845
+ return reply.code(400).send({ error: "sql is required" });
2122
1846
  }
2123
- );
2124
- }
2125
- function isTruthy(value) {
2126
- return value === "1" || value === "true" || value === "yes";
2127
- }
2128
- function parseContactsTimeRangePreset(value) {
2129
- return CONTACTS_TIME_RANGE_PRESETS.includes(value) ? value : "1y";
2130
- }
2131
- function parseContactPool(value) {
2132
- return value === "friend" || value === "non_friend" ? value : void 0;
2133
- }
2134
- function parsePositiveInteger(value) {
2135
- if (!value) return void 0;
2136
- const parsed = Number(value);
2137
- if (!Number.isFinite(parsed)) return void 0;
2138
- return Math.max(1, Math.trunc(parsed));
1847
+ try {
1848
+ return cm.executeAiSQL(sql);
1849
+ } catch (e) {
1850
+ const msg = e instanceof Error ? e.message : String(e);
1851
+ return reply.code(400).send({ error: msg });
1852
+ }
1853
+ });
1854
+ server2.post("/_web/ai/debug/clear-debug-context", async () => {
1855
+ const cleared = cm.clearAllDebugContext();
1856
+ return { success: true, cleared };
1857
+ });
1858
+ server2.get("/_web/ai/chats/:id/estimate-tokens", async (request) => {
1859
+ const history = cm.getHistoryForAgent(request.params.id);
1860
+ const tokens = countMessagesTokens(history.map((m) => ({ role: m.role, content: m.content })));
1861
+ return { success: true, tokens, messageCount: history.length };
1862
+ });
2139
1863
  }
2140
1864
 
2141
- // ../../packages/http-routes/src/routes/web/export.ts
2142
- function registerExportRoutes(server2, ctx) {
1865
+ // ../../packages/http-routes/src/routes/web/ai-summaries.ts
1866
+ function createSummaryDeps(ctx) {
1867
+ const store = ctx.llmConfigStore;
1868
+ if (!store) return null;
1869
+ return {
1870
+ getLlmConfig() {
1871
+ const config = store.getDefaultAssistantConfig();
1872
+ if (!config) return null;
1873
+ return config;
1874
+ },
1875
+ buildPiModel(config) {
1876
+ return buildPiModel(config);
1877
+ }
1878
+ };
1879
+ }
1880
+ function registerAiSummaryRoutes(server2, ctx) {
1881
+ const deps = createSummaryDeps(ctx);
1882
+ if (!deps) return;
2143
1883
  const { sessionAdapter: adapter } = ctx;
2144
- server2.post("/_web/sessions/:id/export", async (request, reply) => {
2145
- const { id } = request.params;
2146
- const body = request.body;
2147
- const sessionName = body?.sessionName || id;
2148
- const format = body?.format || "txt";
2149
- const result = export_service_exports.exportFormatted(adapter, {
2150
- sessionId: id,
2151
- sessionName,
2152
- format,
2153
- timeFilter: body.timeFilter
1884
+ server2.post("/_web/sessions/:id/summaries/generate", async (request, reply) => {
1885
+ const { segmentId, locale, forceRegenerate, strategy } = request.body;
1886
+ const result = await summary_service_exports.generateSummary(adapter, request.params.id, segmentId, deps, {
1887
+ locale,
1888
+ forceRegenerate,
1889
+ strategy
2154
1890
  });
2155
- if (!result.success) {
2156
- return reply.code(result.totalMessages === 0 ? 404 : 500).send({ error: result.error });
1891
+ if ("error" in result && !result.success) {
1892
+ return reply.code(400).send({ error: result.error });
2157
1893
  }
2158
- reply.header("Content-Type", result.mimeType);
2159
- reply.header("Content-Disposition", `attachment; filename="${encodeURIComponent(result.filename)}"`);
2160
- return reply.send(result.content);
2161
- });
2162
- }
2163
-
2164
- // ../../packages/http-routes/src/routes/web/global-insight.ts
2165
- function registerGlobalInsightRoutes(server2, ctx) {
2166
- const service = ctx.globalInsightService ?? createGlobalInsightService({
2167
- adapter: ctx.sessionAdapter,
2168
- pathProvider: ctx.pathProvider,
2169
- runtimeIdentity: ctx.runtimeIdentity,
2170
- nativeBinding: ctx.nativeBinding
1894
+ return result;
2171
1895
  });
2172
- server2.addHook("onClose", async () => service.close());
2173
- server2.get("/_web/global-insight/annual-summary", async (request) => {
2174
- return service.getAnnualSummary({
2175
- ...parseRange(request.query),
2176
- acceptStale: isTruthy2(request.query.acceptStale)
1896
+ server2.post("/_web/sessions/:id/summaries/generate-all", async (request, reply) => {
1897
+ const { locale, forceRegenerate } = request.body;
1898
+ const result = await summary_service_exports.generateAllSummaries(adapter, request.params.id, deps, {
1899
+ locale,
1900
+ forceRegenerate
2177
1901
  });
1902
+ if (result.error) {
1903
+ return reply.code(400).send({ error: result.error });
1904
+ }
1905
+ return result;
1906
+ });
1907
+ server2.post("/_web/sessions/:id/summaries/check-can-generate", async (request) => {
1908
+ const { segmentIds } = request.body;
1909
+ return summary_service_exports.checkCanGenerate(adapter, request.params.id, segmentIds);
2178
1910
  });
2179
- server2.post(
2180
- "/_web/global-insight/annual-summary/recompute",
2181
- async (request) => service.startRecompute(parseRange(request.query))
2182
- );
2183
1911
  }
2184
- function parseRange(query) {
2185
- const mode = query.mode === "recent" ? "recent" : "year";
2186
- return {
2187
- mode,
2188
- year: parseInteger(query.year),
2189
- days: query.days === "365" ? 365 : void 0
2190
- };
1912
+
1913
+ // ../../packages/http-routes/src/routes/web/ai-logs.ts
1914
+ import fs3 from "fs";
1915
+ import path4 from "path";
1916
+ function findLatestAiLogPath(logsDir) {
1917
+ const aiLogDir = path4.join(logsDir, "ai");
1918
+ if (!fs3.existsSync(aiLogDir)) return null;
1919
+ const latest = fs3.readdirSync(aiLogDir).filter((name) => name.startsWith("ai_") && name.endsWith(".log")).map((name) => {
1920
+ const filePath = path4.join(aiLogDir, name);
1921
+ const stat2 = fs3.statSync(filePath);
1922
+ return { path: filePath, mtimeMs: stat2.mtimeMs };
1923
+ }).sort((a, b) => b.mtimeMs - a.mtimeMs)[0];
1924
+ return latest?.path ?? null;
2191
1925
  }
2192
- function parseInteger(value) {
2193
- if (!value) return void 0;
2194
- const parsed = Number(value);
2195
- return Number.isInteger(parsed) ? parsed : void 0;
1926
+ function resolveAiLogPath(ctx) {
1927
+ const currentPath = ctx.getCurrentAiLogPath?.();
1928
+ if (currentPath && fs3.existsSync(currentPath)) return currentPath;
1929
+ return findLatestAiLogPath(ctx.pathProvider.getLogsDir());
2196
1930
  }
2197
- function isTruthy2(value) {
2198
- return value === "1" || value === "true" || value === "yes";
1931
+ function isLoopbackAddress(address) {
1932
+ if (!address) return true;
1933
+ const normalized = address.replace(/^::ffff:/, "");
1934
+ return normalized === "localhost" || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1" || normalized.startsWith("127.");
2199
1935
  }
2200
-
2201
- // ../../packages/http-routes/src/routes/web/logs.ts
2202
- function registerLogRoutes(server2) {
2203
- server2.post("/_web/logs/report", async (req, reply) => {
2204
- const { level, message, stack, url } = req.body ?? {};
2205
- if (!message) return reply.code(400).send({ error: "message required" });
2206
- const detail = [url ? `url=${url}` : null, stack ? `
2207
- ${stack}` : null].filter(Boolean).join(" ");
2208
- if (level === "warn") appLogger.warn("web", message, detail || void 0);
2209
- else appLogger.error("web", message, detail || void 0);
2210
- return { ok: true };
1936
+ function registerAiLogRoutes(server2, ctx) {
1937
+ server2.post("/_web/ai/logs/show", async (request, reply) => {
1938
+ if (!ctx.showInFolder) return reply.code(501).send({ success: false, error: "Not supported" });
1939
+ if (!isLoopbackAddress(request.ip)) {
1940
+ return { success: false, error: "Opening AI logs is only supported on this device" };
1941
+ }
1942
+ const logPath = resolveAiLogPath(ctx);
1943
+ if (!logPath) return { success: false, error: "No AI log files found" };
1944
+ try {
1945
+ await ctx.showInFolder(logPath);
1946
+ return { success: true, path: logPath };
1947
+ } catch (error) {
1948
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
1949
+ }
2211
1950
  });
2212
1951
  }
2213
1952
 
2214
- // ../../packages/http-routes/src/routes/web/members.ts
2215
- function registerMemberRoutes(server2, ctx) {
2216
- const { sessionAdapter: adapter } = ctx;
2217
- server2.get("/_web/sessions/:id/members", async (request) => {
2218
- return member_service_exports.getMembers(adapter, request.params.id);
2219
- });
2220
- server2.get("/_web/sessions/:id/members/paginated", async (request) => {
2221
- return member_service_exports.getMembersPaginated(adapter, request.params.id, {
2222
- page: parseInt(request.query.page || "1", 10),
2223
- pageSize: parseInt(request.query.pageSize || "20", 10),
2224
- search: request.query.search,
2225
- sortOrder: request.query.sortOrder === "asc" ? "asc" : "desc"
2226
- });
2227
- });
2228
- server2.patch(
2229
- "/_web/sessions/:id/members/:memberId/aliases",
2230
- async (request) => {
2231
- const memberId = parseInt(request.params.memberId, 10);
2232
- member_service_exports.updateMemberAliases(adapter, request.params.id, memberId, request.body.aliases);
2233
- return { success: true };
1953
+ // ../../packages/http-routes/src/routes/web/ai-semantic-index.ts
1954
+ import path5 from "path";
1955
+ function registerSemanticIndexRoutes(server2, ctx) {
1956
+ const service = ctx.semanticIndexService;
1957
+ if (!service) {
1958
+ if (ctx.aiDataDir) {
1959
+ const configStore = new SemanticIndexConfigStore(path5.join(ctx.aiDataDir, SEMANTIC_INDEX_CONFIG_FILE));
1960
+ const configResponse = (config) => ({
1961
+ config,
1962
+ apiKeySet: resolveSemanticIndexApiKeySet(config, ctx.resolveApiKey),
1963
+ configured: isSemanticIndexConfigured(config)
1964
+ });
1965
+ server2.get("/_web/ai/semantic-index/config", async () => ({
1966
+ ...configResponse(configStore.get()),
1967
+ modelStatus: "idle"
1968
+ }));
1969
+ server2.put(
1970
+ "/_web/ai/semantic-index/config",
1971
+ async (request) => {
1972
+ const config = persistSemanticIndexConfig(configStore, request.body.config, {
1973
+ apiKey: request.body.apiKey,
1974
+ writeAuthProfile: ctx.writeAuthProfile
1975
+ });
1976
+ return { ...configResponse(config), modelStatus: "idle" };
1977
+ }
1978
+ );
2234
1979
  }
2235
- );
2236
- server2.delete(
2237
- "/_web/sessions/:id/members/:memberId",
1980
+ server2.get("/_web/ai/semantic-index/enabled", async () => ({ sessions: [] }));
1981
+ server2.get("/_web/ai/semantic-index/status", async () => ({ status: null }));
1982
+ server2.post("/_web/ai/semantic-index/status", async () => ({ sessions: [] }));
1983
+ return;
1984
+ }
1985
+ server2.get("/_web/ai/semantic-index/config", async () => {
1986
+ return {
1987
+ config: await service.getConfig(),
1988
+ apiKeySet: await service.hasApiKey(),
1989
+ configured: await service.isConfigured(),
1990
+ modelStatus: await service.getModelStatus()
1991
+ };
1992
+ });
1993
+ server2.put(
1994
+ "/_web/ai/semantic-index/config",
2238
1995
  async (request) => {
2239
- const memberId = parseInt(request.params.memberId, 10);
2240
- member_service_exports.deleteMember(adapter, request.params.id, memberId);
2241
- return { success: true };
1996
+ const config = await service.setConfig(request.body.config, { apiKey: request.body.apiKey });
1997
+ return {
1998
+ config,
1999
+ apiKeySet: await service.hasApiKey(),
2000
+ configured: await service.isConfigured(),
2001
+ modelStatus: await service.getModelStatus()
2002
+ };
2242
2003
  }
2243
2004
  );
2005
+ server2.get("/_web/ai/semantic-index/enabled", async () => {
2006
+ return { sessions: await service.listEnabledStatuses() };
2007
+ });
2008
+ server2.get("/_web/ai/semantic-index/status", async (request) => {
2009
+ return { status: await service.status(request.query.sessionId) };
2010
+ });
2011
+ server2.post("/_web/ai/semantic-index/status", async (request) => {
2012
+ return { sessions: await service.statusForSessions(request.body.sessionIds ?? []) };
2013
+ });
2014
+ const sessionAction = (path14, action) => {
2015
+ server2.post(path14, async (request, reply) => {
2016
+ const { sessionId } = request.body;
2017
+ if (!sessionId) return reply.code(400).send({ error: "sessionId is required" });
2018
+ await action(sessionId);
2019
+ return { status: await service.status(sessionId) };
2020
+ });
2021
+ };
2022
+ sessionAction("/_web/ai/semantic-index/enable", (id) => service.enable(id));
2023
+ sessionAction("/_web/ai/semantic-index/remove", (id) => service.remove(id));
2024
+ sessionAction("/_web/ai/semantic-index/build", (id) => service.build(id));
2025
+ sessionAction("/_web/ai/semantic-index/pause", (id) => service.pause(id));
2026
+ sessionAction("/_web/ai/semantic-index/cancel", (id) => service.cancel(id));
2027
+ sessionAction("/_web/ai/semantic-index/rebuild", (id) => service.rebuild(id));
2028
+ server2.post("/_web/ai/semantic-index/build-pending", async () => {
2029
+ await service.buildAllPending();
2030
+ return { sessions: await service.listEnabledStatuses() };
2031
+ });
2032
+ server2.post("/_web/ai/semantic-index/cleanup", async () => {
2033
+ return await service.cleanupUnused();
2034
+ });
2244
2035
  server2.post(
2245
- "/_web/sessions/:id/members/batch-delete",
2036
+ "/_web/ai/semantic-index/search",
2246
2037
  async (request, reply) => {
2247
- const memberIds = request.body?.memberIds;
2248
- if (!Array.isArray(memberIds) || memberIds.length === 0 || memberIds.some((memberId) => !Number.isSafeInteger(memberId) || Number(memberId) <= 0)) {
2249
- return reply.code(400).send({
2250
- success: false,
2251
- error: "memberIds must contain at least one positive integer"
2252
- });
2253
- }
2254
- const uniqueMemberIds = Array.from(new Set(memberIds));
2255
- try {
2256
- const success = member_service_exports.deleteMembers(adapter, request.params.id, uniqueMemberIds);
2257
- if (!success) {
2258
- throw new Error("Member deletion transaction returned no result");
2259
- }
2260
- } catch (error) {
2261
- if (apiErrorFromUnknown(error) || isHttpClientError(error)) throw error;
2262
- appLogger.error("members", "Batch member deletion failed", error);
2263
- return reply.code(500).send({ success: false, error: "Failed to delete selected members" });
2264
- }
2265
- appLogger.info("members", "Batch member deletion completed", {
2266
- sessionId: request.params.id,
2267
- memberCount: uniqueMemberIds.length
2268
- });
2269
- return { success: true, deletedCount: uniqueMemberIds.length };
2270
- }
2271
- );
2272
- server2.post(
2273
- "/_web/sessions/:id/members/merge",
2274
- async (request) => {
2275
- const { memberId1, memberId2 } = request.body;
2276
- member_service_exports.mergeMembers(adapter, request.params.id, memberId1, memberId2);
2277
- return { success: true };
2278
- }
2279
- );
2280
- server2.get(
2281
- "/_web/sessions/:id/members/:memberId/history",
2282
- async (request) => {
2283
- const memberId = parseInt(request.params.memberId, 10);
2284
- return member_service_exports.getMemberNameHistory(adapter, request.params.id, memberId);
2038
+ const { sessionId, query, finalTopK } = request.body;
2039
+ if (!sessionId || !query) return reply.code(400).send({ error: "sessionId and query are required" });
2040
+ return service.search(sessionId, query, { finalTopK });
2285
2041
  }
2286
2042
  );
2287
2043
  }
2288
- function isHttpClientError(error) {
2289
- return error instanceof Error && "statusCode" in error && typeof error.statusCode === "number" && error.statusCode >= 400 && error.statusCode < 500;
2290
- }
2291
2044
 
2292
2045
  // ../../packages/http-routes/src/routes/web/merge.ts
2293
2046
  import * as fs4 from "fs";
2047
+ import * as os from "os";
2294
2048
  import * as path6 from "path";
2295
- function ensureDb(ctx, sessionId) {
2049
+ function ensureDb2(ctx, sessionId) {
2296
2050
  const db = ctx.dbManager.open(sessionId);
2297
2051
  if (!db) throw sessionNotFound(sessionId);
2298
2052
  return db;
@@ -2308,7 +2062,7 @@ function registerMergeRoutes(server2, ctx) {
2308
2062
  if (contentType.includes("multipart/form-data")) {
2309
2063
  const data = await request.file();
2310
2064
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
2311
- const tmpDir = createChatLabTempDir("merge", "upload-");
2065
+ const tmpDir = fs4.mkdtempSync(path6.join(os.tmpdir(), "chatlab-merge-"));
2312
2066
  const tmpPath = path6.join(tmpDir, data.filename || "upload");
2313
2067
  const chunks = [];
2314
2068
  for await (const chunk of data.file) {
@@ -2371,405 +2125,352 @@ function registerMergeRoutes(server2, ctx) {
2371
2125
  if (!entry) return reply.code(404).send({ error: `Handle not found: ${handle}` });
2372
2126
  readers.push(entry.reader);
2373
2127
  dataSources.push({ source: entry.reader.toDataSource(), filename: entry.filename });
2374
- }
2375
- return checkConflictsFromSources(dataSources);
2376
- } finally {
2377
- for (const r of readers) r.close();
2378
- }
2379
- });
2380
- server2.post("/_web/merge/execute", async (request, reply) => {
2381
- const { handles, outputName, format = "json", andImport } = request.body;
2382
- if (!handles || !Array.isArray(handles) || handles.length === 0) {
2383
- return reply.code(400).send({ error: "Missing handles" });
2384
- }
2385
- if (!outputName) {
2386
- return reply.code(400).send({ error: "Missing outputName" });
2387
- }
2388
- const readers = [];
2389
- try {
2390
- const dataSources = [];
2391
- for (const handle of handles) {
2392
- const entry = mergeCache.openReader(handle);
2393
- if (!entry) return reply.code(404).send({ error: `Handle not found: ${handle}` });
2394
- readers.push(entry.reader);
2395
- dataSources.push({ source: entry.reader.toDataSource(), filename: entry.filename });
2396
- }
2397
- const merged = buildMergedOutput(dataSources, outputName);
2398
- let sessionId;
2399
- if (andImport) {
2400
- if (!streamImport2) {
2401
- return reply.code(501).send({ error: "Import not available on this server" });
2402
- }
2403
- const jsonData = JSON.stringify(merged.chatLabData);
2404
- const tmpDir = createChatLabTempDir("merge", "import-");
2405
- const safeName = path6.basename(outputName).replace(/[/\\?%*:|"<>]/g, "_") || "merged";
2406
- const tmpPath = path6.join(tmpDir, `${safeName}.json`);
2407
- fs4.writeFileSync(tmpPath, jsonData, "utf-8");
2408
- try {
2409
- const importResult = await streamImport2(dbManager2, tmpPath);
2410
- sessionId = importResult.sessionId;
2411
- } finally {
2412
- try {
2413
- fs4.unlinkSync(tmpPath);
2414
- } catch {
2415
- }
2416
- try {
2417
- fs4.rmdirSync(tmpDir);
2418
- } catch {
2419
- }
2420
- }
2421
- }
2422
- for (const handle of handles) {
2423
- mergeCache.delete(handle);
2424
- }
2425
- if (format === "jsonl") {
2426
- const lines = [];
2427
- for (const line of serializeChatLabToJsonl(merged.chatLabData)) {
2428
- lines.push(line);
2429
- }
2430
- return { success: true, sessionId, data: lines.join("\n") };
2431
- }
2432
- return { success: true, sessionId, data: merged.chatLabData };
2433
- } finally {
2434
- for (const r of readers) r.close();
2435
- }
2436
- });
2437
- server2.post("/_web/merge/clear", async (request) => {
2438
- const { handle } = request.body || {};
2439
- if (handle) {
2440
- mergeCache.delete(handle);
2441
- } else {
2442
- mergeCache.clear();
2443
- }
2444
- return { success: true };
2445
- });
2446
- server2.post("/_web/sessions/export-for-merge", async (request, reply) => {
2447
- const { sessionIds } = request.body;
2448
- if (!sessionIds || !Array.isArray(sessionIds) || sessionIds.length === 0) {
2449
- return reply.code(400).send({ error: "Missing sessionIds" });
2450
- }
2451
- const handles = [];
2452
- for (const sid of sessionIds) {
2453
- const db = ensureDb(ctx, sid);
2454
- const exported = exportSessionToJson(db);
2455
- const { db: tempDb, tempDbPath } = mergeCache.createTempDatabase(exported.meta.name);
2456
- const writer = new TempDbWriter(tempDb);
2457
- writer.writeMeta({
2458
- name: exported.meta.name,
2459
- platform: exported.meta.platform,
2460
- type: exported.meta.type,
2461
- groupId: exported.meta.groupId,
2462
- groupAvatar: exported.meta.groupAvatar
2463
- });
2464
- writer.writeMembers(
2465
- exported.members.map((m) => ({
2466
- platformId: m.platformId,
2467
- accountName: m.accountName,
2468
- groupNickname: m.groupNickname,
2469
- avatar: m.avatar
2470
- }))
2471
- );
2472
- writer.writeMessages(
2473
- exported.messages.map((msg) => ({
2474
- senderPlatformId: msg.sender,
2475
- senderAccountName: msg.accountName,
2476
- senderGroupNickname: msg.groupNickname,
2477
- timestamp: msg.timestamp,
2478
- type: msg.type,
2479
- content: msg.content
2480
- }))
2481
- );
2482
- writer.finish();
2483
- const handle = mergeCache.store(exported.meta.name, tempDbPath);
2484
- handles.push({ sessionId: sid, handle });
2485
- }
2486
- return { success: true, handles };
2487
- });
2488
- }
2489
-
2490
- // ../../packages/http-routes/src/routes/web/nlp.ts
2491
- import * as fs5 from "fs";
2492
- import * as path7 from "path";
2493
- function registerNlpRoutes(server2, ctx) {
2494
- const nlpDir = path7.join(ctx.pathProvider.getSystemDir(), "nlp");
2495
- initNlpDir(nlpDir);
2496
- ensureDefaultDict(nlpDir).catch((err) => console.warn("[NLP] Auto-download zh-CN dict failed:", err));
2497
- server2.get("/_web/nlp/pos-tags", async () => {
2498
- return POS_TAG_DEFINITIONS;
2499
- });
2500
- server2.get("/_web/nlp/dicts", async () => {
2501
- return getDictList(nlpDir);
2502
- });
2503
- server2.get("/_web/nlp/dicts/:id/status", async (request) => {
2504
- return isDictDownloaded(nlpDir, request.params.id);
2505
- });
2506
- server2.post("/_web/nlp/dicts/:id/download", async (request) => {
2507
- return downloadDict(nlpDir, request.params.id);
2508
- });
2509
- server2.delete("/_web/nlp/dicts/:id", async (request) => {
2510
- return deleteDict(nlpDir, request.params.id);
2511
- });
2512
- server2.post("/_web/nlp/word-frequency", async (request) => {
2513
- const params = request.body;
2514
- const db = ctx.dbManager.open(params.sessionId);
2515
- if (!db) {
2516
- throw Object.assign(new Error(`Session not found: ${params.sessionId}`), { statusCode: 404 });
2517
- }
2518
- const { sessionId, ...keyParams } = params;
2519
- const effectiveDictId = !params.dictType || params.dictType === "default" ? "zh-CN" : String(params.dictType);
2520
- const dictFilePath = path7.join(nlpDir, `${effectiveDictId}.dict`);
2521
- let dictVersion;
2522
- try {
2523
- const st = fs5.statSync(dictFilePath);
2524
- dictVersion = `${Math.floor(st.mtimeMs)}:${st.size}`;
2525
- } catch {
2526
- dictVersion = "-";
2527
- }
2528
- return withAnalyticsCache(ctx, sessionId, "nlp.word-frequency", keyParams, () => computeWordFrequency(db, params), {
2529
- extraVersion: `dict:${dictVersion}`
2530
- });
2531
- });
2532
- server2.post(
2533
- "/_web/nlp/segment",
2534
- async (request) => {
2535
- const { text, locale, minLength } = request.body;
2536
- return segmentText(text, locale, minLength);
2537
- }
2538
- );
2539
- }
2540
-
2541
- // ../../packages/http-routes/src/routes/web/people-relationships.ts
2542
- function registerPeopleRelationshipsRoutes(server2, ctx) {
2543
- const service = ctx.peopleRelationshipsService ?? createPeopleRelationshipsService({
2544
- adapter: ctx.sessionAdapter,
2545
- pathProvider: ctx.pathProvider,
2546
- runtimeIdentity: ctx.runtimeIdentity,
2547
- nativeBinding: ctx.nativeBinding
2548
- });
2549
- server2.addHook("onClose", async () => {
2550
- await service.close();
2551
- });
2552
- server2.get("/_web/people/relationships", async (request) => {
2553
- return service.getGraph({
2554
- acceptStale: isTruthy3(request.query.acceptStale),
2555
- timeRangePreset: parseContactsTimeRangePreset2(request.query.timeRange),
2556
- graphScope: parsePeopleRelationshipsGraphScope(request.query.scope),
2557
- query: request.query.q
2558
- });
2559
- });
2560
- server2.post("/_web/people/relationships/recompute", async (request) => {
2561
- return service.startRecompute({
2562
- timeRangePreset: parseContactsTimeRangePreset2(request.query.timeRange),
2563
- graphScope: parsePeopleRelationshipsGraphScope(request.query.scope),
2564
- query: request.query.q
2565
- });
2566
- });
2567
- server2.get(
2568
- "/_web/people/relationships/:key/neighborhood",
2569
- async (request) => {
2570
- return service.getNeighborhood(request.params.key, {
2571
- acceptStale: isTruthy3(request.query.acceptStale),
2572
- timeRangePreset: parseContactsTimeRangePreset2(request.query.timeRange)
2573
- });
2574
- }
2575
- );
2576
- }
2577
- function isTruthy3(value) {
2578
- return value === "1" || value === "true" || value === "yes";
2579
- }
2580
- function parseContactsTimeRangePreset2(value) {
2581
- return CONTACTS_TIME_RANGE_PRESETS.includes(value) ? value : "1y";
2582
- }
2583
- function parsePeopleRelationshipsGraphScope(value) {
2584
- return value === "close" || value === "friends" ? value : "panorama";
2585
- }
2586
-
2587
- // ../../packages/http-routes/src/routes/web/preferences.ts
2588
- function registerPreferencesRoutes(server2, ctx) {
2589
- const prefManager = ctx.preferencesManager ?? new PreferencesManager(ctx.pathProvider.getSystemDir());
2590
- server2.get("/_web/preferences", async () => {
2591
- return prefManager.load();
2592
- });
2593
- server2.patch("/_web/preferences", async (request) => {
2594
- return prefManager.save(request.body);
2595
- });
2596
- server2.get("/_web/preferences/ui-config", async () => {
2597
- const config = loadConfig();
2598
- return config.ui;
2128
+ }
2129
+ return checkConflictsFromSources(dataSources);
2130
+ } finally {
2131
+ for (const r of readers) r.close();
2132
+ }
2599
2133
  });
2600
- server2.patch("/_web/preferences/ui-config", async (request) => {
2134
+ server2.post("/_web/merge/execute", async (request, reply) => {
2135
+ const { handles, outputName, format = "json", andImport } = request.body;
2136
+ if (!handles || !Array.isArray(handles) || handles.length === 0) {
2137
+ return reply.code(400).send({ error: "Missing handles" });
2138
+ }
2139
+ if (!outputName) {
2140
+ return reply.code(400).send({ error: "Missing outputName" });
2141
+ }
2142
+ const readers = [];
2601
2143
  try {
2602
- for (const [key, value] of Object.entries(request.body)) {
2603
- if (value !== void 0) {
2604
- writeConfigField("ui", key, value);
2144
+ const dataSources = [];
2145
+ for (const handle of handles) {
2146
+ const entry = mergeCache.openReader(handle);
2147
+ if (!entry) return reply.code(404).send({ error: `Handle not found: ${handle}` });
2148
+ readers.push(entry.reader);
2149
+ dataSources.push({ source: entry.reader.toDataSource(), filename: entry.filename });
2150
+ }
2151
+ const merged = buildMergedOutput(dataSources, outputName);
2152
+ let sessionId;
2153
+ if (andImport) {
2154
+ if (!streamImport2) {
2155
+ return reply.code(501).send({ error: "Import not available on this server" });
2156
+ }
2157
+ const jsonData = JSON.stringify(merged.chatLabData);
2158
+ const tmpDir = fs4.mkdtempSync(path6.join(os.tmpdir(), "chatlab-merged-"));
2159
+ const safeName = path6.basename(outputName).replace(/[/\\?%*:|"<>]/g, "_") || "merged";
2160
+ const tmpPath = path6.join(tmpDir, `${safeName}.json`);
2161
+ fs4.writeFileSync(tmpPath, jsonData, "utf-8");
2162
+ try {
2163
+ const importResult = await streamImport2(dbManager2, tmpPath);
2164
+ sessionId = importResult.sessionId;
2165
+ } finally {
2166
+ try {
2167
+ fs4.unlinkSync(tmpPath);
2168
+ } catch {
2169
+ }
2170
+ try {
2171
+ fs4.rmdirSync(tmpDir);
2172
+ } catch {
2173
+ }
2605
2174
  }
2606
2175
  }
2607
- return { success: true };
2608
- } catch (err) {
2609
- return { success: false, error: err instanceof Error ? err.message : String(err) };
2176
+ for (const handle of handles) {
2177
+ mergeCache.delete(handle);
2178
+ }
2179
+ if (format === "jsonl") {
2180
+ const lines = [];
2181
+ for (const line of serializeChatLabToJsonl(merged.chatLabData)) {
2182
+ lines.push(line);
2183
+ }
2184
+ return { success: true, sessionId, data: lines.join("\n") };
2185
+ }
2186
+ return { success: true, sessionId, data: merged.chatLabData };
2187
+ } finally {
2188
+ for (const r of readers) r.close();
2610
2189
  }
2611
2190
  });
2612
- server2.get("/_web/preferences/locale", async () => {
2613
- const config = loadConfig();
2614
- return { lang: config.locale.lang };
2615
- });
2616
- server2.patch("/_web/preferences/locale", async (request) => {
2617
- try {
2618
- writeConfigField("locale", "lang", request.body.lang);
2619
- return { success: true };
2620
- } catch (err) {
2621
- return { success: false, error: err instanceof Error ? err.message : String(err) };
2191
+ server2.post("/_web/merge/clear", async (request) => {
2192
+ const { handle } = request.body || {};
2193
+ if (handle) {
2194
+ mergeCache.delete(handle);
2195
+ } else {
2196
+ mergeCache.clear();
2622
2197
  }
2623
- });
2624
- }
2625
-
2626
- // ../../packages/http-routes/src/routes/web/session-index.ts
2627
- function registerSessionIndexRoutes(server2, ctx) {
2628
- const { sessionAdapter: adapter } = ctx;
2629
- server2.get("/_web/sessions/index-stats", async () => {
2630
- return session_index_service_exports.getAllIndexStats(adapter);
2631
- });
2632
- server2.post("/_web/sessions/:id/generate-index", async (request) => {
2633
- const gapThreshold = request.body?.gapThreshold ?? 1800;
2634
- const sessionCount = session_index_service_exports.generateIndex(adapter, request.params.id, gapThreshold);
2635
- return { sessionCount };
2636
- });
2637
- server2.post("/_web/sessions/:id/generate-incremental-index", async (request) => {
2638
- const gapThreshold = request.body?.gapThreshold ?? 1800;
2639
- const sessionCount = session_index_service_exports.generateIncrementalIndex(adapter, request.params.id, gapThreshold);
2640
- return { sessionCount };
2641
- });
2642
- server2.post("/_web/sessions/:id/clear-index", async (request) => {
2643
- session_index_service_exports.clearIndex(adapter, request.params.id);
2644
2198
  return { success: true };
2645
2199
  });
2646
- server2.get("/_web/sessions/:id/search/fts", async (request, reply) => {
2647
- if (!session_index_service_exports.getFtsStatus(adapter, request.params.id)) {
2648
- return reply.code(400).send({ error: "FTS index not built for this session" });
2200
+ server2.post("/_web/sessions/export-for-merge", async (request, reply) => {
2201
+ const { sessionIds } = request.body;
2202
+ if (!sessionIds || !Array.isArray(sessionIds) || sessionIds.length === 0) {
2203
+ return reply.code(400).send({ error: "Missing sessionIds" });
2649
2204
  }
2650
- const keywords = request.query.keywords.split(/\s+/).filter(Boolean);
2651
- if (keywords.length === 0) return { rowids: [], total: 0 };
2652
- const limit = parseInt(request.query.limit || "100", 10);
2653
- const offset = parseInt(request.query.offset || "0", 10);
2654
- return session_index_service_exports.searchFts(adapter, request.params.id, keywords, limit, offset);
2655
- });
2656
- server2.get("/_web/sessions/:id/fts/status", async (request) => {
2657
- return { hasFtsIndex: session_index_service_exports.getFtsStatus(adapter, request.params.id) };
2658
- });
2659
- server2.post("/_web/sessions/:id/fts/rebuild", async (request) => {
2660
- const result = session_index_service_exports.rebuildFts(adapter, request.params.id);
2661
- return { success: true, indexed: result.indexed };
2205
+ const handles = [];
2206
+ for (const sid of sessionIds) {
2207
+ const db = ensureDb2(ctx, sid);
2208
+ const exported = exportSessionToJson(db);
2209
+ const { db: tempDb, tempDbPath } = mergeCache.createTempDatabase(exported.meta.name);
2210
+ const writer = new TempDbWriter(tempDb);
2211
+ writer.writeMeta({
2212
+ name: exported.meta.name,
2213
+ platform: exported.meta.platform,
2214
+ type: exported.meta.type,
2215
+ groupId: exported.meta.groupId,
2216
+ groupAvatar: exported.meta.groupAvatar
2217
+ });
2218
+ writer.writeMembers(
2219
+ exported.members.map((m) => ({
2220
+ platformId: m.platformId,
2221
+ accountName: m.accountName,
2222
+ groupNickname: m.groupNickname,
2223
+ avatar: m.avatar
2224
+ }))
2225
+ );
2226
+ writer.writeMessages(
2227
+ exported.messages.map((msg) => ({
2228
+ senderPlatformId: msg.sender,
2229
+ senderAccountName: msg.accountName,
2230
+ senderGroupNickname: msg.groupNickname,
2231
+ timestamp: msg.timestamp,
2232
+ type: msg.type,
2233
+ content: msg.content
2234
+ }))
2235
+ );
2236
+ writer.finish();
2237
+ const handle = mergeCache.store(exported.meta.name, tempDbPath);
2238
+ handles.push({ sessionId: sid, handle });
2239
+ }
2240
+ return { success: true, handles };
2662
2241
  });
2663
2242
  }
2664
2243
 
2665
- // ../../packages/http-routes/src/routes/web/sessions.ts
2666
- function registerSessionRoutes(server2, ctx) {
2667
- const { sessionAdapter: adapter } = ctx;
2668
- let preferencesInstance = null;
2669
- const preferences = () => {
2670
- preferencesInstance ??= ctx.preferencesManager ?? new PreferencesManager(ctx.pathProvider.getSystemDir());
2671
- return preferencesInstance;
2672
- };
2673
- server2.get("/_web/sessions", async () => {
2674
- const aiChatCounts = ctx.aiChatManager?.getAIChatCountsBySession();
2675
- return session_service_exports.listAnalysisSessions(adapter, {
2676
- enrichSession: aiChatCounts?.size ? (dto) => ({ ...dto, aiConversationCount: aiChatCounts.get(dto.id) ?? 0 }) : void 0
2677
- });
2678
- });
2679
- server2.get("/_web/sessions/:id", async (request) => {
2680
- const session = session_service_exports.getAnalysisSession(adapter, request.params.id);
2681
- if (!session) {
2682
- throw Object.assign(new Error(`Session not found: ${request.params.id}`), { statusCode: 404 });
2244
+ // ../../packages/http-routes/src/routes/web/cache.ts
2245
+ import * as fs5 from "fs";
2246
+ import * as fsp from "fs/promises";
2247
+ import * as path7 from "path";
2248
+ async function getDirSize(dirPath) {
2249
+ let totalSize = 0;
2250
+ try {
2251
+ if (!fs5.existsSync(dirPath)) return 0;
2252
+ const files = await fsp.readdir(dirPath, { withFileTypes: true });
2253
+ for (const file of files) {
2254
+ const filePath = path7.join(dirPath, file.name);
2255
+ if (file.isDirectory()) {
2256
+ totalSize += await getDirSize(filePath);
2257
+ } else {
2258
+ const stat2 = await fsp.stat(filePath);
2259
+ totalSize += stat2.size;
2260
+ }
2683
2261
  }
2684
- if (ctx.aiChatManager) {
2685
- session.aiConversationCount = ctx.aiChatManager.getAIChats(request.params.id).length;
2262
+ } catch {
2263
+ }
2264
+ return totalSize;
2265
+ }
2266
+ async function getFileCount(dirPath) {
2267
+ let count = 0;
2268
+ try {
2269
+ if (!fs5.existsSync(dirPath)) return 0;
2270
+ const files = await fsp.readdir(dirPath, { withFileTypes: true });
2271
+ for (const file of files) {
2272
+ const filePath = path7.join(dirPath, file.name);
2273
+ if (file.isDirectory()) {
2274
+ count += await getFileCount(filePath);
2275
+ } else {
2276
+ count++;
2277
+ }
2686
2278
  }
2687
- return session;
2279
+ } catch {
2280
+ }
2281
+ return count;
2282
+ }
2283
+ function hasChatLabDatabases(dirPath) {
2284
+ try {
2285
+ const markerPath = path7.join(dirPath, ".chatlab");
2286
+ const dbDir = path7.join(dirPath, "databases");
2287
+ if (!fs5.existsSync(markerPath) || !fs5.existsSync(dbDir)) return false;
2288
+ return fs5.readdirSync(dbDir).some((file) => file.endsWith(".db"));
2289
+ } catch {
2290
+ return false;
2291
+ }
2292
+ }
2293
+ function registerCacheRoutes(server2, ctx) {
2294
+ const pp = ctx.pathProvider;
2295
+ const downloadsDir = ctx.downloadsDir ?? pp.getDownloadsDir();
2296
+ server2.get("/_web/cache/info", async () => {
2297
+ const directories = [
2298
+ {
2299
+ id: "databases",
2300
+ name: "settings.storage.cache.databases.name",
2301
+ description: "settings.storage.cache.databases.description",
2302
+ path: pp.getDatabaseDir(),
2303
+ scope: "user-data",
2304
+ rootPath: pp.getUserDataDir(),
2305
+ icon: "i-heroicons-circle-stack",
2306
+ canClear: false
2307
+ },
2308
+ {
2309
+ id: "ai",
2310
+ name: "settings.storage.cache.ai.name",
2311
+ description: "settings.storage.cache.ai.description",
2312
+ path: pp.getAiDataDir(),
2313
+ scope: "system-data",
2314
+ rootPath: pp.getSystemDir(),
2315
+ icon: "i-heroicons-sparkles",
2316
+ canClear: false
2317
+ },
2318
+ {
2319
+ id: "cache",
2320
+ name: "settings.storage.cache.statsCache.name",
2321
+ description: "settings.storage.cache.statsCache.description",
2322
+ path: pp.getCacheDir(),
2323
+ scope: "system-data",
2324
+ rootPath: pp.getSystemDir(),
2325
+ icon: "i-heroicons-bolt",
2326
+ canClear: true
2327
+ },
2328
+ {
2329
+ id: "logs",
2330
+ name: "settings.storage.cache.logs.name",
2331
+ description: "settings.storage.cache.logs.description",
2332
+ path: pp.getLogsDir(),
2333
+ scope: "system-data",
2334
+ rootPath: pp.getSystemDir(),
2335
+ icon: "i-heroicons-document-text",
2336
+ canClear: true
2337
+ }
2338
+ ];
2339
+ const results = await Promise.all(
2340
+ directories.map(async (dir) => {
2341
+ const [size, fileCount] = await Promise.all([getDirSize(dir.path), getFileCount(dir.path)]);
2342
+ return { ...dir, size, fileCount, exists: fs5.existsSync(dir.path) };
2343
+ })
2344
+ );
2345
+ return {
2346
+ baseDir: pp.getSystemDir(),
2347
+ directories: results,
2348
+ totalSize: results.reduce((sum, d) => sum + d.size, 0)
2349
+ };
2688
2350
  });
2689
- server2.delete("/_web/sessions/:id", async (request, reply) => {
2690
- const { id } = request.params;
2691
- try {
2692
- const deleted = session_service_exports.deleteSession(adapter, id);
2693
- if (!deleted) {
2694
- return reply.code(404).send({ success: false, error: "File not found" });
2351
+ server2.post("/_web/cache/clear", async (request) => {
2352
+ const { cacheId } = request.body;
2353
+ const allowedDirs = {
2354
+ cache: pp.getCacheDir(),
2355
+ logs: pp.getLogsDir()
2356
+ };
2357
+ const dirPath = allowedDirs[cacheId];
2358
+ if (!dirPath) return { success: false, error: "Not allowed to clear this directory" };
2359
+ if (!fs5.existsSync(dirPath)) return { success: true };
2360
+ const files = await fsp.readdir(dirPath);
2361
+ for (const file of files) {
2362
+ const filePath = path7.join(dirPath, file);
2363
+ const stat2 = await fsp.stat(filePath);
2364
+ if (stat2.isDirectory()) {
2365
+ await fsp.rm(filePath, { recursive: true });
2366
+ } else {
2367
+ await fsp.unlink(filePath);
2695
2368
  }
2696
- return { success: true };
2697
- } catch (err) {
2698
- return reply.code(500).send({ success: false, error: String(err) });
2699
2369
  }
2700
- });
2701
- server2.patch("/_web/sessions/:id/name", async (request) => {
2702
- session_service_exports.renameSession(adapter, request.params.id, request.body.name);
2703
2370
  return { success: true };
2704
2371
  });
2705
- server2.patch(
2706
- "/_web/sessions/:id/owner",
2707
- async (request) => {
2708
- session_service_exports.updateSessionOwnerId(adapter, request.params.id, request.body.ownerId ?? null);
2709
- return { success: true };
2710
- }
2711
- );
2712
- server2.post("/_web/sessions/:id/owner/apply-profile", async (request) => {
2713
- return owner_profile_service_exports.tryApplyOwnerProfile(adapter, preferences(), request.params.id);
2372
+ server2.get("/_web/cache/data-dir", async () => {
2373
+ const pending = ctx.getPendingDataDirMigration?.();
2374
+ return {
2375
+ path: pp.getUserDataDir(),
2376
+ defaultPath: ctx.defaultUserDataDir,
2377
+ isCustom: ctx.isCustomDataDir ?? false,
2378
+ canSetDataDir: ctx.canSetDataDir ?? Boolean(ctx.setDataDir),
2379
+ managedScope: "chat-databases",
2380
+ managedDescription: "settings.storage.dataLocation.managedDescription",
2381
+ hasLegacyDataAtDefaultDir: Boolean(ctx.defaultUserDataDir) && path7.resolve(pp.getUserDataDir()) !== path7.resolve(ctx.defaultUserDataDir ?? "") && hasChatLabDatabases(ctx.defaultUserDataDir ?? ""),
2382
+ pendingMigration: pending ? {
2383
+ from: pending.from,
2384
+ to: pending.to,
2385
+ createdAt: pending.createdAt
2386
+ } : void 0
2387
+ };
2714
2388
  });
2715
- server2.post(
2716
- "/_web/sessions/:id/owner/select",
2717
- async (request) => {
2718
- return owner_profile_service_exports.setOwnerAndApplyProfile(
2719
- adapter,
2720
- preferences(),
2721
- request.params.id,
2722
- request.body.ownerPlatformId
2723
- );
2389
+ server2.post("/_web/cache/data-dir", async (request, reply) => {
2390
+ if (!ctx.setDataDir) {
2391
+ return reply.code(501).send({ success: false, error: "Data directory changes are not supported" });
2724
2392
  }
2725
- );
2726
- server2.post("/_web/sessions/:id/owner/dismiss-prompt", async (request) => {
2727
- owner_profile_service_exports.dismissOwnerPrompt(preferences(), request.params.id);
2728
- return { success: true };
2393
+ const targetPath = typeof request.body?.path === "string" ? request.body.path : null;
2394
+ const migrate = request.body?.migrate !== false;
2395
+ return ctx.setDataDir(targetPath, migrate);
2729
2396
  });
2730
- }
2731
-
2732
- // ../../packages/http-routes/src/routes/web/sql.ts
2733
- function registerSqlRoutes(server2, ctx) {
2734
- const { sessionAdapter: adapter } = ctx;
2735
- server2.post("/_web/sessions/:id/sql", async (request, reply) => {
2736
- const db = adapter.ensureReadonly(request.params.id);
2737
- const { sql } = request.body || {};
2738
- if (!sql || typeof sql !== "string") {
2739
- return reply.code(400).send({ error: "Missing sql parameter" });
2397
+ server2.get("/_web/cache/latest-import-log", async () => {
2398
+ const importLogDir = path7.join(pp.getLogsDir(), "import");
2399
+ if (!fs5.existsSync(importLogDir)) {
2400
+ return { success: false, error: "Log directory not found" };
2740
2401
  }
2741
- try {
2742
- const result = executeSql(db, sql, { columnar: true, timing: true, maxRows: 0 });
2743
- return {
2744
- columns: result.columns,
2745
- rows: result.rows,
2746
- rowCount: result.rowCount,
2747
- duration: result.duration ?? 0,
2748
- limited: result.truncated
2749
- };
2750
- } catch (err) {
2751
- const message = err instanceof Error ? err.message : "SQL execution error";
2752
- return reply.code(400).send({ error: message });
2402
+ const files = await fsp.readdir(importLogDir);
2403
+ const logFiles = files.filter((f) => f.startsWith("import_") && f.endsWith(".log"));
2404
+ if (logFiles.length === 0) {
2405
+ return { success: false, error: "No import logs found" };
2753
2406
  }
2407
+ const fileStats = await Promise.all(
2408
+ logFiles.map(async (f) => {
2409
+ const filePath = path7.join(importLogDir, f);
2410
+ const stat2 = await fsp.stat(filePath);
2411
+ return { name: f, path: filePath, mtime: stat2.mtime.getTime() };
2412
+ })
2413
+ );
2414
+ fileStats.sort((a, b) => b.mtime - a.mtime);
2415
+ return { success: true, path: fileStats[0].path, name: fileStats[0].name };
2754
2416
  });
2755
- server2.get("/_web/sessions/:id/schema", async (request) => {
2756
- const db = adapter.ensureReadonly(request.params.id);
2757
- return getSchemaDetailed(db);
2417
+ server2.post("/_web/cache/save-to-downloads", async (request) => {
2418
+ const { filename, dataUrl } = request.body;
2419
+ if (!filename || !dataUrl) {
2420
+ return { success: false, error: "filename and dataUrl are required" };
2421
+ }
2422
+ let buffer;
2423
+ if (dataUrl.includes(";base64,")) {
2424
+ const base64Data = dataUrl.split(";base64,")[1];
2425
+ buffer = Buffer.from(base64Data, "base64");
2426
+ } else if (dataUrl.includes("charset=utf-8,")) {
2427
+ const textData = dataUrl.split("charset=utf-8,")[1];
2428
+ buffer = Buffer.from(decodeURIComponent(textData), "utf-8");
2429
+ } else {
2430
+ const base64Data = dataUrl.replace(/^data:[^,]+,/, "");
2431
+ buffer = Buffer.from(base64Data, "base64");
2432
+ }
2433
+ if (!fs5.existsSync(downloadsDir)) {
2434
+ fs5.mkdirSync(downloadsDir, { recursive: true });
2435
+ }
2436
+ const filePath = path7.join(downloadsDir, filename);
2437
+ fs5.writeFileSync(filePath, buffer);
2438
+ return { success: true, filePath };
2758
2439
  });
2759
- server2.post("/_web/sessions/:id/query", async (request) => {
2760
- const db = adapter.ensureReadonly(request.params.id);
2761
- const { sql, params = [] } = request.body;
2762
- if (!sql || typeof sql !== "string") {
2763
- throw Object.assign(new Error('Missing or invalid "sql" field'), { statusCode: 400 });
2440
+ server2.post("/_web/cache/open-dir", async (request, reply) => {
2441
+ if (!ctx.openDirectory) return reply.code(501).send({ success: false, error: "Not supported" });
2442
+ const { cacheId } = request.body;
2443
+ const dirPaths = {
2444
+ base: pp.getSystemDir(),
2445
+ userData: pp.getUserDataDir(),
2446
+ databases: pp.getDatabaseDir(),
2447
+ cache: pp.getCacheDir(),
2448
+ ai: pp.getAiDataDir(),
2449
+ logs: pp.getLogsDir(),
2450
+ downloads: downloadsDir
2451
+ };
2452
+ const dirPath = dirPaths[cacheId];
2453
+ if (!dirPath) return { success: false, error: "Unknown directory" };
2454
+ if (!fs5.existsSync(dirPath)) {
2455
+ await fsp.mkdir(dirPath, { recursive: true });
2764
2456
  }
2765
- const stmt = db.prepare(sql.trim());
2766
- if (!stmt.readonly) {
2767
- throw Object.assign(new Error("Only READ-ONLY statements are allowed"), { statusCode: 403 });
2457
+ try {
2458
+ await ctx.openDirectory(dirPath);
2459
+ } catch (error) {
2460
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
2768
2461
  }
2769
- if (Array.isArray(params)) {
2770
- return stmt.all(...params);
2462
+ return { success: true };
2463
+ });
2464
+ server2.post("/_web/cache/show-in-folder", async (request, reply) => {
2465
+ if (!ctx.showInFolder) return reply.code(501).send({ success: false, error: "Not supported" });
2466
+ const { filePath } = request.body;
2467
+ if (!filePath) return { success: false, error: "filePath is required" };
2468
+ try {
2469
+ await ctx.showInFolder(filePath);
2470
+ } catch (error) {
2471
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
2771
2472
  }
2772
- return stmt.all(params);
2473
+ return { success: true };
2773
2474
  });
2774
2475
  }
2775
2476
 
@@ -2799,9 +2500,25 @@ function registerTelemetryRoutes(server2, ctx) {
2799
2500
  );
2800
2501
  }
2801
2502
 
2802
- // ../../packages/http-routes/src/routes/web/register.ts
2803
- function registerWebRoutes(server2, ctx, options) {
2503
+ // ../../packages/http-routes/src/routes/web/logs.ts
2504
+ function registerLogRoutes(server2, _ctx) {
2505
+ server2.post("/_web/logs/report", async (req, reply) => {
2506
+ const { level, message, stack, url } = req.body ?? {};
2507
+ if (!message) return reply.code(400).send({ error: "message required" });
2508
+ const detail = [url ? `url=${url}` : null, stack ? `
2509
+ ${stack}` : null].filter(Boolean).join(" ");
2510
+ if (level === "warn") appLogger.warn("web", message, detail || void 0);
2511
+ else appLogger.error("web", message, detail || void 0);
2512
+ return { ok: true };
2513
+ });
2514
+ }
2515
+
2516
+ // ../../packages/http-routes/src/register.ts
2517
+ function registerSharedRoutes(server2, ctx, options) {
2804
2518
  const resolvedCtx = ctx.preferencesManager ? ctx : { ...ctx, preferencesManager: new PreferencesManager(ctx.pathProvider.getSystemDir()) };
2519
+ registerSystemRoutes(server2, resolvedCtx);
2520
+ registerRestSessionRoutes(server2, resolvedCtx);
2521
+ registerImportRoutes(server2, resolvedCtx);
2805
2522
  registerSessionRoutes(server2, resolvedCtx);
2806
2523
  registerMemberRoutes(server2, resolvedCtx);
2807
2524
  registerContactsRoutes(server2, resolvedCtx);
@@ -2813,17 +2530,77 @@ function registerWebRoutes(server2, ctx, options) {
2813
2530
  registerSessionIndexRoutes(server2, resolvedCtx);
2814
2531
  registerExportRoutes(server2, resolvedCtx);
2815
2532
  registerNlpRoutes(server2, resolvedCtx);
2816
- registerAiRoutes(server2, resolvedCtx, options);
2533
+ if (options?.requireAi) {
2534
+ const missing = [];
2535
+ if (!resolvedCtx.aiDataDir) missing.push("aiDataDir");
2536
+ if (!resolvedCtx.aiChatManager) missing.push("aiChatManager");
2537
+ if (!resolvedCtx.assistantManager) missing.push("assistantManager");
2538
+ if (!resolvedCtx.skillManagerCore) missing.push("skillManagerCore");
2539
+ if (!resolvedCtx.llmConfigStore) missing.push("llmConfigStore");
2540
+ if (!resolvedCtx.customProviderStore) missing.push("customProviderStore");
2541
+ if (!resolvedCtx.customModelStore) missing.push("customModelStore");
2542
+ if (!resolvedCtx.runAgentStream) missing.push("runAgentStream");
2543
+ if (missing.length > 0) {
2544
+ throw new Error(`[http-routes] requireAi is set but missing AI dependencies: ${missing.join(", ")}`);
2545
+ }
2546
+ }
2547
+ registerAiAssistantRoutes(server2, resolvedCtx);
2548
+ registerAiSkillRoutes(server2, resolvedCtx);
2549
+ registerAiLlmRoutes(server2, resolvedCtx);
2550
+ registerAiLlmStreamRoutes(server2, resolvedCtx);
2551
+ registerAiAgentStreamRoutes(server2, resolvedCtx);
2552
+ registerAiToolRoutes(server2, resolvedCtx);
2553
+ registerAiChatRoutes(server2, resolvedCtx);
2554
+ registerAiSummaryRoutes(server2, resolvedCtx);
2555
+ registerAiLogRoutes(server2, resolvedCtx);
2556
+ registerSemanticIndexRoutes(server2, resolvedCtx);
2817
2557
  registerMergeRoutes(server2, resolvedCtx);
2818
2558
  registerCacheRoutes(server2, resolvedCtx);
2819
2559
  registerTelemetryRoutes(server2, resolvedCtx);
2820
- registerLogRoutes(server2);
2560
+ registerLogRoutes(server2, resolvedCtx);
2821
2561
  }
2822
2562
 
2823
- // ../../packages/http-routes/src/register.ts
2824
- function registerSharedRoutes(server2, ctx, options) {
2825
- registerRestRoutes(server2, ctx);
2826
- registerWebRoutes(server2, ctx, options);
2563
+ // ../../packages/http-routes/src/auth.ts
2564
+ import { timingSafeEqual, createHmac, randomBytes } from "crypto";
2565
+ var cachedToken = null;
2566
+ var requireAuthEnabled = false;
2567
+ function setAuthToken(token) {
2568
+ cachedToken = token;
2569
+ }
2570
+ function setRequireAuth(enabled) {
2571
+ requireAuthEnabled = enabled;
2572
+ }
2573
+ var hmacKey = randomBytes(32);
2574
+ function safeTokenCompare(a, b) {
2575
+ const hashA = createHmac("sha256", hmacKey).update(a).digest();
2576
+ const hashB = createHmac("sha256", hmacKey).update(b).digest();
2577
+ return timingSafeEqual(hashA, hashB);
2578
+ }
2579
+ async function authHook(request, reply) {
2580
+ if (!cachedToken) return;
2581
+ const url = request.url;
2582
+ if (url.startsWith("/api/")) {
2583
+ return requireBearerToken(request, reply);
2584
+ }
2585
+ if (url.startsWith("/_web/")) {
2586
+ if (requireAuthEnabled) return requireBearerToken(request, reply);
2587
+ return;
2588
+ }
2589
+ }
2590
+ function requireBearerToken(request, reply) {
2591
+ if (!cachedToken) return;
2592
+ const authHeader = request.headers.authorization;
2593
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
2594
+ const err = unauthorized();
2595
+ reply.code(err.statusCode).send(errorResponse(err));
2596
+ return;
2597
+ }
2598
+ const token = authHeader.slice(7);
2599
+ if (!safeTokenCompare(token, cachedToken)) {
2600
+ const err = unauthorized();
2601
+ reply.code(err.statusCode).send(errorResponse(err));
2602
+ return;
2603
+ }
2827
2604
  }
2828
2605
 
2829
2606
  // ../../packages/http-routes/src/routes/web/automation.ts
@@ -2970,8 +2747,44 @@ function registerAutomationRoutes(server2, ctx) {
2970
2747
  });
2971
2748
  }
2972
2749
 
2750
+ // src/http/server.ts
2751
+ var JSON_BODY_LIMIT = 50 * 1024 * 1024;
2752
+ function createServer() {
2753
+ const server2 = Fastify({
2754
+ logger: false,
2755
+ bodyLimit: JSON_BODY_LIMIT
2756
+ });
2757
+ server2.addHook("onRequest", authHook);
2758
+ server2.setErrorHandler((error, request, reply) => {
2759
+ const apiError = apiErrorFromUnknown(error);
2760
+ if (apiError) {
2761
+ reply.code(apiError.statusCode).send(errorResponse(apiError));
2762
+ return;
2763
+ }
2764
+ if (error.statusCode === 413) {
2765
+ const bodyErr = new ApiError("BODY_TOO_LARGE" /* BODY_TOO_LARGE */, "Request body exceeds 50MB limit");
2766
+ reply.code(413).send(errorResponse(bodyErr));
2767
+ return;
2768
+ }
2769
+ const statusCode = error.statusCode;
2770
+ if (statusCode && statusCode >= 400 && statusCode < 600) {
2771
+ reply.code(statusCode).send({ success: false, error: { code: "CLIENT_ERROR", message: error.message } });
2772
+ return;
2773
+ }
2774
+ appLogger.error("http", `${request.method} ${request.url} -> 500`, error);
2775
+ const err = serverError(error.message);
2776
+ reply.code(err.statusCode).send(errorResponse(err));
2777
+ });
2778
+ return server2;
2779
+ }
2780
+
2781
+ // src/http/routes/web/index.ts
2782
+ import * as os3 from "os";
2783
+ import * as path10 from "path";
2784
+
2973
2785
  // src/http/routes/web/import.ts
2974
2786
  import * as fs7 from "fs";
2787
+ import * as os2 from "os";
2975
2788
  import * as path8 from "path";
2976
2789
  import { randomUUID } from "crypto";
2977
2790
  import { pipeline } from "stream/promises";
@@ -3002,7 +2815,9 @@ function cleanupTemp(...paths) {
3002
2815
  }
3003
2816
  }
3004
2817
  function registerImportRoutes2(server2, dbManager2, options = {}) {
3005
- const sourceManager = options.sourceManager ?? new ArchiveImportSourceManager();
2818
+ const sourceManager = options.sourceManager ?? new ArchiveImportSourceManager({
2819
+ tempRoot: fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-import-sources-"))
2820
+ });
3006
2821
  const runAutoImport = options.runAutoImport ?? autoImport.bind(null, dbManager2);
3007
2822
  const runPreparedImport = options.runPreparedImport ?? (async (manifestPath, onProgress) => {
3008
2823
  const result = await runAutoImport(manifestPath, {
@@ -3020,7 +2835,7 @@ function registerImportRoutes2(server2, dbManager2, options = {}) {
3020
2835
  limits: { fileSize: ARCHIVE_UPLOAD_LIMIT }
3021
2836
  });
3022
2837
  if (!data) return reply.code(400).send({ success: false, error: "error.no_file_selected" });
3023
- const uploadPath = path8.join(getChatLabTempScopeDir("imports"), `archive-${randomUUID()}.zip`);
2838
+ const uploadPath = path8.join(os2.tmpdir(), `chatlab-archive-${randomUUID()}.zip`);
3024
2839
  try {
3025
2840
  await pipeline(data.file, fs7.createWriteStream(uploadPath, { flags: "wx" }));
3026
2841
  if (data.file.truncated) {
@@ -3087,7 +2902,7 @@ data: ${JSON.stringify(eventData)}
3087
2902
  server2.post("/_web/detect-format", async (request, reply) => {
3088
2903
  const data = await request.file();
3089
2904
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
3090
- const tmpDir = createChatLabTempDir("imports", "detect-");
2905
+ const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-detect-"));
3091
2906
  const tmpPath = path8.join(tmpDir, data.filename || "upload");
3092
2907
  try {
3093
2908
  const chunks = [];
@@ -3105,7 +2920,7 @@ data: ${JSON.stringify(eventData)}
3105
2920
  server2.post("/_web/scan-multi-chat", async (request, reply) => {
3106
2921
  const data = await request.file();
3107
2922
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
3108
- const tmpDir = createChatLabTempDir("imports", "scan-");
2923
+ const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-scan-"));
3109
2924
  const tmpPath = path8.join(tmpDir, data.filename || "upload");
3110
2925
  try {
3111
2926
  const chunks = [];
@@ -3122,7 +2937,7 @@ data: ${JSON.stringify(eventData)}
3122
2937
  server2.post("/_web/import", async (request, reply) => {
3123
2938
  const data = await request.file();
3124
2939
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
3125
- const tmpDir = createChatLabTempDir("imports", "upload-");
2940
+ const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-import-"));
3126
2941
  const tmpPath = path8.join(tmpDir, data.filename || "upload");
3127
2942
  const chunks = [];
3128
2943
  for await (const chunk of data.file) {
@@ -3166,7 +2981,7 @@ data: ${JSON.stringify(eventData)}
3166
2981
  server2.post("/_web/import-directory", async (request, reply) => {
3167
2982
  const parts = request.parts();
3168
2983
  if (!parts) return reply.code(400).send({ error: "No files uploaded" });
3169
- const tmpDir = createChatLabTempDir("imports", "directory-");
2984
+ const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-dir-import-"));
3170
2985
  const relativePaths = [];
3171
2986
  const fileBuffers = [];
3172
2987
  try {
@@ -3237,7 +3052,7 @@ data: ${JSON.stringify({ success: false, error: err instanceof Error ? err.messa
3237
3052
  const sessionId = request.params.id;
3238
3053
  const data = await request.file();
3239
3054
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
3240
- const tmpDir = createChatLabTempDir("imports", "incremental-");
3055
+ const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-inc-"));
3241
3056
  const tmpPath = path8.join(tmpDir, data.filename || "upload");
3242
3057
  const chunks = [];
3243
3058
  for await (const chunk of data.file) {
@@ -3275,7 +3090,7 @@ data: ${JSON.stringify(eventData)}
3275
3090
  const sessionId = request.params.id;
3276
3091
  const data = await request.file();
3277
3092
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
3278
- const tmpDir = createChatLabTempDir("imports", "analyze-");
3093
+ const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-analyze-"));
3279
3094
  const tmpPath = path8.join(tmpDir, data.filename || "upload");
3280
3095
  const chunks = [];
3281
3096
  for await (const chunk of data.file) {
@@ -3293,7 +3108,7 @@ data: ${JSON.stringify(eventData)}
3293
3108
  server2.post("/_web/import/analyze", async (request, reply) => {
3294
3109
  const data = await request.file();
3295
3110
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
3296
- const tmpDir = createChatLabTempDir("imports", "analyze-");
3111
+ const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-analyze-"));
3297
3112
  const tmpPath = path8.join(tmpDir, data.filename || "upload");
3298
3113
  const chunks = [];
3299
3114
  for await (const chunk of data.file) {
@@ -3329,7 +3144,7 @@ data: ${JSON.stringify(eventData)}
3329
3144
 
3330
3145
  `);
3331
3146
  }
3332
- const tmpDir = createChatLabTempDir("imports", "cli-demo-");
3147
+ const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-demo-"));
3333
3148
  try {
3334
3149
  const localPaths = [];
3335
3150
  for (let i = 0; i < total; i++) {
@@ -3416,29 +3231,29 @@ function buildWebUpdateCheckResult(options) {
3416
3231
  }
3417
3232
 
3418
3233
  // src/http/routes/web/index.ts
3419
- function registerWebRoutes2(server2, dbManager2, options) {
3234
+ function registerWebRoutes(server2, dbManager2, options) {
3420
3235
  const adapter = createDatabaseManagerAdapter(dbManager2);
3421
3236
  const mergeCache = options?.pathProvider ? new MergeSessionCache(options.pathProvider, { nativeBinding: options.nativeBinding }) : null;
3422
3237
  mergeCache?.cleanupOrphans();
3423
3238
  const fallbackPathProvider = {
3424
- getSystemDir: () => path10.join(os.homedir(), ".chatlab"),
3425
- getUserDataDir: () => path10.join(os.homedir(), ".chatlab", "data"),
3426
- getDatabaseDir: () => path10.join(os.homedir(), ".chatlab", "data", "databases"),
3427
- getVectorDir: () => path10.join(os.homedir(), ".chatlab", "data", "vector"),
3428
- getAiDataDir: () => path10.join(os.homedir(), ".chatlab", "ai"),
3429
- getSettingsDir: () => path10.join(os.homedir(), ".chatlab", "settings"),
3430
- getCacheDir: () => path10.join(os.homedir(), ".chatlab", "cache"),
3431
- getTempDir: () => getChatLabTempScopeDir("runtime"),
3432
- getLogsDir: () => path10.join(os.homedir(), ".chatlab", "logs"),
3433
- getDownloadsDir: () => path10.join(os.homedir(), "Downloads")
3239
+ getSystemDir: () => path10.join(os3.homedir(), ".chatlab"),
3240
+ getUserDataDir: () => path10.join(os3.homedir(), ".chatlab", "data"),
3241
+ getDatabaseDir: () => path10.join(os3.homedir(), ".chatlab", "data", "databases"),
3242
+ getVectorDir: () => path10.join(os3.homedir(), ".chatlab", "data", "vector"),
3243
+ getAiDataDir: () => path10.join(os3.homedir(), ".chatlab", "ai"),
3244
+ getSettingsDir: () => path10.join(os3.homedir(), ".chatlab", "settings"),
3245
+ getCacheDir: () => path10.join(os3.homedir(), ".chatlab", "cache"),
3246
+ getTempDir: () => path10.join(os3.homedir(), ".chatlab", "temp"),
3247
+ getLogsDir: () => path10.join(os3.homedir(), ".chatlab", "logs"),
3248
+ getDownloadsDir: () => path10.join(os3.homedir(), "Downloads")
3434
3249
  };
3435
3250
  const resolvedPathProvider = options?.pathProvider ?? fallbackPathProvider;
3436
3251
  const ai = options?.aiContext;
3437
3252
  const cliStreamImport = async (dm, filePath) => {
3438
- const { streamImport: streamImport2 } = await import("./stream-import-JKQYRISL.mjs");
3253
+ const { streamImport: streamImport2 } = await import("./stream-import-BPCHGRAN.mjs");
3439
3254
  const result = await streamImport2(dm, filePath);
3440
3255
  if (!result.success) {
3441
- const { IMPORT_IN_PROGRESS_ERROR_KEY: IMPORT_IN_PROGRESS_ERROR_KEY2, ImportInProgressError: ImportInProgressError2 } = await import("./src-UY4GCVNF.mjs");
3256
+ const { IMPORT_IN_PROGRESS_ERROR_KEY: IMPORT_IN_PROGRESS_ERROR_KEY2, ImportInProgressError: ImportInProgressError2 } = await import("./src-MNH5S423.mjs");
3442
3257
  if (result.error === IMPORT_IN_PROGRESS_ERROR_KEY2) throw new ImportInProgressError2();
3443
3258
  throw new Error(result.error || "Import failed");
3444
3259
  }
@@ -4339,11 +4154,12 @@ function reloadTimer(dsId, skipInitialPull = false) {
4339
4154
 
4340
4155
  // src/sync/adapters.ts
4341
4156
  import * as fs11 from "fs";
4157
+ import * as os4 from "os";
4342
4158
  import * as path13 from "path";
4343
4159
  import * as crypto3 from "crypto";
4344
4160
  function getTempFilePath(ext) {
4345
4161
  const id = crypto3.randomBytes(8).toString("hex");
4346
- return path13.join(getChatLabTempScopeDir("sync"), `pull-${id}${ext}`);
4162
+ return path13.join(os4.tmpdir(), `chatlab-pull-${id}${ext}`);
4347
4163
  }
4348
4164
  var NodeFetcher = class {
4349
4165
  async fetchToTempFile(baseUrl, remoteSessionId, token, params) {
@@ -4514,6 +4330,21 @@ function cleanupSync() {
4514
4330
  var server = null;
4515
4331
  var dbManager = null;
4516
4332
  var aiChatManager = null;
4333
+ function createFileConfigStorage(aiDataDir) {
4334
+ return {
4335
+ readJson(key) {
4336
+ try {
4337
+ return JSON.parse(fs12.readFileSync(`${aiDataDir}/${key}.json`, "utf-8"));
4338
+ } catch {
4339
+ return null;
4340
+ }
4341
+ },
4342
+ writeJson(key, data) {
4343
+ if (!fs12.existsSync(aiDataDir)) fs12.mkdirSync(aiDataDir, { recursive: true });
4344
+ fs12.writeFileSync(`${aiDataDir}/${key}.json`, JSON.stringify(data, null, 2), "utf-8");
4345
+ }
4346
+ };
4347
+ }
4517
4348
  function resolveNativeBinding2() {
4518
4349
  if (process.versions.electron) return void 0;
4519
4350
  const nativePath = resolveCliPath("native/better_sqlite3.node");
@@ -4571,11 +4402,20 @@ async function startHttpServer(options) {
4571
4402
  aiChatManager = new AIChatManager(aiDataDir, { nativeBinding });
4572
4403
  const assistantManager = getAssistantManager(aiDataDir);
4573
4404
  const skillManagerCore = getSkillManagerCore(aiDataDir);
4574
- const configStorage = createFileConfigStorage(aiDataDir);
4575
- const llmConfigStore = createAuthProfileLlmConfigStore(configStorage);
4405
+ const llmConfigStore = new LLMConfigStore(createFileConfigStorage(aiDataDir), {
4406
+ resolveApiKey: (provider, authProfile) => resolveApiKey(provider, authProfile) || void 0,
4407
+ onApiKeyCreated: (config2, apiKey) => {
4408
+ const profileName = config2.name?.toLowerCase().replace(/\s+/g, "-") || config2.provider;
4409
+ writeAuthProfile(profileName, { type: "api_key", provider: config2.provider, key: apiKey });
4410
+ return profileName;
4411
+ },
4412
+ onApiKeyDeleted: (config2) => {
4413
+ const profileName = config2.authProfile;
4414
+ if (profileName) deleteAuthProfile(profileName);
4415
+ }
4416
+ });
4576
4417
  initAppLogger(pathProvider.getLogsDir());
4577
4418
  initServerAiLogger(pathProvider.getLogsDir());
4578
- appLogger.info("temp-workspace", "Temporary workspace initialized", { root: pathProvider.getTempDir() });
4579
4419
  appLogger.info("server", `HTTP server starting on ${host}:${port}`);
4580
4420
  logNativeParserStatus();
4581
4421
  setAuthToken(token);
@@ -4599,7 +4439,7 @@ async function startHttpServer(options) {
4599
4439
  console.warn("[semantic-index] worker client unavailable:", err instanceof Error ? err.message : String(err));
4600
4440
  semanticIndexService = void 0;
4601
4441
  }
4602
- registerWebRoutes2(server, dbManager, {
4442
+ registerWebRoutes(server, dbManager, {
4603
4443
  pathProvider,
4604
4444
  nativeBinding,
4605
4445
  runtimeIdentity: runtime,
@@ -4610,8 +4450,8 @@ async function startHttpServer(options) {
4610
4450
  assistantManager,
4611
4451
  skillManagerCore,
4612
4452
  llmConfigStore,
4613
- customProviderStore: new CustomProviderStore(configStorage),
4614
- customModelStore: new CustomModelStore(configStorage),
4453
+ customProviderStore: new CustomProviderStore(createFileConfigStorage(aiDataDir)),
4454
+ customModelStore: new CustomModelStore(createFileConfigStorage(aiDataDir)),
4615
4455
  runAgentStream: createCliRunAgentStream(dbManager, aiChatManager, semanticIndexService)
4616
4456
  }
4617
4457
  });
@@ -4653,8 +4493,8 @@ async function stopHttpServer() {
4653
4493
 
4654
4494
  export {
4655
4495
  createServer,
4656
- registerWebRoutes2 as registerWebRoutes,
4496
+ registerWebRoutes,
4657
4497
  startHttpServer,
4658
4498
  stopHttpServer
4659
4499
  };
4660
- //# sourceMappingURL=chunk-67HNEIKY.mjs.map
4500
+ //# sourceMappingURL=chunk-BSSR4ZYW.mjs.map