chatlab-cli 0.31.1 → 0.31.2-beta.1

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 (129) hide show
  1. package/dist/{chat-command-MOETAGU5.mjs → chat-command-V6NYTTPD.mjs} +8 -8
  2. package/dist/{chunk-ZVWUR7ET.mjs → chunk-54GVCXHZ.mjs} +2 -2
  3. package/dist/{chunk-KCPGEILH.mjs → chunk-5G2INROS.mjs} +483 -356
  4. package/dist/chunk-5G2INROS.mjs.map +1 -0
  5. package/dist/{chunk-SZEBCNBB.mjs → chunk-67HNEIKY.mjs} +2084 -1924
  6. package/dist/chunk-67HNEIKY.mjs.map +1 -0
  7. package/dist/{chunk-VOVLPSG6.mjs → chunk-7TDYBU5L.mjs} +4 -4
  8. package/dist/{chunk-LJXSUN25.mjs → chunk-HUKZ2PBC.mjs} +2 -2
  9. package/dist/{chunk-3VMKS7PU.mjs → chunk-LWC7JQ2W.mjs} +2 -2
  10. package/dist/{chunk-N5AGDT7I.mjs → chunk-OIZ64ET3.mjs} +3 -3
  11. package/dist/{chunk-E6W2RURS.mjs → chunk-SJQVAKFA.mjs} +5 -4
  12. package/dist/chunk-SJQVAKFA.mjs.map +1 -0
  13. package/dist/{chunk-2IXPWPLR.mjs → chunk-WL2ISRYH.mjs} +11 -11
  14. package/dist/{chunk-DQ45LDKZ.mjs → chunk-XZ5FFDLX.mjs} +125 -136
  15. package/dist/chunk-XZ5FFDLX.mjs.map +1 -0
  16. package/dist/{chunk-7HBVGVOB.mjs → chunk-ZF67S6RY.mjs} +2 -2
  17. package/dist/cli.mjs +8 -8
  18. package/dist/contacts-worker.mjs +2 -2
  19. package/dist/global-insight-worker.mjs +1 -1
  20. package/dist/{http-M6NHWJZD.mjs → http-AMDWRKOH.mjs} +11 -11
  21. package/dist/{import-SEYHFXS5.mjs → import-WIJHB4LH.mjs} +7 -7
  22. package/dist/index.mjs +11 -11
  23. package/dist/{mcp-MIPFYRLV.mjs → mcp-NC5YNTM5.mjs} +8 -8
  24. package/dist/people-relationships-worker.mjs +2 -2
  25. package/dist/semantic-index-worker.mjs +1 -1
  26. package/dist/{src-7OD6RD5V.mjs → src-UY4GCVNF.mjs} +22 -6
  27. package/dist/{stream-import-AWEHGQJY.mjs → stream-import-JKQYRISL.mjs} +7 -7
  28. package/dist/{worker-runtime-TGYMASIE.mjs → worker-runtime-KKUOJJTN.mjs} +5 -5
  29. package/dist-web/assets/{ActionToolsPanel-fAio70bI.css → ActionToolsPanel-BL0GQR6r.css} +1 -1
  30. package/dist-web/assets/ActionToolsPanel.vue_vue_type_script_setup_true_lang-DVdpZpfD.js +110 -0
  31. package/dist-web/assets/{BufferResource-CHI62pIA.js → BufferResource-i1amDJTV.js} +1 -1
  32. package/dist-web/assets/{CanvasRenderer-OZRoiF0j.js → CanvasRenderer-DGa7AjdC.js} +1 -1
  33. package/dist-web/assets/EChartWordcloud-BXC_4nUP.js +1 -0
  34. package/dist-web/assets/{Filter-DNb5VErW.js → Filter-BTlq0yU-.js} +1 -1
  35. package/dist-web/assets/LoadingState.vue_vue_type_script_setup_true_lang-B2KAoiMG.js +1 -0
  36. package/dist-web/assets/MessageExportModal-D9C5t_zF.js +1 -0
  37. package/dist-web/assets/PageHeader-BEWr2vsg.js +1 -0
  38. package/dist-web/assets/{RelationshipGalaxyCanvas-ioRNObb5.js → RelationshipGalaxyCanvas-CSgPNRif.js} +4 -4
  39. package/dist-web/assets/{RelationshipGalaxyThreeCanvas-L1Y43p1A.js → RelationshipGalaxyThreeCanvas-CBAnU8vN.js} +1 -1
  40. package/dist-web/assets/{RenderTargetSystem-fJ7D7Chv.js → RenderTargetSystem-Ctlu222X.js} +1 -1
  41. package/dist-web/assets/ThemeCard.vue_vue_type_script_setup_true_lang-BWJ6kuSC.js +1 -0
  42. package/dist-web/assets/TimeSelect.vue_vue_type_script_setup_true_lang-D5vov0eO.js +1 -0
  43. package/dist-web/assets/{WebGLRenderer-BD3ZUtxH.js → WebGLRenderer-D2Zvhmcw.js} +1 -1
  44. package/dist-web/assets/{WebGPURenderer-DZXYsT6D.js → WebGPURenderer-BOtP6h9o.js} +1 -1
  45. package/dist-web/assets/annual-summary-time-range-CK0IIPsY.js +1 -0
  46. package/dist-web/assets/{base-DTElkJcC.js → base-BMjDSaj6.js} +1 -1
  47. package/dist-web/assets/{browserAll-ChT6N6a9.js → browserAll-BhwLvWQY.js} +1 -1
  48. package/dist-web/assets/dateFormat-dteP1INW.js +1 -0
  49. package/dist-web/assets/{electron-CIzyGxH_.js → electron-DCgfulPE.js} +1 -1
  50. package/dist-web/assets/{fetch-CuiQUcO_.js → fetch-6q7oz7ZO.js} +1 -1
  51. package/dist-web/assets/{fetch-Avtnh8cJ.js → fetch-B_0HIxs5.js} +1 -1
  52. package/dist-web/assets/{fetch-DnGx-1_t.js → fetch-BbU4MPPu.js} +1 -1
  53. package/dist-web/assets/{fetch-Dd6t3p1r.js → fetch-BxHdB6X1.js} +1 -1
  54. package/dist-web/assets/{fetch-qC7om4QG.js → fetch-CpvfCafG.js} +1 -1
  55. package/dist-web/assets/{fetch-wSSfDBdV.js → fetch-CubnKpLG.js} +1 -1
  56. package/dist-web/assets/{fetch-CyKILLnB.js → fetch-D5BrGna5.js} +1 -1
  57. package/dist-web/assets/{fetch-C9rx2isC.js → fetch-DgXWr9nB.js} +1 -1
  58. package/dist-web/assets/{fetch-Ccq_0ecb.js → fetch-DvZuLuTE.js} +1 -1
  59. package/dist-web/assets/{fetch-BFnrBahy.js → fetch-rJA9qLvd.js} +1 -1
  60. package/dist-web/assets/index-BfGKlkx3.js +2 -0
  61. package/dist-web/assets/index-BoalB6nB.js +1 -0
  62. package/dist-web/assets/index-Bp5VKUF9.js +1 -0
  63. package/dist-web/assets/index-Bs15Yc7I.js +1 -0
  64. package/dist-web/assets/index-CAjUCtGD.css +1 -0
  65. package/dist-web/assets/index-CLijvqy3.js +173 -0
  66. package/dist-web/assets/{index-DXHYzIIj.js → index-CUzFFw5b.js} +1 -1
  67. package/dist-web/assets/index-Cj2GU1SR.js +1 -0
  68. package/dist-web/assets/{index-y7cxH1Et.css → index-CsqMIyA9.css} +1 -1
  69. package/dist-web/assets/index-CtmXho1w.css +1 -0
  70. package/dist-web/assets/index-D7aKwT6w.js +64 -0
  71. package/dist-web/assets/index-DXcFBsM1.js +24 -0
  72. package/dist-web/assets/index-DaZnh1zP.js +2 -0
  73. package/dist-web/assets/index-DeV8SAiM.js +25 -0
  74. package/dist-web/assets/index-WJCmqKnh.js +1 -0
  75. package/dist-web/assets/index-t6s0nOlb.js +1 -0
  76. package/dist-web/assets/people-page-header-D93GTPBO.js +1 -0
  77. package/dist-web/assets/vendor-nuxt-ui-DUpPmItD.js +15 -0
  78. package/dist-web/assets/vendor-reka-ui-BLnO2MqY.js +15 -0
  79. package/dist-web/assets/vendor-snapdom--nZvo8Sg.js +8 -0
  80. package/dist-web/assets/web-BoTDL3HM.js +1 -0
  81. package/dist-web/assets/{webworkerAll-C3dAOQj4.js → webworkerAll-c0ZvpENM.js} +1 -1
  82. package/dist-web/index.html +5 -5
  83. package/package.json +1 -1
  84. package/dist/chunk-DQ45LDKZ.mjs.map +0 -1
  85. package/dist/chunk-E6W2RURS.mjs.map +0 -1
  86. package/dist/chunk-KCPGEILH.mjs.map +0 -1
  87. package/dist/chunk-SZEBCNBB.mjs.map +0 -1
  88. package/dist-web/assets/ActionToolsPanel.vue_vue_type_script_setup_true_lang-CkDGVRXe.js +0 -110
  89. package/dist-web/assets/EChartWordcloud-DU3ZYdgz.js +0 -1
  90. package/dist-web/assets/LoadingState.vue_vue_type_script_setup_true_lang-DACMroOe.js +0 -1
  91. package/dist-web/assets/MessageExportModal-CqP-w7HP.js +0 -1
  92. package/dist-web/assets/PageHeader-Ck8ULyZH.js +0 -1
  93. package/dist-web/assets/ThemeCard.vue_vue_type_script_setup_true_lang-BoZyssJF.js +0 -1
  94. package/dist-web/assets/TimeSelect.vue_vue_type_script_setup_true_lang-7SEaCxDL.js +0 -1
  95. package/dist-web/assets/annual-summary-time-range-DVLXK1r4.js +0 -1
  96. package/dist-web/assets/dateFormat-D9WibwOu.js +0 -1
  97. package/dist-web/assets/index-BH9jI5kV.js +0 -173
  98. package/dist-web/assets/index-BWtNqcYK.js +0 -22
  99. package/dist-web/assets/index-Bkq7-GBp.js +0 -1
  100. package/dist-web/assets/index-Bqr-LkS1.css +0 -1
  101. package/dist-web/assets/index-C6qtsaP7.js +0 -1
  102. package/dist-web/assets/index-C_0GHEcb.css +0 -1
  103. package/dist-web/assets/index-ChFGuu70.js +0 -1
  104. package/dist-web/assets/index-CiS20iAR.js +0 -24
  105. package/dist-web/assets/index-CqD6u57u.js +0 -1
  106. package/dist-web/assets/index-CzDG8LVr.js +0 -2
  107. package/dist-web/assets/index-D0Euk-vQ.js +0 -1
  108. package/dist-web/assets/index-D26BWhHz.js +0 -1
  109. package/dist-web/assets/index-Dq8u7gMb.js +0 -2
  110. package/dist-web/assets/index-u_UH6rDY.js +0 -64
  111. package/dist-web/assets/people-page-header-BdKM1qhX.js +0 -1
  112. package/dist-web/assets/vendor-nuxt-ui-CS1uxE9C.js +0 -15
  113. package/dist-web/assets/vendor-reka-ui-Cta6JPvN.js +0 -11
  114. package/dist-web/assets/vendor-snapdom-XnBoh1gd.js +0 -8
  115. package/dist-web/assets/web-BDBdpdK7.js +0 -1
  116. /package/dist/{chat-command-MOETAGU5.mjs.map → chat-command-V6NYTTPD.mjs.map} +0 -0
  117. /package/dist/{chunk-ZVWUR7ET.mjs.map → chunk-54GVCXHZ.mjs.map} +0 -0
  118. /package/dist/{chunk-VOVLPSG6.mjs.map → chunk-7TDYBU5L.mjs.map} +0 -0
  119. /package/dist/{chunk-LJXSUN25.mjs.map → chunk-HUKZ2PBC.mjs.map} +0 -0
  120. /package/dist/{chunk-3VMKS7PU.mjs.map → chunk-LWC7JQ2W.mjs.map} +0 -0
  121. /package/dist/{chunk-N5AGDT7I.mjs.map → chunk-OIZ64ET3.mjs.map} +0 -0
  122. /package/dist/{chunk-2IXPWPLR.mjs.map → chunk-WL2ISRYH.mjs.map} +0 -0
  123. /package/dist/{chunk-7HBVGVOB.mjs.map → chunk-ZF67S6RY.mjs.map} +0 -0
  124. /package/dist/{http-M6NHWJZD.mjs.map → http-AMDWRKOH.mjs.map} +0 -0
  125. /package/dist/{import-SEYHFXS5.mjs.map → import-WIJHB4LH.mjs.map} +0 -0
  126. /package/dist/{mcp-MIPFYRLV.mjs.map → mcp-NC5YNTM5.mjs.map} +0 -0
  127. /package/dist/{src-7OD6RD5V.mjs.map → src-UY4GCVNF.mjs.map} +0 -0
  128. /package/dist/{stream-import-AWEHGQJY.mjs.map → stream-import-JKQYRISL.mjs.map} +0 -0
  129. /package/dist/{worker-runtime-TGYMASIE.mjs.map → worker-runtime-KKUOJJTN.mjs.map} +0 -0
@@ -9,21 +9,21 @@ import {
9
9
  createCliRunAgentStream,
10
10
  getServerAiLogger,
11
11
  initServerAiLogger
12
- } from "./chunk-VOVLPSG6.mjs";
12
+ } from "./chunk-7TDYBU5L.mjs";
13
13
  import {
14
14
  AGENT_TOOL_REGISTRY,
15
15
  CoreDataProvider
16
- } from "./chunk-3VMKS7PU.mjs";
16
+ } from "./chunk-LWC7JQ2W.mjs";
17
17
  import {
18
18
  analyzeIncrementalImport,
19
19
  analyzeNewImport,
20
20
  autoImport,
21
21
  incrementalImport,
22
22
  streamImport
23
- } from "./chunk-N5AGDT7I.mjs";
23
+ } from "./chunk-OIZ64ET3.mjs";
24
24
  import {
25
25
  assertCliDataDirCompatible
26
- } from "./chunk-ZVWUR7ET.mjs";
26
+ } from "./chunk-54GVCXHZ.mjs";
27
27
  import {
28
28
  getVersion
29
29
  } from "./chunk-5ADKYYNA.mjs";
