chatlab-cli 0.31.0 → 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 (135) hide show
  1. package/dist/{chat-command-CHV7RLBH.mjs → chat-command-V6NYTTPD.mjs} +8 -8
  2. package/dist/{chunk-DA5T64RY.mjs → chunk-54GVCXHZ.mjs} +2 -2
  3. package/dist/{chunk-T64GA5HP.mjs → chunk-5G2INROS.mjs} +867 -537
  4. package/dist/chunk-5G2INROS.mjs.map +1 -0
  5. package/dist/{chunk-JH5RAJT2.mjs → chunk-67HNEIKY.mjs} +2083 -1858
  6. package/dist/chunk-67HNEIKY.mjs.map +1 -0
  7. package/dist/{chunk-RD335EMA.mjs → chunk-7TDYBU5L.mjs} +29 -52
  8. package/dist/chunk-7TDYBU5L.mjs.map +1 -0
  9. package/dist/{chunk-RDCUGC24.mjs → chunk-HUKZ2PBC.mjs} +2 -2
  10. package/dist/{chunk-YPQ72HMX.mjs → chunk-LWC7JQ2W.mjs} +44 -5
  11. package/dist/chunk-LWC7JQ2W.mjs.map +1 -0
  12. package/dist/{chunk-EXD2SXWL.mjs → chunk-OIZ64ET3.mjs} +40 -7
  13. package/dist/chunk-OIZ64ET3.mjs.map +1 -0
  14. package/dist/{chunk-WQCY45U2.mjs → chunk-SJQVAKFA.mjs} +5 -4
  15. package/dist/chunk-SJQVAKFA.mjs.map +1 -0
  16. package/dist/{chunk-DBBL74RK.mjs → chunk-WL2ISRYH.mjs} +164 -82
  17. package/dist/chunk-WL2ISRYH.mjs.map +1 -0
  18. package/dist/{chunk-UWWFOEGF.mjs → chunk-XZ5FFDLX.mjs} +143 -137
  19. package/dist/chunk-XZ5FFDLX.mjs.map +1 -0
  20. package/dist/{chunk-SNCXU2QL.mjs → chunk-ZF67S6RY.mjs} +2 -2
  21. package/dist/cli.mjs +9 -7
  22. package/dist/contacts-worker.mjs +2 -2
  23. package/dist/global-insight-worker.mjs +1 -1
  24. package/dist/{http-3B5ARN7A.mjs → http-AMDWRKOH.mjs} +11 -11
  25. package/dist/{import-2DZGNSEX.mjs → import-WIJHB4LH.mjs} +9 -7
  26. package/dist/index.mjs +12 -12
  27. package/dist/{mcp-44I6FUUQ.mjs → mcp-NC5YNTM5.mjs} +8 -8
  28. package/dist/people-relationships-worker.mjs +2 -2
  29. package/dist/semantic-index-worker.mjs +1 -1
  30. package/dist/{src-R3QX6L3L.mjs → src-UY4GCVNF.mjs} +34 -6
  31. package/dist/{stream-import-VUEL2TTU.mjs → stream-import-JKQYRISL.mjs} +9 -7
  32. package/dist/{worker-runtime-GJAOIIZW.mjs → worker-runtime-KKUOJJTN.mjs} +5 -5
  33. package/dist-web/assets/{ActionToolsPanel-fAio70bI.css → ActionToolsPanel-BL0GQR6r.css} +1 -1
  34. package/dist-web/assets/ActionToolsPanel.vue_vue_type_script_setup_true_lang-DVdpZpfD.js +110 -0
  35. package/dist-web/assets/{BufferResource-BPGkshPa.js → BufferResource-i1amDJTV.js} +1 -1
  36. package/dist-web/assets/{CanvasRenderer-DTKkZhq4.js → CanvasRenderer-DGa7AjdC.js} +1 -1
  37. package/dist-web/assets/EChartWordcloud-BXC_4nUP.js +1 -0
  38. package/dist-web/assets/{Filter-CHXKzS2l.js → Filter-BTlq0yU-.js} +1 -1
  39. package/dist-web/assets/LoadingState.vue_vue_type_script_setup_true_lang-B2KAoiMG.js +1 -0
  40. package/dist-web/assets/MessageExportModal-D9C5t_zF.js +1 -0
  41. package/dist-web/assets/PageHeader-BEWr2vsg.js +1 -0
  42. package/dist-web/assets/{RelationshipGalaxyCanvas-Dwdy5flh.js → RelationshipGalaxyCanvas-CSgPNRif.js} +4 -4
  43. package/dist-web/assets/{RelationshipGalaxyThreeCanvas-CtY4TGen.js → RelationshipGalaxyThreeCanvas-CBAnU8vN.js} +1 -1
  44. package/dist-web/assets/{RenderTargetSystem-BE56mKRG.js → RenderTargetSystem-Ctlu222X.js} +1 -1
  45. package/dist-web/assets/ThemeCard.vue_vue_type_script_setup_true_lang-BWJ6kuSC.js +1 -0
  46. package/dist-web/assets/TimeSelect.vue_vue_type_script_setup_true_lang-D5vov0eO.js +1 -0
  47. package/dist-web/assets/{WebGLRenderer-Dm18dxOf.js → WebGLRenderer-D2Zvhmcw.js} +1 -1
  48. package/dist-web/assets/{WebGPURenderer-BF6TF-is.js → WebGPURenderer-BOtP6h9o.js} +1 -1
  49. package/dist-web/assets/annual-summary-time-range-CK0IIPsY.js +1 -0
  50. package/dist-web/assets/{base-CFPyD9Gy.js → base-BMjDSaj6.js} +1 -1
  51. package/dist-web/assets/{browserAll-Cn_CfkXl.js → browserAll-BhwLvWQY.js} +1 -1
  52. package/dist-web/assets/dateFormat-dteP1INW.js +1 -0
  53. package/dist-web/assets/{electron-B6xOabTq.js → electron-DCgfulPE.js} +1 -1
  54. package/dist-web/assets/{fetch-DMLq3xPG.js → fetch-6q7oz7ZO.js} +1 -1
  55. package/dist-web/assets/{fetch-C_EfVFJi.js → fetch-B_0HIxs5.js} +1 -1
  56. package/dist-web/assets/{fetch-DylpNKCR.js → fetch-BbU4MPPu.js} +1 -1
  57. package/dist-web/assets/fetch-BxHdB6X1.js +6 -0
  58. package/dist-web/assets/{fetch-CEhUu1Yu.js → fetch-CpvfCafG.js} +1 -1
  59. package/dist-web/assets/{fetch-etgwVKIw.js → fetch-CubnKpLG.js} +1 -1
  60. package/dist-web/assets/{fetch-BRXmDI_2.js → fetch-D5BrGna5.js} +1 -1
  61. package/dist-web/assets/fetch-DgXWr9nB.js +31 -0
  62. package/dist-web/assets/{fetch-BPkgnY0R.js → fetch-DvZuLuTE.js} +1 -1
  63. package/dist-web/assets/{fetch-DL_u25Ui.js → fetch-rJA9qLvd.js} +1 -1
  64. package/dist-web/assets/index-BfGKlkx3.js +2 -0
  65. package/dist-web/assets/index-BoalB6nB.js +1 -0
  66. package/dist-web/assets/index-Bp5VKUF9.js +1 -0
  67. package/dist-web/assets/index-Bs15Yc7I.js +1 -0
  68. package/dist-web/assets/index-CAjUCtGD.css +1 -0
  69. package/dist-web/assets/index-CLijvqy3.js +173 -0
  70. package/dist-web/assets/{index-0x1QPzKZ.js → index-CUzFFw5b.js} +1 -1
  71. package/dist-web/assets/index-Cj2GU1SR.js +1 -0
  72. package/dist-web/assets/{index-y7cxH1Et.css → index-CsqMIyA9.css} +1 -1
  73. package/dist-web/assets/index-CtmXho1w.css +1 -0
  74. package/dist-web/assets/index-D7aKwT6w.js +64 -0
  75. package/dist-web/assets/index-DXcFBsM1.js +24 -0
  76. package/dist-web/assets/index-DaZnh1zP.js +2 -0
  77. package/dist-web/assets/index-DeV8SAiM.js +25 -0
  78. package/dist-web/assets/index-WJCmqKnh.js +1 -0
  79. package/dist-web/assets/index-t6s0nOlb.js +1 -0
  80. package/dist-web/assets/people-page-header-D93GTPBO.js +1 -0
  81. package/dist-web/assets/vendor-nuxt-ui-DUpPmItD.js +15 -0
  82. package/dist-web/assets/vendor-reka-ui-BLnO2MqY.js +15 -0
  83. package/dist-web/assets/vendor-snapdom--nZvo8Sg.js +8 -0
  84. package/dist-web/assets/web-BoTDL3HM.js +1 -0
  85. package/dist-web/assets/{webworkerAll-BKGP--A4.js → webworkerAll-c0ZvpENM.js} +1 -1
  86. package/dist-web/index.html +5 -5
  87. package/package.json +1 -1
  88. package/dist/chunk-DBBL74RK.mjs.map +0 -1
  89. package/dist/chunk-EXD2SXWL.mjs.map +0 -1
  90. package/dist/chunk-JH5RAJT2.mjs.map +0 -1
  91. package/dist/chunk-RD335EMA.mjs.map +0 -1
  92. package/dist/chunk-T64GA5HP.mjs.map +0 -1
  93. package/dist/chunk-UWWFOEGF.mjs.map +0 -1
  94. package/dist/chunk-WQCY45U2.mjs.map +0 -1
  95. package/dist/chunk-YPQ72HMX.mjs.map +0 -1
  96. package/dist-web/assets/ActionToolsPanel.vue_vue_type_script_setup_true_lang-CdJT0t2f.js +0 -110
  97. package/dist-web/assets/EChartWordcloud-DU3ZYdgz.js +0 -1
  98. package/dist-web/assets/LoadingState.vue_vue_type_script_setup_true_lang-DACMroOe.js +0 -1
  99. package/dist-web/assets/MessageExportModal-h-zl0dx1.js +0 -1
  100. package/dist-web/assets/PageHeader-CK0hTsdx.js +0 -1
  101. package/dist-web/assets/ThemeCard.vue_vue_type_script_setup_true_lang-BoZyssJF.js +0 -1
  102. package/dist-web/assets/TimeSelect.vue_vue_type_script_setup_true_lang-27hMyAUE.js +0 -1
  103. package/dist-web/assets/annual-summary-time-range-DVLXK1r4.js +0 -1
  104. package/dist-web/assets/dateFormat-BcqNKTQ1.js +0 -1
  105. package/dist-web/assets/fetch-BUm1isWe.js +0 -5
  106. package/dist-web/assets/fetch-CBOJwwJp.js +0 -17
  107. package/dist-web/assets/index-B5S9FnfP.js +0 -1
  108. package/dist-web/assets/index-BS6loLHW.js +0 -1
  109. package/dist-web/assets/index-BrTlAl8b.js +0 -1
  110. package/dist-web/assets/index-C3TnydSt.js +0 -24
  111. package/dist-web/assets/index-CAWO10k1.js +0 -2
  112. package/dist-web/assets/index-C_0GHEcb.css +0 -1
  113. package/dist-web/assets/index-Chm17wNe.js +0 -2
  114. package/dist-web/assets/index-DAr_2DlV.js +0 -173
  115. package/dist-web/assets/index-Em6upenM.js +0 -64
  116. package/dist-web/assets/index-FoTy2Cce.js +0 -1
  117. package/dist-web/assets/index-YADL4XIp.js +0 -22
  118. package/dist-web/assets/index-_6lck7Bv.css +0 -1
  119. package/dist-web/assets/index-aMIgqj43.js +0 -1
  120. package/dist-web/assets/index-zZVsV5HB.js +0 -1
  121. package/dist-web/assets/people-page-header-BdKM1qhX.js +0 -1
  122. package/dist-web/assets/vendor-nuxt-ui-CS1uxE9C.js +0 -15
  123. package/dist-web/assets/vendor-reka-ui-Cta6JPvN.js +0 -11
  124. package/dist-web/assets/vendor-snapdom-XnBoh1gd.js +0 -8
  125. package/dist-web/assets/web-BUZ-dZy0.js +0 -1
  126. /package/dist/{chat-command-CHV7RLBH.mjs.map → chat-command-V6NYTTPD.mjs.map} +0 -0
  127. /package/dist/{chunk-DA5T64RY.mjs.map → chunk-54GVCXHZ.mjs.map} +0 -0
  128. /package/dist/{chunk-RDCUGC24.mjs.map → chunk-HUKZ2PBC.mjs.map} +0 -0
  129. /package/dist/{chunk-SNCXU2QL.mjs.map → chunk-ZF67S6RY.mjs.map} +0 -0
  130. /package/dist/{http-3B5ARN7A.mjs.map → http-AMDWRKOH.mjs.map} +0 -0
  131. /package/dist/{import-2DZGNSEX.mjs.map → import-WIJHB4LH.mjs.map} +0 -0
  132. /package/dist/{mcp-44I6FUUQ.mjs.map → mcp-NC5YNTM5.mjs.map} +0 -0
  133. /package/dist/{src-R3QX6L3L.mjs.map → src-UY4GCVNF.mjs.map} +0 -0
  134. /package/dist/{stream-import-VUEL2TTU.mjs.map → stream-import-JKQYRISL.mjs.map} +0 -0
  135. /package/dist/{worker-runtime-GJAOIIZW.mjs.map → worker-runtime-KKUOJJTN.mjs.map} +0 -0
@@ -4,26 +4,26 @@ import { fileURLToPath as __fileURLToPath } from 'url';
4
4
  const require = __createRequire(import.meta.url);
5
5
  const __filename = __fileURLToPath(import.meta.url);
6
6
  const __dirname = __pathDirname(__filename);
