chatlab-cli 0.31.2 → 0.31.3

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 (111) hide show
  1. package/dist/{chat-command-QPL4GNHP.mjs → chat-command-ZIXKNLSE.mjs} +15 -9
  2. package/dist/chat-command-ZIXKNLSE.mjs.map +1 -0
  3. package/dist/{chunk-OAXVKNPN.mjs → chunk-23AIEWCD.mjs} +2 -2
  4. package/dist/{chunk-F2NZ7HAY.mjs → chunk-3J7WTUEE.mjs} +2 -2
  5. package/dist/{chunk-GYPBWKAU.mjs → chunk-75FZ26HM.mjs} +896 -439
  6. package/dist/chunk-75FZ26HM.mjs.map +1 -0
  7. package/dist/{chunk-OQ6N4PDD.mjs → chunk-BLOBEOCG.mjs} +2 -2
  8. package/dist/{chunk-6JKKHPXY.mjs → chunk-DV4U34MR.mjs} +2 -2
  9. package/dist/{chunk-ZCXCHKQV.mjs → chunk-EINCXXWY.mjs} +3 -3
  10. package/dist/{chunk-3SZWD7UM.mjs → chunk-FDLLEDNW.mjs} +79 -20
  11. package/dist/chunk-FDLLEDNW.mjs.map +1 -0
  12. package/dist/{chunk-BSSR4ZYW.mjs → chunk-GZO3NAHC.mjs} +2130 -2018
  13. package/dist/chunk-GZO3NAHC.mjs.map +1 -0
  14. package/dist/{chunk-S5PQM5J4.mjs → chunk-MFOCDGCC.mjs} +17 -4
  15. package/dist/chunk-MFOCDGCC.mjs.map +1 -0
  16. package/dist/{chunk-64FKPYPJ.mjs → chunk-UIUEG7JB.mjs} +11 -11
  17. package/dist/{chunk-IK7GJTGY.mjs → chunk-YVNZ73O5.mjs} +147 -266
  18. package/dist/chunk-YVNZ73O5.mjs.map +1 -0
  19. package/dist/cli.mjs +8 -8
  20. package/dist/contacts-worker.mjs +2 -2
  21. package/dist/global-insight-worker.mjs +1 -1
  22. package/dist/{http-NOEWVWCP.mjs → http-WDG2OHOV.mjs} +11 -11
  23. package/dist/{import-RS3HW7ZM.mjs → import-4BDC37F7.mjs} +7 -7
  24. package/dist/index.mjs +11 -11
  25. package/dist/{mcp-7F3G47Z4.mjs → mcp-4KUVRGFF.mjs} +8 -8
  26. package/dist/people-relationships-worker.mjs +2 -2
  27. package/dist/semantic-index-worker.mjs +1 -1
  28. package/dist/{src-MNH5S423.mjs → src-ZOCLFBCJ.mjs} +28 -6
  29. package/dist/{stream-import-BPCHGRAN.mjs → stream-import-ERTT7U6V.mjs} +7 -7
  30. package/dist/{worker-runtime-TTAXGQ5R.mjs → worker-runtime-2TUZHV7W.mjs} +5 -5
  31. package/dist-web/assets/ActionToolsPanel-Dp-ojXjO.css +1 -0
  32. package/dist-web/assets/ActionToolsPanel.vue_vue_type_script_setup_true_lang-GgQ5ueMX.js +110 -0
  33. package/dist-web/assets/{BufferResource-Dh2kaMuP.js → BufferResource-DgKoy0Ii.js} +1 -1
  34. package/dist-web/assets/{CanvasRenderer-B8hHywwz.js → CanvasRenderer-CKpp71xg.js} +1 -1
  35. package/dist-web/assets/{EChartWordcloud-BXC_4nUP.js → EChartWordcloud-CSOuFvyM.js} +1 -1
  36. package/dist-web/assets/{Filter-Bap1KMW6.js → Filter-D32zqY5x.js} +1 -1
  37. package/dist-web/assets/{LoadingState.vue_vue_type_script_setup_true_lang-B2KAoiMG.js → LoadingState.vue_vue_type_script_setup_true_lang-DUAtrSZD.js} +1 -1
  38. package/dist-web/assets/{MessageExportModal-H3-LAaXY.js → MessageExportModal-BYLw4vZ8.js} +1 -1
  39. package/dist-web/assets/{PageHeader-CzTV_lyM.js → PageHeader-Dmu6cdAk.js} +1 -1
  40. package/dist-web/assets/{RelationshipGalaxyCanvas-C6ZoUZNc.js → RelationshipGalaxyCanvas-DsMOubHV.js} +3 -3
  41. package/dist-web/assets/{RelationshipGalaxyThreeCanvas-DV6B-7XB.js → RelationshipGalaxyThreeCanvas-Dh5HjVHr.js} +1 -1
  42. package/dist-web/assets/{RenderTargetSystem-Dpgyoac_.js → RenderTargetSystem-CovK4x_D.js} +1 -1
  43. package/dist-web/assets/{ThemeCard.vue_vue_type_script_setup_true_lang-BWJ6kuSC.js → ThemeCard.vue_vue_type_script_setup_true_lang-DhUfYrEx.js} +1 -1
  44. package/dist-web/assets/{TimeSelect.vue_vue_type_script_setup_true_lang--Tbo7p-l.js → TimeSelect.vue_vue_type_script_setup_true_lang-CaumhCAS.js} +1 -1
  45. package/dist-web/assets/{WebGLRenderer-DICnsAm5.js → WebGLRenderer-CawhbcYU.js} +1 -1
  46. package/dist-web/assets/{WebGPURenderer-Bf0gkRK9.js → WebGPURenderer-B7P1D2sA.js} +1 -1
  47. package/dist-web/assets/{annual-summary-time-range-CK0IIPsY.js → annual-summary-time-range-Bya4h2j6.js} +1 -1
  48. package/dist-web/assets/{base-D5T8QZ8b.js → base-ClGWE9aq.js} +1 -1
  49. package/dist-web/assets/{browserAll-DcoCy7xY.js → browserAll-CtQkc1DF.js} +1 -1
  50. package/dist-web/assets/{dateFormat-BDOkdsCR.js → dateFormat-Cfb89ZjM.js} +1 -1
  51. package/dist-web/assets/{electron-CmhzSjaH.js → electron-Brd7jS6W.js} +1 -1
  52. package/dist-web/assets/{fetch-CNz28Lv5.js → fetch-BJ3lnhzK.js} +1 -1
  53. package/dist-web/assets/{fetch-DfTM4Dah.js → fetch-BoB63ns8.js} +1 -1
  54. package/dist-web/assets/fetch-BtK7YvLp.js +1 -0
  55. package/dist-web/assets/{fetch-D71bG6Fc.js → fetch-CeREJiFL.js} +1 -1
  56. package/dist-web/assets/{fetch-C1mlq5VY.js → fetch-D2hvh7lO.js} +1 -1
  57. package/dist-web/assets/{fetch-DtcNaN3d.js → fetch-DVGCzZ0E.js} +1 -1
  58. package/dist-web/assets/{fetch-BddgpyKJ.js → fetch-DWoMmOlr.js} +1 -1
  59. package/dist-web/assets/{fetch-dI5w8lYN.js → fetch-DkfUUq-R.js} +1 -1
  60. package/dist-web/assets/{fetch-D8xv4lgE.js → fetch-Dr_Fjtvp.js} +1 -1
  61. package/dist-web/assets/{fetch-TATpgXht.js → fetch-u7ivV7Uy.js} +1 -1
  62. package/dist-web/assets/{index-CY5yJhCJ.js → index-BHnQaeQY.js} +1 -1
  63. package/dist-web/assets/{index-qZ7jnIna.js → index-BXaKyn8J.js} +2 -2
  64. package/dist-web/assets/{index-Dtbu8m1o.js → index-BzH9cnfy.js} +1 -1
  65. package/dist-web/assets/{index-Bs4ZX2-v.js → index-CuPE4SSh.js} +1 -1
  66. package/dist-web/assets/index-D-qNxFxp.js +173 -0
  67. package/dist-web/assets/index-D494nYee.js +64 -0
  68. package/dist-web/assets/index-D8Uq3_Dc.css +1 -0
  69. package/dist-web/assets/index-DBb03wVF.css +1 -0
  70. package/dist-web/assets/{index-DClxj4T2.js → index-DJ8kB4US.js} +2 -2
  71. package/dist-web/assets/{index-De3ImZb9.js → index-DNMWR2Vj.js} +1 -1
  72. package/dist-web/assets/{index-DphjBwbA.js → index-DSPls1YV.js} +1 -1
  73. package/dist-web/assets/{index-soX5gDgh.js → index-DcAqhCpS.js} +1 -1
  74. package/dist-web/assets/{index-a1ZrlsKo.js → index-DnkaYOFI.js} +1 -1
  75. package/dist-web/assets/index-_UKteDZB.js +25 -0
  76. package/dist-web/assets/index-gi0wgh0M.css +1 -0
  77. package/dist-web/assets/{index-Dp3gCHpK.js → index-w_1EjXiF.js} +1 -1
  78. package/dist-web/assets/{people-page-header-D93GTPBO.js → people-page-header-YGQM9G53.js} +1 -1
  79. package/dist-web/assets/{vendor-nuxt-ui-DUpPmItD.js → vendor-nuxt-ui-DmkGVKod.js} +2 -2
  80. package/dist-web/assets/{vendor-reka-ui-BLnO2MqY.js → vendor-reka-ui-BkhadcDj.js} +1 -1
  81. package/dist-web/assets/{web-H9T1dZys.js → web-zXxtk2KZ.js} +1 -1
  82. package/dist-web/assets/{webworkerAll-D1Kui8G8.js → webworkerAll-BzRoT-6m.js} +1 -1
  83. package/dist-web/index.html +4 -4
  84. package/package.json +1 -1
  85. package/dist/chat-command-QPL4GNHP.mjs.map +0 -1
  86. package/dist/chunk-3SZWD7UM.mjs.map +0 -1
  87. package/dist/chunk-BSSR4ZYW.mjs.map +0 -1
  88. package/dist/chunk-GYPBWKAU.mjs.map +0 -1
  89. package/dist/chunk-IK7GJTGY.mjs.map +0 -1
  90. package/dist/chunk-S5PQM5J4.mjs.map +0 -1
  91. package/dist-web/assets/ActionToolsPanel-CbWhu_Uv.css +0 -1
  92. package/dist-web/assets/ActionToolsPanel.vue_vue_type_script_setup_true_lang-CvvNqXuh.js +0 -110
  93. package/dist-web/assets/fetch-BJka-sCN.js +0 -1
  94. package/dist-web/assets/index-CAjUCtGD.css +0 -1
  95. package/dist-web/assets/index-CF59LLOG.js +0 -25
  96. package/dist-web/assets/index-CJ3WtSLo.js +0 -173
  97. package/dist-web/assets/index-CJS6FVuj.js +0 -64
  98. package/dist-web/assets/index-CsqMIyA9.css +0 -1
  99. package/dist-web/assets/index-CtmXho1w.css +0 -1
  100. /package/dist/{chunk-OAXVKNPN.mjs.map → chunk-23AIEWCD.mjs.map} +0 -0
  101. /package/dist/{chunk-F2NZ7HAY.mjs.map → chunk-3J7WTUEE.mjs.map} +0 -0
  102. /package/dist/{chunk-OQ6N4PDD.mjs.map → chunk-BLOBEOCG.mjs.map} +0 -0
  103. /package/dist/{chunk-6JKKHPXY.mjs.map → chunk-DV4U34MR.mjs.map} +0 -0
  104. /package/dist/{chunk-ZCXCHKQV.mjs.map → chunk-EINCXXWY.mjs.map} +0 -0
  105. /package/dist/{chunk-64FKPYPJ.mjs.map → chunk-UIUEG7JB.mjs.map} +0 -0
  106. /package/dist/{http-NOEWVWCP.mjs.map → http-WDG2OHOV.mjs.map} +0 -0
  107. /package/dist/{import-RS3HW7ZM.mjs.map → import-4BDC37F7.mjs.map} +0 -0
  108. /package/dist/{mcp-7F3G47Z4.mjs.map → mcp-4KUVRGFF.mjs.map} +0 -0
  109. /package/dist/{src-MNH5S423.mjs.map → src-ZOCLFBCJ.mjs.map} +0 -0
  110. /package/dist/{stream-import-BPCHGRAN.mjs.map → stream-import-ERTT7U6V.mjs.map} +0 -0
  111. /package/dist/{worker-runtime-TTAXGQ5R.mjs.map → worker-runtime-2TUZHV7W.mjs.map} +0 -0
@@ -7,23 +7,25 @@ const __dirname = __pathDirname(__filename);
7
7
  import {
8
8
  closeServerAiLogger,
9
9
  createCliRunAgentStream,
10
+ getAssistantManager,
10
11
  getServerAiLogger,
12
+ getSkillManagerCore,
11
13
  initServerAiLogger
12
- } from "./chunk-3SZWD7UM.mjs";
14
+ } from "./chunk-FDLLEDNW.mjs";
13
15
  import {
14
16
  AGENT_TOOL_REGISTRY,
15
17
  CoreDataProvider
16
- } from "./chunk-6JKKHPXY.mjs";
18
+ } from "./chunk-DV4U34MR.mjs";
17
19
  import {
18
20
  analyzeIncrementalImport,
19
21
  analyzeNewImport,
20
22
  autoImport,
21
23
  incrementalImport,
22
24
  streamImport
23
- } from "./chunk-ZCXCHKQV.mjs";
25
+ } from "./chunk-EINCXXWY.mjs";
24
26
  import {
25
27
  assertCliDataDirCompatible
26
- } from "./chunk-OQ6N4PDD.mjs";
28
+ } from "./chunk-BLOBEOCG.mjs";
27
29
  import {
28
30
  getVersion
29
31
  } from "./chunk-5ADKYYNA.mjs";
@@ -41,27 +43,27 @@ import {
41
43
  getPendingNodeDataDirMigration,
42
44
  hasPendingElectronDataWarning,
43
45
  serializeChatLabToJsonl
44
- } from "./chunk-S5PQM5J4.mjs";
46
+ } from "./chunk-MFOCDGCC.mjs";
45
47
  import {
46
48
  AIChatManager,
47
49
  ArchiveImportError,
48
50
  ArchiveImportSourceManager,
49
- AssistantManager,
50
51
  CustomModelStore,
51
52
  CustomProviderStore,
52
53
  IMPORT_IN_PROGRESS_ERROR_KEY,
53
54
  ImportIdempotencyCache,
54
55
  ImportInProgressError,
55
- LLMConfigStore,
56
56
  MergeSessionCache,
57
57
  SEMANTIC_INDEX_CONFIG_FILE,
58
58
  SemanticIndexConfigStore,
59
- SkillManagerCore,
60
59
  TempDbWriter,
61
60
  buildPiModel,
62
61
  computeWordFrequency,
63
62
  countMessagesTokens,
63
+ createAuthProfileLlmConfigStore,
64
+ createChatLabTempDir,
64
65
  createContactsService,
66
+ createFileConfigStorage,
65
67
  createGlobalInsightService,
66
68
  createPeopleRelationshipsService,
67
69
  deleteDict,
@@ -73,12 +75,14 @@ import {
73
75
  export_service_exports,
74
76
  fetchRemoteModels,
75
77
  findEntryFileInDirectory,
78
+ getChatLabTempScopeDir,
76
79
  getDefaultRulesForLocale,
77
80
  getDictList,
78
81
  getSupportedFormats,
79
82
  hashImportBody,
80
83
  isDictDownloaded,
81
84
  isSemanticIndexConfigured,
85
+ isValidImportSessionId,
82
86
  logNativeParserStatus,
83
87
  member_service_exports,
84
88
  mergeRulesForLocale,
@@ -94,7 +98,7 @@ import {
94
98
  streamParseFileInfo,
95
99
  summary_service_exports,
96
100
  validateApiKey
97
- } from "./chunk-GYPBWKAU.mjs";
101
+ } from "./chunk-75FZ26HM.mjs";
98
102
  import {
99
103
  resolveCliPath
100
104
  } from "./chunk-FJ35V3TE.mjs";
@@ -110,7 +114,6 @@ import {
110
114
  POS_TAG_DEFINITIONS,
111
115
  appLogger,
112
116
  createDatabaseManagerAdapter,
113
- deleteAuthProfile,
114
117
  executeReadonlySql,
115
118
  executeSql,
116
119
  getAvailableYears,
@@ -129,8 +132,7 @@ import {
129
132
  getLongMessageCount,
130
133
  getMemberActivity,
131
134
  getMemberMonthlyTrend,
132
- getMembers,
133
- getMembersDetailed,
135
+ getMembersWithAliases,
134
136
  getMemeBattleAnalysis,
135
137
  getMentionAnalysis,
136
138
  getMentionGraph,
@@ -155,18 +157,16 @@ import {
155
157
  isNewerStableVersion,
156
158
  loadConfig,
157
159
  queryMessages,
158
- resolveApiKey,
159
160
  stripAvatarFields,
160
161
  verifyCliDataPath,
161
- writeAuthProfile,
162
162
  writeConfigField
163
- } from "./chunk-IK7GJTGY.mjs";
163
+ } from "./chunk-YVNZ73O5.mjs";
164
164
 
165
165
  // src/http/index.ts
166
- import * as fs12 from "fs";
166
+ import * as fs11 from "fs";
167
167
  import * as crypto4 from "crypto";
168
168
 
169
- // src/http/server.ts
169
+ // ../../packages/http-routes/src/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() {
221
- return new ApiError("IMPORT_IN_PROGRESS" /* IMPORT_IN_PROGRESS */, "Another import is already in progress");
220
+ function importInProgress(message = "Another import is already in progress") {
221
+ return new ApiError("IMPORT_IN_PROGRESS" /* IMPORT_IN_PROGRESS */, message);
222
222
  }
223
223
  function idempotencyConflict() {
224
224
  return new ApiError("IDEMPOTENCY_CONFLICT" /* IDEMPOTENCY_CONFLICT */, "Same Idempotency-Key with different request body hash");
@@ -238,7 +238,18 @@ 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
- if (error instanceof DataDirCompatibilityError) return dataDirIncompatible(error.message);
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
+ }
242
253
  return null;
243
254
  }
244
255
  function successResponse(data, meta) {
@@ -262,195 +273,378 @@ function errorResponse(error) {
262
273
  };
263
274
  }
264
275
 
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 {
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;
272
284
  }
273
- return successResponse({
274
- name: "ChatLab API",
275
- version: ctx.getVersion(),
276
- uptime: Math.floor(process.uptime()),
277
- sessionCount
278
- });
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));
279
298
  });
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
- }
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);
357
+ }
358
+ });
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));
336
420
  }
337
421
  }
338
- });
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);
460
+ }
461
+ }
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)
339
471
  });
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
+ );
340
486
  }
341
487
 
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;
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
+ };
348
494
  }
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);
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
+ };
523
+ }
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);
354
577
  if (!db) return null;
355
- const info = getSessionInfo(db);
356
- if (!info) 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;
357
583
  return {
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
584
+ members,
585
+ messages
367
586
  };