@@ -41,7 +41,7 @@ import {
41
41
  getPendingNodeDataDirMigration,
42
42
  hasPendingElectronDataWarning,
43
43
  serializeChatLabToJsonl
44
- } from "./chunk-E6W2RURS.mjs";
44
+ } from "./chunk-SJQVAKFA.mjs";
45
45
  import {
46
46
  AIChatManager,
47
47
  ArchiveImportError,
@@ -52,7 +52,6 @@ import {
52
52
  IMPORT_IN_PROGRESS_ERROR_KEY,
53
53
  ImportIdempotencyCache,
54
54
  ImportInProgressError,
55
- LLMConfigStore,
56
55
  MergeSessionCache,
57
56
  SEMANTIC_INDEX_CONFIG_FILE,
58
57
  SemanticIndexConfigStore,
@@ -61,7 +60,10 @@ import {
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-KCPGEILH.mjs";
101
+ } from "./chunk-5G2INROS.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-DQ45LDKZ.mjs";
163
+ } from "./chunk-XZ5FFDLX.mjs";
164
164
 
165
165
  // src/http/index.ts
166
166
  import * as fs12 from "fs";
167
167
  import * as crypto4 from "crypto";
168
168
 
169
- // 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);
354
547
  if (!db) return null;
355
- const info = getSessionInfo(db);
356
- if (!info) 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
+ }
357
555
  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
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);
577
+ if (!db) return null;
578
+ const session = getDetail(dbManager2, sessionId);
579
+ const meta = getSessionMeta(db);
580
+ if (!session || !meta) return null;
581
+ const members = getMembersWithAliases(db);
582
+ const messages = queryMessages(db, { limit, offset: 0 }).messages;
583
+ return {
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,1602 @@ 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", async (request, reply) => {
863
+ const config = mgr.getAssistantConfig(request.params.id);
864
+ if (!config) return reply.code(404).send({ error: "Not found" });
865
+ return config;
697
866
  });
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
- });
867
+ server2.post("/_web/ai/assistants", async (request) => {
868
+ return mgr.createAssistant(request.body);
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.put("/_web/ai/assistants/:id", async (request, reply) => {
871
+ const result = mgr.updateAssistant(request.params.id, request.body);
872
+ if (!result.success) return reply.code(404).send(result);
873
+ return result;
712
874
  });
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
875
+ server2.delete("/_web/ai/assistants/:id", async (request, reply) => {
876
+ const result = mgr.deleteAssistant(request.params.id);
877
+ if (!result.success) return reply.code(400).send(result);
878
+ return result;
753
879
  });
754
- server2.addHook("onClose", async () => {
755
- await service.close();
880
+ server2.post("/_web/ai/assistants/:id/reset", async (request, reply) => {
881
+ const result = mgr.resetAssistant(request.params.id);
882
+ if (!result.success) return reply.code(400).send(result);
883
+ return result;
756
884
  });
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
- });
885
+ server2.post("/_web/ai/assistants/import", async (request) => {
886
+ return mgr.importAssistantFromMd(request.body.rawMd);
764
887
  });
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
- });
888
+ server2.post("/_web/ai/assistants/import-builtin", async (request, reply) => {
889
+ const result = mgr.importAssistant(request.body.builtinId);
890
+ if (!result.success) return reply.code(400).send(result);
891
+ return result;
892
+ });
893
+ server2.post("/_web/ai/assistants/:id/reimport", async (request, reply) => {
894
+ const result = mgr.reimportAssistant(request.params.id);
895
+ if (!result.success) return reply.code(400).send(result);
896
+ return result;
771
897
  });
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
- });
779
- }
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
898
  }
791
899
 
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
900
+ // ../../packages/http-routes/src/routes/web/ai-chats.ts
901
+ function registerAiChatRoutes(server2, ctx) {
902
+ const cm = ctx.aiChatManager;
903
+ if (!cm) return;
904
+ server2.post("/_web/ai/chats", async (request) => {
905
+ const { sessionId, title, assistantId } = request.body;
906
+ return cm.createAIChat(sessionId, title, assistantId);
799
907
  });
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
- });
908
+ server2.get("/_web/ai/chats", async (request) => {
909
+ const { sessionId } = request.query;
910
+ if (!sessionId) return [];
911
+ return cm.getAIChats(sessionId);
806
912
  });
807
- server2.post(
808
- "/_web/global-insight/annual-summary/recompute",
809
- async (request) => service.startRecompute(parseRange(request.query))
810
- );
811
- }
812
- function parseRange(query) {
813
- const mode = query.mode === "recent" ? "recent" : "year";
814
- return {
815
- mode,
816
- year: parseInteger(query.year),
817
- days: query.days === "365" ? 365 : void 0
818
- };
819
- }
820
- function parseInteger(value) {
821
- if (!value) return void 0;
822
- const parsed = Number(value);
823
- return Number.isInteger(parsed) ? parsed : void 0;
824
- }
825
- function isTruthy3(value) {
826
- return value === "1" || value === "true" || value === "yes";
827
- }
828
-
829
- // ../../packages/http-routes/src/routes/web/preferences.ts
830
- function registerPreferencesRoutes(server2, ctx) {
831
- const prefManager = ctx.preferencesManager ?? new PreferencesManager(ctx.pathProvider.getSystemDir());
832
- server2.get("/_web/preferences", async () => {
833
- return prefManager.load();
913
+ server2.get("/_web/ai/chats/:id", async (request, reply) => {
914
+ const conv = cm.getAIChat(request.params.id);
915
+ if (!conv) return reply.code(404).send({ error: "AI chat not found" });
916
+ return conv;
834
917
  });
835
- server2.patch("/_web/preferences", async (request) => {
836
- return prefManager.save(request.body);
918
+ server2.put("/_web/ai/chats/:id/title", async (request) => {
919
+ return cm.updateAIChatTitle(request.params.id, request.body.title);
837
920
  });
838
- server2.get("/_web/preferences/ui-config", async () => {
839
- const config = loadConfig();
840
- return config.ui;
921
+ server2.delete("/_web/ai/chats/:id", async (request) => {
922
+ return cm.deleteAIChat(request.params.id);
841
923
  });
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) };
924
+ server2.post("/_web/ai/chats/:id/messages", async (request) => {
925
+ const { role, content, dataKeywords, dataMessageCount, contentBlocks, tokenUsage } = request.body;
926
+ return cm.addMessage(
927
+ request.params.id,
928
+ role,
929
+ content,
930
+ dataKeywords,
931
+ dataMessageCount,
932
+ contentBlocks,
933
+ tokenUsage
934
+ );
935
+ });
936
+ server2.get("/_web/ai/chats/:id/messages", async (request) => {
937
+ return cm.getMessages(request.params.id);
938
+ });
939
+ server2.post("/_web/ai/chats/:id/messages/:messageId/delete-from", async (request, reply) => {
940
+ const { id, messageId } = request.params;
941
+ if (!id || typeof id !== "string" || !messageId || typeof messageId !== "string") {
942
+ return reply.code(400).send({ error: "aiChatId and messageId are required" });
852
943
  }
944
+ cm.deleteMessagesFrom(id, messageId);
945
+ return { success: true };
853
946
  });
854
- server2.get("/_web/preferences/locale", async () => {
855
- const config = loadConfig();
856
- return { lang: config.locale.lang };
947
+ server2.post("/_web/ai/chats/:id/fork", async (request, reply) => {
948
+ const { upToMessageId, title } = request.body;
949
+ if (!upToMessageId || typeof upToMessageId !== "string") {
950
+ return reply.code(400).send({ error: "upToMessageId is required" });
951
+ }
952
+ return cm.forkAIChat(request.params.id, upToMessageId, title);
857
953
  });
858
- server2.patch("/_web/preferences/locale", async (request) => {
954
+ server2.put("/_web/ai/messages/:messageId/content", async (request, reply) => {
955
+ const { content } = request.body;
956
+ if (!content || typeof content !== "string") {
957
+ return reply.code(400).send({ error: "content is required" });
958
+ }
959
+ cm.updateMessageContent(request.params.messageId, content);
960
+ return { success: true };
961
+ });
962
+ server2.post("/_web/ai/chats/:id/messages/:messageId/delete-relink", async (request, reply) => {
963
+ const { id, messageId } = request.params;
964
+ if (!id || !messageId) {
965
+ return reply.code(400).send({ error: "aiChatId and messageId are required" });
966
+ }
967
+ cm.deleteAndRelinkMessage(id, messageId);
968
+ return { success: true };
969
+ });
970
+ server2.post("/_web/ai/chats/:id/messages/insert-after", async (request, reply) => {
971
+ const { afterMessageId, role, content, contentBlocks, tokenUsage } = request.body;
972
+ if (!afterMessageId || typeof afterMessageId !== "string") {
973
+ return reply.code(400).send({ error: "afterMessageId is required" });
974
+ }
975
+ return cm.insertMessageAfter(request.params.id, afterMessageId, role, content, contentBlocks, tokenUsage);
976
+ });
977
+ server2.get("/_web/ai/chats/:id/token-usage", async (request) => {
978
+ return cm.getAIChatTokenUsage(request.params.id);
979
+ });
980
+ server2.get("/_web/ai/debug/schema", async () => {
981
+ return cm.getAiSchema();
982
+ });
983
+ server2.post("/_web/ai/debug/execute-sql", async (request, reply) => {
984
+ const { sql } = request.body;
985
+ if (!sql || typeof sql !== "string") {
986
+ return reply.code(400).send({ error: "sql is required" });
987
+ }
859
988
  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) };
989
+ return cm.executeAiSQL(sql);
990
+ } catch (e) {
991
+ const msg = e instanceof Error ? e.message : String(e);
992
+ return reply.code(400).send({ error: msg });
864
993
  }
865
994
  });
995
+ server2.post("/_web/ai/debug/clear-debug-context", async () => {
996
+ const cleared = cm.clearAllDebugContext();
997
+ return { success: true, cleared };
998
+ });
999
+ server2.get("/_web/ai/chats/:id/estimate-tokens", async (request) => {
1000
+ const history = cm.getHistoryForAgent(request.params.id);
1001
+ const tokens = countMessagesTokens(history.map((m) => ({ role: m.role, content: m.content })));
1002
+ return { success: true, tokens, messageCount: history.length };
1003
+ });
866
1004
  }
867
1005
 
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(",")}}`;
892
- }
893
- function buildAnalyticsCacheKey(namespace, params) {
894
- return `${namespace}:${canonical(params)}`;
895
- }
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);
1006
+ // ../../packages/http-routes/src/routes/web/ai-llm-stream.ts
1007
+ function registerAiLlmStreamRoutes(server2, ctx) {
1008
+ const store = ctx.llmConfigStore;
1009
+ if (!store) return;
1010
+ server2.post("/_web/ai/llm/chat-stream", async (request, reply) => {
1011
+ const { messages, options } = request.body;
1012
+ const llmConfig = store.getDefaultAssistantConfig();
1013
+ if (!llmConfig) {
1014
+ return reply.code(400).send({ success: false, error: "LLM service not configured" });
1015
+ }
1016
+ const piModel = buildPiModel(llmConfig);
1017
+ const abortController = new AbortController();
1018
+ reply.raw.writeHead(200, {
1019
+ "Content-Type": "text/event-stream",
1020
+ "Cache-Control": "no-cache",
1021
+ Connection: "keep-alive"
1022
+ });
1023
+ reply.raw.on("close", () => {
1024
+ if (!abortController.signal.aborted) abortController.abort();
1025
+ });
1026
+ const sendChunk = (data) => {
1027
+ if (reply.raw.writableEnded || reply.raw.destroyed) return;
1028
+ reply.raw.write(`event: chunk
1029
+ data: ${JSON.stringify(data)}
1030
+
1031
+ `);
1032
+ };
1033
+ try {
1034
+ await runSimpleLlmStream({
1035
+ messages,
1036
+ apiKey: llmConfig.apiKey,
1037
+ piModel,
1038
+ temperature: options?.temperature,
1039
+ maxTokens: options?.maxTokens,
1040
+ onChunk: sendChunk,
1041
+ abortSignal: abortController.signal
1042
+ });
1043
+ } catch (error) {
1044
+ if (abortController.signal.aborted) return;
1045
+ const msg = error instanceof Error ? error.message : String(error);
1046
+ sendChunk({ content: "", isFinished: true, finishReason: "error", error: msg });
1047
+ } finally {
1048
+ if (!reply.raw.writableEnded && !reply.raw.destroyed) reply.raw.end();
1049
+ }
1050
+ });
903
1051
  }
904
1052
 
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);
1053
+ // ../../packages/http-routes/src/routes/web/ai-llm.ts
1054
+ function hasRealApiKey(apiKey) {
1055
+ return !!apiKey && apiKey !== "sk-no-key-required";
1056
+ }
1057
+ function toConfigDisplay(config) {
1058
+ const { apiKey: _raw, ...rest } = config;
1059
+ return { ...rest, apiKey: "", apiKeySet: hasRealApiKey(String(_raw || "")) };
1060
+ }
1061
+ function registerAiLlmRoutes(server2, ctx) {
1062
+ const store = ctx.llmConfigStore;
1063
+ const aiDataDir = ctx.aiDataDir;
1064
+ if (!store || !aiDataDir) return;
1065
+ server2.get("/_web/ai/llm/has-config", async () => {
1066
+ return store.hasActiveConfig();
912
1067
  });
913
- server2.get("/_web/sessions/:id/time-range", async (request) => {
914
- const db = adapter.ensureReadonly(request.params.id);
915
- return getTimeRange(db);
1068
+ server2.get("/_web/ai/llm/configs", async () => {
1069
+ const data = store.loadStore();
1070
+ return {
1071
+ configs: data.configs.map((c) => toConfigDisplay(c)),
1072
+ defaultAssistant: data.defaultAssistant,
1073
+ fastModel: data.fastModel
1074
+ };
916
1075
  });
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));
1076
+ server2.post("/_web/ai/llm/configs", async (request) => {
1077
+ const result = store.addConfig(request.body);
1078
+ if (result.success && result.config) {
1079
+ return { ...result, config: toConfigDisplay(result.config) };
947
1080
  }
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));
1081
+ return result;
1082
+ });
1083
+ server2.put("/_web/ai/llm/configs/:id", async (request) => {
1084
+ return store.updateConfig(request.params.id, request.body);
1085
+ });
1086
+ server2.delete("/_web/ai/llm/configs/:id", async (request) => {
1087
+ return store.deleteConfig(request.params.id);
1088
+ });
1089
+ server2.get("/_web/ai/llm/default-assistant-slot", async () => {
1090
+ return store.getDefaultAssistantSlot();
1091
+ });
1092
+ server2.put("/_web/ai/llm/default-assistant-slot", async (request) => {
1093
+ return store.setDefaultAssistantModel(request.body.configId, request.body.modelId);
1094
+ });
1095
+ server2.get("/_web/ai/llm/fast-model-slot", async () => {
1096
+ return store.getFastModelSlot();
1097
+ });
1098
+ server2.put("/_web/ai/llm/fast-model-slot", async (request) => {
1099
+ return store.setFastModel(request.body);
1100
+ });
1101
+ server2.get("/_web/ai/llm/providers", async () => {
1102
+ return BUILTIN_PROVIDERS.map((p) => {
1103
+ const models = getBuiltinModelsByProvider(p.id);
1104
+ return {
1105
+ id: p.id,
1106
+ name: p.name,
1107
+ defaultBaseUrl: p.defaultBaseUrl,
1108
+ models: models.filter((m) => !m.capabilities.includes("embedding") && !m.capabilities.includes("ranking")).map((m) => ({ id: m.id, name: m.name, description: m.description }))
1109
+ };
1110
+ });
1111
+ });
1112
+ server2.get("/_web/ai/llm/provider-registry", async () => {
1113
+ const custom = ctx.customProviderStore?.getAll() ?? [];
1114
+ return [...BUILTIN_PROVIDERS, ...custom];
1115
+ });
1116
+ server2.get("/_web/ai/llm/model-catalog", async () => {
1117
+ const custom = ctx.customModelStore?.getAll() ?? [];
1118
+ return [...BUILTIN_MODELS, ...custom];
1119
+ });
1120
+ server2.post("/_web/ai/llm/custom-providers", async (request, reply) => {
1121
+ if (!ctx.customProviderStore)
1122
+ return reply.code(501).send({ success: false, error: "Custom providers not available" });
1123
+ return ctx.customProviderStore.add(request.body);
1124
+ });
1125
+ server2.put("/_web/ai/llm/custom-providers/:id", async (request, reply) => {
1126
+ if (!ctx.customProviderStore)
1127
+ return reply.code(501).send({ success: false, error: "Custom providers not available" });
1128
+ const result = ctx.customProviderStore.update(request.params.id, request.body);
1129
+ if (!result.success) return reply.code(404).send(result);
1130
+ return result;
1131
+ });
1132
+ server2.delete("/_web/ai/llm/custom-providers/:id", async (request, reply) => {
1133
+ if (!ctx.customProviderStore)
1134
+ return reply.code(501).send({ success: false, error: "Custom providers not available" });
1135
+ const result = ctx.customProviderStore.delete(request.params.id);
1136
+ if (!result.success) return reply.code(404).send(result);
1137
+ return result;
1138
+ });
1139
+ server2.post("/_web/ai/llm/custom-models", async (request, reply) => {
1140
+ if (!ctx.customModelStore) return reply.code(501).send({ success: false, error: "Custom models not available" });
1141
+ const result = ctx.customModelStore.add(request.body);
1142
+ if (!result.success) return reply.code(409).send(result);
1143
+ return result;
1144
+ });
1145
+ server2.put("/_web/ai/llm/custom-models/:providerId/:modelId", async (request, reply) => {
1146
+ if (!ctx.customModelStore) return reply.code(501).send({ success: false, error: "Custom models not available" });
1147
+ const result = ctx.customModelStore.update(request.params.providerId, request.params.modelId, request.body);
1148
+ if (!result.success) return reply.code(404).send(result);
1149
+ return result;
1150
+ });
1151
+ server2.delete("/_web/ai/llm/custom-models/:providerId/:modelId", async (request, reply) => {
1152
+ if (!ctx.customModelStore) return reply.code(501).send({ success: false, error: "Custom models not available" });
1153
+ const result = ctx.customModelStore.delete(request.params.providerId, request.params.modelId);
1154
+ if (!result.success) return reply.code(404).send(result);
1155
+ return result;
1156
+ });
1157
+ server2.post("/_web/ai/llm/validate-key", async (request) => {
1158
+ const { provider, apiKey, baseUrl, model, apiFormat, configId } = request.body;
1159
+ const resolvedKey = apiKey?.trim() ? apiKey : configId ? store.getConfigById(configId)?.apiKey || "" : "";
1160
+ return validateApiKey(provider, resolvedKey, baseUrl, model, apiFormat);
1161
+ });
1162
+ server2.post("/_web/ai/llm/remote-models", async (request) => {
1163
+ const { provider, apiKey, baseUrl, apiFormat, configId } = request.body;
1164
+ const resolvedKey = apiKey?.trim() ? apiKey : configId ? store.getConfigById(configId)?.apiKey || "" : "";
1165
+ return fetchRemoteModels(provider, resolvedKey, baseUrl, apiFormat);
1166
+ });
1167
+ server2.get("/_web/ai/desensitize-rules/defaults", async (request) => {
1168
+ return getDefaultRulesForLocale(request.query.locale ?? "zh-CN");
1169
+ });
1170
+ server2.post("/_web/ai/desensitize-rules/merge", async (request) => {
1171
+ return mergeRulesForLocale(request.body.existingRules, request.body.locale, request.body.overrides ?? {});
1172
+ });
1173
+ }
1174
+
1175
+ // ../../packages/http-routes/src/routes/web/ai-logs.ts
1176
+ import fs from "fs";
1177
+ import path from "path";
1178
+ function findLatestAiLogPath(logsDir) {
1179
+ const aiLogDir = path.join(logsDir, "ai");
1180
+ if (!fs.existsSync(aiLogDir)) return null;
1181
+ const latest = fs.readdirSync(aiLogDir).filter((name) => name.startsWith("ai_") && name.endsWith(".log")).map((name) => {
1182
+ const filePath = path.join(aiLogDir, name);
1183
+ const stat2 = fs.statSync(filePath);
1184
+ return { path: filePath, mtimeMs: stat2.mtimeMs };
1185
+ }).sort((a, b) => b.mtimeMs - a.mtimeMs)[0];
1186
+ return latest?.path ?? null;
1187
+ }
1188
+ function resolveAiLogPath(ctx) {
1189
+ const currentPath = ctx.getCurrentAiLogPath?.();
1190
+ if (currentPath && fs.existsSync(currentPath)) return currentPath;
1191
+ return findLatestAiLogPath(ctx.pathProvider.getLogsDir());
1192
+ }
1193
+ function isLoopbackAddress(address) {
1194
+ if (!address) return true;
1195
+ const normalized = address.replace(/^::ffff:/, "");
1196
+ return normalized === "localhost" || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1" || normalized.startsWith("127.");
1197
+ }
1198
+ function registerAiLogRoutes(server2, ctx) {
1199
+ server2.post("/_web/ai/logs/show", async (request, reply) => {
1200
+ if (!ctx.showInFolder) return reply.code(501).send({ success: false, error: "Not supported" });
1201
+ if (!isLoopbackAddress(request.ip)) {
1202
+ return { success: false, error: "Opening AI logs is only supported on this device" };
955
1203
  }
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));
1204
+ const logPath = resolveAiLogPath(ctx);
1205
+ if (!logPath) return { success: false, error: "No AI log files found" };
1206
+ try {
1207
+ await ctx.showInFolder(logPath);
1208
+ return { success: true, path: logPath };
1209
+ } catch (error) {
1210
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
963
1211
  }
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
1212
  });
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}` }
1213
+ }
1214
+
1215
+ // ../../packages/http-routes/src/routes/web/ai-semantic-index.ts
1216
+ import path2 from "path";
1217
+ function registerSemanticIndexRoutes(server2, ctx) {
1218
+ const service = ctx.semanticIndexService;
1219
+ if (!service) {
1220
+ if (ctx.aiDataDir) {
1221
+ const configStore = new SemanticIndexConfigStore(path2.join(ctx.aiDataDir, SEMANTIC_INDEX_CONFIG_FILE));
1222
+ const configResponse = (config) => ({
1223
+ config,
1224
+ apiKeySet: resolveSemanticIndexApiKeySet(config, ctx.resolveApiKey),
1225
+ configured: isSemanticIndexConfigured(config)
1226
+ });
1227
+ server2.get("/_web/ai/semantic-index/config", async () => ({
1228
+ ...configResponse(configStore.get()),
1229
+ modelStatus: "idle"
1230
+ }));
1231
+ server2.put(
1232
+ "/_web/ai/semantic-index/config",
1233
+ async (request) => {
1234
+ const config = persistSemanticIndexConfig(configStore, request.body.config, {
1235
+ apiKey: request.body.apiKey,
1236
+ writeAuthProfile: ctx.writeAuthProfile
1237
+ });
1238
+ return { ...configResponse(config), modelStatus: "idle" };
1239
+ }
1036
1240
  );
1037
1241
  }
1038
- );
1039
- server2.get(
1040
- "/_web/sessions/:id/analytics/monthly-activity",
1242
+ server2.get("/_web/ai/semantic-index/enabled", async () => ({ sessions: [] }));
1243
+ server2.get("/_web/ai/semantic-index/status", async () => ({ status: null }));
1244
+ server2.post("/_web/ai/semantic-index/status", async () => ({ sessions: [] }));
1245
+ return;
1246
+ }
1247
+ server2.get("/_web/ai/semantic-index/config", async () => {
1248
+ return {
1249
+ config: await service.getConfig(),
1250
+ apiKeySet: await service.hasApiKey(),
1251
+ configured: await service.isConfigured(),
1252
+ modelStatus: await service.getModelStatus()
1253
+ };
1254
+ });
1255
+ server2.put(
1256
+ "/_web/ai/semantic-index/config",
1041
1257
  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));
1258
+ const config = await service.setConfig(request.body.config, { apiKey: request.body.apiKey });
1259
+ return {
1260
+ config,
1261
+ apiKeySet: await service.hasApiKey(),
1262
+ configured: await service.isConfigured(),
1263
+ modelStatus: await service.getModelStatus()
1264
+ };
1045
1265
  }