7
- import {
8
- analyzeIncrementalImport,
9
- analyzeNewImport,
10
- autoImport,
11
- incrementalImport,
12
- streamImport
13
- } from "./chunk-EXD2SXWL.mjs";
14
7
  import {
15
8
  closeServerAiLogger,
16
9
  createCliRunAgentStream,
17
10
  getServerAiLogger,
18
11
  initServerAiLogger
19
- } from "./chunk-RD335EMA.mjs";
12
+ } from "./chunk-7TDYBU5L.mjs";
20
13
  import {
21
14
  AGENT_TOOL_REGISTRY,
22
15
  CoreDataProvider
23
- } from "./chunk-YPQ72HMX.mjs";
16
+ } from "./chunk-LWC7JQ2W.mjs";
17
+ import {
18
+ analyzeIncrementalImport,
19
+ analyzeNewImport,
20
+ autoImport,
21
+ incrementalImport,
22
+ streamImport
23
+ } from "./chunk-OIZ64ET3.mjs";
24
24
  import {
25
25
  assertCliDataDirCompatible
26
- } from "./chunk-DA5T64RY.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-WQCY45U2.mjs";
44
+ } from "./chunk-SJQVAKFA.mjs";
45
45
  import {
46
46
  AIChatManager,
47
47
  ArchiveImportError,
@@ -50,8 +50,8 @@ import {
50
50
  CustomModelStore,
51
51
  CustomProviderStore,
52
52
  IMPORT_IN_PROGRESS_ERROR_KEY,
53
+ ImportIdempotencyCache,
53
54
  ImportInProgressError,
54
- LLMConfigStore,
55
55
  MergeSessionCache,
56
56
  SEMANTIC_INDEX_CONFIG_FILE,
57
57
  SemanticIndexConfigStore,
@@ -60,7 +60,10 @@ import {
60
60
  buildPiModel,
61
61
  computeWordFrequency,
62
62
  countMessagesTokens,
63
+ createAuthProfileLlmConfigStore,
64
+ createChatLabTempDir,
63
65
  createContactsService,
66
+ createFileConfigStorage,
64
67
  createGlobalInsightService,
65
68
  createPeopleRelationshipsService,
66
69
  deleteDict,
@@ -72,11 +75,14 @@ import {
72
75
  export_service_exports,
73
76
  fetchRemoteModels,
74
77
  findEntryFileInDirectory,
78
+ getChatLabTempScopeDir,
75
79
  getDefaultRulesForLocale,
76
80
  getDictList,
77
81
  getSupportedFormats,
82
+ hashImportBody,
78
83
  isDictDownloaded,
79
84
  isSemanticIndexConfigured,
85
+ isValidImportSessionId,
80
86
  logNativeParserStatus,
81
87
  member_service_exports,
82
88
  mergeRulesForLocale,
@@ -92,7 +98,7 @@ import {
92
98
  streamParseFileInfo,
93
99
  summary_service_exports,
94
100
  validateApiKey
95
- } from "./chunk-T64GA5HP.mjs";
101
+ } from "./chunk-5G2INROS.mjs";
96
102
  import {
97
103
  resolveCliPath
98
104
  } from "./chunk-FJ35V3TE.mjs";
@@ -108,7 +114,6 @@ import {
108
114
  POS_TAG_DEFINITIONS,
109
115
  appLogger,
110
116
  createDatabaseManagerAdapter,
111
- deleteAuthProfile,
112
117
  executeReadonlySql,
113
118
  executeSql,
114
119
  getAvailableYears,
@@ -127,8 +132,7 @@ import {
127
132
  getLongMessageCount,
128
133
  getMemberActivity,
129
134
  getMemberMonthlyTrend,
130
- getMembers,
131
- getMembersDetailed,
135
+ getMembersWithAliases,
132
136
  getMemeBattleAnalysis,
133
137
  getMentionAnalysis,
134
138
  getMentionGraph,
@@ -153,18 +157,16 @@ import {
153
157
  isNewerStableVersion,
154
158
  loadConfig,
155
159
  queryMessages,
156
- resolveApiKey,
157
160
  stripAvatarFields,
158
161
  verifyCliDataPath,
159
- writeAuthProfile,
160
162
  writeConfigField
161
- } from "./chunk-UWWFOEGF.mjs";
163
+ } from "./chunk-XZ5FFDLX.mjs";
162
164
 
163
165
  // src/http/index.ts
164
166
  import * as fs12 from "fs";
165
167
  import * as crypto4 from "crypto";
166
168
 
167
- // src/http/server.ts
169
+ // ../../packages/http-routes/src/server.ts
168
170
  import Fastify from "fastify";
169
171
 
170
172
  // ../../packages/http-routes/src/errors.ts
@@ -178,6 +180,8 @@ var HTTP_STATUS = {
178
180
  ["EXPORT_TOO_LARGE" /* EXPORT_TOO_LARGE */]: 400,
179
181
  ["BODY_TOO_LARGE" /* BODY_TOO_LARGE */]: 413,
180
182
  ["IMPORT_IN_PROGRESS" /* IMPORT_IN_PROGRESS */]: 409,
183
+ ["IDEMPOTENCY_CONFLICT" /* IDEMPOTENCY_CONFLICT */]: 409,
184
+ ["IDEMPOTENCY_PENDING" /* IDEMPOTENCY_PENDING */]: 409,
181
185
  ["IMPORT_FAILED" /* IMPORT_FAILED */]: 500,
182
186
  ["DATA_DIR_INCOMPATIBLE" /* DATA_DIR_INCOMPATIBLE */]: 409,
183
187
  ["SERVER_ERROR" /* SERVER_ERROR */]: 500
@@ -213,8 +217,17 @@ function exportTooLarge(count, limit) {
213
217
  function serverError(message = "Internal server error") {
214
218
  return new ApiError("SERVER_ERROR" /* SERVER_ERROR */, message);
215
219
  }
216
- function importInProgress() {
217
- 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
+ }
223
+ function idempotencyConflict() {
224
+ return new ApiError("IDEMPOTENCY_CONFLICT" /* IDEMPOTENCY_CONFLICT */, "Same Idempotency-Key with different request body hash");
225
+ }
226
+ function idempotencyPending() {
227
+ return new ApiError(
228
+ "IDEMPOTENCY_PENDING" /* IDEMPOTENCY_PENDING */,
229
+ "A request with this Idempotency-Key is still in progress. Please retry later."
230
+ );
218
231
  }
219
232
  function importFailed(message) {
220
233
  return new ApiError("IMPORT_FAILED" /* IMPORT_FAILED */, message);
@@ -225,7 +238,18 @@ function dataDirIncompatible(message) {
225
238
  function apiErrorFromUnknown(error) {
226
239
  if (error instanceof ApiError) return error;
227
240
  if (error instanceof ImportInProgressError) return importInProgress();
228
- 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
+ }
229
253
  return null;
230
254
  }
231
255
  function successResponse(data, meta) {
@@ -249,195 +273,378 @@ function errorResponse(error) {
249
273
  };
250
274
  }
251
275
 
252
- // ../../packages/http-routes/src/routes/system.ts
253
- function registerSystemRoutes(server2, ctx) {
254
- server2.get("/api/v1/status", async () => {
255
- let sessionCount = 0;
256
- try {
257
- sessionCount = ctx.dbManager.listSessionIds().length;
258
- } 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;
259
284
  }
260
- return successResponse({
261
- name: "ChatLab API",
262
- version: ctx.getVersion(),
263
- uptime: Math.floor(process.uptime()),
264
- sessionCount
265
- });
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));
266
298
  });
267
- server2.get("/api/v1/schema", async () => {
268
- return successResponse({
269
- format: "ChatLab Format",
270
- version: "0.0.2",
271
- spec: {
272
- chatlab: {
273
- type: "object",
274
- required: ["version"],
275
- properties: {
276
- version: { type: "string" },
277
- exportedAt: { type: "number" },
278
- generator: { type: "string" }
279
- }
280
- },
281
- meta: {
282
- type: "object",
283
- required: ["name", "platform", "type"],
284
- properties: {
285
- name: { type: "string" },
286
- platform: {
287
- type: "string",
288
- enum: ["qq", "wechat", "telegram", "discord", "line", "whatsapp", "instagram", "unknown"]
289
- },
290
- type: { type: "string", enum: ["group", "private"] },
291
- groupId: { type: "string" }
292
- }
293
- },
294
- members: {
295
- type: "array",
296
- items: {
297
- type: "object",
298
- required: ["platformId", "accountName"],
299
- properties: {
300
- platformId: { type: "string" },
301
- accountName: { type: "string" },
302
- groupNickname: { type: "string" },
303
- avatar: { type: "string" }
304
- }
305
- }
306
- },
307
- messages: {
308
- type: "array",
309
- items: {
310
- type: "object",
311
- required: ["sender", "timestamp", "type"],
312
- properties: {
313
- platformMessageId: { type: "string" },
314
- sender: { type: "string" },
315
- accountName: { type: "string" },
316
- groupNickname: { type: "string" },
317
- timestamp: { type: "number" },
318
- type: { type: "number" },
319
- content: { type: ["string", "null"] },
320
- replyToMessageId: { type: "string" }
321
- }
322
- }
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));
323
420
  }
324
421
  }
325
- });
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)
326
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
+ );
327
486
  }
328
487
 
329
- // ../../packages/http-routes/src/routes/sessions.ts
330
- var EXPORT_MESSAGE_LIMIT = 1e5;
331
- function ensureDb(ctx, sessionId) {
332
- const db = ctx.dbManager.open(sessionId);
333
- if (!db) throw sessionNotFound(sessionId);
334
- 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
+ };
335
494
  }
336
- function registerRestSessionRoutes(server2, ctx) {
337
- server2.get("/api/v1/sessions", async () => {
338
- const sessionIds = ctx.dbManager.listSessionIds();
339
- const sessions = sessionIds.map((id) => {
340
- 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);
341
547
  if (!db) return null;
342
- const info = getSessionInfo(db);
343
- 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
+ }
344
555
  return {
345
- id,
346
- name: info.name,
347
- platform: info.platform,
348
- type: info.type,
349
- groupId: info.groupId || void 0,
350
- messageCount: info.messageCount,
351
- memberCount: info.memberCount,
352
- firstTimestamp: info.firstMessageTs,
353
- 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
354
586
  };
355
- }).filter(Boolean);
356
- 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());
357
596
  });
358
597
  server2.get("/api/v1/sessions/:id", async (request) => {
359
- const db = ensureDb(ctx, request.params.id);
360
- const info = getSessionInfo(db);
361
- if (!info) throw sessionNotFound(request.params.id);
362
- return successResponse({
363
- id: request.params.id,
364
- name: info.name,
365
- platform: info.platform,
366
- type: info.type,
367
- groupId: info.groupId || void 0,
368
- messageCount: info.messageCount,
369
- memberCount: info.memberCount,
370
- firstTimestamp: info.firstMessageTs,
371
- lastTimestamp: info.lastMessageTs,
372
- lastPlatformMessageId: getLastPlatformMessageId(db),
373
- importedAt: info.importedAt
374
- });
598
+ const session = await provider.getSession(request.params.id);
599
+ if (!session) throw sessionNotFound(request.params.id);
600
+ return successResponse(session);
375
601
  });
