chatlab-cli 0.23.1 → 0.24.0
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/{chunk-643S2N2U.mjs → chunk-36W2HWQN.mjs} +2 -2
- package/dist/{chunk-AHQQQ5HZ.mjs → chunk-A6JCL4PT.mjs} +26 -14
- package/dist/chunk-A6JCL4PT.mjs.map +1 -0
- package/dist/{chunk-WTQ7LAZR.mjs → chunk-KCBIBRHL.mjs} +2 -2
- package/dist/{chunk-L37WJ33K.mjs → chunk-NE4A6PND.mjs} +1905 -1914
- package/dist/chunk-NE4A6PND.mjs.map +1 -0
- package/dist/{chunk-3MX7VF2B.mjs → chunk-RLCFXTTS.mjs} +2 -2
- package/dist/{chunk-G7Q7EQ3V.mjs → chunk-XJJXSOLO.mjs} +6199 -6988
- package/dist/chunk-XJJXSOLO.mjs.map +1 -0
- package/dist/cli.mjs +2 -2
- package/dist/{http-JDORENHT.mjs → http-OPQ352FZ.mjs} +6 -14
- package/dist/{import-LCBSBLYF.mjs → import-GZMDLK2E.mjs} +4 -4
- package/dist/index.mjs +8 -8
- package/dist/{mcp-AR7HBIK4.mjs → mcp-SHZ2CVXY.mjs} +3 -3
- package/dist/{service-WCGGRZTA.mjs → service-KZTHBDQ5.mjs} +3 -1
- package/dist/service-KZTHBDQ5.mjs.map +1 -0
- package/dist/{stream-import-QM5R5FDG.mjs → stream-import-RDDB3N7B.mjs} +3 -3
- package/dist-web/assets/{DebugToolsPanel-Yzfz2ntn.css → DebugToolsPanel-DaSNc1o-.css} +1 -1
- package/dist-web/assets/DebugToolsPanel.vue_vue_type_script_setup_true_lang-_gbTn1ha.js +161 -0
- package/dist-web/assets/{EChartWordcloud-Cenbzcyj.js → EChartWordcloud-VpNJDurs.js} +1 -1
- package/dist-web/assets/{MessageExportModal-DMHA4X0b.js → MessageExportModal-n4JgNg_F.js} +1 -1
- package/dist-web/assets/electron-BIWBW8k_.js +1 -0
- package/dist-web/assets/electron-Bt9dK4Sr.js +1 -0
- package/dist-web/assets/fetch-BOevuRK_.js +1 -0
- package/dist-web/assets/fetch-BhsrPRzn.js +3 -0
- package/dist-web/assets/fetch-ChECfwh7.js +1 -0
- package/dist-web/assets/fetch-ClDGP9DK.js +1 -0
- package/dist-web/assets/{fetch-CvfdN4gM.js → fetch-CnyupWWc.js} +1 -1
- package/dist-web/assets/fetch-CvaXKzA-.js +1 -0
- package/dist-web/assets/fetch-D1_jL6PO.js +1 -0
- package/dist-web/assets/fetch-DFWZ48Xw.js +1 -0
- package/dist-web/assets/fetch-DKD1bKt2.js +1 -0
- package/dist-web/assets/fetch-DwPB8NOH.js +1 -0
- package/dist-web/assets/index-BN79SJSr.js +173 -0
- package/dist-web/assets/index-BVQsKKwg.js +1 -0
- package/dist-web/assets/{index-C2439065.js → index-Bzg8BftZ.js} +6 -6
- package/dist-web/assets/{index-DC0LjKV6.js → index-C-5bP7_E.js} +2 -2
- package/dist-web/assets/index-C3A_Rjh-.css +1 -0
- package/dist-web/assets/{index-BUfibEyG.css → index-CGNgUIT-.css} +1 -1
- package/dist-web/assets/index-D6ga3hbF.js +163 -0
- package/dist-web/assets/index-ESCx9pRR.js +8 -0
- package/dist-web/assets/index-JbR9Q-bb.css +1 -0
- package/dist-web/assets/vendor-nuxt-ui-BLMzqeB8.js +15 -0
- package/dist-web/assets/{vendor-reka-ui-BrTYuIfH.js → vendor-reka-ui-CnB0mMNX.js} +1 -1
- package/dist-web/assets/web-DtlveK3q.js +1 -0
- package/dist-web/index.html +4 -4
- package/package.json +2 -1
- package/dist/chunk-AHQQQ5HZ.mjs.map +0 -1
- package/dist/chunk-G7Q7EQ3V.mjs.map +0 -1
- package/dist/chunk-L37WJ33K.mjs.map +0 -1
- package/dist/service-WCGGRZTA.mjs.map +0 -1
- package/dist/src-EGXULYFQ.mjs +0 -340
- package/dist/stream-import-QM5R5FDG.mjs.map +0 -1
- package/dist-web/assets/DebugToolsPanel.vue_vue_type_script_setup_true_lang-DXxg55kL.js +0 -161
- package/dist-web/assets/electron-DPsrYwTt.js +0 -1
- package/dist-web/assets/electron-DfpXsXsS.js +0 -1
- package/dist-web/assets/electron-DpE69lO4.js +0 -1
- package/dist-web/assets/electron-HoVzpIVc.js +0 -1
- package/dist-web/assets/electron-SyPZX9OU.js +0 -1
- package/dist-web/assets/fetch-B7OBmANJ.js +0 -1
- package/dist-web/assets/fetch-BikSxuED.js +0 -3
- package/dist-web/assets/fetch-BwMZFwbF.js +0 -1
- package/dist-web/assets/fetch-BxE8RQYE.js +0 -1
- package/dist-web/assets/fetch-LBHcBBkv.js +0 -1
- package/dist-web/assets/http-C741fIwb.js +0 -1
- package/dist-web/assets/index-CD5Fz-JY.js +0 -7
- package/dist-web/assets/index-Dl-X-izi.css +0 -1
- package/dist-web/assets/index-DywUneNT.css +0 -1
- package/dist-web/assets/index-JvmxkAGk.js +0 -173
- package/dist-web/assets/index-vEVpfw-R.js +0 -163
- package/dist-web/assets/sse-CACcGGWv.js +0 -2
- package/dist-web/assets/vendor-nuxt-ui-3XQAyMd0.js +0 -15
- package/dist-web/assets/web-Bm94rL5B.js +0 -1
- /package/dist/{chunk-643S2N2U.mjs.map → chunk-36W2HWQN.mjs.map} +0 -0
- /package/dist/{chunk-WTQ7LAZR.mjs.map → chunk-KCBIBRHL.mjs.map} +0 -0
- /package/dist/{chunk-3MX7VF2B.mjs.map → chunk-RLCFXTTS.mjs.map} +0 -0
- /package/dist/{http-JDORENHT.mjs.map → http-OPQ352FZ.mjs.map} +0 -0
- /package/dist/{import-LCBSBLYF.mjs.map → import-GZMDLK2E.mjs.map} +0 -0
- /package/dist/{mcp-AR7HBIK4.mjs.map → mcp-SHZ2CVXY.mjs.map} +0 -0
- /package/dist/{src-EGXULYFQ.mjs.map → stream-import-RDDB3N7B.mjs.map} +0 -0
|
@@ -7,17 +7,17 @@ const __dirname = __pathDirname(__filename);
|
|
|
7
7
|
import {
|
|
8
8
|
importData,
|
|
9
9
|
parseFile
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-RLCFXTTS.mjs";
|
|
11
11
|
import {
|
|
12
12
|
analyzeIncrementalImport,
|
|
13
13
|
analyzeNewImport,
|
|
14
14
|
incrementalImport,
|
|
15
15
|
streamImport
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-36W2HWQN.mjs";
|
|
17
17
|
import {
|
|
18
18
|
AGENT_TOOL_REGISTRY,
|
|
19
19
|
CoreDataProvider
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-KCBIBRHL.mjs";
|
|
21
21
|
import {
|
|
22
22
|
getVersion
|
|
23
23
|
} from "./chunk-5ADKYYNA.mjs";
|
|
@@ -30,14 +30,19 @@ import {
|
|
|
30
30
|
BUILTIN_MODELS,
|
|
31
31
|
BUILTIN_PROVIDERS,
|
|
32
32
|
BUILTIN_TOOL_CATALOG,
|
|
33
|
+
CustomModelStore,
|
|
34
|
+
CustomProviderStore,
|
|
33
35
|
DatabaseManager,
|
|
36
|
+
LLMConfigStore,
|
|
37
|
+
MergeSessionCache,
|
|
34
38
|
MigrationRunner,
|
|
35
39
|
NodePathProvider,
|
|
36
40
|
POS_TAG_DEFINITIONS,
|
|
37
41
|
PreferencesManager,
|
|
38
42
|
SkillManager,
|
|
39
43
|
SkillManagerCore,
|
|
40
|
-
|
|
44
|
+
TempDbWriter,
|
|
45
|
+
applyPendingNodeDataDirMigrationIfNeeded,
|
|
41
46
|
applyPreprocessingPipeline,
|
|
42
47
|
batchSegmentWithFrequency,
|
|
43
48
|
buildMergedOutput,
|
|
@@ -45,15 +50,15 @@ import {
|
|
|
45
50
|
buildSystemPrompt,
|
|
46
51
|
checkAndCompress,
|
|
47
52
|
checkConflictsFromSources,
|
|
48
|
-
cleanupTempDatabases,
|
|
49
53
|
computeWordFrequency,
|
|
54
|
+
countMessagesTokens,
|
|
50
55
|
createActivateSkillTool,
|
|
51
56
|
createAiTranslate,
|
|
52
57
|
createCompressionLlmAdapter,
|
|
53
58
|
createDatabaseManagerAdapter,
|
|
54
59
|
createJiebaNlpProvider,
|
|
60
|
+
createNodeDataDirSwitch,
|
|
55
61
|
deleteDict,
|
|
56
|
-
deleteTempDatabase,
|
|
57
62
|
detectAllFormats,
|
|
58
63
|
detectFormat,
|
|
59
64
|
downloadDict,
|
|
@@ -63,6 +68,7 @@ import {
|
|
|
63
68
|
exportSessionToJson,
|
|
64
69
|
export_service_exports,
|
|
65
70
|
fetchRemoteModels,
|
|
71
|
+
filterMessagesWithContext,
|
|
66
72
|
findEntryFileInDirectory,
|
|
67
73
|
formatAIError,
|
|
68
74
|
getAvailableYears,
|
|
@@ -71,6 +77,8 @@ import {
|
|
|
71
77
|
getChatOverview,
|
|
72
78
|
getClusterGraph,
|
|
73
79
|
getDailyActivity,
|
|
80
|
+
getDefaultNodeUserDataDir,
|
|
81
|
+
getDefaultRulesForLocale,
|
|
74
82
|
getDictList,
|
|
75
83
|
getHourlyActivity,
|
|
76
84
|
getLanguagePreferenceAnalysis,
|
|
@@ -83,6 +91,8 @@ import {
|
|
|
83
91
|
getMessageLengthDistribution,
|
|
84
92
|
getMessageTypeStats,
|
|
85
93
|
getMonthlyActivity,
|
|
94
|
+
getMultipleSessionsMessages,
|
|
95
|
+
getPendingNodeDataDirMigration,
|
|
86
96
|
getRelationshipStats,
|
|
87
97
|
getSchemaDetailed,
|
|
88
98
|
getSessionInfo,
|
|
@@ -94,9 +104,11 @@ import {
|
|
|
94
104
|
getYearlyActivity,
|
|
95
105
|
hasPendingElectronDataWarning,
|
|
96
106
|
initNlpDir,
|
|
107
|
+
initTokenizer,
|
|
97
108
|
isDictDownloaded,
|
|
98
109
|
loadConfig,
|
|
99
110
|
member_service_exports,
|
|
111
|
+
mergeRulesForLocale,
|
|
100
112
|
openBetterSqliteDatabase,
|
|
101
113
|
parseAssistantFile,
|
|
102
114
|
queryMessages,
|
|
@@ -109,27 +121,25 @@ import {
|
|
|
109
121
|
session_index_service_exports,
|
|
110
122
|
session_service_exports,
|
|
111
123
|
streamParseFileInfo,
|
|
124
|
+
stripAvatarFields,
|
|
112
125
|
summary_service_exports,
|
|
113
126
|
validateApiKey,
|
|
114
127
|
verifyCliDataPath,
|
|
115
128
|
writeAuthProfile,
|
|
116
129
|
writeConfigField
|
|
117
|
-
} from "./chunk-
|
|
130
|
+
} from "./chunk-XJJXSOLO.mjs";
|
|
118
131
|
import {
|
|
119
132
|
resolveCliPath
|
|
120
133
|
} from "./chunk-FJ35V3TE.mjs";
|
|
121
134
|
|
|
122
135
|
// src/http/index.ts
|
|
123
|
-
import * as
|
|
124
|
-
import * as
|
|
136
|
+
import * as fs11 from "fs";
|
|
137
|
+
import * as crypto4 from "crypto";
|
|
125
138
|
|
|
126
139
|
// src/http/server.ts
|
|
127
140
|
import Fastify from "fastify";
|
|
128
141
|
|
|
129
|
-
//
|
|
130
|
-
import { timingSafeEqual } from "crypto";
|
|
131
|
-
|
|
132
|
-
// src/http/errors.ts
|
|
142
|
+
// ../../packages/http-routes/src/errors.ts
|
|
133
143
|
var HTTP_STATUS = {
|
|
134
144
|
["UNAUTHORIZED" /* UNAUTHORIZED */]: 401,
|
|
135
145
|
["SESSION_NOT_FOUND" /* SESSION_NOT_FOUND */]: 404,
|
|
@@ -190,83 +200,17 @@ function errorResponse(error) {
|
|
|
190
200
|
};
|
|
191
201
|
}
|
|
192
202
|
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
var webModeEnabled = false;
|
|
196
|
-
function setAuthToken(token) {
|
|
197
|
-
cachedToken = token;
|
|
198
|
-
}
|
|
199
|
-
function setWebMode(enabled) {
|
|
200
|
-
webModeEnabled = enabled;
|
|
201
|
-
}
|
|
202
|
-
function safeTokenCompare(a, b) {
|
|
203
|
-
const bufA = Buffer.from(a);
|
|
204
|
-
const bufB = Buffer.from(b);
|
|
205
|
-
if (bufA.length !== bufB.length) return false;
|
|
206
|
-
return timingSafeEqual(bufA, bufB);
|
|
207
|
-
}
|
|
208
|
-
async function authHook(request, reply) {
|
|
209
|
-
if (!cachedToken) return;
|
|
210
|
-
if (request.url.startsWith("/_web/")) return;
|
|
211
|
-
if (webModeEnabled && !request.url.startsWith("/api/")) return;
|
|
212
|
-
return requireBearerToken(request, reply);
|
|
213
|
-
}
|
|
214
|
-
function requireBearerToken(request, reply) {
|
|
215
|
-
if (!cachedToken) return;
|
|
216
|
-
const authHeader = request.headers.authorization;
|
|
217
|
-
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
218
|
-
const err = unauthorized();
|
|
219
|
-
reply.code(err.statusCode).send(errorResponse(err));
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
const token = authHeader.slice(7);
|
|
223
|
-
if (!safeTokenCompare(token, cachedToken)) {
|
|
224
|
-
const err = unauthorized();
|
|
225
|
-
reply.code(err.statusCode).send(errorResponse(err));
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// src/http/server.ts
|
|
231
|
-
var JSON_BODY_LIMIT = 50 * 1024 * 1024;
|
|
232
|
-
function createServer() {
|
|
233
|
-
const server2 = Fastify({
|
|
234
|
-
logger: false,
|
|
235
|
-
bodyLimit: JSON_BODY_LIMIT
|
|
236
|
-
});
|
|
237
|
-
server2.addHook("onRequest", authHook);
|
|
238
|
-
server2.setErrorHandler((error, _request, reply) => {
|
|
239
|
-
if (error instanceof ApiError) {
|
|
240
|
-
reply.code(error.statusCode).send(errorResponse(error));
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
if (error.statusCode === 413) {
|
|
244
|
-
const bodyErr = new ApiError("BODY_TOO_LARGE" /* BODY_TOO_LARGE */, "Request body exceeds 50MB limit");
|
|
245
|
-
reply.code(413).send(errorResponse(bodyErr));
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
const statusCode = error.statusCode;
|
|
249
|
-
if (statusCode && statusCode >= 400 && statusCode < 600) {
|
|
250
|
-
reply.code(statusCode).send({ success: false, error: { code: "CLIENT_ERROR", message: error.message } });
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
const err = serverError(error.message);
|
|
254
|
-
reply.code(err.statusCode).send(errorResponse(err));
|
|
255
|
-
});
|
|
256
|
-
return server2;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// src/http/routes/system.ts
|
|
260
|
-
function registerSystemRoutes(server2, dbManager2) {
|
|
203
|
+
// ../../packages/http-routes/src/routes/system.ts
|
|
204
|
+
function registerSystemRoutes(server2, ctx) {
|
|
261
205
|
server2.get("/api/v1/status", async () => {
|
|
262
206
|
let sessionCount = 0;
|
|
263
207
|
try {
|
|
264
|
-
sessionCount =
|
|
208
|
+
sessionCount = ctx.dbManager.listSessionIds().length;
|
|
265
209
|
} catch {
|
|
266
210
|
}
|
|
267
211
|
return successResponse({
|
|
268
212
|
name: "ChatLab API",
|
|
269
|
-
version: getVersion(),
|
|
213
|
+
version: ctx.getVersion(),
|
|
270
214
|
uptime: Math.floor(process.uptime()),
|
|
271
215
|
sessionCount
|
|
272
216
|
});
|
|
@@ -333,18 +277,18 @@ function registerSystemRoutes(server2, dbManager2) {
|
|
|
333
277
|
});
|
|
334
278
|
}
|
|
335
279
|
|
|
336
|
-
//
|
|
280
|
+
// ../../packages/http-routes/src/routes/sessions.ts
|
|
337
281
|
var EXPORT_MESSAGE_LIMIT = 1e5;
|
|
338
|
-
function ensureDb(
|
|
339
|
-
const db =
|
|
282
|
+
function ensureDb(ctx, sessionId) {
|
|
283
|
+
const db = ctx.dbManager.open(sessionId);
|
|
340
284
|
if (!db) throw sessionNotFound(sessionId);
|
|
341
285
|
return db;
|
|
342
286
|
}
|
|
343
|
-
function
|
|
287
|
+
function registerRestSessionRoutes(server2, ctx) {
|
|
344
288
|
server2.get("/api/v1/sessions", async () => {
|
|
345
|
-
const sessionIds =
|
|
289
|
+
const sessionIds = ctx.dbManager.listSessionIds();
|
|
346
290
|
const sessions = sessionIds.map((id) => {
|
|
347
|
-
const db =
|
|
291
|
+
const db = ctx.dbManager.open(id);
|
|
348
292
|
if (!db) return null;
|
|
349
293
|
const info = getSessionInfo(db);
|
|
350
294
|
if (!info) return null;
|
|
@@ -363,7 +307,7 @@ function registerSessionRoutes(server2, dbManager2) {
|
|
|
363
307
|
return successResponse(sessions);
|
|
364
308
|
});
|
|
365
309
|
server2.get("/api/v1/sessions/:id", async (request) => {
|
|
366
|
-
const db = ensureDb(
|
|
310
|
+
const db = ensureDb(ctx, request.params.id);
|
|
367
311
|
const info = getSessionInfo(db);
|
|
368
312
|
if (!info) throw sessionNotFound(request.params.id);
|
|
369
313
|
return successResponse({
|
|
@@ -380,7 +324,7 @@ function registerSessionRoutes(server2, dbManager2) {
|
|
|
380
324
|
});
|
|
381
325
|
server2.get("/api/v1/sessions/:id/messages", async (request) => {
|
|
382
326
|
const { id } = request.params;
|
|
383
|
-
const db = ensureDb(
|
|
327
|
+
const db = ensureDb(ctx, id);
|
|
384
328
|
const page = Math.max(1, parseInt(request.query.page || "1", 10) || 1);
|
|
385
329
|
const limit = Math.min(1e3, Math.max(1, parseInt(request.query.limit || "100", 10) || 100));
|
|
386
330
|
const offset = (page - 1) * limit;
|
|
@@ -402,13 +346,13 @@ function registerSessionRoutes(server2, dbManager2) {
|
|
|
402
346
|
});
|
|
403
347
|
});
|
|
404
348
|
server2.get("/api/v1/sessions/:id/members", async (request) => {
|
|
405
|
-
const db = ensureDb(
|
|
349
|
+
const db = ensureDb(ctx, request.params.id);
|
|
406
350
|
const members = getMembers(db);
|
|
407
351
|
return successResponse(members);
|
|
408
352
|
});
|
|
409
353
|
server2.get("/api/v1/sessions/:id/stats/overview", async (request) => {
|
|
410
354
|
const { id } = request.params;
|
|
411
|
-
const db = ensureDb(
|
|
355
|
+
const db = ensureDb(ctx, id);
|
|
412
356
|
const overview = getSessionOverview(db);
|
|
413
357
|
const memberActivity = getMemberActivity(db);
|
|
414
358
|
const typeDistribution = getMessageTypeStats(db);
|
|
@@ -435,7 +379,7 @@ function registerSessionRoutes(server2, dbManager2) {
|
|
|
435
379
|
});
|
|
436
380
|
server2.post("/api/v1/sessions/:id/sql", async (request, reply) => {
|
|
437
381
|
const { id } = request.params;
|
|
438
|
-
const db = ensureDb(
|
|
382
|
+
const db = ensureDb(ctx, id);
|
|
439
383
|
const { sql } = request.body || {};
|
|
440
384
|
if (!sql || typeof sql !== "string") {
|
|
441
385
|
const err = sqlExecutionError("Missing sql parameter");
|
|
@@ -457,7 +401,7 @@ function registerSessionRoutes(server2, dbManager2) {
|
|
|
457
401
|
});
|
|
458
402
|
server2.get("/api/v1/sessions/:id/export", async (request, reply) => {
|
|
459
403
|
const { id } = request.params;
|
|
460
|
-
const db = ensureDb(
|
|
404
|
+
const db = ensureDb(ctx, id);
|
|
461
405
|
const meta = getSessionMeta(db);
|
|
462
406
|
if (!meta) throw sessionNotFound(id);
|
|
463
407
|
const overview = getSessionOverview(db);
|
|
@@ -496,86 +440,9 @@ function registerSessionRoutes(server2, dbManager2) {
|
|
|
496
440
|
});
|
|
497
441
|
}
|
|
498
442
|
|
|
499
|
-
//
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
// src/merger/merge-cache.ts
|
|
504
|
-
import * as fs from "fs";
|
|
505
|
-
import * as path from "path";
|
|
506
|
-
import * as crypto from "crypto";
|
|
507
|
-
var MergeSessionCache = class {
|
|
508
|
-
cache = /* @__PURE__ */ new Map();
|
|
509
|
-
tempDir;
|
|
510
|
-
nativeBinding;
|
|
511
|
-
constructor(pathProvider, options) {
|
|
512
|
-
this.tempDir = pathProvider.getTempDir();
|
|
513
|
-
this.nativeBinding = options?.nativeBinding;
|
|
514
|
-
if (!fs.existsSync(this.tempDir)) {
|
|
515
|
-
fs.mkdirSync(this.tempDir, { recursive: true });
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
generateTempDbPath(filename) {
|
|
519
|
-
const ts = Date.now();
|
|
520
|
-
const rand = Math.random().toString(36).slice(2, 8);
|
|
521
|
-
const safeName = filename.replace(/[/\\?%*:|"<>]/g, "_").slice(0, 40);
|
|
522
|
-
return path.join(this.tempDir, `merge_${safeName}_${ts}_${rand}.db`);
|
|
523
|
-
}
|
|
524
|
-
/** Store a parsed file's temp DB and return a handle for subsequent requests. */
|
|
525
|
-
store(filename, tempDbPath) {
|
|
526
|
-
const handle = crypto.randomUUID();
|
|
527
|
-
this.cache.set(handle, { tempDbPath, filename, createdAt: Date.now() });
|
|
528
|
-
return handle;
|
|
529
|
-
}
|
|
530
|
-
/** Open a TempDbReader for a given handle. Caller must close the reader. */
|
|
531
|
-
openReader(handle) {
|
|
532
|
-
const entry = this.cache.get(handle);
|
|
533
|
-
if (!entry || !fs.existsSync(entry.tempDbPath)) return null;
|
|
534
|
-
const adapter = openBetterSqliteDatabase(entry.tempDbPath, {
|
|
535
|
-
readonly: true,
|
|
536
|
-
nativeBinding: this.nativeBinding
|
|
537
|
-
});
|
|
538
|
-
return { reader: new TempDbReader(adapter), filename: entry.filename };
|
|
539
|
-
}
|
|
540
|
-
/** Create a writable temp DB for streaming parse. */
|
|
541
|
-
createTempDatabase(filename) {
|
|
542
|
-
const tempDbPath = this.generateTempDbPath(filename);
|
|
543
|
-
const adapter = openBetterSqliteDatabase(tempDbPath, { nativeBinding: this.nativeBinding });
|
|
544
|
-
return { db: adapter, tempDbPath };
|
|
545
|
-
}
|
|
546
|
-
/** Remove a single entry and its temp DB file. */
|
|
547
|
-
delete(handle) {
|
|
548
|
-
const entry = this.cache.get(handle);
|
|
549
|
-
if (entry) {
|
|
550
|
-
deleteTempDatabase(entry.tempDbPath);
|
|
551
|
-
this.cache.delete(handle);
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
/** Clear all entries and delete their temp DB files. */
|
|
555
|
-
clear() {
|
|
556
|
-
for (const entry of this.cache.values()) {
|
|
557
|
-
deleteTempDatabase(entry.tempDbPath);
|
|
558
|
-
}
|
|
559
|
-
this.cache.clear();
|
|
560
|
-
}
|
|
561
|
-
/** Cleanup orphan temp DBs from previous runs. */
|
|
562
|
-
cleanupOrphans() {
|
|
563
|
-
cleanupTempDatabases(this.tempDir);
|
|
564
|
-
}
|
|
565
|
-
/** Auto-cleanup entries older than maxAge (default 1 hour). */
|
|
566
|
-
cleanupExpired(maxAgeMs = 36e5) {
|
|
567
|
-
const now = Date.now();
|
|
568
|
-
for (const [handle, entry] of this.cache) {
|
|
569
|
-
if (now - entry.createdAt > maxAgeMs) {
|
|
570
|
-
deleteTempDatabase(entry.tempDbPath);
|
|
571
|
-
this.cache.delete(handle);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
};
|
|
576
|
-
|
|
577
|
-
// src/http/routes/web/sessions.ts
|
|
578
|
-
function registerSessionRoutes2(server2, adapter) {
|
|
443
|
+
// ../../packages/http-routes/src/routes/web/sessions.ts
|
|
444
|
+
function registerSessionRoutes(server2, ctx) {
|
|
445
|
+
const { sessionAdapter: adapter } = ctx;
|
|
579
446
|
server2.get("/_web/sessions", async () => {
|
|
580
447
|
return session_service_exports.listAnalysisSessions(adapter);
|
|
581
448
|
});
|
|
@@ -611,8 +478,9 @@ function registerSessionRoutes2(server2, adapter) {
|
|
|
611
478
|
);
|
|
612
479
|
}
|
|
613
480
|
|
|
614
|
-
//
|
|
615
|
-
function registerMemberRoutes(server2,
|
|
481
|
+
// ../../packages/http-routes/src/routes/web/members.ts
|
|
482
|
+
function registerMemberRoutes(server2, ctx) {
|
|
483
|
+
const { sessionAdapter: adapter } = ctx;
|
|
616
484
|
server2.get("/_web/sessions/:id/members", async (request) => {
|
|
617
485
|
return member_service_exports.getMembers(adapter, request.params.id);
|
|
618
486
|
});
|
|
@@ -657,8 +525,46 @@ function registerMemberRoutes(server2, adapter) {
|
|
|
657
525
|
);
|
|
658
526
|
}
|
|
659
527
|
|
|
660
|
-
//
|
|
661
|
-
|
|
528
|
+
// ../../packages/http-routes/src/routes/web/preferences.ts
|
|
529
|
+
function registerPreferencesRoutes(server2, ctx) {
|
|
530
|
+
const prefManager = ctx.preferencesManager ?? new PreferencesManager(ctx.pathProvider.getSystemDir());
|
|
531
|
+
server2.get("/_web/preferences", async () => {
|
|
532
|
+
return prefManager.load();
|
|
533
|
+
});
|
|
534
|
+
server2.patch("/_web/preferences", async (request) => {
|
|
535
|
+
return prefManager.save(request.body);
|
|
536
|
+
});
|
|
537
|
+
server2.get("/_web/preferences/ui-config", async () => {
|
|
538
|
+
const config = loadConfig();
|
|
539
|
+
return config.ui;
|
|
540
|
+
});
|
|
541
|
+
server2.patch("/_web/preferences/ui-config", async (request) => {
|
|
542
|
+
try {
|
|
543
|
+
for (const [key, value] of Object.entries(request.body)) {
|
|
544
|
+
if (value !== void 0) {
|
|
545
|
+
writeConfigField("ui", key, value);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return { success: true };
|
|
549
|
+
} catch (err) {
|
|
550
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
server2.get("/_web/preferences/locale", async () => {
|
|
554
|
+
const config = loadConfig();
|
|
555
|
+
return { lang: config.locale.lang };
|
|
556
|
+
});
|
|
557
|
+
server2.patch("/_web/preferences/locale", async (request) => {
|
|
558
|
+
try {
|
|
559
|
+
writeConfigField("locale", "lang", request.body.lang);
|
|
560
|
+
return { success: true };
|
|
561
|
+
} catch (err) {
|
|
562
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ../../packages/http-routes/src/helpers.ts
|
|
662
568
|
function parseTimeFilter(query) {
|
|
663
569
|
const { startTs, endTs, memberId } = query;
|
|
664
570
|
if (!startTs && !endTs && !memberId) return void 0;
|
|
@@ -668,29 +574,10 @@ function parseTimeFilter(query) {
|
|
|
668
574
|
if (memberId) filter.memberId = parseInt(memberId, 10);
|
|
669
575
|
return filter;
|
|
670
576
|
}
|
|
671
|
-
function resolveNativeBinding() {
|
|
672
|
-
if (process.versions.electron) return void 0;
|
|
673
|
-
const nativePath = resolveCliPath("native/better_sqlite3.node");
|
|
674
|
-
if (fs2.existsSync(nativePath)) return nativePath;
|
|
675
|
-
return void 0;
|
|
676
|
-
}
|
|
677
|
-
function getAiDataDir(dbManager2) {
|
|
678
|
-
const pathProvider = dbManager2["pathProvider"];
|
|
679
|
-
if (!pathProvider) {
|
|
680
|
-
throw Object.assign(new Error("PathProvider not available"), { statusCode: 500 });
|
|
681
|
-
}
|
|
682
|
-
return pathProvider.getAiDataDir();
|
|
683
|
-
}
|
|
684
|
-
function ensureDb2(dbManager2, sessionId) {
|
|
685
|
-
const db = dbManager2.open(sessionId);
|
|
686
|
-
if (!db) {
|
|
687
|
-
throw Object.assign(new Error(`Session not found: ${sessionId}`), { statusCode: 404 });
|
|
688
|
-
}
|
|
689
|
-
return db;
|
|
690
|
-
}
|
|
691
577
|
|
|
692
|
-
//
|
|
693
|
-
function registerAnalyticsRoutes(server2,
|
|
578
|
+
// ../../packages/http-routes/src/routes/web/analytics.ts
|
|
579
|
+
function registerAnalyticsRoutes(server2, ctx) {
|
|
580
|
+
const { sessionAdapter: adapter } = ctx;
|
|
694
581
|
server2.get("/_web/sessions/:id/years", async (request) => {
|
|
695
582
|
const db = adapter.ensureReadonly(request.params.id);
|
|
696
583
|
return getAvailableYears(db);
|
|
@@ -811,8 +698,9 @@ function registerAnalyticsRoutes(server2, _dbManager, adapter) {
|
|
|
811
698
|
);
|
|
812
699
|
}
|
|
813
700
|
|
|
814
|
-
//
|
|
815
|
-
function registerSqlRoutes(server2,
|
|
701
|
+
// ../../packages/http-routes/src/routes/web/sql.ts
|
|
702
|
+
function registerSqlRoutes(server2, ctx) {
|
|
703
|
+
const { sessionAdapter: adapter } = ctx;
|
|
816
704
|
server2.post("/_web/sessions/:id/sql", async (request, reply) => {
|
|
817
705
|
const db = adapter.ensureReadonly(request.params.id);
|
|
818
706
|
const { sql } = request.body || {};
|
|
@@ -847,8 +735,9 @@ function registerSqlRoutes(server2, adapter) {
|
|
|
847
735
|
});
|
|
848
736
|
}
|
|
849
737
|
|
|
850
|
-
//
|
|
851
|
-
function registerSessionIndexRoutes(server2,
|
|
738
|
+
// ../../packages/http-routes/src/routes/web/session-index.ts
|
|
739
|
+
function registerSessionIndexRoutes(server2, ctx) {
|
|
740
|
+
const { sessionAdapter: adapter } = ctx;
|
|
852
741
|
server2.get("/_web/sessions/index-stats", async () => {
|
|
853
742
|
return session_index_service_exports.getAllIndexStats(adapter);
|
|
854
743
|
});
|
|
@@ -885,586 +774,715 @@ function registerSessionIndexRoutes(server2, adapter) {
|
|
|
885
774
|
});
|
|
886
775
|
}
|
|
887
776
|
|
|
888
|
-
// src/
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
const fallbackKey = rawKey.startsWith("enc:") ? "" : rawKey;
|
|
905
|
-
return {
|
|
906
|
-
...c,
|
|
907
|
-
apiKey: profileKey || fallbackKey
|
|
908
|
-
};
|
|
909
|
-
});
|
|
910
|
-
return {
|
|
911
|
-
configs,
|
|
912
|
-
defaultAssistant: data.defaultAssistant ?? null,
|
|
913
|
-
fastModel: data.fastModel ?? null
|
|
914
|
-
};
|
|
915
|
-
} catch {
|
|
916
|
-
return { configs: [], defaultAssistant: null, fastModel: null };
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
function resolveSlot(slot, configs) {
|
|
920
|
-
if (slot && configs.some((c) => c.id === slot.configId)) return slot;
|
|
921
|
-
const fallback = configs[0];
|
|
922
|
-
if (!fallback) return null;
|
|
923
|
-
return { configId: fallback.id, modelId: fallback.model };
|
|
924
|
-
}
|
|
925
|
-
function getDefaultAssistantConfig(aiDataDir) {
|
|
926
|
-
const store = loadLlmConfig(aiDataDir);
|
|
927
|
-
const slot = resolveSlot(store.defaultAssistant, store.configs);
|
|
928
|
-
if (!slot) return null;
|
|
929
|
-
const config = store.configs.find((c) => c.id === slot.configId);
|
|
930
|
-
if (!config) return null;
|
|
931
|
-
return { ...config, model: slot.modelId || config.model };
|
|
932
|
-
}
|
|
933
|
-
var MAX_CONFIG_COUNT = 99;
|
|
934
|
-
function saveLlmConfig(aiDataDir, store) {
|
|
935
|
-
const configPath = path2.join(aiDataDir, "llm-config.json");
|
|
936
|
-
const toSave = {
|
|
937
|
-
...store,
|
|
938
|
-
configs: store.configs.map((c) => {
|
|
939
|
-
const { apiKey: _k, ...rest } = c;
|
|
940
|
-
return rest;
|
|
941
|
-
}),
|
|
942
|
-
schemaVersion: 3
|
|
943
|
-
};
|
|
944
|
-
fs3.writeFileSync(configPath, JSON.stringify(toSave, null, 2), "utf-8");
|
|
945
|
-
}
|
|
946
|
-
function addLlmConfig(aiDataDir, config) {
|
|
947
|
-
const store = loadLlmConfig(aiDataDir);
|
|
948
|
-
if (store.configs.length >= MAX_CONFIG_COUNT) {
|
|
949
|
-
return { success: false, error: `Maximum ${MAX_CONFIG_COUNT} configs reached` };
|
|
950
|
-
}
|
|
951
|
-
const newConfig = {
|
|
952
|
-
...config,
|
|
953
|
-
id: randomUUID2()
|
|
954
|
-
};
|
|
955
|
-
const storeForSave = loadRawConfigStore(aiDataDir);
|
|
956
|
-
storeForSave.configs.push({
|
|
957
|
-
...newConfig,
|
|
958
|
-
apiKey: ""
|
|
959
|
-
});
|
|
960
|
-
if (storeForSave.configs.length === 1) {
|
|
961
|
-
storeForSave.defaultAssistant = { configId: newConfig.id, modelId: newConfig.model || "" };
|
|
962
|
-
}
|
|
963
|
-
if (config.apiKey) {
|
|
964
|
-
const profileName = config.name?.toLowerCase().replace(/\s+/g, "-") || config.provider;
|
|
965
|
-
writeAuthProfile(profileName, {
|
|
966
|
-
type: "api_key",
|
|
967
|
-
provider: config.provider,
|
|
968
|
-
key: config.apiKey
|
|
969
|
-
});
|
|
970
|
-
storeForSave.configs[storeForSave.configs.length - 1].authProfile = profileName;
|
|
971
|
-
}
|
|
972
|
-
saveLlmConfig(aiDataDir, storeForSave);
|
|
973
|
-
return { success: true, config: { ...newConfig, apiKey: "" } };
|
|
974
|
-
}
|
|
975
|
-
function updateLlmConfig(aiDataDir, id, updates) {
|
|
976
|
-
const storeForSave = loadRawConfigStore(aiDataDir);
|
|
977
|
-
const index = storeForSave.configs.findIndex((c) => c.id === id);
|
|
978
|
-
if (index === -1) {
|
|
979
|
-
return { success: false, error: "Config not found" };
|
|
980
|
-
}
|
|
981
|
-
const { apiKey: newApiKey, ...restUpdates } = updates;
|
|
982
|
-
const updated = {
|
|
983
|
-
...storeForSave.configs[index],
|
|
984
|
-
...restUpdates
|
|
985
|
-
};
|
|
986
|
-
storeForSave.configs[index] = updated;
|
|
987
|
-
if (newApiKey) {
|
|
988
|
-
const profileName = updated.name?.toLowerCase().replace(/\s+/g, "-") || updated.provider;
|
|
989
|
-
writeAuthProfile(profileName, {
|
|
990
|
-
type: "api_key",
|
|
991
|
-
provider: updated.provider,
|
|
992
|
-
key: newApiKey
|
|
777
|
+
// ../../packages/http-routes/src/routes/web/export.ts
|
|
778
|
+
function registerExportRoutes(server2, ctx) {
|
|
779
|
+
const { sessionAdapter: adapter } = ctx;
|
|
780
|
+
server2.post("/_web/sessions/:id/export/markdown", async (request, reply) => {
|
|
781
|
+
const { id } = request.params;
|
|
782
|
+
const body = request.body;
|
|
783
|
+
const sessionName = body?.sessionName || id;
|
|
784
|
+
const { result, content } = export_service_exports.exportMarkdown(adapter, {
|
|
785
|
+
sessionId: id,
|
|
786
|
+
sessionName,
|
|
787
|
+
filterMode: body.filterMode || "condition",
|
|
788
|
+
keywords: body.keywords,
|
|
789
|
+
timeFilter: body.timeFilter,
|
|
790
|
+
senderIds: body.senderIds,
|
|
791
|
+
contextSize: body.contextSize,
|
|
792
|
+
chatSessionIds: body.chatSessionIds
|
|
993
793
|
});
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
saveLlmConfig(aiDataDir, storeForSave);
|
|
997
|
-
return { success: true };
|
|
998
|
-
}
|
|
999
|
-
function deleteLlmConfig(aiDataDir, id) {
|
|
1000
|
-
const storeForSave = loadRawConfigStore(aiDataDir);
|
|
1001
|
-
const index = storeForSave.configs.findIndex((c) => c.id === id);
|
|
1002
|
-
if (index === -1) {
|
|
1003
|
-
return { success: false, error: "Config not found" };
|
|
1004
|
-
}
|
|
1005
|
-
storeForSave.configs.splice(index, 1);
|
|
1006
|
-
const fallback = storeForSave.configs[0];
|
|
1007
|
-
if (storeForSave.defaultAssistant?.configId === id) {
|
|
1008
|
-
storeForSave.defaultAssistant = fallback ? { configId: fallback.id, modelId: fallback.model || "" } : null;
|
|
1009
|
-
}
|
|
1010
|
-
if (storeForSave.fastModel?.configId === id) {
|
|
1011
|
-
storeForSave.fastModel = fallback ? { configId: fallback.id, modelId: fallback.model || "" } : null;
|
|
1012
|
-
}
|
|
1013
|
-
saveLlmConfig(aiDataDir, storeForSave);
|
|
1014
|
-
return { success: true };
|
|
1015
|
-
}
|
|
1016
|
-
function setDefaultAssistantSlot(aiDataDir, configId, modelId) {
|
|
1017
|
-
const storeForSave = loadRawConfigStore(aiDataDir);
|
|
1018
|
-
const config = storeForSave.configs.find((c) => c.id === configId);
|
|
1019
|
-
if (!config) {
|
|
1020
|
-
return { success: false, error: "Config not found" };
|
|
1021
|
-
}
|
|
1022
|
-
storeForSave.defaultAssistant = { configId, modelId };
|
|
1023
|
-
saveLlmConfig(aiDataDir, storeForSave);
|
|
1024
|
-
return { success: true };
|
|
1025
|
-
}
|
|
1026
|
-
function setFastModelSlot(aiDataDir, slot) {
|
|
1027
|
-
const storeForSave = loadRawConfigStore(aiDataDir);
|
|
1028
|
-
if (slot !== null) {
|
|
1029
|
-
const config = storeForSave.configs.find((c) => c.id === slot.configId);
|
|
1030
|
-
if (!config) {
|
|
1031
|
-
return { success: false, error: "Config not found" };
|
|
794
|
+
if (!result.success) {
|
|
795
|
+
return reply.code(500).send({ error: result.error });
|
|
1032
796
|
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
}
|
|
1038
|
-
function loadRawConfigStore(aiDataDir) {
|
|
1039
|
-
const configPath = path2.join(aiDataDir, "llm-config.json");
|
|
1040
|
-
if (!fs3.existsSync(configPath)) {
|
|
1041
|
-
return { configs: [], defaultAssistant: null, fastModel: null };
|
|
1042
|
-
}
|
|
1043
|
-
try {
|
|
1044
|
-
const data = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
|
|
1045
|
-
return {
|
|
1046
|
-
configs: data.configs || [],
|
|
1047
|
-
defaultAssistant: data.defaultAssistant ?? null,
|
|
1048
|
-
fastModel: data.fastModel ?? null
|
|
1049
|
-
};
|
|
1050
|
-
} catch {
|
|
1051
|
-
return { configs: [], defaultAssistant: null, fastModel: null };
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
function buildPiModel2(config) {
|
|
1055
|
-
return buildPiModel(config);
|
|
797
|
+
reply.header("Content-Type", "text/markdown; charset=utf-8");
|
|
798
|
+
reply.header("Content-Disposition", `attachment; filename="${encodeURIComponent(sessionName)}_export.md"`);
|
|
799
|
+
return reply.send(content);
|
|
800
|
+
});
|
|
1056
801
|
}
|
|
1057
802
|
|
|
1058
|
-
//
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
return buildPiModel2(config);
|
|
1067
|
-
}
|
|
1068
|
-
};
|
|
1069
|
-
}
|
|
1070
|
-
function registerSummaryRoutes(server2, dbManager2, adapter) {
|
|
1071
|
-
const deps = createSummaryDeps(dbManager2);
|
|
1072
|
-
server2.post("/_web/sessions/:id/summaries/generate", async (request, reply) => {
|
|
1073
|
-
const { chatSessionId, locale, forceRegenerate, strategy } = request.body;
|
|
1074
|
-
const result = await summary_service_exports.generateSummary(adapter, request.params.id, chatSessionId, deps, {
|
|
1075
|
-
locale,
|
|
1076
|
-
forceRegenerate,
|
|
1077
|
-
strategy
|
|
1078
|
-
});
|
|
1079
|
-
if ("error" in result && !result.success) {
|
|
1080
|
-
return reply.code(400).send({ error: result.error });
|
|
1081
|
-
}
|
|
1082
|
-
return result;
|
|
803
|
+
// ../../packages/http-routes/src/routes/web/nlp.ts
|
|
804
|
+
import * as path from "path";
|
|
805
|
+
function registerNlpRoutes(server2, ctx) {
|
|
806
|
+
const nlpDir = path.join(ctx.pathProvider.getSystemDir(), "nlp");
|
|
807
|
+
initNlpDir(nlpDir);
|
|
808
|
+
ensureDefaultDict(nlpDir).catch((err) => console.warn("[NLP] Auto-download zh-CN dict failed:", err));
|
|
809
|
+
server2.get("/_web/nlp/pos-tags", async () => {
|
|
810
|
+
return POS_TAG_DEFINITIONS;
|
|
1083
811
|
});
|
|
1084
|
-
server2.
|
|
1085
|
-
|
|
1086
|
-
const result = await summary_service_exports.generateAllSummaries(adapter, request.params.id, deps, {
|
|
1087
|
-
locale,
|
|
1088
|
-
forceRegenerate
|
|
1089
|
-
});
|
|
1090
|
-
if (result.error) {
|
|
1091
|
-
return reply.code(400).send({ error: result.error });
|
|
1092
|
-
}
|
|
1093
|
-
return result;
|
|
812
|
+
server2.get("/_web/nlp/dicts", async () => {
|
|
813
|
+
return getDictList(nlpDir);
|
|
1094
814
|
});
|
|
1095
|
-
server2.
|
|
1096
|
-
|
|
1097
|
-
return summary_service_exports.checkCanGenerate(adapter, request.params.id, chatSessionIds);
|
|
815
|
+
server2.get("/_web/nlp/dicts/:id/status", async (request) => {
|
|
816
|
+
return isDictDownloaded(nlpDir, request.params.id);
|
|
1098
817
|
});
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
if (stat2.isDirectory()) {
|
|
1111
|
-
fs4.rmSync(p, { recursive: true, force: true });
|
|
1112
|
-
} else {
|
|
1113
|
-
fs4.unlinkSync(p);
|
|
1114
|
-
}
|
|
1115
|
-
} catch {
|
|
818
|
+
server2.post("/_web/nlp/dicts/:id/download", async (request) => {
|
|
819
|
+
return downloadDict(nlpDir, request.params.id);
|
|
820
|
+
});
|
|
821
|
+
server2.delete("/_web/nlp/dicts/:id", async (request) => {
|
|
822
|
+
return deleteDict(nlpDir, request.params.id);
|
|
823
|
+
});
|
|
824
|
+
server2.post("/_web/nlp/word-frequency", async (request) => {
|
|
825
|
+
const params = request.body;
|
|
826
|
+
const db = ctx.dbManager.open(params.sessionId);
|
|
827
|
+
if (!db) {
|
|
828
|
+
throw Object.assign(new Error(`Session not found: ${params.sessionId}`), { statusCode: 404 });
|
|
1116
829
|
}
|
|
1117
|
-
|
|
1118
|
-
}
|
|
1119
|
-
function registerImportRoutes(server2, dbManager2) {
|
|
1120
|
-
server2.get("/_web/supported-formats", async () => {
|
|
1121
|
-
return getSupportedFormats();
|
|
830
|
+
return computeWordFrequency(db, params);
|
|
1122
831
|
});
|
|
1123
|
-
server2.post(
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
try {
|
|
1129
|
-
const chunks = [];
|
|
1130
|
-
for await (const chunk of data.file) {
|
|
1131
|
-
chunks.push(chunk);
|
|
1132
|
-
}
|
|
1133
|
-
fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
1134
|
-
const format = detectFormat(tmpPath);
|
|
1135
|
-
const allFormats = detectAllFormats(tmpPath);
|
|
1136
|
-
return { format, allFormats };
|
|
1137
|
-
} finally {
|
|
1138
|
-
cleanupTemp(tmpPath, tmpDir);
|
|
832
|
+
server2.post(
|
|
833
|
+
"/_web/nlp/segment",
|
|
834
|
+
async (request) => {
|
|
835
|
+
const { text, locale, minLength } = request.body;
|
|
836
|
+
return segmentText(text, locale, minLength);
|
|
1139
837
|
}
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// ../../packages/http-routes/src/routes/web/ai-assistants.ts
|
|
842
|
+
function registerAiAssistantRoutes(server2, ctx) {
|
|
843
|
+
server2.get("/_web/ai/tools/catalog", async () => {
|
|
844
|
+
return BUILTIN_TOOL_CATALOG;
|
|
1140
845
|
});
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
846
|
+
const mgr = ctx.assistantManager;
|
|
847
|
+
if (!mgr) return;
|
|
848
|
+
server2.get("/_web/ai/assistants", async () => {
|
|
849
|
+
return mgr.getAllAssistants();
|
|
850
|
+
});
|
|
851
|
+
server2.get("/_web/ai/assistants/:id", async (request, reply) => {
|
|
852
|
+
const config = mgr.getAssistantConfig(request.params.id);
|
|
853
|
+
if (!config) return reply.code(404).send({ error: "Not found" });
|
|
854
|
+
return config;
|
|
855
|
+
});
|
|
856
|
+
server2.post("/_web/ai/assistants", async (request) => {
|
|
857
|
+
return mgr.createAssistant(request.body);
|
|
858
|
+
});
|
|
859
|
+
server2.put("/_web/ai/assistants/:id", async (request, reply) => {
|
|
860
|
+
const result = mgr.updateAssistant(request.params.id, request.body);
|
|
861
|
+
if (!result.success) return reply.code(404).send(result);
|
|
862
|
+
return result;
|
|
863
|
+
});
|
|
864
|
+
server2.delete("/_web/ai/assistants/:id", async (request, reply) => {
|
|
865
|
+
const result = mgr.deleteAssistant(request.params.id);
|
|
866
|
+
if (!result.success) return reply.code(400).send(result);
|
|
867
|
+
return result;
|
|
868
|
+
});
|
|
869
|
+
server2.post("/_web/ai/assistants/:id/reset", async (request, reply) => {
|
|
870
|
+
const result = mgr.resetAssistant(request.params.id);
|
|
871
|
+
if (!result.success) return reply.code(400).send(result);
|
|
872
|
+
return result;
|
|
873
|
+
});
|
|
874
|
+
server2.post("/_web/ai/assistants/import", async (request) => {
|
|
875
|
+
return mgr.importAssistantFromMd(request.body.rawMd);
|
|
876
|
+
});
|
|
877
|
+
server2.post("/_web/ai/assistants/import-builtin", async (request, reply) => {
|
|
878
|
+
const result = mgr.importAssistant(request.body.builtinId);
|
|
879
|
+
if (!result.success) return reply.code(400).send(result);
|
|
880
|
+
return result;
|
|
881
|
+
});
|
|
882
|
+
server2.post("/_web/ai/assistants/:id/reimport", async (request, reply) => {
|
|
883
|
+
const result = mgr.reimportAssistant(request.params.id);
|
|
884
|
+
if (!result.success) return reply.code(400).send(result);
|
|
885
|
+
return result;
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// ../../packages/http-routes/src/routes/web/ai-skills.ts
|
|
890
|
+
function registerAiSkillRoutes(server2, ctx) {
|
|
891
|
+
const mgr = ctx.skillManagerCore;
|
|
892
|
+
if (!mgr) return;
|
|
893
|
+
server2.get("/_web/ai/skills", async () => {
|
|
894
|
+
return mgr.getAllSkills();
|
|
895
|
+
});
|
|
896
|
+
server2.get("/_web/ai/skills/:id", async (request, reply) => {
|
|
897
|
+
const config = mgr.getSkillConfig(request.params.id);
|
|
898
|
+
if (!config) return reply.code(404).send({ error: "Not found" });
|
|
899
|
+
return config;
|
|
900
|
+
});
|
|
901
|
+
server2.post("/_web/ai/skills", async (request) => {
|
|
902
|
+
return mgr.createSkill(request.body.rawMd);
|
|
903
|
+
});
|
|
904
|
+
server2.put("/_web/ai/skills/:id", async (request, reply) => {
|
|
905
|
+
const result = mgr.updateSkill(request.params.id, request.body.rawMd);
|
|
906
|
+
if (!result.success) return reply.code(404).send(result);
|
|
907
|
+
return result;
|
|
908
|
+
});
|
|
909
|
+
server2.delete("/_web/ai/skills/:id", async (request, reply) => {
|
|
910
|
+
const result = mgr.deleteSkill(request.params.id);
|
|
911
|
+
if (!result.success) return reply.code(400).send(result);
|
|
912
|
+
return result;
|
|
913
|
+
});
|
|
914
|
+
server2.post("/_web/ai/skills/import", async (request) => {
|
|
915
|
+
return mgr.importSkillFromMd(request.body.rawMd);
|
|
916
|
+
});
|
|
917
|
+
server2.get("/_web/ai/skills/builtin-catalog", async () => {
|
|
918
|
+
return mgr.getBuiltinCatalog();
|
|
919
|
+
});
|
|
920
|
+
server2.post("/_web/ai/skills/import-builtin", async (request, reply) => {
|
|
921
|
+
const result = mgr.importSkill(request.body.builtinId);
|
|
922
|
+
if (!result.success) return reply.code(400).send(result);
|
|
923
|
+
return result;
|
|
924
|
+
});
|
|
925
|
+
server2.post("/_web/ai/skills/:id/reimport", async (request, reply) => {
|
|
926
|
+
const result = mgr.reimportSkill(request.params.id);
|
|
927
|
+
if (!result.success) return reply.code(400).send(result);
|
|
928
|
+
return result;
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// ../../packages/http-routes/src/routes/web/ai-llm.ts
|
|
933
|
+
function hasRealApiKey(apiKey) {
|
|
934
|
+
return !!apiKey && apiKey !== "sk-no-key-required";
|
|
935
|
+
}
|
|
936
|
+
function toConfigDisplay(config) {
|
|
937
|
+
const { apiKey: _raw, ...rest } = config;
|
|
938
|
+
return { ...rest, apiKey: "", apiKeySet: hasRealApiKey(String(_raw || "")) };
|
|
939
|
+
}
|
|
940
|
+
function registerAiLlmRoutes(server2, ctx) {
|
|
941
|
+
const store = ctx.llmConfigStore;
|
|
942
|
+
const aiDataDir = ctx.aiDataDir;
|
|
943
|
+
if (!store || !aiDataDir) return;
|
|
944
|
+
server2.get("/_web/ai/llm/has-config", async () => {
|
|
945
|
+
return store.hasActiveConfig();
|
|
946
|
+
});
|
|
947
|
+
server2.get("/_web/ai/llm/configs", async () => {
|
|
948
|
+
const data = store.loadStore();
|
|
949
|
+
return {
|
|
950
|
+
configs: data.configs.map((c) => toConfigDisplay(c)),
|
|
951
|
+
defaultAssistant: data.defaultAssistant,
|
|
952
|
+
fastModel: data.fastModel
|
|
953
|
+
};
|
|
954
|
+
});
|
|
955
|
+
server2.post("/_web/ai/llm/configs", async (request) => {
|
|
956
|
+
const result = store.addConfig(request.body);
|
|
957
|
+
if (result.success && result.config) {
|
|
958
|
+
return { ...result, config: toConfigDisplay(result.config) };
|
|
1156
959
|
}
|
|
960
|
+
return result;
|
|
1157
961
|
});
|
|
1158
|
-
server2.
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
962
|
+
server2.put("/_web/ai/llm/configs/:id", async (request) => {
|
|
963
|
+
return store.updateConfig(request.params.id, request.body);
|
|
964
|
+
});
|
|
965
|
+
server2.delete("/_web/ai/llm/configs/:id", async (request) => {
|
|
966
|
+
return store.deleteConfig(request.params.id);
|
|
967
|
+
});
|
|
968
|
+
server2.get("/_web/ai/llm/default-assistant-slot", async () => {
|
|
969
|
+
return store.getDefaultAssistantSlot();
|
|
970
|
+
});
|
|
971
|
+
server2.put("/_web/ai/llm/default-assistant-slot", async (request) => {
|
|
972
|
+
return store.setDefaultAssistantModel(request.body.configId, request.body.modelId);
|
|
973
|
+
});
|
|
974
|
+
server2.get("/_web/ai/llm/fast-model-slot", async () => {
|
|
975
|
+
return store.getFastModelSlot();
|
|
976
|
+
});
|
|
977
|
+
server2.put("/_web/ai/llm/fast-model-slot", async (request) => {
|
|
978
|
+
return store.setFastModel(request.body);
|
|
979
|
+
});
|
|
980
|
+
server2.get("/_web/ai/llm/providers", async () => {
|
|
981
|
+
return BUILTIN_PROVIDERS.map((p) => {
|
|
982
|
+
const models = getBuiltinModelsByProvider(p.id);
|
|
983
|
+
return {
|
|
984
|
+
id: p.id,
|
|
985
|
+
name: p.name,
|
|
986
|
+
defaultBaseUrl: p.defaultBaseUrl,
|
|
987
|
+
models: models.filter((m) => !m.capabilities.includes("embedding") && !m.capabilities.includes("ranking")).map((m) => ({ id: m.id, name: m.name, description: m.description }))
|
|
988
|
+
};
|
|
989
|
+
});
|
|
990
|
+
});
|
|
991
|
+
server2.get("/_web/ai/llm/provider-registry", async () => {
|
|
992
|
+
const custom = ctx.customProviderStore?.getAll() ?? [];
|
|
993
|
+
return [...BUILTIN_PROVIDERS, ...custom];
|
|
994
|
+
});
|
|
995
|
+
server2.get("/_web/ai/llm/model-catalog", async () => {
|
|
996
|
+
const custom = ctx.customModelStore?.getAll() ?? [];
|
|
997
|
+
return [...BUILTIN_MODELS, ...custom];
|
|
998
|
+
});
|
|
999
|
+
server2.post("/_web/ai/llm/custom-providers", async (request, reply) => {
|
|
1000
|
+
if (!ctx.customProviderStore)
|
|
1001
|
+
return reply.code(501).send({ success: false, error: "Custom providers not available" });
|
|
1002
|
+
return ctx.customProviderStore.add(request.body);
|
|
1003
|
+
});
|
|
1004
|
+
server2.put("/_web/ai/llm/custom-providers/:id", async (request, reply) => {
|
|
1005
|
+
if (!ctx.customProviderStore)
|
|
1006
|
+
return reply.code(501).send({ success: false, error: "Custom providers not available" });
|
|
1007
|
+
const result = ctx.customProviderStore.update(request.params.id, request.body);
|
|
1008
|
+
if (!result.success) return reply.code(404).send(result);
|
|
1009
|
+
return result;
|
|
1010
|
+
});
|
|
1011
|
+
server2.delete("/_web/ai/llm/custom-providers/:id", async (request, reply) => {
|
|
1012
|
+
if (!ctx.customProviderStore)
|
|
1013
|
+
return reply.code(501).send({ success: false, error: "Custom providers not available" });
|
|
1014
|
+
const result = ctx.customProviderStore.delete(request.params.id);
|
|
1015
|
+
if (!result.success) return reply.code(404).send(result);
|
|
1016
|
+
return result;
|
|
1017
|
+
});
|
|
1018
|
+
server2.post("/_web/ai/llm/custom-models", async (request, reply) => {
|
|
1019
|
+
if (!ctx.customModelStore) return reply.code(501).send({ success: false, error: "Custom models not available" });
|
|
1020
|
+
const result = ctx.customModelStore.add(request.body);
|
|
1021
|
+
if (!result.success) return reply.code(409).send(result);
|
|
1022
|
+
return result;
|
|
1023
|
+
});
|
|
1024
|
+
server2.put("/_web/ai/llm/custom-models/:providerId/:modelId", async (request, reply) => {
|
|
1025
|
+
if (!ctx.customModelStore) return reply.code(501).send({ success: false, error: "Custom models not available" });
|
|
1026
|
+
const result = ctx.customModelStore.update(request.params.providerId, request.params.modelId, request.body);
|
|
1027
|
+
if (!result.success) return reply.code(404).send(result);
|
|
1028
|
+
return result;
|
|
1029
|
+
});
|
|
1030
|
+
server2.delete("/_web/ai/llm/custom-models/:providerId/:modelId", async (request, reply) => {
|
|
1031
|
+
if (!ctx.customModelStore) return reply.code(501).send({ success: false, error: "Custom models not available" });
|
|
1032
|
+
const result = ctx.customModelStore.delete(request.params.providerId, request.params.modelId);
|
|
1033
|
+
if (!result.success) return reply.code(404).send(result);
|
|
1034
|
+
return result;
|
|
1035
|
+
});
|
|
1036
|
+
server2.post("/_web/ai/llm/validate-key", async (request) => {
|
|
1037
|
+
const { provider, apiKey, baseUrl, model, apiFormat, configId } = request.body;
|
|
1038
|
+
const resolvedKey = apiKey?.trim() ? apiKey : configId ? store.getConfigById(configId)?.apiKey || "" : "";
|
|
1039
|
+
return validateApiKey(provider, resolvedKey, baseUrl, model, apiFormat);
|
|
1040
|
+
});
|
|
1041
|
+
server2.post("/_web/ai/llm/remote-models", async (request) => {
|
|
1042
|
+
const { provider, apiKey, baseUrl, apiFormat, configId } = request.body;
|
|
1043
|
+
const resolvedKey = apiKey?.trim() ? apiKey : configId ? store.getConfigById(configId)?.apiKey || "" : "";
|
|
1044
|
+
return fetchRemoteModels(provider, resolvedKey, baseUrl, apiFormat);
|
|
1045
|
+
});
|
|
1046
|
+
server2.get("/_web/ai/desensitize-rules/defaults", async (request) => {
|
|
1047
|
+
return getDefaultRulesForLocale(request.query.locale ?? "zh-CN");
|
|
1048
|
+
});
|
|
1049
|
+
server2.post("/_web/ai/desensitize-rules/merge", async (request) => {
|
|
1050
|
+
return mergeRulesForLocale(request.body.existingRules, request.body.locale);
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// ../../packages/http-routes/src/routes/web/ai-llm-stream.ts
|
|
1055
|
+
function registerAiLlmStreamRoutes(server2, ctx) {
|
|
1056
|
+
const store = ctx.llmConfigStore;
|
|
1057
|
+
if (!store) return;
|
|
1058
|
+
server2.post("/_web/ai/llm/chat-stream", async (request, reply) => {
|
|
1059
|
+
const { messages, options } = request.body;
|
|
1060
|
+
const llmConfig = store.getDefaultAssistantConfig();
|
|
1061
|
+
if (!llmConfig) {
|
|
1062
|
+
return reply.code(400).send({ success: false, error: "LLM service not configured" });
|
|
1166
1063
|
}
|
|
1167
|
-
|
|
1168
|
-
const
|
|
1169
|
-
const chatIndexStr = data.fields?.chatIndex?.value;
|
|
1170
|
-
const chatIndex = chatIndexStr !== void 0 ? parseInt(chatIndexStr, 10) : void 0;
|
|
1064
|
+
const piModel = buildPiModel(llmConfig);
|
|
1065
|
+
const abortController = new AbortController();
|
|
1171
1066
|
reply.raw.writeHead(200, {
|
|
1172
1067
|
"Content-Type": "text/event-stream",
|
|
1173
1068
|
"Cache-Control": "no-cache",
|
|
1174
1069
|
Connection: "keep-alive"
|
|
1175
1070
|
});
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1071
|
+
reply.raw.on("close", () => {
|
|
1072
|
+
if (!abortController.signal.aborted) abortController.abort();
|
|
1073
|
+
});
|
|
1074
|
+
const sendChunk = (data) => {
|
|
1075
|
+
if (reply.raw.writableEnded || reply.raw.destroyed) return;
|
|
1076
|
+
reply.raw.write(`event: chunk
|
|
1077
|
+
data: ${JSON.stringify(data)}
|
|
1179
1078
|
|
|
1180
1079
|
`);
|
|
1181
|
-
}
|
|
1080
|
+
};
|
|
1182
1081
|
try {
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1082
|
+
await runSimpleLlmStream({
|
|
1083
|
+
messages,
|
|
1084
|
+
apiKey: llmConfig.apiKey,
|
|
1085
|
+
piModel,
|
|
1086
|
+
temperature: options?.temperature,
|
|
1087
|
+
maxTokens: options?.maxTokens,
|
|
1088
|
+
onChunk: sendChunk,
|
|
1089
|
+
abortSignal: abortController.signal
|
|
1189
1090
|
});
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
messageCount: result.diagnostics?.messagesWritten ?? 0,
|
|
1195
|
-
memberCount: 0
|
|
1196
|
-
});
|
|
1197
|
-
} else {
|
|
1198
|
-
sendEvent("error", { success: false, error: result.error });
|
|
1199
|
-
}
|
|
1200
|
-
} catch (err) {
|
|
1201
|
-
sendEvent("error", { success: false, error: err instanceof Error ? err.message : String(err) });
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
if (abortController.signal.aborted) return;
|
|
1093
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1094
|
+
sendChunk({ content: "", isFinished: true, finishReason: "error", error: msg });
|
|
1202
1095
|
} finally {
|
|
1203
|
-
reply.raw.end();
|
|
1204
|
-
cleanupTemp(tmpPath, tmpDir);
|
|
1096
|
+
if (!reply.raw.writableEnded && !reply.raw.destroyed) reply.raw.end();
|
|
1205
1097
|
}
|
|
1206
1098
|
});
|
|
1207
|
-
|
|
1208
|
-
const parts = request.parts();
|
|
1209
|
-
if (!parts) return reply.code(400).send({ error: "No files uploaded" });
|
|
1210
|
-
const tmpDir = fs4.mkdtempSync(path3.join(os.tmpdir(), "chatlab-dir-import-"));
|
|
1211
|
-
const relativePaths = [];
|
|
1212
|
-
const fileBuffers = [];
|
|
1213
|
-
try {
|
|
1214
|
-
let sendEvent2 = function(event, eventData) {
|
|
1215
|
-
reply.raw.write(`event: ${event}
|
|
1216
|
-
data: ${JSON.stringify(eventData)}
|
|
1217
|
-
|
|
1218
|
-
`);
|
|
1219
|
-
};
|
|
1220
|
-
var sendEvent = sendEvent2;
|
|
1221
|
-
for await (const part of parts) {
|
|
1222
|
-
if (part.type === "field" && part.fieldname === "relativePaths") {
|
|
1223
|
-
relativePaths.push(String(part.value));
|
|
1224
|
-
} else if (part.type === "file") {
|
|
1225
|
-
const chunks = [];
|
|
1226
|
-
for await (const chunk of part.file) {
|
|
1227
|
-
chunks.push(chunk);
|
|
1228
|
-
}
|
|
1229
|
-
fileBuffers.push({ data: Buffer.concat(chunks), filename: part.filename || "" });
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
for (let i = 0; i < fileBuffers.length; i++) {
|
|
1233
|
-
let relPath = relativePaths[i] || fileBuffers[i].filename || `file_${i}`;
|
|
1234
|
-
const segments = relPath.split("/");
|
|
1235
|
-
if (segments.length > 1) {
|
|
1236
|
-
relPath = segments.slice(1).join("/");
|
|
1237
|
-
}
|
|
1238
|
-
const targetPath = path3.resolve(tmpDir, relPath);
|
|
1239
|
-
if (!targetPath.startsWith(tmpDir + path3.sep)) continue;
|
|
1240
|
-
fs4.mkdirSync(path3.dirname(targetPath), { recursive: true });
|
|
1241
|
-
fs4.writeFileSync(targetPath, fileBuffers[i].data);
|
|
1242
|
-
}
|
|
1243
|
-
const entryPath = findEntryFileInDirectory(tmpDir);
|
|
1244
|
-
if (!entryPath) {
|
|
1245
|
-
return reply.code(400).send({ error: "No recognizable import format found in directory" });
|
|
1246
|
-
}
|
|
1247
|
-
reply.raw.writeHead(200, {
|
|
1248
|
-
"Content-Type": "text/event-stream",
|
|
1249
|
-
"Cache-Control": "no-cache",
|
|
1250
|
-
Connection: "keep-alive"
|
|
1251
|
-
});
|
|
1252
|
-
const nativeBinding = resolveNativeBinding();
|
|
1253
|
-
const result = await streamImport(dbManager2, entryPath, {
|
|
1254
|
-
nativeBinding,
|
|
1255
|
-
onProgress: (p) => sendEvent2("progress", p)
|
|
1256
|
-
});
|
|
1257
|
-
if (result.success) {
|
|
1258
|
-
sendEvent2("done", {
|
|
1259
|
-
success: true,
|
|
1260
|
-
sessionId: result.sessionId,
|
|
1261
|
-
messageCount: result.diagnostics?.messagesWritten ?? 0,
|
|
1262
|
-
memberCount: 0
|
|
1263
|
-
});
|
|
1264
|
-
} else {
|
|
1265
|
-
sendEvent2("error", { success: false, error: result.error });
|
|
1266
|
-
}
|
|
1267
|
-
} catch (err) {
|
|
1268
|
-
if (!reply.raw.headersSent) {
|
|
1269
|
-
return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
|
|
1270
|
-
}
|
|
1271
|
-
reply.raw.write(
|
|
1272
|
-
`event: error
|
|
1273
|
-
data: ${JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) })}
|
|
1099
|
+
}
|
|
1274
1100
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
const data = await request.file();
|
|
1285
|
-
if (!data) return reply.code(400).send({ error: "No file uploaded" });
|
|
1286
|
-
const tmpDir = fs4.mkdtempSync(path3.join(os.tmpdir(), "chatlab-inc-"));
|
|
1287
|
-
const tmpPath = path3.join(tmpDir, data.filename || "upload");
|
|
1288
|
-
const chunks = [];
|
|
1289
|
-
for await (const chunk of data.file) {
|
|
1290
|
-
chunks.push(chunk);
|
|
1291
|
-
}
|
|
1292
|
-
fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
1101
|
+
// ../../packages/http-routes/src/routes/web/ai-agent-stream.ts
|
|
1102
|
+
var activeAgentAborts = /* @__PURE__ */ new Map();
|
|
1103
|
+
function registerAiAgentStreamRoutes(server2, ctx) {
|
|
1104
|
+
if (!ctx.runAgentStream) return;
|
|
1105
|
+
const runAgentStream = ctx.runAgentStream;
|
|
1106
|
+
server2.post("/_web/ai/agent/stream", async (request, reply) => {
|
|
1107
|
+
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1108
|
+
const abortController = new AbortController();
|
|
1109
|
+
activeAgentAborts.set(requestId, abortController);
|
|
1293
1110
|
reply.raw.writeHead(200, {
|
|
1294
1111
|
"Content-Type": "text/event-stream",
|
|
1295
1112
|
"Cache-Control": "no-cache",
|
|
1296
|
-
Connection: "keep-alive"
|
|
1113
|
+
Connection: "keep-alive",
|
|
1114
|
+
"X-Request-Id": requestId
|
|
1297
1115
|
});
|
|
1298
|
-
|
|
1116
|
+
let emittedDone = false;
|
|
1117
|
+
const safeSendSSE = (event, data) => {
|
|
1118
|
+
if (reply.raw.writableEnded || reply.raw.destroyed) return;
|
|
1299
1119
|
reply.raw.write(`event: ${event}
|
|
1300
|
-
data: ${JSON.stringify(
|
|
1120
|
+
data: ${JSON.stringify(data)}
|
|
1301
1121
|
|
|
1302
1122
|
`);
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
onProgress: (p) => sendEvent("progress", p)
|
|
1307
|
-
});
|
|
1308
|
-
if (result.success) {
|
|
1309
|
-
sendEvent("done", result);
|
|
1310
|
-
} else {
|
|
1311
|
-
sendEvent("error", { success: false, error: result.error });
|
|
1312
|
-
}
|
|
1313
|
-
} catch (err) {
|
|
1314
|
-
sendEvent("error", { success: false, error: err instanceof Error ? err.message : String(err) });
|
|
1315
|
-
} finally {
|
|
1123
|
+
};
|
|
1124
|
+
const safeEnd = () => {
|
|
1125
|
+
if (reply.raw.writableEnded || reply.raw.destroyed) return;
|
|
1316
1126
|
reply.raw.end();
|
|
1317
|
-
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
const tmpPath = path3.join(tmpDir, data.filename || "upload");
|
|
1326
|
-
const chunks = [];
|
|
1327
|
-
for await (const chunk of data.file) {
|
|
1328
|
-
chunks.push(chunk);
|
|
1329
|
-
}
|
|
1330
|
-
fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
1127
|
+
};
|
|
1128
|
+
safeSendSSE("meta", { requestId });
|
|
1129
|
+
reply.raw.on("close", () => {
|
|
1130
|
+
if (!abortController.signal.aborted) {
|
|
1131
|
+
abortController.abort();
|
|
1132
|
+
}
|
|
1133
|
+
activeAgentAborts.delete(requestId);
|
|
1134
|
+
});
|
|
1331
1135
|
try {
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1136
|
+
await runAgentStream(
|
|
1137
|
+
request.body,
|
|
1138
|
+
(chunk) => {
|
|
1139
|
+
if (chunk.type === "done") emittedDone = true;
|
|
1140
|
+
safeSendSSE(chunk.type, chunk);
|
|
1141
|
+
},
|
|
1142
|
+
abortController.signal
|
|
1143
|
+
);
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1146
|
+
safeSendSSE("error", { type: "error", error: { name: "ServerError", message: msg } });
|
|
1335
1147
|
} finally {
|
|
1336
|
-
|
|
1148
|
+
if (!emittedDone) safeSendSSE("done", { type: "done", isFinished: true });
|
|
1149
|
+
activeAgentAborts.delete(requestId);
|
|
1150
|
+
safeEnd();
|
|
1337
1151
|
}
|
|
1338
1152
|
});
|
|
1339
|
-
server2.post("/_web/
|
|
1340
|
-
const
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
chunks.push(chunk);
|
|
1347
|
-
}
|
|
1348
|
-
fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
1349
|
-
try {
|
|
1350
|
-
return await analyzeNewImport(tmpPath);
|
|
1351
|
-
} catch (err) {
|
|
1352
|
-
return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
|
|
1353
|
-
} finally {
|
|
1354
|
-
cleanupTemp(tmpPath, tmpDir);
|
|
1153
|
+
server2.post("/_web/ai/agent/abort", async (request) => {
|
|
1154
|
+
const { requestId } = request.body;
|
|
1155
|
+
const controller = activeAgentAborts.get(requestId);
|
|
1156
|
+
if (controller) {
|
|
1157
|
+
controller.abort();
|
|
1158
|
+
activeAgentAborts.delete(requestId);
|
|
1159
|
+
return { success: true };
|
|
1355
1160
|
}
|
|
1161
|
+
return { success: false };
|
|
1356
1162
|
});
|
|
1357
|
-
|
|
1358
|
-
"demo-group.json",
|
|
1359
|
-
"demo-private-A-cuilan.json",
|
|
1360
|
-
"demo-private-B-wukong.json",
|
|
1361
|
-
"demo-private-C-spider.json"
|
|
1362
|
-
];
|
|
1363
|
-
server2.post("/_web/demo/import", async (request, reply) => {
|
|
1364
|
-
const locale = request.body?.locale || "en";
|
|
1365
|
-
const nativeBinding = resolveNativeBinding();
|
|
1366
|
-
const total = DEMO_FILES.length;
|
|
1367
|
-
reply.raw.writeHead(200, {
|
|
1368
|
-
"Content-Type": "text/event-stream",
|
|
1369
|
-
"Cache-Control": "no-cache",
|
|
1370
|
-
Connection: "keep-alive"
|
|
1371
|
-
});
|
|
1372
|
-
function sendEvent(event, eventData) {
|
|
1373
|
-
reply.raw.write(`event: ${event}
|
|
1374
|
-
data: ${JSON.stringify(eventData)}
|
|
1163
|
+
}
|
|
1375
1164
|
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
localPaths.push(localPath);
|
|
1390
|
-
}
|
|
1391
|
-
sendEvent("progress", { stage: "importing", current: 1, total });
|
|
1392
|
-
const groupResult = await streamImport(dbManager2, localPaths[0], { nativeBinding });
|
|
1393
|
-
if (!groupResult.success) throw new Error(groupResult.error || "Failed to import group demo");
|
|
1394
|
-
const privateSessionIds = [];
|
|
1395
|
-
for (let i = 1; i < localPaths.length; i++) {
|
|
1396
|
-
sendEvent("progress", { stage: "importing", current: i + 1, total });
|
|
1397
|
-
const result = await streamImport(dbManager2, localPaths[i], { nativeBinding });
|
|
1398
|
-
if (!result.success) throw new Error(result.error || `Failed to import private demo: ${DEMO_FILES[i]}`);
|
|
1399
|
-
if (result.sessionId) privateSessionIds.push(result.sessionId);
|
|
1400
|
-
}
|
|
1401
|
-
sendEvent("progress", { stage: "done", current: total, total });
|
|
1402
|
-
sendEvent("result", {
|
|
1403
|
-
success: true,
|
|
1404
|
-
groupSessionId: groupResult.sessionId,
|
|
1405
|
-
privateSessionIds
|
|
1406
|
-
});
|
|
1407
|
-
} catch (err) {
|
|
1408
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1409
|
-
sendEvent("progress", { stage: "error", current: 0, total, message });
|
|
1410
|
-
sendEvent("result", { success: false, error: message });
|
|
1411
|
-
} finally {
|
|
1412
|
-
reply.raw.end();
|
|
1413
|
-
cleanupTemp(tmpDir);
|
|
1414
|
-
}
|
|
1165
|
+
// ../../packages/http-routes/src/routes/web/ai-filter.ts
|
|
1166
|
+
function registerAiFilterRoutes(server2, ctx) {
|
|
1167
|
+
server2.post("/_web/ai/filter-messages", async (request, reply) => {
|
|
1168
|
+
const { sessionId, keywords, timeFilter, senderIds, contextSize, page, pageSize } = request.body;
|
|
1169
|
+
const db = ctx.dbManager.open(sessionId);
|
|
1170
|
+
if (!db) return reply.code(404).send({ error: `Session not found: ${sessionId}` });
|
|
1171
|
+
return filterMessagesWithContext(db, { keywords, timeFilter, senderIds, contextSize, page, pageSize });
|
|
1172
|
+
});
|
|
1173
|
+
server2.post("/_web/ai/multiple-sessions-messages", async (request, reply) => {
|
|
1174
|
+
const { sessionId, chatSessionIds, page, pageSize } = request.body;
|
|
1175
|
+
const db = ctx.dbManager.open(sessionId);
|
|
1176
|
+
if (!db) return reply.code(404).send({ error: `Session not found: ${sessionId}` });
|
|
1177
|
+
return getMultipleSessionsMessages(db, chatSessionIds, page, pageSize);
|
|
1415
1178
|
});
|
|
1416
1179
|
}
|
|
1417
1180
|
|
|
1418
|
-
//
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1181
|
+
// ../../packages/http-routes/src/routes/web/ai-tools.ts
|
|
1182
|
+
var MAX_RESULT_CHARS = 5e5;
|
|
1183
|
+
var activeToolTests = /* @__PURE__ */ new Map();
|
|
1184
|
+
function registerAiToolRoutes(server2, ctx) {
|
|
1185
|
+
server2.get("/_web/ai/tools/full-catalog", async () => {
|
|
1186
|
+
return AGENT_TOOL_REGISTRY.map((tool) => ({
|
|
1187
|
+
name: tool.name,
|
|
1188
|
+
category: tool.category ?? "core",
|
|
1189
|
+
description: tool.description,
|
|
1190
|
+
parameters: tool.inputSchema ?? {}
|
|
1191
|
+
}));
|
|
1192
|
+
});
|
|
1193
|
+
server2.post("/_web/ai/tools/execute", async (request, reply) => {
|
|
1194
|
+
const { testId, toolName, params, sessionId } = request.body;
|
|
1195
|
+
const entry = AGENT_TOOL_REGISTRY.find((t) => t.name === toolName);
|
|
1196
|
+
if (!entry) {
|
|
1197
|
+
return reply.code(404).send({ success: false, error: `Tool not found: ${toolName}` });
|
|
1198
|
+
}
|
|
1199
|
+
const db = ctx.dbManager.open(sessionId);
|
|
1200
|
+
if (!db) {
|
|
1201
|
+
return reply.code(404).send({ success: false, error: `Session not found: ${sessionId}` });
|
|
1431
1202
|
}
|
|
1432
|
-
|
|
1203
|
+
const abortController = new AbortController();
|
|
1204
|
+
activeToolTests.set(testId, abortController);
|
|
1433
1205
|
try {
|
|
1434
|
-
const
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1206
|
+
const execCtx = {
|
|
1207
|
+
sessionId,
|
|
1208
|
+
db,
|
|
1209
|
+
dataProvider: new CoreDataProvider(db)
|
|
1210
|
+
};
|
|
1211
|
+
const startTime = Date.now();
|
|
1212
|
+
const result = await entry.handler(params, execCtx);
|
|
1213
|
+
const elapsed = Date.now() - startTime;
|
|
1214
|
+
if (abortController.signal.aborted) {
|
|
1215
|
+
return { success: false, error: "cancelled" };
|
|
1216
|
+
}
|
|
1217
|
+
let details = result.data ?? void 0;
|
|
1218
|
+
let truncated = false;
|
|
1219
|
+
if (details) {
|
|
1220
|
+
stripAvatarFields(details);
|
|
1221
|
+
const raw = JSON.stringify(details);
|
|
1222
|
+
if (raw.length > MAX_RESULT_CHARS) {
|
|
1223
|
+
truncated = true;
|
|
1224
|
+
details = { _truncated: true, _originalSize: raw.length, _preview: raw.slice(0, MAX_RESULT_CHARS) };
|
|
1439
1225
|
}
|
|
1440
|
-
}
|
|
1441
|
-
const handle = mergeCache.store(data.filename || "upload", result.tempDbPath);
|
|
1226
|
+
}
|
|
1442
1227
|
return {
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
memberCount: result.memberCount,
|
|
1449
|
-
fileSize: result.fileSize
|
|
1228
|
+
success: true,
|
|
1229
|
+
elapsed,
|
|
1230
|
+
content: [{ type: "text", text: result.content }],
|
|
1231
|
+
details,
|
|
1232
|
+
truncated
|
|
1450
1233
|
};
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
} catch {
|
|
1455
|
-
}
|
|
1456
|
-
try {
|
|
1457
|
-
fs5.rmdirSync(tmpDir);
|
|
1458
|
-
} catch {
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
if (abortController.signal.aborted) {
|
|
1236
|
+
return { success: false, error: "cancelled" };
|
|
1459
1237
|
}
|
|
1238
|
+
console.error(`Failed to execute tool ${toolName}:`, error);
|
|
1239
|
+
return { success: false, error: String(error) };
|
|
1240
|
+
} finally {
|
|
1241
|
+
activeToolTests.delete(testId);
|
|
1460
1242
|
}
|
|
1461
1243
|
});
|
|
1462
|
-
server2.post("/_web/
|
|
1463
|
-
const {
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1244
|
+
server2.post("/_web/ai/tools/cancel", async (request) => {
|
|
1245
|
+
const { testId } = request.body;
|
|
1246
|
+
const controller = activeToolTests.get(testId);
|
|
1247
|
+
if (controller) {
|
|
1248
|
+
controller.abort();
|
|
1249
|
+
activeToolTests.delete(testId);
|
|
1250
|
+
return { success: true };
|
|
1251
|
+
}
|
|
1252
|
+
return { success: false };
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// ../../packages/http-routes/src/routes/web/ai-conversations.ts
|
|
1257
|
+
function registerAiConversationRoutes(server2, ctx) {
|
|
1258
|
+
const cm = ctx.conversationManager;
|
|
1259
|
+
if (!cm) return;
|
|
1260
|
+
server2.post("/_web/ai/conversations", async (request) => {
|
|
1261
|
+
const { sessionId, title, assistantId } = request.body;
|
|
1262
|
+
return cm.createConversation(sessionId, title, assistantId);
|
|
1263
|
+
});
|
|
1264
|
+
server2.get("/_web/ai/conversations", async (request) => {
|
|
1265
|
+
const { sessionId } = request.query;
|
|
1266
|
+
if (!sessionId) return [];
|
|
1267
|
+
return cm.getConversations(sessionId);
|
|
1268
|
+
});
|
|
1269
|
+
server2.get("/_web/ai/conversations/:id", async (request, reply) => {
|
|
1270
|
+
const conv = cm.getConversation(request.params.id);
|
|
1271
|
+
if (!conv) return reply.code(404).send({ error: "Conversation not found" });
|
|
1272
|
+
return conv;
|
|
1273
|
+
});
|
|
1274
|
+
server2.put("/_web/ai/conversations/:id/title", async (request) => {
|
|
1275
|
+
return cm.updateConversationTitle(request.params.id, request.body.title);
|
|
1276
|
+
});
|
|
1277
|
+
server2.delete("/_web/ai/conversations/:id", async (request) => {
|
|
1278
|
+
return cm.deleteConversation(request.params.id);
|
|
1279
|
+
});
|
|
1280
|
+
server2.post("/_web/ai/conversations/:id/messages", async (request) => {
|
|
1281
|
+
const { role, content, dataKeywords, dataMessageCount, contentBlocks, tokenUsage } = request.body;
|
|
1282
|
+
return cm.addMessage(
|
|
1283
|
+
request.params.id,
|
|
1284
|
+
role,
|
|
1285
|
+
content,
|
|
1286
|
+
dataKeywords,
|
|
1287
|
+
dataMessageCount,
|
|
1288
|
+
contentBlocks,
|
|
1289
|
+
tokenUsage
|
|
1290
|
+
);
|
|
1291
|
+
});
|
|
1292
|
+
server2.get("/_web/ai/conversations/:id/messages", async (request) => {
|
|
1293
|
+
return cm.getMessages(request.params.id);
|
|
1294
|
+
});
|
|
1295
|
+
server2.post("/_web/ai/conversations/:id/messages/:messageId/delete-from", async (request, reply) => {
|
|
1296
|
+
const { id, messageId } = request.params;
|
|
1297
|
+
if (!id || typeof id !== "string" || !messageId || typeof messageId !== "string") {
|
|
1298
|
+
return reply.code(400).send({ error: "conversation id and messageId are required" });
|
|
1299
|
+
}
|
|
1300
|
+
cm.deleteMessagesFrom(id, messageId);
|
|
1301
|
+
return { success: true };
|
|
1302
|
+
});
|
|
1303
|
+
server2.post("/_web/ai/conversations/:id/fork", async (request, reply) => {
|
|
1304
|
+
const { upToMessageId, title } = request.body;
|
|
1305
|
+
if (!upToMessageId || typeof upToMessageId !== "string") {
|
|
1306
|
+
return reply.code(400).send({ error: "upToMessageId is required" });
|
|
1307
|
+
}
|
|
1308
|
+
return cm.forkConversation(request.params.id, upToMessageId, title);
|
|
1309
|
+
});
|
|
1310
|
+
server2.put("/_web/ai/messages/:messageId/content", async (request, reply) => {
|
|
1311
|
+
const { content } = request.body;
|
|
1312
|
+
if (!content || typeof content !== "string") {
|
|
1313
|
+
return reply.code(400).send({ error: "content is required" });
|
|
1314
|
+
}
|
|
1315
|
+
cm.updateMessageContent(request.params.messageId, content);
|
|
1316
|
+
return { success: true };
|
|
1317
|
+
});
|
|
1318
|
+
server2.post("/_web/ai/conversations/:id/messages/:messageId/delete-relink", async (request, reply) => {
|
|
1319
|
+
const { id, messageId } = request.params;
|
|
1320
|
+
if (!id || !messageId) {
|
|
1321
|
+
return reply.code(400).send({ error: "conversation id and messageId are required" });
|
|
1322
|
+
}
|
|
1323
|
+
cm.deleteAndRelinkMessage(id, messageId);
|
|
1324
|
+
return { success: true };
|
|
1325
|
+
});
|
|
1326
|
+
server2.post("/_web/ai/conversations/:id/messages/insert-after", async (request, reply) => {
|
|
1327
|
+
const { afterMessageId, role, content, contentBlocks, tokenUsage } = request.body;
|
|
1328
|
+
if (!afterMessageId || typeof afterMessageId !== "string") {
|
|
1329
|
+
return reply.code(400).send({ error: "afterMessageId is required" });
|
|
1330
|
+
}
|
|
1331
|
+
return cm.insertMessageAfter(request.params.id, afterMessageId, role, content, contentBlocks, tokenUsage);
|
|
1332
|
+
});
|
|
1333
|
+
server2.get("/_web/ai/conversations/:id/token-usage", async (request) => {
|
|
1334
|
+
return cm.getConversationTokenUsage(request.params.id);
|
|
1335
|
+
});
|
|
1336
|
+
server2.get("/_web/ai/debug/schema", async () => {
|
|
1337
|
+
return cm.getAiSchema();
|
|
1338
|
+
});
|
|
1339
|
+
server2.post("/_web/ai/debug/execute-sql", async (request, reply) => {
|
|
1340
|
+
const { sql } = request.body;
|
|
1341
|
+
if (!sql || typeof sql !== "string") {
|
|
1342
|
+
return reply.code(400).send({ error: "sql is required" });
|
|
1343
|
+
}
|
|
1344
|
+
try {
|
|
1345
|
+
return cm.executeAiSQL(sql);
|
|
1346
|
+
} catch (e) {
|
|
1347
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1348
|
+
return reply.code(400).send({ error: msg });
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
server2.post("/_web/ai/debug/clear-debug-context", async () => {
|
|
1352
|
+
const cleared = cm.clearAllDebugContext();
|
|
1353
|
+
return { success: true, cleared };
|
|
1354
|
+
});
|
|
1355
|
+
server2.get("/_web/ai/conversations/:id/estimate-tokens", async (request) => {
|
|
1356
|
+
const history = cm.getHistoryForAgent(request.params.id);
|
|
1357
|
+
const tokens = countMessagesTokens(history.map((m) => ({ role: m.role, content: m.content })));
|
|
1358
|
+
return { success: true, tokens, messageCount: history.length };
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// ../../packages/http-routes/src/routes/web/ai-summaries.ts
|
|
1363
|
+
function createSummaryDeps(ctx) {
|
|
1364
|
+
const store = ctx.llmConfigStore;
|
|
1365
|
+
if (!store) return null;
|
|
1366
|
+
return {
|
|
1367
|
+
getLlmConfig() {
|
|
1368
|
+
const config = store.getDefaultAssistantConfig();
|
|
1369
|
+
if (!config) return null;
|
|
1370
|
+
return config;
|
|
1371
|
+
},
|
|
1372
|
+
buildPiModel(config) {
|
|
1373
|
+
return buildPiModel(config);
|
|
1374
|
+
}
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
function registerAiSummaryRoutes(server2, ctx) {
|
|
1378
|
+
const deps = createSummaryDeps(ctx);
|
|
1379
|
+
if (!deps) return;
|
|
1380
|
+
const { sessionAdapter: adapter } = ctx;
|
|
1381
|
+
server2.post("/_web/sessions/:id/summaries/generate", async (request, reply) => {
|
|
1382
|
+
const { chatSessionId, locale, forceRegenerate, strategy } = request.body;
|
|
1383
|
+
const result = await summary_service_exports.generateSummary(adapter, request.params.id, chatSessionId, deps, {
|
|
1384
|
+
locale,
|
|
1385
|
+
forceRegenerate,
|
|
1386
|
+
strategy
|
|
1387
|
+
});
|
|
1388
|
+
if ("error" in result && !result.success) {
|
|
1389
|
+
return reply.code(400).send({ error: result.error });
|
|
1390
|
+
}
|
|
1391
|
+
return result;
|
|
1392
|
+
});
|
|
1393
|
+
server2.post("/_web/sessions/:id/summaries/generate-all", async (request, reply) => {
|
|
1394
|
+
const { locale, forceRegenerate } = request.body;
|
|
1395
|
+
const result = await summary_service_exports.generateAllSummaries(adapter, request.params.id, deps, {
|
|
1396
|
+
locale,
|
|
1397
|
+
forceRegenerate
|
|
1398
|
+
});
|
|
1399
|
+
if (result.error) {
|
|
1400
|
+
return reply.code(400).send({ error: result.error });
|
|
1401
|
+
}
|
|
1402
|
+
return result;
|
|
1403
|
+
});
|
|
1404
|
+
server2.post("/_web/sessions/:id/summaries/check-can-generate", async (request) => {
|
|
1405
|
+
const { chatSessionIds } = request.body;
|
|
1406
|
+
return summary_service_exports.checkCanGenerate(adapter, request.params.id, chatSessionIds);
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// ../../packages/http-routes/src/routes/web/merge.ts
|
|
1411
|
+
import * as fs from "fs";
|
|
1412
|
+
import * as os from "os";
|
|
1413
|
+
import * as path2 from "path";
|
|
1414
|
+
function ensureDb2(ctx, sessionId) {
|
|
1415
|
+
const db = ctx.dbManager.open(sessionId);
|
|
1416
|
+
if (!db) throw sessionNotFound(sessionId);
|
|
1417
|
+
return db;
|
|
1418
|
+
}
|
|
1419
|
+
function registerMergeRoutes(server2, ctx) {
|
|
1420
|
+
const { dbManager: dbManager2, mergeSessionCache: mergeCache, streamImport: streamImport2 } = ctx;
|
|
1421
|
+
if (!mergeCache) return;
|
|
1422
|
+
server2.post("/_web/merge/parse", async (request, reply) => {
|
|
1423
|
+
const contentType = request.headers["content-type"] || "";
|
|
1424
|
+
let filePath;
|
|
1425
|
+
let cleanupFile = false;
|
|
1426
|
+
let cleanupDir;
|
|
1427
|
+
if (contentType.includes("multipart/form-data")) {
|
|
1428
|
+
const data = await request.file();
|
|
1429
|
+
if (!data) return reply.code(400).send({ error: "No file uploaded" });
|
|
1430
|
+
const tmpDir = fs.mkdtempSync(path2.join(os.tmpdir(), "chatlab-merge-"));
|
|
1431
|
+
const tmpPath = path2.join(tmpDir, data.filename || "upload");
|
|
1432
|
+
const chunks = [];
|
|
1433
|
+
for await (const chunk of data.file) {
|
|
1434
|
+
chunks.push(chunk);
|
|
1435
|
+
}
|
|
1436
|
+
fs.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
1437
|
+
filePath = tmpPath;
|
|
1438
|
+
cleanupFile = true;
|
|
1439
|
+
cleanupDir = tmpDir;
|
|
1440
|
+
} else {
|
|
1441
|
+
const body = request.body;
|
|
1442
|
+
if (!body?.filePath) return reply.code(400).send({ error: "Missing filePath in body" });
|
|
1443
|
+
if (!fs.existsSync(body.filePath)) return reply.code(400).send({ error: "File not found" });
|
|
1444
|
+
filePath = body.filePath;
|
|
1445
|
+
}
|
|
1446
|
+
try {
|
|
1447
|
+
const result = await streamParseFileInfo(filePath, {
|
|
1448
|
+
createTempDatabase(sourceFilePath) {
|
|
1449
|
+
return mergeCache.createTempDatabase(path2.basename(sourceFilePath));
|
|
1450
|
+
},
|
|
1451
|
+
onProgress() {
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
const filename = path2.basename(filePath);
|
|
1455
|
+
const handle = mergeCache.store(filename, result.tempDbPath);
|
|
1456
|
+
return {
|
|
1457
|
+
handle,
|
|
1458
|
+
name: result.name,
|
|
1459
|
+
format: result.format,
|
|
1460
|
+
platform: result.platform,
|
|
1461
|
+
messageCount: result.messageCount,
|
|
1462
|
+
memberCount: result.memberCount,
|
|
1463
|
+
fileSize: result.fileSize
|
|
1464
|
+
};
|
|
1465
|
+
} finally {
|
|
1466
|
+
if (cleanupFile) {
|
|
1467
|
+
try {
|
|
1468
|
+
fs.unlinkSync(filePath);
|
|
1469
|
+
} catch {
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
if (cleanupDir) {
|
|
1473
|
+
try {
|
|
1474
|
+
fs.rmdirSync(cleanupDir);
|
|
1475
|
+
} catch {
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
server2.post("/_web/merge/conflicts", async (request, reply) => {
|
|
1481
|
+
const { handles } = request.body;
|
|
1482
|
+
if (!handles || !Array.isArray(handles) || handles.length === 0) {
|
|
1483
|
+
return reply.code(400).send({ error: "Missing or empty handles array" });
|
|
1484
|
+
}
|
|
1485
|
+
const readers = [];
|
|
1468
1486
|
try {
|
|
1469
1487
|
const dataSources = [];
|
|
1470
1488
|
for (const handle of handles) {
|
|
@@ -1498,21 +1516,24 @@ function registerMergeRoutes(server2, dbManager2, mergeCache) {
|
|
|
1498
1516
|
const merged = buildMergedOutput(dataSources, outputName);
|
|
1499
1517
|
let sessionId;
|
|
1500
1518
|
if (andImport) {
|
|
1501
|
-
|
|
1519
|
+
if (!streamImport2) {
|
|
1520
|
+
return reply.code(501).send({ error: "Import not available on this server" });
|
|
1521
|
+
}
|
|
1502
1522
|
const jsonData = JSON.stringify(merged.chatLabData);
|
|
1503
|
-
const tmpDir =
|
|
1504
|
-
const
|
|
1505
|
-
|
|
1523
|
+
const tmpDir = fs.mkdtempSync(path2.join(os.tmpdir(), "chatlab-merged-"));
|
|
1524
|
+
const safeName = path2.basename(outputName).replace(/[/\\?%*:|"<>]/g, "_") || "merged";
|
|
1525
|
+
const tmpPath = path2.join(tmpDir, `${safeName}.json`);
|
|
1526
|
+
fs.writeFileSync(tmpPath, jsonData, "utf-8");
|
|
1506
1527
|
try {
|
|
1507
|
-
const importResult = await
|
|
1528
|
+
const importResult = await streamImport2(dbManager2, tmpPath);
|
|
1508
1529
|
sessionId = importResult.sessionId;
|
|
1509
1530
|
} finally {
|
|
1510
1531
|
try {
|
|
1511
|
-
|
|
1532
|
+
fs.unlinkSync(tmpPath);
|
|
1512
1533
|
} catch {
|
|
1513
1534
|
}
|
|
1514
1535
|
try {
|
|
1515
|
-
|
|
1536
|
+
fs.rmdirSync(tmpDir);
|
|
1516
1537
|
} catch {
|
|
1517
1538
|
}
|
|
1518
1539
|
}
|
|
@@ -1548,10 +1569,9 @@ function registerMergeRoutes(server2, dbManager2, mergeCache) {
|
|
|
1548
1569
|
}
|
|
1549
1570
|
const handles = [];
|
|
1550
1571
|
for (const sid of sessionIds) {
|
|
1551
|
-
const db = ensureDb2(
|
|
1572
|
+
const db = ensureDb2(ctx, sid);
|
|
1552
1573
|
const exported = exportSessionToJson(db);
|
|
1553
1574
|
const { db: tempDb, tempDbPath } = mergeCache.createTempDatabase(exported.meta.name);
|
|
1554
|
-
const { TempDbWriter } = await import("./src-EGXULYFQ.mjs");
|
|
1555
1575
|
const writer = new TempDbWriter(tempDb);
|
|
1556
1576
|
writer.writeMeta({
|
|
1557
1577
|
name: exported.meta.name,
|
|
@@ -1586,72 +1606,17 @@ function registerMergeRoutes(server2, dbManager2, mergeCache) {
|
|
|
1586
1606
|
});
|
|
1587
1607
|
}
|
|
1588
1608
|
|
|
1589
|
-
//
|
|
1590
|
-
|
|
1591
|
-
server2.post("/_web/sessions/:id/export/markdown", async (request, reply) => {
|
|
1592
|
-
const { id } = request.params;
|
|
1593
|
-
const body = request.body;
|
|
1594
|
-
const sessionName = body?.sessionName || id;
|
|
1595
|
-
const { result, content } = export_service_exports.exportMarkdown(adapter, {
|
|
1596
|
-
sessionId: id,
|
|
1597
|
-
sessionName,
|
|
1598
|
-
filterMode: body.filterMode || "condition",
|
|
1599
|
-
keywords: body.keywords,
|
|
1600
|
-
timeFilter: body.timeFilter,
|
|
1601
|
-
senderIds: body.senderIds,
|
|
1602
|
-
contextSize: body.contextSize,
|
|
1603
|
-
chatSessionIds: body.chatSessionIds
|
|
1604
|
-
});
|
|
1605
|
-
if (!result.success) {
|
|
1606
|
-
return reply.code(500).send({ error: result.error });
|
|
1607
|
-
}
|
|
1608
|
-
reply.header("Content-Type", "text/markdown; charset=utf-8");
|
|
1609
|
-
reply.header("Content-Disposition", `attachment; filename="${encodeURIComponent(sessionName)}_export.md"`);
|
|
1610
|
-
return reply.send(content);
|
|
1611
|
-
});
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
|
-
// src/http/routes/web/cache.ts
|
|
1615
|
-
import * as fs6 from "fs";
|
|
1609
|
+
// ../../packages/http-routes/src/routes/web/cache.ts
|
|
1610
|
+
import * as fs2 from "fs";
|
|
1616
1611
|
import * as fsp from "fs/promises";
|
|
1617
|
-
import * as
|
|
1618
|
-
import { execFile } from "child_process";
|
|
1619
|
-
function runExecFile(file, args, runner = execFile) {
|
|
1620
|
-
return new Promise((resolve2, reject) => {
|
|
1621
|
-
runner(file, args, (error) => {
|
|
1622
|
-
if (error) {
|
|
1623
|
-
reject(error);
|
|
1624
|
-
return;
|
|
1625
|
-
}
|
|
1626
|
-
resolve2();
|
|
1627
|
-
});
|
|
1628
|
-
});
|
|
1629
|
-
}
|
|
1630
|
-
async function openDirectoryPath(dirPath, platform = process.platform, runner) {
|
|
1631
|
-
if (platform === "darwin") {
|
|
1632
|
-
await runExecFile("open", [dirPath], runner);
|
|
1633
|
-
} else if (platform === "win32") {
|
|
1634
|
-
await runExecFile("explorer.exe", [dirPath], runner);
|
|
1635
|
-
} else {
|
|
1636
|
-
await runExecFile("xdg-open", [dirPath], runner);
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
async function showPathInFolder(filePath, platform = process.platform, runner) {
|
|
1640
|
-
if (platform === "darwin") {
|
|
1641
|
-
await runExecFile("open", ["-R", filePath], runner);
|
|
1642
|
-
} else if (platform === "win32") {
|
|
1643
|
-
await runExecFile("explorer.exe", [`/select,${filePath}`], runner);
|
|
1644
|
-
} else {
|
|
1645
|
-
await runExecFile("xdg-open", [path5.dirname(filePath)], runner);
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1612
|
+
import * as path3 from "path";
|
|
1648
1613
|
async function getDirSize(dirPath) {
|
|
1649
1614
|
let totalSize = 0;
|
|
1650
1615
|
try {
|
|
1651
|
-
if (!
|
|
1616
|
+
if (!fs2.existsSync(dirPath)) return 0;
|
|
1652
1617
|
const files = await fsp.readdir(dirPath, { withFileTypes: true });
|
|
1653
1618
|
for (const file of files) {
|
|
1654
|
-
const filePath =
|
|
1619
|
+
const filePath = path3.join(dirPath, file.name);
|
|
1655
1620
|
if (file.isDirectory()) {
|
|
1656
1621
|
totalSize += await getDirSize(filePath);
|
|
1657
1622
|
} else {
|
|
@@ -1666,10 +1631,10 @@ async function getDirSize(dirPath) {
|
|
|
1666
1631
|
async function getFileCount(dirPath) {
|
|
1667
1632
|
let count = 0;
|
|
1668
1633
|
try {
|
|
1669
|
-
if (!
|
|
1634
|
+
if (!fs2.existsSync(dirPath)) return 0;
|
|
1670
1635
|
const files = await fsp.readdir(dirPath, { withFileTypes: true });
|
|
1671
1636
|
for (const file of files) {
|
|
1672
|
-
const filePath =
|
|
1637
|
+
const filePath = path3.join(dirPath, file.name);
|
|
1673
1638
|
if (file.isDirectory()) {
|
|
1674
1639
|
count += await getFileCount(filePath);
|
|
1675
1640
|
} else {
|
|
@@ -1680,15 +1645,26 @@ async function getFileCount(dirPath) {
|
|
|
1680
1645
|
}
|
|
1681
1646
|
return count;
|
|
1682
1647
|
}
|
|
1683
|
-
function
|
|
1684
|
-
|
|
1648
|
+
function hasChatLabDatabases(dirPath) {
|
|
1649
|
+
try {
|
|
1650
|
+
const markerPath = path3.join(dirPath, ".chatlab");
|
|
1651
|
+
const dbDir = path3.join(dirPath, "databases");
|
|
1652
|
+
if (!fs2.existsSync(markerPath) || !fs2.existsSync(dbDir)) return false;
|
|
1653
|
+
return fs2.readdirSync(dbDir).some((file) => file.endsWith(".db"));
|
|
1654
|
+
} catch {
|
|
1655
|
+
return false;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
function registerCacheRoutes(server2, ctx) {
|
|
1659
|
+
const pp = ctx.pathProvider;
|
|
1660
|
+
const downloadsDir = ctx.downloadsDir ?? pp.getDownloadsDir();
|
|
1685
1661
|
server2.get("/_web/cache/info", async () => {
|
|
1686
1662
|
const directories = [
|
|
1687
1663
|
{
|
|
1688
1664
|
id: "databases",
|
|
1689
1665
|
name: "settings.storage.cache.databases.name",
|
|
1690
1666
|
description: "settings.storage.cache.databases.description",
|
|
1691
|
-
path:
|
|
1667
|
+
path: pp.getDatabaseDir(),
|
|
1692
1668
|
icon: "i-heroicons-circle-stack",
|
|
1693
1669
|
canClear: false
|
|
1694
1670
|
},
|
|
@@ -1696,7 +1672,7 @@ function registerCacheRoutes(server2, pathProvider) {
|
|
|
1696
1672
|
id: "ai",
|
|
1697
1673
|
name: "settings.storage.cache.ai.name",
|
|
1698
1674
|
description: "settings.storage.cache.ai.description",
|
|
1699
|
-
path:
|
|
1675
|
+
path: pp.getAiDataDir(),
|
|
1700
1676
|
icon: "i-heroicons-sparkles",
|
|
1701
1677
|
canClear: false
|
|
1702
1678
|
},
|
|
@@ -1704,7 +1680,7 @@ function registerCacheRoutes(server2, pathProvider) {
|
|
|
1704
1680
|
id: "cache",
|
|
1705
1681
|
name: "settings.storage.cache.statsCache.name",
|
|
1706
1682
|
description: "settings.storage.cache.statsCache.description",
|
|
1707
|
-
path:
|
|
1683
|
+
path: pp.getCacheDir(),
|
|
1708
1684
|
icon: "i-heroicons-bolt",
|
|
1709
1685
|
canClear: true
|
|
1710
1686
|
},
|
|
@@ -1712,7 +1688,7 @@ function registerCacheRoutes(server2, pathProvider) {
|
|
|
1712
1688
|
id: "logs",
|
|
1713
1689
|
name: "settings.storage.cache.logs.name",
|
|
1714
1690
|
description: "settings.storage.cache.logs.description",
|
|
1715
|
-
path:
|
|
1691
|
+
path: pp.getLogsDir(),
|
|
1716
1692
|
icon: "i-heroicons-document-text",
|
|
1717
1693
|
canClear: true
|
|
1718
1694
|
}
|
|
@@ -1720,12 +1696,11 @@ function registerCacheRoutes(server2, pathProvider) {
|
|
|
1720
1696
|
const results = await Promise.all(
|
|
1721
1697
|
directories.map(async (dir) => {
|
|
1722
1698
|
const [size, fileCount] = await Promise.all([getDirSize(dir.path), getFileCount(dir.path)]);
|
|
1723
|
-
return { ...dir, size, fileCount, exists:
|
|
1699
|
+
return { ...dir, size, fileCount, exists: fs2.existsSync(dir.path) };
|
|
1724
1700
|
})
|
|
1725
1701
|
);
|
|
1726
1702
|
return {
|
|
1727
|
-
baseDir:
|
|
1728
|
-
systemDir: pathProvider.getSystemDir(),
|
|
1703
|
+
baseDir: pp.getUserDataDir(),
|
|
1729
1704
|
directories: results,
|
|
1730
1705
|
totalSize: results.reduce((sum, d) => sum + d.size, 0)
|
|
1731
1706
|
};
|
|
@@ -1733,15 +1708,15 @@ function registerCacheRoutes(server2, pathProvider) {
|
|
|
1733
1708
|
server2.post("/_web/cache/clear", async (request) => {
|
|
1734
1709
|
const { cacheId } = request.body;
|
|
1735
1710
|
const allowedDirs = {
|
|
1736
|
-
cache:
|
|
1737
|
-
logs:
|
|
1711
|
+
cache: pp.getCacheDir(),
|
|
1712
|
+
logs: pp.getLogsDir()
|
|
1738
1713
|
};
|
|
1739
1714
|
const dirPath = allowedDirs[cacheId];
|
|
1740
1715
|
if (!dirPath) return { success: false, error: "Not allowed to clear this directory" };
|
|
1741
|
-
if (!
|
|
1716
|
+
if (!fs2.existsSync(dirPath)) return { success: true };
|
|
1742
1717
|
const files = await fsp.readdir(dirPath);
|
|
1743
1718
|
for (const file of files) {
|
|
1744
|
-
const filePath =
|
|
1719
|
+
const filePath = path3.join(dirPath, file);
|
|
1745
1720
|
const stat2 = await fsp.stat(filePath);
|
|
1746
1721
|
if (stat2.isDirectory()) {
|
|
1747
1722
|
await fsp.rm(filePath, { recursive: true });
|
|
@@ -1751,37 +1726,32 @@ function registerCacheRoutes(server2, pathProvider) {
|
|
|
1751
1726
|
}
|
|
1752
1727
|
return { success: true };
|
|
1753
1728
|
});
|
|
1754
|
-
server2.
|
|
1755
|
-
const
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1729
|
+
server2.get("/_web/cache/data-dir", async () => {
|
|
1730
|
+
const pending = ctx.getPendingDataDirMigration?.();
|
|
1731
|
+
return {
|
|
1732
|
+
path: pp.getUserDataDir(),
|
|
1733
|
+
defaultPath: ctx.defaultUserDataDir,
|
|
1734
|
+
isCustom: ctx.isCustomDataDir ?? false,
|
|
1735
|
+
canSetDataDir: ctx.canSetDataDir ?? Boolean(ctx.setDataDir),
|
|
1736
|
+
hasLegacyDataAtDefaultDir: Boolean(ctx.defaultUserDataDir) && path3.resolve(pp.getUserDataDir()) !== path3.resolve(ctx.defaultUserDataDir ?? "") && hasChatLabDatabases(ctx.defaultUserDataDir ?? ""),
|
|
1737
|
+
pendingMigration: pending ? {
|
|
1738
|
+
from: pending.from,
|
|
1739
|
+
to: pending.to,
|
|
1740
|
+
createdAt: pending.createdAt
|
|
1741
|
+
} : void 0
|
|
1764
1742
|
};
|
|
1765
|
-
const dirPath = dirPaths[cacheId];
|
|
1766
|
-
if (!dirPath) return { success: false, error: "Unknown directory" };
|
|
1767
|
-
if (!fs6.existsSync(dirPath)) {
|
|
1768
|
-
await fsp.mkdir(dirPath, { recursive: true });
|
|
1769
|
-
}
|
|
1770
|
-
try {
|
|
1771
|
-
await openDirectoryPath(dirPath);
|
|
1772
|
-
} catch (error) {
|
|
1773
|
-
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
1774
|
-
}
|
|
1775
|
-
return { success: true };
|
|
1776
1743
|
});
|
|
1777
|
-
server2.
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1744
|
+
server2.post("/_web/cache/data-dir", async (request, reply) => {
|
|
1745
|
+
if (!ctx.setDataDir) {
|
|
1746
|
+
return reply.code(501).send({ success: false, error: "Data directory changes are not supported" });
|
|
1747
|
+
}
|
|
1748
|
+
const targetPath = typeof request.body?.path === "string" ? request.body.path : null;
|
|
1749
|
+
const migrate = request.body?.migrate !== false;
|
|
1750
|
+
return ctx.setDataDir(targetPath, migrate);
|
|
1781
1751
|
});
|
|
1782
1752
|
server2.get("/_web/cache/latest-import-log", async () => {
|
|
1783
|
-
const importLogDir =
|
|
1784
|
-
if (!
|
|
1753
|
+
const importLogDir = path3.join(pp.getLogsDir(), "import");
|
|
1754
|
+
if (!fs2.existsSync(importLogDir)) {
|
|
1785
1755
|
return { success: false, error: "Log directory not found" };
|
|
1786
1756
|
}
|
|
1787
1757
|
const files = await fsp.readdir(importLogDir);
|
|
@@ -1791,7 +1761,7 @@ function registerCacheRoutes(server2, pathProvider) {
|
|
|
1791
1761
|
}
|
|
1792
1762
|
const fileStats = await Promise.all(
|
|
1793
1763
|
logFiles.map(async (f) => {
|
|
1794
|
-
const filePath =
|
|
1764
|
+
const filePath = path3.join(importLogDir, f);
|
|
1795
1765
|
const stat2 = await fsp.stat(filePath);
|
|
1796
1766
|
return { name: f, path: filePath, mtime: stat2.mtime.getTime() };
|
|
1797
1767
|
})
|
|
@@ -1804,898 +1774,1012 @@ function registerCacheRoutes(server2, pathProvider) {
|
|
|
1804
1774
|
if (!filename || !dataUrl) {
|
|
1805
1775
|
return { success: false, error: "filename and dataUrl are required" };
|
|
1806
1776
|
}
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1777
|
+
let buffer;
|
|
1778
|
+
if (dataUrl.includes(";base64,")) {
|
|
1779
|
+
const base64Data = dataUrl.split(";base64,")[1];
|
|
1780
|
+
buffer = Buffer.from(base64Data, "base64");
|
|
1781
|
+
} else if (dataUrl.includes("charset=utf-8,")) {
|
|
1782
|
+
const textData = dataUrl.split("charset=utf-8,")[1];
|
|
1783
|
+
buffer = Buffer.from(decodeURIComponent(textData), "utf-8");
|
|
1784
|
+
} else {
|
|
1785
|
+
const base64Data = dataUrl.replace(/^data:[^,]+,/, "");
|
|
1786
|
+
buffer = Buffer.from(base64Data, "base64");
|
|
1787
|
+
}
|
|
1788
|
+
if (!fs2.existsSync(downloadsDir)) {
|
|
1789
|
+
fs2.mkdirSync(downloadsDir, { recursive: true });
|
|
1812
1790
|
}
|
|
1813
|
-
const filePath =
|
|
1814
|
-
|
|
1791
|
+
const filePath = path3.join(downloadsDir, filename);
|
|
1792
|
+
fs2.writeFileSync(filePath, buffer);
|
|
1815
1793
|
return { success: true, filePath };
|
|
1816
1794
|
});
|
|
1817
|
-
server2.post("/_web/cache/
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1795
|
+
server2.post("/_web/cache/open-dir", async (request, reply) => {
|
|
1796
|
+
if (!ctx.openDirectory) return reply.code(501).send({ success: false, error: "Not supported" });
|
|
1797
|
+
const { cacheId } = request.body;
|
|
1798
|
+
const dirPaths = {
|
|
1799
|
+
base: pp.getUserDataDir(),
|
|
1800
|
+
databases: pp.getDatabaseDir(),
|
|
1801
|
+
cache: pp.getCacheDir(),
|
|
1802
|
+
ai: pp.getAiDataDir(),
|
|
1803
|
+
logs: pp.getLogsDir(),
|
|
1804
|
+
downloads: downloadsDir
|
|
1805
|
+
};
|
|
1806
|
+
const dirPath = dirPaths[cacheId];
|
|
1807
|
+
if (!dirPath) return { success: false, error: "Unknown directory" };
|
|
1808
|
+
if (!fs2.existsSync(dirPath)) {
|
|
1809
|
+
await fsp.mkdir(dirPath, { recursive: true });
|
|
1810
|
+
}
|
|
1811
|
+
try {
|
|
1812
|
+
await ctx.openDirectory(dirPath);
|
|
1813
|
+
} catch (error) {
|
|
1814
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
1815
|
+
}
|
|
1827
1816
|
return { success: true };
|
|
1828
1817
|
});
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
const parse = (v) => {
|
|
1834
|
-
const [core, pre] = v.split("-", 2);
|
|
1835
|
-
const parts = core.split(".").map(Number);
|
|
1836
|
-
return { major: parts[0] || 0, minor: parts[1] || 0, patch: parts[2] || 0, pre };
|
|
1837
|
-
};
|
|
1838
|
-
const l = parse(latest);
|
|
1839
|
-
const c = parse(current);
|
|
1840
|
-
if (l.major !== c.major) return l.major > c.major;
|
|
1841
|
-
if (l.minor !== c.minor) return l.minor > c.minor;
|
|
1842
|
-
if (l.patch !== c.patch) return l.patch > c.patch;
|
|
1843
|
-
if (c.pre && !l.pre) return true;
|
|
1844
|
-
return false;
|
|
1845
|
-
}
|
|
1846
|
-
function registerWebRoutes(server2, dbManager2, options) {
|
|
1847
|
-
const adapter = createDatabaseManagerAdapter(dbManager2);
|
|
1848
|
-
const mergeCache = options?.pathProvider ? new MergeSessionCache(options.pathProvider, { nativeBinding: options.nativeBinding }) : null;
|
|
1849
|
-
mergeCache?.cleanupOrphans();
|
|
1850
|
-
const fallbackPathProvider = {
|
|
1851
|
-
getSystemDir: () => path6.join(os3.homedir(), ".chatlab"),
|
|
1852
|
-
getUserDataDir: () => path6.join(os3.homedir(), ".chatlab", "data"),
|
|
1853
|
-
getDatabaseDir: () => path6.join(os3.homedir(), ".chatlab", "data", "databases"),
|
|
1854
|
-
getAiDataDir: () => path6.join(os3.homedir(), ".chatlab", "ai"),
|
|
1855
|
-
getSettingsDir: () => path6.join(os3.homedir(), ".chatlab", "settings"),
|
|
1856
|
-
getCacheDir: () => path6.join(os3.homedir(), ".chatlab", "cache"),
|
|
1857
|
-
getTempDir: () => path6.join(os3.homedir(), ".chatlab", "temp"),
|
|
1858
|
-
getLogsDir: () => path6.join(os3.homedir(), ".chatlab", "logs"),
|
|
1859
|
-
getDownloadsDir: () => path6.join(os3.homedir(), "Downloads")
|
|
1860
|
-
};
|
|
1861
|
-
const resolvedPathProvider = options?.pathProvider ?? fallbackPathProvider;
|
|
1862
|
-
registerSessionRoutes2(server2, adapter);
|
|
1863
|
-
registerMemberRoutes(server2, adapter);
|
|
1864
|
-
registerAnalyticsRoutes(server2, dbManager2, adapter);
|
|
1865
|
-
registerSqlRoutes(server2, adapter);
|
|
1866
|
-
registerSessionIndexRoutes(server2, adapter);
|
|
1867
|
-
registerSummaryRoutes(server2, dbManager2, adapter);
|
|
1868
|
-
registerImportRoutes(server2, dbManager2);
|
|
1869
|
-
if (mergeCache) {
|
|
1870
|
-
registerMergeRoutes(server2, dbManager2, mergeCache);
|
|
1871
|
-
}
|
|
1872
|
-
registerExportRoutes(server2, adapter);
|
|
1873
|
-
registerCacheRoutes(server2, resolvedPathProvider);
|
|
1874
|
-
server2.get("/_web/system/check-update", async () => {
|
|
1875
|
-
const currentVersion = getVersion();
|
|
1876
|
-
const packageName = "chatlab-cli";
|
|
1818
|
+
server2.post("/_web/cache/show-in-folder", async (request, reply) => {
|
|
1819
|
+
if (!ctx.showInFolder) return reply.code(501).send({ success: false, error: "Not supported" });
|
|
1820
|
+
const { filePath } = request.body;
|
|
1821
|
+
if (!filePath) return { success: false, error: "filePath is required" };
|
|
1877
1822
|
try {
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
});
|
|
1882
|
-
if (!resp.ok) {
|
|
1883
|
-
return { hasUpdate: false, currentVersion, error: `npm registry HTTP ${resp.status}` };
|
|
1884
|
-
}
|
|
1885
|
-
const data = await resp.json();
|
|
1886
|
-
const latestVersion = data.version || currentVersion;
|
|
1887
|
-
const hasUpdate = isNewerVersion(latestVersion, currentVersion);
|
|
1888
|
-
return { hasUpdate, currentVersion, latestVersion };
|
|
1889
|
-
} catch (err) {
|
|
1890
|
-
return {
|
|
1891
|
-
hasUpdate: false,
|
|
1892
|
-
currentVersion,
|
|
1893
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1894
|
-
};
|
|
1823
|
+
await ctx.showInFolder(filePath);
|
|
1824
|
+
} catch (error) {
|
|
1825
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
1895
1826
|
}
|
|
1827
|
+
return { success: true };
|
|
1896
1828
|
});
|
|
1897
1829
|
}
|
|
1898
1830
|
|
|
1899
|
-
//
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
server2
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
server2
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
server2
|
|
1935
|
-
|
|
1936
|
-
const db = ensureDb3(dbManager2, params.sessionId);
|
|
1937
|
-
return computeWordFrequency(db, params);
|
|
1938
|
-
});
|
|
1939
|
-
server2.post(
|
|
1940
|
-
"/_web/nlp/segment",
|
|
1941
|
-
async (request) => {
|
|
1942
|
-
const { text, locale, minLength } = request.body;
|
|
1943
|
-
return segmentText(text, locale, minLength);
|
|
1944
|
-
}
|
|
1945
|
-
);
|
|
1946
|
-
}
|
|
1947
|
-
function resolveNlpDir(dbManager2) {
|
|
1948
|
-
const pathProvider = dbManager2["pathProvider"];
|
|
1949
|
-
return path7.join(pathProvider.getSystemDir(), "nlp");
|
|
1831
|
+
// ../../packages/http-routes/src/register.ts
|
|
1832
|
+
function registerSharedRoutes(server2, ctx, options) {
|
|
1833
|
+
registerSystemRoutes(server2, ctx);
|
|
1834
|
+
registerRestSessionRoutes(server2, ctx);
|
|
1835
|
+
registerSessionRoutes(server2, ctx);
|
|
1836
|
+
registerMemberRoutes(server2, ctx);
|
|
1837
|
+
registerPreferencesRoutes(server2, ctx);
|
|
1838
|
+
registerAnalyticsRoutes(server2, ctx);
|
|
1839
|
+
registerSqlRoutes(server2, ctx);
|
|
1840
|
+
registerSessionIndexRoutes(server2, ctx);
|
|
1841
|
+
registerExportRoutes(server2, ctx);
|
|
1842
|
+
registerNlpRoutes(server2, ctx);
|
|
1843
|
+
if (options?.requireAi) {
|
|
1844
|
+
const missing = [];
|
|
1845
|
+
if (!ctx.aiDataDir) missing.push("aiDataDir");
|
|
1846
|
+
if (!ctx.conversationManager) missing.push("conversationManager");
|
|
1847
|
+
if (!ctx.assistantManager) missing.push("assistantManager");
|
|
1848
|
+
if (!ctx.skillManagerCore) missing.push("skillManagerCore");
|
|
1849
|
+
if (!ctx.llmConfigStore) missing.push("llmConfigStore");
|
|
1850
|
+
if (!ctx.customProviderStore) missing.push("customProviderStore");
|
|
1851
|
+
if (!ctx.customModelStore) missing.push("customModelStore");
|
|
1852
|
+
if (!ctx.runAgentStream) missing.push("runAgentStream");
|
|
1853
|
+
if (missing.length > 0) {
|
|
1854
|
+
throw new Error(`[http-routes] requireAi is set but missing AI dependencies: ${missing.join(", ")}`);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
registerAiAssistantRoutes(server2, ctx);
|
|
1858
|
+
registerAiSkillRoutes(server2, ctx);
|
|
1859
|
+
registerAiLlmRoutes(server2, ctx);
|
|
1860
|
+
registerAiLlmStreamRoutes(server2, ctx);
|
|
1861
|
+
registerAiAgentStreamRoutes(server2, ctx);
|
|
1862
|
+
registerAiFilterRoutes(server2, ctx);
|
|
1863
|
+
registerAiToolRoutes(server2, ctx);
|
|
1864
|
+
registerAiConversationRoutes(server2, ctx);
|
|
1865
|
+
registerAiSummaryRoutes(server2, ctx);
|
|
1866
|
+
registerMergeRoutes(server2, ctx);
|
|
1867
|
+
registerCacheRoutes(server2, ctx);
|
|
1950
1868
|
}
|
|
1951
1869
|
|
|
1952
|
-
//
|
|
1953
|
-
import
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
function initServerAiLogger(logsDir) {
|
|
1959
|
-
if (!logger) {
|
|
1960
|
-
logger = new AiLogger(logsDir);
|
|
1961
|
-
}
|
|
1962
|
-
return logger;
|
|
1870
|
+
// ../../packages/http-routes/src/auth.ts
|
|
1871
|
+
import { timingSafeEqual, createHmac, randomBytes } from "crypto";
|
|
1872
|
+
var cachedToken = null;
|
|
1873
|
+
var requireAuthEnabled = false;
|
|
1874
|
+
function setAuthToken(token) {
|
|
1875
|
+
cachedToken = token;
|
|
1963
1876
|
}
|
|
1964
|
-
function
|
|
1965
|
-
|
|
1877
|
+
function setRequireAuth(enabled) {
|
|
1878
|
+
requireAuthEnabled = enabled;
|
|
1966
1879
|
}
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1880
|
+
var hmacKey = randomBytes(32);
|
|
1881
|
+
function safeTokenCompare(a, b) {
|
|
1882
|
+
const hashA = createHmac("sha256", hmacKey).update(a).digest();
|
|
1883
|
+
const hashB = createHmac("sha256", hmacKey).update(b).digest();
|
|
1884
|
+
return timingSafeEqual(hashA, hashB);
|
|
1885
|
+
}
|
|
1886
|
+
async function authHook(request, reply) {
|
|
1887
|
+
if (!cachedToken) return;
|
|
1888
|
+
const url = request.url;
|
|
1889
|
+
if (url.startsWith("/api/")) {
|
|
1890
|
+
return requireBearerToken(request, reply);
|
|
1891
|
+
}
|
|
1892
|
+
if (url.startsWith("/_web/")) {
|
|
1893
|
+
if (requireAuthEnabled) return requireBearerToken(request, reply);
|
|
1894
|
+
return;
|
|
1971
1895
|
}
|
|
1972
1896
|
}
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
1987
|
-
properties[key] = { ...prop };
|
|
1897
|
+
function requireBearerToken(request, reply) {
|
|
1898
|
+
if (!cachedToken) return;
|
|
1899
|
+
const authHeader = request.headers.authorization;
|
|
1900
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
1901
|
+
const err = unauthorized();
|
|
1902
|
+
reply.code(err.statusCode).send(errorResponse(err));
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
const token = authHeader.slice(7);
|
|
1906
|
+
if (!safeTokenCompare(token, cachedToken)) {
|
|
1907
|
+
const err = unauthorized();
|
|
1908
|
+
reply.code(err.statusCode).send(errorResponse(err));
|
|
1909
|
+
return;
|
|
1988
1910
|
}
|
|
1989
|
-
return {
|
|
1990
|
-
type: "object",
|
|
1991
|
-
properties,
|
|
1992
|
-
required: schema.required || []
|
|
1993
|
-
};
|
|
1994
1911
|
}
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
sessionId: ctx.sessionId,
|
|
2009
|
-
locale: ctx.locale,
|
|
2010
|
-
segmentText: (texts, locale, options2) => batchSegmentWithFrequency(texts, locale, options2)
|
|
2011
|
-
};
|
|
2012
|
-
try {
|
|
2013
|
-
const result = await tool.handler(toolParams, execCtx);
|
|
2014
|
-
if (result.rawMessages && result.rawMessages.length > 0) {
|
|
2015
|
-
const pipelineResult = applyPreprocessingPipeline({
|
|
2016
|
-
rawMessages: result.rawMessages,
|
|
2017
|
-
locale: ctx.locale,
|
|
2018
|
-
maxToolResultTokens: tokenBudget,
|
|
2019
|
-
truncationStrategy: TOOL_TRUNCATION_STRATEGY[tool.name] ?? "keep_last",
|
|
2020
|
-
extraDetails: result.data ?? {},
|
|
2021
|
-
logger: getServerAiLogger() ?? void 0
|
|
2022
|
-
});
|
|
2023
|
-
return { content: [{ type: "text", text: pipelineResult.text }], details: null };
|
|
2024
|
-
}
|
|
2025
|
-
return { content: [{ type: "text", text: result.content }], details: null };
|
|
2026
|
-
} catch (error) {
|
|
2027
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
2028
|
-
return { content: [{ type: "text", text: `Error: ${msg}` }], details: null };
|
|
2029
|
-
}
|
|
1912
|
+
|
|
1913
|
+
// src/http/server.ts
|
|
1914
|
+
var JSON_BODY_LIMIT = 50 * 1024 * 1024;
|
|
1915
|
+
function createServer() {
|
|
1916
|
+
const server2 = Fastify({
|
|
1917
|
+
logger: false,
|
|
1918
|
+
bodyLimit: JSON_BODY_LIMIT
|
|
1919
|
+
});
|
|
1920
|
+
server2.addHook("onRequest", authHook);
|
|
1921
|
+
server2.setErrorHandler((error, _request, reply) => {
|
|
1922
|
+
if (error instanceof ApiError) {
|
|
1923
|
+
reply.code(error.statusCode).send(errorResponse(error));
|
|
1924
|
+
return;
|
|
2030
1925
|
}
|
|
2031
|
-
|
|
1926
|
+
if (error.statusCode === 413) {
|
|
1927
|
+
const bodyErr = new ApiError("BODY_TOO_LARGE" /* BODY_TOO_LARGE */, "Request body exceeds 50MB limit");
|
|
1928
|
+
reply.code(413).send(errorResponse(bodyErr));
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
const statusCode = error.statusCode;
|
|
1932
|
+
if (statusCode && statusCode >= 400 && statusCode < 600) {
|
|
1933
|
+
reply.code(statusCode).send({ success: false, error: { code: "CLIENT_ERROR", message: error.message } });
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
const err = serverError(error.message);
|
|
1937
|
+
reply.code(err.statusCode).send(errorResponse(err));
|
|
1938
|
+
});
|
|
1939
|
+
return server2;
|
|
2032
1940
|
}
|
|
2033
1941
|
|
|
2034
|
-
// src/
|
|
2035
|
-
import * as
|
|
2036
|
-
import * as
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
1942
|
+
// src/http/routes/web/index.ts
|
|
1943
|
+
import * as os3 from "os";
|
|
1944
|
+
import * as path6 from "path";
|
|
1945
|
+
|
|
1946
|
+
// src/http/routes/web/import.ts
|
|
1947
|
+
import * as fs4 from "fs";
|
|
1948
|
+
import * as os2 from "os";
|
|
1949
|
+
import * as path4 from "path";
|
|
1950
|
+
|
|
1951
|
+
// src/http/routes/web/helpers.ts
|
|
1952
|
+
import * as fs3 from "fs";
|
|
1953
|
+
function resolveNativeBinding() {
|
|
1954
|
+
if (process.versions.electron) return void 0;
|
|
1955
|
+
const nativePath = resolveCliPath("native/better_sqlite3.node");
|
|
1956
|
+
if (fs3.existsSync(nativePath)) return nativePath;
|
|
1957
|
+
return void 0;
|
|
2046
1958
|
}
|
|
2047
1959
|
|
|
2048
|
-
// src/
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
return fs8.readFileSync(filePath, "utf-8");
|
|
2062
|
-
},
|
|
2063
|
-
writeFile(filePath, content) {
|
|
2064
|
-
fs8.writeFileSync(filePath, content, "utf-8");
|
|
2065
|
-
},
|
|
2066
|
-
deleteFile(filePath) {
|
|
2067
|
-
if (fs8.existsSync(filePath)) fs8.unlinkSync(filePath);
|
|
2068
|
-
},
|
|
2069
|
-
fileExists(filePath) {
|
|
2070
|
-
return fs8.existsSync(filePath);
|
|
2071
|
-
},
|
|
2072
|
-
joinPath(...parts) {
|
|
2073
|
-
return path9.join(...parts);
|
|
2074
|
-
}
|
|
2075
|
-
};
|
|
2076
|
-
var managerCache = /* @__PURE__ */ new Map();
|
|
2077
|
-
function getAssistantManager(aiDataDir) {
|
|
2078
|
-
let cached = managerCache.get(aiDataDir);
|
|
2079
|
-
if (!cached) {
|
|
2080
|
-
cached = {
|
|
2081
|
-
assistant: new AssistantManager({
|
|
2082
|
-
fs: nodeFs,
|
|
2083
|
-
assistantsDir: path9.join(aiDataDir, "assistants"),
|
|
2084
|
-
builtinRawConfigs: [],
|
|
2085
|
-
generateId: () => `custom_${randomUUID3().replace(/-/g, "").slice(0, 12)}`
|
|
2086
|
-
}),
|
|
2087
|
-
skill: new SkillManagerCore({
|
|
2088
|
-
fs: nodeFs,
|
|
2089
|
-
skillsDir: path9.join(aiDataDir, "skills"),
|
|
2090
|
-
builtinRawSkills: [],
|
|
2091
|
-
contentHash: (content) => createHash("md5").update(content).digest("hex")
|
|
2092
|
-
})
|
|
2093
|
-
};
|
|
2094
|
-
managerCache.set(aiDataDir, cached);
|
|
2095
|
-
}
|
|
2096
|
-
return cached.assistant;
|
|
2097
|
-
}
|
|
2098
|
-
function getSkillManagerCore(aiDataDir) {
|
|
2099
|
-
let cached = managerCache.get(aiDataDir);
|
|
2100
|
-
if (!cached) {
|
|
2101
|
-
getAssistantManager(aiDataDir);
|
|
2102
|
-
cached = managerCache.get(aiDataDir);
|
|
1960
|
+
// src/http/routes/web/import.ts
|
|
1961
|
+
var DEMO_BASE_URL = "https://chatlab.fun/assets/demo";
|
|
1962
|
+
function cleanupTemp(...paths) {
|
|
1963
|
+
for (const p of paths) {
|
|
1964
|
+
try {
|
|
1965
|
+
const stat2 = fs4.statSync(p);
|
|
1966
|
+
if (stat2.isDirectory()) {
|
|
1967
|
+
fs4.rmSync(p, { recursive: true, force: true });
|
|
1968
|
+
} else {
|
|
1969
|
+
fs4.unlinkSync(p);
|
|
1970
|
+
}
|
|
1971
|
+
} catch {
|
|
1972
|
+
}
|
|
2103
1973
|
}
|
|
2104
|
-
return cached.skill;
|
|
2105
1974
|
}
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
const {
|
|
2110
|
-
userMessage,
|
|
2111
|
-
conversationId,
|
|
2112
|
-
historyLeafMessageId,
|
|
2113
|
-
chatType = "group",
|
|
2114
|
-
locale = "zh-CN",
|
|
2115
|
-
assistantSystemPrompt,
|
|
2116
|
-
skillMenu,
|
|
2117
|
-
compressionConfig,
|
|
2118
|
-
tools = [],
|
|
2119
|
-
aiDataDir,
|
|
2120
|
-
convManager: convManager2,
|
|
2121
|
-
onEvent,
|
|
2122
|
-
abortSignal,
|
|
2123
|
-
ownerInfo,
|
|
2124
|
-
mentionedMembers,
|
|
2125
|
-
dataSnapshot,
|
|
2126
|
-
thinkingLevel
|
|
2127
|
-
} = options;
|
|
2128
|
-
const aiLogger = getServerAiLogger();
|
|
2129
|
-
const llmConfig = getDefaultAssistantConfig(aiDataDir);
|
|
2130
|
-
if (!llmConfig) {
|
|
2131
|
-
onEvent({ type: "error", error: { name: "ConfigError", message: "LLM service not configured" } });
|
|
2132
|
-
onEvent({ type: "done", isFinished: true });
|
|
2133
|
-
return;
|
|
2134
|
-
}
|
|
2135
|
-
const piModel = buildPiModel2(llmConfig);
|
|
2136
|
-
const t = createAiTranslate(locale);
|
|
2137
|
-
let skillCtx;
|
|
2138
|
-
if (skillMenu) {
|
|
2139
|
-
skillCtx = { skillMenu };
|
|
2140
|
-
}
|
|
2141
|
-
const systemPrompt = buildSystemPrompt({
|
|
2142
|
-
t,
|
|
2143
|
-
chatType,
|
|
2144
|
-
assistantSystemPrompt,
|
|
2145
|
-
ownerInfo,
|
|
2146
|
-
locale,
|
|
2147
|
-
skillCtx,
|
|
2148
|
-
mentionedMembers,
|
|
2149
|
-
dataSnapshot
|
|
1975
|
+
function registerImportRoutes(server2, dbManager2) {
|
|
1976
|
+
server2.get("/_web/supported-formats", async () => {
|
|
1977
|
+
return getSupportedFormats();
|
|
2150
1978
|
});
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
1979
|
+
server2.post("/_web/detect-format", async (request, reply) => {
|
|
1980
|
+
const data = await request.file();
|
|
1981
|
+
if (!data) return reply.code(400).send({ error: "No file uploaded" });
|
|
1982
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os2.tmpdir(), "chatlab-detect-"));
|
|
1983
|
+
const tmpPath = path4.join(tmpDir, data.filename || "upload");
|
|
1984
|
+
try {
|
|
1985
|
+
const chunks = [];
|
|
1986
|
+
for await (const chunk of data.file) {
|
|
1987
|
+
chunks.push(chunk);
|
|
1988
|
+
}
|
|
1989
|
+
fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
1990
|
+
const format = detectFormat(tmpPath);
|
|
1991
|
+
const allFormats = detectAllFormats(tmpPath);
|
|
1992
|
+
return { format, allFormats };
|
|
1993
|
+
} finally {
|
|
1994
|
+
cleanupTemp(tmpPath, tmpDir);
|
|
1995
|
+
}
|
|
2155
1996
|
});
|
|
2156
|
-
|
|
2157
|
-
const
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
onEvent({
|
|
2172
|
-
type: "compression_done",
|
|
2173
|
-
compressionResult: {
|
|
2174
|
-
summaryContent: compressionResult.summaryContent ?? "",
|
|
2175
|
-
tokensBefore: compressionResult.tokensBefore ?? 0,
|
|
2176
|
-
tokensAfter: compressionResult.tokensAfter ?? 0,
|
|
2177
|
-
timestamp: Date.now()
|
|
2178
|
-
}
|
|
2179
|
-
});
|
|
1997
|
+
server2.post("/_web/scan-multi-chat", async (request, reply) => {
|
|
1998
|
+
const data = await request.file();
|
|
1999
|
+
if (!data) return reply.code(400).send({ error: "No file uploaded" });
|
|
2000
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os2.tmpdir(), "chatlab-scan-"));
|
|
2001
|
+
const tmpPath = path4.join(tmpDir, data.filename || "upload");
|
|
2002
|
+
try {
|
|
2003
|
+
const chunks = [];
|
|
2004
|
+
for await (const chunk of data.file) {
|
|
2005
|
+
chunks.push(chunk);
|
|
2006
|
+
}
|
|
2007
|
+
fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
2008
|
+
const chats = await scanMultiChatFile(tmpPath);
|
|
2009
|
+
return { chats };
|
|
2010
|
+
} finally {
|
|
2011
|
+
cleanupTemp(tmpPath, tmpDir);
|
|
2180
2012
|
}
|
|
2181
|
-
}
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2013
|
+
});
|
|
2014
|
+
server2.post("/_web/import", async (request, reply) => {
|
|
2015
|
+
const data = await request.file();
|
|
2016
|
+
if (!data) return reply.code(400).send({ error: "No file uploaded" });
|
|
2017
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os2.tmpdir(), "chatlab-import-"));
|
|
2018
|
+
const tmpPath = path4.join(tmpDir, data.filename || "upload");
|
|
2019
|
+
const chunks = [];
|
|
2020
|
+
for await (const chunk of data.file) {
|
|
2021
|
+
chunks.push(chunk);
|
|
2022
|
+
}
|
|
2023
|
+
fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
2024
|
+
const formatId = data.fields?.formatId?.value;
|
|
2025
|
+
const chatIndexStr = data.fields?.chatIndex?.value;
|
|
2026
|
+
const chatIndex = chatIndexStr !== void 0 ? parseInt(chatIndexStr, 10) : void 0;
|
|
2027
|
+
reply.raw.writeHead(200, {
|
|
2028
|
+
"Content-Type": "text/event-stream",
|
|
2029
|
+
"Cache-Control": "no-cache",
|
|
2030
|
+
Connection: "keep-alive"
|
|
2185
2031
|
});
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
abortSignal,
|
|
2210
|
-
steerMessage,
|
|
2211
|
-
thinkingLevel,
|
|
2212
|
-
onConvertToLlm: (filteredMessages) => {
|
|
2213
|
-
cachedMessages = filteredMessages;
|
|
2214
|
-
},
|
|
2215
|
-
onEvent: (coreEvent) => handler.handleCoreEvent(coreEvent, cachedMessages),
|
|
2216
|
-
onDebugContext: (messages) => {
|
|
2217
|
-
try {
|
|
2218
|
-
convManager2.setPendingDebugContext(conversationId, JSON.stringify(messages, null, 2));
|
|
2219
|
-
} catch {
|
|
2220
|
-
}
|
|
2032
|
+
function sendEvent(event, eventData) {
|
|
2033
|
+
reply.raw.write(`event: ${event}
|
|
2034
|
+
data: ${JSON.stringify(eventData)}
|
|
2035
|
+
|
|
2036
|
+
`);
|
|
2037
|
+
}
|
|
2038
|
+
try {
|
|
2039
|
+
const nativeBinding = resolveNativeBinding();
|
|
2040
|
+
const result = await streamImport(dbManager2, tmpPath, {
|
|
2041
|
+
formatId,
|
|
2042
|
+
chatIndex,
|
|
2043
|
+
nativeBinding,
|
|
2044
|
+
onProgress: (p) => sendEvent("progress", p)
|
|
2045
|
+
});
|
|
2046
|
+
if (result.success) {
|
|
2047
|
+
sendEvent("done", {
|
|
2048
|
+
success: true,
|
|
2049
|
+
sessionId: result.sessionId,
|
|
2050
|
+
messageCount: result.diagnostics?.messagesWritten ?? 0,
|
|
2051
|
+
memberCount: 0
|
|
2052
|
+
});
|
|
2053
|
+
} else {
|
|
2054
|
+
sendEvent("error", { success: false, error: result.error });
|
|
2221
2055
|
}
|
|
2222
|
-
})
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
handler.emitStatus("completed", cachedMessages, { force: true });
|
|
2228
|
-
onEvent({ type: "done", isFinished: true, usage: result.usage });
|
|
2229
|
-
} catch (error) {
|
|
2230
|
-
const friendlyMessage = formatAIError(error);
|
|
2231
|
-
aiLogger?.error("ServerAgent", "Agent execution error", { error: String(error) });
|
|
2232
|
-
handler.emitStatus("error", cachedMessages, { force: true });
|
|
2233
|
-
onEvent({ type: "error", error: { name: "AgentError", message: friendlyMessage } });
|
|
2234
|
-
onEvent({ type: "done", isFinished: true, usage: handler.cloneUsage() });
|
|
2235
|
-
}
|
|
2236
|
-
}
|
|
2237
|
-
|
|
2238
|
-
// src/ai/custom-store.ts
|
|
2239
|
-
import * as fs9 from "fs";
|
|
2240
|
-
import * as path10 from "path";
|
|
2241
|
-
import { randomUUID as randomUUID4 } from "crypto";
|
|
2242
|
-
function readProviders(aiDataDir) {
|
|
2243
|
-
const storePath = path10.join(aiDataDir, "custom-providers.json");
|
|
2244
|
-
if (!fs9.existsSync(storePath)) return [];
|
|
2245
|
-
try {
|
|
2246
|
-
const content = fs9.readFileSync(storePath, "utf-8");
|
|
2247
|
-
return JSON.parse(content);
|
|
2248
|
-
} catch {
|
|
2249
|
-
return [];
|
|
2250
|
-
}
|
|
2251
|
-
}
|
|
2252
|
-
function writeProviders(aiDataDir, providers) {
|
|
2253
|
-
const storePath = path10.join(aiDataDir, "custom-providers.json");
|
|
2254
|
-
const dir = path10.dirname(storePath);
|
|
2255
|
-
if (!fs9.existsSync(dir)) fs9.mkdirSync(dir, { recursive: true });
|
|
2256
|
-
fs9.writeFileSync(storePath, JSON.stringify(providers, null, 2), "utf-8");
|
|
2257
|
-
}
|
|
2258
|
-
function addCustomProvider(aiDataDir, input) {
|
|
2259
|
-
const providers = readProviders(aiDataDir);
|
|
2260
|
-
const newProvider = {
|
|
2261
|
-
...input,
|
|
2262
|
-
id: `custom:${randomUUID4()}`,
|
|
2263
|
-
builtin: false,
|
|
2264
|
-
enabledByDefault: false
|
|
2265
|
-
};
|
|
2266
|
-
providers.push(newProvider);
|
|
2267
|
-
writeProviders(aiDataDir, providers);
|
|
2268
|
-
return newProvider;
|
|
2269
|
-
}
|
|
2270
|
-
function updateCustomProvider(aiDataDir, id, updates) {
|
|
2271
|
-
const providers = readProviders(aiDataDir);
|
|
2272
|
-
const index = providers.findIndex((p) => p.id === id);
|
|
2273
|
-
if (index === -1) return { success: false, error: "Custom provider not found" };
|
|
2274
|
-
providers[index] = { ...providers[index], ...updates };
|
|
2275
|
-
writeProviders(aiDataDir, providers);
|
|
2276
|
-
return { success: true };
|
|
2277
|
-
}
|
|
2278
|
-
function deleteCustomProvider(aiDataDir, id) {
|
|
2279
|
-
const providers = readProviders(aiDataDir);
|
|
2280
|
-
const index = providers.findIndex((p) => p.id === id);
|
|
2281
|
-
if (index === -1) return { success: false, error: "Custom provider not found" };
|
|
2282
|
-
providers.splice(index, 1);
|
|
2283
|
-
writeProviders(aiDataDir, providers);
|
|
2284
|
-
return { success: true };
|
|
2285
|
-
}
|
|
2286
|
-
function readModels(aiDataDir) {
|
|
2287
|
-
const storePath = path10.join(aiDataDir, "custom-models.json");
|
|
2288
|
-
if (!fs9.existsSync(storePath)) return [];
|
|
2289
|
-
try {
|
|
2290
|
-
const content = fs9.readFileSync(storePath, "utf-8");
|
|
2291
|
-
return JSON.parse(content);
|
|
2292
|
-
} catch {
|
|
2293
|
-
return [];
|
|
2294
|
-
}
|
|
2295
|
-
}
|
|
2296
|
-
function writeModels(aiDataDir, models) {
|
|
2297
|
-
const storePath = path10.join(aiDataDir, "custom-models.json");
|
|
2298
|
-
const dir = path10.dirname(storePath);
|
|
2299
|
-
if (!fs9.existsSync(dir)) fs9.mkdirSync(dir, { recursive: true });
|
|
2300
|
-
fs9.writeFileSync(storePath, JSON.stringify(models, null, 2), "utf-8");
|
|
2301
|
-
}
|
|
2302
|
-
function addCustomModel(aiDataDir, input) {
|
|
2303
|
-
const models = readModels(aiDataDir);
|
|
2304
|
-
const existing = models.find((m) => m.id === input.id && m.providerId === input.providerId);
|
|
2305
|
-
if (existing) {
|
|
2306
|
-
return { success: false, error: `Model "${input.id}" already exists under provider "${input.providerId}"` };
|
|
2307
|
-
}
|
|
2308
|
-
const newModel = {
|
|
2309
|
-
...input,
|
|
2310
|
-
builtin: false,
|
|
2311
|
-
editable: true
|
|
2312
|
-
};
|
|
2313
|
-
models.push(newModel);
|
|
2314
|
-
writeModels(aiDataDir, models);
|
|
2315
|
-
return { success: true, model: newModel };
|
|
2316
|
-
}
|
|
2317
|
-
function updateCustomModel(aiDataDir, providerId, modelId, updates) {
|
|
2318
|
-
const models = readModels(aiDataDir);
|
|
2319
|
-
const index = models.findIndex((m) => m.id === modelId && m.providerId === providerId);
|
|
2320
|
-
if (index === -1) return { success: false, error: "Custom model not found" };
|
|
2321
|
-
models[index] = { ...models[index], ...updates };
|
|
2322
|
-
writeModels(aiDataDir, models);
|
|
2323
|
-
return { success: true };
|
|
2324
|
-
}
|
|
2325
|
-
function deleteCustomModel(aiDataDir, providerId, modelId) {
|
|
2326
|
-
const models = readModels(aiDataDir);
|
|
2327
|
-
const index = models.findIndex((m) => m.id === modelId && m.providerId === providerId);
|
|
2328
|
-
if (index === -1) return { success: false, error: "Custom model not found" };
|
|
2329
|
-
models.splice(index, 1);
|
|
2330
|
-
writeModels(aiDataDir, models);
|
|
2331
|
-
return { success: true };
|
|
2332
|
-
}
|
|
2333
|
-
|
|
2334
|
-
// src/http/routes/ai-config-display.ts
|
|
2335
|
-
var NO_KEY_PLACEHOLDER = "sk-no-key-required";
|
|
2336
|
-
function hasRealApiKey(apiKey) {
|
|
2337
|
-
return !!apiKey && apiKey !== NO_KEY_PLACEHOLDER;
|
|
2338
|
-
}
|
|
2339
|
-
function toLlmConfigDisplay(config, resolveApiKey2) {
|
|
2340
|
-
const { apiKey: _rawApiKey, ...rest } = config;
|
|
2341
|
-
const provider = typeof config.provider === "string" ? config.provider : "";
|
|
2342
|
-
const authProfile = typeof config.authProfile === "string" ? config.authProfile : void 0;
|
|
2343
|
-
const resolvedApiKey = provider ? resolveApiKey2(provider, authProfile) : "";
|
|
2344
|
-
return {
|
|
2345
|
-
...rest,
|
|
2346
|
-
apiKey: "",
|
|
2347
|
-
apiKeySet: hasRealApiKey(resolvedApiKey || "")
|
|
2348
|
-
};
|
|
2349
|
-
}
|
|
2350
|
-
|
|
2351
|
-
// src/http/routes/ai.ts
|
|
2352
|
-
function getAiDir(dbManager2) {
|
|
2353
|
-
const pathProvider = dbManager2["pathProvider"];
|
|
2354
|
-
if (!pathProvider) {
|
|
2355
|
-
throw Object.assign(new Error("PathProvider not available"), { statusCode: 500 });
|
|
2356
|
-
}
|
|
2357
|
-
return pathProvider.getAiDataDir();
|
|
2358
|
-
}
|
|
2359
|
-
function registerAiRoutes(server2, dbManager2, convManager2) {
|
|
2360
|
-
server2.get("/_web/ai/assistants", async () => {
|
|
2361
|
-
const mgr = getAssistantManager(getAiDir(dbManager2));
|
|
2362
|
-
return mgr.getAllAssistants();
|
|
2363
|
-
});
|
|
2364
|
-
server2.get("/_web/ai/assistants/:id", async (request, reply) => {
|
|
2365
|
-
const mgr = getAssistantManager(getAiDir(dbManager2));
|
|
2366
|
-
const config = mgr.getAssistantConfig(request.params.id);
|
|
2367
|
-
if (!config) return reply.code(404).send({ error: "Not found" });
|
|
2368
|
-
return config;
|
|
2369
|
-
});
|
|
2370
|
-
server2.post("/_web/ai/assistants", async (request) => {
|
|
2371
|
-
const mgr = getAssistantManager(getAiDir(dbManager2));
|
|
2372
|
-
return mgr.createAssistant(request.body);
|
|
2373
|
-
});
|
|
2374
|
-
server2.put("/_web/ai/assistants/:id", async (request, reply) => {
|
|
2375
|
-
const mgr = getAssistantManager(getAiDir(dbManager2));
|
|
2376
|
-
const result = mgr.updateAssistant(request.params.id, request.body);
|
|
2377
|
-
if (!result.success) return reply.code(404).send(result);
|
|
2378
|
-
return result;
|
|
2379
|
-
});
|
|
2380
|
-
server2.delete("/_web/ai/assistants/:id", async (request, reply) => {
|
|
2381
|
-
const mgr = getAssistantManager(getAiDir(dbManager2));
|
|
2382
|
-
const result = mgr.deleteAssistant(request.params.id);
|
|
2383
|
-
if (!result.success) return reply.code(400).send(result);
|
|
2384
|
-
return result;
|
|
2385
|
-
});
|
|
2386
|
-
server2.post("/_web/ai/assistants/:id/reset", async (request, reply) => {
|
|
2387
|
-
const mgr = getAssistantManager(getAiDir(dbManager2));
|
|
2388
|
-
const result = mgr.resetAssistant(request.params.id);
|
|
2389
|
-
if (!result.success) return reply.code(400).send(result);
|
|
2390
|
-
return result;
|
|
2391
|
-
});
|
|
2392
|
-
server2.post("/_web/ai/assistants/import", async (request) => {
|
|
2393
|
-
const mgr = getAssistantManager(getAiDir(dbManager2));
|
|
2394
|
-
return mgr.importAssistantFromMd(request.body.rawMd);
|
|
2395
|
-
});
|
|
2396
|
-
server2.get("/_web/ai/skills", async () => {
|
|
2397
|
-
const mgr = getSkillManagerCore(getAiDir(dbManager2));
|
|
2398
|
-
return mgr.getAllSkills();
|
|
2399
|
-
});
|
|
2400
|
-
server2.get("/_web/ai/skills/:id", async (request, reply) => {
|
|
2401
|
-
const mgr = getSkillManagerCore(getAiDir(dbManager2));
|
|
2402
|
-
const config = mgr.getSkillConfig(request.params.id);
|
|
2403
|
-
if (!config) return reply.code(404).send({ error: "Not found" });
|
|
2404
|
-
return config;
|
|
2405
|
-
});
|
|
2406
|
-
server2.post("/_web/ai/skills", async (request) => {
|
|
2407
|
-
const mgr = getSkillManagerCore(getAiDir(dbManager2));
|
|
2408
|
-
return mgr.createSkill(request.body.rawMd);
|
|
2409
|
-
});
|
|
2410
|
-
server2.put("/_web/ai/skills/:id", async (request, reply) => {
|
|
2411
|
-
const mgr = getSkillManagerCore(getAiDir(dbManager2));
|
|
2412
|
-
const result = mgr.updateSkill(request.params.id, request.body.rawMd);
|
|
2413
|
-
if (!result.success) return reply.code(404).send(result);
|
|
2414
|
-
return result;
|
|
2415
|
-
});
|
|
2416
|
-
server2.delete("/_web/ai/skills/:id", async (request, reply) => {
|
|
2417
|
-
const mgr = getSkillManagerCore(getAiDir(dbManager2));
|
|
2418
|
-
const result = mgr.deleteSkill(request.params.id);
|
|
2419
|
-
if (!result.success) return reply.code(400).send(result);
|
|
2420
|
-
return result;
|
|
2421
|
-
});
|
|
2422
|
-
server2.post("/_web/ai/skills/import", async (request) => {
|
|
2423
|
-
const mgr = getSkillManagerCore(getAiDir(dbManager2));
|
|
2424
|
-
return mgr.importSkillFromMd(request.body.rawMd);
|
|
2425
|
-
});
|
|
2426
|
-
server2.get("/_web/ai/skills/builtin-catalog", async () => {
|
|
2427
|
-
const mgr = getSkillManagerCore(getAiDir(dbManager2));
|
|
2428
|
-
return mgr.getBuiltinCatalog();
|
|
2429
|
-
});
|
|
2430
|
-
server2.get("/_web/ai/llm/has-config", async () => {
|
|
2431
|
-
const configPath = path11.join(getAiDir(dbManager2), "llm-config.json");
|
|
2432
|
-
if (!fs10.existsSync(configPath)) return false;
|
|
2433
|
-
try {
|
|
2434
|
-
const data = JSON.parse(fs10.readFileSync(configPath, "utf-8"));
|
|
2435
|
-
if (!data.configs || !Array.isArray(data.configs) || data.configs.length === 0) return false;
|
|
2436
|
-
return data.defaultAssistant != null;
|
|
2437
|
-
} catch {
|
|
2438
|
-
return false;
|
|
2056
|
+
} catch (err) {
|
|
2057
|
+
sendEvent("error", { success: false, error: err instanceof Error ? err.message : String(err) });
|
|
2058
|
+
} finally {
|
|
2059
|
+
reply.raw.end();
|
|
2060
|
+
cleanupTemp(tmpPath, tmpDir);
|
|
2439
2061
|
}
|
|
2440
2062
|
});
|
|
2441
|
-
server2.
|
|
2442
|
-
const
|
|
2443
|
-
if (!
|
|
2063
|
+
server2.post("/_web/import-directory", async (request, reply) => {
|
|
2064
|
+
const parts = request.parts();
|
|
2065
|
+
if (!parts) return reply.code(400).send({ error: "No files uploaded" });
|
|
2066
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os2.tmpdir(), "chatlab-dir-import-"));
|
|
2067
|
+
const relativePaths = [];
|
|
2068
|
+
const fileBuffers = [];
|
|
2444
2069
|
try {
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
fastModel: data.fastModel ?? null
|
|
2451
|
-
};
|
|
2452
|
-
} catch {
|
|
2453
|
-
return { configs: [], defaultAssistant: null, fastModel: null };
|
|
2454
|
-
}
|
|
2455
|
-
});
|
|
2456
|
-
server2.get("/_web/ai/llm/providers", async () => {
|
|
2457
|
-
return BUILTIN_PROVIDERS.map((p) => {
|
|
2458
|
-
const models = getBuiltinModelsByProvider(p.id);
|
|
2459
|
-
return {
|
|
2460
|
-
id: p.id,
|
|
2461
|
-
name: p.name,
|
|
2462
|
-
defaultBaseUrl: p.defaultBaseUrl,
|
|
2463
|
-
models: models.filter((m) => !m.capabilities.includes("embedding") && !m.capabilities.includes("ranking")).map((m) => ({ id: m.id, name: m.name, description: m.description }))
|
|
2070
|
+
let sendEvent2 = function(event, eventData) {
|
|
2071
|
+
reply.raw.write(`event: ${event}
|
|
2072
|
+
data: ${JSON.stringify(eventData)}
|
|
2073
|
+
|
|
2074
|
+
`);
|
|
2464
2075
|
};
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2076
|
+
var sendEvent = sendEvent2;
|
|
2077
|
+
for await (const part of parts) {
|
|
2078
|
+
if (part.type === "field" && part.fieldname === "relativePaths") {
|
|
2079
|
+
relativePaths.push(String(part.value));
|
|
2080
|
+
} else if (part.type === "file") {
|
|
2081
|
+
const chunks = [];
|
|
2082
|
+
for await (const chunk of part.file) {
|
|
2083
|
+
chunks.push(chunk);
|
|
2084
|
+
}
|
|
2085
|
+
fileBuffers.push({ data: Buffer.concat(chunks), filename: part.filename || "" });
|
|
2086
|
+
}
|
|
2476
2087
|
}
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
} catch {
|
|
2488
|
-
custom = [];
|
|
2088
|
+
for (let i = 0; i < fileBuffers.length; i++) {
|
|
2089
|
+
let relPath = relativePaths[i] || fileBuffers[i].filename || `file_${i}`;
|
|
2090
|
+
const segments = relPath.split("/");
|
|
2091
|
+
if (segments.length > 1) {
|
|
2092
|
+
relPath = segments.slice(1).join("/");
|
|
2093
|
+
}
|
|
2094
|
+
const targetPath = path4.resolve(tmpDir, relPath);
|
|
2095
|
+
if (!targetPath.startsWith(tmpDir + path4.sep)) continue;
|
|
2096
|
+
fs4.mkdirSync(path4.dirname(targetPath), { recursive: true });
|
|
2097
|
+
fs4.writeFileSync(targetPath, fileBuffers[i].data);
|
|
2489
2098
|
}
|
|
2099
|
+
const entryPath = findEntryFileInDirectory(tmpDir);
|
|
2100
|
+
if (!entryPath) {
|
|
2101
|
+
return reply.code(400).send({ error: "No recognizable import format found in directory" });
|
|
2102
|
+
}
|
|
2103
|
+
reply.raw.writeHead(200, {
|
|
2104
|
+
"Content-Type": "text/event-stream",
|
|
2105
|
+
"Cache-Control": "no-cache",
|
|
2106
|
+
Connection: "keep-alive"
|
|
2107
|
+
});
|
|
2108
|
+
const nativeBinding = resolveNativeBinding();
|
|
2109
|
+
const result = await streamImport(dbManager2, entryPath, {
|
|
2110
|
+
nativeBinding,
|
|
2111
|
+
onProgress: (p) => sendEvent2("progress", p)
|
|
2112
|
+
});
|
|
2113
|
+
if (result.success) {
|
|
2114
|
+
sendEvent2("done", {
|
|
2115
|
+
success: true,
|
|
2116
|
+
sessionId: result.sessionId,
|
|
2117
|
+
messageCount: result.diagnostics?.messagesWritten ?? 0,
|
|
2118
|
+
memberCount: 0
|
|
2119
|
+
});
|
|
2120
|
+
} else {
|
|
2121
|
+
sendEvent2("error", { success: false, error: result.error });
|
|
2122
|
+
}
|
|
2123
|
+
} catch (err) {
|
|
2124
|
+
if (!reply.raw.headersSent) {
|
|
2125
|
+
return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
|
|
2126
|
+
}
|
|
2127
|
+
reply.raw.write(
|
|
2128
|
+
`event: error
|
|
2129
|
+
data: ${JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) })}
|
|
2130
|
+
|
|
2131
|
+
`
|
|
2132
|
+
);
|
|
2133
|
+
} finally {
|
|
2134
|
+
reply.raw.end();
|
|
2135
|
+
cleanupTemp(tmpDir);
|
|
2490
2136
|
}
|
|
2491
|
-
return [...BUILTIN_MODELS, ...custom];
|
|
2492
|
-
});
|
|
2493
|
-
server2.get("/_web/ai/llm/default-assistant-slot", async () => {
|
|
2494
|
-
const configPath = path11.join(getAiDir(dbManager2), "llm-config.json");
|
|
2495
|
-
if (!fs10.existsSync(configPath)) return null;
|
|
2496
|
-
try {
|
|
2497
|
-
const data = JSON.parse(fs10.readFileSync(configPath, "utf-8"));
|
|
2498
|
-
return data.defaultAssistant ?? null;
|
|
2499
|
-
} catch {
|
|
2500
|
-
return null;
|
|
2501
|
-
}
|
|
2502
|
-
});
|
|
2503
|
-
server2.get("/_web/ai/llm/fast-model-slot", async () => {
|
|
2504
|
-
const configPath = path11.join(getAiDir(dbManager2), "llm-config.json");
|
|
2505
|
-
if (!fs10.existsSync(configPath)) return null;
|
|
2506
|
-
try {
|
|
2507
|
-
const data = JSON.parse(fs10.readFileSync(configPath, "utf-8"));
|
|
2508
|
-
return data.fastModel ?? null;
|
|
2509
|
-
} catch {
|
|
2510
|
-
return null;
|
|
2511
|
-
}
|
|
2512
|
-
});
|
|
2513
|
-
server2.post("/_web/ai/llm/configs", async (request) => {
|
|
2514
|
-
const aiDataDir = getAiDir(dbManager2);
|
|
2515
|
-
return addLlmConfig(aiDataDir, request.body);
|
|
2516
|
-
});
|
|
2517
|
-
server2.put("/_web/ai/llm/configs/:id", async (request) => {
|
|
2518
|
-
const aiDataDir = getAiDir(dbManager2);
|
|
2519
|
-
return updateLlmConfig(aiDataDir, request.params.id, request.body);
|
|
2520
|
-
});
|
|
2521
|
-
server2.delete("/_web/ai/llm/configs/:id", async (request) => {
|
|
2522
|
-
const aiDataDir = getAiDir(dbManager2);
|
|
2523
|
-
return deleteLlmConfig(aiDataDir, request.params.id);
|
|
2524
|
-
});
|
|
2525
|
-
server2.put("/_web/ai/llm/default-assistant-slot", async (request) => {
|
|
2526
|
-
const aiDataDir = getAiDir(dbManager2);
|
|
2527
|
-
return setDefaultAssistantSlot(aiDataDir, request.body.configId, request.body.modelId);
|
|
2528
|
-
});
|
|
2529
|
-
server2.put("/_web/ai/llm/fast-model-slot", async (request) => {
|
|
2530
|
-
const aiDataDir = getAiDir(dbManager2);
|
|
2531
|
-
return setFastModelSlot(aiDataDir, request.body);
|
|
2532
|
-
});
|
|
2533
|
-
server2.post("/_web/ai/llm/custom-providers", async (request) => {
|
|
2534
|
-
const aiDataDir = getAiDir(dbManager2);
|
|
2535
|
-
return addCustomProvider(aiDataDir, {
|
|
2536
|
-
...request.body,
|
|
2537
|
-
kind: request.body.kind || "openai-compatible",
|
|
2538
|
-
authMode: "api-key",
|
|
2539
|
-
supportsCustomModels: request.body.supportsCustomModels ?? true,
|
|
2540
|
-
modelIds: request.body.modelIds ?? []
|
|
2541
|
-
});
|
|
2542
|
-
});
|
|
2543
|
-
server2.put("/_web/ai/llm/custom-providers/:id", async (request) => {
|
|
2544
|
-
const aiDataDir = getAiDir(dbManager2);
|
|
2545
|
-
return updateCustomProvider(aiDataDir, request.params.id, request.body);
|
|
2546
|
-
});
|
|
2547
|
-
server2.delete("/_web/ai/llm/custom-providers/:id", async (request) => {
|
|
2548
|
-
const aiDataDir = getAiDir(dbManager2);
|
|
2549
|
-
return deleteCustomProvider(aiDataDir, request.params.id);
|
|
2550
|
-
});
|
|
2551
|
-
server2.post("/_web/ai/llm/custom-models", async (request) => {
|
|
2552
|
-
const aiDataDir = getAiDir(dbManager2);
|
|
2553
|
-
return addCustomModel(aiDataDir, {
|
|
2554
|
-
...request.body,
|
|
2555
|
-
capabilities: request.body.capabilities ?? ["chat"],
|
|
2556
|
-
recommendedFor: request.body.recommendedFor ?? ["chat"],
|
|
2557
|
-
status: request.body.status ?? "stable"
|
|
2558
|
-
});
|
|
2559
|
-
});
|
|
2560
|
-
server2.put("/_web/ai/llm/custom-models/:providerId/:modelId", async (request) => {
|
|
2561
|
-
const aiDataDir = getAiDir(dbManager2);
|
|
2562
|
-
return updateCustomModel(aiDataDir, request.params.providerId, request.params.modelId, request.body);
|
|
2563
|
-
});
|
|
2564
|
-
server2.delete("/_web/ai/llm/custom-models/:providerId/:modelId", async (request) => {
|
|
2565
|
-
const aiDataDir = getAiDir(dbManager2);
|
|
2566
|
-
return deleteCustomModel(aiDataDir, request.params.providerId, request.params.modelId);
|
|
2567
|
-
});
|
|
2568
|
-
server2.post("/_web/ai/llm/validate-key", async (request) => {
|
|
2569
|
-
const { provider, apiKey, baseUrl, model, apiFormat } = request.body;
|
|
2570
|
-
return validateApiKey(provider, apiKey, baseUrl, model, apiFormat);
|
|
2571
|
-
});
|
|
2572
|
-
server2.post("/_web/ai/llm/remote-models", async (request) => {
|
|
2573
|
-
const { provider, apiKey, baseUrl, apiFormat } = request.body;
|
|
2574
|
-
return fetchRemoteModels(provider, apiKey, baseUrl, apiFormat);
|
|
2575
2137
|
});
|
|
2576
|
-
server2.post("/_web/
|
|
2577
|
-
const
|
|
2578
|
-
const
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2138
|
+
server2.post("/_web/sessions/:id/import/incremental", async (request, reply) => {
|
|
2139
|
+
const sessionId = request.params.id;
|
|
2140
|
+
const data = await request.file();
|
|
2141
|
+
if (!data) return reply.code(400).send({ error: "No file uploaded" });
|
|
2142
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os2.tmpdir(), "chatlab-inc-"));
|
|
2143
|
+
const tmpPath = path4.join(tmpDir, data.filename || "upload");
|
|
2144
|
+
const chunks = [];
|
|
2145
|
+
for await (const chunk of data.file) {
|
|
2146
|
+
chunks.push(chunk);
|
|
2582
2147
|
}
|
|
2583
|
-
|
|
2148
|
+
fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
2584
2149
|
reply.raw.writeHead(200, {
|
|
2585
2150
|
"Content-Type": "text/event-stream",
|
|
2586
2151
|
"Cache-Control": "no-cache",
|
|
2587
2152
|
Connection: "keep-alive"
|
|
2588
2153
|
});
|
|
2589
|
-
|
|
2590
|
-
reply.raw.write(`event:
|
|
2591
|
-
data: ${JSON.stringify(
|
|
2154
|
+
function sendEvent(event, eventData) {
|
|
2155
|
+
reply.raw.write(`event: ${event}
|
|
2156
|
+
data: ${JSON.stringify(eventData)}
|
|
2592
2157
|
|
|
2593
2158
|
`);
|
|
2594
|
-
}
|
|
2159
|
+
}
|
|
2595
2160
|
try {
|
|
2596
|
-
await
|
|
2597
|
-
|
|
2598
|
-
apiKey: llmConfig.apiKey,
|
|
2599
|
-
piModel,
|
|
2600
|
-
temperature: options?.temperature,
|
|
2601
|
-
maxTokens: options?.maxTokens,
|
|
2602
|
-
onChunk: sendChunk
|
|
2161
|
+
const result = await incrementalImport(dbManager2, sessionId, tmpPath, {
|
|
2162
|
+
onProgress: (p) => sendEvent("progress", p)
|
|
2603
2163
|
});
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2164
|
+
if (result.success) {
|
|
2165
|
+
sendEvent("done", result);
|
|
2166
|
+
} else {
|
|
2167
|
+
sendEvent("error", { success: false, error: result.error });
|
|
2168
|
+
}
|
|
2169
|
+
} catch (err) {
|
|
2170
|
+
sendEvent("error", { success: false, error: err instanceof Error ? err.message : String(err) });
|
|
2171
|
+
} finally {
|
|
2172
|
+
reply.raw.end();
|
|
2173
|
+
cleanupTemp(tmpPath, tmpDir);
|
|
2607
2174
|
}
|
|
2608
|
-
reply.raw.end();
|
|
2609
|
-
});
|
|
2610
|
-
server2.get("/_web/ai/tools/catalog", async () => {
|
|
2611
|
-
return BUILTIN_TOOL_CATALOG;
|
|
2612
|
-
});
|
|
2613
|
-
if (!convManager2) return;
|
|
2614
|
-
server2.post("/_web/ai/conversations", async (request) => {
|
|
2615
|
-
const { sessionId, title, assistantId } = request.body;
|
|
2616
|
-
return convManager2.createConversation(sessionId, title, assistantId);
|
|
2617
|
-
});
|
|
2618
|
-
server2.get("/_web/ai/conversations", async (request) => {
|
|
2619
|
-
const { sessionId } = request.query;
|
|
2620
|
-
if (!sessionId) return [];
|
|
2621
|
-
return convManager2.getConversations(sessionId);
|
|
2622
|
-
});
|
|
2623
|
-
server2.get("/_web/ai/conversations/:id", async (request, reply) => {
|
|
2624
|
-
const conv = convManager2.getConversation(request.params.id);
|
|
2625
|
-
if (!conv) return reply.code(404).send({ error: "Conversation not found" });
|
|
2626
|
-
return conv;
|
|
2627
2175
|
});
|
|
2628
|
-
server2.
|
|
2629
|
-
|
|
2176
|
+
server2.post("/_web/sessions/:id/import/incremental/analyze", async (request, reply) => {
|
|
2177
|
+
const sessionId = request.params.id;
|
|
2178
|
+
const data = await request.file();
|
|
2179
|
+
if (!data) return reply.code(400).send({ error: "No file uploaded" });
|
|
2180
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os2.tmpdir(), "chatlab-analyze-"));
|
|
2181
|
+
const tmpPath = path4.join(tmpDir, data.filename || "upload");
|
|
2182
|
+
const chunks = [];
|
|
2183
|
+
for await (const chunk of data.file) {
|
|
2184
|
+
chunks.push(chunk);
|
|
2185
|
+
}
|
|
2186
|
+
fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
2187
|
+
try {
|
|
2188
|
+
return await analyzeIncrementalImport(dbManager2, sessionId, tmpPath);
|
|
2189
|
+
} catch (err) {
|
|
2190
|
+
return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
|
|
2191
|
+
} finally {
|
|
2192
|
+
cleanupTemp(tmpPath, tmpDir);
|
|
2193
|
+
}
|
|
2630
2194
|
});
|
|
2631
|
-
server2.
|
|
2632
|
-
|
|
2195
|
+
server2.post("/_web/import/analyze", async (request, reply) => {
|
|
2196
|
+
const data = await request.file();
|
|
2197
|
+
if (!data) return reply.code(400).send({ error: "No file uploaded" });
|
|
2198
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os2.tmpdir(), "chatlab-analyze-"));
|
|
2199
|
+
const tmpPath = path4.join(tmpDir, data.filename || "upload");
|
|
2200
|
+
const chunks = [];
|
|
2201
|
+
for await (const chunk of data.file) {
|
|
2202
|
+
chunks.push(chunk);
|
|
2203
|
+
}
|
|
2204
|
+
fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
2205
|
+
try {
|
|
2206
|
+
return await analyzeNewImport(tmpPath);
|
|
2207
|
+
} catch (err) {
|
|
2208
|
+
return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
|
|
2209
|
+
} finally {
|
|
2210
|
+
cleanupTemp(tmpPath, tmpDir);
|
|
2211
|
+
}
|
|
2633
2212
|
});
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2213
|
+
const DEMO_FILES = [
|
|
2214
|
+
"demo-group.json",
|
|
2215
|
+
"demo-private-A-cuilan.json",
|
|
2216
|
+
"demo-private-B-wukong.json",
|
|
2217
|
+
"demo-private-C-spider.json"
|
|
2218
|
+
];
|
|
2219
|
+
server2.post("/_web/demo/import", async (request, reply) => {
|
|
2220
|
+
const locale = request.body?.locale || "en";
|
|
2221
|
+
const nativeBinding = resolveNativeBinding();
|
|
2222
|
+
const total = DEMO_FILES.length;
|
|
2223
|
+
reply.raw.writeHead(200, {
|
|
2224
|
+
"Content-Type": "text/event-stream",
|
|
2225
|
+
"Cache-Control": "no-cache",
|
|
2226
|
+
Connection: "keep-alive"
|
|
2227
|
+
});
|
|
2228
|
+
function sendEvent(event, eventData) {
|
|
2229
|
+
reply.raw.write(`event: ${event}
|
|
2230
|
+
data: ${JSON.stringify(eventData)}
|
|
2231
|
+
|
|
2232
|
+
`);
|
|
2233
|
+
}
|
|
2234
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os2.tmpdir(), "chatlab-demo-"));
|
|
2235
|
+
try {
|
|
2236
|
+
const localPaths = [];
|
|
2237
|
+
for (let i = 0; i < total; i++) {
|
|
2238
|
+
sendEvent("progress", { stage: "downloading", current: i + 1, total });
|
|
2239
|
+
const localPath = path4.join(tmpDir, DEMO_FILES[i]);
|
|
2240
|
+
const resp = await fetch(`${DEMO_BASE_URL}/${locale}/${DEMO_FILES[i]}`, {
|
|
2241
|
+
signal: AbortSignal.timeout(6e4)
|
|
2242
|
+
});
|
|
2243
|
+
if (!resp.ok) throw new Error(`Download demo failed (${DEMO_FILES[i]}): ${resp.status}`);
|
|
2244
|
+
fs4.writeFileSync(localPath, Buffer.from(await resp.arrayBuffer()));
|
|
2245
|
+
localPaths.push(localPath);
|
|
2246
|
+
}
|
|
2247
|
+
sendEvent("progress", { stage: "importing", current: 1, total });
|
|
2248
|
+
const groupResult = await streamImport(dbManager2, localPaths[0], { nativeBinding });
|
|
2249
|
+
if (!groupResult.success) throw new Error(groupResult.error || "Failed to import group demo");
|
|
2250
|
+
const privateSessionIds = [];
|
|
2251
|
+
for (let i = 1; i < localPaths.length; i++) {
|
|
2252
|
+
sendEvent("progress", { stage: "importing", current: i + 1, total });
|
|
2253
|
+
const result = await streamImport(dbManager2, localPaths[i], { nativeBinding });
|
|
2254
|
+
if (!result.success) throw new Error(result.error || `Failed to import private demo: ${DEMO_FILES[i]}`);
|
|
2255
|
+
if (result.sessionId) privateSessionIds.push(result.sessionId);
|
|
2256
|
+
}
|
|
2257
|
+
sendEvent("progress", { stage: "done", current: total, total });
|
|
2258
|
+
sendEvent("result", {
|
|
2259
|
+
success: true,
|
|
2260
|
+
groupSessionId: groupResult.sessionId,
|
|
2261
|
+
privateSessionIds
|
|
2262
|
+
});
|
|
2263
|
+
} catch (err) {
|
|
2264
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2265
|
+
sendEvent("progress", { stage: "error", current: 0, total, message });
|
|
2266
|
+
sendEvent("result", { success: false, error: message });
|
|
2267
|
+
} finally {
|
|
2268
|
+
reply.raw.end();
|
|
2269
|
+
cleanupTemp(tmpDir);
|
|
2270
|
+
}
|
|
2645
2271
|
});
|
|
2646
|
-
|
|
2647
|
-
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
// src/http/routes/web/cache.ts
|
|
2275
|
+
import * as path5 from "path";
|
|
2276
|
+
import { execFile } from "child_process";
|
|
2277
|
+
function runExecFile(file, args, runner = execFile) {
|
|
2278
|
+
return new Promise((resolve4, reject) => {
|
|
2279
|
+
runner(file, args, (error) => {
|
|
2280
|
+
if (error) {
|
|
2281
|
+
reject(error);
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
resolve4();
|
|
2285
|
+
});
|
|
2648
2286
|
});
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2287
|
+
}
|
|
2288
|
+
async function openDirectoryPath(dirPath, platform = process.platform, runner) {
|
|
2289
|
+
if (platform === "darwin") {
|
|
2290
|
+
await runExecFile("open", [dirPath], runner);
|
|
2291
|
+
} else if (platform === "win32") {
|
|
2292
|
+
await runExecFile("explorer.exe", [dirPath], runner);
|
|
2293
|
+
} else {
|
|
2294
|
+
await runExecFile("xdg-open", [dirPath], runner);
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
async function showPathInFolder(filePath, platform = process.platform, runner) {
|
|
2298
|
+
if (platform === "darwin") {
|
|
2299
|
+
await runExecFile("open", ["-R", filePath], runner);
|
|
2300
|
+
} else if (platform === "win32") {
|
|
2301
|
+
await runExecFile("explorer.exe", [`/select,${filePath}`], runner);
|
|
2302
|
+
} else {
|
|
2303
|
+
await runExecFile("xdg-open", [path5.dirname(filePath)], runner);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
// src/http/routes/web/index.ts
|
|
2308
|
+
function isNewerVersion(latest, current) {
|
|
2309
|
+
const parse = (v) => {
|
|
2310
|
+
const [core, pre] = v.split("-", 2);
|
|
2311
|
+
const parts = core.split(".").map(Number);
|
|
2312
|
+
return { major: parts[0] || 0, minor: parts[1] || 0, patch: parts[2] || 0, pre };
|
|
2313
|
+
};
|
|
2314
|
+
const l = parse(latest);
|
|
2315
|
+
const c = parse(current);
|
|
2316
|
+
if (l.major !== c.major) return l.major > c.major;
|
|
2317
|
+
if (l.minor !== c.minor) return l.minor > c.minor;
|
|
2318
|
+
if (l.patch !== c.patch) return l.patch > c.patch;
|
|
2319
|
+
if (c.pre && !l.pre) return true;
|
|
2320
|
+
return false;
|
|
2321
|
+
}
|
|
2322
|
+
function registerWebRoutes(server2, dbManager2, options) {
|
|
2323
|
+
const adapter = createDatabaseManagerAdapter(dbManager2);
|
|
2324
|
+
const mergeCache = options?.pathProvider ? new MergeSessionCache(options.pathProvider, { nativeBinding: options.nativeBinding }) : null;
|
|
2325
|
+
mergeCache?.cleanupOrphans();
|
|
2326
|
+
const fallbackPathProvider = {
|
|
2327
|
+
getSystemDir: () => path6.join(os3.homedir(), ".chatlab"),
|
|
2328
|
+
getUserDataDir: () => path6.join(os3.homedir(), ".chatlab", "data"),
|
|
2329
|
+
getDatabaseDir: () => path6.join(os3.homedir(), ".chatlab", "data", "databases"),
|
|
2330
|
+
getAiDataDir: () => path6.join(os3.homedir(), ".chatlab", "ai"),
|
|
2331
|
+
getSettingsDir: () => path6.join(os3.homedir(), ".chatlab", "settings"),
|
|
2332
|
+
getCacheDir: () => path6.join(os3.homedir(), ".chatlab", "cache"),
|
|
2333
|
+
getTempDir: () => path6.join(os3.homedir(), ".chatlab", "temp"),
|
|
2334
|
+
getLogsDir: () => path6.join(os3.homedir(), ".chatlab", "logs"),
|
|
2335
|
+
getDownloadsDir: () => path6.join(os3.homedir(), "Downloads")
|
|
2336
|
+
};
|
|
2337
|
+
const resolvedPathProvider = options?.pathProvider ?? fallbackPathProvider;
|
|
2338
|
+
const ai = options?.aiContext;
|
|
2339
|
+
const cliStreamImport = async (dm, filePath) => {
|
|
2340
|
+
const { streamImport: streamImport2 } = await import("./stream-import-RDDB3N7B.mjs");
|
|
2341
|
+
const result = await streamImport2(dm, filePath);
|
|
2342
|
+
if (!result.sessionId) throw new Error("Import succeeded but no sessionId returned");
|
|
2343
|
+
return { sessionId: result.sessionId };
|
|
2344
|
+
};
|
|
2345
|
+
const defaultUserDataDir = getDefaultNodeUserDataDir();
|
|
2346
|
+
const isCustom = path6.resolve(resolvedPathProvider.getUserDataDir()) !== path6.resolve(defaultUserDataDir);
|
|
2347
|
+
registerSharedRoutes(
|
|
2348
|
+
server2,
|
|
2349
|
+
{
|
|
2350
|
+
dbManager: dbManager2,
|
|
2351
|
+
sessionAdapter: adapter,
|
|
2352
|
+
pathProvider: resolvedPathProvider,
|
|
2353
|
+
getVersion,
|
|
2354
|
+
nativeBinding: options?.nativeBinding,
|
|
2355
|
+
openDirectory: openDirectoryPath,
|
|
2356
|
+
showInFolder: showPathInFolder,
|
|
2357
|
+
downloadsDir: resolvedPathProvider.getDownloadsDir(),
|
|
2358
|
+
defaultUserDataDir,
|
|
2359
|
+
isCustomDataDir: isCustom,
|
|
2360
|
+
canSetDataDir: !process.env.CHATLAB_DATA_DIR,
|
|
2361
|
+
getPendingDataDirMigration: () => getPendingNodeDataDirMigration(resolvedPathProvider.getSystemDir()),
|
|
2362
|
+
setDataDir: (dirPath, migrate) => createNodeDataDirSwitch({
|
|
2363
|
+
systemDir: resolvedPathProvider.getSystemDir(),
|
|
2364
|
+
currentDir: resolvedPathProvider.getUserDataDir(),
|
|
2365
|
+
targetDir: dirPath,
|
|
2366
|
+
defaultDir: defaultUserDataDir,
|
|
2367
|
+
migrate,
|
|
2368
|
+
envDataDir: process.env.CHATLAB_DATA_DIR
|
|
2369
|
+
}),
|
|
2370
|
+
...mergeCache && {
|
|
2371
|
+
mergeSessionCache: mergeCache,
|
|
2372
|
+
streamImport: cliStreamImport
|
|
2373
|
+
},
|
|
2374
|
+
...ai && {
|
|
2375
|
+
aiDataDir: ai.aiDataDir,
|
|
2376
|
+
conversationManager: ai.convManager,
|
|
2377
|
+
assistantManager: ai.assistantManager,
|
|
2378
|
+
skillManagerCore: ai.skillManagerCore,
|
|
2379
|
+
llmConfigStore: ai.llmConfigStore,
|
|
2380
|
+
customProviderStore: ai.customProviderStore,
|
|
2381
|
+
customModelStore: ai.customModelStore,
|
|
2382
|
+
runAgentStream: ai.runAgentStream
|
|
2383
|
+
}
|
|
2384
|
+
},
|
|
2385
|
+
ai ? { requireAi: true } : void 0
|
|
2386
|
+
);
|
|
2387
|
+
registerImportRoutes(server2, dbManager2);
|
|
2388
|
+
server2.get("/_web/system/check-update", async () => {
|
|
2389
|
+
const currentVersion = getVersion();
|
|
2390
|
+
const packageName = "chatlab-cli";
|
|
2391
|
+
try {
|
|
2392
|
+
const resp = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
|
|
2393
|
+
headers: { Accept: "application/json" },
|
|
2394
|
+
signal: AbortSignal.timeout(1e4)
|
|
2395
|
+
});
|
|
2396
|
+
if (!resp.ok) {
|
|
2397
|
+
return { hasUpdate: false, currentVersion, error: `npm registry HTTP ${resp.status}` };
|
|
2398
|
+
}
|
|
2399
|
+
const data = await resp.json();
|
|
2400
|
+
const latestVersion = data.version || currentVersion;
|
|
2401
|
+
const hasUpdate = isNewerVersion(latestVersion, currentVersion);
|
|
2402
|
+
return { hasUpdate, currentVersion, latestVersion };
|
|
2403
|
+
} catch (err) {
|
|
2404
|
+
return {
|
|
2405
|
+
hasUpdate: false,
|
|
2406
|
+
currentVersion,
|
|
2407
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2408
|
+
};
|
|
2653
2409
|
}
|
|
2654
|
-
convManager2.deleteMessagesFrom(id, messageId);
|
|
2655
|
-
return { success: true };
|
|
2656
2410
|
});
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
// src/http/routes/proxy.ts
|
|
2414
|
+
var PROXY_TARGET = "https://chatlab.fun";
|
|
2415
|
+
function registerProxyRoutes(server2) {
|
|
2416
|
+
server2.get("/_proxy/chatlab.fun/*", async (request, reply) => {
|
|
2417
|
+
const subPath = request.params["*"] ?? "";
|
|
2418
|
+
const queryString = new URLSearchParams(
|
|
2419
|
+
request.query
|
|
2420
|
+
).toString();
|
|
2421
|
+
const targetUrl = queryString ? `${PROXY_TARGET}/${subPath}?${queryString}` : `${PROXY_TARGET}/${subPath}`;
|
|
2422
|
+
try {
|
|
2423
|
+
const upstream = await fetch(targetUrl, {
|
|
2424
|
+
headers: { Accept: request.headers.accept ?? "*/*" },
|
|
2425
|
+
signal: AbortSignal.timeout(1e4)
|
|
2426
|
+
});
|
|
2427
|
+
const contentType = upstream.headers.get("content-type") ?? "application/octet-stream";
|
|
2428
|
+
const body = Buffer.from(await upstream.arrayBuffer());
|
|
2429
|
+
reply.code(upstream.status).header("content-type", contentType).send(body);
|
|
2430
|
+
} catch (err) {
|
|
2431
|
+
reply.code(502).send({
|
|
2432
|
+
error: "Proxy error",
|
|
2433
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2434
|
+
});
|
|
2661
2435
|
}
|
|
2662
|
-
return convManager2.forkConversation(request.params.id, upToMessageId, title);
|
|
2663
2436
|
});
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
// src/ai/logger.ts
|
|
2440
|
+
var logger = null;
|
|
2441
|
+
function initServerAiLogger(logsDir) {
|
|
2442
|
+
if (!logger) {
|
|
2443
|
+
logger = new AiLogger(logsDir);
|
|
2444
|
+
}
|
|
2445
|
+
return logger;
|
|
2446
|
+
}
|
|
2447
|
+
function getServerAiLogger() {
|
|
2448
|
+
return logger;
|
|
2449
|
+
}
|
|
2450
|
+
function closeServerAiLogger() {
|
|
2451
|
+
if (logger) {
|
|
2452
|
+
logger.close();
|
|
2453
|
+
logger = null;
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// src/ai/manager-factory.ts
|
|
2458
|
+
import * as fs5 from "fs";
|
|
2459
|
+
import * as path7 from "path";
|
|
2460
|
+
import { createHash, randomUUID } from "crypto";
|
|
2461
|
+
var nodeFs = {
|
|
2462
|
+
ensureDir(dir) {
|
|
2463
|
+
if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
|
|
2464
|
+
},
|
|
2465
|
+
listFiles(dir, ext) {
|
|
2466
|
+
if (!fs5.existsSync(dir)) return [];
|
|
2467
|
+
return fs5.readdirSync(dir).filter((f) => f.endsWith(ext));
|
|
2468
|
+
},
|
|
2469
|
+
readFile(filePath) {
|
|
2470
|
+
return fs5.readFileSync(filePath, "utf-8");
|
|
2471
|
+
},
|
|
2472
|
+
writeFile(filePath, content) {
|
|
2473
|
+
fs5.writeFileSync(filePath, content, "utf-8");
|
|
2474
|
+
},
|
|
2475
|
+
deleteFile(filePath) {
|
|
2476
|
+
if (fs5.existsSync(filePath)) fs5.unlinkSync(filePath);
|
|
2477
|
+
},
|
|
2478
|
+
fileExists(filePath) {
|
|
2479
|
+
return fs5.existsSync(filePath);
|
|
2480
|
+
},
|
|
2481
|
+
joinPath(...parts) {
|
|
2482
|
+
return path7.join(...parts);
|
|
2483
|
+
}
|
|
2484
|
+
};
|
|
2485
|
+
var managerCache = /* @__PURE__ */ new Map();
|
|
2486
|
+
function getAssistantManager(aiDataDir) {
|
|
2487
|
+
let cached = managerCache.get(aiDataDir);
|
|
2488
|
+
if (!cached) {
|
|
2489
|
+
cached = {
|
|
2490
|
+
assistant: new AssistantManager({
|
|
2491
|
+
fs: nodeFs,
|
|
2492
|
+
assistantsDir: path7.join(aiDataDir, "assistants"),
|
|
2493
|
+
builtinRawConfigs: [],
|
|
2494
|
+
generateId: () => `custom_${randomUUID().replace(/-/g, "").slice(0, 12)}`
|
|
2495
|
+
}),
|
|
2496
|
+
skill: new SkillManagerCore({
|
|
2497
|
+
fs: nodeFs,
|
|
2498
|
+
skillsDir: path7.join(aiDataDir, "skills"),
|
|
2499
|
+
builtinRawSkills: [],
|
|
2500
|
+
contentHash: (content) => createHash("md5").update(content).digest("hex")
|
|
2501
|
+
})
|
|
2502
|
+
};
|
|
2503
|
+
managerCache.set(aiDataDir, cached);
|
|
2504
|
+
}
|
|
2505
|
+
return cached.assistant;
|
|
2506
|
+
}
|
|
2507
|
+
function getSkillManagerCore(aiDataDir) {
|
|
2508
|
+
let cached = managerCache.get(aiDataDir);
|
|
2509
|
+
if (!cached) {
|
|
2510
|
+
getAssistantManager(aiDataDir);
|
|
2511
|
+
cached = managerCache.get(aiDataDir);
|
|
2512
|
+
}
|
|
2513
|
+
return cached.skill;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
// src/ai/tool-adapter.ts
|
|
2517
|
+
var DEFAULT_MAX_TOOL_RESULT_TOKENS = 8e3;
|
|
2518
|
+
var TOOL_TRUNCATION_STRATEGY = {
|
|
2519
|
+
search_messages: "keep_first",
|
|
2520
|
+
deep_search_messages: "keep_first",
|
|
2521
|
+
get_recent_messages: "keep_last",
|
|
2522
|
+
get_message_context: "keep_last",
|
|
2523
|
+
get_session_messages: "keep_last",
|
|
2524
|
+
get_conversation_between: "keep_last"
|
|
2525
|
+
};
|
|
2526
|
+
function convertJsonSchemaToParameters(schema) {
|
|
2527
|
+
const properties = {};
|
|
2528
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
2529
|
+
properties[key] = { ...prop };
|
|
2530
|
+
}
|
|
2531
|
+
return {
|
|
2532
|
+
type: "object",
|
|
2533
|
+
properties,
|
|
2534
|
+
required: schema.required || []
|
|
2535
|
+
};
|
|
2536
|
+
}
|
|
2537
|
+
function adaptToolsForAgent(tools, getContext, options) {
|
|
2538
|
+
const tokenBudget = options?.maxToolResultTokens ?? DEFAULT_MAX_TOOL_RESULT_TOKENS;
|
|
2539
|
+
return tools.map((tool) => ({
|
|
2540
|
+
name: tool.name,
|
|
2541
|
+
label: tool.name,
|
|
2542
|
+
description: tool.description,
|
|
2543
|
+
parameters: convertJsonSchemaToParameters(tool.inputSchema),
|
|
2544
|
+
async execute(_toolCallId, params) {
|
|
2545
|
+
const toolParams = params && typeof params === "object" ? params : {};
|
|
2546
|
+
const ctx = getContext();
|
|
2547
|
+
const execCtx = {
|
|
2548
|
+
db: ctx.db,
|
|
2549
|
+
dataProvider: new CoreDataProvider(ctx.db),
|
|
2550
|
+
sessionId: ctx.sessionId,
|
|
2551
|
+
locale: ctx.locale,
|
|
2552
|
+
segmentText: (texts, locale, options2) => batchSegmentWithFrequency(texts, locale, options2)
|
|
2553
|
+
};
|
|
2554
|
+
try {
|
|
2555
|
+
const result = await tool.handler(toolParams, execCtx);
|
|
2556
|
+
if (result.rawMessages && result.rawMessages.length > 0) {
|
|
2557
|
+
const pipelineResult = applyPreprocessingPipeline({
|
|
2558
|
+
rawMessages: result.rawMessages,
|
|
2559
|
+
locale: ctx.locale,
|
|
2560
|
+
maxToolResultTokens: tokenBudget,
|
|
2561
|
+
truncationStrategy: TOOL_TRUNCATION_STRATEGY[tool.name] ?? "keep_last",
|
|
2562
|
+
extraDetails: result.data ?? {},
|
|
2563
|
+
logger: getServerAiLogger() ?? void 0
|
|
2564
|
+
});
|
|
2565
|
+
return { content: [{ type: "text", text: pipelineResult.text }], details: null };
|
|
2566
|
+
}
|
|
2567
|
+
return { content: [{ type: "text", text: result.content }], details: null };
|
|
2568
|
+
} catch (error) {
|
|
2569
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2570
|
+
return { content: [{ type: "text", text: `Error: ${msg}` }], details: null };
|
|
2571
|
+
}
|
|
2668
2572
|
}
|
|
2669
|
-
|
|
2670
|
-
|
|
2573
|
+
}));
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
// src/ai/llm-config.ts
|
|
2577
|
+
import * as fs6 from "fs";
|
|
2578
|
+
import * as path8 from "path";
|
|
2579
|
+
function loadLlmConfig(aiDataDir) {
|
|
2580
|
+
const configPath = path8.join(aiDataDir, "llm-config.json");
|
|
2581
|
+
if (!fs6.existsSync(configPath)) {
|
|
2582
|
+
return { configs: [], defaultAssistant: null, fastModel: null };
|
|
2583
|
+
}
|
|
2584
|
+
try {
|
|
2585
|
+
const data = JSON.parse(fs6.readFileSync(configPath, "utf-8"));
|
|
2586
|
+
const configs = (data.configs || []).map((c) => {
|
|
2587
|
+
const provider = c.provider || "";
|
|
2588
|
+
const authProfile = c.authProfile;
|
|
2589
|
+
const profileKey = resolveApiKey(provider, authProfile);
|
|
2590
|
+
const rawKey = c.apiKey || "";
|
|
2591
|
+
const fallbackKey = rawKey.startsWith("enc:") ? "" : rawKey;
|
|
2592
|
+
return {
|
|
2593
|
+
...c,
|
|
2594
|
+
apiKey: profileKey || fallbackKey
|
|
2595
|
+
};
|
|
2596
|
+
});
|
|
2597
|
+
return {
|
|
2598
|
+
configs,
|
|
2599
|
+
defaultAssistant: data.defaultAssistant ?? null,
|
|
2600
|
+
fastModel: data.fastModel ?? null
|
|
2601
|
+
};
|
|
2602
|
+
} catch {
|
|
2603
|
+
return { configs: [], defaultAssistant: null, fastModel: null };
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
function resolveSlot(slot, configs) {
|
|
2607
|
+
if (slot && configs.some((c) => c.id === slot.configId)) return slot;
|
|
2608
|
+
const fallback = configs[0];
|
|
2609
|
+
if (!fallback) return null;
|
|
2610
|
+
return { configId: fallback.id, modelId: fallback.model };
|
|
2611
|
+
}
|
|
2612
|
+
function getDefaultAssistantConfig(aiDataDir) {
|
|
2613
|
+
const store = loadLlmConfig(aiDataDir);
|
|
2614
|
+
const slot = resolveSlot(store.defaultAssistant, store.configs);
|
|
2615
|
+
if (!slot) return null;
|
|
2616
|
+
const config = store.configs.find((c) => c.id === slot.configId);
|
|
2617
|
+
if (!config) return null;
|
|
2618
|
+
return { ...config, model: slot.modelId || config.model };
|
|
2619
|
+
}
|
|
2620
|
+
function buildPiModel2(config) {
|
|
2621
|
+
return buildPiModel(config);
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
// src/ai/assistant-loader.ts
|
|
2625
|
+
import * as fs7 from "fs";
|
|
2626
|
+
import * as path9 from "path";
|
|
2627
|
+
function loadAssistantConfig(aiDataDir, assistantId) {
|
|
2628
|
+
const filePath = path9.join(aiDataDir, "assistants", `${assistantId}.md`);
|
|
2629
|
+
if (!fs7.existsSync(filePath)) return null;
|
|
2630
|
+
try {
|
|
2631
|
+
const content = fs7.readFileSync(filePath, "utf-8");
|
|
2632
|
+
return parseAssistantFile(content, filePath);
|
|
2633
|
+
} catch {
|
|
2634
|
+
return null;
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
// src/ai/agent.ts
|
|
2639
|
+
async function runServerAgent(options) {
|
|
2640
|
+
const {
|
|
2641
|
+
userMessage,
|
|
2642
|
+
conversationId,
|
|
2643
|
+
historyLeafMessageId,
|
|
2644
|
+
chatType = "group",
|
|
2645
|
+
locale = "zh-CN",
|
|
2646
|
+
assistantSystemPrompt,
|
|
2647
|
+
skillMenu,
|
|
2648
|
+
skillDef,
|
|
2649
|
+
compressionConfig,
|
|
2650
|
+
tools = [],
|
|
2651
|
+
aiDataDir,
|
|
2652
|
+
convManager: convManager2,
|
|
2653
|
+
onEvent,
|
|
2654
|
+
abortSignal,
|
|
2655
|
+
ownerInfo,
|
|
2656
|
+
mentionedMembers,
|
|
2657
|
+
dataSnapshot,
|
|
2658
|
+
thinkingLevel
|
|
2659
|
+
} = options;
|
|
2660
|
+
const aiLogger = getServerAiLogger();
|
|
2661
|
+
await initTokenizer();
|
|
2662
|
+
const llmConfig = getDefaultAssistantConfig(aiDataDir);
|
|
2663
|
+
if (!llmConfig) {
|
|
2664
|
+
onEvent({ type: "error", error: { name: "ConfigError", message: "LLM service not configured" } });
|
|
2665
|
+
onEvent({ type: "done", isFinished: true });
|
|
2666
|
+
return;
|
|
2667
|
+
}
|
|
2668
|
+
const piModel = buildPiModel2(llmConfig);
|
|
2669
|
+
const t = createAiTranslate(locale);
|
|
2670
|
+
let skillCtx;
|
|
2671
|
+
if (skillDef) {
|
|
2672
|
+
skillCtx = { skillDef };
|
|
2673
|
+
} else if (skillMenu) {
|
|
2674
|
+
skillCtx = { skillMenu };
|
|
2675
|
+
}
|
|
2676
|
+
const systemPrompt = buildSystemPrompt({
|
|
2677
|
+
t,
|
|
2678
|
+
chatType,
|
|
2679
|
+
assistantSystemPrompt,
|
|
2680
|
+
ownerInfo,
|
|
2681
|
+
locale,
|
|
2682
|
+
skillCtx,
|
|
2683
|
+
mentionedMembers,
|
|
2684
|
+
dataSnapshot
|
|
2671
2685
|
});
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2686
|
+
const handler = new AgentEventHandler({
|
|
2687
|
+
onChunk: onEvent,
|
|
2688
|
+
context: {},
|
|
2689
|
+
systemPrompt
|
|
2690
|
+
});
|
|
2691
|
+
if (compressionConfig?.enabled && historyLeafMessageId === void 0) {
|
|
2692
|
+
const llmAdapter = createCompressionLlmAdapter({
|
|
2693
|
+
piModel,
|
|
2694
|
+
apiKey: llmConfig.apiKey,
|
|
2695
|
+
onCompressing: () => handler.emitStatus("compressing", [])
|
|
2696
|
+
});
|
|
2697
|
+
const compressionResult = await checkAndCompress(
|
|
2698
|
+
conversationId,
|
|
2699
|
+
compressionConfig,
|
|
2700
|
+
systemPrompt,
|
|
2701
|
+
llmAdapter,
|
|
2702
|
+
convManager2,
|
|
2703
|
+
aiLogger ?? void 0
|
|
2704
|
+
);
|
|
2705
|
+
if (compressionResult.compressed) {
|
|
2706
|
+
onEvent({
|
|
2707
|
+
type: "compression_done",
|
|
2708
|
+
compressionResult: {
|
|
2709
|
+
summaryContent: compressionResult.summaryContent ?? "",
|
|
2710
|
+
tokensBefore: compressionResult.tokensBefore ?? 0,
|
|
2711
|
+
tokensAfter: compressionResult.tokensAfter ?? 0,
|
|
2712
|
+
timestamp: Date.now()
|
|
2713
|
+
}
|
|
2714
|
+
});
|
|
2676
2715
|
}
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2716
|
+
} else if (compressionConfig?.enabled && historyLeafMessageId !== void 0) {
|
|
2717
|
+
aiLogger?.info?.("Compression", "Skipping compression for edited branch request", {
|
|
2718
|
+
conversationId,
|
|
2719
|
+
historyLeafMessageId
|
|
2720
|
+
});
|
|
2721
|
+
}
|
|
2722
|
+
if (abortSignal?.aborted) {
|
|
2723
|
+
handler.emitStatus("aborted", [], { force: true });
|
|
2724
|
+
onEvent({ type: "done", isFinished: true, usage: handler.cloneUsage() });
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
let history = [];
|
|
2728
|
+
try {
|
|
2729
|
+
history = convManager2.getHistoryForAgent(conversationId, void 0, historyLeafMessageId);
|
|
2730
|
+
} catch {
|
|
2731
|
+
}
|
|
2732
|
+
handler.emitStatus("preparing", [], { pendingUserMessage: userMessage, force: true });
|
|
2733
|
+
const steerMessage = t("ai.agent.answerWithoutTools");
|
|
2734
|
+
let cachedMessages = [];
|
|
2735
|
+
try {
|
|
2736
|
+
const result = await runAgentCore({
|
|
2737
|
+
piModel,
|
|
2738
|
+
apiKey: llmConfig.apiKey,
|
|
2739
|
+
systemPrompt,
|
|
2740
|
+
tools,
|
|
2741
|
+
history,
|
|
2742
|
+
userMessage,
|
|
2743
|
+
maxToolRounds: 5,
|
|
2744
|
+
abortSignal,
|
|
2745
|
+
steerMessage,
|
|
2746
|
+
thinkingLevel,
|
|
2747
|
+
onConvertToLlm: (filteredMessages) => {
|
|
2748
|
+
cachedMessages = filteredMessages;
|
|
2749
|
+
},
|
|
2750
|
+
onEvent: (coreEvent) => handler.handleCoreEvent(coreEvent, cachedMessages),
|
|
2751
|
+
onDebugContext: (messages) => {
|
|
2752
|
+
try {
|
|
2753
|
+
convManager2.setPendingDebugContext(conversationId, JSON.stringify(messages, null, 2));
|
|
2754
|
+
} catch {
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
});
|
|
2758
|
+
if (result.error) {
|
|
2759
|
+
const friendlyMessage = formatAIError(result.error);
|
|
2760
|
+
onEvent({ type: "error", error: { name: "AgentError", message: friendlyMessage } });
|
|
2684
2761
|
}
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
);
|
|
2693
|
-
}
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2762
|
+
handler.emitStatus("completed", cachedMessages, { force: true });
|
|
2763
|
+
onEvent({ type: "done", isFinished: true, usage: result.usage });
|
|
2764
|
+
} catch (error) {
|
|
2765
|
+
const friendlyMessage = formatAIError(error);
|
|
2766
|
+
aiLogger?.error("ServerAgent", "Agent execution error", { error: String(error) });
|
|
2767
|
+
handler.emitStatus("error", cachedMessages, { force: true });
|
|
2768
|
+
onEvent({ type: "error", error: { name: "AgentError", message: friendlyMessage } });
|
|
2769
|
+
onEvent({ type: "done", isFinished: true, usage: handler.cloneUsage() });
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
// src/ai/agent-stream-runner.ts
|
|
2774
|
+
function getAiDir(dbManager2) {
|
|
2775
|
+
const pathProvider = dbManager2["pathProvider"];
|
|
2776
|
+
if (!pathProvider) {
|
|
2777
|
+
throw Object.assign(new Error("PathProvider not available"), { statusCode: 500 });
|
|
2778
|
+
}
|
|
2779
|
+
return pathProvider.getAiDataDir();
|
|
2780
|
+
}
|
|
2781
|
+
function createCliRunAgentStream(dbManager2, convManager2) {
|
|
2782
|
+
return async (params, onEvent, abortSignal) => {
|
|
2699
2783
|
const {
|
|
2700
2784
|
userMessage,
|
|
2701
2785
|
conversationId,
|
|
@@ -2704,27 +2788,13 @@ data: ${JSON.stringify(data)}
|
|
|
2704
2788
|
chatType,
|
|
2705
2789
|
locale,
|
|
2706
2790
|
assistantId,
|
|
2791
|
+
skillId,
|
|
2792
|
+
enableAutoSkill,
|
|
2707
2793
|
compressionConfig,
|
|
2708
2794
|
ownerInfo,
|
|
2709
2795
|
mentionedMembers,
|
|
2710
2796
|
thinkingLevel
|
|
2711
|
-
} =
|
|
2712
|
-
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2713
|
-
const abortController = new AbortController();
|
|
2714
|
-
activeAgentAborts.set(requestId, abortController);
|
|
2715
|
-
reply.raw.writeHead(200, {
|
|
2716
|
-
"Content-Type": "text/event-stream",
|
|
2717
|
-
"Cache-Control": "no-cache",
|
|
2718
|
-
Connection: "keep-alive",
|
|
2719
|
-
"X-Request-Id": requestId
|
|
2720
|
-
});
|
|
2721
|
-
const sendSSE = (event, data) => {
|
|
2722
|
-
reply.raw.write(`event: ${event}
|
|
2723
|
-
data: ${JSON.stringify(data)}
|
|
2724
|
-
|
|
2725
|
-
`);
|
|
2726
|
-
};
|
|
2727
|
-
sendSSE("meta", { requestId });
|
|
2797
|
+
} = params;
|
|
2728
2798
|
const aiDataDir = getAiDir(dbManager2);
|
|
2729
2799
|
let assistantSystemPrompt;
|
|
2730
2800
|
if (assistantId) {
|
|
@@ -2739,191 +2809,73 @@ data: ${JSON.stringify(data)}
|
|
|
2739
2809
|
const maxToolResultTokens = Math.floor(contextWindow * (maxToolResultPercent / 100));
|
|
2740
2810
|
const db = dbManager2.open?.(sessionId);
|
|
2741
2811
|
const agentTools = db ? adaptToolsForAgent(AGENT_TOOL_REGISTRY, () => ({ db, sessionId, locale }), { maxToolResultTokens }) : [];
|
|
2742
|
-
const
|
|
2743
|
-
|
|
2812
|
+
const skillMgr = new SkillManager(aiDataDir);
|
|
2813
|
+
skillMgr.init();
|
|
2744
2814
|
const toolNames = agentTools.map((t) => t.name);
|
|
2745
|
-
|
|
2746
|
-
|
|
2815
|
+
let resolvedSkillDef;
|
|
2816
|
+
let resolvedSkillMenu;
|
|
2817
|
+
if (skillId) {
|
|
2818
|
+
const def = skillMgr.getSkillConfig(skillId);
|
|
2819
|
+
if (def) resolvedSkillDef = { name: def.name, prompt: def.prompt };
|
|
2820
|
+
} else if (enableAutoSkill) {
|
|
2821
|
+
const menu = skillMgr.getSkillMenu(chatType ?? "group", toolNames);
|
|
2822
|
+
if (menu) resolvedSkillMenu = menu;
|
|
2823
|
+
}
|
|
2824
|
+
if (resolvedSkillMenu) {
|
|
2747
2825
|
const activateSkillTool = createActivateSkillTool({
|
|
2748
2826
|
chatType: chatType ?? "group",
|
|
2749
2827
|
allowedTools: toolNames,
|
|
2750
2828
|
locale,
|
|
2751
|
-
getSkillConfig: (id) =>
|
|
2829
|
+
getSkillConfig: (id) => skillMgr.getSkillConfig(id)
|
|
2752
2830
|
});
|
|
2753
2831
|
agentTools.push(activateSkillTool);
|
|
2754
2832
|
}
|
|
2755
|
-
const
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
if (db) {
|
|
2777
|
-
try {
|
|
2778
|
-
const overview = getChatOverview(db, 5);
|
|
2779
|
-
if (overview) {
|
|
2780
|
-
dataSnapshot = {
|
|
2781
|
-
name: overview.name,
|
|
2782
|
-
platform: overview.platform,
|
|
2783
|
-
type: overview.type,
|
|
2784
|
-
totalMessages: overview.totalMessages,
|
|
2785
|
-
totalMembers: overview.totalMembers,
|
|
2786
|
-
firstMessageTs: overview.firstMessageTs,
|
|
2787
|
-
lastMessageTs: overview.lastMessageTs,
|
|
2788
|
-
capturedAt: Math.floor(Date.now() / 1e3)
|
|
2789
|
-
};
|
|
2790
|
-
}
|
|
2791
|
-
} catch {
|
|
2792
|
-
}
|
|
2793
|
-
}
|
|
2794
|
-
await runServerAgent({
|
|
2795
|
-
userMessage,
|
|
2796
|
-
conversationId,
|
|
2797
|
-
historyLeafMessageId,
|
|
2798
|
-
chatType,
|
|
2799
|
-
locale,
|
|
2800
|
-
assistantSystemPrompt,
|
|
2801
|
-
skillMenu,
|
|
2802
|
-
compressionConfig: resolvedCompression,
|
|
2803
|
-
tools: agentTools,
|
|
2804
|
-
aiDataDir,
|
|
2805
|
-
convManager: convManager2,
|
|
2806
|
-
onEvent,
|
|
2807
|
-
abortSignal: abortController.signal,
|
|
2808
|
-
ownerInfo,
|
|
2809
|
-
mentionedMembers,
|
|
2810
|
-
dataSnapshot,
|
|
2811
|
-
thinkingLevel
|
|
2812
|
-
});
|
|
2813
|
-
} catch (error) {
|
|
2814
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
2815
|
-
sendSSE("error", { type: "error", error: { name: "ServerError", message: msg } });
|
|
2816
|
-
sendSSE("done", { type: "done", isFinished: true });
|
|
2817
|
-
activeAgentAborts.delete(requestId);
|
|
2818
|
-
reply.raw.end();
|
|
2819
|
-
}
|
|
2820
|
-
});
|
|
2821
|
-
server2.post("/_web/ai/agent/abort", async (request) => {
|
|
2822
|
-
const { requestId } = request.body;
|
|
2823
|
-
const controller = activeAgentAborts.get(requestId);
|
|
2824
|
-
if (controller) {
|
|
2825
|
-
controller.abort();
|
|
2826
|
-
activeAgentAborts.delete(requestId);
|
|
2827
|
-
return { success: true };
|
|
2828
|
-
}
|
|
2829
|
-
return { success: false };
|
|
2830
|
-
});
|
|
2831
|
-
server2.get("/_web/ai/debug/schema", async (_request, reply) => {
|
|
2832
|
-
if (!convManager2) {
|
|
2833
|
-
return reply.code(503).send({ error: "AI conversation manager not available" });
|
|
2834
|
-
}
|
|
2835
|
-
return convManager2.getAiSchema();
|
|
2836
|
-
});
|
|
2837
|
-
server2.post("/_web/ai/debug/execute-sql", async (request, reply) => {
|
|
2838
|
-
if (!convManager2) {
|
|
2839
|
-
return reply.code(503).send({ error: "AI conversation manager not available" });
|
|
2840
|
-
}
|
|
2841
|
-
const { sql } = request.body;
|
|
2842
|
-
if (!sql || typeof sql !== "string") {
|
|
2843
|
-
return reply.code(400).send({ error: "sql is required" });
|
|
2844
|
-
}
|
|
2845
|
-
try {
|
|
2846
|
-
return convManager2.executeAiSQL(sql);
|
|
2847
|
-
} catch (e) {
|
|
2848
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
2849
|
-
return reply.code(400).send({ error: msg });
|
|
2850
|
-
}
|
|
2851
|
-
});
|
|
2852
|
-
server2.post("/_web/ai/debug/clear-debug-context", async (_request, reply) => {
|
|
2853
|
-
if (!convManager2) {
|
|
2854
|
-
return reply.code(503).send({ error: "AI conversation manager not available" });
|
|
2855
|
-
}
|
|
2856
|
-
const cleared = convManager2.clearAllDebugContext();
|
|
2857
|
-
return { success: true, cleared };
|
|
2858
|
-
});
|
|
2859
|
-
}
|
|
2860
|
-
|
|
2861
|
-
// src/http/routes/preferences.ts
|
|
2862
|
-
var prefManager = null;
|
|
2863
|
-
function registerPreferencesRoutes(server2, pathProvider) {
|
|
2864
|
-
if (!prefManager) {
|
|
2865
|
-
prefManager = new PreferencesManager(pathProvider.getSystemDir());
|
|
2866
|
-
}
|
|
2867
|
-
server2.get("/_web/preferences", async () => {
|
|
2868
|
-
return prefManager.load();
|
|
2869
|
-
});
|
|
2870
|
-
server2.patch("/_web/preferences", async (request) => {
|
|
2871
|
-
return prefManager.save(request.body);
|
|
2872
|
-
});
|
|
2873
|
-
server2.get("/_web/preferences/ui-config", async () => {
|
|
2874
|
-
const config = loadConfig();
|
|
2875
|
-
return config.ui;
|
|
2876
|
-
});
|
|
2877
|
-
server2.patch("/_web/preferences/ui-config", async (request) => {
|
|
2878
|
-
try {
|
|
2879
|
-
for (const [key, value] of Object.entries(request.body)) {
|
|
2880
|
-
if (value !== void 0) {
|
|
2881
|
-
writeConfigField("ui", key, value);
|
|
2833
|
+
const resolvedCompression = compressionConfig?.enabled ? {
|
|
2834
|
+
enabled: true,
|
|
2835
|
+
tokenThresholdPercent: compressionConfig.tokenThresholdPercent ?? 75,
|
|
2836
|
+
bufferSizePercent: compressionConfig.bufferSizePercent ?? 20,
|
|
2837
|
+
maxToolResultPercent: compressionConfig.maxToolResultPercent
|
|
2838
|
+
} : void 0;
|
|
2839
|
+
let dataSnapshot;
|
|
2840
|
+
if (db) {
|
|
2841
|
+
try {
|
|
2842
|
+
const overview = getChatOverview(db, 5);
|
|
2843
|
+
if (overview) {
|
|
2844
|
+
dataSnapshot = {
|
|
2845
|
+
name: overview.name,
|
|
2846
|
+
platform: overview.platform,
|
|
2847
|
+
type: overview.type,
|
|
2848
|
+
totalMessages: overview.totalMessages,
|
|
2849
|
+
totalMembers: overview.totalMembers,
|
|
2850
|
+
firstMessageTs: overview.firstMessageTs,
|
|
2851
|
+
lastMessageTs: overview.lastMessageTs,
|
|
2852
|
+
capturedAt: Math.floor(Date.now() / 1e3)
|
|
2853
|
+
};
|
|
2882
2854
|
}
|
|
2855
|
+
} catch {
|
|
2883
2856
|
}
|
|
2884
|
-
return { success: true };
|
|
2885
|
-
} catch (err) {
|
|
2886
|
-
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
2887
|
-
}
|
|
2888
|
-
});
|
|
2889
|
-
server2.get("/_web/preferences/locale", async () => {
|
|
2890
|
-
const config = loadConfig();
|
|
2891
|
-
return { lang: config.locale.lang };
|
|
2892
|
-
});
|
|
2893
|
-
server2.patch("/_web/preferences/locale", async (request) => {
|
|
2894
|
-
try {
|
|
2895
|
-
writeConfigField("locale", "lang", request.body.lang);
|
|
2896
|
-
return { success: true };
|
|
2897
|
-
} catch (err) {
|
|
2898
|
-
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
2899
|
-
}
|
|
2900
|
-
});
|
|
2901
|
-
}
|
|
2902
|
-
|
|
2903
|
-
// src/http/routes/proxy.ts
|
|
2904
|
-
var PROXY_TARGET = "https://chatlab.fun";
|
|
2905
|
-
function registerProxyRoutes(server2) {
|
|
2906
|
-
server2.get("/_proxy/chatlab.fun/*", async (request, reply) => {
|
|
2907
|
-
const subPath = request.params["*"] ?? "";
|
|
2908
|
-
const queryString = new URLSearchParams(
|
|
2909
|
-
request.query
|
|
2910
|
-
).toString();
|
|
2911
|
-
const targetUrl = queryString ? `${PROXY_TARGET}/${subPath}?${queryString}` : `${PROXY_TARGET}/${subPath}`;
|
|
2912
|
-
try {
|
|
2913
|
-
const upstream = await fetch(targetUrl, {
|
|
2914
|
-
headers: { Accept: request.headers.accept ?? "*/*" },
|
|
2915
|
-
signal: AbortSignal.timeout(1e4)
|
|
2916
|
-
});
|
|
2917
|
-
const contentType = upstream.headers.get("content-type") ?? "application/octet-stream";
|
|
2918
|
-
const body = Buffer.from(await upstream.arrayBuffer());
|
|
2919
|
-
reply.code(upstream.status).header("content-type", contentType).send(body);
|
|
2920
|
-
} catch (err) {
|
|
2921
|
-
reply.code(502).send({
|
|
2922
|
-
error: "Proxy error",
|
|
2923
|
-
message: err instanceof Error ? err.message : String(err)
|
|
2924
|
-
});
|
|
2925
2857
|
}
|
|
2926
|
-
|
|
2858
|
+
await runServerAgent({
|
|
2859
|
+
userMessage,
|
|
2860
|
+
conversationId,
|
|
2861
|
+
historyLeafMessageId,
|
|
2862
|
+
chatType,
|
|
2863
|
+
locale,
|
|
2864
|
+
assistantSystemPrompt,
|
|
2865
|
+
skillMenu: resolvedSkillMenu,
|
|
2866
|
+
skillDef: resolvedSkillDef,
|
|
2867
|
+
compressionConfig: resolvedCompression,
|
|
2868
|
+
tools: agentTools,
|
|
2869
|
+
aiDataDir,
|
|
2870
|
+
convManager: convManager2,
|
|
2871
|
+
onEvent,
|
|
2872
|
+
abortSignal,
|
|
2873
|
+
ownerInfo,
|
|
2874
|
+
mentionedMembers,
|
|
2875
|
+
dataSnapshot,
|
|
2876
|
+
thinkingLevel
|
|
2877
|
+
});
|
|
2878
|
+
};
|
|
2927
2879
|
}
|
|
2928
2880
|
|
|
2929
2881
|
// ../../packages/sync/src/types.ts
|
|
@@ -2932,12 +2884,12 @@ var noop = () => {
|
|
|
2932
2884
|
var NOOP_LOGGER = { info: noop, warn: noop, error: noop };
|
|
2933
2885
|
|
|
2934
2886
|
// ../../packages/sync/src/data-source-manager.ts
|
|
2935
|
-
import * as
|
|
2936
|
-
import * as
|
|
2937
|
-
import * as
|
|
2887
|
+
import * as fs8 from "fs";
|
|
2888
|
+
import * as path10 from "path";
|
|
2889
|
+
import * as crypto from "crypto";
|
|
2938
2890
|
var CONFIG_FILE = "data-sources.json";
|
|
2939
2891
|
function generateId(prefix = "ds") {
|
|
2940
|
-
return `${prefix}_${
|
|
2892
|
+
return `${prefix}_${crypto.randomBytes(6).toString("hex")}`;
|
|
2941
2893
|
}
|
|
2942
2894
|
function normalizeBaseUrl(input) {
|
|
2943
2895
|
let url = input.trim().replace(/\/+$/, "");
|
|
@@ -2961,17 +2913,17 @@ var DataSourceManager = class {
|
|
|
2961
2913
|
this.logger = logger2 ?? NOOP_LOGGER;
|
|
2962
2914
|
}
|
|
2963
2915
|
getConfigPath() {
|
|
2964
|
-
return
|
|
2916
|
+
return path10.join(this.configDir, CONFIG_FILE);
|
|
2965
2917
|
}
|
|
2966
2918
|
ensureDir() {
|
|
2967
|
-
|
|
2919
|
+
fs8.mkdirSync(this.configDir, { recursive: true });
|
|
2968
2920
|
}
|
|
2969
2921
|
// ==================== Load / Save ====================
|
|
2970
2922
|
loadAll() {
|
|
2971
2923
|
try {
|
|
2972
2924
|
const filePath = this.getConfigPath();
|
|
2973
|
-
if (
|
|
2974
|
-
const raw =
|
|
2925
|
+
if (fs8.existsSync(filePath)) {
|
|
2926
|
+
const raw = fs8.readFileSync(filePath, "utf-8");
|
|
2975
2927
|
const parsed = JSON.parse(raw);
|
|
2976
2928
|
if (!isValidDataSourceArray(parsed)) {
|
|
2977
2929
|
this.logger.warn("[DataSource] Incompatible config format detected, returning empty.");
|
|
@@ -2990,7 +2942,7 @@ var DataSourceManager = class {
|
|
|
2990
2942
|
saveAll(sources) {
|
|
2991
2943
|
try {
|
|
2992
2944
|
this.ensureDir();
|
|
2993
|
-
|
|
2945
|
+
fs8.writeFileSync(this.getConfigPath(), JSON.stringify(sources, null, 2), "utf-8");
|
|
2994
2946
|
} catch (err) {
|
|
2995
2947
|
this.logger.error("[DataSource] Failed to save config", err);
|
|
2996
2948
|
}
|
|
@@ -3115,8 +3067,8 @@ function parseRemoteSessionsResponse(body) {
|
|
|
3115
3067
|
}
|
|
3116
3068
|
|
|
3117
3069
|
// ../../packages/sync/src/pull-engine.ts
|
|
3118
|
-
import * as
|
|
3119
|
-
import * as
|
|
3070
|
+
import * as fs9 from "fs";
|
|
3071
|
+
import * as crypto2 from "crypto";
|
|
3120
3072
|
var MAX_PAGES_PER_PULL = 5e3;
|
|
3121
3073
|
var PULL_OVERLAP_SECONDS = 60;
|
|
3122
3074
|
function buildPullUrl(baseUrl, remoteSessionId, params) {
|
|
@@ -3128,14 +3080,14 @@ function buildPullUrl(baseUrl, remoteSessionId, params) {
|
|
|
3128
3080
|
return base + "?" + qs.join("&");
|
|
3129
3081
|
}
|
|
3130
3082
|
function deriveLocalSessionId(baseUrl, remoteSessionId) {
|
|
3131
|
-
const hash =
|
|
3083
|
+
const hash = crypto2.createHash("sha256").update(`${baseUrl}\0${remoteSessionId}`).digest("hex").slice(0, 12);
|
|
3132
3084
|
return `remote_${hash}`;
|
|
3133
3085
|
}
|
|
3134
3086
|
function parseSyncFromFile(filePath) {
|
|
3135
3087
|
try {
|
|
3136
3088
|
const isJsonl = filePath.endsWith(".jsonl");
|
|
3137
3089
|
if (isJsonl) {
|
|
3138
|
-
const content =
|
|
3090
|
+
const content = fs9.readFileSync(filePath, "utf-8");
|
|
3139
3091
|
const lines = content.trimEnd().split("\n");
|
|
3140
3092
|
for (let i = lines.length - 1; i >= Math.max(0, lines.length - 5); i--) {
|
|
3141
3093
|
try {
|
|
@@ -3154,7 +3106,7 @@ function parseSyncFromFile(filePath) {
|
|
|
3154
3106
|
}
|
|
3155
3107
|
return null;
|
|
3156
3108
|
}
|
|
3157
|
-
const raw =
|
|
3109
|
+
const raw = fs9.readFileSync(filePath, "utf-8");
|
|
3158
3110
|
const parsed = JSON.parse(raw);
|
|
3159
3111
|
if (parsed.sync && typeof parsed.sync === "object") {
|
|
3160
3112
|
const s = parsed.sync;
|
|
@@ -3168,7 +3120,7 @@ function parseSyncFromFile(filePath) {
|
|
|
3168
3120
|
function fileContainsMessages(filePath) {
|
|
3169
3121
|
try {
|
|
3170
3122
|
if (filePath.endsWith(".jsonl")) {
|
|
3171
|
-
const content =
|
|
3123
|
+
const content = fs9.readFileSync(filePath, "utf-8");
|
|
3172
3124
|
for (const line of content.split("\n")) {
|
|
3173
3125
|
const trimmed = line.trim();
|
|
3174
3126
|
if (!trimmed) continue;
|
|
@@ -3181,7 +3133,7 @@ function fileContainsMessages(filePath) {
|
|
|
3181
3133
|
}
|
|
3182
3134
|
return false;
|
|
3183
3135
|
}
|
|
3184
|
-
const raw =
|
|
3136
|
+
const raw = fs9.readFileSync(filePath, "utf-8");
|
|
3185
3137
|
const parsed = JSON.parse(raw);
|
|
3186
3138
|
return Array.isArray(parsed.messages) && parsed.messages.length > 0;
|
|
3187
3139
|
} catch {
|
|
@@ -3190,7 +3142,7 @@ function fileContainsMessages(filePath) {
|
|
|
3190
3142
|
}
|
|
3191
3143
|
function cleanupTempFile(filePath) {
|
|
3192
3144
|
try {
|
|
3193
|
-
if (
|
|
3145
|
+
if (fs9.existsSync(filePath)) fs9.unlinkSync(filePath);
|
|
3194
3146
|
} catch {
|
|
3195
3147
|
}
|
|
3196
3148
|
}
|
|
@@ -3250,7 +3202,7 @@ var PullEngine = class {
|
|
|
3250
3202
|
limit: ds.pullLimit
|
|
3251
3203
|
});
|
|
3252
3204
|
try {
|
|
3253
|
-
const stat2 =
|
|
3205
|
+
const stat2 = fs9.statSync(tempFile);
|
|
3254
3206
|
this.logger.info(`[Pull] "${sess.name}" page ${pageCount}: fetched ${stat2.size} bytes`);
|
|
3255
3207
|
const sync0 = parseSyncFromFile(tempFile);
|
|
3256
3208
|
if (stat2.size < 1024) {
|
|
@@ -3268,7 +3220,7 @@ var PullEngine = class {
|
|
|
3268
3220
|
since,
|
|
3269
3221
|
limit: ds.pullLimit
|
|
3270
3222
|
});
|
|
3271
|
-
const retryStat =
|
|
3223
|
+
const retryStat = fs9.statSync(retryFile);
|
|
3272
3224
|
this.logger.info(`[Pull] "${sess.name}" retry ${ri + 1}: fetched ${retryStat.size} bytes`);
|
|
3273
3225
|
const retrySync = parseSyncFromFile(retryFile);
|
|
3274
3226
|
if (retryStat.size < 1024 && !fileContainsMessages(retryFile) && retrySync?.hasMore === false) {
|
|
@@ -3542,13 +3494,13 @@ function reloadTimer(dsId, skipInitialPull = false) {
|
|
|
3542
3494
|
}
|
|
3543
3495
|
|
|
3544
3496
|
// src/sync/adapters.ts
|
|
3545
|
-
import * as
|
|
3497
|
+
import * as fs10 from "fs";
|
|
3546
3498
|
import * as os4 from "os";
|
|
3547
|
-
import * as
|
|
3548
|
-
import * as
|
|
3499
|
+
import * as path11 from "path";
|
|
3500
|
+
import * as crypto3 from "crypto";
|
|
3549
3501
|
function getTempFilePath(ext) {
|
|
3550
|
-
const id =
|
|
3551
|
-
return
|
|
3502
|
+
const id = crypto3.randomBytes(8).toString("hex");
|
|
3503
|
+
return path11.join(os4.tmpdir(), `chatlab-pull-${id}${ext}`);
|
|
3552
3504
|
}
|
|
3553
3505
|
var NodeFetcher = class {
|
|
3554
3506
|
async fetchToTempFile(baseUrl, remoteSessionId, token, params) {
|
|
@@ -3565,14 +3517,14 @@ var NodeFetcher = class {
|
|
|
3565
3517
|
const isJsonl = contentType.includes("ndjson") || contentType.includes("jsonl");
|
|
3566
3518
|
const tempFile = getTempFilePath(isJsonl ? ".jsonl" : ".json");
|
|
3567
3519
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
3568
|
-
|
|
3520
|
+
fs10.writeFileSync(tempFile, buffer);
|
|
3569
3521
|
return tempFile;
|
|
3570
3522
|
}
|
|
3571
3523
|
};
|
|
3572
3524
|
function resolveNativeBinding2() {
|
|
3573
3525
|
if (process.versions.electron) return void 0;
|
|
3574
3526
|
const nativePath = resolveCliPath("native/better_sqlite3.node");
|
|
3575
|
-
if (
|
|
3527
|
+
if (fs10.existsSync(nativePath)) return nativePath;
|
|
3576
3528
|
return void 0;
|
|
3577
3529
|
}
|
|
3578
3530
|
var DirectImporter = class {
|
|
@@ -3586,7 +3538,7 @@ var DirectImporter = class {
|
|
|
3586
3538
|
}
|
|
3587
3539
|
sessionExists(sessionId) {
|
|
3588
3540
|
const dbPath = this.dbManager.getDbPath(sessionId);
|
|
3589
|
-
if (!
|
|
3541
|
+
if (!fs10.existsSync(dbPath)) return false;
|
|
3590
3542
|
try {
|
|
3591
3543
|
const db = openBetterSqliteDatabase(dbPath, { readonly: true, nativeBinding: this.nativeBinding });
|
|
3592
3544
|
const row = db.prepare("SELECT COUNT(*) as cnt FROM sqlite_master WHERE type='table' AND name='message'").get();
|
|
@@ -3594,7 +3546,7 @@ var DirectImporter = class {
|
|
|
3594
3546
|
if (row.cnt === 0) {
|
|
3595
3547
|
this.logger.warn(`[DirectImporter] DB file exists but has no message table: ${sessionId}, removing`);
|
|
3596
3548
|
try {
|
|
3597
|
-
|
|
3549
|
+
fs10.unlinkSync(dbPath);
|
|
3598
3550
|
} catch {
|
|
3599
3551
|
}
|
|
3600
3552
|
return false;
|
|
@@ -3603,7 +3555,7 @@ var DirectImporter = class {
|
|
|
3603
3555
|
} catch {
|
|
3604
3556
|
this.logger.warn(`[DirectImporter] Cannot validate DB file: ${sessionId}, removing`);
|
|
3605
3557
|
try {
|
|
3606
|
-
|
|
3558
|
+
fs10.unlinkSync(dbPath);
|
|
3607
3559
|
} catch {
|
|
3608
3560
|
}
|
|
3609
3561
|
return false;
|
|
@@ -3723,8 +3675,8 @@ function registerAutomationRoutes(server2, ctx) {
|
|
|
3723
3675
|
if (request.query.deleteData === "true" && removed.targetSessionId) {
|
|
3724
3676
|
dbManager2.close(removed.targetSessionId);
|
|
3725
3677
|
const dbPath = dbManager2.getDbPath(removed.targetSessionId);
|
|
3726
|
-
const
|
|
3727
|
-
if (
|
|
3678
|
+
const fs12 = await import("fs");
|
|
3679
|
+
if (fs12.existsSync(dbPath)) fs12.unlinkSync(dbPath);
|
|
3728
3680
|
}
|
|
3729
3681
|
return { success: true };
|
|
3730
3682
|
});
|
|
@@ -3800,15 +3752,30 @@ function cleanupSync() {
|
|
|
3800
3752
|
var server = null;
|
|
3801
3753
|
var dbManager = null;
|
|
3802
3754
|
var convManager = null;
|
|
3755
|
+
function createFileConfigStorage(aiDataDir) {
|
|
3756
|
+
return {
|
|
3757
|
+
readJson(key) {
|
|
3758
|
+
try {
|
|
3759
|
+
return JSON.parse(fs11.readFileSync(`${aiDataDir}/${key}.json`, "utf-8"));
|
|
3760
|
+
} catch {
|
|
3761
|
+
return null;
|
|
3762
|
+
}
|
|
3763
|
+
},
|
|
3764
|
+
writeJson(key, data) {
|
|
3765
|
+
if (!fs11.existsSync(aiDataDir)) fs11.mkdirSync(aiDataDir, { recursive: true });
|
|
3766
|
+
fs11.writeFileSync(`${aiDataDir}/${key}.json`, JSON.stringify(data, null, 2), "utf-8");
|
|
3767
|
+
}
|
|
3768
|
+
};
|
|
3769
|
+
}
|
|
3803
3770
|
function resolveNativeBinding3() {
|
|
3804
3771
|
if (process.versions.electron) return void 0;
|
|
3805
3772
|
const nativePath = resolveCliPath("native/better_sqlite3.node");
|
|
3806
|
-
if (
|
|
3773
|
+
if (fs11.existsSync(nativePath)) return nativePath;
|
|
3807
3774
|
return void 0;
|
|
3808
3775
|
}
|
|
3809
3776
|
function ensureToken(config) {
|
|
3810
3777
|
if (config.api.token) return config.api.token;
|
|
3811
|
-
const token = `clb_${
|
|
3778
|
+
const token = `clb_${crypto4.randomBytes(32).toString("hex")}`;
|
|
3812
3779
|
try {
|
|
3813
3780
|
writeConfigField("api", "token", token);
|
|
3814
3781
|
} catch {
|
|
@@ -3819,10 +3786,19 @@ async function startHttpServer(options) {
|
|
|
3819
3786
|
if (server) {
|
|
3820
3787
|
throw new Error("HTTP server is already running");
|
|
3821
3788
|
}
|
|
3822
|
-
|
|
3789
|
+
let config = loadConfig();
|
|
3823
3790
|
const port = options?.port ?? config.api.port;
|
|
3824
3791
|
const host = options?.host ?? config.api.host;
|
|
3825
3792
|
const token = options?.token ?? ensureToken(config);
|
|
3793
|
+
const pendingMigration = applyPendingNodeDataDirMigrationIfNeeded();
|
|
3794
|
+
if (!pendingMigration.skipped) {
|
|
3795
|
+
if (pendingMigration.success) {
|
|
3796
|
+
console.log("[Migration] Pending data directory migration completed");
|
|
3797
|
+
config = loadConfig();
|
|
3798
|
+
} else {
|
|
3799
|
+
console.error("[Migration] Pending data directory migration failed:", pendingMigration.error);
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3826
3802
|
const userDataDir = config.data.user_data_dir || void 0;
|
|
3827
3803
|
const pathProvider = new NodePathProvider(userDataDir);
|
|
3828
3804
|
pathProvider.ensureAllDirs();
|
|
@@ -3843,24 +3819,43 @@ async function startHttpServer(options) {
|
|
|
3843
3819
|
await migrationRunner.run();
|
|
3844
3820
|
const nativeBinding = resolveNativeBinding3();
|
|
3845
3821
|
dbManager = new DatabaseManager(pathProvider, { nativeBinding });
|
|
3846
|
-
|
|
3822
|
+
const aiDataDir = pathProvider.getAiDataDir();
|
|
3823
|
+
convManager = new AIConversationManager(aiDataDir, { nativeBinding });
|
|
3824
|
+
const assistantManager = getAssistantManager(aiDataDir);
|
|
3825
|
+
const skillManagerCore = getSkillManagerCore(aiDataDir);
|
|
3826
|
+
const llmConfigStore = new LLMConfigStore(createFileConfigStorage(aiDataDir), {
|
|
3827
|
+
resolveApiKey: (provider, authProfile) => resolveApiKey(provider, authProfile) || void 0,
|
|
3828
|
+
onApiKeyCreated: (config2, apiKey) => {
|
|
3829
|
+
const profileName = config2.name?.toLowerCase().replace(/\s+/g, "-") || config2.provider;
|
|
3830
|
+
writeAuthProfile(profileName, { type: "api_key", provider: config2.provider, key: apiKey });
|
|
3831
|
+
return profileName;
|
|
3832
|
+
}
|
|
3833
|
+
});
|
|
3847
3834
|
initServerAiLogger(pathProvider.getLogsDir());
|
|
3848
3835
|
setAuthToken(token);
|
|
3836
|
+
setRequireAuth(!!(options?.requireAuth ?? config.api.require_auth));
|
|
3849
3837
|
server = createServer();
|
|
3850
3838
|
const multipart = await import("@fastify/multipart");
|
|
3851
3839
|
await server.register(multipart.default, {
|
|
3852
3840
|
limits: { fileSize: 1024 * 1024 * 1024 }
|
|
3853
3841
|
// 1GB
|
|
3854
3842
|
});
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3843
|
+
registerWebRoutes(server, dbManager, {
|
|
3844
|
+
pathProvider,
|
|
3845
|
+
nativeBinding,
|
|
3846
|
+
aiContext: {
|
|
3847
|
+
aiDataDir,
|
|
3848
|
+
convManager,
|
|
3849
|
+
assistantManager,
|
|
3850
|
+
skillManagerCore,
|
|
3851
|
+
llmConfigStore,
|
|
3852
|
+
customProviderStore: new CustomProviderStore(createFileConfigStorage(aiDataDir)),
|
|
3853
|
+
customModelStore: new CustomModelStore(createFileConfigStorage(aiDataDir)),
|
|
3854
|
+
runAgentStream: createCliRunAgentStream(dbManager, convManager)
|
|
3855
|
+
}
|
|
3856
|
+
});
|
|
3861
3857
|
initSync(server, dbManager, pathProvider, { port, host, token });
|
|
3862
|
-
if (options?.webRoot &&
|
|
3863
|
-
setWebMode(true);
|
|
3858
|
+
if (options?.webRoot && fs11.existsSync(options.webRoot)) {
|
|
3864
3859
|
registerProxyRoutes(server);
|
|
3865
3860
|
const fastifyStatic = await import("@fastify/static");
|
|
3866
3861
|
await server.register(fastifyStatic.default, {
|
|
@@ -3890,19 +3885,15 @@ async function stopHttpServer() {
|
|
|
3890
3885
|
dbManager = null;
|
|
3891
3886
|
}
|
|
3892
3887
|
closeServerAiLogger();
|
|
3893
|
-
|
|
3888
|
+
setRequireAuth(false);
|
|
3894
3889
|
server = null;
|
|
3895
3890
|
}
|
|
3896
3891
|
}
|
|
3897
3892
|
|
|
3898
3893
|
export {
|
|
3899
3894
|
createServer,
|
|
3900
|
-
registerSystemRoutes,
|
|
3901
|
-
registerSessionRoutes,
|
|
3902
3895
|
registerWebRoutes,
|
|
3903
|
-
registerNlpRoutes,
|
|
3904
|
-
registerAiRoutes,
|
|
3905
3896
|
startHttpServer,
|
|
3906
3897
|
stopHttpServer
|
|
3907
3898
|
};
|
|
3908
|
-
//# sourceMappingURL=chunk-
|
|
3899
|
+
//# sourceMappingURL=chunk-NE4A6PND.mjs.map
|