1046
1266
  );
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));
1267
+ server2.get("/_web/ai/semantic-index/enabled", async () => {
1268
+ return { sessions: await service.listEnabledStatuses() };
1269
+ });
1270
+ server2.get("/_web/ai/semantic-index/status", async (request) => {
1271
+ return { status: await service.status(request.query.sessionId) };
1272
+ });
1273
+ server2.post("/_web/ai/semantic-index/status", async (request) => {
1274
+ return { sessions: await service.statusForSessions(request.body.sessionIds ?? []) };
1275
+ });
1276
+ const sessionAction = (path14, action) => {
1277
+ server2.post(path14, async (request, reply) => {
1278
+ const { sessionId } = request.body;
1279
+ if (!sessionId) return reply.code(400).send({ error: "sessionId is required" });
1280
+ await action(sessionId);
1281
+ return { status: await service.status(sessionId) };
1282
+ });
1283
+ };
1284
+ sessionAction("/_web/ai/semantic-index/enable", (id) => service.enable(id));
1285
+ sessionAction("/_web/ai/semantic-index/remove", (id) => service.remove(id));
1286
+ sessionAction("/_web/ai/semantic-index/build", (id) => service.build(id));
1287
+ sessionAction("/_web/ai/semantic-index/pause", (id) => service.pause(id));
1288
+ sessionAction("/_web/ai/semantic-index/cancel", (id) => service.cancel(id));
1289
+ sessionAction("/_web/ai/semantic-index/rebuild", (id) => service.rebuild(id));
1290
+ server2.post("/_web/ai/semantic-index/build-pending", async () => {
1291
+ await service.buildAllPending();
1292
+ return { sessions: await service.listEnabledStatuses() };
1293
+ });
1294
+ server2.post("/_web/ai/semantic-index/cleanup", async () => {
1295
+ return await service.cleanupUnused();
1296
+ });
1297
+ server2.post(
1298
+ "/_web/ai/semantic-index/search",
1299
+ async (request, reply) => {
1300
+ const { sessionId, query, finalTopK } = request.body;
1301
+ if (!sessionId || !query) return reply.code(400).send({ error: "sessionId and query are required" });
1302
+ return service.search(sessionId, query, { finalTopK });
1053
1303
  }
1054
1304
  );
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
- );
1305
+ }
1306
+
1307
+ // ../../packages/http-routes/src/routes/web/ai-skills.ts
1308
+ function registerAiSkillRoutes(server2, ctx) {
1309
+ const mgr = ctx.skillManagerCore;
1310
+ if (!mgr) return;
1311
+ server2.get("/_web/ai/skills", async () => {
1312
+ return mgr.getAllSkills();
1313
+ });
1314
+ server2.get("/_web/ai/skills/:id", async (request, reply) => {
1315
+ const config = mgr.getSkillConfig(request.params.id);
1316
+ if (!config) return reply.code(404).send({ error: "Not found" });
1317
+ return config;
1318
+ });
1319
+ server2.post("/_web/ai/skills", async (request) => {
1320
+ return mgr.createSkill(request.body.rawMd);
1321
+ });
1322
+ server2.put("/_web/ai/skills/:id", async (request, reply) => {
1323
+ const result = mgr.updateSkill(request.params.id, request.body.rawMd);
1324
+ if (!result.success) return reply.code(404).send(result);
1325
+ return result;
1326
+ });
1327
+ server2.delete("/_web/ai/skills/:id", async (request, reply) => {
1328
+ const result = mgr.deleteSkill(request.params.id);
1329
+ if (!result.success) return reply.code(400).send(result);
1330
+ return result;
1331
+ });
1332
+ server2.post("/_web/ai/skills/import", async (request) => {
1333
+ return mgr.importSkillFromMd(request.body.rawMd);
1334
+ });
1335
+ server2.get("/_web/ai/skills/builtin-catalog", async () => {
1336
+ return mgr.getBuiltinCatalog();
1337
+ });
1338
+ server2.post("/_web/ai/skills/import-builtin", async (request, reply) => {
1339
+ const result = mgr.importSkill(request.body.builtinId);
1340
+ if (!result.success) return reply.code(400).send(result);
1341
+ return result;
1342
+ });
1343
+ server2.post("/_web/ai/skills/:id/reimport", async (request, reply) => {
1344
+ const result = mgr.reimportSkill(request.params.id);
1345
+ if (!result.success) return reply.code(400).send(result);
1346
+ return result;
1347
+ });
1348
+ }
1349
+
1350
+ // ../../packages/http-routes/src/routes/web/ai-summaries.ts
1351
+ function createSummaryDeps(ctx) {
1352
+ const store = ctx.llmConfigStore;
1353
+ if (!store) return null;
1354
+ return {
1355
+ getLlmConfig() {
1356
+ const config = store.getDefaultAssistantConfig();
1357
+ if (!config) return null;
1358
+ return config;
1359
+ },
1360
+ buildPiModel(config) {
1361
+ return buildPiModel(config);
1066
1362
  }
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));
1363
+ };
1364
+ }
1365
+ function registerAiSummaryRoutes(server2, ctx) {
1366
+ const deps = createSummaryDeps(ctx);
1367
+ if (!deps) return;
1368
+ const { sessionAdapter: adapter } = ctx;
1369
+ server2.post("/_web/sessions/:id/summaries/generate", async (request, reply) => {
1370
+ const { segmentId, locale, forceRegenerate, strategy } = request.body;
1371
+ const result = await summary_service_exports.generateSummary(adapter, request.params.id, segmentId, deps, {
1372
+ locale,
1373
+ forceRegenerate,
1374
+ strategy
1375
+ });
1376
+ if ("error" in result && !result.success) {
1377
+ return reply.code(400).send({ error: result.error });
1074
1378
  }
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)
1379
+ return result;
1380
+ });
1381
+ server2.post("/_web/sessions/:id/summaries/generate-all", async (request, reply) => {
1382
+ const { locale, forceRegenerate } = request.body;
1383
+ const result = await summary_service_exports.generateAllSummaries(adapter, request.params.id, deps, {
1384
+ locale,
1385
+ forceRegenerate
1386
+ });
1387
+ if (result.error) {
1388
+ return reply.code(400).send({ error: result.error });
1389
+ }
1390
+ return result;
1391
+ });
1392
+ server2.post("/_web/sessions/:id/summaries/check-can-generate", async (request) => {
1393
+ const { segmentIds } = request.body;
1394
+ return summary_service_exports.checkCanGenerate(adapter, request.params.id, segmentIds);
1395
+ });
1396
+ }
1397
+
1398
+ // ../../packages/http-routes/src/ai/tool-executor.ts
1399
+ var MAX_RESULT_CHARS = 5e5;
1400
+ async function executeRegistryTool(params, deps) {
1401
+ const { toolName, params: toolParams, abortSignal } = params;
1402
+ const entry = AGENT_TOOL_REGISTRY.find((tool) => tool.name === toolName);
1403
+ if (!entry) {
1404
+ return { success: false, error: `Tool not found: ${toolName}` };
1405
+ }
1406
+ try {
1407
+ if (abortSignal.aborted) return { success: false, error: "cancelled" };
1408
+ const execCtx = {
1409
+ sessionId: params.sessionId,
1410
+ abortSignal,
1411
+ db: deps.db,
1412
+ dataProvider: deps.dataProvider,
1413
+ semanticIndexService: deps.semanticIndexService,
1414
+ segmentText: deps.segmentText,
1415
+ translateTemplate: deps.translateTemplate
1416
+ };
1417
+ const startTime = Date.now();
1418
+ const result = await entry.handler(toolParams, execCtx);
1419
+ const elapsed = Date.now() - startTime;
1420
+ if (abortSignal.aborted) return { success: false, error: "cancelled" };
1421
+ let details = result.data ?? void 0;
1422
+ let truncated = false;
1423
+ if (details) {
1424
+ stripAvatarFields(details);
1425
+ const raw = JSON.stringify(details);
1426
+ if (raw.length > MAX_RESULT_CHARS) {
1427
+ truncated = true;
1428
+ details = { _truncated: true, _originalSize: raw.length, _preview: raw.slice(0, MAX_RESULT_CHARS) };
1429
+ }
1430
+ }
1431
+ return {
1432
+ success: true,
1433
+ elapsed,
1434
+ content: [{ type: "text", text: result.content }],
1435
+ details,
1436
+ truncated
1437
+ };
1438
+ } catch (error) {
1439
+ if (abortSignal.aborted) {
1440
+ return { success: false, error: "cancelled" };
1441
+ }
1442
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
1443
+ }
1444
+ }
1445
+
1446
+ // ../../packages/http-routes/src/routes/web/ai-tools.ts
1447
+ var activeToolTests = /* @__PURE__ */ new Map();
1448
+ function registerAiToolRoutes(server2, ctx) {
1449
+ server2.get("/_web/ai/tools/full-catalog", async () => {
1450
+ return AGENT_TOOL_REGISTRY.map((tool) => ({
1451
+ name: tool.name,
1452
+ category: tool.category ?? "core",
1453
+ description: tool.description,
1454
+ parameters: tool.inputSchema ?? {}
1455
+ }));
1456
+ });
1457
+ server2.post("/_web/ai/tools/execute", async (request, reply) => {
1458
+ const { testId, toolName, params, sessionId } = request.body;
1459
+ const entry = AGENT_TOOL_REGISTRY.find((t) => t.name === toolName);
1460
+ if (!entry) {
1461
+ return reply.code(404).send({ success: false, error: `Tool not found: ${toolName}` });
1462
+ }
1463
+ const abortController = new AbortController();
1464
+ activeToolTests.set(testId, abortController);
1465
+ try {
1466
+ if (ctx.executeAiTool) {
1467
+ return await ctx.executeAiTool({ testId, toolName, params, sessionId, abortSignal: abortController.signal });
1468
+ }
1469
+ const db = ctx.dbManager.open(sessionId);
1470
+ if (!db) {
1471
+ return reply.code(404).send({ success: false, error: `Session not found: ${sessionId}` });
1472
+ }
1473
+ const result = await executeRegistryTool(
1474
+ { testId, toolName, params, sessionId, abortSignal: abortController.signal },
1475
+ {
1476
+ db,
1477
+ dataProvider: new CoreDataProvider(db)
1478
+ }
1087
1479
  );
1480
+ if (!result.success && result.error !== "cancelled") {
1481
+ appLogger.error("ai-tools", `Failed to execute tool ${toolName}`, { error: result.error });
1482
+ }
1483
+ return result;
1484
+ } catch (error) {
1485
+ if (abortController.signal.aborted) {
1486
+ return { success: false, error: "cancelled" };
1487
+ }
1488
+ appLogger.error("ai-tools", `Failed to execute tool ${toolName}`, error);
1489
+ return { success: false, error: String(error) };
1490
+ } finally {
1491
+ activeToolTests.delete(testId);
1492
+ }
1493
+ });
1494
+ server2.post("/_web/ai/tools/cancel", async (request) => {
1495
+ const { testId } = request.body;
1496
+ const controller = activeToolTests.get(testId);
1497
+ if (controller) {
1498
+ controller.abort();
1499
+ activeToolTests.delete(testId);
1500
+ return { success: true };
1501
+ }
1502
+ return { success: false };
1503
+ });
1504
+ }
1505
+
1506
+ // ../../packages/http-routes/src/routes/web/register-ai.ts
1507
+ var REQUIRED_AI_DEPENDENCIES = [
1508
+ "aiDataDir",
1509
+ "aiChatManager",
1510
+ "assistantManager",
1511
+ "skillManagerCore",
1512
+ "llmConfigStore",
1513
+ "customProviderStore",
1514
+ "customModelStore",
1515
+ "runAgentStream"
1516
+ ];
1517
+ function assertRequiredAiContext(ctx) {
1518
+ const missing = REQUIRED_AI_DEPENDENCIES.filter((key) => !ctx[key]);
1519
+ if (missing.length > 0) {
1520
+ throw new Error(`[http-routes] requireAi is set but missing AI dependencies: ${missing.join(", ")}`);
1521
+ }
1522
+ }
1523
+ function registerAiRoutes(server2, ctx, options) {
1524
+ if (options?.requireAi) assertRequiredAiContext(ctx);
1525
+ registerAiAssistantRoutes(server2, ctx);
1526
+ registerAiSkillRoutes(server2, ctx);
1527
+ registerAiLlmRoutes(server2, ctx);
1528
+ registerAiLlmStreamRoutes(server2, ctx);
1529
+ registerAiAgentStreamRoutes(server2, ctx);
1530
+ registerAiToolRoutes(server2, ctx);
1531
+ registerAiChatRoutes(server2, ctx);
1532
+ registerAiSummaryRoutes(server2, ctx);
1533
+ registerAiLogRoutes(server2, ctx);
1534
+ registerSemanticIndexRoutes(server2, ctx);
1535
+ }
1536
+
1537
+ // ../../packages/http-routes/src/routes/web/analytics.ts
1538
+ import * as fs2 from "fs";
1539
+ import * as path4 from "path";
1540
+
1541
+ // ../../packages/http-routes/src/routes/web/time-filter.ts
1542
+ function parseTimeFilter(query) {
1543
+ const { startTs, endTs, memberId } = query;
1544
+ if (!startTs && !endTs && !memberId) return void 0;
1545
+ const filter = {};
1546
+ if (startTs) filter.startTs = parseInt(startTs, 10);
1547
+ if (endTs) filter.endTs = parseInt(endTs, 10);
1548
+ if (memberId) filter.memberId = parseInt(memberId, 10);
1549
+ return filter;
1550
+ }
1551
+
1552
+ // ../../packages/http-routes/src/routes/web/analytics-cache.ts
1553
+ import * as path3 from "path";
1554
+ function canonical(value) {
1555
+ if (value === void 0) return "null";
1556
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
1557
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
1558
+ const obj = value;
1559
+ const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
1560
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonical(obj[k])}`).join(",")}}`;
1561
+ }
1562
+ function buildAnalyticsCacheKey(namespace, params) {
1563
+ return `${namespace}:${canonical(params)}`;
1564
+ }
1565
+ function withAnalyticsCache(ctx, sessionId, namespace, params, compute, options) {
1566
+ const queryCacheDir = path3.join(ctx.pathProvider.getCacheDir(), "query");
1567
+ const dateStr = options?.dailyInvalidate ? `|date:${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}` : "";
1568
+ const extraStr = options?.extraVersion ? `|${options.extraVersion}` : "";
1569
+ const version = `${ctx.getVersion()}|${getDbFileVersion(ctx.sessionAdapter.getDbPath(sessionId))}${dateStr}${extraStr}`;
1570
+ const key = buildAnalyticsCacheKey(namespace, params);
1571
+ return getOrComputeAnalysisCache(sessionId, key, queryCacheDir, version, compute);
1572
+ }
1573
+
1574
+ // ../../packages/http-routes/src/routes/web/analytics.ts
1575
+ function registerAnalyticsRoutes(server2, ctx) {
1576
+ const { sessionAdapter: adapter } = ctx;
1577
+ const cached = (name, sessionId, params, compute, options) => withAnalyticsCache(ctx, sessionId, `analytics.${name}`, params, compute, options);
1578
+ server2.get("/_web/sessions/:id/years", async (request) => {
1579
+ const db = adapter.ensureReadonly(request.params.id);
1580
+ return getAvailableYears(db);
1581
+ });
1582
+ server2.get("/_web/sessions/:id/time-range", async (request) => {
1583
+ const db = adapter.ensureReadonly(request.params.id);
1584
+ return getTimeRange(db);
1585
+ });
1586
+ server2.get(
1587
+ "/_web/sessions/:id/stats/member-activity",
1588
+ async (request) => {
1589
+ const id = request.params.id;
1590
+ const filter = parseTimeFilter(request.query);
1591
+ return cached("member-activity", id, { ...filter }, () => getMemberActivity(adapter.ensureReadonly(id), filter));
1088
1592
  }
1089
1593
  );
1090
1594
  server2.get(
1091
- "/_web/sessions/:id/analytics/member-monthly-trend",
1595
+ "/_web/sessions/:id/stats/hourly",
1092
1596
  async (request) => {
1093
1597
  const id = request.params.id;
1094
1598
  const filter = parseTimeFilter(request.query);
1095
- return cached(
1096
- "member-monthly-trend",
1097
- id,
1098
- { ...filter },
1099
- () => getMemberMonthlyTrend(adapter.ensureReadonly(id), filter)
1100
- );
1599
+ return cached("hourly", id, { ...filter }, () => getHourlyActivity(adapter.ensureReadonly(id), filter));
1101
1600
  }
1102
1601
  );
1103
1602
  server2.get(
1104
- "/_web/sessions/:id/analytics/text-length-percentiles",
1603
+ "/_web/sessions/:id/stats/daily",
1105
1604
  async (request) => {
1106
1605
  const id = request.params.id;
1107
1606
  const filter = parseTimeFilter(request.query);
1108
- return cached(
1109
- "text-length-percentiles",
1110
- id,
1111
- { ...filter },
1112
- () => getTextLengthPercentiles(adapter.ensureReadonly(id), filter)
1113
- );
1607
+ return cached("daily", id, { ...filter }, () => getDailyActivity(adapter.ensureReadonly(id), filter));
1114
1608
  }
1115
1609
  );
1116
1610
  server2.get(
1117
- "/_web/sessions/:id/analytics/dragon-king",
1611
+ "/_web/sessions/:id/stats/weekday",
1118
1612
  async (request) => {
1119
1613
  const id = request.params.id;
1120
1614
  const filter = parseTimeFilter(request.query);
1121
- return cached("dragon-king", id, { ...filter }, () => getDragonKingAnalysis(adapter.ensureReadonly(id), filter));
1615
+ return cached("weekday", id, { ...filter }, () => getWeekdayActivity(adapter.ensureReadonly(id), filter));
1122
1616
  }
1123
1617
  );
1124
1618
  server2.get(
1125
- "/_web/sessions/:id/analytics/diving",
1619
+ "/_web/sessions/:id/stats/message-types",
1126
1620
  async (request) => {
1127
1621
  const id = request.params.id;
1128
1622
  const filter = parseTimeFilter(request.query);
1129
- return cached("diving", id, { ...filter }, () => getDivingAnalysis(adapter.ensureReadonly(id), filter), {
1130
- dailyInvalidate: true
1131
- });
1623
+ return cached("message-types", id, { ...filter }, () => getMessageTypeStats(adapter.ensureReadonly(id), filter));
1132
1624
  }
1133
1625
  );
1134
1626
  server2.get(
1135
- "/_web/sessions/:id/analytics/check-in",
1627
+ "/_web/sessions/:id/analytics/relationship",
1136
1628
  async (request) => {
1137
1629
  const id = request.params.id;
1138
1630
  const filter = parseTimeFilter(request.query);
1139
- return cached("check-in", id, { ...filter }, () => getCheckInAnalysis(adapter.ensureReadonly(id), filter));
1631
+ return cached("relationship", id, { ...filter }, () => getRelationshipStats(adapter.ensureReadonly(id), filter));
1140
1632
  }
1141
1633
  );
1142
1634
  server2.get(
1143
- "/_web/sessions/:id/analytics/meme-battle",
1635
+ "/_web/sessions/:id/analytics/catchphrase",
1144
1636
  async (request) => {
1145
1637
  const id = request.params.id;
1146
1638
  const filter = parseTimeFilter(request.query);
1147
- return cached("meme-battle", id, { ...filter }, () => getMemeBattleAnalysis(adapter.ensureReadonly(id), filter));
1639
+ return cached("catchphrase", id, { ...filter }, () => getCatchphraseAnalysis(adapter.ensureReadonly(id), filter));
1148
1640
  }
1149
1641
  );
1150
1642
  server2.get(
1151
- "/_web/sessions/:id/analytics/night-owl",
1643
+ "/_web/sessions/:id/analytics/mention",
1152
1644
  async (request) => {
1153
1645
  const id = request.params.id;
1154
1646
  const filter = parseTimeFilter(request.query);
1155
- return cached("night-owl", id, { ...filter }, () => getNightOwlAnalysis(adapter.ensureReadonly(id), filter), {
1156
- dailyInvalidate: true
1157
- });
1647
+ return cached("mention", id, { ...filter }, () => getMentionAnalysis(adapter.ensureReadonly(id), filter));
1158
1648
  }
1159
1649
  );
1160
1650
  server2.get(
1161
- "/_web/sessions/:id/analytics/repeat",
1651
+ "/_web/sessions/:id/analytics/mention-graph",
1162
1652
  async (request) => {
1163
1653
  const id = request.params.id;
1164
1654
  const filter = parseTimeFilter(request.query);
1165
- return cached("repeat", id, { ...filter }, () => getRepeatAnalysis(adapter.ensureReadonly(id), filter));
1655
+ return cached("mention-graph", id, { ...filter }, () => getMentionGraph(adapter.ensureReadonly(id), filter));
1166
1656
  }
1167
1657
  );
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" });
1658
+ server2.get(
1659
+ "/_web/sessions/:id/analytics/laugh",
1660
+ async (request) => {
1661
+ const id = request.params.id;
1662
+ const filter = parseTimeFilter(request.query);
1663
+ return cached("laugh", id, { ...filter }, () => getLaughAnalysis(adapter.ensureReadonly(id), filter));
1178
1664
  }
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);
1665
+ );
1666
+ server2.get("/_web/sessions/:id/analytics/cluster", async (request) => {
1667
+ const id = request.params.id;
1668
+ const filter = parseTimeFilter(request.query);
1669
+ const options = {};
1670
+ if (request.query.topEdges) options.topEdges = parseInt(request.query.topEdges, 10);
1671
+ if (request.query.lookAhead) options.lookAhead = parseInt(request.query.lookAhead, 10);
1672
+ if (request.query.decaySeconds) options.decaySeconds = parseInt(request.query.decaySeconds, 10);
1673
+ const hasOptions = Object.keys(options).length > 0;
1674
+ return cached(
1675
+ "cluster",
1676
+ id,
1677
+ { ...filter, ...options },
1678
+ () => getClusterGraph(adapter.ensureReadonly(id), filter, hasOptions ? options : void 0)
1679
+ );
1196
1680
  });
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 });
1681
+ server2.get(
1682
+ "/_web/sessions/:id/analytics/language-preference",
1683
+ async (request) => {
1684
+ const id = request.params.id;
1685
+ const filter = parseTimeFilter(request.query);
1686
+ const locale = request.query.locale || "zh-CN";
1687
+ const zhDictPath = path4.join(ctx.pathProvider.getSystemDir(), "nlp", "zh-CN.dict");
1688
+ let zhDictVersion;
1689
+ try {
1690
+ const st = fs2.statSync(zhDictPath);
1691
+ zhDictVersion = `${Math.floor(st.mtimeMs)}:${st.size}`;
1692
+ } catch {
1693
+ zhDictVersion = "-";
1694
+ }
1695
+ return cached(
1696
+ "language-preference",
1697
+ id,
1698
+ { ...filter, locale },
1699
+ () => getLanguagePreferenceAnalysis(adapter.ensureReadonly(id), {
1700
+ locale,
1701
+ timeFilter: filter,
1702
+ nlpProvider: createJiebaNlpProvider()
1703
+ }),
1704
+ { extraVersion: `dict:${zhDictVersion}` }
1705
+ );
1202
1706
  }
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 });
1707
+ );
1708
+ server2.get(
1709
+ "/_web/sessions/:id/analytics/monthly-activity",
1710
+ async (request) => {
1711
+ const id = request.params.id;
1712
+ const filter = parseTimeFilter(request.query);
1713
+ return cached("monthly-activity", id, { ...filter }, () => getMonthlyActivity(adapter.ensureReadonly(id), filter));
1206
1714
  }
1207
- if (Array.isArray(params)) {
1208
- return stmt.all(...params);
1715
+ );
1716
+ server2.get(
1717
+ "/_web/sessions/:id/analytics/yearly-activity",
1718
+ async (request) => {
1719
+ const id = request.params.id;
1720
+ const filter = parseTimeFilter(request.query);
1721
+ return cached("yearly-activity", id, { ...filter }, () => getYearlyActivity(adapter.ensureReadonly(id), filter));
1209
1722
  }
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" });
1723
+ );
1724
+ server2.get(
1725
+ "/_web/sessions/:id/analytics/message-length-distribution",
1726
+ async (request) => {
1727
+ const id = request.params.id;
1728
+ const filter = parseTimeFilter(request.query);
1729
+ return cached(
1730
+ "message-length-distribution",
1731
+ id,
1732
+ { ...filter },
1733
+ () => getMessageLengthDistribution(adapter.ensureReadonly(id), filter)
1734
+ );
1237
1735
  }
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 });
1736
+ );
1737
+ server2.get(
1738
+ "/_web/sessions/:id/analytics/text-stats",
1739
+ async (request) => {
1740
+ const id = request.params.id;
1741
+ const filter = parseTimeFilter(request.query);
1742
+ return cached("text-stats", id, { ...filter }, () => getTextStats(adapter.ensureReadonly(id), filter));
1269
1743
  }
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 });
1744
+ );
1745
+ server2.get(
1746
+ "/_web/sessions/:id/analytics/long-message-count",
1747
+ async (request) => {
1748
+ const id = request.params.id;
1749
+ const filter = parseTimeFilter(request.query);
1750
+ const minLength = request.query.minLength ? parseInt(request.query.minLength, 10) : void 0;
1751
+ return cached(
1752
+ "long-message-count",
1753
+ id,
1754
+ { ...filter, minLength },
1755
+ () => getLongMessageCount(adapter.ensureReadonly(id), filter, minLength)
1756
+ );
1303
1757
  }
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 = "-";
1758
+ );
1759
+ server2.get(
1760
+ "/_web/sessions/:id/analytics/member-monthly-trend",
1761
+ async (request) => {
1762
+ const id = request.params.id;
1763
+ const filter = parseTimeFilter(request.query);
1764
+ return cached(
1765
+ "member-monthly-trend",
1766
+ id,
1767
+ { ...filter },
1768
+ () => getMemberMonthlyTrend(adapter.ensureReadonly(id), filter)
1769
+ );
1313
1770
  }
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",
1771
+ );
1772
+ server2.get(
1773
+ "/_web/sessions/:id/analytics/text-length-percentiles",
1320
1774
  async (request) => {
1321
- const { text, locale, minLength } = request.body;
1322
- return segmentText(text, locale, minLength);
1775
+ const id = request.params.id;
1776
+ const filter = parseTimeFilter(request.query);
1777
+ return cached(
1778
+ "text-length-percentiles",
1779
+ id,
1780
+ { ...filter },
1781
+ () => getTextLengthPercentiles(adapter.ensureReadonly(id), filter)
1782
+ );
1323
1783
  }
1324
1784
  );
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) };
1785
+ server2.get(
1786
+ "/_web/sessions/:id/analytics/dragon-king",
1787
+ async (request) => {
1788
+ const id = request.params.id;
1789
+ const filter = parseTimeFilter(request.query);
1790
+ return cached("dragon-king", id, { ...filter }, () => getDragonKingAnalysis(adapter.ensureReadonly(id), filter));
1445
1791
  }
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" });
1792
+ );
1793
+ server2.get(
1794
+ "/_web/sessions/:id/analytics/diving",
1795
+ async (request) => {
1796
+ const id = request.params.id;
1797
+ const filter = parseTimeFilter(request.query);
1798
+ return cached("diving", id, { ...filter }, () => getDivingAnalysis(adapter.ensureReadonly(id), filter), {
1799
+ dailyInvalidate: true
1800
+ });
1549
1801
  }
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
1802
+ );
1803
+ server2.get(
1804
+ "/_web/sessions/:id/analytics/check-in",
1805
+ async (request) => {
1806
+ const id = request.params.id;
1807
+ const filter = parseTimeFilter(request.query);
1808
+ return cached("check-in", id, { ...filter }, () => getCheckInAnalysis(adapter.ensureReadonly(id), filter));
1809
+ }
1810
+ );
1811
+ server2.get(
1812
+ "/_web/sessions/:id/analytics/meme-battle",
1813
+ async (request) => {
1814
+ const id = request.params.id;
1815
+ const filter = parseTimeFilter(request.query);
1816
+ return cached("meme-battle", id, { ...filter }, () => getMemeBattleAnalysis(adapter.ensureReadonly(id), filter));
1817
+ }
1818
+ );
1819
+ server2.get(
1820
+ "/_web/sessions/:id/analytics/night-owl",
1821
+ async (request) => {
1822
+ const id = request.params.id;
1823
+ const filter = parseTimeFilter(request.query);
1824
+ return cached("night-owl", id, { ...filter }, () => getNightOwlAnalysis(adapter.ensureReadonly(id), filter), {
1825
+ dailyInvalidate: true
1576
1826
  });
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();
1583
1827
  }
1584
- });
1828
+ );
1829
+ server2.get(
1830
+ "/_web/sessions/:id/analytics/repeat",
1831
+ async (request) => {
1832
+ const id = request.params.id;
1833
+ const filter = parseTimeFilter(request.query);
1834
+ return cached("repeat", id, { ...filter }, () => getRepeatAnalysis(adapter.ensureReadonly(id), filter));
1835
+ }
1836
+ );
1585
1837
  }