376
602
  server2.get("/api/v1/sessions/:id/messages", async (request) => {
377
603
  const { id } = request.params;
378
- const db = ensureDb(ctx, id);
379
604
  const page = Math.max(1, parseInt(request.query.page || "1", 10) || 1);
380
605
  const limit = Math.min(1e3, Math.max(1, parseInt(request.query.limit || "100", 10) || 100));
381
606
  const offset = (page - 1) * limit;
382
607
  const { startTime, endTime, keyword, senderId } = request.query;
383
- const result = queryMessages(db, {
608
+ const result = await provider.queryMessages(id, {
384
609
  keyword: keyword || void 0,
385
- startTs: startTime ? parseInt(startTime, 10) : void 0,
386
- endTs: endTime ? parseInt(endTime, 10) : void 0,
610
+ startTime: startTime ? parseInt(startTime, 10) : void 0,
611
+ endTime: endTime ? parseInt(endTime, 10) : void 0,
387
612
  senderId: senderId ? parseInt(senderId, 10) : void 0,
388
613
  limit,
389
614
  offset
390
615
  });
616
+ if (!result) throw sessionNotFound(id);
391
617
  return successResponse({
392
618
  messages: result.messages,
393
619
  total: result.total,
394
620
  page,
395
621
  limit,
396
- totalPages: result.totalPages
622
+ totalPages: result.totalPages ?? Math.ceil(result.total / limit)
397
623
  });
398
624
  });
399
625
  server2.get("/api/v1/sessions/:id/members", async (request) => {
400
- const db = ensureDb(ctx, request.params.id);
401
- const members = getMembers(db);
626
+ const members = await provider.getMembers(request.params.id);
627
+ if (!members) throw sessionNotFound(request.params.id);
402
628
  return successResponse(members);
403
629
  });
404
630
  server2.get("/api/v1/sessions/:id/stats/overview", async (request) => {
405
- const { id } = request.params;
406
- const db = ensureDb(ctx, id);
407
- const overview = getSessionOverview(db);
408
- const memberActivity = getMemberActivity(db);
409
- const typeDistribution = getMessageTypeStats(db);
410
- const typeMap = {};
411
- for (const item of typeDistribution) {
412
- typeMap[String(item.type)] = item.count;
413
- }
414
- const topMembers = memberActivity.slice(0, 10).map((m) => ({
415
- platformId: m.platformId,
416
- name: m.name,
417
- messageCount: m.messageCount,
418
- percentage: m.percentage
419
- }));
420
- return successResponse({
421
- messageCount: overview.totalMessages,
422
- memberCount: overview.totalMembers,
423
- timeRange: {
424
- start: overview.firstMessageTs ?? 0,
425
- end: overview.lastMessageTs ?? 0
426
- },
427
- messageTypeDistribution: typeMap,
428
- topMembers
429
- });
631
+ const overview = await provider.getOverview(request.params.id);
632
+ if (!overview) throw sessionNotFound(request.params.id);
633
+ return successResponse(overview);
430
634
  });
431
635
  server2.post("/api/v1/sessions/:id/sql", async (request, reply) => {
432
636
  const { id } = request.params;
433
- const db = ensureDb(ctx, id);
434
637
  const { sql } = request.body || {};
435
638
  if (!sql || typeof sql !== "string") {
436
639
  const err = sqlExecutionError("Missing sql parameter");
437
640
  return reply.code(err.statusCode).send(errorResponse(err));
438
641
  }
439
642
  try {
440
- 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
+ }
441
648
  return successResponse(result);
442
649
  } catch (err) {
443
650
  const message = err instanceof Error ? err.message : "SQL execution error";
@@ -452,16 +659,14 @@ function registerRestSessionRoutes(server2, ctx) {
452
659
  });
453
660
  server2.get("/api/v1/sessions/:id/export", async (request, reply) => {
454
661
  const { id } = request.params;
455
- const db = ensureDb(ctx, id);
456
- const meta = getSessionMeta(db);
457
- if (!meta) throw sessionNotFound(id);
458
- const overview = getSessionOverview(db);
459
- if (overview.totalMessages > EXPORT_MESSAGE_LIMIT) {
460
- 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);
461
666
  return reply.code(err.statusCode).send(errorResponse(err));
462
667
  }
463
- const members = getMembersDetailed(db);
464
- 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);
465
670
  const chatLabFormat = {
466
671
  chatlab: {
467
672
  version: "0.0.2",
@@ -469,17 +674,18 @@ function registerRestSessionRoutes(server2, ctx) {
469
674
  generator: "ChatLab API"
470
675
  },
471
676
  meta: {
472
- name: meta.name,
473
- platform: meta.platform,
474
- type: meta.type,
475
- groupId: meta.groupId || void 0
677
+ name: session.name,
678
+ platform: session.platform,
679
+ type: session.type,
680
+ groupId: session.groupId || void 0
476
681
  },
477
- members: members.map((m) => ({
682
+ members: exportData.members.map((m) => ({
478
683
  platformId: m.platformId,
479
684
  accountName: m.accountName || m.platformId,
480
- 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
481
687
  })),
482
- messages: allMessages.messages.map((msg) => ({
688
+ messages: exportData.messages.map((msg) => ({
483
689
  sender: msg.senderPlatformId,
484
690
  accountName: msg.senderName || void 0,
485
691
  timestamp: msg.timestamp,
@@ -491,528 +697,1027 @@ function registerRestSessionRoutes(server2, ctx) {
491
697
  });
492
698
  }
493
699
 
494
- // ../../packages/http-routes/src/routes/imports.ts
495
- function registerImportRoutes(server2, ctx) {
496
- server2.post(
497
- "/api/v1/imports/:sessionId",
498
- async (request, reply) => {
499
- const { sessionId } = request.params;
500
- const contentType = request.headers["content-type"] || "";
501
- if (!contentType.includes("application/json")) {
502
- const err = invalidPayload("Content-Type must be application/json (JSONL is not yet supported)");
503
- return reply.code(err.statusCode).send(errorResponse(err));
504
- }
505
- if (request.headers["x-dry-run"] === "true") {
506
- const err = invalidPayload("X-Dry-Run is not yet supported");
507
- return reply.code(err.statusCode).send(errorResponse(err));
508
- }
509
- const outcome = await pushImport(ctx.dbManager, sessionId, request.body ?? {});
510
- if (!outcome.ok) {
511
- if (outcome.reason === "import_in_progress") {
512
- const err2 = importInProgress();
513
- return reply.code(err2.statusCode).send(errorResponse(err2));
514
- }
515
- if (outcome.reason === "invalid_payload") {
516
- const err2 = invalidPayload(outcome.message);
517
- return reply.code(err2.statusCode).send(errorResponse(err2));
518
- }
519
- const err = importFailed(outcome.message);
520
- return reply.code(err.statusCode).send(errorResponse(err));
521
- }
522
- return successResponse(outcome.result);
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 {
523
707
  }
524
- );
525
- }
526
-
527
- // ../../packages/http-routes/src/routes/web/sessions.ts
528
- function registerSessionRoutes(server2, ctx) {
529
- const { sessionAdapter: adapter } = ctx;
530
- let preferencesInstance = null;
531
- const preferences = () => {
532
- preferencesInstance ??= ctx.preferencesManager ?? new PreferencesManager(ctx.pathProvider.getSystemDir());
533
- return preferencesInstance;
534
- };
535
- server2.get("/_web/sessions", async () => {
536
- const aiChatCounts = ctx.aiChatManager?.getAIChatCountsBySession();
537
- return session_service_exports.listAnalysisSessions(adapter, {
538
- 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
539
713
  });
540
714
  });
541
- server2.get("/_web/sessions/:id", async (request) => {
542
- const session = session_service_exports.getAnalysisSession(adapter, request.params.id);
543
- if (!session) {
544
- throw Object.assign(new Error(`Session not found: ${request.params.id}`), { statusCode: 404 });
545
- }
546
- if (ctx.aiChatManager) {
547
- session.aiConversationCount = ctx.aiChatManager.getAIChats(request.params.id).length;
548
- }
549
- return session;
550
- });
551
- server2.delete("/_web/sessions/:id", async (request, reply) => {
552
- const { id } = request.params;
553
- try {
554
- const deleted = session_service_exports.deleteSession(adapter, id);
555
- if (!deleted) {
556
- 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
+ }
557
772
  }
558
- return { success: true };
559
- } catch (err) {
560
- return reply.code(500).send({ success: false, error: String(err) });
561
- }
562
- });
563
- server2.patch("/_web/sessions/:id/name", async (request) => {
564
- session_service_exports.renameSession(adapter, request.params.id, request.body.name);
565
- return { success: true };
566
- });
567
- server2.patch(
568
- "/_web/sessions/:id/owner",
569
- async (request) => {
570
- session_service_exports.updateSessionOwnerId(adapter, request.params.id, request.body.ownerId ?? null);
571
- return { success: true };
572
- }
573
- );
574
- server2.post("/_web/sessions/:id/owner/apply-profile", async (request) => {
575
- return owner_profile_service_exports.tryApplyOwnerProfile(adapter, preferences(), request.params.id);
576
- });
577
- server2.post(
578
- "/_web/sessions/:id/owner/select",
579
- async (request) => {
580
- return owner_profile_service_exports.setOwnerAndApplyProfile(
581
- adapter,
582
- preferences(),
583
- request.params.id,
584
- request.body.ownerPlatformId
585
- );
586
- }
587
- );
588
- server2.post("/_web/sessions/:id/owner/dismiss-prompt", async (request) => {
589
- owner_profile_service_exports.dismissOwnerPrompt(preferences(), request.params.id);
590
- return { success: true };
773
+ });
591
774
  });
592
775
  }
593
776
 
594
- // ../../packages/http-routes/src/routes/web/members.ts
595
- function registerMemberRoutes(server2, ctx) {
596
- const { sessionAdapter: adapter } = ctx;
597
- server2.get("/_web/sessions/:id/members", async (request) => {
598
- return member_service_exports.getMembers(adapter, request.params.id);
599
- });
600
- server2.get("/_web/sessions/:id/members/paginated", async (request) => {
601
- return member_service_exports.getMembersPaginated(adapter, request.params.id, {
602
- page: parseInt(request.query.page || "1", 10),
603
- pageSize: parseInt(request.query.pageSize || "20", 10),
604
- search: request.query.search,
605
- 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
606
802
  });
607
- });
608
- server2.patch(
609
- "/_web/sessions/:id/members/:memberId/aliases",
610
- async (request) => {
611
- const memberId = parseInt(request.params.memberId, 10);
612
- member_service_exports.updateMemberAliases(adapter, request.params.id, memberId, request.body.aliases);
613
- return { success: true };
614
- }
615
- );
616
- server2.delete(
617
- "/_web/sessions/:id/members/:memberId",
618
- async (request) => {
619
- const memberId = parseInt(request.params.memberId, 10);
620
- member_service_exports.deleteMember(adapter, request.params.id, memberId);
621
- 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();
622
838
  }
623
- );
624
- server2.post(
625
- "/_web/sessions/:id/members/merge",
626
- async (request) => {
627
- const { memberId1, memberId2 } = request.body;
628
- 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);
629
846
  return { success: true };
630
847
  }
631
- );
632
- server2.get(
633
- "/_web/sessions/:id/members/:memberId/history",
634
- async (request) => {
635
- const memberId = parseInt(request.params.memberId, 10);
636
- return member_service_exports.getMemberNameHistory(adapter, request.params.id, memberId);
637
- }
638
- );
848
+ return { success: false };
849
+ });
639
850
  }
640
851
 
641
- // ../../packages/http-routes/src/routes/web/contacts.ts
642
- function registerContactsRoutes(server2, ctx) {
643
- const service = ctx.contactsService ?? createContactsService({
644
- adapter: ctx.sessionAdapter,
645
- pathProvider: ctx.pathProvider,
646
- runtimeIdentity: ctx.runtimeIdentity,
647
- 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;
648
856
  });
649
- server2.addHook("onClose", async () => {
650
- await service.close();
857
+ const mgr = ctx.assistantManager;
858
+ if (!mgr) return;
859
+ server2.get("/_web/ai/assistants", async () => {
860
+ return mgr.getAllAssistants();
651
861
  });
652
- server2.get("/_web/contacts", async (request) => {
653
- return service.getContactsPage({
654
- acceptStale: isTruthy(request.query.acceptStale),
655
- timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange),
656
- pool: parseContactPool(request.query.pool),
657
- page: parsePositiveInteger(request.query.page),
658
- pageSize: parsePositiveInteger(request.query.pageSize),
659
- query: request.query.q
660
- });
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;
661
866
  });
662
- server2.post("/_web/contacts/recompute", async (request) => {
663
- return service.startRecompute({
664
- timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange),
665
- pool: parseContactPool(request.query.pool),
666
- page: parsePositiveInteger(request.query.page),
667
- pageSize: parsePositiveInteger(request.query.pageSize),
668
- query: request.query.q
669
- });
867
+ server2.post("/_web/ai/assistants", async (request) => {
868
+ return mgr.createAssistant(request.body);
670
869
  });
671
- server2.get("/_web/contacts/:key/detail", async (request) => {
672
- return service.getContactDetail(request.params.key, {
673
- acceptStale: isTruthy(request.query.acceptStale),
674
- timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange)
675
- });
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;
676
874
  });
677
- server2.put(
678
- "/_web/contacts/:key/mark-friend",
679
- async (request) => {
680
- return service.markContactAsFriend(request.params.key, {
681
- timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange)
682
- });
683
- }
684
- );
685
- server2.delete(
686
- "/_web/contacts/:key/mark-friend",
687
- async (request) => {
688
- return service.unmarkContactAsFriend(request.params.key, {
689
- timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange)
690
- });
691
- }
692
- );
693
- }
694
- function isTruthy(value) {
695
- return value === "1" || value === "true" || value === "yes";
696
- }
697
- function parseContactsTimeRangePreset(value) {
698
- return CONTACTS_TIME_RANGE_PRESETS.includes(value) ? value : "1y";
699
- }
700
- function parseContactPool(value) {
701
- return value === "friend" || value === "non_friend" ? value : void 0;
702
- }
703
- function parsePositiveInteger(value) {
704
- if (!value) return void 0;
705
- const parsed = Number(value);
706
- if (!Number.isFinite(parsed)) return void 0;
707
- return Math.max(1, Math.trunc(parsed));
708
- }
709
-
710
- // ../../packages/http-routes/src/routes/web/people-relationships.ts
711
- function registerPeopleRelationshipsRoutes(server2, ctx) {
712
- const service = ctx.peopleRelationshipsService ?? createPeopleRelationshipsService({
713
- adapter: ctx.sessionAdapter,
714
- pathProvider: ctx.pathProvider,
715
- runtimeIdentity: ctx.runtimeIdentity,
716
- 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;
717
879
  });
718
- server2.addHook("onClose", async () => {
719
- 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;
720
884
  });
721
- server2.get("/_web/people/relationships", async (request) => {
722
- return service.getGraph({
723
- acceptStale: isTruthy2(request.query.acceptStale),
724
- timeRangePreset: parseContactsTimeRangePreset2(request.query.timeRange),
725
- graphScope: parsePeopleRelationshipsGraphScope(request.query.scope),
726
- query: request.query.q
727
- });
885
+ server2.post("/_web/ai/assistants/import", async (request) => {
886
+ return mgr.importAssistantFromMd(request.body.rawMd);
728
887
  });
729
- server2.post("/_web/people/relationships/recompute", async (request) => {
730
- return service.startRecompute({
731
- timeRangePreset: parseContactsTimeRangePreset2(request.query.timeRange),
732
- graphScope: parsePeopleRelationshipsGraphScope(request.query.scope),
733
- query: request.query.q
734
- });
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;
735
897
  });
736
- server2.get(
737
- "/_web/people/relationships/:key/neighborhood",
738
- async (request) => {
739
- return service.getNeighborhood(request.params.key, {
740
- acceptStale: isTruthy2(request.query.acceptStale),
741
- timeRangePreset: parseContactsTimeRangePreset2(request.query.timeRange)
742
- });
743
- }
744
- );
745
- }
746
- function isTruthy2(value) {
747
- return value === "1" || value === "true" || value === "yes";
748
- }
749
- function parseContactsTimeRangePreset2(value) {
750
- return CONTACTS_TIME_RANGE_PRESETS.includes(value) ? value : "1y";
751
- }
752
- function parsePeopleRelationshipsGraphScope(value) {
753
- return value === "close" || value === "friends" ? value : "panorama";
754
898
  }
755
899
 
756
- // ../../packages/http-routes/src/routes/web/global-insight.ts
757
- function registerGlobalInsightRoutes(server2, ctx) {
758
- const service = ctx.globalInsightService ?? createGlobalInsightService({
759
- adapter: ctx.sessionAdapter,
760
- pathProvider: ctx.pathProvider,
761
- runtimeIdentity: ctx.runtimeIdentity,
762
- 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);
763
907
  });
764
- server2.addHook("onClose", async () => service.close());
765
- server2.get("/_web/global-insight/annual-summary", async (request) => {
766
- return service.getAnnualSummary({
767
- ...parseRange(request.query),
768
- acceptStale: isTruthy3(request.query.acceptStale)
769
- });
908
+ server2.get("/_web/ai/chats", async (request) => {
909
+ const { sessionId } = request.query;
910
+ if (!sessionId) return [];
911
+ return cm.getAIChats(sessionId);
770
912
  });
771
- server2.post(
772
- "/_web/global-insight/annual-summary/recompute",
773
- async (request) => service.startRecompute(parseRange(request.query))
774
- );
775
- }
776
- function parseRange(query) {
777
- const mode = query.mode === "recent" ? "recent" : "year";
778
- return {
779
- mode,
780
- year: parseInteger(query.year),
781
- days: query.days === "365" ? 365 : void 0
782
- };
783
- }
784
- function parseInteger(value) {
785
- if (!value) return void 0;
786
- const parsed = Number(value);
787
- return Number.isInteger(parsed) ? parsed : void 0;
788
- }
789
- function isTruthy3(value) {
790
- return value === "1" || value === "true" || value === "yes";
791
- }
792
-
793
- // ../../packages/http-routes/src/routes/web/preferences.ts
794
- function registerPreferencesRoutes(server2, ctx) {
795
- const prefManager = ctx.preferencesManager ?? new PreferencesManager(ctx.pathProvider.getSystemDir());
796
- server2.get("/_web/preferences", async () => {
797
- 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;
798
917
  });
799
- server2.patch("/_web/preferences", async (request) => {
800
- 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);
801
920
  });
802
- server2.get("/_web/preferences/ui-config", async () => {
803
- const config = loadConfig();
804
- return config.ui;
921
+ server2.delete("/_web/ai/chats/:id", async (request) => {
922
+ return cm.deleteAIChat(request.params.id);
805
923
  });
806
- server2.patch("/_web/preferences/ui-config", async (request) => {
807
- try {
808
- for (const [key, value] of Object.entries(request.body)) {
809
- if (value !== void 0) {
810
- writeConfigField("ui", key, value);
811
- }
812
- }
813
- return { success: true };
814
- } catch (err) {
815
- 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" });
816
943
  }
944
+ cm.deleteMessagesFrom(id, messageId);
945
+ return { success: true };
817
946
  });
818
- server2.get("/_web/preferences/locale", async () => {
819
- const config = loadConfig();
820
- 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);
821
953
  });
822
- 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
+ }
823
988
  try {
824
- writeConfigField("locale", "lang", request.body.lang);
825
- return { success: true };
826
- } catch (err) {
827
- 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 });
828
993
  }
829
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
+ });
830
1004
  }
831
1005
 
832
- // ../../packages/http-routes/src/routes/web/analytics.ts
833
- import * as fs from "fs";
834
- import * as path2 from "path";
835
-
836
- // ../../packages/http-routes/src/helpers.ts
837
- function parseTimeFilter(query) {
838
- const { startTs, endTs, memberId } = query;
839
- if (!startTs && !endTs && !memberId) return void 0;
840
- const filter = {};
841
- if (startTs) filter.startTs = parseInt(startTs, 10);
842
- if (endTs) filter.endTs = parseInt(endTs, 10);
843
- if (memberId) filter.memberId = parseInt(memberId, 10);
844
- return filter;
845
- }
846
-
847
- // ../../packages/http-routes/src/analytics-cache.ts
848
- import * as path from "path";
849
- function canonical(value) {
850
- if (value === void 0) return "null";
851
- if (value === null || typeof value !== "object") return JSON.stringify(value);
852
- if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
853
- const obj = value;
854
- const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
855
- return `{${keys.map((k) => `${JSON.stringify(k)}:${canonical(obj[k])}`).join(",")}}`;
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
+ });
856
1051
  }
857
- function buildAnalyticsCacheKey(namespace, params) {
858
- return `${namespace}:${canonical(params)}`;
1052
+
1053
+ // ../../packages/http-routes/src/routes/web/ai-llm.ts
1054
+ function hasRealApiKey(apiKey) {
1055
+ return !!apiKey && apiKey !== "sk-no-key-required";
859
1056
  }
860
- function withAnalyticsCache(ctx, sessionId, namespace, params, compute, options) {
861
- const queryCacheDir = path.join(ctx.pathProvider.getCacheDir(), "query");
862
- const dateStr = options?.dailyInvalidate ? `|date:${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}` : "";
863
- const extraStr = options?.extraVersion ? `|${options.extraVersion}` : "";
864
- const version = `${ctx.getVersion()}|${getDbFileVersion(ctx.sessionAdapter.getDbPath(sessionId))}${dateStr}${extraStr}`;
865
- const key = buildAnalyticsCacheKey(namespace, params);
866
- return getOrComputeAnalysisCache(sessionId, key, queryCacheDir, version, compute);
1057
+ function toConfigDisplay(config) {
1058
+ const { apiKey: _raw, ...rest } = config;
1059
+ return { ...rest, apiKey: "", apiKeySet: hasRealApiKey(String(_raw || "")) };
867
1060
  }
868
-
869
- // ../../packages/http-routes/src/routes/web/analytics.ts
870
- function registerAnalyticsRoutes(server2, ctx) {
871
- const { sessionAdapter: adapter } = ctx;
872
- const cached = (name, sessionId, params, compute, options) => withAnalyticsCache(ctx, sessionId, `analytics.${name}`, params, compute, options);
873
- server2.get("/_web/sessions/:id/years", async (request) => {
874
- const db = adapter.ensureReadonly(request.params.id);
875
- return getAvailableYears(db);
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();
876
1067
  });
877
- server2.get("/_web/sessions/:id/time-range", async (request) => {
878
- const db = adapter.ensureReadonly(request.params.id);
879
- 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
+ };
880
1075
  });
881
- server2.get(
882
- "/_web/sessions/:id/stats/member-activity",
883
- async (request) => {
884
- const id = request.params.id;
885
- const filter = parseTimeFilter(request.query);
886
- return cached("member-activity", id, { ...filter }, () => getMemberActivity(adapter.ensureReadonly(id), filter));
887
- }
888
- );
889
- server2.get(
890
- "/_web/sessions/:id/stats/hourly",
891
- async (request) => {
892
- const id = request.params.id;
893
- const filter = parseTimeFilter(request.query);
894
- return cached("hourly", id, { ...filter }, () => getHourlyActivity(adapter.ensureReadonly(id), filter));
895
- }
896
- );
897
- server2.get(
898
- "/_web/sessions/:id/stats/daily",
899
- async (request) => {
900
- const id = request.params.id;
901
- const filter = parseTimeFilter(request.query);
902
- return cached("daily", id, { ...filter }, () => getDailyActivity(adapter.ensureReadonly(id), filter));
903
- }
904
- );
905
- server2.get(
906
- "/_web/sessions/:id/stats/weekday",
907
- async (request) => {
908
- const id = request.params.id;
909
- const filter = parseTimeFilter(request.query);
910
- 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) };
911
1080
  }
912
- );
913
- server2.get(
914
- "/_web/sessions/:id/stats/message-types",
915
- async (request) => {
916
- const id = request.params.id;
917
- const filter = parseTimeFilter(request.query);
918
- 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" };
919
1203
  }
920
- );
921
- server2.get(
922
- "/_web/sessions/:id/analytics/relationship",
923
- async (request) => {
924
- const id = request.params.id;
925
- const filter = parseTimeFilter(request.query);
926
- 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) };
927
1211
  }
928
- );
929
- server2.get(
930
- "/_web/sessions/:id/analytics/catchphrase",
931
- async (request) => {
932
- const id = request.params.id;
933
- const filter = parseTimeFilter(request.query);
934
- return cached("catchphrase", id, { ...filter }, () => getCatchphraseAnalysis(adapter.ensureReadonly(id), filter));
1212
+ });
1213
+ }
1214
+
1215
+ // ../../packages/http-routes/src/routes/web/ai-semantic-index.ts
1216
+ import path2 from "path";
1217
+ function registerSemanticIndexRoutes(server2, ctx) {
1218
+ const service = ctx.semanticIndexService;
1219
+ if (!service) {
1220
+ if (ctx.aiDataDir) {
1221
+ const configStore = new SemanticIndexConfigStore(path2.join(ctx.aiDataDir, SEMANTIC_INDEX_CONFIG_FILE));
1222
+ const configResponse = (config) => ({
1223
+ config,
1224
+ apiKeySet: resolveSemanticIndexApiKeySet(config, ctx.resolveApiKey),
1225
+ configured: isSemanticIndexConfigured(config)
1226
+ });
1227
+ server2.get("/_web/ai/semantic-index/config", async () => ({
1228
+ ...configResponse(configStore.get()),
1229
+ modelStatus: "idle"
1230
+ }));
1231
+ server2.put(
1232
+ "/_web/ai/semantic-index/config",
1233
+ async (request) => {
1234
+ const config = persistSemanticIndexConfig(configStore, request.body.config, {
1235
+ apiKey: request.body.apiKey,
1236
+ writeAuthProfile: ctx.writeAuthProfile
1237
+ });
1238
+ return { ...configResponse(config), modelStatus: "idle" };
1239
+ }
1240
+ );
935
1241
  }
936
- );
937
- server2.get(
938
- "/_web/sessions/:id/analytics/mention",
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",
939
1257
  async (request) => {
940
- const id = request.params.id;
941
- const filter = parseTimeFilter(request.query);
942
- return cached("mention", id, { ...filter }, () => getMentionAnalysis(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
+ };
943
1265
  }
944
1266
  );
945
- server2.get(
946
- "/_web/sessions/:id/analytics/mention-graph",
947
- async (request) => {
948
- const id = request.params.id;
949
- const filter = parseTimeFilter(request.query);
950
- return cached("mention-graph", id, { ...filter }, () => getMentionGraph(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 });
951
1303
  }
952
1304
  );
953
- server2.get(
954
- "/_web/sessions/:id/analytics/laugh",
955
- async (request) => {
956
- const id = request.params.id;
957
- const filter = parseTimeFilter(request.query);
958
- return cached("laugh", id, { ...filter }, () => getLaughAnalysis(adapter.ensureReadonly(id), filter));
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);
959
1362
  }
960
- );
961
- server2.get("/_web/sessions/:id/analytics/cluster", async (request) => {
962
- const id = request.params.id;
963
- const filter = parseTimeFilter(request.query);
964
- const options = {};
965
- if (request.query.topEdges) options.topEdges = parseInt(request.query.topEdges, 10);
966
- if (request.query.lookAhead) options.lookAhead = parseInt(request.query.lookAhead, 10);
967
- if (request.query.decaySeconds) options.decaySeconds = parseInt(request.query.decaySeconds, 10);
968
- const hasOptions = Object.keys(options).length > 0;
969
- return cached(
970
- "cluster",
971
- id,
972
- { ...filter, ...options },
973
- () => getClusterGraph(adapter.ensureReadonly(id), filter, hasOptions ? options : void 0)
974
- );
1363
+ };
1364
+ }
1365
+ function registerAiSummaryRoutes(server2, ctx) {
1366
+ const deps = createSummaryDeps(ctx);
1367
+ if (!deps) return;
1368
+ const { sessionAdapter: adapter } = ctx;
1369
+ server2.post("/_web/sessions/:id/summaries/generate", async (request, reply) => {
1370
+ const { segmentId, locale, forceRegenerate, strategy } = request.body;
1371
+ const result = await summary_service_exports.generateSummary(adapter, request.params.id, segmentId, deps, {
1372
+ locale,
1373
+ forceRegenerate,
1374
+ strategy
1375
+ });
1376
+ if ("error" in result && !result.success) {
1377
+ return reply.code(400).send({ error: result.error });
1378
+ }
1379
+ return result;
975
1380
  });
976
- server2.get(
977
- "/_web/sessions/:id/analytics/language-preference",
978
- async (request) => {
979
- const id = request.params.id;
980
- const filter = parseTimeFilter(request.query);
981
- const locale = request.query.locale || "zh-CN";
982
- const zhDictPath = path2.join(ctx.pathProvider.getSystemDir(), "nlp", "zh-CN.dict");
983
- let zhDictVersion;
984
- try {
985
- const st = fs.statSync(zhDictPath);
986
- zhDictVersion = `${Math.floor(st.mtimeMs)}:${st.size}`;
987
- } catch {
988
- zhDictVersion = "-";
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) };
989
1429
  }
990
- return cached(
991
- "language-preference",
992
- id,
993
- { ...filter, locale },
994
- () => getLanguagePreferenceAnalysis(adapter.ensureReadonly(id), {
995
- locale,
996
- timeFilter: filter,
997
- nlpProvider: createJiebaNlpProvider()
998
- }),
999
- { extraVersion: `dict:${zhDictVersion}` }
1000
- );
1001
1430
  }
1002
- );
1003
- server2.get(
1004
- "/_web/sessions/:id/analytics/monthly-activity",
1005
- async (request) => {
1006
- const id = request.params.id;
1007
- const filter = parseTimeFilter(request.query);
1008
- return cached("monthly-activity", id, { ...filter }, () => getMonthlyActivity(adapter.ensureReadonly(id), filter));
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" };
1009
1441
  }
1010
- );
1011
- server2.get(
1012
- "/_web/sessions/:id/analytics/yearly-activity",
1013
- async (request) => {
1014
- const id = request.params.id;
1015
- const filter = parseTimeFilter(request.query);
1442
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
1443
+ }
1444
+ }
1445
+
1446
+ // ../../packages/http-routes/src/routes/web/ai-tools.ts
1447
+ var activeToolTests = /* @__PURE__ */ new Map();
1448
+ function registerAiToolRoutes(server2, ctx) {
1449
+ server2.get("/_web/ai/tools/full-catalog", async () => {
1450
+ return AGENT_TOOL_REGISTRY.map((tool) => ({
1451
+ name: tool.name,
1452
+ category: tool.category ?? "core",
1453
+ description: tool.description,
1454
+ parameters: tool.inputSchema ?? {}
1455
+ }));
1456
+ });
1457
+ server2.post("/_web/ai/tools/execute", async (request, reply) => {
1458
+ const { testId, toolName, params, sessionId } = request.body;
1459
+ const entry = AGENT_TOOL_REGISTRY.find((t) => t.name === toolName);
1460
+ if (!entry) {
1461
+ return reply.code(404).send({ success: false, error: `Tool not found: ${toolName}` });
1462
+ }
1463
+ const abortController = new AbortController();
1464
+ activeToolTests.set(testId, abortController);
1465
+ try {
1466
+ if (ctx.executeAiTool) {
1467
+ return await ctx.executeAiTool({ testId, toolName, params, sessionId, abortSignal: abortController.signal });
1468
+ }
1469
+ const db = ctx.dbManager.open(sessionId);
1470
+ if (!db) {
1471
+ return reply.code(404).send({ success: false, error: `Session not found: ${sessionId}` });
1472
+ }
1473
+ const result = await executeRegistryTool(
1474
+ { testId, toolName, params, sessionId, abortSignal: abortController.signal },
1475
+ {
1476
+ db,
1477
+ dataProvider: new CoreDataProvider(db)
1478
+ }
1479
+ );
1480
+ if (!result.success && result.error !== "cancelled") {
1481
+ appLogger.error("ai-tools", `Failed to execute tool ${toolName}`, { error: result.error });
1482
+ }
1483
+ return result;
1484
+ } catch (error) {
1485
+ if (abortController.signal.aborted) {
1486
+ return { success: false, error: "cancelled" };
1487
+ }
1488
+ appLogger.error("ai-tools", `Failed to execute tool ${toolName}`, error);
1489
+ return { success: false, error: String(error) };
1490
+ } finally {
1491
+ activeToolTests.delete(testId);
1492
+ }
1493
+ });
1494
+ server2.post("/_web/ai/tools/cancel", async (request) => {
1495
+ const { testId } = request.body;
1496
+ const controller = activeToolTests.get(testId);
1497
+ if (controller) {
1498
+ controller.abort();
1499
+ activeToolTests.delete(testId);
1500
+ return { success: true };
1501
+ }
1502
+ return { success: false };
1503
+ });
1504
+ }
1505
+
1506
+ // ../../packages/http-routes/src/routes/web/register-ai.ts
1507
+ var REQUIRED_AI_DEPENDENCIES = [
1508
+ "aiDataDir",
1509
+ "aiChatManager",
1510
+ "assistantManager",
1511
+ "skillManagerCore",
1512
+ "llmConfigStore",
1513
+ "customProviderStore",
1514
+ "customModelStore",
1515
+ "runAgentStream"
1516
+ ];
1517
+ function assertRequiredAiContext(ctx) {
1518
+ const missing = REQUIRED_AI_DEPENDENCIES.filter((key) => !ctx[key]);
1519
+ if (missing.length > 0) {
1520
+ throw new Error(`[http-routes] requireAi is set but missing AI dependencies: ${missing.join(", ")}`);
1521
+ }
1522
+ }
1523
+ function registerAiRoutes(server2, ctx, options) {
1524
+ if (options?.requireAi) assertRequiredAiContext(ctx);
1525
+ registerAiAssistantRoutes(server2, ctx);
1526
+ registerAiSkillRoutes(server2, ctx);
1527
+ registerAiLlmRoutes(server2, ctx);
1528
+ registerAiLlmStreamRoutes(server2, ctx);
1529
+ registerAiAgentStreamRoutes(server2, ctx);
1530
+ registerAiToolRoutes(server2, ctx);
1531
+ registerAiChatRoutes(server2, ctx);
1532
+ registerAiSummaryRoutes(server2, ctx);
1533
+ registerAiLogRoutes(server2, ctx);
1534
+ registerSemanticIndexRoutes(server2, ctx);
1535
+ }
1536
+
1537
+ // ../../packages/http-routes/src/routes/web/analytics.ts
1538
+ import * as fs2 from "fs";
1539
+ import * as path4 from "path";
1540
+
1541
+ // ../../packages/http-routes/src/routes/web/time-filter.ts
1542
+ function parseTimeFilter(query) {
1543
+ const { startTs, endTs, memberId } = query;
1544
+ if (!startTs && !endTs && !memberId) return void 0;
1545
+ const filter = {};
1546
+ if (startTs) filter.startTs = parseInt(startTs, 10);
1547
+ if (endTs) filter.endTs = parseInt(endTs, 10);
1548
+ if (memberId) filter.memberId = parseInt(memberId, 10);
1549
+ return filter;
1550
+ }
1551
+
1552
+ // ../../packages/http-routes/src/routes/web/analytics-cache.ts
1553
+ import * as path3 from "path";
1554
+ function canonical(value) {
1555
+ if (value === void 0) return "null";
1556
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
1557
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
1558
+ const obj = value;
1559
+ const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
1560
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonical(obj[k])}`).join(",")}}`;
1561
+ }
1562
+ function buildAnalyticsCacheKey(namespace, params) {
1563
+ return `${namespace}:${canonical(params)}`;
1564
+ }
1565
+ function withAnalyticsCache(ctx, sessionId, namespace, params, compute, options) {
1566
+ const queryCacheDir = path3.join(ctx.pathProvider.getCacheDir(), "query");
1567
+ const dateStr = options?.dailyInvalidate ? `|date:${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}` : "";
1568
+ const extraStr = options?.extraVersion ? `|${options.extraVersion}` : "";
1569
+ const version = `${ctx.getVersion()}|${getDbFileVersion(ctx.sessionAdapter.getDbPath(sessionId))}${dateStr}${extraStr}`;
1570
+ const key = buildAnalyticsCacheKey(namespace, params);
1571
+ return getOrComputeAnalysisCache(sessionId, key, queryCacheDir, version, compute);
1572
+ }
1573
+
1574
+ // ../../packages/http-routes/src/routes/web/analytics.ts
1575
+ function registerAnalyticsRoutes(server2, ctx) {
1576
+ const { sessionAdapter: adapter } = ctx;
1577
+ const cached = (name, sessionId, params, compute, options) => withAnalyticsCache(ctx, sessionId, `analytics.${name}`, params, compute, options);
1578
+ server2.get("/_web/sessions/:id/years", async (request) => {
1579
+ const db = adapter.ensureReadonly(request.params.id);
1580
+ return getAvailableYears(db);
1581
+ });
1582
+ server2.get("/_web/sessions/:id/time-range", async (request) => {
1583
+ const db = adapter.ensureReadonly(request.params.id);
1584
+ return getTimeRange(db);
1585
+ });
1586
+ server2.get(
1587
+ "/_web/sessions/:id/stats/member-activity",
1588
+ async (request) => {
1589
+ const id = request.params.id;
1590
+ const filter = parseTimeFilter(request.query);
1591
+ return cached("member-activity", id, { ...filter }, () => getMemberActivity(adapter.ensureReadonly(id), filter));
1592
+ }
1593
+ );
1594
+ server2.get(
1595
+ "/_web/sessions/:id/stats/hourly",
1596
+ async (request) => {
1597
+ const id = request.params.id;
1598
+ const filter = parseTimeFilter(request.query);
1599
+ return cached("hourly", id, { ...filter }, () => getHourlyActivity(adapter.ensureReadonly(id), filter));
1600
+ }
1601
+ );
1602
+ server2.get(
1603
+ "/_web/sessions/:id/stats/daily",
1604
+ async (request) => {
1605
+ const id = request.params.id;
1606
+ const filter = parseTimeFilter(request.query);
1607
+ return cached("daily", id, { ...filter }, () => getDailyActivity(adapter.ensureReadonly(id), filter));
1608
+ }
1609
+ );
1610
+ server2.get(
1611
+ "/_web/sessions/:id/stats/weekday",
1612
+ async (request) => {
1613
+ const id = request.params.id;
1614
+ const filter = parseTimeFilter(request.query);
1615
+ return cached("weekday", id, { ...filter }, () => getWeekdayActivity(adapter.ensureReadonly(id), filter));
1616
+ }
1617
+ );
1618
+ server2.get(
1619
+ "/_web/sessions/:id/stats/message-types",
1620
+ async (request) => {
1621
+ const id = request.params.id;
1622
+ const filter = parseTimeFilter(request.query);
1623
+ return cached("message-types", id, { ...filter }, () => getMessageTypeStats(adapter.ensureReadonly(id), filter));
1624
+ }
1625
+ );
1626
+ server2.get(
1627
+ "/_web/sessions/:id/analytics/relationship",
1628
+ async (request) => {
1629
+ const id = request.params.id;
1630
+ const filter = parseTimeFilter(request.query);
1631
+ return cached("relationship", id, { ...filter }, () => getRelationshipStats(adapter.ensureReadonly(id), filter));
1632
+ }
1633
+ );
1634
+ server2.get(
1635
+ "/_web/sessions/:id/analytics/catchphrase",
1636
+ async (request) => {
1637
+ const id = request.params.id;
1638
+ const filter = parseTimeFilter(request.query);
1639
+ return cached("catchphrase", id, { ...filter }, () => getCatchphraseAnalysis(adapter.ensureReadonly(id), filter));
1640
+ }
1641
+ );
1642
+ server2.get(
1643
+ "/_web/sessions/:id/analytics/mention",
1644
+ async (request) => {
1645
+ const id = request.params.id;
1646
+ const filter = parseTimeFilter(request.query);
1647
+ return cached("mention", id, { ...filter }, () => getMentionAnalysis(adapter.ensureReadonly(id), filter));
1648
+ }
1649
+ );
1650
+ server2.get(
1651
+ "/_web/sessions/:id/analytics/mention-graph",
1652
+ async (request) => {
1653
+ const id = request.params.id;
1654
+ const filter = parseTimeFilter(request.query);
1655
+ return cached("mention-graph", id, { ...filter }, () => getMentionGraph(adapter.ensureReadonly(id), filter));
1656
+ }
1657
+ );
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));
1664
+ }
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
+ );
1680
+ });
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
+ );
1706
+ }
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));
1714
+ }
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);
1016
1721
  return cached("yearly-activity", id, { ...filter }, () => getYearlyActivity(adapter.ensureReadonly(id), filter));