368
- }).filter(Boolean);
369
- return successResponse(sessions);
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());
370
596
  });
371
597
  server2.get("/api/v1/sessions/:id", async (request) => {
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
- });
598
+ const session = await provider.getSession(request.params.id);
599
+ if (!session) throw sessionNotFound(request.params.id);
600
+ return successResponse(session);
388
601
  });
389
602
  server2.get("/api/v1/sessions/:id/messages", async (request) => {
390
603
  const { id } = request.params;
391
- const db = ensureDb(ctx, id);
392
604
  const page = Math.max(1, parseInt(request.query.page || "1", 10) || 1);
393
605
  const limit = Math.min(1e3, Math.max(1, parseInt(request.query.limit || "100", 10) || 100));
394
606
  const offset = (page - 1) * limit;
395
607
  const { startTime, endTime, keyword, senderId } = request.query;
396
- const result = queryMessages(db, {
608
+ const result = await provider.queryMessages(id, {
397
609
  keyword: keyword || void 0,
398
- startTs: startTime ? parseInt(startTime, 10) : void 0,
399
- endTs: endTime ? parseInt(endTime, 10) : void 0,
610
+ startTime: startTime ? parseInt(startTime, 10) : void 0,
611
+ endTime: endTime ? parseInt(endTime, 10) : void 0,
400
612
  senderId: senderId ? parseInt(senderId, 10) : void 0,
401
613
  limit,
402
614
  offset
403
615
  });
616
+ if (!result) throw sessionNotFound(id);
404
617
  return successResponse({
405
618
  messages: result.messages,
406
619
  total: result.total,
407
620
  page,
408
621
  limit,
409
- totalPages: result.totalPages
622
+ totalPages: result.totalPages ?? Math.ceil(result.total / limit)
410
623
  });
411
624
  });
412
625
  server2.get("/api/v1/sessions/:id/members", async (request) => {
413
- const db = ensureDb(ctx, request.params.id);
414
- const members = getMembers(db);
626
+ const members = await provider.getMembers(request.params.id);
627
+ if (!members) throw sessionNotFound(request.params.id);
415
628
  return successResponse(members);
416
629
  });
417
630
  server2.get("/api/v1/sessions/:id/stats/overview", async (request) => {
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
- });
631
+ const overview = await provider.getOverview(request.params.id);
632
+ if (!overview) throw sessionNotFound(request.params.id);
633
+ return successResponse(overview);
443
634
  });
444
635
  server2.post("/api/v1/sessions/:id/sql", async (request, reply) => {
445
636
  const { id } = request.params;
446
- const db = ensureDb(ctx, id);
447
637
  const { sql } = request.body || {};
448
638
  if (!sql || typeof sql !== "string") {
449
639
  const err = sqlExecutionError("Missing sql parameter");
450
640
  return reply.code(err.statusCode).send(errorResponse(err));
451
641
  }
452
642
  try {
453
- const result = executeReadonlySql(db, sql);
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
+ }
454
648
  return successResponse(result);
455
649
  } catch (err) {
456
650
  const message = err instanceof Error ? err.message : "SQL execution error";
@@ -465,16 +659,14 @@ function registerRestSessionRoutes(server2, ctx) {
465
659
  });
466
660
  server2.get("/api/v1/sessions/:id/export", async (request, reply) => {
467
661
  const { id } = request.params;
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);
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);
474
666
  return reply.code(err.statusCode).send(errorResponse(err));
475
667
  }
476
- const members = getMembersDetailed(db);
477
- const allMessages = queryMessages(db, { limit: EXPORT_MESSAGE_LIMIT, offset: 0 });
668
+ const exportData = await provider.getExportData(id, EXPORT_MESSAGE_LIMIT);
669
+ if (!exportData) throw sessionNotFound(id);
478
670
  const chatLabFormat = {
479
671
  chatlab: {
480
672
  version: "0.0.2",
@@ -482,17 +674,18 @@ function registerRestSessionRoutes(server2, ctx) {
482
674
  generator: "ChatLab API"
483
675
  },
484
676
  meta: {
485
- name: meta.name,
486
- platform: meta.platform,
487
- type: meta.type,
488
- groupId: meta.groupId || void 0
677
+ name: session.name,
678
+ platform: session.platform,
679
+ type: session.type,
680
+ groupId: session.groupId || void 0
489
681
  },
490
- members: members.map((m) => ({
682
+ members: exportData.members.map((m) => ({
491
683
  platformId: m.platformId,
492
684
  accountName: m.accountName || m.platformId,
493
- groupNickname: m.groupNickname || void 0
685
+ groupNickname: m.groupNickname || void 0,
686
+ aliases: Array.isArray(m.aliases) && m.aliases.length > 0 ? m.aliases : void 0
494
687
  })),
495
- messages: allMessages.messages.map((msg) => ({
688
+ messages: exportData.messages.map((msg) => ({
496
689
  sender: msg.senderPlatformId,
497
690
  accountName: msg.senderName || void 0,
498
691
  timestamp: msg.timestamp,
@@ -504,1549 +697,1613 @@ function registerRestSessionRoutes(server2, ctx) {
504
697
  });
505
698
  }
506
699
 
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
- }
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 {
559
707
  }
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
708
+ return successResponse({
709
+ name: "ChatLab API",
710
+ version: ctx.getVersion(),
711
+ uptime: Math.floor(process.uptime()),
712
+ sessionCount
575
713
  });
576
714
  });
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;
586
- });
587
- server2.delete("/_web/sessions/:id", async (request, reply) => {
588
- const { id } = request.params;
589
- try {
590
- const deleted = session_service_exports.deleteSession(adapter, id);
591
- if (!deleted) {
592
- return reply.code(404).send({ success: false, error: "File not found" });
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
+ }
593
772
  }
594
- return { success: true };
595
- } catch (err) {
596
- return reply.code(500).send({ success: false, error: String(err) });
597
- }
598
- });
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);
607
- return { success: true };
608
- }
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 };
773
+ });
627
774
  });
628
775
  }
629
776
 
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);
635
- });
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"
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
642
802
  });
643
- });
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 };
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
+ });
822
+ 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();
658
838
  }
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);
839
+ });
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);
665
846
  return { success: true };
666
847
  }
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
- );
848
+ return { success: false };
849
+ });
675
850
  }
676
851
 
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
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;
684
856
  });
685
- server2.addHook("onClose", async () => {
686
- await service.close();
857
+ const mgr = ctx.assistantManager;
858
+ if (!mgr) return;
859
+ server2.get("/_web/ai/assistants", async () => {
860
+ return mgr.getAllAssistants();
687
861
  });
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
- });
862
+ server2.get("/_web/ai/assistants/:id/upgrade-status", async (request) => {
863
+ return mgr.getAssistantUpgradeInfo(request.params.id);
697
864
  });
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
- });
865
+ server2.get("/_web/ai/assistants/:id", async (request, reply) => {
866
+ const config = mgr.getAssistantConfig(request.params.id);
867
+ if (!config) return reply.code(404).send({ error: "Not found" });
868
+ return config;
706
869
  });
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
- });
870
+ server2.post("/_web/ai/assistants", async (request) => {
871
+ return mgr.createAssistant(request.body);
712
872
  });
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));
744
- }
745
-
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
753
- });
754
- server2.addHook("onClose", async () => {
755
- await service.close();
873
+ server2.put("/_web/ai/assistants/:id", async (request, reply) => {
874
+ const result = mgr.updateAssistant(request.params.id, request.body);
875
+ if (!result.success) return reply.code(404).send(result);
876
+ return result;
756
877
  });
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
- });
878
+ server2.delete("/_web/ai/assistants/:id", async (request, reply) => {
879
+ const result = mgr.deleteAssistant(request.params.id);
880
+ if (!result.success) return reply.code(400).send(result);
881
+ return result;
764
882
  });
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
- });
883
+ server2.post("/_web/ai/assistants/:id/reset", async (request, reply) => {
884
+ const result = mgr.resetAssistant(request.params.id);
885
+ if (!result.success) return reply.code(400).send(result);
886
+ return result;
771
887
  });
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
- });
888
+ server2.post("/_web/ai/assistants/:id/upgrade", async (request, reply) => {
889
+ if (typeof request.body?.backupName !== "string") {
890
+ return reply.code(400).send({ success: false, error: "backupName must be a string" });
779
891
  }
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
892
+ const result = mgr.upgradeAssistantWithBackup(request.params.id, request.body.backupName);
893
+ if (!result.success) return reply.code(400).send(result);
894
+ return result;
799
895
  });
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
- });
896
+ server2.post("/_web/ai/assistants/import", async (request) => {
897
+ return mgr.importAssistantFromMd(request.body.rawMd);
898
+ });
899
+ server2.post("/_web/ai/assistants/import-builtin", async (request, reply) => {
900
+ const result = mgr.importAssistant(request.body.builtinId);
901
+ if (!result.success) return reply.code(400).send(result);
902
+ return result;
903
+ });
904
+ server2.post("/_web/ai/assistants/:id/reimport", async (request, reply) => {
905
+ const result = mgr.reimportAssistant(request.params.id);
906
+ if (!result.success) return reply.code(400).send(result);
907
+ return result;
806
908
  });
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
909
  }