1586
1838
 
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();
1839
+ // ../../packages/http-routes/src/routes/web/cache.ts
1840
+ import * as fs3 from "fs";
1841
+ import * as fsp from "fs/promises";
1842
+ import * as path5 from "path";
1843
+ async function getDirSize(dirPath) {
1844
+ let totalSize = 0;
1845
+ try {
1846
+ if (!fs3.existsSync(dirPath)) return 0;
1847
+ const files = await fsp.readdir(dirPath, { withFileTypes: true });
1848
+ for (const file of files) {
1849
+ const filePath = path5.join(dirPath, file.name);
1850
+ if (file.isDirectory()) {
1851
+ totalSize += await getDirSize(filePath);
1852
+ } else {
1853
+ const stat2 = await fsp.stat(filePath);
1854
+ totalSize += stat2.size;
1618
1855
  }
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();
1637
1856
  }
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 };
1646
- }
1647
- return { success: false };
1648
- });
1649
- }
1650
-
1651
- // ../../packages/http-routes/src/ai/tool-executor.ts
1652
- var MAX_RESULT_CHARS = 5e5;
1653
- async function executeRegistryTool(params, deps) {
1654
- const { toolName, params: toolParams, abortSignal } = params;
1655
- const entry = AGENT_TOOL_REGISTRY.find((tool) => tool.name === toolName);
1656
- if (!entry) {
1657
- return { success: false, error: `Tool not found: ${toolName}` };
1857
+ } catch {
1658
1858
  }
1859
+ return totalSize;
1860
+ }
1861
+ async function getFileCount(dirPath) {
1862
+ let count = 0;
1659
1863
  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) };