1017
1722
  }
1018
1723
  );
@@ -1099,889 +1804,495 @@ function registerAnalyticsRoutes(server2, ctx) {
1099
1804
  "/_web/sessions/:id/analytics/check-in",
1100
1805
  async (request) => {
1101
1806
  const id = request.params.id;
1102
- const filter = parseTimeFilter(request.query);
1103
- return cached("check-in", id, { ...filter }, () => getCheckInAnalysis(adapter.ensureReadonly(id), filter));
1104
- }
1105
- );
1106
- server2.get(
1107
- "/_web/sessions/:id/analytics/meme-battle",
1108
- async (request) => {
1109
- const id = request.params.id;
1110
- const filter = parseTimeFilter(request.query);
1111
- return cached("meme-battle", id, { ...filter }, () => getMemeBattleAnalysis(adapter.ensureReadonly(id), filter));
1112
- }
1113
- );
1114
- server2.get(
1115
- "/_web/sessions/:id/analytics/night-owl",
1116
- async (request) => {
1117
- const id = request.params.id;
1118
- const filter = parseTimeFilter(request.query);
1119
- return cached("night-owl", id, { ...filter }, () => getNightOwlAnalysis(adapter.ensureReadonly(id), filter), {
1120
- dailyInvalidate: true
1121
- });
1122
- }
1123
- );
1124
- server2.get(
1125
- "/_web/sessions/:id/analytics/repeat",
1126
- async (request) => {
1127
- const id = request.params.id;
1128
- const filter = parseTimeFilter(request.query);
1129
- return cached("repeat", id, { ...filter }, () => getRepeatAnalysis(adapter.ensureReadonly(id), filter));
1130
- }
1131
- );
1132
- }
1133
-
1134
- // ../../packages/http-routes/src/routes/web/sql.ts
1135
- function registerSqlRoutes(server2, ctx) {
1136
- const { sessionAdapter: adapter } = ctx;
1137
- server2.post("/_web/sessions/:id/sql", async (request, reply) => {
1138
- const db = adapter.ensureReadonly(request.params.id);
1139
- const { sql } = request.body || {};
1140
- if (!sql || typeof sql !== "string") {
1141
- return reply.code(400).send({ error: "Missing sql parameter" });
1142
- }
1143
- try {
1144
- const result = executeSql(db, sql, { columnar: true, timing: true, maxRows: 0 });
1145
- return {
1146
- columns: result.columns,
1147
- rows: result.rows,
1148
- rowCount: result.rowCount,
1149
- duration: result.duration ?? 0,
1150
- limited: result.truncated
1151
- };
1152
- } catch (err) {
1153
- const message = err instanceof Error ? err.message : "SQL execution error";
1154
- return reply.code(400).send({ error: message });
1155
- }
1156
- });
1157
- server2.get("/_web/sessions/:id/schema", async (request) => {
1158
- const db = adapter.ensureReadonly(request.params.id);
1159
- return getSchemaDetailed(db);
1160
- });
1161
- server2.post("/_web/sessions/:id/query", async (request) => {
1162
- const db = adapter.ensureReadonly(request.params.id);
1163
- const { sql, params = [] } = request.body;
1164
- if (!sql || typeof sql !== "string") {
1165
- throw Object.assign(new Error('Missing or invalid "sql" field'), { statusCode: 400 });
1166
- }
1167
- const stmt = db.prepare(sql.trim());
1168
- if (!stmt.readonly) {
1169
- throw Object.assign(new Error("Only READ-ONLY statements are allowed"), { statusCode: 403 });
1170
- }
1171
- if (Array.isArray(params)) {
1172
- return stmt.all(...params);
1173
- }
1174
- return stmt.all(params);
1175
- });
1176
- }
1177
-
1178
- // ../../packages/http-routes/src/routes/web/session-index.ts
1179
- function registerSessionIndexRoutes(server2, ctx) {
1180
- const { sessionAdapter: adapter } = ctx;
1181
- server2.get("/_web/sessions/index-stats", async () => {
1182
- return session_index_service_exports.getAllIndexStats(adapter);
1183
- });
1184
- server2.post("/_web/sessions/:id/generate-index", async (request) => {
1185
- const gapThreshold = request.body?.gapThreshold ?? 1800;
1186
- const sessionCount = session_index_service_exports.generateIndex(adapter, request.params.id, gapThreshold);
1187
- return { sessionCount };
1188
- });
1189
- server2.post("/_web/sessions/:id/generate-incremental-index", async (request) => {
1190
- const gapThreshold = request.body?.gapThreshold ?? 1800;
1191
- const sessionCount = session_index_service_exports.generateIncrementalIndex(adapter, request.params.id, gapThreshold);
1192
- return { sessionCount };
1193
- });
1194
- server2.post("/_web/sessions/:id/clear-index", async (request) => {
1195
- session_index_service_exports.clearIndex(adapter, request.params.id);
1196
- return { success: true };
1197
- });
1198
- server2.get("/_web/sessions/:id/search/fts", async (request, reply) => {
1199
- if (!session_index_service_exports.getFtsStatus(adapter, request.params.id)) {
1200
- return reply.code(400).send({ error: "FTS index not built for this session" });
1201
- }
1202
- const keywords = request.query.keywords.split(/\s+/).filter(Boolean);
1203
- if (keywords.length === 0) return { rowids: [], total: 0 };
1204
- const limit = parseInt(request.query.limit || "100", 10);
1205
- const offset = parseInt(request.query.offset || "0", 10);
1206
- return session_index_service_exports.searchFts(adapter, request.params.id, keywords, limit, offset);
1207
- });
1208
- server2.get("/_web/sessions/:id/fts/status", async (request) => {
1209
- return { hasFtsIndex: session_index_service_exports.getFtsStatus(adapter, request.params.id) };
1210
- });
1211
- server2.post("/_web/sessions/:id/fts/rebuild", async (request) => {
1212
- const result = session_index_service_exports.rebuildFts(adapter, request.params.id);
1213
- return { success: true, indexed: result.indexed };
1214
- });
1215
- }
1216
-
1217
- // ../../packages/http-routes/src/routes/web/export.ts
1218
- function registerExportRoutes(server2, ctx) {
1219
- const { sessionAdapter: adapter } = ctx;
1220
- server2.post("/_web/sessions/:id/export", async (request, reply) => {
1221
- const { id } = request.params;
1222
- const body = request.body;
1223
- const sessionName = body?.sessionName || id;
1224
- const format = body?.format || "txt";
1225
- const result = export_service_exports.exportFormatted(adapter, {
1226
- sessionId: id,
1227
- sessionName,
1228
- format,
1229
- timeFilter: body.timeFilter
1230
- });
1231
- if (!result.success) {
1232
- return reply.code(result.totalMessages === 0 ? 404 : 500).send({ error: result.error });
1233
- }
1234
- reply.header("Content-Type", result.mimeType);
1235
- reply.header("Content-Disposition", `attachment; filename="${encodeURIComponent(result.filename)}"`);
1236
- return reply.send(result.content);
1237
- });
1238
- }
1239
-
1240
- // ../../packages/http-routes/src/routes/web/nlp.ts
1241
- import * as fs2 from "fs";
1242
- import * as path3 from "path";
1243
- function registerNlpRoutes(server2, ctx) {
1244
- const nlpDir = path3.join(ctx.pathProvider.getSystemDir(), "nlp");
1245
- initNlpDir(nlpDir);
1246
- ensureDefaultDict(nlpDir).catch((err) => console.warn("[NLP] Auto-download zh-CN dict failed:", err));
1247
- server2.get("/_web/nlp/pos-tags", async () => {
1248
- return POS_TAG_DEFINITIONS;
1249
- });
1250
- server2.get("/_web/nlp/dicts", async () => {
1251
- return getDictList(nlpDir);
1252
- });
1253
- server2.get("/_web/nlp/dicts/:id/status", async (request) => {
1254
- return isDictDownloaded(nlpDir, request.params.id);
1255
- });
1256
- server2.post("/_web/nlp/dicts/:id/download", async (request) => {
1257
- return downloadDict(nlpDir, request.params.id);
1258
- });
1259
- server2.delete("/_web/nlp/dicts/:id", async (request) => {
1260
- return deleteDict(nlpDir, request.params.id);
1261
- });
1262
- server2.post("/_web/nlp/word-frequency", async (request) => {
1263
- const params = request.body;
1264
- const db = ctx.dbManager.open(params.sessionId);
1265
- if (!db) {
1266
- throw Object.assign(new Error(`Session not found: ${params.sessionId}`), { statusCode: 404 });
1267
- }
1268
- const { sessionId, ...keyParams } = params;
1269
- const effectiveDictId = !params.dictType || params.dictType === "default" ? "zh-CN" : String(params.dictType);
1270
- const dictFilePath = path3.join(nlpDir, `${effectiveDictId}.dict`);
1271
- let dictVersion;
1272
- try {
1273
- const st = fs2.statSync(dictFilePath);
1274
- dictVersion = `${Math.floor(st.mtimeMs)}:${st.size}`;
1275
- } catch {
1276
- dictVersion = "-";
1277
- }
1278
- return withAnalyticsCache(ctx, sessionId, "nlp.word-frequency", keyParams, () => computeWordFrequency(db, params), {
1279
- extraVersion: `dict:${dictVersion}`
1280
- });
1281
- });
1282
- server2.post(
1283
- "/_web/nlp/segment",
1284
- async (request) => {
1285
- const { text, locale, minLength } = request.body;
1286
- return segmentText(text, locale, minLength);
1287
- }
1288
- );
1289
- }
1290
-
1291
- // ../../packages/http-routes/src/routes/web/ai-assistants.ts
1292
- function registerAiAssistantRoutes(server2, ctx) {
1293
- server2.get("/_web/ai/tools/catalog", async () => {
1294
- return BUILTIN_TOOL_CATALOG;
1295
- });
1296
- const mgr = ctx.assistantManager;
1297
- if (!mgr) return;
1298
- server2.get("/_web/ai/assistants", async () => {
1299
- return mgr.getAllAssistants();
1300
- });
1301
- server2.get("/_web/ai/assistants/:id", async (request, reply) => {
1302
- const config = mgr.getAssistantConfig(request.params.id);
1303
- if (!config) return reply.code(404).send({ error: "Not found" });
1304
- return config;
1305
- });
1306
- server2.post("/_web/ai/assistants", async (request) => {
1307
- return mgr.createAssistant(request.body);
1308
- });
1309
- server2.put("/_web/ai/assistants/:id", async (request, reply) => {
1310
- const result = mgr.updateAssistant(request.params.id, request.body);
1311
- if (!result.success) return reply.code(404).send(result);
1312
- return result;
1313
- });
1314
- server2.delete("/_web/ai/assistants/:id", async (request, reply) => {
1315
- const result = mgr.deleteAssistant(request.params.id);
1316
- if (!result.success) return reply.code(400).send(result);
1317
- return result;
1318
- });
1319
- server2.post("/_web/ai/assistants/:id/reset", async (request, reply) => {
1320
- const result = mgr.resetAssistant(request.params.id);
1321
- if (!result.success) return reply.code(400).send(result);
1322
- return result;
1323
- });
1324
- server2.post("/_web/ai/assistants/import", async (request) => {
1325
- return mgr.importAssistantFromMd(request.body.rawMd);
1326
- });
1327
- server2.post("/_web/ai/assistants/import-builtin", async (request, reply) => {
1328
- const result = mgr.importAssistant(request.body.builtinId);
1329
- if (!result.success) return reply.code(400).send(result);
1330
- return result;
1331
- });
1332
- server2.post("/_web/ai/assistants/:id/reimport", async (request, reply) => {
1333
- const result = mgr.reimportAssistant(request.params.id);
1334
- if (!result.success) return reply.code(400).send(result);
1335
- return result;
1336
- });
1337
- }
1338
-
1339
- // ../../packages/http-routes/src/routes/web/ai-skills.ts
1340
- function registerAiSkillRoutes(server2, ctx) {
1341
- const mgr = ctx.skillManagerCore;
1342
- if (!mgr) return;
1343
- server2.get("/_web/ai/skills", async () => {
1344
- return mgr.getAllSkills();
1345
- });
1346
- server2.get("/_web/ai/skills/:id", async (request, reply) => {
1347
- const config = mgr.getSkillConfig(request.params.id);
1348
- if (!config) return reply.code(404).send({ error: "Not found" });
1349
- return config;
1350
- });
1351
- server2.post("/_web/ai/skills", async (request) => {
1352
- return mgr.createSkill(request.body.rawMd);
1353
- });
1354
- server2.put("/_web/ai/skills/:id", async (request, reply) => {
1355
- const result = mgr.updateSkill(request.params.id, request.body.rawMd);
1356
- if (!result.success) return reply.code(404).send(result);
1357
- return result;
1358
- });
1359
- server2.delete("/_web/ai/skills/:id", async (request, reply) => {
1360
- const result = mgr.deleteSkill(request.params.id);
1361
- if (!result.success) return reply.code(400).send(result);
1362
- return result;
1363
- });
1364
- server2.post("/_web/ai/skills/import", async (request) => {
1365
- return mgr.importSkillFromMd(request.body.rawMd);
1366
- });
1367
- server2.get("/_web/ai/skills/builtin-catalog", async () => {
1368
- return mgr.getBuiltinCatalog();
1369
- });
1370
- server2.post("/_web/ai/skills/import-builtin", async (request, reply) => {
1371
- const result = mgr.importSkill(request.body.builtinId);
1372
- if (!result.success) return reply.code(400).send(result);
1373
- return result;
1374
- });
1375
- server2.post("/_web/ai/skills/:id/reimport", async (request, reply) => {
1376
- const result = mgr.reimportSkill(request.params.id);
1377
- if (!result.success) return reply.code(400).send(result);
1378
- return result;
1379
- });
1380
- }
1381
-
1382
- // ../../packages/http-routes/src/routes/web/ai-llm.ts
1383
- function hasRealApiKey(apiKey) {
1384
- return !!apiKey && apiKey !== "sk-no-key-required";
1385
- }
1386
- function toConfigDisplay(config) {
1387
- const { apiKey: _raw, ...rest } = config;
1388
- return { ...rest, apiKey: "", apiKeySet: hasRealApiKey(String(_raw || "")) };
1389
- }
1390
- function registerAiLlmRoutes(server2, ctx) {
1391
- const store = ctx.llmConfigStore;
1392
- const aiDataDir = ctx.aiDataDir;
1393
- if (!store || !aiDataDir) return;
1394
- server2.get("/_web/ai/llm/has-config", async () => {
1395
- return store.hasActiveConfig();
1396
- });
1397
- server2.get("/_web/ai/llm/configs", async () => {
1398
- const data = store.loadStore();
1399
- return {
1400
- configs: data.configs.map((c) => toConfigDisplay(c)),
1401
- defaultAssistant: data.defaultAssistant,
1402
- fastModel: data.fastModel
1403
- };
1404
- });
1405
- server2.post("/_web/ai/llm/configs", async (request) => {
1406
- const result = store.addConfig(request.body);
1407
- if (result.success && result.config) {
1408
- return { ...result, config: toConfigDisplay(result.config) };
1409
- }
1410
- return result;
1411
- });
1412
- server2.put("/_web/ai/llm/configs/:id", async (request) => {
1413
- return store.updateConfig(request.params.id, request.body);
1414
- });
1415
- server2.delete("/_web/ai/llm/configs/:id", async (request) => {
1416
- return store.deleteConfig(request.params.id);
1417
- });
1418
- server2.get("/_web/ai/llm/default-assistant-slot", async () => {
1419
- return store.getDefaultAssistantSlot();
1420
- });
1421
- server2.put("/_web/ai/llm/default-assistant-slot", async (request) => {
1422
- return store.setDefaultAssistantModel(request.body.configId, request.body.modelId);
1423
- });
1424
- server2.get("/_web/ai/llm/fast-model-slot", async () => {
1425
- return store.getFastModelSlot();
1426
- });
1427
- server2.put("/_web/ai/llm/fast-model-slot", async (request) => {
1428
- return store.setFastModel(request.body);
1429
- });
1430
- server2.get("/_web/ai/llm/providers", async () => {
1431
- return BUILTIN_PROVIDERS.map((p) => {
1432
- const models = getBuiltinModelsByProvider(p.id);
1433
- return {
1434
- id: p.id,
1435
- name: p.name,
1436
- defaultBaseUrl: p.defaultBaseUrl,
1437
- models: models.filter((m) => !m.capabilities.includes("embedding") && !m.capabilities.includes("ranking")).map((m) => ({ id: m.id, name: m.name, description: m.description }))
1438
- };
1439
- });
1440
- });
1441
- server2.get("/_web/ai/llm/provider-registry", async () => {
1442
- const custom = ctx.customProviderStore?.getAll() ?? [];
1443
- return [...BUILTIN_PROVIDERS, ...custom];
1444
- });
1445
- server2.get("/_web/ai/llm/model-catalog", async () => {
1446
- const custom = ctx.customModelStore?.getAll() ?? [];
1447
- return [...BUILTIN_MODELS, ...custom];
1448
- });
1449
- server2.post("/_web/ai/llm/custom-providers", async (request, reply) => {
1450
- if (!ctx.customProviderStore)
1451
- return reply.code(501).send({ success: false, error: "Custom providers not available" });
1452
- return ctx.customProviderStore.add(request.body);
1453
- });
1454
- server2.put("/_web/ai/llm/custom-providers/:id", async (request, reply) => {
1455
- if (!ctx.customProviderStore)
1456
- return reply.code(501).send({ success: false, error: "Custom providers not available" });
1457
- const result = ctx.customProviderStore.update(request.params.id, request.body);
1458
- if (!result.success) return reply.code(404).send(result);
1459
- return result;
1460
- });
1461
- server2.delete("/_web/ai/llm/custom-providers/:id", async (request, reply) => {
1462
- if (!ctx.customProviderStore)
1463
- return reply.code(501).send({ success: false, error: "Custom providers not available" });
1464
- const result = ctx.customProviderStore.delete(request.params.id);
1465
- if (!result.success) return reply.code(404).send(result);
1466
- return result;
1467
- });
1468
- server2.post("/_web/ai/llm/custom-models", async (request, reply) => {
1469
- if (!ctx.customModelStore) return reply.code(501).send({ success: false, error: "Custom models not available" });
1470
- const result = ctx.customModelStore.add(request.body);
1471
- if (!result.success) return reply.code(409).send(result);
1472
- return result;
1473
- });
1474
- server2.put("/_web/ai/llm/custom-models/:providerId/:modelId", async (request, reply) => {
1475
- if (!ctx.customModelStore) return reply.code(501).send({ success: false, error: "Custom models not available" });
1476
- const result = ctx.customModelStore.update(request.params.providerId, request.params.modelId, request.body);
1477
- if (!result.success) return reply.code(404).send(result);
1478
- return result;
1479
- });
1480
- server2.delete("/_web/ai/llm/custom-models/:providerId/:modelId", async (request, reply) => {
1481
- if (!ctx.customModelStore) return reply.code(501).send({ success: false, error: "Custom models not available" });
1482
- const result = ctx.customModelStore.delete(request.params.providerId, request.params.modelId);
1483
- if (!result.success) return reply.code(404).send(result);
1484
- return result;
1485
- });
1486
- server2.post("/_web/ai/llm/validate-key", async (request) => {
1487
- const { provider, apiKey, baseUrl, model, apiFormat, configId } = request.body;
1488
- const resolvedKey = apiKey?.trim() ? apiKey : configId ? store.getConfigById(configId)?.apiKey || "" : "";
1489
- return validateApiKey(provider, resolvedKey, baseUrl, model, apiFormat);
1490
- });
1491
- server2.post("/_web/ai/llm/remote-models", async (request) => {
1492
- const { provider, apiKey, baseUrl, apiFormat, configId } = request.body;
1493
- const resolvedKey = apiKey?.trim() ? apiKey : configId ? store.getConfigById(configId)?.apiKey || "" : "";
1494
- return fetchRemoteModels(provider, resolvedKey, baseUrl, apiFormat);
1495
- });
1496
- server2.get("/_web/ai/desensitize-rules/defaults", async (request) => {
1497
- return getDefaultRulesForLocale(request.query.locale ?? "zh-CN");
1498
- });
1499
- server2.post("/_web/ai/desensitize-rules/merge", async (request) => {
1500
- return mergeRulesForLocale(request.body.existingRules, request.body.locale, request.body.overrides ?? {});
1501
- });
1502
- }
1503
-
1504
- // ../../packages/http-routes/src/routes/web/ai-llm-stream.ts
1505
- function registerAiLlmStreamRoutes(server2, ctx) {
1506
- const store = ctx.llmConfigStore;
1507
- if (!store) return;
1508
- server2.post("/_web/ai/llm/chat-stream", async (request, reply) => {
1509
- const { messages, options } = request.body;
1510
- const llmConfig = store.getDefaultAssistantConfig();
1511
- if (!llmConfig) {
1512
- return reply.code(400).send({ success: false, error: "LLM service not configured" });
1807
+ const filter = parseTimeFilter(request.query);
1808
+ return cached("check-in", id, { ...filter }, () => getCheckInAnalysis(adapter.ensureReadonly(id), filter));
1513
1809
  }
1514
- const piModel = buildPiModel(llmConfig);
1515
- const abortController = new AbortController();
1516
- reply.raw.writeHead(200, {
1517
- "Content-Type": "text/event-stream",
1518
- "Cache-Control": "no-cache",
1519
- Connection: "keep-alive"
1520
- });
1521
- reply.raw.on("close", () => {
1522
- if (!abortController.signal.aborted) abortController.abort();
1523
- });
1524
- const sendChunk = (data) => {
1525
- if (reply.raw.writableEnded || reply.raw.destroyed) return;
1526
- reply.raw.write(`event: chunk
1527
- data: ${JSON.stringify(data)}
1528
-
1529
- `);
1530
- };
1531
- try {
1532
- await runSimpleLlmStream({
1533
- messages,
1534
- apiKey: llmConfig.apiKey,
1535
- piModel,
1536
- temperature: options?.temperature,
1537
- maxTokens: options?.maxTokens,
1538
- onChunk: sendChunk,
1539
- abortSignal: abortController.signal
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
1540
1826
  });
1541
- } catch (error) {
1542
- if (abortController.signal.aborted) return;
1543
- const msg = error instanceof Error ? error.message : String(error);
1544
- sendChunk({ content: "", isFinished: true, finishReason: "error", error: msg });
1545
- } finally {
1546
- if (!reply.raw.writableEnded && !reply.raw.destroyed) reply.raw.end();
1547
1827
  }
1548
- });
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
+ );
1549
1837
  }
