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