1864
+ if (!fs3.existsSync(dirPath)) return 0;
1865
+ const files = await fsp.readdir(dirPath, { withFileTypes: true });
1866
+ for (const file of files) {
1867
+ const filePath = path5.join(dirPath, file.name);
1868
+ if (file.isDirectory()) {
1869
+ count += await getFileCount(filePath);
1870
+ } else {
1871
+ count++;
1682
1872
  }
1683
1873
  }
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) };
1874
+ } catch {
1696
1875
  }
1876
+ return count;
1697
1877
  }
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 });
1878
+ function hasChatLabDatabases(dirPath) {
1879
+ try {
1880
+ const markerPath = path5.join(dirPath, ".chatlab");
1881
+ const dbDir = path5.join(dirPath, "databases");
1882
+ if (!fs3.existsSync(markerPath) || !fs3.existsSync(dbDir)) return false;
1883
+ return fs3.readdirSync(dbDir).some((file) => file.endsWith(".db"));
1884
+ } catch {
1885
+ return false;
1886
+ }
1887
+ }
1888
+ function registerCacheRoutes(server2, ctx) {
1889
+ const pp = ctx.pathProvider;
1890
+ const downloadsDir = ctx.downloadsDir ?? pp.getDownloadsDir();
1891
+ server2.get("/_web/cache/info", async () => {
1892
+ const directories = [
1893
+ {
1894
+ id: "databases",
1895
+ name: "settings.storage.cache.databases.name",
1896
+ description: "settings.storage.cache.databases.description",
1897
+ path: pp.getDatabaseDir(),
1898
+ scope: "user-data",
1899
+ rootPath: pp.getUserDataDir(),
1900
+ icon: "i-heroicons-circle-stack",
1901
+ canClear: false
1902
+ },
1903
+ {
1904
+ id: "ai",
1905
+ name: "settings.storage.cache.ai.name",
1906
+ description: "settings.storage.cache.ai.description",
1907
+ path: pp.getAiDataDir(),
1908
+ scope: "system-data",
1909
+ rootPath: pp.getSystemDir(),
1910
+ icon: "i-heroicons-sparkles",
1911
+ canClear: false
1912
+ },
1913
+ {
1914
+ id: "cache",
1915
+ name: "settings.storage.cache.statsCache.name",
1916
+ description: "settings.storage.cache.statsCache.description",
1917
+ path: pp.getCacheDir(),
1918
+ scope: "system-data",
1919
+ rootPath: pp.getSystemDir(),
1920
+ icon: "i-heroicons-bolt",
1921
+ canClear: true
1922
+ },
1923
+ {
1924
+ id: "logs",
1925
+ name: "settings.storage.cache.logs.name",
1926
+ description: "settings.storage.cache.logs.description",
1927
+ path: pp.getLogsDir(),
1928
+ scope: "system-data",
1929
+ rootPath: pp.getSystemDir(),
1930
+ icon: "i-heroicons-document-text",
1931
+ canClear: true
1735
1932
  }
1736
- return result;
1737
- } catch (error) {
1738
- if (abortController.signal.aborted) {
1739
- return { success: false, error: "cancelled" };
1933
+ ];
1934
+ const results = await Promise.all(
1935
+ directories.map(async (dir) => {
1936
+ const [size, fileCount] = await Promise.all([getDirSize(dir.path), getFileCount(dir.path)]);
1937
+ return { ...dir, size, fileCount, exists: fs3.existsSync(dir.path) };
1938
+ })
1939
+ );
1940
+ return {
1941
+ baseDir: pp.getSystemDir(),
1942
+ directories: results,
1943
+ totalSize: results.reduce((sum, d) => sum + d.size, 0)
1944
+ };
1945
+ });
1946
+ server2.post("/_web/cache/clear", async (request) => {
1947
+ const { cacheId } = request.body;
1948
+ const allowedDirs = {
1949
+ cache: pp.getCacheDir(),
1950
+ logs: pp.getLogsDir()
1951
+ };
1952
+ const dirPath = allowedDirs[cacheId];
1953
+ if (!dirPath) return { success: false, error: "Not allowed to clear this directory" };
1954
+ if (!fs3.existsSync(dirPath)) return { success: true };
1955
+ const files = await fsp.readdir(dirPath);
1956
+ for (const file of files) {
1957
+ const filePath = path5.join(dirPath, file);
1958
+ const stat2 = await fsp.stat(filePath);
1959
+ if (stat2.isDirectory()) {
1960
+ await fsp.rm(filePath, { recursive: true });
1961
+ } else {
1962
+ await fsp.unlink(filePath);
1740
1963
  }
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
- }
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
1964
  }
1755
- return { success: false };
1756
- });
1757
- }
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;
1965
+ return { success: true };
1776
1966
  });