1550
1838
 
1551
- // ../../packages/http-routes/src/routes/web/ai-agent-stream.ts
1552
- var activeAgentAborts = /* @__PURE__ */ new Map();
1553
- function registerAiAgentStreamRoutes(server2, ctx) {
1554
- if (!ctx.runAgentStream) return;
1555
- const runAgentStream = ctx.runAgentStream;
1556
- server2.post("/_web/ai/agent/stream", async (request, reply) => {
1557
- const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1558
- const abortController = new AbortController();
1559
- activeAgentAborts.set(requestId, abortController);
1560
- reply.raw.writeHead(200, {
1561
- "Content-Type": "text/event-stream",
1562
- "Cache-Control": "no-cache",
1563
- Connection: "keep-alive",
1564
- "X-Request-Id": requestId
1565
- });
1566
- let emittedDone = false;
1567
- const safeSendSSE = (event, data) => {
1568
- if (reply.raw.writableEnded || reply.raw.destroyed) return;
1569
- reply.raw.write(`event: ${event}
1570
- data: ${JSON.stringify(data)}
1571
-
1572
- `);
1573
- };
1574
- const safeEnd = () => {
1575
- if (reply.raw.writableEnded || reply.raw.destroyed) return;
1576
- reply.raw.end();
1577
- };
1578
- safeSendSSE("meta", { requestId });
1579
- reply.raw.on("close", () => {
1580
- if (!abortController.signal.aborted) {
1581
- 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;
1582
1855
  }
1583
- activeAgentAborts.delete(requestId);
1584
- });
1585
- try {
1586
- await runAgentStream(
1587
- request.body,
1588
- (chunk) => {
1589
- if (chunk.type === "done") emittedDone = true;
1590
- safeSendSSE(chunk.type, chunk);
1591
- },
1592
- abortController.signal
1593
- );
1594
- } catch (error) {
1595
- const msg = error instanceof Error ? error.message : String(error);
1596
- safeSendSSE("error", { type: "error", error: { name: "ServerError", message: msg } });
1597
- } finally {
1598
- if (!emittedDone) safeSendSSE("done", { type: "done", isFinished: true });
1599
- activeAgentAborts.delete(requestId);
1600
- safeEnd();
1601
- }
1602
- });
1603
- server2.post("/_web/ai/agent/abort", async (request) => {
1604
- const { requestId } = request.body;
1605
- const controller = activeAgentAborts.get(requestId);
1606
- if (controller) {
1607
- controller.abort();
1608
- activeAgentAborts.delete(requestId);
1609
- return { success: true };
1610
1856
  }
1611
- return { success: false };
1612
- });
1857
+ } catch {
1858
+ }
1859
+ return totalSize;
1613
1860
  }
