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