828
910
 
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();
911
+ // ../../packages/http-routes/src/routes/web/ai-chats.ts
912
+ function registerAiChatRoutes(server2, ctx) {
913
+ const cm = ctx.aiChatManager;
914
+ if (!cm) return;
915
+ server2.post("/_web/ai/chats", async (request) => {
916
+ const { sessionId, title, assistantId } = request.body;
917
+ return cm.createAIChat(sessionId, title, assistantId);
834
918
  });
835
- server2.patch("/_web/preferences", async (request) => {
836
- return prefManager.save(request.body);
919
+ server2.get("/_web/ai/chats", async (request) => {
920
+ const { sessionId } = request.query;
921
+ if (!sessionId) return [];
922
+ return cm.getAIChats(sessionId);
837
923
  });
838
- server2.get("/_web/preferences/ui-config", async () => {
839
- const config = loadConfig();
840
- return config.ui;
924
+ server2.get("/_web/ai/chats/:id", async (request, reply) => {
925
+ const conv = cm.getAIChat(request.params.id);
926
+ if (!conv) return reply.code(404).send({ error: "AI chat not found" });
927
+ return conv;
841
928
  });
842
- server2.patch("/_web/preferences/ui-config", async (request) => {
843
- try {
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) };
929
+ server2.put("/_web/ai/chats/:id/title", async (request) => {
930
+ return cm.updateAIChatTitle(request.params.id, request.body.title);
931
+ });
932
+ server2.delete("/_web/ai/chats/:id", async (request) => {
933
+ return cm.deleteAIChat(request.params.id);
934
+ });
935
+ server2.post("/_web/ai/chats/:id/messages", async (request) => {
936
+ const { role, content, dataKeywords, dataMessageCount, contentBlocks, tokenUsage } = request.body;
937
+ return cm.addMessage(
938
+ request.params.id,
939
+ role,
940
+ content,
941
+ dataKeywords,
942
+ dataMessageCount,
943
+ contentBlocks,
944
+ tokenUsage
945
+ );
946
+ });
947
+ server2.get("/_web/ai/chats/:id/messages", async (request) => {
948
+ return cm.getMessages(request.params.id);
949
+ });
950
+ server2.post("/_web/ai/chats/:id/messages/:messageId/delete-from", async (request, reply) => {
951
+ const { id, messageId } = request.params;
952
+ if (!id || typeof id !== "string" || !messageId || typeof messageId !== "string") {
953
+ return reply.code(400).send({ error: "aiChatId and messageId are required" });
852
954
  }
955
+ cm.deleteMessagesFrom(id, messageId);
956
+ return { success: true };
853
957
  });
854
- server2.get("/_web/preferences/locale", async () => {
855
- const config = loadConfig();
856
- return { lang: config.locale.lang };
958
+ server2.post("/_web/ai/chats/:id/fork", async (request, reply) => {
959
+ const { upToMessageId, title } = request.body;
960
+ if (!upToMessageId || typeof upToMessageId !== "string") {
961
+ return reply.code(400).send({ error: "upToMessageId is required" });
962
+ }
963
+ return cm.forkAIChat(request.params.id, upToMessageId, title);
857
964
  });
858
- server2.patch("/_web/preferences/locale", async (request) => {
965
+ server2.put("/_web/ai/messages/:messageId/content", async (request, reply) => {
966
+ const { content } = request.body;
967
+ if (!content || typeof content !== "string") {
968
+ return reply.code(400).send({ error: "content is required" });
969
+ }
970
+ cm.updateMessageContent(request.params.messageId, content);
971
+ return { success: true };
972
+ });
973
+ server2.post("/_web/ai/chats/:id/messages/:messageId/delete-relink", async (request, reply) => {
974
+ const { id, messageId } = request.params;
975
+ if (!id || !messageId) {
976
+ return reply.code(400).send({ error: "aiChatId and messageId are required" });
977
+ }
978
+ cm.deleteAndRelinkMessage(id, messageId);
979
+ return { success: true };
980
+ });
981
+ server2.post("/_web/ai/chats/:id/messages/insert-after", async (request, reply) => {
982
+ const { afterMessageId, role, content, contentBlocks, tokenUsage } = request.body;
983
+ if (!afterMessageId || typeof afterMessageId !== "string") {
984
+ return reply.code(400).send({ error: "afterMessageId is required" });
985
+ }
986
+ return cm.insertMessageAfter(request.params.id, afterMessageId, role, content, contentBlocks, tokenUsage);
987
+ });
988
+ server2.get("/_web/ai/chats/:id/token-usage", async (request) => {
989
+ return cm.getAIChatTokenUsage(request.params.id);
990
+ });
991
+ server2.get("/_web/ai/debug/schema", async () => {
992
+ return cm.getAiSchema();
993
+ });
994
+ server2.post("/_web/ai/debug/execute-sql", async (request, reply) => {
995
+ const { sql } = request.body;
996
+ if (!sql || typeof sql !== "string") {
997
+ return reply.code(400).send({ error: "sql is required" });
998
+ }
859
999
  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) };
1000
+ return cm.executeAiSQL(sql);
1001
+ } catch (e) {
1002
+ const msg = e instanceof Error ? e.message : String(e);
1003
+ return reply.code(400).send({ error: msg });
864
1004
  }
865
1005
  });
1006
+ server2.post("/_web/ai/debug/clear-debug-context", async () => {
1007
+ const cleared = cm.clearAllDebugContext();
1008
+ return { success: true, cleared };
1009
+ });
1010
+ server2.get("/_web/ai/chats/:id/estimate-tokens", async (request) => {
1011
+ const history = cm.getHistoryForAgent(request.params.id);
1012
+ const tokens = countMessagesTokens(history.map((m) => ({ role: m.role, content: m.content })));
1013
+ return { success: true, tokens, messageCount: history.length };
1014
+ });
866
1015
  }
867
1016
 
868
- // ../../packages/http-routes/src/routes/web/analytics.ts
869
- import * as fs from "fs";
870
- import * as path2 from "path";
871
-
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;
881
- }
882
-
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(",")}}`;
1017
+ // ../../packages/http-routes/src/routes/web/ai-llm-stream.ts
1018
+ function registerAiLlmStreamRoutes(server2, ctx) {
1019
+ const store = ctx.llmConfigStore;
1020
+ if (!store) return;
1021
+ server2.post("/_web/ai/llm/chat-stream", async (request, reply) => {
1022
+ const { messages, options } = request.body;
1023
+ const llmConfig = store.getDefaultAssistantConfig();
1024
+ if (!llmConfig) {
1025
+ return reply.code(400).send({ success: false, error: "LLM service not configured" });
1026
+ }
1027
+ const piModel = buildPiModel(llmConfig);
1028
+ const abortController = new AbortController();
1029
+ reply.raw.writeHead(200, {
1030
+ "Content-Type": "text/event-stream",
1031
+ "Cache-Control": "no-cache",
1032
+ Connection: "keep-alive"
1033
+ });
1034
+ reply.raw.on("close", () => {
1035
+ if (!abortController.signal.aborted) abortController.abort();
1036
+ });
1037
+ const sendChunk = (data) => {
1038
+ if (reply.raw.writableEnded || reply.raw.destroyed) return;
1039
+ reply.raw.write(`event: chunk
1040
+ data: ${JSON.stringify(data)}
1041
+
1042
+ `);
1043
+ };
1044
+ try {
1045
+ await runSimpleLlmStream({
1046
+ messages,
1047
+ apiKey: llmConfig.apiKey,
1048
+ piModel,
1049
+ temperature: options?.temperature,
1050
+ maxTokens: options?.maxTokens,
1051
+ onChunk: sendChunk,
1052
+ abortSignal: abortController.signal
1053
+ });
1054
+ } catch (error) {
1055
+ if (abortController.signal.aborted) return;
1056
+ const msg = error instanceof Error ? error.message : String(error);
1057
+ sendChunk({ content: "", isFinished: true, finishReason: "error", error: msg });
1058
+ } finally {
1059
+ if (!reply.raw.writableEnded && !reply.raw.destroyed) reply.raw.end();
1060
+ }
1061
+ });
892
1062
  }
893
- function buildAnalyticsCacheKey(namespace, params) {
894
- return `${namespace}:${canonical(params)}`;
1063
+
1064
+ // ../../packages/http-routes/src/routes/web/ai-llm.ts
1065
+ function hasRealApiKey(apiKey) {
1066
+ return !!apiKey && apiKey !== "sk-no-key-required";
895
1067
  }
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);
1068
+ function toConfigDisplay(config) {
1069
+ const { apiKey: _raw, ...rest } = config;
1070
+ return { ...rest, apiKey: "", apiKeySet: hasRealApiKey(String(_raw || "")) };
903
1071
  }
904
-
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);
1072
+ function registerAiLlmRoutes(server2, ctx) {
1073
+ const store = ctx.llmConfigStore;
1074
+ const aiDataDir = ctx.aiDataDir;
1075
+ if (!store || !aiDataDir) return;
1076
+ server2.get("/_web/ai/llm/has-config", async () => {
1077
+ return store.hasActiveConfig();
912
1078
  });
913
- server2.get("/_web/sessions/:id/time-range", async (request) => {
914
- const db = adapter.ensureReadonly(request.params.id);
915
- return getTimeRange(db);
1079
+ server2.get("/_web/ai/llm/configs", async () => {
1080
+ const data = store.loadStore();
1081
+ return {
1082
+ configs: data.configs.map((c) => toConfigDisplay(c)),
1083
+ defaultAssistant: data.defaultAssistant,
1084
+ fastModel: data.fastModel
1085
+ };
916
1086
  });
917
- server2.get(
918
- "/_web/sessions/:id/stats/member-activity",
919
- async (request) => {
920
- const id = request.params.id;
921
- const filter = parseTimeFilter(request.query);
922
- return cached("member-activity", id, { ...filter }, () => getMemberActivity(adapter.ensureReadonly(id), filter));
923
- }
924
- );
925
- server2.get(
926
- "/_web/sessions/:id/stats/hourly",
927
- async (request) => {
928
- const id = request.params.id;
929
- const filter = parseTimeFilter(request.query);
930
- return cached("hourly", id, { ...filter }, () => getHourlyActivity(adapter.ensureReadonly(id), filter));
931
- }
932
- );
933
- server2.get(
934
- "/_web/sessions/:id/stats/daily",
935
- async (request) => {
936
- const id = request.params.id;
937
- const filter = parseTimeFilter(request.query);
938
- return cached("daily", id, { ...filter }, () => getDailyActivity(adapter.ensureReadonly(id), filter));
939
- }
940
- );
941
- server2.get(
942
- "/_web/sessions/:id/stats/weekday",
943
- async (request) => {
944
- const id = request.params.id;
945
- const filter = parseTimeFilter(request.query);
946
- return cached("weekday", id, { ...filter }, () => getWeekdayActivity(adapter.ensureReadonly(id), filter));
1087
+ server2.post("/_web/ai/llm/configs", async (request) => {
1088
+ const result = store.addConfig(request.body);
1089
+ if (result.success && result.config) {
1090
+ return { ...result, config: toConfigDisplay(result.config) };
947
1091
  }
948
- );
949
- server2.get(
950
- "/_web/sessions/:id/stats/message-types",
951
- async (request) => {
952
- const id = request.params.id;
953
- const filter = parseTimeFilter(request.query);
954
- return cached("message-types", id, { ...filter }, () => getMessageTypeStats(adapter.ensureReadonly(id), filter));
1092
+ return result;
1093
+ });
1094
+ server2.put("/_web/ai/llm/configs/:id", async (request) => {
1095
+ return store.updateConfig(request.params.id, request.body);
1096
+ });
1097
+ server2.delete("/_web/ai/llm/configs/:id", async (request) => {
1098
+ return store.deleteConfig(request.params.id);
1099
+ });
1100
+ server2.get("/_web/ai/llm/default-assistant-slot", async () => {
1101
+ return store.getDefaultAssistantSlot();
1102
+ });
1103
+ server2.put("/_web/ai/llm/default-assistant-slot", async (request) => {
1104
+ return store.setDefaultAssistantModel(request.body.configId, request.body.modelId);
1105
+ });
1106
+ server2.get("/_web/ai/llm/fast-model-slot", async () => {
1107
+ return store.getFastModelSlot();
1108
+ });
1109
+ server2.put("/_web/ai/llm/fast-model-slot", async (request) => {
1110
+ return store.setFastModel(request.body);
1111
+ });
1112
+ server2.get("/_web/ai/llm/providers", async () => {
1113
+ return BUILTIN_PROVIDERS.map((p) => {
1114
+ const models = getBuiltinModelsByProvider(p.id);
1115
+ return {
1116
+ id: p.id,
1117
+ name: p.name,
1118
+ defaultBaseUrl: p.defaultBaseUrl,
1119
+ models: models.filter((m) => !m.capabilities.includes("embedding") && !m.capabilities.includes("ranking")).map((m) => ({ id: m.id, name: m.name, description: m.description }))
1120
+ };
1121
+ });
1122
+ });
1123
+ server2.get("/_web/ai/llm/provider-registry", async () => {
1124
+ const custom = ctx.customProviderStore?.getAll() ?? [];
1125
+ return [...BUILTIN_PROVIDERS, ...custom];
1126
+ });
1127
+ server2.get("/_web/ai/llm/model-catalog", async () => {
1128
+ const custom = ctx.customModelStore?.getAll() ?? [];
1129
+ return [...BUILTIN_MODELS, ...custom];
1130
+ });
1131
+ server2.post("/_web/ai/llm/custom-providers", async (request, reply) => {
1132
+ if (!ctx.customProviderStore)
1133
+ return reply.code(501).send({ success: false, error: "Custom providers not available" });
1134
+ return ctx.customProviderStore.add(request.body);
1135
+ });
1136
+ server2.put("/_web/ai/llm/custom-providers/:id", async (request, reply) => {
1137
+ if (!ctx.customProviderStore)
1138
+ return reply.code(501).send({ success: false, error: "Custom providers not available" });
1139
+ const result = ctx.customProviderStore.update(request.params.id, request.body);
1140
+ if (!result.success) return reply.code(404).send(result);
1141
+ return result;
1142
+ });
1143
+ server2.delete("/_web/ai/llm/custom-providers/:id", async (request, reply) => {
1144
+ if (!ctx.customProviderStore)
1145
+ return reply.code(501).send({ success: false, error: "Custom providers not available" });
1146
+ const result = ctx.customProviderStore.delete(request.params.id);
1147
+ if (!result.success) return reply.code(404).send(result);
1148
+ return result;
1149
+ });
1150
+ server2.post("/_web/ai/llm/custom-models", async (request, reply) => {
1151
+ if (!ctx.customModelStore) return reply.code(501).send({ success: false, error: "Custom models not available" });
1152
+ const result = ctx.customModelStore.add(request.body);
1153
+ if (!result.success) return reply.code(409).send(result);
1154
+ return result;
1155
+ });
1156
+ server2.put("/_web/ai/llm/custom-models/:providerId/:modelId", async (request, reply) => {
1157
+ if (!ctx.customModelStore) return reply.code(501).send({ success: false, error: "Custom models not available" });
1158
+ const result = ctx.customModelStore.update(request.params.providerId, request.params.modelId, request.body);
1159
+ if (!result.success) return reply.code(404).send(result);
1160
+ return result;
1161
+ });
1162
+ server2.delete("/_web/ai/llm/custom-models/:providerId/:modelId", async (request, reply) => {
1163
+ if (!ctx.customModelStore) return reply.code(501).send({ success: false, error: "Custom models not available" });
1164
+ const result = ctx.customModelStore.delete(request.params.providerId, request.params.modelId);
1165
+ if (!result.success) return reply.code(404).send(result);
1166
+ return result;
1167
+ });
1168
+ server2.post("/_web/ai/llm/validate-key", async (request) => {
1169
+ const { provider, apiKey, baseUrl, model, apiFormat, configId } = request.body;
1170
+ const resolvedKey = apiKey?.trim() ? apiKey : configId ? store.getConfigById(configId)?.apiKey || "" : "";
1171
+ return validateApiKey(provider, resolvedKey, baseUrl, model, apiFormat);
1172
+ });
1173
+ server2.post("/_web/ai/llm/remote-models", async (request) => {
1174
+ const { provider, apiKey, baseUrl, apiFormat, configId } = request.body;
1175
+ const resolvedKey = apiKey?.trim() ? apiKey : configId ? store.getConfigById(configId)?.apiKey || "" : "";
1176
+ return fetchRemoteModels(provider, resolvedKey, baseUrl, apiFormat);
1177
+ });
1178
+ server2.get("/_web/ai/desensitize-rules/defaults", async (request) => {
1179
+ return getDefaultRulesForLocale(request.query.locale ?? "zh-CN");
1180
+ });
1181
+ server2.post("/_web/ai/desensitize-rules/merge", async (request) => {
1182
+ return mergeRulesForLocale(request.body.existingRules, request.body.locale, request.body.overrides ?? {});
1183
+ });
1184
+ }
1185
+
1186
+ // ../../packages/http-routes/src/routes/web/ai-logs.ts
1187
+ import fs from "fs";
1188
+ import path from "path";
1189
+ function findLatestAiLogPath(logsDir) {
1190
+ const aiLogDir = path.join(logsDir, "ai");
1191
+ if (!fs.existsSync(aiLogDir)) return null;
1192
+ const latest = fs.readdirSync(aiLogDir).filter((name) => name.startsWith("ai_") && name.endsWith(".log")).map((name) => {
1193
+ const filePath = path.join(aiLogDir, name);
1194
+ const stat2 = fs.statSync(filePath);
1195
+ return { path: filePath, mtimeMs: stat2.mtimeMs };
1196
+ }).sort((a, b) => b.mtimeMs - a.mtimeMs)[0];
1197
+ return latest?.path ?? null;
1198
+ }
1199
+ function resolveAiLogPath(ctx) {
1200
+ const currentPath = ctx.getCurrentAiLogPath?.();
1201
+ if (currentPath && fs.existsSync(currentPath)) return currentPath;
1202
+ return findLatestAiLogPath(ctx.pathProvider.getLogsDir());
1203
+ }
1204
+ function isLoopbackAddress(address) {
1205
+ if (!address) return true;
1206
+ const normalized = address.replace(/^::ffff:/, "");
1207
+ return normalized === "localhost" || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1" || normalized.startsWith("127.");
1208
+ }
1209
+ function registerAiLogRoutes(server2, ctx) {
1210
+ server2.post("/_web/ai/logs/show", async (request, reply) => {
1211
+ if (!ctx.showInFolder) return reply.code(501).send({ success: false, error: "Not supported" });
1212
+ if (!isLoopbackAddress(request.ip)) {
1213
+ return { success: false, error: "Opening AI logs is only supported on this device" };
955
1214
  }
956
- );
957
- server2.get(
958
- "/_web/sessions/:id/analytics/relationship",
959
- async (request) => {
960
- const id = request.params.id;
961
- const filter = parseTimeFilter(request.query);
962
- return cached("relationship", id, { ...filter }, () => getRelationshipStats(adapter.ensureReadonly(id), filter));
1215
+ const logPath = resolveAiLogPath(ctx);
1216
+ if (!logPath) return { success: false, error: "No AI log files found" };
1217
+ try {
1218
+ await ctx.showInFolder(logPath);
1219
+ return { success: true, path: logPath };
1220
+ } catch (error) {
1221
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
963
1222
  }
964
- );
965
- server2.get(
966
- "/_web/sessions/:id/analytics/catchphrase",
967
- async (request) => {
968
- const id = request.params.id;
969
- const filter = parseTimeFilter(request.query);
970
- return cached("catchphrase", id, { ...filter }, () => getCatchphraseAnalysis(adapter.ensureReadonly(id), filter));
971
- }
972
- );
973
- server2.get(
974
- "/_web/sessions/:id/analytics/mention",
975
- async (request) => {
976
- const id = request.params.id;
977
- const filter = parseTimeFilter(request.query);
978
- return cached("mention", id, { ...filter }, () => getMentionAnalysis(adapter.ensureReadonly(id), filter));
979
- }
980
- );
981
- server2.get(
982
- "/_web/sessions/:id/analytics/mention-graph",
983
- async (request) => {
984
- const id = request.params.id;
985
- const filter = parseTimeFilter(request.query);
986
- return cached("mention-graph", id, { ...filter }, () => getMentionGraph(adapter.ensureReadonly(id), filter));
987
- }
988
- );
989
- server2.get(
990
- "/_web/sessions/:id/analytics/laugh",
991
- async (request) => {
992
- const id = request.params.id;
993
- const filter = parseTimeFilter(request.query);
994
- return cached("laugh", id, { ...filter }, () => getLaughAnalysis(adapter.ensureReadonly(id), filter));
995
- }
996
- );
997
- server2.get("/_web/sessions/:id/analytics/cluster", async (request) => {
998
- const id = request.params.id;
999
- const filter = parseTimeFilter(request.query);
1000
- const options = {};
1001
- if (request.query.topEdges) options.topEdges = parseInt(request.query.topEdges, 10);
1002
- if (request.query.lookAhead) options.lookAhead = parseInt(request.query.lookAhead, 10);
1003
- if (request.query.decaySeconds) options.decaySeconds = parseInt(request.query.decaySeconds, 10);
1004
- const hasOptions = Object.keys(options).length > 0;
1005
- return cached(
1006
- "cluster",
1007
- id,
1008
- { ...filter, ...options },
1009
- () => getClusterGraph(adapter.ensureReadonly(id), filter, hasOptions ? options : void 0)
1010
- );
1011
1223
  });
1012
- server2.get(
1013
- "/_web/sessions/:id/analytics/language-preference",
1014
- async (request) => {
1015
- const id = request.params.id;
1016
- const filter = parseTimeFilter(request.query);
1017
- const locale = request.query.locale || "zh-CN";
1018
- const zhDictPath = path2.join(ctx.pathProvider.getSystemDir(), "nlp", "zh-CN.dict");
1019
- let zhDictVersion;
1020
- try {
1021
- const st = fs.statSync(zhDictPath);
1022
- zhDictVersion = `${Math.floor(st.mtimeMs)}:${st.size}`;
1023
- } catch {
1024
- zhDictVersion = "-";
1025
- }
1026
- return cached(
1027
- "language-preference",
1028
- id,
1029
- { ...filter, locale },
1030
- () => getLanguagePreferenceAnalysis(adapter.ensureReadonly(id), {
1031
- locale,
1032
- timeFilter: filter,
1033
- nlpProvider: createJiebaNlpProvider()
1034
- }),
1035
- { extraVersion: `dict:${zhDictVersion}` }
1224
+ }
1225
+
1226
+ // ../../packages/http-routes/src/routes/web/ai-semantic-index.ts
1227
+ import path2 from "path";
1228
+ function registerSemanticIndexRoutes(server2, ctx) {
1229
+ const service = ctx.semanticIndexService;
1230
+ if (!service) {
1231
+ if (ctx.aiDataDir) {
1232
+ const configStore = new SemanticIndexConfigStore(path2.join(ctx.aiDataDir, SEMANTIC_INDEX_CONFIG_FILE));
1233
+ const configResponse = (config) => ({
1234
+ config,
1235
+ apiKeySet: resolveSemanticIndexApiKeySet(config, ctx.resolveApiKey),
1236
+ configured: isSemanticIndexConfigured(config)
1237
+ });
1238
+ server2.get("/_web/ai/semantic-index/config", async () => ({
1239
+ ...configResponse(configStore.get()),
1240
+ modelStatus: "idle"
1241
+ }));
1242
+ server2.put(
1243
+ "/_web/ai/semantic-index/config",
1244
+ async (request) => {
1245
+ const config = persistSemanticIndexConfig(configStore, request.body.config, {
1246
+ apiKey: request.body.apiKey,
1247
+ writeAuthProfile: ctx.writeAuthProfile
1248
+ });
1249
+ return { ...configResponse(config), modelStatus: "idle" };
1250
+ }
1036
1251
  );
1037
1252
  }
1038
- );
1039
- server2.get(
1040
- "/_web/sessions/:id/analytics/monthly-activity",
1253
+ server2.get("/_web/ai/semantic-index/enabled", async () => ({ sessions: [] }));
1254
+ server2.get("/_web/ai/semantic-index/status", async () => ({ status: null }));
1255
+ server2.post("/_web/ai/semantic-index/status", async () => ({ sessions: [] }));
1256
+ return;
1257
+ }
1258
+ server2.get("/_web/ai/semantic-index/config", async () => {
1259
+ return {
1260
+ config: await service.getConfig(),
1261
+ apiKeySet: await service.hasApiKey(),
1262
+ configured: await service.isConfigured(),
1263
+ modelStatus: await service.getModelStatus()
1264
+ };
1265
+ });
1266
+ server2.put(
1267
+ "/_web/ai/semantic-index/config",
1041
1268
  async (request) => {
1042
- const id = request.params.id;
1043
- const filter = parseTimeFilter(request.query);
1044
- return cached("monthly-activity", id, { ...filter }, () => getMonthlyActivity(adapter.ensureReadonly(id), filter));
1269
+ const config = await service.setConfig(request.body.config, { apiKey: request.body.apiKey });
1270
+ return {
1271
+ config,
1272
+ apiKeySet: await service.hasApiKey(),
1273
+ configured: await service.isConfigured(),
1274
+ modelStatus: await service.getModelStatus()
1275
+ };
1045
1276
  }
1046
1277
  );
1047
- server2.get(
1048
- "/_web/sessions/:id/analytics/yearly-activity",
1049
- async (request) => {
1050
- const id = request.params.id;
1051
- const filter = parseTimeFilter(request.query);
1052
- return cached("yearly-activity", id, { ...filter }, () => getYearlyActivity(adapter.ensureReadonly(id), filter));
1278
+ server2.get("/_web/ai/semantic-index/enabled", async () => {
1279
+ return { sessions: await service.listEnabledStatuses() };
1280
+ });
1281
+ server2.get("/_web/ai/semantic-index/status", async (request) => {
1282
+ return { status: await service.status(request.query.sessionId) };
1283
+ });
1284
+ server2.post("/_web/ai/semantic-index/status", async (request) => {
1285
+ return { sessions: await service.statusForSessions(request.body.sessionIds ?? []) };
1286
+ });
1287
+ const sessionAction = (path13, action) => {
1288
+ server2.post(path13, async (request, reply) => {
1289
+ const { sessionId } = request.body;
1290
+ if (!sessionId) return reply.code(400).send({ error: "sessionId is required" });
1291
+ await action(sessionId);
1292
+ return { status: await service.status(sessionId) };
1293
+ });
1294
+ };
1295
+ sessionAction("/_web/ai/semantic-index/enable", (id) => service.enable(id));
1296
+ sessionAction("/_web/ai/semantic-index/remove", (id) => service.remove(id));
1297
+ sessionAction("/_web/ai/semantic-index/build", (id) => service.build(id));
1298
+ sessionAction("/_web/ai/semantic-index/pause", (id) => service.pause(id));
1299
+ sessionAction("/_web/ai/semantic-index/cancel", (id) => service.cancel(id));
1300
+ sessionAction("/_web/ai/semantic-index/rebuild", (id) => service.rebuild(id));
1301
+ server2.post("/_web/ai/semantic-index/build-pending", async () => {
1302
+ await service.buildAllPending();
1303
+ return { sessions: await service.listEnabledStatuses() };
1304
+ });
1305
+ server2.post("/_web/ai/semantic-index/cleanup", async () => {
1306
+ return await service.cleanupUnused();
1307
+ });
1308
+ server2.post(
1309
+ "/_web/ai/semantic-index/search",
1310
+ async (request, reply) => {
1311
+ const { sessionId, query, finalTopK } = request.body;
1312
+ if (!sessionId || !query) return reply.code(400).send({ error: "sessionId and query are required" });
1313
+ return service.search(sessionId, query, { finalTopK });
1053
1314
  }
1054
1315
  );
1055
- server2.get(
1056
- "/_web/sessions/:id/analytics/message-length-distribution",
1057
- async (request) => {
1058
- const id = request.params.id;
1059
- const filter = parseTimeFilter(request.query);
1060
- return cached(
1061
- "message-length-distribution",
1062
- id,
1063
- { ...filter },
1064
- () => getMessageLengthDistribution(adapter.ensureReadonly(id), filter)
1065
- );
1316
+ }
1317
+
1318
+ // ../../packages/http-routes/src/routes/web/ai-skills.ts
1319
+ function registerAiSkillRoutes(server2, ctx) {
1320
+ const mgr = ctx.skillManagerCore;
1321
+ if (!mgr) return;
1322
+ server2.get("/_web/ai/skills", async () => {
1323
+ return mgr.getAllSkills();
1324
+ });
1325
+ server2.get("/_web/ai/skills/:id", async (request, reply) => {
1326
+ const config = mgr.getSkillConfig(request.params.id);
1327
+ if (!config) return reply.code(404).send({ error: "Not found" });
1328
+ return config;
1329
+ });
1330
+ server2.post("/_web/ai/skills", async (request) => {
1331
+ return mgr.createSkill(request.body.rawMd);
1332
+ });
1333
+ server2.put("/_web/ai/skills/:id", async (request, reply) => {
1334
+ const result = mgr.updateSkill(request.params.id, request.body.rawMd);
1335
+ if (!result.success) return reply.code(404).send(result);
1336
+ return result;
1337
+ });
1338
+ server2.delete("/_web/ai/skills/:id", async (request, reply) => {
1339
+ const result = mgr.deleteSkill(request.params.id);
1340
+ if (!result.success) return reply.code(400).send(result);
1341
+ return result;
1342
+ });
1343
+ server2.post("/_web/ai/skills/import", async (request) => {
1344
+ return mgr.importSkillFromMd(request.body.rawMd);
1345
+ });
1346
+ server2.get("/_web/ai/skills/builtin-catalog", async () => {
1347
+ return mgr.getBuiltinCatalog();
1348
+ });
1349
+ server2.post("/_web/ai/skills/import-builtin", async (request, reply) => {
1350
+ const result = mgr.importSkill(request.body.builtinId);
1351
+ if (!result.success) return reply.code(400).send(result);
1352
+ return result;
1353
+ });
1354
+ server2.post("/_web/ai/skills/:id/reimport", async (request, reply) => {
1355
+ const result = mgr.reimportSkill(request.params.id);
1356
+ if (!result.success) return reply.code(400).send(result);
1357
+ return result;
1358
+ });
1359
+ }
1360
+
1361
+ // ../../packages/http-routes/src/routes/web/ai-summaries.ts
1362
+ function createSummaryDeps(ctx) {
1363
+ const store = ctx.llmConfigStore;
1364
+ if (!store) return null;
1365
+ return {
1366
+ getLlmConfig() {
1367
+ const config = store.getDefaultAssistantConfig();
1368
+ if (!config) return null;
1369
+ return config;
1370
+ },
1371
+ buildPiModel(config) {
1372
+ return buildPiModel(config);
1066
1373
  }
1067
- );
1068
- server2.get(
1069
- "/_web/sessions/:id/analytics/text-stats",
1070
- async (request) => {
1071
- const id = request.params.id;
1072
- const filter = parseTimeFilter(request.query);
1073
- return cached("text-stats", id, { ...filter }, () => getTextStats(adapter.ensureReadonly(id), filter));
1374
+ };
1375
+ }
1376
+ function registerAiSummaryRoutes(server2, ctx) {
1377
+ const deps = createSummaryDeps(ctx);
1378
+ if (!deps) return;
1379
+ const { sessionAdapter: adapter } = ctx;
1380
+ server2.post("/_web/sessions/:id/summaries/generate", async (request, reply) => {
1381
+ const { segmentId, locale, forceRegenerate, strategy } = request.body;
1382
+ const result = await summary_service_exports.generateSummary(adapter, request.params.id, segmentId, deps, {
1383
+ locale,
1384
+ forceRegenerate,
1385
+ strategy
1386
+ });
1387
+ if ("error" in result && !result.success) {
1388
+ return reply.code(400).send({ error: result.error });
1074
1389
  }
1075
- );
1076
- server2.get(
1077
- "/_web/sessions/:id/analytics/long-message-count",
1078
- async (request) => {
1079
- const id = request.params.id;
1080
- const filter = parseTimeFilter(request.query);
1081
- const minLength = request.query.minLength ? parseInt(request.query.minLength, 10) : void 0;
1082
- return cached(
1083
- "long-message-count",
1084
- id,
1085
- { ...filter, minLength },
1086
- () => getLongMessageCount(adapter.ensureReadonly(id), filter, minLength)
1390
+ return result;
1391
+ });
1392
+ server2.post("/_web/sessions/:id/summaries/generate-all", async (request, reply) => {
1393
+ const { locale, forceRegenerate } = request.body;
1394
+ const result = await summary_service_exports.generateAllSummaries(adapter, request.params.id, deps, {
1395
+ locale,
1396
+ forceRegenerate
1397
+ });
1398
+ if (result.error) {
1399
+ return reply.code(400).send({ error: result.error });
1400
+ }
1401
+ return result;
1402
+ });
1403
+ server2.post("/_web/sessions/:id/summaries/check-can-generate", async (request) => {
1404
+ const { segmentIds } = request.body;
1405
+ return summary_service_exports.checkCanGenerate(adapter, request.params.id, segmentIds);
1406
+ });
1407
+ }
1408
+
1409
+ // ../../packages/http-routes/src/ai/tool-executor.ts
1410
+ var MAX_RESULT_CHARS = 5e5;
1411
+ async function executeRegistryTool(params, deps) {
1412
+ const { toolName, params: toolParams, abortSignal } = params;
1413
+ const entry = AGENT_TOOL_REGISTRY.find((tool) => tool.name === toolName);
1414
+ if (!entry) {
1415
+ return { success: false, error: `Tool not found: ${toolName}` };
1416
+ }
1417
+ try {
1418
+ if (abortSignal.aborted) return { success: false, error: "cancelled" };
1419
+ const execCtx = {
1420
+ sessionId: params.sessionId,
1421
+ abortSignal,
1422
+ db: deps.db,
1423
+ dataProvider: deps.dataProvider,
1424
+ semanticIndexService: deps.semanticIndexService,
1425
+ segmentText: deps.segmentText,
1426
+ translateTemplate: deps.translateTemplate
1427
+ };
1428
+ const startTime = Date.now();
1429
+ const result = await entry.handler(toolParams, execCtx);
1430
+ const elapsed = Date.now() - startTime;
1431
+ if (abortSignal.aborted) return { success: false, error: "cancelled" };
1432
+ let details = result.data ?? void 0;
1433
+ let truncated = false;
1434
+ if (details) {
1435
+ stripAvatarFields(details);
1436
+ const raw = JSON.stringify(details);
1437
+ if (raw.length > MAX_RESULT_CHARS) {
1438
+ truncated = true;
1439
+ details = { _truncated: true, _originalSize: raw.length, _preview: raw.slice(0, MAX_RESULT_CHARS) };
1440
+ }
1441
+ }
1442
+ return {
1443
+ success: true,
1444
+ elapsed,
1445
+ content: [{ type: "text", text: result.content }],
1446
+ details,
1447
+ truncated
1448
+ };
1449
+ } catch (error) {
1450
+ if (abortSignal.aborted) {
1451
+ return { success: false, error: "cancelled" };
1452
+ }
1453
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
1454
+ }
1455
+ }
1456
+
1457
+ // ../../packages/http-routes/src/routes/web/ai-tools.ts
1458
+ var activeToolTests = /* @__PURE__ */ new Map();
1459
+ function registerAiToolRoutes(server2, ctx) {
1460
+ server2.get("/_web/ai/tools/full-catalog", async () => {
1461
+ return AGENT_TOOL_REGISTRY.map((tool) => ({
1462
+ name: tool.name,
1463
+ category: tool.category ?? "core",
1464
+ description: tool.description,
1465
+ parameters: tool.inputSchema ?? {}
1466
+ }));
1467
+ });
1468
+ server2.post("/_web/ai/tools/execute", async (request, reply) => {
1469
+ const { testId, toolName, params, sessionId } = request.body;
1470
+ const entry = AGENT_TOOL_REGISTRY.find((t) => t.name === toolName);
1471
+ if (!entry) {
1472
+ return reply.code(404).send({ success: false, error: `Tool not found: ${toolName}` });
1473
+ }
1474
+ const abortController = new AbortController();
1475
+ activeToolTests.set(testId, abortController);
1476
+ try {
1477
+ if (ctx.executeAiTool) {
1478
+ return await ctx.executeAiTool({ testId, toolName, params, sessionId, abortSignal: abortController.signal });
1479
+ }
1480
+ const db = ctx.dbManager.open(sessionId);
1481
+ if (!db) {
1482
+ return reply.code(404).send({ success: false, error: `Session not found: ${sessionId}` });
1483
+ }
1484
+ const result = await executeRegistryTool(
1485
+ { testId, toolName, params, sessionId, abortSignal: abortController.signal },
1486
+ {
1487
+ db,
1488
+ dataProvider: new CoreDataProvider(db)
1489
+ }
1087
1490
  );
1491
+ if (!result.success && result.error !== "cancelled") {
1492
+ appLogger.error("ai-tools", `Failed to execute tool ${toolName}`, { error: result.error });
1493
+ }
1494
+ return result;
1495
+ } catch (error) {
1496
+ if (abortController.signal.aborted) {
1497
+ return { success: false, error: "cancelled" };
1498
+ }
1499
+ appLogger.error("ai-tools", `Failed to execute tool ${toolName}`, error);
1500
+ return { success: false, error: String(error) };
1501
+ } finally {
1502
+ activeToolTests.delete(testId);
1503
+ }
1504
+ });
1505
+ server2.post("/_web/ai/tools/cancel", async (request) => {
1506
+ const { testId } = request.body;
1507
+ const controller = activeToolTests.get(testId);
1508
+ if (controller) {
1509
+ controller.abort();
1510
+ activeToolTests.delete(testId);
1511
+ return { success: true };
1512
+ }
1513
+ return { success: false };
1514
+ });
1515
+ }
1516
+
1517
+ // ../../packages/http-routes/src/routes/web/register-ai.ts
1518
+ var REQUIRED_AI_DEPENDENCIES = [
1519
+ "aiDataDir",
1520
+ "aiChatManager",
1521
+ "assistantManager",
1522
+ "skillManagerCore",
1523
+ "llmConfigStore",
1524
+ "customProviderStore",
1525
+ "customModelStore",
1526
+ "runAgentStream"
1527
+ ];
1528
+ function assertRequiredAiContext(ctx) {
1529
+ const missing = REQUIRED_AI_DEPENDENCIES.filter((key) => !ctx[key]);
1530
+ if (missing.length > 0) {
1531
+ throw new Error(`[http-routes] requireAi is set but missing AI dependencies: ${missing.join(", ")}`);
1532
+ }
1533
+ }
1534
+ function registerAiRoutes(server2, ctx, options) {
1535
+ if (options?.requireAi) assertRequiredAiContext(ctx);
1536
+ registerAiAssistantRoutes(server2, ctx);
1537
+ registerAiSkillRoutes(server2, ctx);
1538
+ registerAiLlmRoutes(server2, ctx);
1539
+ registerAiLlmStreamRoutes(server2, ctx);
1540
+ registerAiAgentStreamRoutes(server2, ctx);
1541
+ registerAiToolRoutes(server2, ctx);
1542
+ registerAiChatRoutes(server2, ctx);
1543
+ registerAiSummaryRoutes(server2, ctx);
1544
+ registerAiLogRoutes(server2, ctx);
1545
+ registerSemanticIndexRoutes(server2, ctx);
1546
+ }
1547
+
1548
+ // ../../packages/http-routes/src/routes/web/analytics.ts
1549
+ import * as fs2 from "fs";
1550
+ import * as path4 from "path";
1551
+
1552
+ // ../../packages/http-routes/src/routes/web/time-filter.ts
1553
+ function parseTimeFilter(query) {
1554
+ const { startTs, endTs, memberId } = query;
1555
+ if (!startTs && !endTs && !memberId) return void 0;
1556
+ const filter = {};
1557
+ if (startTs) filter.startTs = parseInt(startTs, 10);
1558
+ if (endTs) filter.endTs = parseInt(endTs, 10);
1559
+ if (memberId) filter.memberId = parseInt(memberId, 10);
1560
+ return filter;
1561
+ }
1562
+
1563
+ // ../../packages/http-routes/src/routes/web/analytics-cache.ts
1564
+ import * as path3 from "path";
1565
+ function canonical(value) {
1566
+ if (value === void 0) return "null";
1567
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
1568
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
1569
+ const obj = value;
1570
+ const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
1571
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonical(obj[k])}`).join(",")}}`;
1572
+ }
1573
+ function buildAnalyticsCacheKey(namespace, params) {
1574
+ return `${namespace}:${canonical(params)}`;
1575
+ }
1576
+ function withAnalyticsCache(ctx, sessionId, namespace, params, compute, options) {
1577
+ const queryCacheDir = path3.join(ctx.pathProvider.getCacheDir(), "query");
1578
+ const dateStr = options?.dailyInvalidate ? `|date:${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}` : "";
1579
+ const extraStr = options?.extraVersion ? `|${options.extraVersion}` : "";
1580
+ const version = `${ctx.getVersion()}|${getDbFileVersion(ctx.sessionAdapter.getDbPath(sessionId))}${dateStr}${extraStr}`;
1581
+ const key = buildAnalyticsCacheKey(namespace, params);
1582
+ return getOrComputeAnalysisCache(sessionId, key, queryCacheDir, version, compute);
1583
+ }
1584
+
1585
+ // ../../packages/http-routes/src/routes/web/analytics.ts
1586
+ function registerAnalyticsRoutes(server2, ctx) {
1587
+ const { sessionAdapter: adapter } = ctx;
1588
+ const cached = (name, sessionId, params, compute, options) => withAnalyticsCache(ctx, sessionId, `analytics.${name}`, params, compute, options);
1589
+ server2.get("/_web/sessions/:id/years", async (request) => {
1590
+ const db = adapter.ensureReadonly(request.params.id);
1591
+ return getAvailableYears(db);
1592
+ });
1593
+ server2.get("/_web/sessions/:id/time-range", async (request) => {
1594
+ const db = adapter.ensureReadonly(request.params.id);
1595
+ return getTimeRange(db);
1596
+ });
1597
+ server2.get(
1598
+ "/_web/sessions/:id/stats/member-activity",
1599
+ async (request) => {
1600
+ const id = request.params.id;
1601
+ const filter = parseTimeFilter(request.query);
1602
+ return cached("member-activity", id, { ...filter }, () => getMemberActivity(adapter.ensureReadonly(id), filter));
1088
1603
  }
1089
1604
  );
1090
1605
  server2.get(
1091
- "/_web/sessions/:id/analytics/member-monthly-trend",
1606
+ "/_web/sessions/:id/stats/hourly",
1092
1607
  async (request) => {
1093
1608
  const id = request.params.id;
1094
1609
  const filter = parseTimeFilter(request.query);
1095
- return cached(
1096
- "member-monthly-trend",
1097
- id,
1098
- { ...filter },
1099
- () => getMemberMonthlyTrend(adapter.ensureReadonly(id), filter)
1100
- );
1610
+ return cached("hourly", id, { ...filter }, () => getHourlyActivity(adapter.ensureReadonly(id), filter));
1101
1611
  }
1102
1612
  );
1103
1613
  server2.get(
1104
- "/_web/sessions/:id/analytics/text-length-percentiles",
1614
+ "/_web/sessions/:id/stats/daily",
1105
1615
  async (request) => {
1106
1616
  const id = request.params.id;
1107
1617
  const filter = parseTimeFilter(request.query);
1108
- return cached(
1109
- "text-length-percentiles",
1110
- id,
1111
- { ...filter },
1112
- () => getTextLengthPercentiles(adapter.ensureReadonly(id), filter)
1113
- );
1618
+ return cached("daily", id, { ...filter }, () => getDailyActivity(adapter.ensureReadonly(id), filter));
1114
1619
  }
1115
1620
  );
1116
1621
  server2.get(
1117
- "/_web/sessions/:id/analytics/dragon-king",
1622
+ "/_web/sessions/:id/stats/weekday",
1118
1623
  async (request) => {
1119
1624
  const id = request.params.id;
1120
1625
  const filter = parseTimeFilter(request.query);
1121
- return cached("dragon-king", id, { ...filter }, () => getDragonKingAnalysis(adapter.ensureReadonly(id), filter));
1626
+ return cached("weekday", id, { ...filter }, () => getWeekdayActivity(adapter.ensureReadonly(id), filter));
1122
1627
  }
1123
1628
  );
1124
1629
  server2.get(
1125
- "/_web/sessions/:id/analytics/diving",
1630
+ "/_web/sessions/:id/stats/message-types",
1126
1631
  async (request) => {
1127
1632
  const id = request.params.id;
1128
1633
  const filter = parseTimeFilter(request.query);
1129
- return cached("diving", id, { ...filter }, () => getDivingAnalysis(adapter.ensureReadonly(id), filter), {
1130
- dailyInvalidate: true
1131
- });
1634
+ return cached("message-types", id, { ...filter }, () => getMessageTypeStats(adapter.ensureReadonly(id), filter));
1132
1635
  }
1133
1636
  );
1134
1637
  server2.get(
1135
- "/_web/sessions/:id/analytics/check-in",
1638
+ "/_web/sessions/:id/analytics/relationship",
1136
1639
  async (request) => {
1137
1640
  const id = request.params.id;
1138
1641
  const filter = parseTimeFilter(request.query);
1139
- return cached("check-in", id, { ...filter }, () => getCheckInAnalysis(adapter.ensureReadonly(id), filter));
1642
+ return cached("relationship", id, { ...filter }, () => getRelationshipStats(adapter.ensureReadonly(id), filter));
1140
1643
  }
1141
1644
  );
1142
1645
  server2.get(
1143
- "/_web/sessions/:id/analytics/meme-battle",
1646
+ "/_web/sessions/:id/analytics/catchphrase",
1144
1647
  async (request) => {
1145
1648
  const id = request.params.id;
1146
1649
  const filter = parseTimeFilter(request.query);
1147
- return cached("meme-battle", id, { ...filter }, () => getMemeBattleAnalysis(adapter.ensureReadonly(id), filter));
1650
+ return cached("catchphrase", id, { ...filter }, () => getCatchphraseAnalysis(adapter.ensureReadonly(id), filter));
1148
1651
  }
1149
1652
  );
1150
1653
  server2.get(
1151
- "/_web/sessions/:id/analytics/night-owl",
1654
+ "/_web/sessions/:id/analytics/mention",
1152
1655
  async (request) => {
1153
1656
  const id = request.params.id;
1154
1657
  const filter = parseTimeFilter(request.query);
1155
- return cached("night-owl", id, { ...filter }, () => getNightOwlAnalysis(adapter.ensureReadonly(id), filter), {
1156
- dailyInvalidate: true
1157
- });
1658
+ return cached("mention", id, { ...filter }, () => getMentionAnalysis(adapter.ensureReadonly(id), filter));
1158
1659
  }
1159
1660
  );
1160
1661
  server2.get(
1161
- "/_web/sessions/:id/analytics/repeat",
1662
+ "/_web/sessions/:id/analytics/mention-graph",
1162
1663
  async (request) => {
1163
1664
  const id = request.params.id;
1164
1665
  const filter = parseTimeFilter(request.query);
1165
- return cached("repeat", id, { ...filter }, () => getRepeatAnalysis(adapter.ensureReadonly(id), filter));
1666
+ return cached("mention-graph", id, { ...filter }, () => getMentionGraph(adapter.ensureReadonly(id), filter));
1166
1667
  }
1167
1668
  );
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" });
1669
+ server2.get(
1670
+ "/_web/sessions/:id/analytics/laugh",
1671
+ async (request) => {
1672
+ const id = request.params.id;
1673
+ const filter = parseTimeFilter(request.query);
1674
+ return cached("laugh", id, { ...filter }, () => getLaughAnalysis(adapter.ensureReadonly(id), filter));
1178
1675
  }
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);
1676
+ );
1677
+ server2.get("/_web/sessions/:id/analytics/cluster", async (request) => {
1678
+ const id = request.params.id;
1679
+ const filter = parseTimeFilter(request.query);
1680
+ const options = {};
1681
+ if (request.query.topEdges) options.topEdges = parseInt(request.query.topEdges, 10);
1682
+ if (request.query.lookAhead) options.lookAhead = parseInt(request.query.lookAhead, 10);
1683
+ if (request.query.decaySeconds) options.decaySeconds = parseInt(request.query.decaySeconds, 10);
1684
+ const hasOptions = Object.keys(options).length > 0;
1685
+ return cached(
1686
+ "cluster",
1687
+ id,
1688
+ { ...filter, ...options },
1689
+ () => getClusterGraph(adapter.ensureReadonly(id), filter, hasOptions ? options : void 0)
1690
+ );
1196
1691
  });
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 });
1692
+ server2.get(
1693
+ "/_web/sessions/:id/analytics/language-preference",
1694
+ async (request) => {
1695
+ const id = request.params.id;
1696
+ const filter = parseTimeFilter(request.query);
1697
+ const locale = request.query.locale || "zh-CN";
1698
+ const zhDictPath = path4.join(ctx.pathProvider.getSystemDir(), "nlp", "zh-CN.dict");
1699
+ let zhDictVersion;
1700
+ try {
1701
+ const st = fs2.statSync(zhDictPath);
1702
+ zhDictVersion = `${Math.floor(st.mtimeMs)}:${st.size}`;
1703
+ } catch {
1704
+ zhDictVersion = "-";
1705
+ }
1706
+ return cached(
1707
+ "language-preference",
1708
+ id,
1709
+ { ...filter, locale },
1710
+ () => getLanguagePreferenceAnalysis(adapter.ensureReadonly(id), {
1711
+ locale,
1712
+ timeFilter: filter,
1713
+ nlpProvider: createJiebaNlpProvider()
1714
+ }),
1715
+ { extraVersion: `dict:${zhDictVersion}` }
1716
+ );
1202
1717
  }
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 });
1718
+ );
1719
+ server2.get(
1720
+ "/_web/sessions/:id/analytics/monthly-activity",
1721
+ async (request) => {
1722
+ const id = request.params.id;
1723
+ const filter = parseTimeFilter(request.query);
1724
+ return cached("monthly-activity", id, { ...filter }, () => getMonthlyActivity(adapter.ensureReadonly(id), filter));
1206
1725
  }
1207
- if (Array.isArray(params)) {
1208
- return stmt.all(...params);
1726
+ );
1727
+ server2.get(
1728
+ "/_web/sessions/:id/analytics/yearly-activity",
1729
+ async (request) => {
1730
+ const id = request.params.id;
1731
+ const filter = parseTimeFilter(request.query);
1732
+ return cached("yearly-activity", id, { ...filter }, () => getYearlyActivity(adapter.ensureReadonly(id), filter));
1209
1733
  }
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" });
1734
+ );
1735
+ server2.get(
1736
+ "/_web/sessions/:id/analytics/message-length-distribution",
1737
+ async (request) => {
1738
+ const id = request.params.id;
1739
+ const filter = parseTimeFilter(request.query);
1740
+ return cached(
1741
+ "message-length-distribution",
1742
+ id,
1743
+ { ...filter },
1744
+ () => getMessageLengthDistribution(adapter.ensureReadonly(id), filter)
1745
+ );
1237
1746
  }
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 });
1747
+ );
1748
+ server2.get(
1749
+ "/_web/sessions/:id/analytics/text-stats",
1750
+ async (request) => {
1751
+ const id = request.params.id;
1752
+ const filter = parseTimeFilter(request.query);
1753
+ return cached("text-stats", id, { ...filter }, () => getTextStats(adapter.ensureReadonly(id), filter));
1269
1754
  }
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 });
1755
+ );
1756
+ server2.get(
1757
+ "/_web/sessions/:id/analytics/long-message-count",
1758
+ async (request) => {
1759
+ const id = request.params.id;
1760
+ const filter = parseTimeFilter(request.query);
1761
+ const minLength = request.query.minLength ? parseInt(request.query.minLength, 10) : void 0;
1762
+ return cached(
1763
+ "long-message-count",
1764
+ id,
1765
+ { ...filter, minLength },
1766
+ () => getLongMessageCount(adapter.ensureReadonly(id), filter, minLength)
1767
+ );
1303
1768
  }
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 = "-";
1769
+ );
1770
+ server2.get(
1771
+ "/_web/sessions/:id/analytics/member-monthly-trend",
1772
+ async (request) => {
1773
+ const id = request.params.id;
1774
+ const filter = parseTimeFilter(request.query);
1775
+ return cached(
1776
+ "member-monthly-trend",
1777
+ id,
1778
+ { ...filter },
1779
+ () => getMemberMonthlyTrend(adapter.ensureReadonly(id), filter)
1780
+ );
1313
1781
  }
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",
1782
+ );
1783
+ server2.get(
1784
+ "/_web/sessions/:id/analytics/text-length-percentiles",
1320
1785
  async (request) => {
1321
- const { text, locale, minLength } = request.body;
1322
- return segmentText(text, locale, minLength);
1786
+ const id = request.params.id;
1787
+ const filter = parseTimeFilter(request.query);
1788
+ return cached(
1789
+ "text-length-percentiles",
1790
+ id,
1791
+ { ...filter },
1792
+ () => getTextLengthPercentiles(adapter.ensureReadonly(id), filter)
1793
+ );
1323
1794
  }
1324
1795
  );
1325
- }
1326
-
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
- });
1373
- }
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();
1381
- });
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();
1432
- });
1433
- server2.get("/_web/ai/llm/configs", async () => {
1434
- const data = store.loadStore();
1435
- return {
1436
- configs: data.configs.map((c) => toConfigDisplay(c)),
1437
- defaultAssistant: data.defaultAssistant,
1438
- fastModel: data.fastModel
1439
- };
1440
- });
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) };
1445
- }
1446
- return result;
1447
- });
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" });
1549
- }
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();
1796
+ server2.get(
1797
+ "/_web/sessions/:id/analytics/dragon-king",
1798
+ async (request) => {
1799
+ const id = request.params.id;
1800
+ const filter = parseTimeFilter(request.query);
1801
+ return cached("dragon-king", id, { ...filter }, () => getDragonKingAnalysis(adapter.ensureReadonly(id), filter));
1583
1802
  }
1584
- });
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();
1803
+ );
1804
+ server2.get(
1805
+ "/_web/sessions/:id/analytics/diving",
1806
+ async (request) => {
1807
+ const id = request.params.id;
1808
+ const filter = parseTimeFilter(request.query);
1809
+ return cached("diving", id, { ...filter }, () => getDivingAnalysis(adapter.ensureReadonly(id), filter), {
1810
+ dailyInvalidate: true
1811
+ });
1637
1812
  }
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 };
1813
+ );
1814
+ server2.get(
1815
+ "/_web/sessions/:id/analytics/check-in",
1816
+ async (request) => {
1817
+ const id = request.params.id;
1818
+ const filter = parseTimeFilter(request.query);
1819
+ return cached("check-in", id, { ...filter }, () => getCheckInAnalysis(adapter.ensureReadonly(id), filter));
1646
1820
  }
1647
- return { success: false };
1648
- });
1821
+ );
1822
+ server2.get(
1823
+ "/_web/sessions/:id/analytics/meme-battle",
1824
+ async (request) => {
1825
+ const id = request.params.id;
1826
+ const filter = parseTimeFilter(request.query);
1827
+ return cached("meme-battle", id, { ...filter }, () => getMemeBattleAnalysis(adapter.ensureReadonly(id), filter));
1828
+ }
1829
+ );
1830
+ server2.get(
1831
+ "/_web/sessions/:id/analytics/night-owl",
1832
+ async (request) => {
1833
+ const id = request.params.id;
1834
+ const filter = parseTimeFilter(request.query);
1835
+ return cached("night-owl", id, { ...filter }, () => getNightOwlAnalysis(adapter.ensureReadonly(id), filter), {
1836
+ dailyInvalidate: true
1837
+ });
1838
+ }
1839
+ );
1840
+ server2.get(
1841
+ "/_web/sessions/:id/analytics/repeat",
1842
+ async (request) => {
1843
+ const id = request.params.id;
1844
+ const filter = parseTimeFilter(request.query);
1845
+ return cached("repeat", id, { ...filter }, () => getRepeatAnalysis(adapter.ensureReadonly(id), filter));
1846
+ }
1847
+ );
1649
1848
  }
1650
1849
 
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
- }
1850
+ // ../../packages/http-routes/src/routes/web/cache.ts
1851
+ import * as fs3 from "fs";
1852
+ import * as fsp from "fs/promises";
1853
+ import * as path5 from "path";
1854
+ async function getDirSize(dirPath) {
1855
+ let totalSize = 0;
1659
1856
  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
1669
- };
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) };
1857
+ if (!fs3.existsSync(dirPath)) return 0;
1858
+ const files = await fsp.readdir(dirPath, { withFileTypes: true });
1859
+ for (const file of files) {
1860
+ const filePath = path5.join(dirPath, file.name);
1861
+ if (file.isDirectory()) {
1862
+ totalSize += await getDirSize(filePath);
1863
+ } else {
1864
+ const stat2 = await fsp.stat(filePath);
1865
+ totalSize += stat2.size;
1682
1866
  }
1683
1867
  }
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" };
1694
- }
1695
- return { success: false, error: error instanceof Error ? error.message : String(error) };
1868
+ } catch {
1696
1869
  }
1870
+ return totalSize;
1697
1871
  }
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
- }));
1709
- });
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);
1718
- try {
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;
1737
- } catch (error) {
1738
- if (abortController.signal.aborted) {
1739
- return { success: false, error: "cancelled" };
1872
+ async function getFileCount(dirPath) {
1873
+ let count = 0;
1874
+ try {
1875
+ if (!fs3.existsSync(dirPath)) return 0;
1876
+ const files = await fsp.readdir(dirPath, { withFileTypes: true });
1877
+ for (const file of files) {
1878
+ const filePath = path5.join(dirPath, file.name);
1879
+ if (file.isDirectory()) {
1880
+ count += await getFileCount(filePath);
1881
+ } else {
1882
+ count++;
1740
1883
  }
1741
- appLogger.error("ai-tools", `Failed to execute tool ${toolName}`, error);
1742
- return { success: false, error: String(error) };
1743
- } finally {
1744
- activeToolTests.delete(testId);
1745
1884
  }
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 };
1756
- });
1885
+ } catch {
1886
+ }
1887
+ return count;
1757
1888
  }
1758
-
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);
1766
- });
1767
- server2.get("/_web/ai/chats", async (request) => {
1768
- const { sessionId } = request.query;
1769
- if (!sessionId) return [];
1770
- return cm.getAIChats(sessionId);
1771
- });
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;
1776
- });
1777
- server2.put("/_web/ai/chats/:id/title", async (request) => {
1778
- return cm.updateAIChatTitle(request.params.id, request.body.title);
1779
- });
1780
- server2.delete("/_web/ai/chats/:id", async (request) => {
1781
- return cm.deleteAIChat(request.params.id);
1782
- });
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
1889
+ function hasChatLabDatabases(dirPath) {
1890
+ try {
1891
+ const markerPath = path5.join(dirPath, ".chatlab");
1892
+ const dbDir = path5.join(dirPath, "databases");
1893
+ if (!fs3.existsSync(markerPath) || !fs3.existsSync(dbDir)) return false;
1894
+ return fs3.readdirSync(dbDir).some((file) => file.endsWith(".db"));
1895
+ } catch {
1896
+ return false;
1897
+ }
1898
+ }
1899
+ function registerCacheRoutes(server2, ctx) {
1900
+ const pp = ctx.pathProvider;
1901
+ const downloadsDir = ctx.downloadsDir ?? pp.getDownloadsDir();
1902
+ server2.get("/_web/cache/info", async () => {
1903
+ const directories = [
1904
+ {
1905
+ id: "databases",
1906
+ name: "settings.storage.cache.databases.name",
1907
+ description: "settings.storage.cache.databases.description",
1908
+ path: pp.getDatabaseDir(),
1909
+ scope: "user-data",
1910
+ rootPath: pp.getUserDataDir(),
1911
+ icon: "i-heroicons-circle-stack",
1912
+ canClear: false
1913
+ },
1914
+ {
1915
+ id: "ai",
1916
+ name: "settings.storage.cache.ai.name",
1917
+ description: "settings.storage.cache.ai.description",
1918
+ path: pp.getAiDataDir(),
1919
+ scope: "system-data",
1920
+ rootPath: pp.getSystemDir(),
1921
+ icon: "i-heroicons-sparkles",
1922
+ canClear: false
1923
+ },
1924
+ {
1925
+ id: "cache",
1926
+ name: "settings.storage.cache.statsCache.name",
1927
+ description: "settings.storage.cache.statsCache.description",
1928
+ path: pp.getCacheDir(),
1929
+ scope: "system-data",
1930
+ rootPath: pp.getSystemDir(),
1931
+ icon: "i-heroicons-bolt",
1932
+ canClear: true
1933
+ },
1934
+ {
1935
+ id: "logs",
1936
+ name: "settings.storage.cache.logs.name",
1937
+ description: "settings.storage.cache.logs.description",
1938
+ path: pp.getLogsDir(),
1939
+ scope: "system-data",
1940
+ rootPath: pp.getSystemDir(),
1941
+ icon: "i-heroicons-document-text",
1942
+ canClear: true
1943
+ }
1944
+ ];
1945
+ const results = await Promise.all(
1946
+ directories.map(async (dir) => {
1947
+ const [size, fileCount] = await Promise.all([getDirSize(dir.path), getFileCount(dir.path)]);
1948
+ return { ...dir, size, fileCount, exists: fs3.existsSync(dir.path) };
1949
+ })
1793
1950
  );
1951
+ return {
1952
+ baseDir: pp.getSystemDir(),
1953
+ directories: results,
1954
+ totalSize: results.reduce((sum, d) => sum + d.size, 0)
1955
+ };
1794
1956
  });
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" });
1957
+ server2.post("/_web/cache/clear", async (request) => {
1958
+ const { cacheId } = request.body;
1959
+ const allowedDirs = {
1960
+ cache: pp.getCacheDir(),
1961
+ logs: pp.getLogsDir()
1962
+ };
1963
+ const dirPath = allowedDirs[cacheId];
1964
+ if (!dirPath) return { success: false, error: "Not allowed to clear this directory" };
1965
+ if (!fs3.existsSync(dirPath)) return { success: true };
1966
+ const files = await fsp.readdir(dirPath);
1967
+ for (const file of files) {
1968
+ const filePath = path5.join(dirPath, file);
1969
+ const stat2 = await fsp.stat(filePath);
1970
+ if (stat2.isDirectory()) {
1971
+ await fsp.rm(filePath, { recursive: true });
1972
+ } else {
1973
+ await fsp.unlink(filePath);
1974
+ }
1802
1975
  }
1803
- cm.deleteMessagesFrom(id, messageId);
1804
1976
  return { success: true };
1805
1977
  });
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);
1978
+ server2.get("/_web/cache/data-dir", async () => {
1979
+ const pending = ctx.getPendingDataDirMigration?.();
1980
+ return {
1981
+ path: pp.getUserDataDir(),
1982
+ defaultPath: ctx.defaultUserDataDir,
1983
+ isCustom: ctx.isCustomDataDir ?? false,
1984
+ canSetDataDir: ctx.canSetDataDir ?? Boolean(ctx.setDataDir),
1985
+ managedScope: "chat-databases",
1986
+ managedDescription: "settings.storage.dataLocation.managedDescription",
1987
+ hasLegacyDataAtDefaultDir: Boolean(ctx.defaultUserDataDir) && path5.resolve(pp.getUserDataDir()) !== path5.resolve(ctx.defaultUserDataDir ?? "") && hasChatLabDatabases(ctx.defaultUserDataDir ?? ""),
1988
+ pendingMigration: pending ? {
1989
+ from: pending.from,
1990
+ to: pending.to,
1991
+ createdAt: pending.createdAt
1992
+ } : void 0
1993
+ };
1812
1994
  });
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" });
1995
+ server2.post("/_web/cache/data-dir", async (request, reply) => {
1996
+ if (!ctx.setDataDir) {
1997
+ return reply.code(501).send({ success: false, error: "Data directory changes are not supported" });
1817
1998
  }
1818
- cm.updateMessageContent(request.params.messageId, content);
1819
- return { success: true };
1999
+ const targetPath = typeof request.body?.path === "string" ? request.body.path : null;
2000
+ const migrate = request.body?.migrate !== false;
2001
+ return ctx.setDataDir(targetPath, migrate);
1820
2002
  });
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" });
2003
+ server2.get("/_web/cache/latest-import-log", async () => {
2004
+ const importLogDir = path5.join(pp.getLogsDir(), "import");
2005
+ if (!fs3.existsSync(importLogDir)) {
2006
+ return { success: false, error: "Log directory not found" };
1825
2007
  }
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" });
2008
+ const files = await fsp.readdir(importLogDir);
2009
+ const logFiles = files.filter((f) => f.startsWith("import_") && f.endsWith(".log"));
2010
+ if (logFiles.length === 0) {
2011
+ return { success: false, error: "No import logs found" };
1833
2012
  }
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);
2013
+ const fileStats = await Promise.all(
2014
+ logFiles.map(async (f) => {
2015
+ const filePath = path5.join(importLogDir, f);
2016
+ const stat2 = await fsp.stat(filePath);
2017
+ return { name: f, path: filePath, mtime: stat2.mtime.getTime() };
2018
+ })
2019
+ );
2020
+ fileStats.sort((a, b) => b.mtime - a.mtime);
2021
+ return { success: true, path: fileStats[0].path, name: fileStats[0].name };
1838
2022
  });
1839
- server2.get("/_web/ai/debug/schema", async () => {
1840
- return cm.getAiSchema();
2023
+ server2.post("/_web/cache/save-to-downloads", async (request) => {
2024
+ const { filename, dataUrl } = request.body;
2025
+ if (!filename || !dataUrl) {
2026
+ return { success: false, error: "filename and dataUrl are required" };
2027
+ }
2028
+ let buffer;
2029
+ if (dataUrl.includes(";base64,")) {
2030
+ const base64Data = dataUrl.split(";base64,")[1];
2031
+ buffer = Buffer.from(base64Data, "base64");
2032
+ } else if (dataUrl.includes("charset=utf-8,")) {
2033
+ const textData = dataUrl.split("charset=utf-8,")[1];
2034
+ buffer = Buffer.from(decodeURIComponent(textData), "utf-8");
2035
+ } else {
2036
+ const base64Data = dataUrl.replace(/^data:[^,]+,/, "");
2037
+ buffer = Buffer.from(base64Data, "base64");
2038
+ }
2039
+ if (!fs3.existsSync(downloadsDir)) {
2040
+ fs3.mkdirSync(downloadsDir, { recursive: true });
2041
+ }
2042
+ const filePath = path5.join(downloadsDir, filename);
2043
+ fs3.writeFileSync(filePath, buffer);
2044
+ return { success: true, filePath };
1841
2045
  });
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" });
2046
+ server2.post("/_web/cache/open-dir", async (request, reply) => {
2047
+ if (!ctx.openDirectory) return reply.code(501).send({ success: false, error: "Not supported" });
2048
+ const { cacheId } = request.body;
2049
+ const dirPaths = {
2050
+ base: pp.getSystemDir(),
2051
+ userData: pp.getUserDataDir(),
2052
+ databases: pp.getDatabaseDir(),
2053
+ cache: pp.getCacheDir(),
2054
+ ai: pp.getAiDataDir(),
2055
+ logs: pp.getLogsDir(),
2056
+ downloads: downloadsDir
2057
+ };
2058
+ const dirPath = dirPaths[cacheId];
2059
+ if (!dirPath) return { success: false, error: "Unknown directory" };
2060
+ if (!fs3.existsSync(dirPath)) {
2061
+ await fsp.mkdir(dirPath, { recursive: true });
1846
2062
  }
1847
2063
  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 });
2064
+ await ctx.openDirectory(dirPath);
2065
+ } catch (error) {
2066
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
1852
2067
  }
2068
+ return { success: true };
1853
2069
  });
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 };
2070
+ server2.post("/_web/cache/show-in-folder", async (request, reply) => {
2071
+ if (!ctx.showInFolder) return reply.code(501).send({ success: false, error: "Not supported" });
2072
+ const { filePath } = request.body;
2073
+ if (!filePath) return { success: false, error: "filePath is required" };
2074
+ try {
2075
+ await ctx.showInFolder(filePath);
2076
+ } catch (error) {
2077
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
2078
+ }
2079
+ return { success: true };
1862
2080
  });
1863
2081
  }
1864
2082
 
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);
2083
+ // ../../packages/http-routes/src/routes/web/contacts.ts
2084
+ function registerContactsRoutes(server2, ctx) {
2085
+ const service = ctx.contactsService ?? createContactsService({
2086
+ adapter: ctx.sessionAdapter,
2087
+ pathProvider: ctx.pathProvider,
2088
+ runtimeIdentity: ctx.runtimeIdentity,
2089
+ nativeBinding: ctx.nativeBinding
2090
+ });
2091
+ server2.addHook("onClose", async () => {
2092
+ await service.close();
2093
+ });
2094
+ server2.get("/_web/contacts", async (request) => {
2095
+ return service.getContactsPage({
2096
+ acceptStale: isTruthy(request.query.acceptStale),
2097
+ timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange),
2098
+ pool: parseContactPool(request.query.pool),
2099
+ page: parsePositiveInteger(request.query.page),
2100
+ pageSize: parsePositiveInteger(request.query.pageSize),
2101
+ query: request.query.q
2102
+ });
2103
+ });
2104
+ server2.post("/_web/contacts/recompute", async (request) => {
2105
+ return service.startRecompute({
2106
+ timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange),
2107
+ pool: parseContactPool(request.query.pool),
2108
+ page: parsePositiveInteger(request.query.page),
2109
+ pageSize: parsePositiveInteger(request.query.pageSize),
2110
+ query: request.query.q
2111
+ });
2112
+ });
2113
+ server2.get("/_web/contacts/:key/detail", async (request) => {
2114
+ return service.getContactDetail(request.params.key, {
2115
+ acceptStale: isTruthy(request.query.acceptStale),
2116
+ timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange)
2117
+ });
2118
+ });
2119
+ server2.put(
2120
+ "/_web/contacts/:key/mark-friend",
2121
+ async (request) => {
2122
+ return service.markContactAsFriend(request.params.key, {
2123
+ timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange)
2124
+ });
1877
2125
  }
1878
- };
2126
+ );
2127
+ server2.delete(
2128
+ "/_web/contacts/:key/mark-friend",
2129
+ async (request) => {
2130
+ return service.unmarkContactAsFriend(request.params.key, {
2131
+ timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange)
2132
+ });
2133
+ }
2134
+ );
1879
2135
  }
1880
- function registerAiSummaryRoutes(server2, ctx) {
1881
- const deps = createSummaryDeps(ctx);
1882
- if (!deps) return;
2136
+ function isTruthy(value) {
2137
+ return value === "1" || value === "true" || value === "yes";
2138
+ }
2139
+ function parseContactsTimeRangePreset(value) {
2140
+ return CONTACTS_TIME_RANGE_PRESETS.includes(value) ? value : "1y";
2141
+ }
2142
+ function parseContactPool(value) {
2143
+ return value === "friend" || value === "non_friend" ? value : void 0;
2144
+ }
2145
+ function parsePositiveInteger(value) {
2146
+ if (!value) return void 0;
2147
+ const parsed = Number(value);
2148
+ if (!Number.isFinite(parsed)) return void 0;
2149
+ return Math.max(1, Math.trunc(parsed));
2150
+ }
2151
+
2152
+ // ../../packages/http-routes/src/routes/web/export.ts
2153
+ function registerExportRoutes(server2, ctx) {
1883
2154
  const { sessionAdapter: adapter } = ctx;
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
2155
+ server2.post("/_web/sessions/:id/export", async (request, reply) => {
2156
+ const { id } = request.params;
2157
+ const body = request.body;
2158
+ const sessionName = body?.sessionName || id;
2159
+ const format = body?.format || "txt";
2160
+ const result = export_service_exports.exportFormatted(adapter, {
2161
+ sessionId: id,
2162
+ sessionName,
2163
+ format,
2164
+ timeFilter: body.timeFilter
1890
2165
  });
1891
- if ("error" in result && !result.success) {
1892
- return reply.code(400).send({ error: result.error });
2166
+ if (!result.success) {
2167
+ return reply.code(result.totalMessages === 0 ? 404 : 500).send({ error: result.error });
1893
2168
  }
1894
- return result;
2169
+ reply.header("Content-Type", result.mimeType);
2170
+ reply.header("Content-Disposition", `attachment; filename="${encodeURIComponent(result.filename)}"`);
2171
+ return reply.send(result.content);
1895
2172
  });
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
1901
- });
1902
- if (result.error) {
1903
- return reply.code(400).send({ error: result.error });
1904
- }
1905
- return result;
2173
+ }
2174
+
2175
+ // ../../packages/http-routes/src/routes/web/global-insight.ts
2176
+ function registerGlobalInsightRoutes(server2, ctx) {
2177
+ const service = ctx.globalInsightService ?? createGlobalInsightService({
2178
+ adapter: ctx.sessionAdapter,
2179
+ pathProvider: ctx.pathProvider,
2180
+ runtimeIdentity: ctx.runtimeIdentity,
2181
+ nativeBinding: ctx.nativeBinding
1906
2182
  });
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);
2183
+ server2.addHook("onClose", async () => service.close());
2184
+ server2.get("/_web/global-insight/annual-summary", async (request) => {
2185
+ return service.getAnnualSummary({
2186
+ ...parseRange(request.query),
2187
+ acceptStale: isTruthy2(request.query.acceptStale)
2188
+ });
1910
2189
  });
2190
+ server2.post(
2191
+ "/_web/global-insight/annual-summary/recompute",
2192
+ async (request) => service.startRecompute(parseRange(request.query))
2193
+ );
1911
2194
  }
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;
2195
+ function parseRange(query) {
2196
+ const mode = query.mode === "recent" ? "recent" : "year";
2197
+ return {
2198
+ mode,
2199
+ year: parseInteger(query.year),
2200
+ days: query.days === "365" ? 365 : void 0
2201
+ };
1925
2202
  }
1926
- function resolveAiLogPath(ctx) {
1927
- const currentPath = ctx.getCurrentAiLogPath?.();
1928
- if (currentPath && fs3.existsSync(currentPath)) return currentPath;
1929
- return findLatestAiLogPath(ctx.pathProvider.getLogsDir());
2203
+ function parseInteger(value) {
2204
+ if (!value) return void 0;
2205
+ const parsed = Number(value);
2206
+ return Number.isInteger(parsed) ? parsed : void 0;
1930
2207
  }
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.");
2208
+ function isTruthy2(value) {
2209
+ return value === "1" || value === "true" || value === "yes";
1935
2210
  }
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
+
2212
+ // ../../packages/http-routes/src/routes/web/logs.ts
2213
+ function registerLogRoutes(server2) {
2214
+ server2.post("/_web/logs/report", async (req, reply) => {
2215
+ const { level, message, stack, url } = req.body ?? {};
2216
+ if (!message) return reply.code(400).send({ error: "message required" });
2217
+ const detail = [url ? `url=${url}` : null, stack ? `
2218
+ ${stack}` : null].filter(Boolean).join(" ");
2219
+ if (level === "warn") appLogger.warn("web", message, detail || void 0);
2220
+ else appLogger.error("web", message, detail || void 0);
2221
+ return { ok: true };
1950
2222
  });
1951
2223
  }
1952
2224
 
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
- );
1979
- }
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
- };
2225
+ // ../../packages/http-routes/src/routes/web/members.ts
2226
+ function registerMemberRoutes(server2, ctx) {
2227
+ const { sessionAdapter: adapter } = ctx;
2228
+ server2.get("/_web/sessions/:id/members", async (request) => {
2229
+ return member_service_exports.getMembers(adapter, request.params.id);
1992
2230
  });
1993
- server2.put(
1994
- "/_web/ai/semantic-index/config",
2231
+ server2.get("/_web/sessions/:id/members/paginated", async (request) => {
2232
+ return member_service_exports.getMembersPaginated(adapter, request.params.id, {
2233
+ page: parseInt(request.query.page || "1", 10),
2234
+ pageSize: parseInt(request.query.pageSize || "20", 10),
2235
+ search: request.query.search,
2236
+ sortOrder: request.query.sortOrder === "asc" ? "asc" : "desc"
2237
+ });
2238
+ });
2239
+ server2.patch(
2240
+ "/_web/sessions/:id/members/:memberId/aliases",
1995
2241
  async (request) => {
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
+ const memberId = parseInt(request.params.memberId, 10);
2243
+ member_service_exports.updateMemberAliases(adapter, request.params.id, memberId, request.body.aliases);
2244
+ return { success: true };
2245
+ }
2246
+ );
2247
+ server2.delete(
2248
+ "/_web/sessions/:id/members/:memberId",
2249
+ async (request) => {
2250
+ const memberId = parseInt(request.params.memberId, 10);
2251
+ member_service_exports.deleteMember(adapter, request.params.id, memberId);
2252
+ return { success: true };
2003
2253
  }
2004
2254
  );
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
- });
2035
2255
  server2.post(
2036
- "/_web/ai/semantic-index/search",
2256
+ "/_web/sessions/:id/members/batch-delete",
2037
2257
  async (request, reply) => {
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 });
2258
+ const memberIds = request.body?.memberIds;
2259
+ if (!Array.isArray(memberIds) || memberIds.length === 0 || memberIds.some((memberId) => !Number.isSafeInteger(memberId) || Number(memberId) <= 0)) {
2260
+ return reply.code(400).send({
2261
+ success: false,
2262
+ error: "memberIds must contain at least one positive integer"
2263
+ });
2264
+ }
2265
+ const uniqueMemberIds = Array.from(new Set(memberIds));
2266
+ try {
2267
+ const success = member_service_exports.deleteMembers(adapter, request.params.id, uniqueMemberIds);
2268
+ if (!success) {
2269
+ throw new Error("Member deletion transaction returned no result");
2270
+ }
2271
+ } catch (error) {
2272
+ if (apiErrorFromUnknown(error) || isHttpClientError(error)) throw error;
2273
+ appLogger.error("members", "Batch member deletion failed", error);
2274
+ return reply.code(500).send({ success: false, error: "Failed to delete selected members" });
2275
+ }
2276
+ appLogger.info("members", "Batch member deletion completed", {
2277
+ sessionId: request.params.id,
2278
+ memberCount: uniqueMemberIds.length
2279
+ });
2280
+ return { success: true, deletedCount: uniqueMemberIds.length };
2281
+ }
2282
+ );
2283
+ server2.post(
2284
+ "/_web/sessions/:id/members/merge",
2285
+ async (request) => {
2286
+ const { memberId1, memberId2 } = request.body;
2287
+ member_service_exports.mergeMembers(adapter, request.params.id, memberId1, memberId2);
2288
+ return { success: true };
2289
+ }
2290
+ );
2291
+ server2.get(
2292
+ "/_web/sessions/:id/members/:memberId/history",
2293
+ async (request) => {
2294
+ const memberId = parseInt(request.params.memberId, 10);
2295
+ return member_service_exports.getMemberNameHistory(adapter, request.params.id, memberId);
2041
2296
  }
2042
2297
  );
2043
2298
  }
2299
+ function isHttpClientError(error) {
2300
+ return error instanceof Error && "statusCode" in error && typeof error.statusCode === "number" && error.statusCode >= 400 && error.statusCode < 500;
2301
+ }
2044
2302
 
2045
2303
  // ../../packages/http-routes/src/routes/web/merge.ts
2046
2304
  import * as fs4 from "fs";
2047
- import * as os from "os";
2048
2305
  import * as path6 from "path";
2049
- function ensureDb2(ctx, sessionId) {
2306
+ function ensureDb(ctx, sessionId) {
2050
2307
  const db = ctx.dbManager.open(sessionId);
2051
2308
  if (!db) throw sessionNotFound(sessionId);
2052
2309
  return db;
@@ -2062,7 +2319,7 @@ function registerMergeRoutes(server2, ctx) {
2062
2319
  if (contentType.includes("multipart/form-data")) {
2063
2320
  const data = await request.file();
2064
2321
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
2065
- const tmpDir = fs4.mkdtempSync(path6.join(os.tmpdir(), "chatlab-merge-"));
2322
+ const tmpDir = createChatLabTempDir("merge", "upload-");
2066
2323
  const tmpPath = path6.join(tmpDir, data.filename || "upload");
2067
2324
  const chunks = [];
2068
2325
  for await (const chunk of data.file) {
@@ -2155,7 +2412,7 @@ function registerMergeRoutes(server2, ctx) {
2155
2412
  return reply.code(501).send({ error: "Import not available on this server" });
2156
2413
  }
2157
2414
  const jsonData = JSON.stringify(merged.chatLabData);
2158
- const tmpDir = fs4.mkdtempSync(path6.join(os.tmpdir(), "chatlab-merged-"));
2415
+ const tmpDir = createChatLabTempDir("merge", "import-");
2159
2416
  const safeName = path6.basename(outputName).replace(/[/\\?%*:|"<>]/g, "_") || "merged";
2160
2417
  const tmpPath = path6.join(tmpDir, `${safeName}.json`);
2161
2418
  fs4.writeFileSync(tmpPath, jsonData, "utf-8");
@@ -2204,7 +2461,7 @@ function registerMergeRoutes(server2, ctx) {
2204
2461
  }
2205
2462
  const handles = [];
2206
2463
  for (const sid of sessionIds) {
2207
- const db = ensureDb2(ctx, sid);
2464
+ const db = ensureDb(ctx, sid);
2208
2465
  const exported = exportSessionToJson(db);
2209
2466
  const { db: tempDb, tempDbPath } = mergeCache.createTempDatabase(exported.meta.name);
2210
2467
  const writer = new TempDbWriter(tempDb);
@@ -2237,240 +2494,293 @@ function registerMergeRoutes(server2, ctx) {
2237
2494
  const handle = mergeCache.store(exported.meta.name, tempDbPath);
2238
2495
  handles.push({ sessionId: sid, handle });
2239
2496
  }
2240
- return { success: true, handles };
2497
+ return { success: true, handles };
2498
+ });
2499
+ }
2500
+
2501
+ // ../../packages/http-routes/src/routes/web/nlp.ts
2502
+ import * as fs5 from "fs";
2503
+ import * as path7 from "path";
2504
+ function registerNlpRoutes(server2, ctx) {
2505
+ const nlpDir = path7.join(ctx.pathProvider.getSystemDir(), "nlp");
2506
+ initNlpDir(nlpDir);
2507
+ ensureDefaultDict(nlpDir).catch((err) => console.warn("[NLP] Auto-download zh-CN dict failed:", err));
2508
+ server2.get("/_web/nlp/pos-tags", async () => {
2509
+ return POS_TAG_DEFINITIONS;
2510
+ });
2511
+ server2.get("/_web/nlp/dicts", async () => {
2512
+ return getDictList(nlpDir);
2513
+ });
2514
+ server2.get("/_web/nlp/dicts/:id/status", async (request) => {
2515
+ return isDictDownloaded(nlpDir, request.params.id);
2516
+ });
2517
+ server2.post("/_web/nlp/dicts/:id/download", async (request) => {
2518
+ return downloadDict(nlpDir, request.params.id);
2519
+ });
2520
+ server2.delete("/_web/nlp/dicts/:id", async (request) => {
2521
+ return deleteDict(nlpDir, request.params.id);
2522
+ });
2523
+ server2.post("/_web/nlp/word-frequency", async (request) => {
2524
+ const params = request.body;
2525
+ const db = ctx.dbManager.open(params.sessionId);
2526
+ if (!db) {
2527
+ throw Object.assign(new Error(`Session not found: ${params.sessionId}`), { statusCode: 404 });
2528
+ }
2529
+ const { sessionId, ...keyParams } = params;
2530
+ const effectiveDictId = !params.dictType || params.dictType === "default" ? "zh-CN" : String(params.dictType);
2531
+ const dictFilePath = path7.join(nlpDir, `${effectiveDictId}.dict`);
2532
+ let dictVersion;
2533
+ try {
2534
+ const st = fs5.statSync(dictFilePath);
2535
+ dictVersion = `${Math.floor(st.mtimeMs)}:${st.size}`;
2536
+ } catch {
2537
+ dictVersion = "-";
2538
+ }
2539
+ return withAnalyticsCache(ctx, sessionId, "nlp.word-frequency", keyParams, () => computeWordFrequency(db, params), {
2540
+ extraVersion: `dict:${dictVersion}`
2541
+ });
2241
2542
  });
2543
+ server2.post(
2544
+ "/_web/nlp/segment",
2545
+ async (request) => {
2546
+ const { text, locale, minLength } = request.body;
2547
+ return segmentText(text, locale, minLength);
2548
+ }
2549
+ );
2242
2550
  }
2243
2551
 
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
- }
2552
+ // ../../packages/http-routes/src/routes/web/people-relationships.ts
2553
+ function registerPeopleRelationshipsRoutes(server2, ctx) {
2554
+ const service = ctx.peopleRelationshipsService ?? createPeopleRelationshipsService({
2555
+ adapter: ctx.sessionAdapter,
2556
+ pathProvider: ctx.pathProvider,
2557
+ runtimeIdentity: ctx.runtimeIdentity,
2558
+ nativeBinding: ctx.nativeBinding
2559
+ });
2560
+ server2.addHook("onClose", async () => {
2561
+ await service.close();
2562
+ });
2563
+ server2.get("/_web/people/relationships", async (request) => {
2564
+ return service.getGraph({
2565
+ acceptStale: isTruthy3(request.query.acceptStale),
2566
+ timeRangePreset: parseContactsTimeRangePreset2(request.query.timeRange),
2567
+ graphScope: parsePeopleRelationshipsGraphScope(request.query.scope),
2568
+ query: request.query.q
2569
+ });
2570
+ });
2571
+ server2.post("/_web/people/relationships/recompute", async (request) => {
2572
+ return service.startRecompute({
2573
+ timeRangePreset: parseContactsTimeRangePreset2(request.query.timeRange),
2574
+ graphScope: parsePeopleRelationshipsGraphScope(request.query.scope),
2575
+ query: request.query.q
2576
+ });
2577
+ });
2578
+ server2.get(
2579
+ "/_web/people/relationships/:key/neighborhood",
2580
+ async (request) => {
2581
+ return service.getNeighborhood(request.params.key, {
2582
+ acceptStale: isTruthy3(request.query.acceptStale),
2583
+ timeRangePreset: parseContactsTimeRangePreset2(request.query.timeRange)
2584
+ });
2261
2585
  }
2262
- } catch {
2263
- }
2264
- return totalSize;
2586
+ );
2265
2587
  }
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
- }
2278
- }
2279
- } catch {
2280
- }
2281
- return count;
2588
+ function isTruthy3(value) {
2589
+ return value === "1" || value === "true" || value === "yes";
2282
2590
  }
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
- }
2591
+ function parseContactsTimeRangePreset2(value) {
2592
+ return CONTACTS_TIME_RANGE_PRESETS.includes(value) ? value : "1y";
2292
2593
  }
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
- };
2594
+ function parsePeopleRelationshipsGraphScope(value) {
2595
+ return value === "close" || value === "friends" ? value : "panorama";
2596
+ }
2597
+
2598
+ // ../../packages/http-routes/src/routes/web/preferences.ts
2599
+ function registerPreferencesRoutes(server2, ctx) {
2600
+ const prefManager = ctx.preferencesManager ?? new PreferencesManager(ctx.pathProvider.getSystemDir());
2601
+ server2.get("/_web/preferences", async () => {
2602
+ return prefManager.load();
2350
2603
  });
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);
2604
+ server2.patch("/_web/preferences", async (request) => {
2605
+ return prefManager.save(request.body);
2606
+ });
2607
+ server2.get("/_web/preferences/ui-config", async () => {
2608
+ const config = loadConfig();
2609
+ return config.ui;
2610
+ });
2611
+ server2.patch("/_web/preferences/ui-config", async (request) => {
2612
+ try {
2613
+ for (const [key, value] of Object.entries(request.body)) {
2614
+ if (value !== void 0) {
2615
+ writeConfigField("ui", key, value);
2616
+ }
2368
2617
  }
2618
+ return { success: true };
2619
+ } catch (err) {
2620
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
2369
2621
  }
2622
+ });
2623
+ server2.get("/_web/preferences/locale", async () => {
2624
+ const config = loadConfig();
2625
+ return { lang: config.locale.lang };
2626
+ });
2627
+ server2.patch("/_web/preferences/locale", async (request) => {
2628
+ try {
2629
+ writeConfigField("locale", "lang", request.body.lang);
2630
+ return { success: true };
2631
+ } catch (err) {
2632
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
2633
+ }
2634
+ });
2635
+ }
2636
+
2637
+ // ../../packages/http-routes/src/routes/web/session-index.ts
2638
+ function registerSessionIndexRoutes(server2, ctx) {
2639
+ const { sessionAdapter: adapter } = ctx;
2640
+ server2.get("/_web/sessions/index-stats", async () => {
2641
+ return session_index_service_exports.getAllIndexStats(adapter);
2642
+ });
2643
+ server2.post("/_web/sessions/:id/generate-index", async (request) => {
2644
+ const gapThreshold = request.body?.gapThreshold ?? 1800;
2645
+ const sessionCount = session_index_service_exports.generateIndex(adapter, request.params.id, gapThreshold);
2646
+ return { sessionCount };
2647
+ });
2648
+ server2.post("/_web/sessions/:id/generate-incremental-index", async (request) => {
2649
+ const gapThreshold = request.body?.gapThreshold ?? 1800;
2650
+ const sessionCount = session_index_service_exports.generateIncrementalIndex(adapter, request.params.id, gapThreshold);
2651
+ return { sessionCount };
2652
+ });
2653
+ server2.post("/_web/sessions/:id/clear-index", async (request) => {
2654
+ session_index_service_exports.clearIndex(adapter, request.params.id);
2370
2655
  return { success: true };
2371
2656
  });
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
- };
2657
+ server2.get("/_web/sessions/:id/search/fts", async (request, reply) => {
2658
+ if (!session_index_service_exports.getFtsStatus(adapter, request.params.id)) {
2659
+ return reply.code(400).send({ error: "FTS index not built for this session" });
2660
+ }
2661
+ const keywords = request.query.keywords.split(/\s+/).filter(Boolean);
2662
+ if (keywords.length === 0) return { rowids: [], total: 0 };
2663
+ const limit = parseInt(request.query.limit || "100", 10);
2664
+ const offset = parseInt(request.query.offset || "0", 10);
2665
+ return session_index_service_exports.searchFts(adapter, request.params.id, keywords, limit, offset);
2666
+ });
2667
+ server2.get("/_web/sessions/:id/fts/status", async (request) => {
2668
+ return { hasFtsIndex: session_index_service_exports.getFtsStatus(adapter, request.params.id) };
2669
+ });
2670
+ server2.post("/_web/sessions/:id/fts/rebuild", async (request) => {
2671
+ const result = session_index_service_exports.rebuildFts(adapter, request.params.id);
2672
+ return { success: true, indexed: result.indexed };
2388
2673
  });
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" });
2392
- }
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);
2674
+ }
2675
+
2676
+ // ../../packages/http-routes/src/routes/web/sessions.ts
2677
+ function registerSessionRoutes(server2, ctx) {
2678
+ const { sessionAdapter: adapter } = ctx;
2679
+ let preferencesInstance = null;
2680
+ const preferences = () => {
2681
+ preferencesInstance ??= ctx.preferencesManager ?? new PreferencesManager(ctx.pathProvider.getSystemDir());
2682
+ return preferencesInstance;
2683
+ };
2684
+ server2.get("/_web/sessions", async () => {
2685
+ const aiChatCounts = ctx.aiChatManager?.getAIChatCountsBySession();
2686
+ return session_service_exports.listAnalysisSessions(adapter, {
2687
+ enrichSession: aiChatCounts?.size ? (dto) => ({ ...dto, aiConversationCount: aiChatCounts.get(dto.id) ?? 0 }) : void 0
2688
+ });
2396
2689
  });
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" };
2690
+ server2.get("/_web/sessions/:id", async (request) => {
2691
+ const session = session_service_exports.getAnalysisSession(adapter, request.params.id);
2692
+ if (!session) {
2693
+ throw Object.assign(new Error(`Session not found: ${request.params.id}`), { statusCode: 404 });
2401
2694
  }
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" };
2695
+ if (ctx.aiChatManager) {
2696
+ session.aiConversationCount = ctx.aiChatManager.getAIChats(request.params.id).length;
2406
2697
  }
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 };
2698
+ return session;
2416
2699
  });
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" };
2700
+ server2.delete("/_web/sessions/:id", async (request, reply) => {
2701
+ const { id } = request.params;
2702
+ try {
2703
+ const deleted = session_service_exports.deleteSession(adapter, id);
2704
+ if (!deleted) {
2705
+ return reply.code(404).send({ success: false, error: "File not found" });
2706
+ }
2707
+ return { success: true };
2708
+ } catch (err) {
2709
+ return reply.code(500).send({ success: false, error: String(err) });
2421
2710
  }
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");
2711
+ });
2712
+ server2.patch("/_web/sessions/:id/name", async (request) => {
2713
+ session_service_exports.renameSession(adapter, request.params.id, request.body.name);
2714
+ return { success: true };
2715
+ });
2716
+ server2.patch(
2717
+ "/_web/sessions/:id/owner",
2718
+ async (request) => {
2719
+ session_service_exports.updateSessionOwnerId(adapter, request.params.id, request.body.ownerId ?? null);
2720
+ return { success: true };
2432
2721
  }
2433
- if (!fs5.existsSync(downloadsDir)) {
2434
- fs5.mkdirSync(downloadsDir, { recursive: true });
2722
+ );
2723
+ server2.post("/_web/sessions/:id/owner/apply-profile", async (request) => {
2724
+ return owner_profile_service_exports.tryApplyOwnerProfile(adapter, preferences(), request.params.id);
2725
+ });
2726
+ server2.post(
2727
+ "/_web/sessions/:id/owner/select",
2728
+ async (request) => {
2729
+ return owner_profile_service_exports.setOwnerAndApplyProfile(
2730
+ adapter,
2731
+ preferences(),
2732
+ request.params.id,
2733
+ request.body.ownerPlatformId
2734
+ );
2435
2735
  }
2436
- const filePath = path7.join(downloadsDir, filename);
2437
- fs5.writeFileSync(filePath, buffer);
2438
- return { success: true, filePath };
2736
+ );
2737
+ server2.post("/_web/sessions/:id/owner/dismiss-prompt", async (request) => {
2738
+ owner_profile_service_exports.dismissOwnerPrompt(preferences(), request.params.id);
2739
+ return { success: true };
2439
2740
  });
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 });
2741
+ }
2742
+
2743
+ // ../../packages/http-routes/src/routes/web/sql.ts
2744
+ function registerSqlRoutes(server2, ctx) {
2745
+ const { sessionAdapter: adapter } = ctx;
2746
+ server2.post("/_web/sessions/:id/sql", async (request, reply) => {
2747
+ const db = adapter.ensureReadonly(request.params.id);
2748
+ const { sql } = request.body || {};
2749
+ if (!sql || typeof sql !== "string") {
2750
+ return reply.code(400).send({ error: "Missing sql parameter" });
2456
2751
  }
2457
2752
  try {
2458
- await ctx.openDirectory(dirPath);
2459
- } catch (error) {
2460
- return { success: false, error: error instanceof Error ? error.message : String(error) };
2753
+ const result = executeSql(db, sql, { columnar: true, timing: true, maxRows: 0 });
2754
+ return {
2755
+ columns: result.columns,
2756
+ rows: result.rows,
2757
+ rowCount: result.rowCount,
2758
+ duration: result.duration ?? 0,
2759
+ limited: result.truncated
2760
+ };
2761
+ } catch (err) {
2762
+ const message = err instanceof Error ? err.message : "SQL execution error";
2763
+ return reply.code(400).send({ error: message });
2461
2764
  }
2462
- return { success: true };
2463
2765
  });
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) };
2766
+ server2.get("/_web/sessions/:id/schema", async (request) => {
2767
+ const db = adapter.ensureReadonly(request.params.id);
2768
+ return getSchemaDetailed(db);
2769
+ });
2770
+ server2.post("/_web/sessions/:id/query", async (request) => {
2771
+ const db = adapter.ensureReadonly(request.params.id);
2772
+ const { sql, params = [] } = request.body;
2773
+ if (!sql || typeof sql !== "string") {
2774
+ throw Object.assign(new Error('Missing or invalid "sql" field'), { statusCode: 400 });
2472
2775
  }
2473
- return { success: true };
2776
+ const stmt = db.prepare(sql.trim());
2777
+ if (!stmt.readonly) {
2778
+ throw Object.assign(new Error("Only READ-ONLY statements are allowed"), { statusCode: 403 });
2779
+ }
2780
+ if (Array.isArray(params)) {
2781
+ return stmt.all(...params);
2782
+ }
2783
+ return stmt.all(params);
2474
2784
  });
2475
2785
  }
2476
2786
 
@@ -2500,25 +2810,9 @@ function registerTelemetryRoutes(server2, ctx) {
2500
2810
  );
2501
2811
  }
2502
2812
 
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) {
2813
+ // ../../packages/http-routes/src/routes/web/register.ts
2814
+ function registerWebRoutes(server2, ctx, options) {
2518
2815
  const resolvedCtx = ctx.preferencesManager ? ctx : { ...ctx, preferencesManager: new PreferencesManager(ctx.pathProvider.getSystemDir()) };
2519
- registerSystemRoutes(server2, resolvedCtx);
2520
- registerRestSessionRoutes(server2, resolvedCtx);
2521
- registerImportRoutes(server2, resolvedCtx);
2522
2816
  registerSessionRoutes(server2, resolvedCtx);
2523
2817
  registerMemberRoutes(server2, resolvedCtx);
2524
2818
  registerContactsRoutes(server2, resolvedCtx);
@@ -2530,77 +2824,17 @@ function registerSharedRoutes(server2, ctx, options) {
2530
2824
  registerSessionIndexRoutes(server2, resolvedCtx);
2531
2825
  registerExportRoutes(server2, resolvedCtx);
2532
2826
  registerNlpRoutes(server2, resolvedCtx);
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);
2827
+ registerAiRoutes(server2, resolvedCtx, options);
2557
2828
  registerMergeRoutes(server2, resolvedCtx);
2558
2829
  registerCacheRoutes(server2, resolvedCtx);
2559
2830
  registerTelemetryRoutes(server2, resolvedCtx);
2560
- registerLogRoutes(server2, resolvedCtx);
2831
+ registerLogRoutes(server2);
2561
2832
  }
2562
2833
 
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
- }
2834
+ // ../../packages/http-routes/src/register.ts
2835
+ function registerSharedRoutes(server2, ctx, options) {
2836
+ registerRestRoutes(server2, ctx);
2837
+ registerWebRoutes(server2, ctx, options);
2604
2838
  }
2605
2839
 
2606
2840
  // ../../packages/http-routes/src/routes/web/automation.ts
@@ -2747,44 +2981,8 @@ function registerAutomationRoutes(server2, ctx) {
2747
2981
  });
2748
2982
  }
2749
2983
 
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
-
2785
2984
  // src/http/routes/web/import.ts
2786
2985
  import * as fs7 from "fs";
2787
- import * as os2 from "os";
2788
2986
  import * as path8 from "path";
2789
2987
  import { randomUUID } from "crypto";
2790
2988
  import { pipeline } from "stream/promises";
@@ -2815,9 +3013,7 @@ function cleanupTemp(...paths) {
2815
3013
  }
2816
3014
  }
2817
3015
  function registerImportRoutes2(server2, dbManager2, options = {}) {
2818
- const sourceManager = options.sourceManager ?? new ArchiveImportSourceManager({
2819
- tempRoot: fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-import-sources-"))
2820
- });
3016
+ const sourceManager = options.sourceManager ?? new ArchiveImportSourceManager();
2821
3017
  const runAutoImport = options.runAutoImport ?? autoImport.bind(null, dbManager2);
2822
3018
  const runPreparedImport = options.runPreparedImport ?? (async (manifestPath, onProgress) => {
2823
3019
  const result = await runAutoImport(manifestPath, {
@@ -2835,7 +3031,7 @@ function registerImportRoutes2(server2, dbManager2, options = {}) {
2835
3031
  limits: { fileSize: ARCHIVE_UPLOAD_LIMIT }
2836
3032
  });
2837
3033
  if (!data) return reply.code(400).send({ success: false, error: "error.no_file_selected" });
2838
- const uploadPath = path8.join(os2.tmpdir(), `chatlab-archive-${randomUUID()}.zip`);
3034
+ const uploadPath = path8.join(getChatLabTempScopeDir("imports"), `archive-${randomUUID()}.zip`);
2839
3035
  try {
2840
3036
  await pipeline(data.file, fs7.createWriteStream(uploadPath, { flags: "wx" }));
2841
3037
  if (data.file.truncated) {
@@ -2902,7 +3098,7 @@ data: ${JSON.stringify(eventData)}
2902
3098
  server2.post("/_web/detect-format", async (request, reply) => {
2903
3099
  const data = await request.file();
2904
3100
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
2905
- const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-detect-"));
3101
+ const tmpDir = createChatLabTempDir("imports", "detect-");
2906
3102
  const tmpPath = path8.join(tmpDir, data.filename || "upload");
2907
3103
  try {
2908
3104
  const chunks = [];
@@ -2920,7 +3116,7 @@ data: ${JSON.stringify(eventData)}
2920
3116
  server2.post("/_web/scan-multi-chat", async (request, reply) => {
2921
3117
  const data = await request.file();
2922
3118
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
2923
- const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-scan-"));
3119
+ const tmpDir = createChatLabTempDir("imports", "scan-");
2924
3120
  const tmpPath = path8.join(tmpDir, data.filename || "upload");
2925
3121
  try {
2926
3122
  const chunks = [];
@@ -2937,7 +3133,7 @@ data: ${JSON.stringify(eventData)}
2937
3133
  server2.post("/_web/import", async (request, reply) => {
2938
3134
  const data = await request.file();
2939
3135
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
2940
- const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-import-"));
3136
+ const tmpDir = createChatLabTempDir("imports", "upload-");
2941
3137
  const tmpPath = path8.join(tmpDir, data.filename || "upload");
2942
3138
  const chunks = [];
2943
3139
  for await (const chunk of data.file) {
@@ -2981,7 +3177,7 @@ data: ${JSON.stringify(eventData)}
2981
3177
  server2.post("/_web/import-directory", async (request, reply) => {
2982
3178
  const parts = request.parts();
2983
3179
  if (!parts) return reply.code(400).send({ error: "No files uploaded" });
2984
- const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-dir-import-"));
3180
+ const tmpDir = createChatLabTempDir("imports", "directory-");
2985
3181
  const relativePaths = [];
2986
3182
  const fileBuffers = [];
2987
3183
  try {
@@ -3052,7 +3248,7 @@ data: ${JSON.stringify({ success: false, error: err instanceof Error ? err.messa
3052
3248
  const sessionId = request.params.id;
3053
3249
  const data = await request.file();
3054
3250
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
3055
- const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-inc-"));
3251
+ const tmpDir = createChatLabTempDir("imports", "incremental-");
3056
3252
  const tmpPath = path8.join(tmpDir, data.filename || "upload");
3057
3253
  const chunks = [];
3058
3254
  for await (const chunk of data.file) {
@@ -3090,7 +3286,7 @@ data: ${JSON.stringify(eventData)}
3090
3286
  const sessionId = request.params.id;
3091
3287
  const data = await request.file();
3092
3288
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
3093
- const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-analyze-"));
3289
+ const tmpDir = createChatLabTempDir("imports", "analyze-");
3094
3290
  const tmpPath = path8.join(tmpDir, data.filename || "upload");
3095
3291
  const chunks = [];
3096
3292
  for await (const chunk of data.file) {
@@ -3108,7 +3304,7 @@ data: ${JSON.stringify(eventData)}
3108
3304
  server2.post("/_web/import/analyze", async (request, reply) => {
3109
3305
  const data = await request.file();
3110
3306
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
3111
- const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-analyze-"));
3307
+ const tmpDir = createChatLabTempDir("imports", "analyze-");
3112
3308
  const tmpPath = path8.join(tmpDir, data.filename || "upload");
3113
3309
  const chunks = [];
3114
3310
  for await (const chunk of data.file) {
@@ -3144,7 +3340,7 @@ data: ${JSON.stringify(eventData)}
3144
3340
 
3145
3341
  `);