1614
-
1615
- // ../../packages/http-routes/src/routes/web/ai-tools.ts
1616
- var MAX_RESULT_CHARS = 5e5;
1617
- var activeToolTests = /* @__PURE__ */ new Map();
1618
- function registerAiToolRoutes(server2, ctx) {
1619
- server2.get("/_web/ai/tools/full-catalog", async () => {
1620
- return AGENT_TOOL_REGISTRY.map((tool) => ({
1621
- name: tool.name,
1622
- category: tool.category ?? "core",
1623
- description: tool.description,
1624
- parameters: tool.inputSchema ?? {}
1625
- }));
1626
- });
1627
- server2.post("/_web/ai/tools/execute", async (request, reply) => {
1628
- const { testId, toolName, params, sessionId } = request.body;
1629
- const entry = AGENT_TOOL_REGISTRY.find((t) => t.name === toolName);
1630
- if (!entry) {
1631
- return reply.code(404).send({ success: false, error: `Tool not found: ${toolName}` });
1632
- }
1633
- const abortController = new AbortController();
1634
- activeToolTests.set(testId, abortController);
1635
- try {
1636
- if (ctx.executeAiTool) {
1637
- return await ctx.executeAiTool({ testId, toolName, params, sessionId, abortSignal: abortController.signal });
1638
- }
1639
- const db = ctx.dbManager.open(sessionId);
1640
- if (!db) {
1641
- return reply.code(404).send({ success: false, error: `Session not found: ${sessionId}` });
1642
- }
1643
- const execCtx = {
1644
- sessionId,
1645
- db,
1646
- dataProvider: new CoreDataProvider(db),
1647
- abortSignal: abortController.signal
1648
- };
1649
- const startTime = Date.now();
1650
- const result = await entry.handler(params, execCtx);
1651
- const elapsed = Date.now() - startTime;
1652
- if (abortController.signal.aborted) {
1653
- return { success: false, error: "cancelled" };
1861
+ async function getFileCount(dirPath) {
1862
+ let count = 0;
1863
+ try {
1864
+ if (!fs3.existsSync(dirPath)) return 0;
1865
+ const files = await fsp.readdir(dirPath, { withFileTypes: true });
1866
+ for (const file of files) {
1867
+ const filePath = path5.join(dirPath, file.name);
1868
+ if (file.isDirectory()) {
1869
+ count += await getFileCount(filePath);
1870
+ } else {
1871
+ count++;
1654
1872
  }
1655
- let details = result.data ?? void 0;
1656
- let truncated = false;
1657
- if (details) {
1658
- stripAvatarFields(details);
1659
- const raw = JSON.stringify(details);
1660
- if (raw.length > MAX_RESULT_CHARS) {
1661
- truncated = true;
1662
- details = { _truncated: true, _originalSize: raw.length, _preview: raw.slice(0, MAX_RESULT_CHARS) };
1663
- }
1873
+ }
1874
+ } catch {
1875
+ }
1876
+ return count;
1877
+ }
1878
+ function hasChatLabDatabases(dirPath) {
1879
+ try {
1880
+ const markerPath = path5.join(dirPath, ".chatlab");
1881
+ const dbDir = path5.join(dirPath, "databases");
1882
+ if (!fs3.existsSync(markerPath) || !fs3.existsSync(dbDir)) return false;
1883
+ return fs3.readdirSync(dbDir).some((file) => file.endsWith(".db"));
1884
+ } catch {
1885
+ return false;
1886
+ }
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
1664
1932
  }
1665
- return {
1666
- success: true,
1667
- elapsed,
1668
- content: [{ type: "text", text: result.content }],
1669
- details,
1670
- truncated
1671
- };
1672
- } catch (error) {
1673
- if (abortController.signal.aborted) {
1674
- 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);
1675
1963
  }
1676
- console.error(`Failed to execute tool ${toolName}:`, error);
1677
- return { success: false, error: String(error) };
1678
- } finally {
1679
- activeToolTests.delete(testId);
1680
- }
1681
- });
1682
- server2.post("/_web/ai/tools/cancel", async (request) => {
1683
- const { testId } = request.body;
1684
- const controller = activeToolTests.get(testId);
1685
- if (controller) {
1686
- controller.abort();
1687
- activeToolTests.delete(testId);
1688
- return { success: true };
1689
1964
  }
1690
- return { success: false };
1691
- });
1692
- }
1693
-
1694
- // ../../packages/http-routes/src/routes/web/ai-chats.ts
1695
- function registerAiChatRoutes(server2, ctx) {
1696
- const cm = ctx.aiChatManager;
1697
- if (!cm) return;
1698
- server2.post("/_web/ai/chats", async (request) => {
1699
- const { sessionId, title, assistantId } = request.body;
1700
- return cm.createAIChat(sessionId, title, assistantId);
1701
- });
1702
- server2.get("/_web/ai/chats", async (request) => {
1703
- const { sessionId } = request.query;
1704
- if (!sessionId) return [];
1705
- return cm.getAIChats(sessionId);
1706
- });
1707
- server2.get("/_web/ai/chats/:id", async (request, reply) => {
1708
- const conv = cm.getAIChat(request.params.id);
1709
- if (!conv) return reply.code(404).send({ error: "AI chat not found" });
1710
- return conv;
1965
+ return { success: true };
1711
1966
  });
1712
- server2.put("/_web/ai/chats/:id/title", async (request) => {
1713
- 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
+ };
1714
1983
  });
1715
- server2.delete("/_web/ai/chats/:id", async (request) => {
1716
- 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);
1717
1991
  });
1718
- server2.post("/_web/ai/chats/:id/messages", async (request) => {
1719
- const { role, content, dataKeywords, dataMessageCount, contentBlocks, tokenUsage } = request.body;
1720
- return cm.addMessage(
1721
- request.params.id,
1722
- role,
1723
- content,
1724
- dataKeywords,
1725
- dataMessageCount,
1726
- contentBlocks,
1727
- 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
+ })
1728
2008
  );
2009
+ fileStats.sort((a, b) => b.mtime - a.mtime);
2010
+ return { success: true, path: fileStats[0].path, name: fileStats[0].name };
1729
2011
  });
1730
- server2.get("/_web/ai/chats/:id/messages", async (request) => {
1731
- return cm.getMessages(request.params.id);
1732
- });
1733
- server2.post("/_web/ai/chats/:id/messages/:messageId/delete-from", async (request, reply) => {
1734
- const { id, messageId } = request.params;
1735
- if (!id || typeof id !== "string" || !messageId || typeof messageId !== "string") {
1736
- 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" };
1737
2016
  }
1738
- cm.deleteMessagesFrom(id, messageId);
1739
- return { success: true };
1740
- });
1741
- server2.post("/_web/ai/chats/:id/fork", async (request, reply) => {
1742
- const { upToMessageId, title } = request.body;
1743
- if (!upToMessageId || typeof upToMessageId !== "string") {
1744
- 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");
1745
2027
  }
1746
- return cm.forkAIChat(request.params.id, upToMessageId, title);
1747
- });
1748
- server2.put("/_web/ai/messages/:messageId/content", async (request, reply) => {
1749
- const { content } = request.body;
1750
- if (!content || typeof content !== "string") {
1751
- return reply.code(400).send({ error: "content is required" });
2028
+ if (!fs3.existsSync(downloadsDir)) {
2029
+ fs3.mkdirSync(downloadsDir, { recursive: true });
1752
2030
  }
1753
- cm.updateMessageContent(request.params.messageId, content);
1754
- return { success: true };
2031
+ const filePath = path5.join(downloadsDir, filename);
2032
+ fs3.writeFileSync(filePath, buffer);
2033
+ return { success: true, filePath };
1755
2034
  });
1756
- server2.post("/_web/ai/chats/:id/messages/:messageId/delete-relink", async (request, reply) => {
1757
- const { id, messageId } = request.params;
1758
- if (!id || !messageId) {
1759
- 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) };
1760
2056
  }
1761
- cm.deleteAndRelinkMessage(id, messageId);
1762
2057
  return { success: true };
1763
2058
  });
1764
- server2.post("/_web/ai/chats/:id/messages/insert-after", async (request, reply) => {
1765
- const { afterMessageId, role, content, contentBlocks, tokenUsage } = request.body;
1766
- if (!afterMessageId || typeof afterMessageId !== "string") {
1767
- 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) };
1768
2067
  }
1769
- return cm.insertMessageAfter(request.params.id, afterMessageId, role, content, contentBlocks, tokenUsage);
2068
+ return { success: true };
1770
2069
  });
1771
- server2.get("/_web/ai/chats/:id/token-usage", async (request) => {
1772
- 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
1773
2079
  });
1774
- server2.get("/_web/ai/debug/schema", async () => {
1775
- return cm.getAiSchema();
2080
+ server2.addHook("onClose", async () => {
2081
+ await service.close();
1776
2082
  });
1777
- server2.post("/_web/ai/debug/execute-sql", async (request, reply) => {
1778
- const { sql } = request.body;
1779
- if (!sql || typeof sql !== "string") {
1780
- return reply.code(400).send({ error: "sql is required" });
1781
- }
1782
- try {
1783
- return cm.executeAiSQL(sql);
1784
- } catch (e) {
1785
- const msg = e instanceof Error ? e.message : String(e);
1786
- return reply.code(400).send({ error: msg });
1787
- }
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
+ });
1788
2092
  });
1789
- server2.post("/_web/ai/debug/clear-debug-context", async () => {
1790
- const cleared = cm.clearAllDebugContext();
1791
- 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
+ });
1792
2101
  });
1793
- server2.get("/_web/ai/chats/:id/estimate-tokens", async (request) => {
1794
- const history = cm.getHistoryForAgent(request.params.id);
1795
- const tokens = countMessagesTokens(history.map((m) => ({ role: m.role, content: m.content })));
1796
- 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
+ });
1797
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));
1798
2139
  }
1799
2140
 
1800
- // ../../packages/http-routes/src/routes/web/ai-summaries.ts
1801
- function createSummaryDeps(ctx) {
1802
- const store = ctx.llmConfigStore;
1803
- if (!store) return null;
1804
- return {
1805
- getLlmConfig() {
1806
- const config = store.getDefaultAssistantConfig();
1807
- if (!config) return null;
1808
- return config;
1809
- },
1810
- buildPiModel(config) {
1811
- return buildPiModel(config);
1812
- }
1813
- };
1814
- }
1815
- function registerAiSummaryRoutes(server2, ctx) {
1816
- const deps = createSummaryDeps(ctx);
1817
- if (!deps) return;
2141
+ // ../../packages/http-routes/src/routes/web/export.ts
2142
+ function registerExportRoutes(server2, ctx) {
1818
2143
  const { sessionAdapter: adapter } = ctx;
1819
- server2.post("/_web/sessions/:id/summaries/generate", async (request, reply) => {
1820
- const { segmentId, locale, forceRegenerate, strategy } = request.body;
1821
- const result = await summary_service_exports.generateSummary(adapter, request.params.id, segmentId, deps, {
1822
- locale,
1823
- forceRegenerate,
1824
- 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
1825
2154
  });
1826
- if ("error" in result && !result.success) {
1827
- 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 });
1828
2157
  }
1829
- 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);
1830
2161
  });
1831
- server2.post("/_web/sessions/:id/summaries/generate-all", async (request, reply) => {
1832
- const { locale, forceRegenerate } = request.body;
1833
- const result = await summary_service_exports.generateAllSummaries(adapter, request.params.id, deps, {
1834
- locale,
1835
- forceRegenerate
1836
- });
1837
- if (result.error) {
1838
- return reply.code(400).send({ error: result.error });
1839
- }
1840
- 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
1841
2171
  });
1842
- server2.post("/_web/sessions/:id/summaries/check-can-generate", async (request) => {
1843
- const { segmentIds } = request.body;
1844
- 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
+ });
1845
2178
  });
2179
+ server2.post(
2180
+ "/_web/global-insight/annual-summary/recompute",
2181
+ async (request) => service.startRecompute(parseRange(request.query))
2182
+ );
1846
2183
  }
1847
-
1848
- // ../../packages/http-routes/src/routes/web/ai-logs.ts
1849
- import fs3 from "fs";
1850
- import path4 from "path";
1851
- function findLatestAiLogPath(logsDir) {
1852
- const aiLogDir = path4.join(logsDir, "ai");
1853
- if (!fs3.existsSync(aiLogDir)) return null;
1854
- const latest = fs3.readdirSync(aiLogDir).filter((name) => name.startsWith("ai_") && name.endsWith(".log")).map((name) => {
1855
- const filePath = path4.join(aiLogDir, name);
1856
- const stat2 = fs3.statSync(filePath);
1857
- return { path: filePath, mtimeMs: stat2.mtimeMs };
1858
- }).sort((a, b) => b.mtimeMs - a.mtimeMs)[0];
1859
- 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
+ };
1860
2191
  }
1861
- function resolveAiLogPath(ctx) {
1862
- const currentPath = ctx.getCurrentAiLogPath?.();
1863
- if (currentPath && fs3.existsSync(currentPath)) return currentPath;
1864
- 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;
1865
2196
  }
1866
- function isLoopbackAddress(address) {
1867
- if (!address) return true;
1868
- const normalized = address.replace(/^::ffff:/, "");
1869
- 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";
1870
2199
  }
1871
- function registerAiLogRoutes(server2, ctx) {
1872
- server2.post("/_web/ai/logs/show", async (request, reply) => {
1873
- if (!ctx.showInFolder) return reply.code(501).send({ success: false, error: "Not supported" });
1874
- if (!isLoopbackAddress(request.ip)) {
1875
- return { success: false, error: "Opening AI logs is only supported on this device" };
1876
- }
1877
- const logPath = resolveAiLogPath(ctx);
1878
- if (!logPath) return { success: false, error: "No AI log files found" };
1879
- try {
1880
- await ctx.showInFolder(logPath);
1881
- return { success: true, path: logPath };
1882
- } catch (error) {
1883
- return { success: false, error: error instanceof Error ? error.message : String(error) };
1884
- }
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 };
1885
2211
  });
1886
2212
  }
1887
2213
 
1888
- // ../../packages/http-routes/src/routes/web/ai-semantic-index.ts
1889
- import path5 from "path";
1890
- function registerSemanticIndexRoutes(server2, ctx) {
1891
- const service = ctx.semanticIndexService;
1892
- if (!service) {
1893
- if (ctx.aiDataDir) {
1894
- const configStore = new SemanticIndexConfigStore(path5.join(ctx.aiDataDir, SEMANTIC_INDEX_CONFIG_FILE));
1895
- const configResponse = (config) => ({
1896
- config,
1897
- apiKeySet: resolveSemanticIndexApiKeySet(config, ctx.resolveApiKey),
1898
- configured: isSemanticIndexConfigured(config)
1899
- });
1900
- server2.get("/_web/ai/semantic-index/config", async () => ({
1901
- ...configResponse(configStore.get()),
1902
- modelStatus: "idle"
1903
- }));
1904
- server2.put(
1905
- "/_web/ai/semantic-index/config",
1906
- async (request) => {
1907
- const config = persistSemanticIndexConfig(configStore, request.body.config, {
1908
- apiKey: request.body.apiKey,
1909
- writeAuthProfile: ctx.writeAuthProfile
1910
- });
1911
- return { ...configResponse(config), modelStatus: "idle" };
1912
- }
1913
- );
1914
- }
1915
- server2.get("/_web/ai/semantic-index/enabled", async () => ({ sessions: [] }));
1916
- server2.get("/_web/ai/semantic-index/status", async () => ({ status: null }));
1917
- server2.post("/_web/ai/semantic-index/status", async () => ({ sessions: [] }));
1918
- return;
1919
- }
1920
- server2.get("/_web/ai/semantic-index/config", async () => {
1921
- return {
1922
- config: await service.getConfig(),
1923
- apiKeySet: await service.hasApiKey(),
1924
- configured: await service.isConfigured(),
1925
- modelStatus: await service.getModelStatus()
1926
- };
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);
1927
2219
  });
1928
- server2.put(
1929
- "/_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",
1930
2230
  async (request) => {
1931
- const config = await service.setConfig(request.body.config, { apiKey: request.body.apiKey });
1932
- return {
1933
- config,
1934
- apiKeySet: await service.hasApiKey(),
1935
- configured: await service.isConfigured(),
1936
- modelStatus: await service.getModelStatus()
1937
- };
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 };
1938
2242
  }
1939
2243
  );
