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