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