3146
3342
  }
3147
- const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-demo-"));
3343
+ const tmpDir = createChatLabTempDir("imports", "cli-demo-");
3148
3344
  try {
3149
3345
  const localPaths = [];
3150
3346
  for (let i = 0; i < total; i++) {
@@ -3231,29 +3427,29 @@ function buildWebUpdateCheckResult(options) {
3231
3427
  }
3232
3428
 
3233
3429
  // src/http/routes/web/index.ts
3234
- function registerWebRoutes(server2, dbManager2, options) {
3430
+ function registerWebRoutes2(server2, dbManager2, options) {
3235
3431
  const adapter = createDatabaseManagerAdapter(dbManager2);
3236
3432
  const mergeCache = options?.pathProvider ? new MergeSessionCache(options.pathProvider, { nativeBinding: options.nativeBinding }) : null;
3237
3433
  mergeCache?.cleanupOrphans();
3238
3434
  const fallbackPathProvider = {
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")
3435
+ getSystemDir: () => path10.join(os.homedir(), ".chatlab"),
3436
+ getUserDataDir: () => path10.join(os.homedir(), ".chatlab", "data"),
3437
+ getDatabaseDir: () => path10.join(os.homedir(), ".chatlab", "data", "databases"),
3438
+ getVectorDir: () => path10.join(os.homedir(), ".chatlab", "data", "vector"),
3439
+ getAiDataDir: () => path10.join(os.homedir(), ".chatlab", "ai"),
3440
+ getSettingsDir: () => path10.join(os.homedir(), ".chatlab", "settings"),
3441
+ getCacheDir: () => path10.join(os.homedir(), ".chatlab", "cache"),
3442
+ getTempDir: () => getChatLabTempScopeDir("runtime"),
3443
+ getLogsDir: () => path10.join(os.homedir(), ".chatlab", "logs"),
3444
+ getDownloadsDir: () => path10.join(os.homedir(), "Downloads")
3249
3445
  };
3250
3446
  const resolvedPathProvider = options?.pathProvider ?? fallbackPathProvider;
3251
3447
  const ai = options?.aiContext;
3252
3448
  const cliStreamImport = async (dm, filePath) => {
3253
- const { streamImport: streamImport2 } = await import("./stream-import-BPCHGRAN.mjs");
3449
+ const { streamImport: streamImport2 } = await import("./stream-import-ERTT7U6V.mjs");
3254
3450
  const result = await streamImport2(dm, filePath);
3255
3451
  if (!result.success) {
3256
- const { IMPORT_IN_PROGRESS_ERROR_KEY: IMPORT_IN_PROGRESS_ERROR_KEY2, ImportInProgressError: ImportInProgressError2 } = await import("./src-MNH5S423.mjs");
3452
+ const { IMPORT_IN_PROGRESS_ERROR_KEY: IMPORT_IN_PROGRESS_ERROR_KEY2, ImportInProgressError: ImportInProgressError2 } = await import("./src-ZOCLFBCJ.mjs");
3257
3453
  if (result.error === IMPORT_IN_PROGRESS_ERROR_KEY2) throw new ImportInProgressError2();
3258
3454
  throw new Error(result.error || "Import failed");
3259
3455
  }
@@ -3357,73 +3553,14 @@ function registerProxyRoutes(server2) {
3357
3553
  });
3358
3554
  }
3359
3555
 
3360
- // src/ai/manager-factory.ts
3361
- import * as fs8 from "fs";
3362
- import * as path11 from "path";
3363
- import { createHash, randomUUID as randomUUID2 } from "crypto";
3364
- var nodeFs = {
3365
- ensureDir(dir) {
3366
- if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
3367
- },
3368
- listFiles(dir, ext) {
3369
- if (!fs8.existsSync(dir)) return [];
3370
- return fs8.readdirSync(dir).filter((f) => f.endsWith(ext));
3371
- },
3372
- readFile(filePath) {
3373
- return fs8.readFileSync(filePath, "utf-8");
3374
- },
3375
- writeFile(filePath, content) {
3376
- fs8.writeFileSync(filePath, content, "utf-8");
3377
- },
3378
- deleteFile(filePath) {
3379
- if (fs8.existsSync(filePath)) fs8.unlinkSync(filePath);
3380
- },
3381
- fileExists(filePath) {
3382
- return fs8.existsSync(filePath);
3383
- },
3384
- joinPath(...parts) {
3385
- return path11.join(...parts);
3386
- }
3387
- };
3388
- var managerCache = /* @__PURE__ */ new Map();
3389
- function getAssistantManager(aiDataDir) {
3390
- let cached = managerCache.get(aiDataDir);
3391
- if (!cached) {
3392
- cached = {
3393
- assistant: new AssistantManager({
3394
- fs: nodeFs,
3395
- assistantsDir: path11.join(aiDataDir, "assistants"),
3396
- builtinRawConfigs: [],
3397
- generateId: () => `custom_${randomUUID2().replace(/-/g, "").slice(0, 12)}`
3398
- }),
3399
- skill: new SkillManagerCore({
3400
- fs: nodeFs,
3401
- skillsDir: path11.join(aiDataDir, "skills"),
3402
- builtinRawSkills: [],
3403
- contentHash: (content) => createHash("md5").update(content).digest("hex")
3404
- })
3405
- };
3406
- managerCache.set(aiDataDir, cached);
3407
- }
3408
- return cached.assistant;
3409
- }
3410
- function getSkillManagerCore(aiDataDir) {
3411
- let cached = managerCache.get(aiDataDir);
3412
- if (!cached) {
3413
- getAssistantManager(aiDataDir);
3414
- cached = managerCache.get(aiDataDir);
3415
- }
3416
- return cached.skill;
3417
- }
3418
-
3419
3556
  // ../../packages/sync/src/types.ts
3420
3557
  var noop = () => {
3421
3558
  };
3422
3559
  var NOOP_LOGGER = { info: noop, warn: noop, error: noop };
3423
3560
 
3424
3561
  // ../../packages/sync/src/data-source-manager.ts
3425
- import * as fs9 from "fs";
3426
- import * as path12 from "path";
3562
+ import * as fs8 from "fs";
3563
+ import * as path11 from "path";
3427
3564
  import * as crypto from "crypto";
3428
3565
  var CONFIG_FILE = "data-sources.json";
3429
3566
  function generateId(prefix = "ds") {
@@ -3451,17 +3588,17 @@ var DataSourceManager = class {
3451
3588
  this.logger = logger ?? NOOP_LOGGER;
3452
3589
  }
3453
3590
  getConfigPath() {
3454
- return path12.join(this.configDir, CONFIG_FILE);
3591
+ return path11.join(this.configDir, CONFIG_FILE);
3455
3592
  }
3456
3593
  ensureDir() {
3457
- fs9.mkdirSync(this.configDir, { recursive: true });
3594
+ fs8.mkdirSync(this.configDir, { recursive: true });
3458
3595
  }
3459
3596
  // ==================== Load / Save ====================
3460
3597
  loadAll() {
3461
3598
  try {
3462
3599
  const filePath = this.getConfigPath();
3463
- if (fs9.existsSync(filePath)) {
3464
- const raw = fs9.readFileSync(filePath, "utf-8");
3600
+ if (fs8.existsSync(filePath)) {
3601
+ const raw = fs8.readFileSync(filePath, "utf-8");
3465
3602
  const parsed = JSON.parse(raw);
3466
3603
  if (!isValidDataSourceArray(parsed)) {
3467
3604
  this.logger.warn("[DataSource] Incompatible config format detected, returning empty.");
@@ -3480,7 +3617,7 @@ var DataSourceManager = class {
3480
3617
  saveAll(sources) {
3481
3618
  try {
3482
3619
  this.ensureDir();
3483
- fs9.writeFileSync(this.getConfigPath(), JSON.stringify(sources, null, 2), "utf-8");
3620
+ fs8.writeFileSync(this.getConfigPath(), JSON.stringify(sources, null, 2), "utf-8");
3484
3621
  } catch (err) {
3485
3622
  this.logger.error("[DataSource] Failed to save config", err);
3486
3623
  }
@@ -3574,7 +3711,7 @@ var DataSourceManager = class {
3574
3711
  };
3575
3712
 
3576
3713
  // ../../packages/sync/src/pull-engine.ts
3577
- import * as fs10 from "fs";
3714
+ import * as fs9 from "fs";
3578
3715
  import * as crypto2 from "crypto";
3579
3716
  var MAX_PAGES_PER_PULL = 5e3;
3580
3717
  var PULL_OVERLAP_SECONDS = 60;
@@ -3596,7 +3733,7 @@ function parseSyncFromFile(filePath) {
3596
3733
  try {
3597
3734
  const isJsonl = filePath.endsWith(".jsonl");
3598
3735
  if (isJsonl) {
3599
- const content = fs10.readFileSync(filePath, "utf-8");
3736
+ const content = fs9.readFileSync(filePath, "utf-8");
3600
3737
  const lines = content.trimEnd().split("\n");
3601
3738
  for (let i = lines.length - 1; i >= Math.max(0, lines.length - 5); i--) {
3602
3739
  try {
@@ -3615,7 +3752,7 @@ function parseSyncFromFile(filePath) {
3615
3752
  }
3616
3753
  return null;
3617
3754
  }
3618
- const raw = fs10.readFileSync(filePath, "utf-8");
3755
+ const raw = fs9.readFileSync(filePath, "utf-8");
3619
3756
  const parsed = JSON.parse(raw);
3620
3757
  if (parsed.sync && typeof parsed.sync === "object") {
3621
3758
  const s = parsed.sync;
@@ -3629,7 +3766,7 @@ function parseSyncFromFile(filePath) {
3629
3766
  function fileContainsMessages(filePath) {
3630
3767
  try {
3631
3768
  if (filePath.endsWith(".jsonl")) {
3632
- const content = fs10.readFileSync(filePath, "utf-8");
3769
+ const content = fs9.readFileSync(filePath, "utf-8");
3633
3770
  for (const line of content.split("\n")) {
3634
3771
  const trimmed = line.trim();
3635
3772
  if (!trimmed) continue;
@@ -3642,7 +3779,7 @@ function fileContainsMessages(filePath) {
3642
3779
  }
3643
3780
  return false;
3644
3781
  }
3645
- const raw = fs10.readFileSync(filePath, "utf-8");
3782
+ const raw = fs9.readFileSync(filePath, "utf-8");
3646
3783
  const parsed = JSON.parse(raw);
3647
3784
  return Array.isArray(parsed.messages) && parsed.messages.length > 0;
3648
3785
  } catch {
@@ -3659,7 +3796,7 @@ function getMaxMessageTimestampFromFile(filePath) {
3659
3796
  }
3660
3797
  };
3661
3798
  if (filePath.endsWith(".jsonl")) {
3662
- const content = fs10.readFileSync(filePath, "utf-8");
3799
+ const content = fs9.readFileSync(filePath, "utf-8");
3663
3800
  for (const line of content.split("\n")) {
3664
3801
  const trimmed = line.trim();
3665
3802
  if (!trimmed) continue;
@@ -3672,7 +3809,7 @@ function getMaxMessageTimestampFromFile(filePath) {
3672
3809
  }
3673
3810
  return maxTs;
3674
3811
  }
3675
- const raw = fs10.readFileSync(filePath, "utf-8");
3812
+ const raw = fs9.readFileSync(filePath, "utf-8");
3676
3813
  const parsed = JSON.parse(raw);
3677
3814
  if (Array.isArray(parsed.messages)) {
3678
3815
  for (const message of parsed.messages) {
@@ -3686,7 +3823,7 @@ function getMaxMessageTimestampFromFile(filePath) {
3686
3823
  }
3687
3824
  function cleanupTempFile(filePath) {
3688
3825
  try {
3689
- if (fs10.existsSync(filePath)) fs10.unlinkSync(filePath);
3826
+ if (fs9.existsSync(filePath)) fs9.unlinkSync(filePath);
3690
3827
  } catch {
3691
3828
  }
3692
3829
  }
@@ -3783,7 +3920,7 @@ var PullEngine = class {
3783
3920
  limit: ds.pullLimit
3784
3921
  });
3785
3922
  try {
3786
- const stat2 = fs10.statSync(tempFile);
3923
+ const stat2 = fs9.statSync(tempFile);
3787
3924
  this.logger.info(`[Pull] "${sess.name}" page ${pageCount}: fetched ${stat2.size} bytes`);
3788
3925
  const sync0 = parseSyncFromFile(tempFile);
3789
3926
  if (stat2.size < 1024) {
@@ -3803,7 +3940,7 @@ var PullEngine = class {
3803
3940
  offset,
3804
3941
  limit: ds.pullLimit
3805
3942
  });
3806
- const retryStat = fs10.statSync(retryFile);
3943
+ const retryStat = fs9.statSync(retryFile);
3807
3944
  this.logger.info(`[Pull] "${sess.name}" retry ${ri + 1}: fetched ${retryStat.size} bytes`);
3808
3945
  const retrySync = parseSyncFromFile(retryFile);
3809
3946
  if (retryStat.size < 1024 && !fileContainsMessages(retryFile) && retrySync?.hasMore === false) {
@@ -4153,13 +4290,12 @@ function reloadTimer(dsId, skipInitialPull = false) {
4153
4290
  }
4154
4291
 
4155
4292
  // src/sync/adapters.ts
4156
- import * as fs11 from "fs";
4157
- import * as os4 from "os";
4158
- import * as path13 from "path";
4293
+ import * as fs10 from "fs";
4294
+ import * as path12 from "path";
4159
4295
  import * as crypto3 from "crypto";
4160
4296
  function getTempFilePath(ext) {
4161
4297
  const id = crypto3.randomBytes(8).toString("hex");
4162
- return path13.join(os4.tmpdir(), `chatlab-pull-${id}${ext}`);
4298
+ return path12.join(getChatLabTempScopeDir("sync"), `pull-${id}${ext}`);
4163
4299
  }
4164
4300
  var NodeFetcher = class {
4165
4301
  async fetchToTempFile(baseUrl, remoteSessionId, token, params) {
@@ -4176,7 +4312,7 @@ var NodeFetcher = class {
4176
4312
  const isJsonl = contentType.includes("ndjson") || contentType.includes("jsonl");
4177
4313
  const tempFile = getTempFilePath(isJsonl ? ".jsonl" : ".json");
4178
4314
  const buffer = Buffer.from(await response.arrayBuffer());
4179
- fs11.writeFileSync(tempFile, buffer);
4315
+ fs10.writeFileSync(tempFile, buffer);
4180
4316
  return tempFile;
4181
4317
  }
4182
4318
  };
@@ -4189,7 +4325,7 @@ var DirectImporter = class {
4189
4325
  }
4190
4326
  sessionExists(sessionId) {
4191
4327
  const dbPath = this.dbManager.getDbPath(sessionId);
4192
- if (!fs11.existsSync(dbPath)) return false;
4328
+ if (!fs10.existsSync(dbPath)) return false;
4193
4329
  try {
4194
4330
  const db = this.dbManager.openRawSessionDatabase(sessionId, { readonly: true });
4195
4331
  const row = db.prepare("SELECT COUNT(*) as cnt FROM sqlite_master WHERE type='table' AND name='message'").get();
@@ -4197,7 +4333,7 @@ var DirectImporter = class {
4197
4333
  if (row.cnt === 0) {
4198
4334
  this.logger.warn(`[DirectImporter] DB file exists but has no message table: ${sessionId}, removing`);
4199
4335
  try {
4200
- fs11.unlinkSync(dbPath);
4336
+ fs10.unlinkSync(dbPath);
4201
4337
  } catch {
4202
4338
  }
4203
4339
  return false;
@@ -4207,7 +4343,7 @@ var DirectImporter = class {
4207
4343
  if (error instanceof DataDirCompatibilityError) throw error;
4208
4344
  this.logger.warn(`[DirectImporter] Cannot validate DB file: ${sessionId}, removing`);
4209
4345
  try {
4210
- fs11.unlinkSync(dbPath);
4346
+ fs10.unlinkSync(dbPath);
4211
4347
  } catch {
4212
4348
  }
4213
4349
  return false;
@@ -4330,25 +4466,10 @@ function cleanupSync() {
4330
4466
  var server = null;
4331
4467
  var dbManager = null;
4332
4468
  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
- }
4348
4469
  function resolveNativeBinding2() {
4349
4470
  if (process.versions.electron) return void 0;
4350
4471
  const nativePath = resolveCliPath("native/better_sqlite3.node");
4351
- if (fs12.existsSync(nativePath)) return nativePath;
4472
+ if (fs11.existsSync(nativePath)) return nativePath;
4352
4473
  return void 0;
4353
4474
  }
4354
4475
  function ensureToken(config) {
@@ -4402,20 +4523,11 @@ async function startHttpServer(options) {
4402
4523
  aiChatManager = new AIChatManager(aiDataDir, { nativeBinding });
4403
4524
  const assistantManager = getAssistantManager(aiDataDir);
4404
4525
  const skillManagerCore = getSkillManagerCore(aiDataDir);
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
- });
4526
+ const configStorage = createFileConfigStorage(aiDataDir);
4527
+ const llmConfigStore = createAuthProfileLlmConfigStore(configStorage);
4417
4528
  initAppLogger(pathProvider.getLogsDir());
4418
4529
  initServerAiLogger(pathProvider.getLogsDir());
4530
+ appLogger.info("temp-workspace", "Temporary workspace initialized", { root: pathProvider.getTempDir() });
4419
4531
  appLogger.info("server", `HTTP server starting on ${host}:${port}`);
4420
4532
  logNativeParserStatus();
4421
4533
  setAuthToken(token);
@@ -4439,7 +4551,7 @@ async function startHttpServer(options) {
4439
4551
  console.warn("[semantic-index] worker client unavailable:", err instanceof Error ? err.message : String(err));
4440
4552
  semanticIndexService = void 0;
4441
4553
  }
4442
- registerWebRoutes(server, dbManager, {
4554
+ registerWebRoutes2(server, dbManager, {
4443
4555
  pathProvider,
4444
4556
  nativeBinding,
4445
4557
  runtimeIdentity: runtime,
@@ -4450,13 +4562,13 @@ async function startHttpServer(options) {
4450
4562
  assistantManager,
4451
4563
  skillManagerCore,
4452
4564
  llmConfigStore,
4453
- customProviderStore: new CustomProviderStore(createFileConfigStorage(aiDataDir)),
4454
- customModelStore: new CustomModelStore(createFileConfigStorage(aiDataDir)),
4565
+ customProviderStore: new CustomProviderStore(configStorage),
4566
+ customModelStore: new CustomModelStore(configStorage),
4455
4567
  runAgentStream: createCliRunAgentStream(dbManager, aiChatManager, semanticIndexService)
4456
4568
  }
4457
4569
  });
4458
4570
  initSync(server, dbManager, pathProvider, { port, host, token });
4459
- if (options?.webRoot && fs12.existsSync(options.webRoot)) {
4571
+ if (options?.webRoot && fs11.existsSync(options.webRoot)) {
4460
4572
  registerProxyRoutes(server);
4461
4573
  const fastifyStatic = await import("@fastify/static");
4462
4574
  await server.register(fastifyStatic.default, {
@@ -4493,8 +4605,8 @@ async function stopHttpServer() {
4493
4605
 
4494
4606
  export {
4495
4607
  createServer,
4496
- registerWebRoutes,
4608
+ registerWebRoutes2 as registerWebRoutes,
4497
4609
  startHttpServer,
4498
4610
  stopHttpServer
4499
4611
  };
4500
- //# sourceMappingURL=chunk-BSSR4ZYW.mjs.map
4612
+ //# sourceMappingURL=chunk-GZO3NAHC.mjs.map