1777
- server2.put("/_web/ai/chats/:id/title", async (request) => {
1778
- return cm.updateAIChatTitle(request.params.id, request.body.title);
1967
+ server2.get("/_web/cache/data-dir", async () => {
1968
+ const pending = ctx.getPendingDataDirMigration?.();
1969
+ return {
1970
+ path: pp.getUserDataDir(),
1971
+ defaultPath: ctx.defaultUserDataDir,
1972
+ isCustom: ctx.isCustomDataDir ?? false,
1973
+ canSetDataDir: ctx.canSetDataDir ?? Boolean(ctx.setDataDir),
1974
+ managedScope: "chat-databases",
1975
+ managedDescription: "settings.storage.dataLocation.managedDescription",
1976
+ hasLegacyDataAtDefaultDir: Boolean(ctx.defaultUserDataDir) && path5.resolve(pp.getUserDataDir()) !== path5.resolve(ctx.defaultUserDataDir ?? "") && hasChatLabDatabases(ctx.defaultUserDataDir ?? ""),
1977
+ pendingMigration: pending ? {
1978
+ from: pending.from,
1979
+ to: pending.to,
1980
+ createdAt: pending.createdAt
1981
+ } : void 0
1982
+ };
1779
1983
  });
1780
- server2.delete("/_web/ai/chats/:id", async (request) => {
1781
- return cm.deleteAIChat(request.params.id);
1984
+ server2.post("/_web/cache/data-dir", async (request, reply) => {
1985
+ if (!ctx.setDataDir) {
1986
+ return reply.code(501).send({ success: false, error: "Data directory changes are not supported" });
1987
+ }
1988
+ const targetPath = typeof request.body?.path === "string" ? request.body.path : null;
1989
+ const migrate = request.body?.migrate !== false;
1990
+ return ctx.setDataDir(targetPath, migrate);
1782
1991
  });
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
1992
+ server2.get("/_web/cache/latest-import-log", async () => {
1993
+ const importLogDir = path5.join(pp.getLogsDir(), "import");
1994
+ if (!fs3.existsSync(importLogDir)) {
1995
+ return { success: false, error: "Log directory not found" };
1996
+ }
1997
+ const files = await fsp.readdir(importLogDir);
1998
+ const logFiles = files.filter((f) => f.startsWith("import_") && f.endsWith(".log"));
1999
+ if (logFiles.length === 0) {
2000
+ return { success: false, error: "No import logs found" };
2001
+ }
2002
+ const fileStats = await Promise.all(
2003
+ logFiles.map(async (f) => {
2004
+ const filePath = path5.join(importLogDir, f);
2005
+ const stat2 = await fsp.stat(filePath);
2006
+ return { name: f, path: filePath, mtime: stat2.mtime.getTime() };
2007
+ })
1793
2008
  );
2009
+ fileStats.sort((a, b) => b.mtime - a.mtime);
2010
+ return { success: true, path: fileStats[0].path, name: fileStats[0].name };
1794
2011
  });
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" });
2012
+ server2.post("/_web/cache/save-to-downloads", async (request) => {
2013
+ const { filename, dataUrl } = request.body;
2014
+ if (!filename || !dataUrl) {
2015
+ return { success: false, error: "filename and dataUrl are required" };
1802
2016
  }
1803
- cm.deleteMessagesFrom(id, messageId);
1804
- return { success: true };
1805
- });
1806
- server2.post("/_web/ai/chats/:id/fork", async (request, reply) => {
1807
- const { upToMessageId, title } = request.body;
1808
- if (!upToMessageId || typeof upToMessageId !== "string") {
1809
- return reply.code(400).send({ error: "upToMessageId is required" });
2017
+ let buffer;
2018
+ if (dataUrl.includes(";base64,")) {
2019
+ const base64Data = dataUrl.split(";base64,")[1];
2020
+ buffer = Buffer.from(base64Data, "base64");
2021
+ } else if (dataUrl.includes("charset=utf-8,")) {
2022
+ const textData = dataUrl.split("charset=utf-8,")[1];
2023
+ buffer = Buffer.from(decodeURIComponent(textData), "utf-8");
2024
+ } else {
2025
+ const base64Data = dataUrl.replace(/^data:[^,]+,/, "");
2026
+ buffer = Buffer.from(base64Data, "base64");
1810
2027
  }
1811
- return cm.forkAIChat(request.params.id, upToMessageId, title);
1812
- });
1813
- server2.put("/_web/ai/messages/:messageId/content", async (request, reply) => {
1814
- const { content } = request.body;
1815
- if (!content || typeof content !== "string") {
1816
- return reply.code(400).send({ error: "content is required" });
2028
+ if (!fs3.existsSync(downloadsDir)) {
2029
+ fs3.mkdirSync(downloadsDir, { recursive: true });
1817
2030
  }
1818
- cm.updateMessageContent(request.params.messageId, content);
1819
- return { success: true };
2031
+ const filePath = path5.join(downloadsDir, filename);
2032
+ fs3.writeFileSync(filePath, buffer);
2033
+ return { success: true, filePath };
1820
2034
  });
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" });
2035
+ server2.post("/_web/cache/open-dir", async (request, reply) => {
2036
+ if (!ctx.openDirectory) return reply.code(501).send({ success: false, error: "Not supported" });
2037
+ const { cacheId } = request.body;
2038
+ const dirPaths = {
2039
+ base: pp.getSystemDir(),
2040
+ userData: pp.getUserDataDir(),
2041
+ databases: pp.getDatabaseDir(),
2042
+ cache: pp.getCacheDir(),
2043
+ ai: pp.getAiDataDir(),
2044
+ logs: pp.getLogsDir(),
2045
+ downloads: downloadsDir
2046
+ };
2047
+ const dirPath = dirPaths[cacheId];
2048
+ if (!dirPath) return { success: false, error: "Unknown directory" };
2049
+ if (!fs3.existsSync(dirPath)) {
2050
+ await fsp.mkdir(dirPath, { recursive: true });
2051
+ }
2052
+ try {
2053
+ await ctx.openDirectory(dirPath);
2054
+ } catch (error) {
2055
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
1825
2056
  }
1826
- cm.deleteAndRelinkMessage(id, messageId);
1827
2057
  return { success: true };
1828
2058
  });
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" });
2059
+ server2.post("/_web/cache/show-in-folder", async (request, reply) => {
2060
+ if (!ctx.showInFolder) return reply.code(501).send({ success: false, error: "Not supported" });
2061
+ const { filePath } = request.body;
2062
+ if (!filePath) return { success: false, error: "filePath is required" };
2063
+ try {
2064
+ await ctx.showInFolder(filePath);
2065
+ } catch (error) {
2066
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
1833
2067
  }
1834
- return cm.insertMessageAfter(request.params.id, afterMessageId, role, content, contentBlocks, tokenUsage);
2068
+ return { success: true };
1835
2069
  });
1836
- server2.get("/_web/ai/chats/:id/token-usage", async (request) => {
1837
- return cm.getAIChatTokenUsage(request.params.id);
2070
+ }
2071
+
2072
+ // ../../packages/http-routes/src/routes/web/contacts.ts
2073
+ function registerContactsRoutes(server2, ctx) {
2074
+ const service = ctx.contactsService ?? createContactsService({
2075
+ adapter: ctx.sessionAdapter,
2076
+ pathProvider: ctx.pathProvider,
2077
+ runtimeIdentity: ctx.runtimeIdentity,
2078
+ nativeBinding: ctx.nativeBinding
1838
2079
  });
1839
- server2.get("/_web/ai/debug/schema", async () => {
1840
- return cm.getAiSchema();
2080
+ server2.addHook("onClose", async () => {
2081
+ await service.close();
1841
2082
  });
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" });
1846
- }
1847
- try {
1848
- return cm.executeAiSQL(sql);
1849
- } catch (e) {
1850
- const msg = e instanceof Error ? e.message : String(e);
1851
- return reply.code(400).send({ error: msg });
1852
- }
2083
+ server2.get("/_web/contacts", async (request) => {
2084
+ return service.getContactsPage({
2085
+ acceptStale: isTruthy(request.query.acceptStale),
2086
+ timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange),
2087
+ pool: parseContactPool(request.query.pool),
2088
+ page: parsePositiveInteger(request.query.page),
2089
+ pageSize: parsePositiveInteger(request.query.pageSize),
2090
+ query: request.query.q
2091
+ });
1853
2092
  });
1854
- server2.post("/_web/ai/debug/clear-debug-context", async () => {
1855
- const cleared = cm.clearAllDebugContext();
1856
- return { success: true, cleared };
2093
+ server2.post("/_web/contacts/recompute", async (request) => {
2094
+ return service.startRecompute({
2095
+ timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange),
2096
+ pool: parseContactPool(request.query.pool),
2097
+ page: parsePositiveInteger(request.query.page),
2098
+ pageSize: parsePositiveInteger(request.query.pageSize),
2099
+ query: request.query.q
2100
+ });
1857
2101
  });
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 };
2102
+ server2.get("/_web/contacts/:key/detail", async (request) => {
2103
+ return service.getContactDetail(request.params.key, {
2104
+ acceptStale: isTruthy(request.query.acceptStale),
2105
+ timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange)
2106
+ });
1862
2107
  });
2108
+ server2.put(
2109
+ "/_web/contacts/:key/mark-friend",
2110
+ async (request) => {
2111
+ return service.markContactAsFriend(request.params.key, {
2112
+ timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange)
2113
+ });
2114
+ }
2115
+ );
2116
+ server2.delete(
2117
+ "/_web/contacts/:key/mark-friend",
2118
+ async (request) => {
2119
+ return service.unmarkContactAsFriend(request.params.key, {
2120
+ timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange)
2121
+ });
2122
+ }
2123
+ );
2124
+ }
2125
+ function isTruthy(value) {
2126
+ return value === "1" || value === "true" || value === "yes";
2127
+ }
2128
+ function parseContactsTimeRangePreset(value) {
2129
+ return CONTACTS_TIME_RANGE_PRESETS.includes(value) ? value : "1y";
2130
+ }
2131
+ function parseContactPool(value) {
2132
+ return value === "friend" || value === "non_friend" ? value : void 0;
2133
+ }
2134
+ function parsePositiveInteger(value) {
2135
+ if (!value) return void 0;
2136
+ const parsed = Number(value);
2137
+ if (!Number.isFinite(parsed)) return void 0;
2138
+ return Math.max(1, Math.trunc(parsed));
1863
2139
  }
1864
2140
 
1865
- // ../../packages/http-routes/src/routes/web/ai-summaries.ts
1866
- function createSummaryDeps(ctx) {
1867
- const store = ctx.llmConfigStore;
1868
- if (!store) return null;
1869
- return {
1870
- getLlmConfig() {
1871
- const config = store.getDefaultAssistantConfig();
1872
- if (!config) return null;
1873
- return config;
1874
- },
1875
- buildPiModel(config) {
1876
- return buildPiModel(config);
1877
- }
1878
- };
1879
- }
1880
- function registerAiSummaryRoutes(server2, ctx) {
1881
- const deps = createSummaryDeps(ctx);
1882
- if (!deps) return;
2141
+ // ../../packages/http-routes/src/routes/web/export.ts
2142
+ function registerExportRoutes(server2, ctx) {
1883
2143
  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
2144
+ server2.post("/_web/sessions/:id/export", async (request, reply) => {
2145
+ const { id } = request.params;
2146
+ const body = request.body;
2147
+ const sessionName = body?.sessionName || id;
2148
+ const format = body?.format || "txt";
2149
+ const result = export_service_exports.exportFormatted(adapter, {
2150
+ sessionId: id,
2151
+ sessionName,
2152
+ format,
2153
+ timeFilter: body.timeFilter
1890
2154
  });
1891
- if ("error" in result && !result.success) {
1892
- return reply.code(400).send({ error: result.error });
2155
+ if (!result.success) {
2156
+ return reply.code(result.totalMessages === 0 ? 404 : 500).send({ error: result.error });
1893
2157
  }
1894
- return result;
2158
+ reply.header("Content-Type", result.mimeType);
2159
+ reply.header("Content-Disposition", `attachment; filename="${encodeURIComponent(result.filename)}"`);
2160
+ return reply.send(result.content);
1895
2161
  });
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;
2162
+ }
2163
+
2164
+ // ../../packages/http-routes/src/routes/web/global-insight.ts
2165
+ function registerGlobalInsightRoutes(server2, ctx) {
2166
+ const service = ctx.globalInsightService ?? createGlobalInsightService({
2167
+ adapter: ctx.sessionAdapter,
2168
+ pathProvider: ctx.pathProvider,
2169
+ runtimeIdentity: ctx.runtimeIdentity,
2170
+ nativeBinding: ctx.nativeBinding
1906
2171
  });
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);
2172
+ server2.addHook("onClose", async () => service.close());
2173
+ server2.get("/_web/global-insight/annual-summary", async (request) => {
2174
+ return service.getAnnualSummary({
2175
+ ...parseRange(request.query),
2176
+ acceptStale: isTruthy2(request.query.acceptStale)
2177
+ });
1910
2178
  });
2179
+ server2.post(
2180
+ "/_web/global-insight/annual-summary/recompute",
2181
+ async (request) => service.startRecompute(parseRange(request.query))
2182
+ );
1911
2183
  }
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;
2184
+ function parseRange(query) {
2185
+ const mode = query.mode === "recent" ? "recent" : "year";
2186
+ return {
2187
+ mode,
2188
+ year: parseInteger(query.year),
2189
+ days: query.days === "365" ? 365 : void 0
2190
+ };
1925
2191
  }
1926
- function resolveAiLogPath(ctx) {
1927
- const currentPath = ctx.getCurrentAiLogPath?.();
1928
- if (currentPath && fs3.existsSync(currentPath)) return currentPath;
1929
- return findLatestAiLogPath(ctx.pathProvider.getLogsDir());
2192
+ function parseInteger(value) {
2193
+ if (!value) return void 0;
2194
+ const parsed = Number(value);
2195
+ return Number.isInteger(parsed) ? parsed : void 0;
1930
2196
  }
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.");
2197
+ function isTruthy2(value) {
2198
+ return value === "1" || value === "true" || value === "yes";
1935
2199
  }
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
- }
2200
+
2201
+ // ../../packages/http-routes/src/routes/web/logs.ts
2202
+ function registerLogRoutes(server2) {
2203
+ server2.post("/_web/logs/report", async (req, reply) => {
2204
+ const { level, message, stack, url } = req.body ?? {};
2205
+ if (!message) return reply.code(400).send({ error: "message required" });
2206
+ const detail = [url ? `url=${url}` : null, stack ? `
2207
+ ${stack}` : null].filter(Boolean).join(" ");
2208
+ if (level === "warn") appLogger.warn("web", message, detail || void 0);
2209
+ else appLogger.error("web", message, detail || void 0);
2210
+ return { ok: true };
1950
2211
  });
1951
2212
  }
1952
2213
 
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
- };
2214
+ // ../../packages/http-routes/src/routes/web/members.ts
2215
+ function registerMemberRoutes(server2, ctx) {
2216
+ const { sessionAdapter: adapter } = ctx;
2217
+ server2.get("/_web/sessions/:id/members", async (request) => {
2218
+ return member_service_exports.getMembers(adapter, request.params.id);
1992
2219
  });
1993
- server2.put(
1994
- "/_web/ai/semantic-index/config",
2220
+ server2.get("/_web/sessions/:id/members/paginated", async (request) => {
2221
+ return member_service_exports.getMembersPaginated(adapter, request.params.id, {
2222
+ page: parseInt(request.query.page || "1", 10),
2223
+ pageSize: parseInt(request.query.pageSize || "20", 10),
2224
+ search: request.query.search,
2225
+ sortOrder: request.query.sortOrder === "asc" ? "asc" : "desc"
2226
+ });
2227
+ });
2228
+ server2.patch(
2229
+ "/_web/sessions/:id/members/:memberId/aliases",
1995
2230
  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
- };
2231
+ const memberId = parseInt(request.params.memberId, 10);
2232
+ member_service_exports.updateMemberAliases(adapter, request.params.id, memberId, request.body.aliases);
2233
+ return { success: true };
2234
+ }
2235
+ );
2236
+ server2.delete(
2237
+ "/_web/sessions/:id/members/:memberId",
2238
+ async (request) => {
2239
+ const memberId = parseInt(request.params.memberId, 10);
2240
+ member_service_exports.deleteMember(adapter, request.params.id, memberId);
2241
+ return { success: true };
2003
2242
  }
2004
2243
  );
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
2244
  server2.post(
2036
- "/_web/ai/semantic-index/search",
2245
+ "/_web/sessions/:id/members/batch-delete",
2037
2246
  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 });
2247
+ const memberIds = request.body?.memberIds;
2248
+ if (!Array.isArray(memberIds) || memberIds.length === 0 || memberIds.some((memberId) => !Number.isSafeInteger(memberId) || Number(memberId) <= 0)) {
2249
+ return reply.code(400).send({
2250
+ success: false,
2251
+ error: "memberIds must contain at least one positive integer"
2252
+ });
2253
+ }
2254
+ const uniqueMemberIds = Array.from(new Set(memberIds));
2255
+ try {
2256
+ const success = member_service_exports.deleteMembers(adapter, request.params.id, uniqueMemberIds);
2257
+ if (!success) {
2258
+ throw new Error("Member deletion transaction returned no result");
2259
+ }
2260
+ } catch (error) {
2261
+ if (apiErrorFromUnknown(error) || isHttpClientError(error)) throw error;
2262
+ appLogger.error("members", "Batch member deletion failed", error);
2263
+ return reply.code(500).send({ success: false, error: "Failed to delete selected members" });
2264
+ }
2265
+ appLogger.info("members", "Batch member deletion completed", {
2266
+ sessionId: request.params.id,
2267
+ memberCount: uniqueMemberIds.length
2268
+ });
2269
+ return { success: true, deletedCount: uniqueMemberIds.length };
2041
2270
  }
2042
2271
  );
2272
+ server2.post(
2273
+ "/_web/sessions/:id/members/merge",
2274
+ async (request) => {
2275
+ const { memberId1, memberId2 } = request.body;
2276
+ member_service_exports.mergeMembers(adapter, request.params.id, memberId1, memberId2);
2277
+ return { success: true };
2278
+ }
2279
+ );
2280
+ server2.get(
2281
+ "/_web/sessions/:id/members/:memberId/history",
2282
+ async (request) => {
2283
+ const memberId = parseInt(request.params.memberId, 10);
2284
+ return member_service_exports.getMemberNameHistory(adapter, request.params.id, memberId);
2285
+ }
2286
+ );
2287
+ }
2288
+ function isHttpClientError(error) {
2289
+ return error instanceof Error && "statusCode" in error && typeof error.statusCode === "number" && error.statusCode >= 400 && error.statusCode < 500;
2043
2290
  }
2044
2291
 
2045
2292
  // ../../packages/http-routes/src/routes/web/merge.ts
2046
2293
  import * as fs4 from "fs";
2047
- import * as os from "os";
2048
2294
  import * as path6 from "path";