1940
- server2.get("/_web/ai/semantic-index/enabled", async () => {
1941
- return { sessions: await service.listEnabledStatuses() };
1942
- });
1943
- server2.get("/_web/ai/semantic-index/status", async (request) => {
1944
- return { status: await service.status(request.query.sessionId) };
1945
- });
1946
- server2.post("/_web/ai/semantic-index/status", async (request) => {
1947
- return { sessions: await service.statusForSessions(request.body.sessionIds ?? []) };
1948
- });
1949
- const sessionAction = (path14, action) => {
1950
- server2.post(path14, async (request, reply) => {
1951
- const { sessionId } = request.body;
1952
- if (!sessionId) return reply.code(400).send({ error: "sessionId is required" });
1953
- await action(sessionId);
1954
- return { status: await service.status(sessionId) };
1955
- });
1956
- };
1957
- sessionAction("/_web/ai/semantic-index/enable", (id) => service.enable(id));
1958
- sessionAction("/_web/ai/semantic-index/remove", (id) => service.remove(id));
1959
- sessionAction("/_web/ai/semantic-index/build", (id) => service.build(id));
1960
- sessionAction("/_web/ai/semantic-index/pause", (id) => service.pause(id));
1961
- sessionAction("/_web/ai/semantic-index/cancel", (id) => service.cancel(id));
1962
- sessionAction("/_web/ai/semantic-index/rebuild", (id) => service.rebuild(id));
1963
- server2.post("/_web/ai/semantic-index/build-pending", async () => {
1964
- await service.buildAllPending();
1965
- return { sessions: await service.listEnabledStatuses() };
1966
- });
1967
- server2.post("/_web/ai/semantic-index/cleanup", async () => {
1968
- return await service.cleanupUnused();
1969
- });
1970
2244
  server2.post(
1971
- "/_web/ai/semantic-index/search",
2245
+ "/_web/sessions/:id/members/batch-delete",
1972
2246
  async (request, reply) => {
1973
- const { sessionId, query, finalTopK } = request.body;
1974
- if (!sessionId || !query) return reply.code(400).send({ error: "sessionId and query are required" });
1975
- 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 };
1976
2270
  }
1977
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;
1978
2290
  }
1979
2291
 
1980
2292
  // ../../packages/http-routes/src/routes/web/merge.ts
1981
2293
  import * as fs4 from "fs";
1982
- import * as os from "os";
1983
2294
  import * as path6 from "path";
1984
- function ensureDb2(ctx, sessionId) {
2295
+ function ensureDb(ctx, sessionId) {
1985
2296
  const db = ctx.dbManager.open(sessionId);
1986
2297
  if (!db) throw sessionNotFound(sessionId);
1987
2298
  return db;
@@ -1997,7 +2308,7 @@ function registerMergeRoutes(server2, ctx) {
1997
2308
  if (contentType.includes("multipart/form-data")) {
1998
2309
  const data = await request.file();
1999
2310
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
2000
- const tmpDir = fs4.mkdtempSync(path6.join(os.tmpdir(), "chatlab-merge-"));
2311
+ const tmpDir = createChatLabTempDir("merge", "upload-");
2001
2312
  const tmpPath = path6.join(tmpDir, data.filename || "upload");
2002
2313
  const chunks = [];
2003
2314
  for await (const chunk of data.file) {
@@ -2090,7 +2401,7 @@ function registerMergeRoutes(server2, ctx) {
2090
2401
  return reply.code(501).send({ error: "Import not available on this server" });
2091
2402
  }
2092
2403
  const jsonData = JSON.stringify(merged.chatLabData);
2093
- const tmpDir = fs4.mkdtempSync(path6.join(os.tmpdir(), "chatlab-merged-"));
2404
+ const tmpDir = createChatLabTempDir("merge", "import-");
2094
2405
  const safeName = path6.basename(outputName).replace(/[/\\?%*:|"<>]/g, "_") || "merged";
2095
2406
  const tmpPath = path6.join(tmpDir, `${safeName}.json`);
2096
2407
  fs4.writeFileSync(tmpPath, jsonData, "utf-8");
@@ -2139,7 +2450,7 @@ function registerMergeRoutes(server2, ctx) {
2139
2450
  }
2140
2451
  const handles = [];
2141
2452
  for (const sid of sessionIds) {
2142
- const db = ensureDb2(ctx, sid);
2453
+ const db = ensureDb(ctx, sid);
2143
2454
  const exported = exportSessionToJson(db);
2144
2455
  const { db: tempDb, tempDbPath } = mergeCache.createTempDatabase(exported.meta.name);
2145
2456
  const writer = new TempDbWriter(tempDb);
@@ -2176,236 +2487,289 @@ function registerMergeRoutes(server2, ctx) {
2176
2487
  });
2177
2488
  }
2178
2489
 
2179
- // ../../packages/http-routes/src/routes/web/cache.ts
2490
+ // ../../packages/http-routes/src/routes/web/nlp.ts
2180
2491
  import * as fs5 from "fs";
2181
- import * as fsp from "fs/promises";
2182
2492
  import * as path7 from "path";
2183
- async function getDirSize(dirPath) {
2184
- let totalSize = 0;
2185
- try {
2186
- if (!fs5.existsSync(dirPath)) return 0;
2187
- const files = await fsp.readdir(dirPath, { withFileTypes: true });
2188
- for (const file of files) {
2189
- const filePath = path7.join(dirPath, file.name);
2190
- if (file.isDirectory()) {
2191
- totalSize += await getDirSize(filePath);
2192
- } else {
2193
- const stat2 = await fsp.stat(filePath);
2194
- totalSize += stat2.size;
2195
- }
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 });
2196
2517
  }
2197
- } catch {
2198
- }
2199
- 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
+ );
2200
2539
  }
2201
- async function getFileCount(dirPath) {
2202
- let count = 0;
2203
- try {
2204
- if (!fs5.existsSync(dirPath)) return 0;
2205
- const files = await fsp.readdir(dirPath, { withFileTypes: true });
2206
- for (const file of files) {
2207
- const filePath = path7.join(dirPath, file.name);
2208
- if (file.isDirectory()) {
2209
- count += await getFileCount(filePath);
2210
- } else {
2211
- count++;
2212
- }
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
+ });
2213
2574
  }
2214
- } catch {
2215
- }
2216
- return count;
2575
+ );
2217
2576
  }
2218
- function hasChatLabDatabases(dirPath) {
2219
- try {
2220
- const markerPath = path7.join(dirPath, ".chatlab");
2221
- const dbDir = path7.join(dirPath, "databases");
2222
- if (!fs5.existsSync(markerPath) || !fs5.existsSync(dbDir)) return false;
2223
- return fs5.readdirSync(dbDir).some((file) => file.endsWith(".db"));
2224
- } catch {
2225
- return false;
2226
- }
2577
+ function isTruthy3(value) {
2578
+ return value === "1" || value === "true" || value === "yes";
2227
2579
  }
2228
- function registerCacheRoutes(server2, ctx) {
2229
- const pp = ctx.pathProvider;
2230
- const downloadsDir = ctx.downloadsDir ?? pp.getDownloadsDir();
2231
- server2.get("/_web/cache/info", async () => {
2232
- const directories = [
2233
- {
2234
- id: "databases",
2235
- name: "settings.storage.cache.databases.name",
2236
- description: "settings.storage.cache.databases.description",
2237
- path: pp.getDatabaseDir(),
2238
- scope: "user-data",
2239
- rootPath: pp.getUserDataDir(),
2240
- icon: "i-heroicons-circle-stack",
2241
- canClear: false
2242
- },
2243
- {
2244
- id: "ai",
2245
- name: "settings.storage.cache.ai.name",
2246
- description: "settings.storage.cache.ai.description",
2247
- path: pp.getAiDataDir(),
2248
- scope: "system-data",
2249
- rootPath: pp.getSystemDir(),
2250
- icon: "i-heroicons-sparkles",
2251
- canClear: false
2252
- },
2253
- {
2254
- id: "cache",
2255
- name: "settings.storage.cache.statsCache.name",
2256
- description: "settings.storage.cache.statsCache.description",
2257
- path: pp.getCacheDir(),
2258
- scope: "system-data",
2259
- rootPath: pp.getSystemDir(),
2260
- icon: "i-heroicons-bolt",
2261
- canClear: true
2262
- },
2263
- {
2264
- id: "logs",
2265
- name: "settings.storage.cache.logs.name",
2266
- description: "settings.storage.cache.logs.description",
2267
- path: pp.getLogsDir(),
2268
- scope: "system-data",
2269
- rootPath: pp.getSystemDir(),
2270
- icon: "i-heroicons-document-text",
2271
- canClear: true
2272
- }
2273
- ];
2274
- const results = await Promise.all(
2275
- directories.map(async (dir) => {
2276
- const [size, fileCount] = await Promise.all([getDirSize(dir.path), getFileCount(dir.path)]);
2277
- return { ...dir, size, fileCount, exists: fs5.existsSync(dir.path) };
2278
- })
2279
- );
2280
- return {
2281
- baseDir: pp.getSystemDir(),
2282
- directories: results,
2283
- totalSize: results.reduce((sum, d) => sum + d.size, 0)
2284
- };
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();
2285
2592
  });
2286
- server2.post("/_web/cache/clear", async (request) => {
2287
- const { cacheId } = request.body;
2288
- const allowedDirs = {
2289
- cache: pp.getCacheDir(),
2290
- logs: pp.getLogsDir()
2291
- };
2292
- const dirPath = allowedDirs[cacheId];
2293
- if (!dirPath) return { success: false, error: "Not allowed to clear this directory" };
2294
- if (!fs5.existsSync(dirPath)) return { success: true };
2295
- const files = await fsp.readdir(dirPath);
2296
- for (const file of files) {
2297
- const filePath = path7.join(dirPath, file);
2298
- const stat2 = await fsp.stat(filePath);
2299
- if (stat2.isDirectory()) {
2300
- await fsp.rm(filePath, { recursive: true });
2301
- } else {
2302
- 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
+ }
2303
2606
  }
2607
+ return { success: true };
2608
+ } catch (err) {
2609
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
2304
2610
  }
2305
- return { success: true };
2306
2611
  });
2307
- server2.get("/_web/cache/data-dir", async () => {
2308
- const pending = ctx.getPendingDataDirMigration?.();
2309
- return {
2310
- path: pp.getUserDataDir(),
2311
- defaultPath: ctx.defaultUserDataDir,
2312
- isCustom: ctx.isCustomDataDir ?? false,
2313
- canSetDataDir: ctx.canSetDataDir ?? Boolean(ctx.setDataDir),
2314
- managedScope: "chat-databases",
2315
- managedDescription: "settings.storage.dataLocation.managedDescription",
2316
- hasLegacyDataAtDefaultDir: Boolean(ctx.defaultUserDataDir) && path7.resolve(pp.getUserDataDir()) !== path7.resolve(ctx.defaultUserDataDir ?? "") && hasChatLabDatabases(ctx.defaultUserDataDir ?? ""),
2317
- pendingMigration: pending ? {
2318
- from: pending.from,
2319
- to: pending.to,
2320
- createdAt: pending.createdAt
2321
- } : void 0
2322
- };
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 };
2323
2641
  });
2324
- server2.post("/_web/cache/data-dir", async (request, reply) => {
2325
- if (!ctx.setDataDir) {
2326
- 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" });
2327
2649
  }
2328
- const targetPath = typeof request.body?.path === "string" ? request.body.path : null;
2329
- const migrate = request.body?.migrate !== false;
2330
- 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);
2331
2655
  });
2332
- server2.get("/_web/cache/latest-import-log", async () => {
2333
- const importLogDir = path7.join(pp.getLogsDir(), "import");
2334
- if (!fs5.existsSync(importLogDir)) {
2335
- 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 });
2336
2683
  }
2337
- const files = await fsp.readdir(importLogDir);
2338
- const logFiles = files.filter((f) => f.startsWith("import_") && f.endsWith(".log"));
2339
- if (logFiles.length === 0) {
2340
- return { success: false, error: "No import logs found" };
2684
+ if (ctx.aiChatManager) {
2685
+ session.aiConversationCount = ctx.aiChatManager.getAIChats(request.params.id).length;
2341
2686
  }
2342
- const fileStats = await Promise.all(
2343
- logFiles.map(async (f) => {
2344
- const filePath = path7.join(importLogDir, f);
2345
- const stat2 = await fsp.stat(filePath);
2346
- return { name: f, path: filePath, mtime: stat2.mtime.getTime() };
2347
- })
2348
- );
2349
- fileStats.sort((a, b) => b.mtime - a.mtime);
2350
- return { success: true, path: fileStats[0].path, name: fileStats[0].name };
2687
+ return session;
2351
2688
  });
2352
- server2.post("/_web/cache/save-to-downloads", async (request) => {
2353
- const { filename, dataUrl } = request.body;
2354
- if (!filename || !dataUrl) {
2355
- 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) });
2356
2699
  }
2357
- let buffer;
2358
- if (dataUrl.includes(";base64,")) {
2359
- const base64Data = dataUrl.split(";base64,")[1];
2360
- buffer = Buffer.from(base64Data, "base64");
2361
- } else if (dataUrl.includes("charset=utf-8,")) {
2362
- const textData = dataUrl.split("charset=utf-8,")[1];
2363
- buffer = Buffer.from(decodeURIComponent(textData), "utf-8");
2364
- } else {
2365
- const base64Data = dataUrl.replace(/^data:[^,]+,/, "");
2366
- 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 };
2367
2710
  }
2368
- if (!fs5.existsSync(downloadsDir)) {
2369
- 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
+ );
2370
2724
  }
2371
- const filePath = path7.join(downloadsDir, filename);
2372
- fs5.writeFileSync(filePath, buffer);
2373
- 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 };
2374
2729
  });
2375
- server2.post("/_web/cache/open-dir", async (request, reply) => {
2376
- if (!ctx.openDirectory) return reply.code(501).send({ success: false, error: "Not supported" });
2377
- const { cacheId } = request.body;
2378
- const dirPaths = {
2379
- base: pp.getSystemDir(),
2380
- userData: pp.getUserDataDir(),
2381
- databases: pp.getDatabaseDir(),
2382
- cache: pp.getCacheDir(),
2383
- ai: pp.getAiDataDir(),
2384
- logs: pp.getLogsDir(),
2385
- downloads: downloadsDir
2386
- };
2387
- const dirPath = dirPaths[cacheId];
2388
- if (!dirPath) return { success: false, error: "Unknown directory" };
2389
- if (!fs5.existsSync(dirPath)) {
2390
- 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" });
2391
2740
  }
2392
2741
  try {
2393
- await ctx.openDirectory(dirPath);
2394
- } catch (error) {
2395
- 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 });
2396
2753
  }
2397
- return { success: true };
2398
2754
  });
2399
- server2.post("/_web/cache/show-in-folder", async (request, reply) => {
2400
- if (!ctx.showInFolder) return reply.code(501).send({ success: false, error: "Not supported" });
2401
- const { filePath } = request.body;
2402
- if (!filePath) return { success: false, error: "filePath is required" };
2403
- try {
2404
- await ctx.showInFolder(filePath);
2405
- } catch (error) {
2406
- 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 });
2407
2764
  }
2408
- 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);
2409
2773
  });
2410
2774
  }
2411
2775
 
@@ -2435,25 +2799,9 @@ function registerTelemetryRoutes(server2, ctx) {
2435
2799
  );
2436
2800
  }
2437
2801
 