2049
- function ensureDb2(ctx, sessionId) {
2295
+ function ensureDb(ctx, sessionId) {
2050
2296
  const db = ctx.dbManager.open(sessionId);
2051
2297
  if (!db) throw sessionNotFound(sessionId);
2052
2298
  return db;
@@ -2062,7 +2308,7 @@ function registerMergeRoutes(server2, ctx) {
2062
2308
  if (contentType.includes("multipart/form-data")) {
2063
2309
  const data = await request.file();
2064
2310
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
2065
- const tmpDir = fs4.mkdtempSync(path6.join(os.tmpdir(), "chatlab-merge-"));
2311
+ const tmpDir = createChatLabTempDir("merge", "upload-");
2066
2312
  const tmpPath = path6.join(tmpDir, data.filename || "upload");
2067
2313
  const chunks = [];
2068
2314
  for await (const chunk of data.file) {
@@ -2155,7 +2401,7 @@ function registerMergeRoutes(server2, ctx) {
2155
2401
  return reply.code(501).send({ error: "Import not available on this server" });
2156
2402
  }
2157
2403
  const jsonData = JSON.stringify(merged.chatLabData);
2158
- const tmpDir = fs4.mkdtempSync(path6.join(os.tmpdir(), "chatlab-merged-"));
2404
+ const tmpDir = createChatLabTempDir("merge", "import-");
2159
2405
  const safeName = path6.basename(outputName).replace(/[/\\?%*:|"<>]/g, "_") || "merged";
2160
2406
  const tmpPath = path6.join(tmpDir, `${safeName}.json`);
2161
2407
  fs4.writeFileSync(tmpPath, jsonData, "utf-8");
@@ -2204,7 +2450,7 @@ function registerMergeRoutes(server2, ctx) {
2204
2450
  }
2205
2451
  const handles = [];
2206
2452
  for (const sid of sessionIds) {
2207
- const db = ensureDb2(ctx, sid);
2453
+ const db = ensureDb(ctx, sid);
2208
2454
  const exported = exportSessionToJson(db);
2209
2455
  const { db: tempDb, tempDbPath } = mergeCache.createTempDatabase(exported.meta.name);
2210
2456
  const writer = new TempDbWriter(tempDb);
@@ -2241,236 +2487,289 @@ function registerMergeRoutes(server2, ctx) {
2241
2487
  });
2242
2488
  }
2243
2489
 
2244
- // ../../packages/http-routes/src/routes/web/cache.ts
2490
+ // ../../packages/http-routes/src/routes/web/nlp.ts
2245
2491
  import * as fs5 from "fs";
2246
- import * as fsp from "fs/promises";
2247
2492
  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
- }
2493
+ function registerNlpRoutes(server2, ctx) {
2494
+ const nlpDir = path7.join(ctx.pathProvider.getSystemDir(), "nlp");
2495
+ initNlpDir(nlpDir);
2496
+ ensureDefaultDict(nlpDir).catch((err) => console.warn("[NLP] Auto-download zh-CN dict failed:", err));
2497
+ server2.get("/_web/nlp/pos-tags", async () => {
2498
+ return POS_TAG_DEFINITIONS;
2499
+ });
2500
+ server2.get("/_web/nlp/dicts", async () => {
2501
+ return getDictList(nlpDir);
2502
+ });
2503
+ server2.get("/_web/nlp/dicts/:id/status", async (request) => {
2504
+ return isDictDownloaded(nlpDir, request.params.id);
2505
+ });
2506
+ server2.post("/_web/nlp/dicts/:id/download", async (request) => {
2507
+ return downloadDict(nlpDir, request.params.id);
2508
+ });
2509
+ server2.delete("/_web/nlp/dicts/:id", async (request) => {
2510
+ return deleteDict(nlpDir, request.params.id);
2511
+ });
2512
+ server2.post("/_web/nlp/word-frequency", async (request) => {
2513
+ const params = request.body;
2514
+ const db = ctx.dbManager.open(params.sessionId);
2515
+ if (!db) {
2516
+ throw Object.assign(new Error(`Session not found: ${params.sessionId}`), { statusCode: 404 });
2261
2517
  }
2262
- } catch {
2263
- }
2264
- return totalSize;
2518
+ const { sessionId, ...keyParams } = params;
2519
+ const effectiveDictId = !params.dictType || params.dictType === "default" ? "zh-CN" : String(params.dictType);
2520
+ const dictFilePath = path7.join(nlpDir, `${effectiveDictId}.dict`);
2521
+ let dictVersion;
2522
+ try {
2523
+ const st = fs5.statSync(dictFilePath);
2524
+ dictVersion = `${Math.floor(st.mtimeMs)}:${st.size}`;
2525
+ } catch {
2526
+ dictVersion = "-";
2527
+ }
2528
+ return withAnalyticsCache(ctx, sessionId, "nlp.word-frequency", keyParams, () => computeWordFrequency(db, params), {
2529
+ extraVersion: `dict:${dictVersion}`
2530
+ });
2531
+ });
2532
+ server2.post(
2533
+ "/_web/nlp/segment",
2534
+ async (request) => {
2535
+ const { text, locale, minLength } = request.body;
2536
+ return segmentText(text, locale, minLength);
2537
+ }
2538
+ );
2265
2539
  }
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
- }
2540
+
2541
+ // ../../packages/http-routes/src/routes/web/people-relationships.ts
2542
+ function registerPeopleRelationshipsRoutes(server2, ctx) {
2543
+ const service = ctx.peopleRelationshipsService ?? createPeopleRelationshipsService({
2544
+ adapter: ctx.sessionAdapter,
2545
+ pathProvider: ctx.pathProvider,
2546
+ runtimeIdentity: ctx.runtimeIdentity,
2547
+ nativeBinding: ctx.nativeBinding
2548
+ });
2549
+ server2.addHook("onClose", async () => {
2550
+ await service.close();
2551
+ });
2552
+ server2.get("/_web/people/relationships", async (request) => {
2553
+ return service.getGraph({
2554
+ acceptStale: isTruthy3(request.query.acceptStale),
2555
+ timeRangePreset: parseContactsTimeRangePreset2(request.query.timeRange),
2556
+ graphScope: parsePeopleRelationshipsGraphScope(request.query.scope),
2557
+ query: request.query.q
2558
+ });
2559
+ });
2560
+ server2.post("/_web/people/relationships/recompute", async (request) => {
2561
+ return service.startRecompute({
2562
+ timeRangePreset: parseContactsTimeRangePreset2(request.query.timeRange),
2563
+ graphScope: parsePeopleRelationshipsGraphScope(request.query.scope),
2564
+ query: request.query.q
2565
+ });
2566
+ });
2567
+ server2.get(
2568
+ "/_web/people/relationships/:key/neighborhood",
2569
+ async (request) => {
2570
+ return service.getNeighborhood(request.params.key, {
2571
+ acceptStale: isTruthy3(request.query.acceptStale),
2572
+ timeRangePreset: parseContactsTimeRangePreset2(request.query.timeRange)
2573
+ });
2278
2574
  }
2279
- } catch {
2280
- }
2281
- return count;
2575
+ );
2282
2576
  }
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
- }
2577
+ function isTruthy3(value) {
2578
+ return value === "1" || value === "true" || value === "yes";
2292
2579
  }
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
- };
2580
+ function parseContactsTimeRangePreset2(value) {
2581
+ return CONTACTS_TIME_RANGE_PRESETS.includes(value) ? value : "1y";
2582
+ }
2583
+ function parsePeopleRelationshipsGraphScope(value) {
2584
+ return value === "close" || value === "friends" ? value : "panorama";
2585
+ }
2586
+
2587
+ // ../../packages/http-routes/src/routes/web/preferences.ts
2588
+ function registerPreferencesRoutes(server2, ctx) {
2589
+ const prefManager = ctx.preferencesManager ?? new PreferencesManager(ctx.pathProvider.getSystemDir());
2590
+ server2.get("/_web/preferences", async () => {
2591
+ return prefManager.load();
2350
2592
  });
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);
2593
+ server2.patch("/_web/preferences", async (request) => {
2594
+ return prefManager.save(request.body);
2595
+ });
2596
+ server2.get("/_web/preferences/ui-config", async () => {
2597
+ const config = loadConfig();
2598
+ return config.ui;
2599
+ });
2600
+ server2.patch("/_web/preferences/ui-config", async (request) => {
2601
+ try {
2602
+ for (const [key, value] of Object.entries(request.body)) {
2603
+ if (value !== void 0) {
2604
+ writeConfigField("ui", key, value);
2605
+ }
2368
2606
  }
2607
+ return { success: true };
2608
+ } catch (err) {
2609
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
2369
2610
  }
2370
- return { success: true };
2371
2611
  });
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
- };
2612
+ server2.get("/_web/preferences/locale", async () => {
2613
+ const config = loadConfig();
2614
+ return { lang: config.locale.lang };
2615
+ });
2616
+ server2.patch("/_web/preferences/locale", async (request) => {
2617
+ try {
2618
+ writeConfigField("locale", "lang", request.body.lang);
2619
+ return { success: true };
2620
+ } catch (err) {
2621
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
2622
+ }
2623
+ });
2624
+ }
2625
+
2626
+ // ../../packages/http-routes/src/routes/web/session-index.ts
2627
+ function registerSessionIndexRoutes(server2, ctx) {
2628
+ const { sessionAdapter: adapter } = ctx;
2629
+ server2.get("/_web/sessions/index-stats", async () => {
2630
+ return session_index_service_exports.getAllIndexStats(adapter);
2631
+ });
2632
+ server2.post("/_web/sessions/:id/generate-index", async (request) => {
2633
+ const gapThreshold = request.body?.gapThreshold ?? 1800;
2634
+ const sessionCount = session_index_service_exports.generateIndex(adapter, request.params.id, gapThreshold);
2635
+ return { sessionCount };
2636
+ });
2637
+ server2.post("/_web/sessions/:id/generate-incremental-index", async (request) => {
2638
+ const gapThreshold = request.body?.gapThreshold ?? 1800;
2639
+ const sessionCount = session_index_service_exports.generateIncrementalIndex(adapter, request.params.id, gapThreshold);
2640
+ return { sessionCount };
2388
2641
  });
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" });
2642
+ server2.post("/_web/sessions/:id/clear-index", async (request) => {
2643
+ session_index_service_exports.clearIndex(adapter, request.params.id);
2644
+ return { success: true };
2645
+ });
2646
+ server2.get("/_web/sessions/:id/search/fts", async (request, reply) => {
2647
+ if (!session_index_service_exports.getFtsStatus(adapter, request.params.id)) {
2648
+ return reply.code(400).send({ error: "FTS index not built for this session" });
2392
2649
  }
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);
2650
+ const keywords = request.query.keywords.split(/\s+/).filter(Boolean);
2651
+ if (keywords.length === 0) return { rowids: [], total: 0 };
2652
+ const limit = parseInt(request.query.limit || "100", 10);
2653
+ const offset = parseInt(request.query.offset || "0", 10);
2654
+ return session_index_service_exports.searchFts(adapter, request.params.id, keywords, limit, offset);
2396
2655
  });
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" };
2656
+ server2.get("/_web/sessions/:id/fts/status", async (request) => {
2657
+ return { hasFtsIndex: session_index_service_exports.getFtsStatus(adapter, request.params.id) };
2658
+ });
2659
+ server2.post("/_web/sessions/:id/fts/rebuild", async (request) => {
2660
+ const result = session_index_service_exports.rebuildFts(adapter, request.params.id);
2661
+ return { success: true, indexed: result.indexed };
2662
+ });
2663
+ }
2664
+
2665
+ // ../../packages/http-routes/src/routes/web/sessions.ts
2666
+ function registerSessionRoutes(server2, ctx) {
2667
+ const { sessionAdapter: adapter } = ctx;
2668
+ let preferencesInstance = null;
2669
+ const preferences = () => {
2670
+ preferencesInstance ??= ctx.preferencesManager ?? new PreferencesManager(ctx.pathProvider.getSystemDir());
2671
+ return preferencesInstance;
2672
+ };
2673
+ server2.get("/_web/sessions", async () => {
2674
+ const aiChatCounts = ctx.aiChatManager?.getAIChatCountsBySession();
2675
+ return session_service_exports.listAnalysisSessions(adapter, {
2676
+ enrichSession: aiChatCounts?.size ? (dto) => ({ ...dto, aiConversationCount: aiChatCounts.get(dto.id) ?? 0 }) : void 0
2677
+ });
2678
+ });
2679
+ server2.get("/_web/sessions/:id", async (request) => {
2680
+ const session = session_service_exports.getAnalysisSession(adapter, request.params.id);
2681
+ if (!session) {
2682
+ throw Object.assign(new Error(`Session not found: ${request.params.id}`), { statusCode: 404 });
2401
2683
  }
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" };
2684
+ if (ctx.aiChatManager) {
2685
+ session.aiConversationCount = ctx.aiChatManager.getAIChats(request.params.id).length;
2406
2686
  }
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 };
2687
+ return session;
2416
2688
  });
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" };
2689
+ server2.delete("/_web/sessions/:id", async (request, reply) => {
2690
+ const { id } = request.params;
2691
+ try {
2692
+ const deleted = session_service_exports.deleteSession(adapter, id);
2693
+ if (!deleted) {
2694
+ return reply.code(404).send({ success: false, error: "File not found" });
2695
+ }
2696
+ return { success: true };
2697
+ } catch (err) {
2698
+ return reply.code(500).send({ success: false, error: String(err) });
2421
2699
  }
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");
2700
+ });
2701
+ server2.patch("/_web/sessions/:id/name", async (request) => {
2702
+ session_service_exports.renameSession(adapter, request.params.id, request.body.name);
2703
+ return { success: true };
2704
+ });
2705
+ server2.patch(
2706
+ "/_web/sessions/:id/owner",
2707
+ async (request) => {
2708
+ session_service_exports.updateSessionOwnerId(adapter, request.params.id, request.body.ownerId ?? null);
2709
+ return { success: true };
2432
2710
  }
2433
- if (!fs5.existsSync(downloadsDir)) {
2434
- fs5.mkdirSync(downloadsDir, { recursive: true });
2711
+ );
2712
+ server2.post("/_web/sessions/:id/owner/apply-profile", async (request) => {
2713
+ return owner_profile_service_exports.tryApplyOwnerProfile(adapter, preferences(), request.params.id);
2714
+ });
2715
+ server2.post(
2716
+ "/_web/sessions/:id/owner/select",
2717
+ async (request) => {
2718
+ return owner_profile_service_exports.setOwnerAndApplyProfile(
2719
+ adapter,
2720
+ preferences(),
2721
+ request.params.id,
2722
+ request.body.ownerPlatformId
2723
+ );
2435
2724
  }
2436
- const filePath = path7.join(downloadsDir, filename);
2437
- fs5.writeFileSync(filePath, buffer);
2438
- return { success: true, filePath };
2725
+ );
2726
+ server2.post("/_web/sessions/:id/owner/dismiss-prompt", async (request) => {
2727
+ owner_profile_service_exports.dismissOwnerPrompt(preferences(), request.params.id);
2728
+ return { success: true };
2439
2729
  });
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 });
2730
+ }
2731
+
2732
+ // ../../packages/http-routes/src/routes/web/sql.ts
2733
+ function registerSqlRoutes(server2, ctx) {
2734
+ const { sessionAdapter: adapter } = ctx;
2735
+ server2.post("/_web/sessions/:id/sql", async (request, reply) => {
2736
+ const db = adapter.ensureReadonly(request.params.id);
2737
+ const { sql } = request.body || {};
2738
+ if (!sql || typeof sql !== "string") {
2739
+ return reply.code(400).send({ error: "Missing sql parameter" });
2456
2740
  }
2457
2741
  try {
2458
- await ctx.openDirectory(dirPath);
2459
- } catch (error) {
2460
- return { success: false, error: error instanceof Error ? error.message : String(error) };
2742
+ const result = executeSql(db, sql, { columnar: true, timing: true, maxRows: 0 });
2743
+ return {
2744
+ columns: result.columns,
2745
+ rows: result.rows,
2746
+ rowCount: result.rowCount,
2747
+ duration: result.duration ?? 0,
2748
+ limited: result.truncated
2749
+ };
2750
+ } catch (err) {
2751
+ const message = err instanceof Error ? err.message : "SQL execution error";
2752
+ return reply.code(400).send({ error: message });
2461
2753
  }
2462
- return { success: true };
2463
2754
  });
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) };
2755
+ server2.get("/_web/sessions/:id/schema", async (request) => {
2756
+ const db = adapter.ensureReadonly(request.params.id);
2757
+ return getSchemaDetailed(db);
2758
+ });
2759
+ server2.post("/_web/sessions/:id/query", async (request) => {
2760
+ const db = adapter.ensureReadonly(request.params.id);
2761
+ const { sql, params = [] } = request.body;
2762
+ if (!sql || typeof sql !== "string") {
2763
+ throw Object.assign(new Error('Missing or invalid "sql" field'), { statusCode: 400 });
2472
2764
  }
2473
- return { success: true };
2765
+ const stmt = db.prepare(sql.trim());
2766
+ if (!stmt.readonly) {
2767
+ throw Object.assign(new Error("Only READ-ONLY statements are allowed"), { statusCode: 403 });
2768
+ }
2769
+ if (Array.isArray(params)) {
2770
+ return stmt.all(...params);
2771
+ }
2772
+ return stmt.all(params);
2474
2773
  });