2438
- // ../../packages/http-routes/src/routes/web/logs.ts
2439
- function registerLogRoutes(server2, _ctx) {
2440
- server2.post("/_web/logs/report", async (req, reply) => {
2441
- const { level, message, stack, url } = req.body ?? {};
2442
- if (!message) return reply.code(400).send({ error: "message required" });
2443
- const detail = [url ? `url=${url}` : null, stack ? `
2444
- ${stack}` : null].filter(Boolean).join(" ");
2445
- if (level === "warn") appLogger.warn("web", message, detail || void 0);
2446
- else appLogger.error("web", message, detail || void 0);
2447
- return { ok: true };
2448
- });
2449
- }
2450
-
2451
- // ../../packages/http-routes/src/register.ts
2452
- function registerSharedRoutes(server2, ctx, options) {
2802
+ // ../../packages/http-routes/src/routes/web/register.ts
2803
+ function registerWebRoutes(server2, ctx, options) {
2453
2804
  const resolvedCtx = ctx.preferencesManager ? ctx : { ...ctx, preferencesManager: new PreferencesManager(ctx.pathProvider.getSystemDir()) };
2454
- registerSystemRoutes(server2, resolvedCtx);
2455
- registerRestSessionRoutes(server2, resolvedCtx);
2456
- registerImportRoutes(server2, resolvedCtx);
2457
2805
  registerSessionRoutes(server2, resolvedCtx);
2458
2806
  registerMemberRoutes(server2, resolvedCtx);
2459
2807
  registerContactsRoutes(server2, resolvedCtx);
@@ -2465,77 +2813,17 @@ function registerSharedRoutes(server2, ctx, options) {
2465
2813
  registerSessionIndexRoutes(server2, resolvedCtx);
2466
2814
  registerExportRoutes(server2, resolvedCtx);
2467
2815
  registerNlpRoutes(server2, resolvedCtx);
2468
- if (options?.requireAi) {
2469
- const missing = [];
2470
- if (!resolvedCtx.aiDataDir) missing.push("aiDataDir");
2471
- if (!resolvedCtx.aiChatManager) missing.push("aiChatManager");
2472
- if (!resolvedCtx.assistantManager) missing.push("assistantManager");
2473
- if (!resolvedCtx.skillManagerCore) missing.push("skillManagerCore");
2474
- if (!resolvedCtx.llmConfigStore) missing.push("llmConfigStore");
2475
- if (!resolvedCtx.customProviderStore) missing.push("customProviderStore");
2476
- if (!resolvedCtx.customModelStore) missing.push("customModelStore");
2477
- if (!resolvedCtx.runAgentStream) missing.push("runAgentStream");
2478
- if (missing.length > 0) {
2479
- throw new Error(`[http-routes] requireAi is set but missing AI dependencies: ${missing.join(", ")}`);
2480
- }
2481
- }
2482
- registerAiAssistantRoutes(server2, resolvedCtx);
2483
- registerAiSkillRoutes(server2, resolvedCtx);
2484
- registerAiLlmRoutes(server2, resolvedCtx);
2485
- registerAiLlmStreamRoutes(server2, resolvedCtx);
2486
- registerAiAgentStreamRoutes(server2, resolvedCtx);
2487
- registerAiToolRoutes(server2, resolvedCtx);
2488
- registerAiChatRoutes(server2, resolvedCtx);
2489
- registerAiSummaryRoutes(server2, resolvedCtx);
2490
- registerAiLogRoutes(server2, resolvedCtx);
2491
- registerSemanticIndexRoutes(server2, resolvedCtx);
2816
+ registerAiRoutes(server2, resolvedCtx, options);
2492
2817
  registerMergeRoutes(server2, resolvedCtx);
2493
2818
  registerCacheRoutes(server2, resolvedCtx);
2494
2819
  registerTelemetryRoutes(server2, resolvedCtx);
2495
- registerLogRoutes(server2, resolvedCtx);
2820
+ registerLogRoutes(server2);
2496
2821
  }
2497
2822
 
2498
- // ../../packages/http-routes/src/auth.ts
2499
- import { timingSafeEqual, createHmac, randomBytes } from "crypto";
2500
- var cachedToken = null;
2501
- var requireAuthEnabled = false;
2502
- function setAuthToken(token) {
2503
- cachedToken = token;
2504
- }
2505
- function setRequireAuth(enabled) {
2506
- requireAuthEnabled = enabled;
2507
- }
2508
- var hmacKey = randomBytes(32);
2509
- function safeTokenCompare(a, b) {
2510
- const hashA = createHmac("sha256", hmacKey).update(a).digest();
2511
- const hashB = createHmac("sha256", hmacKey).update(b).digest();
2512
- return timingSafeEqual(hashA, hashB);
2513
- }
2514
- async function authHook(request, reply) {
2515
- if (!cachedToken) return;
2516
- const url = request.url;
2517
- if (url.startsWith("/api/")) {
2518
- return requireBearerToken(request, reply);
2519
- }
2520
- if (url.startsWith("/_web/")) {
2521
- if (requireAuthEnabled) return requireBearerToken(request, reply);
2522
- return;
2523
- }
2524
- }
2525
- function requireBearerToken(request, reply) {
2526
- if (!cachedToken) return;
2527
- const authHeader = request.headers.authorization;
2528
- if (!authHeader || !authHeader.startsWith("Bearer ")) {
2529
- const err = unauthorized();
2530
- reply.code(err.statusCode).send(errorResponse(err));
2531
- return;
2532
- }
2533
- const token = authHeader.slice(7);
2534
- if (!safeTokenCompare(token, cachedToken)) {
2535
- const err = unauthorized();
2536
- reply.code(err.statusCode).send(errorResponse(err));
2537
- return;
2538
- }
2823
+ // ../../packages/http-routes/src/register.ts
2824
+ function registerSharedRoutes(server2, ctx, options) {
2825
+ registerRestRoutes(server2, ctx);
2826
+ registerWebRoutes(server2, ctx, options);
2539
2827
  }
2540
2828
 
2541
2829
  // ../../packages/http-routes/src/routes/web/automation.ts
@@ -2682,44 +2970,8 @@ function registerAutomationRoutes(server2, ctx) {
2682
2970
  });
2683
2971
  }
2684
2972
 
2685
- // src/http/server.ts
2686
- var JSON_BODY_LIMIT = 50 * 1024 * 1024;
2687
- function createServer() {
2688
- const server2 = Fastify({
2689
- logger: false,
2690
- bodyLimit: JSON_BODY_LIMIT
2691
- });
2692
- server2.addHook("onRequest", authHook);
2693
- server2.setErrorHandler((error, request, reply) => {
2694
- const apiError = apiErrorFromUnknown(error);
2695
- if (apiError) {
2696
- reply.code(apiError.statusCode).send(errorResponse(apiError));
2697
- return;
2698
- }
2699
- if (error.statusCode === 413) {
2700
- const bodyErr = new ApiError("BODY_TOO_LARGE" /* BODY_TOO_LARGE */, "Request body exceeds 50MB limit");
2701
- reply.code(413).send(errorResponse(bodyErr));
2702
- return;
2703
- }
2704
- const statusCode = error.statusCode;
2705
- if (statusCode && statusCode >= 400 && statusCode < 600) {
2706
- reply.code(statusCode).send({ success: false, error: { code: "CLIENT_ERROR", message: error.message } });
2707
- return;
2708
- }
2709
- appLogger.error("http", `${request.method} ${request.url} -> 500`, error);
2710
- const err = serverError(error.message);
2711
- reply.code(err.statusCode).send(errorResponse(err));
2712
- });
2713
- return server2;
2714
- }
2715
-
2716
- // src/http/routes/web/index.ts
2717
- import * as os3 from "os";
2718
- import * as path10 from "path";
2719
-
2720
2973
  // src/http/routes/web/import.ts
2721
2974
  import * as fs7 from "fs";
2722
- import * as os2 from "os";
2723
2975
  import * as path8 from "path";
2724
2976
  import { randomUUID } from "crypto";
2725
2977
  import { pipeline } from "stream/promises";
@@ -2750,9 +3002,7 @@ function cleanupTemp(...paths) {
2750
3002
  }
2751
3003
  }
2752
3004
  function registerImportRoutes2(server2, dbManager2, options = {}) {
2753
- const sourceManager = options.sourceManager ?? new ArchiveImportSourceManager({
2754
- tempRoot: fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-import-sources-"))
2755
- });
3005
+ const sourceManager = options.sourceManager ?? new ArchiveImportSourceManager();
2756
3006
  const runAutoImport = options.runAutoImport ?? autoImport.bind(null, dbManager2);
2757
3007
  const runPreparedImport = options.runPreparedImport ?? (async (manifestPath, onProgress) => {
2758
3008
  const result = await runAutoImport(manifestPath, {
@@ -2770,7 +3020,7 @@ function registerImportRoutes2(server2, dbManager2, options = {}) {
2770
3020
  limits: { fileSize: ARCHIVE_UPLOAD_LIMIT }
2771
3021
  });
2772
3022
  if (!data) return reply.code(400).send({ success: false, error: "error.no_file_selected" });
2773
- const uploadPath = path8.join(os2.tmpdir(), `chatlab-archive-${randomUUID()}.zip`);
3023
+ const uploadPath = path8.join(getChatLabTempScopeDir("imports"), `archive-${randomUUID()}.zip`);
2774
3024
  try {
2775
3025
  await pipeline(data.file, fs7.createWriteStream(uploadPath, { flags: "wx" }));
2776
3026
  if (data.file.truncated) {
@@ -2837,7 +3087,7 @@ data: ${JSON.stringify(eventData)}
2837
3087
  server2.post("/_web/detect-format", async (request, reply) => {
2838
3088
  const data = await request.file();
2839
3089
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
2840
- const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-detect-"));
3090
+ const tmpDir = createChatLabTempDir("imports", "detect-");
2841
3091
  const tmpPath = path8.join(tmpDir, data.filename || "upload");
2842
3092
  try {
2843
3093
  const chunks = [];
@@ -2855,7 +3105,7 @@ data: ${JSON.stringify(eventData)}
2855
3105
  server2.post("/_web/scan-multi-chat", async (request, reply) => {
2856
3106
  const data = await request.file();
2857
3107
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
2858
- const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-scan-"));
3108
+ const tmpDir = createChatLabTempDir("imports", "scan-");
2859
3109
  const tmpPath = path8.join(tmpDir, data.filename || "upload");
2860
3110
  try {
2861
3111
  const chunks = [];
@@ -2872,7 +3122,7 @@ data: ${JSON.stringify(eventData)}
2872
3122
  server2.post("/_web/import", async (request, reply) => {
2873
3123
  const data = await request.file();
2874
3124
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
2875
- const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-import-"));
3125
+ const tmpDir = createChatLabTempDir("imports", "upload-");
2876
3126
  const tmpPath = path8.join(tmpDir, data.filename || "upload");
2877
3127
  const chunks = [];
2878
3128
  for await (const chunk of data.file) {
@@ -2916,7 +3166,7 @@ data: ${JSON.stringify(eventData)}
2916
3166
  server2.post("/_web/import-directory", async (request, reply) => {
2917
3167
  const parts = request.parts();
2918
3168
  if (!parts) return reply.code(400).send({ error: "No files uploaded" });
2919
- const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-dir-import-"));
3169
+ const tmpDir = createChatLabTempDir("imports", "directory-");
2920
3170
  const relativePaths = [];
2921
3171
  const fileBuffers = [];
2922
3172
  try {
@@ -2987,7 +3237,7 @@ data: ${JSON.stringify({ success: false, error: err instanceof Error ? err.messa
2987
3237
  const sessionId = request.params.id;
2988
3238
  const data = await request.file();
2989
3239
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
2990
- const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-inc-"));
3240
+ const tmpDir = createChatLabTempDir("imports", "incremental-");
2991
3241
  const tmpPath = path8.join(tmpDir, data.filename || "upload");
2992
3242
  const chunks = [];
2993
3243
  for await (const chunk of data.file) {
@@ -3025,7 +3275,7 @@ data: ${JSON.stringify(eventData)}
3025
3275
  const sessionId = request.params.id;
3026
3276
  const data = await request.file();
3027
3277
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
3028
- const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-analyze-"));
3278
+ const tmpDir = createChatLabTempDir("imports", "analyze-");
3029
3279
  const tmpPath = path8.join(tmpDir, data.filename || "upload");
3030
3280
  const chunks = [];
3031
3281
  for await (const chunk of data.file) {
@@ -3043,7 +3293,7 @@ data: ${JSON.stringify(eventData)}
3043
3293
  server2.post("/_web/import/analyze", async (request, reply) => {
3044
3294
  const data = await request.file();
3045
3295
  if (!data) return reply.code(400).send({ error: "No file uploaded" });
3046
- const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-analyze-"));
3296
+ const tmpDir = createChatLabTempDir("imports", "analyze-");
3047
3297
  const tmpPath = path8.join(tmpDir, data.filename || "upload");
3048
3298
  const chunks = [];
3049
3299
  for await (const chunk of data.file) {
@@ -3079,7 +3329,7 @@ data: ${JSON.stringify(eventData)}
3079
3329
 
3080
3330
  `);
3081
3331
  }
3082
- const tmpDir = fs7.mkdtempSync(path8.join(os2.tmpdir(), "chatlab-demo-"));
3332
+ const tmpDir = createChatLabTempDir("imports", "cli-demo-");
3083
3333
  try {
3084
3334
  const localPaths = [];
3085
3335
  for (let i = 0; i < total; i++) {
@@ -3166,29 +3416,29 @@ function buildWebUpdateCheckResult(options) {
3166
3416
  }
3167
3417
 
3168
3418
  // src/http/routes/web/index.ts
3169
- function registerWebRoutes(server2, dbManager2, options) {
3419
+ function registerWebRoutes2(server2, dbManager2, options) {
3170
3420
  const adapter = createDatabaseManagerAdapter(dbManager2);
3171
3421
  const mergeCache = options?.pathProvider ? new MergeSessionCache(options.pathProvider, { nativeBinding: options.nativeBinding }) : null;
3172
3422
  mergeCache?.cleanupOrphans();
3173
3423
  const fallbackPathProvider = {
3174
- getSystemDir: () => path10.join(os3.homedir(), ".chatlab"),
3175
- getUserDataDir: () => path10.join(os3.homedir(), ".chatlab", "data"),
3176
- getDatabaseDir: () => path10.join(os3.homedir(), ".chatlab", "data", "databases"),
3177
- getVectorDir: () => path10.join(os3.homedir(), ".chatlab", "data", "vector"),
3178
- getAiDataDir: () => path10.join(os3.homedir(), ".chatlab", "ai"),
3179
- getSettingsDir: () => path10.join(os3.homedir(), ".chatlab", "settings"),
3180
- getCacheDir: () => path10.join(os3.homedir(), ".chatlab", "cache"),
3181
- getTempDir: () => path10.join(os3.homedir(), ".chatlab", "temp"),
3182
- getLogsDir: () => path10.join(os3.homedir(), ".chatlab", "logs"),
3183
- 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")
3184
3434
  };
3185
3435
  const resolvedPathProvider = options?.pathProvider ?? fallbackPathProvider;
3186
3436
  const ai = options?.aiContext;
3187
3437
  const cliStreamImport = async (dm, filePath) => {
3188
- const { streamImport: streamImport2 } = await import("./stream-import-VUEL2TTU.mjs");
3438
+ const { streamImport: streamImport2 } = await import("./stream-import-JKQYRISL.mjs");
3189
3439
  const result = await streamImport2(dm, filePath);
3190
3440
  if (!result.success) {
3191
- const { IMPORT_IN_PROGRESS_ERROR_KEY: IMPORT_IN_PROGRESS_ERROR_KEY2, ImportInProgressError: ImportInProgressError2 } = await import("./src-R3QX6L3L.mjs");
3441
+ const { IMPORT_IN_PROGRESS_ERROR_KEY: IMPORT_IN_PROGRESS_ERROR_KEY2, ImportInProgressError: ImportInProgressError2 } = await import("./src-UY4GCVNF.mjs");
3192
3442
  if (result.error === IMPORT_IN_PROGRESS_ERROR_KEY2) throw new ImportInProgressError2();
3193
3443
  throw new Error(result.error || "Import failed");
3194
3444
  }
@@ -4089,12 +4339,11 @@ function reloadTimer(dsId, skipInitialPull = false) {
4089
4339
 
4090
4340
  // src/sync/adapters.ts
4091
4341
  import * as fs11 from "fs";
4092
- import * as os4 from "os";
4093
4342
  import * as path13 from "path";
4094
4343
  import * as crypto3 from "crypto";
4095
4344
  function getTempFilePath(ext) {
4096
4345
  const id = crypto3.randomBytes(8).toString("hex");
4097
- return path13.join(os4.tmpdir(), `chatlab-pull-${id}${ext}`);
4346
+ return path13.join(getChatLabTempScopeDir("sync"), `pull-${id}${ext}`);
4098
4347
  }
4099
4348
  var NodeFetcher = class {
4100
4349
  async fetchToTempFile(baseUrl, remoteSessionId, token, params) {
@@ -4265,21 +4514,6 @@ function cleanupSync() {
4265
4514
  var server = null;
4266
4515
  var dbManager = null;
4267
4516
  var aiChatManager = null;
4268
- function createFileConfigStorage(aiDataDir) {
4269
- return {
4270
- readJson(key) {
4271
- try {
4272
- return JSON.parse(fs12.readFileSync(`${aiDataDir}/${key}.json`, "utf-8"));
4273
- } catch {
4274
- return null;
4275
- }
4276
- },
4277
- writeJson(key, data) {
4278
- if (!fs12.existsSync(aiDataDir)) fs12.mkdirSync(aiDataDir, { recursive: true });
4279
- fs12.writeFileSync(`${aiDataDir}/${key}.json`, JSON.stringify(data, null, 2), "utf-8");
4280
- }
4281
- };
4282
- }
4283
4517
  function resolveNativeBinding2() {
4284
4518
  if (process.versions.electron) return void 0;
4285
4519
  const nativePath = resolveCliPath("native/better_sqlite3.node");
@@ -4337,20 +4571,11 @@ async function startHttpServer(options) {
4337
4571
  aiChatManager = new AIChatManager(aiDataDir, { nativeBinding });
4338
4572
  const assistantManager = getAssistantManager(aiDataDir);
4339
4573
  const skillManagerCore = getSkillManagerCore(aiDataDir);
4340
- const llmConfigStore = new LLMConfigStore(createFileConfigStorage(aiDataDir), {
4341
- resolveApiKey: (provider, authProfile) => resolveApiKey(provider, authProfile) || void 0,
4342
- onApiKeyCreated: (config2, apiKey) => {
4343
- const profileName = config2.name?.toLowerCase().replace(/\s+/g, "-") || config2.provider;
4344
- writeAuthProfile(profileName, { type: "api_key", provider: config2.provider, key: apiKey });
4345
- return profileName;
4346
- },
4347
- onApiKeyDeleted: (config2) => {
4348
- const profileName = config2.authProfile;
4349
- if (profileName) deleteAuthProfile(profileName);
4350
- }
4351
- });
4574
+ const configStorage = createFileConfigStorage(aiDataDir);
4575
+ const llmConfigStore = createAuthProfileLlmConfigStore(configStorage);
4352
4576
  initAppLogger(pathProvider.getLogsDir());
4353
4577
  initServerAiLogger(pathProvider.getLogsDir());
4578
+ appLogger.info("temp-workspace", "Temporary workspace initialized", { root: pathProvider.getTempDir() });
4354
4579
  appLogger.info("server", `HTTP server starting on ${host}:${port}`);
4355
4580
  logNativeParserStatus();
4356
4581
  setAuthToken(token);
@@ -4374,7 +4599,7 @@ async function startHttpServer(options) {
4374
4599
  console.warn("[semantic-index] worker client unavailable:", err instanceof Error ? err.message : String(err));
4375
4600
  semanticIndexService = void 0;
4376
4601
  }
4377
- registerWebRoutes(server, dbManager, {
4602
+ registerWebRoutes2(server, dbManager, {
4378
4603
  pathProvider,
4379
4604
  nativeBinding,
4380
4605
  runtimeIdentity: runtime,
@@ -4385,8 +4610,8 @@ async function startHttpServer(options) {
4385
4610
  assistantManager,
4386
4611
  skillManagerCore,
4387
4612
  llmConfigStore,
4388
- customProviderStore: new CustomProviderStore(createFileConfigStorage(aiDataDir)),
4389
- customModelStore: new CustomModelStore(createFileConfigStorage(aiDataDir)),
4613
+ customProviderStore: new CustomProviderStore(configStorage),
4614
+ customModelStore: new CustomModelStore(configStorage),
4390
4615
  runAgentStream: createCliRunAgentStream(dbManager, aiChatManager, semanticIndexService)
4391
4616
  }
4392
4617
  });
@@ -4428,8 +4653,8 @@ async function stopHttpServer() {
4428
4653
 
4429
4654
  export {
4430
4655
  createServer,
4431
- registerWebRoutes,
4656
+ registerWebRoutes2 as registerWebRoutes,
4432
4657
  startHttpServer,
4433
4658
  stopHttpServer
4434
4659
  };
4435
- //# sourceMappingURL=chunk-JH5RAJT2.mjs.map
4660
+ //# sourceMappingURL=chunk-67HNEIKY.mjs.map