2475
2774
  }
2476
2775
 
@@ -2500,25 +2799,9 @@ function registerTelemetryRoutes(server2, ctx) {
2500
2799
  );
2501
2800
  }
2502
2801
 
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) {
2802
+ // ../../packages/http-routes/src/routes/web/register.ts
2803
+ function registerWebRoutes(server2, ctx, options) {
2518
2804
  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
2805
  registerSessionRoutes(server2, resolvedCtx);
2523
2806
  registerMemberRoutes(server2, resolvedCtx);
2524
2807
  registerContactsRoutes(server2, resolvedCtx);
@@ -2530,77 +2813,17 @@ function registerSharedRoutes(server2, ctx, options) {
2530
2813
  registerSessionIndexRoutes(server2, resolvedCtx);
2531
2814
  registerExportRoutes(server2, resolvedCtx);
2532
2815
  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);
2816
+ registerAiRoutes(server2, resolvedCtx, options);
2557
2817
  registerMergeRoutes(server2, resolvedCtx);
2558
2818
  registerCacheRoutes(server2, resolvedCtx);
2559
2819
  registerTelemetryRoutes(server2, resolvedCtx);
2560
- registerLogRoutes(server2, resolvedCtx);
2820
+ registerLogRoutes(server2);
2561
2821
  }
2562
2822
 
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
- }
2823
+ // ../../packages/http-routes/src/register.ts
2824
+ function registerSharedRoutes(server2, ctx, options) {
2825
+ registerRestRoutes(server2, ctx);
2826
+ registerWebRoutes(server2, ctx, options);
2604
2827
  }
2605
2828
 
2606
2829
  // ../../packages/http-routes/src/routes/web/automation.ts
@@ -2747,44 +2970,8 @@ function registerAutomationRoutes(server2, ctx) {
2747
2970
  });
2748
2971
  }
2749
2972
 
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
2973
  // src/http/routes/web/import.ts
2786
2974
  import * as fs7 from "fs";
2787
- import * as os2 from "os";
2788
2975
  import * as path8 from "path";
2789
2976
  import { randomUUID } from "crypto";
2790
2977
  import { pipeline } from "stream/promises";
@@ -2815,9 +3002,7 @@ function cleanupTemp(...paths) {
2815
3002
  }
2816
3003
  }
2817
3004
  function registerImportRoutes2(server2, dbManager2, options = {}) {
2818
- const sourceManager = options.sourceManager ?? new ArchiveImportSourceManager({
2819
- tempRoot: fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-import-sources-"))
2820
- });
3005
+ const sourceManager = options.sourceManager ?? new ArchiveImportSourceManager();
2821
3006
  const runAutoImport = options.runAutoImport ?? autoImport.bind(null, dbManager2);
2822
3007
  const runPreparedImport = options.runPreparedImport ?? (async (manifestPath, onProgress) => {
2823
3008
  const result = await runAutoImport(manifestPath, {
@@ -2835,7 +3020,7 @@ function registerImportRoutes2(server2, dbManager2, options = {}) {
2835
3020
  limits: { fileSize: ARCHIVE_UPLOAD_LIMIT }
2836
3021
  });
2837
3022
  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`);
3023
+ const uploadPath = path8.join(getChatLabTempScopeDir("imports"), `archive-${randomUUID()}.zip`);
2839
3024
  try {
2840
3025
  await pipeline(data.file, fs7.createWriteStream(uploadPath, { flags: "wx" }));
2841
3026
  if (data.file.truncated) {
@@ -2902,7 +3087,7 @@ data: ${JSON.stringify(eventData)}
2902
3087
  server2.post("/_web/detect-format", async (request, reply) => {
2903
3088
  const data = await request.file();
2904
3089
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
2905
- const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-detect-"));
3090
+ const tmpDir = createChatLabTempDir("imports", "detect-");
2906
3091
  const tmpPath = path8.join(tmpDir, data.filename || "upload");
2907
3092
  try {
2908
3093
  const chunks = [];
@@ -2920,7 +3105,7 @@ data: ${JSON.stringify(eventData)}
2920
3105
  server2.post("/_web/scan-multi-chat", async (request, reply) => {
2921
3106
  const data = await request.file();
2922
3107
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
2923
- const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-scan-"));
3108
+ const tmpDir = createChatLabTempDir("imports", "scan-");
2924
3109
  const tmpPath = path8.join(tmpDir, data.filename || "upload");
2925
3110
  try {
2926
3111
  const chunks = [];
@@ -2937,7 +3122,7 @@ data: ${JSON.stringify(eventData)}
2937
3122
  server2.post("/_web/import", async (request, reply) => {
2938
3123
  const data = await request.file();
2939
3124
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
2940
- const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-import-"));
3125
+ const tmpDir = createChatLabTempDir("imports", "upload-");
2941
3126
  const tmpPath = path8.join(tmpDir, data.filename || "upload");
2942
3127
  const chunks = [];
2943
3128
  for await (const chunk of data.file) {
@@ -2981,7 +3166,7 @@ data: ${JSON.stringify(eventData)}
2981
3166
  server2.post("/_web/import-directory", async (request, reply) => {
2982
3167
  const parts = request.parts();
2983
3168
  if (!parts) return reply.code(400).send({ error: "No files uploaded" });
2984
- const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-dir-import-"));
3169
+ const tmpDir = createChatLabTempDir("imports", "directory-");
2985
3170
  const relativePaths = [];
2986
3171
  const fileBuffers = [];
2987
3172
  try {
@@ -3052,7 +3237,7 @@ data: ${JSON.stringify({ success: false, error: err instanceof Error ? err.messa
3052
3237
  const sessionId = request.params.id;
3053
3238
  const data = await request.file();
3054
3239
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
3055
- const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-inc-"));
3240
+ const tmpDir = createChatLabTempDir("imports", "incremental-");
3056
3241
  const tmpPath = path8.join(tmpDir, data.filename || "upload");
3057
3242
  const chunks = [];
3058
3243
  for await (const chunk of data.file) {
@@ -3090,7 +3275,7 @@ data: ${JSON.stringify(eventData)}
3090
3275
  const sessionId = request.params.id;
3091
3276
  const data = await request.file();
3092
3277
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
3093
- const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-analyze-"));
3278
+ const tmpDir = createChatLabTempDir("imports", "analyze-");
3094
3279
  const tmpPath = path8.join(tmpDir, data.filename || "upload");
3095
3280
  const chunks = [];
3096
3281
  for await (const chunk of data.file) {
@@ -3108,7 +3293,7 @@ data: ${JSON.stringify(eventData)}
3108
3293
  server2.post("/_web/import/analyze", async (request, reply) => {
3109
3294
  const data = await request.file();
3110
3295
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
3111
- const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-analyze-"));
3296
+ const tmpDir = createChatLabTempDir("imports", "analyze-");
3112
3297
  const tmpPath = path8.join(tmpDir, data.filename || "upload");
3113
3298
  const chunks = [];
3114
3299
  for await (const chunk of data.file) {
@@ -3144,7 +3329,7 @@ data: ${JSON.stringify(eventData)}
3144
3329
 
3145
3330
  `);
3146
3331
  }
3147
- const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-demo-"));
3332
+ const tmpDir = createChatLabTempDir("imports", "cli-demo-");
3148
3333
  try {
3149
3334
  const localPaths = [];
3150
3335
  for (let i = 0; i < total; i++) {
@@ -3231,29 +3416,29 @@ function buildWebUpdateCheckResult(options) {
3231
3416
  }
3232
3417
 
3233
3418
  // src/http/routes/web/index.ts
3234
- function registerWebRoutes(server2, dbManager2, options) {
3419
+ function registerWebRoutes2(server2, dbManager2, options) {
3235
3420
  const adapter = createDatabaseManagerAdapter(dbManager2);
3236
3421
  const mergeCache = options?.pathProvider ? new MergeSessionCache(options.pathProvider, { nativeBinding: options.nativeBinding }) : null;
3237
3422
  mergeCache?.cleanupOrphans();
3238
3423
  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")
3424
+ getSystemDir: () => path10.join(os.homedir(), ".chatlab"),
3425
+ getUserDataDir: () => path10.join(os.homedir(), ".chatlab", "data"),
3426
+ getDatabaseDir: () => path10.join(os.homedir(), ".chatlab", "data", "databases"),
3427
+ getVectorDir: () => path10.join(os.homedir(), ".chatlab", "data", "vector"),
3428
+ getAiDataDir: () => path10.join(os.homedir(), ".chatlab", "ai"),
3429
+ getSettingsDir: () => path10.join(os.homedir(), ".chatlab", "settings"),
3430
+ getCacheDir: () => path10.join(os.homedir(), ".chatlab", "cache"),
3431
+ getTempDir: () => getChatLabTempScopeDir("runtime"),
3432
+ getLogsDir: () => path10.join(os.homedir(), ".chatlab", "logs"),
3433
+ getDownloadsDir: () => path10.join(os.homedir(), "Downloads")
3249
3434
  };
3250
3435
  const resolvedPathProvider = options?.pathProvider ?? fallbackPathProvider;
3251
3436
  const ai = options?.aiContext;
3252
3437
  const cliStreamImport = async (dm, filePath) => {
3253
- const { streamImport: streamImport2 } = await import("./stream-import-AWEHGQJY.mjs");
3438
+ const { streamImport: streamImport2 } = await import("./stream-import-JKQYRISL.mjs");
3254
3439
  const result = await streamImport2(dm, filePath);
3255
3440
  if (!result.success) {
3256
- const { IMPORT_IN_PROGRESS_ERROR_KEY: IMPORT_IN_PROGRESS_ERROR_KEY2, ImportInProgressError: ImportInProgressError2 } = await import("./src-7OD6RD5V.mjs");
3441
+ const { IMPORT_IN_PROGRESS_ERROR_KEY: IMPORT_IN_PROGRESS_ERROR_KEY2, ImportInProgressError: ImportInProgressError2 } = await import("./src-UY4GCVNF.mjs");
3257
3442
  if (result.error === IMPORT_IN_PROGRESS_ERROR_KEY2) throw new ImportInProgressError2();
3258
3443
  throw new Error(result.error || "Import failed");
3259
3444
  }
@@ -4154,12 +4339,11 @@ function reloadTimer(dsId, skipInitialPull = false) {
4154
4339
 
4155
4340
  // src/sync/adapters.ts
4156
4341
  import * as fs11 from "fs";
4157
- import * as os4 from "os";
4158
4342
  import * as path13 from "path";
4159
4343
  import * as crypto3 from "crypto";
4160
4344
  function getTempFilePath(ext) {
4161
4345
  const id = crypto3.randomBytes(8).toString("hex");
4162
- return path13.join(os4.tmpdir(), `chatlab-pull-${id}${ext}`);
4346
+ return path13.join(getChatLabTempScopeDir("sync"), `pull-${id}${ext}`);
4163
4347
  }
4164
4348
  var NodeFetcher = class {
4165
4349
  async fetchToTempFile(baseUrl, remoteSessionId, token, params) {
@@ -4330,21 +4514,6 @@ function cleanupSync() {
4330
4514
  var server = null;
4331
4515
  var dbManager = null;
4332
4516
  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
4517
  function resolveNativeBinding2() {
4349
4518
  if (process.versions.electron) return void 0;
4350
4519
  const nativePath = resolveCliPath("native/better_sqlite3.node");
@@ -4402,20 +4571,11 @@ async function startHttpServer(options) {
4402
4571
  aiChatManager = new AIChatManager(aiDataDir, { nativeBinding });
4403
4572
  const assistantManager = getAssistantManager(aiDataDir);
4404
4573
  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
- });
4574
+ const configStorage = createFileConfigStorage(aiDataDir);
4575
+ const llmConfigStore = createAuthProfileLlmConfigStore(configStorage);
4417
4576
  initAppLogger(pathProvider.getLogsDir());
4418
4577
  initServerAiLogger(pathProvider.getLogsDir());
4578
+ appLogger.info("temp-workspace", "Temporary workspace initialized", { root: pathProvider.getTempDir() });
4419
4579
  appLogger.info("server", `HTTP server starting on ${host}:${port}`);
4420
4580
  logNativeParserStatus();
4421
4581
  setAuthToken(token);
@@ -4439,7 +4599,7 @@ async function startHttpServer(options) {
4439
4599
  console.warn("[semantic-index] worker client unavailable:", err instanceof Error ? err.message : String(err));
4440
4600
  semanticIndexService = void 0;
4441
4601
  }
4442
- registerWebRoutes(server, dbManager, {
4602
+ registerWebRoutes2(server, dbManager, {
4443
4603
  pathProvider,
4444
4604
  nativeBinding,
4445
4605
  runtimeIdentity: runtime,
@@ -4450,8 +4610,8 @@ async function startHttpServer(options) {
4450
4610
  assistantManager,
4451
4611
  skillManagerCore,
4452
4612
  llmConfigStore,
4453
- customProviderStore: new CustomProviderStore(createFileConfigStorage(aiDataDir)),
4454
- customModelStore: new CustomModelStore(createFileConfigStorage(aiDataDir)),
4613
+ customProviderStore: new CustomProviderStore(configStorage),
4614
+ customModelStore: new CustomModelStore(configStorage),
4455
4615
  runAgentStream: createCliRunAgentStream(dbManager, aiChatManager, semanticIndexService)
4456
4616
  }
4457
4617
  });
@@ -4493,8 +4653,8 @@ async function stopHttpServer() {
4493
4653
 
4494
4654
  export {
4495
4655
  createServer,
4496
- registerWebRoutes,
4656
+ registerWebRoutes2 as registerWebRoutes,
4497
4657
  startHttpServer,
4498
4658
  stopHttpServer
4499
4659
  };
4500
- //# sourceMappingURL=chunk-SZEBCNBB.mjs.map
4660
+ //# sourceMappingURL=chunk-67HNEIKY.mjs.map