chatlab-cli 0.1.0 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-YAOFJ73U.mjs → chunk-4GI2UPCS.mjs} +80 -129
- package/dist/chunk-4GI2UPCS.mjs.map +1 -0
- package/dist/{chunk-JMRDNHIR.mjs → chunk-7HQWGLX6.mjs} +829 -280
- package/dist/chunk-7HQWGLX6.mjs.map +1 -0
- package/dist/{chunk-DL3BFEAH.mjs → chunk-AIR7EAF4.mjs} +2 -2
- package/dist/chunk-AIR7EAF4.mjs.map +1 -0
- package/dist/{chunk-PF4QFHC6.mjs → chunk-LI4X52E7.mjs} +1050 -864
- package/dist/chunk-LI4X52E7.mjs.map +1 -0
- package/dist/{chunk-OC73KSMV.mjs → chunk-OQVKZTVD.mjs} +2 -2
- package/dist/{chunk-P4N5GEFF.mjs → chunk-OZWXLRJL.mjs} +9 -9
- package/dist/cli.mjs +2 -2
- package/dist/{http-PQEOFGJK.mjs → http-DHQI4G5J.mjs} +6 -6
- package/dist/{import-UR2IE2O7.mjs → import-CQWGIMZQ.mjs} +6 -4
- package/dist/index.mjs +6 -6
- package/dist/{mcp-4XCDZ5PU.mjs → mcp-7Q3FEI3Z.mjs} +5 -6
- package/dist/mcp-7Q3FEI3Z.mjs.map +1 -0
- package/dist/{src-ZELOFE7P.mjs → src-QBT5SYW5.mjs} +14 -2
- package/dist/{stream-import-RP3XB6NM.mjs → stream-import-VG7RP6KJ.mjs} +5 -3
- package/dist-web/assets/{DebugToolsPanel-dUV6pVqD.css → DebugToolsPanel-CoBtqb--.css} +1 -1
- package/dist-web/assets/DebugToolsPanel.vue_vue_type_script_setup_true_lang-CpFGhsl5.js +147 -0
- package/dist-web/assets/{EChartWordcloud-DcC12j9S.js → EChartWordcloud-BYLFxPAX.js} +1 -1
- package/dist-web/assets/{MessageExportModal-CUS9nnwi.js → MessageExportModal-DTgxz1eQ.js} +1 -1
- package/dist-web/assets/electron-2m4Vdk2z.js +1 -0
- package/dist-web/assets/electron-H65bUnhW.js +1 -0
- package/dist-web/assets/{fetch-DvfIZxuz.js → fetch-C2vUkdnA.js} +1 -1
- package/dist-web/assets/fetch-CTZXLLcn.js +1 -0
- package/dist-web/assets/fetch-o5NJ2uWE.js +3 -0
- package/dist-web/assets/index-B8867dX7.css +1 -0
- package/dist-web/assets/{index-CmU6x-uJ.css → index-BE64tUWq.css} +1 -1
- package/dist-web/assets/{index-D3siJhmm.js → index-C4B0hv6C.js} +4 -4
- package/dist-web/assets/index-CbQMfheg.css +1 -0
- package/dist-web/assets/index-DUXf21MT.js +173 -0
- package/dist-web/assets/index-DZiH2cC1.js +7 -0
- package/dist-web/assets/index-DeG0EHOT.js +24 -0
- package/dist-web/assets/index-DuhN8jAf.js +2 -0
- package/dist-web/assets/{vendor-echarts-BXkS9r5W.js → vendor-echarts-C13vDgZW.js} +5 -5
- package/dist-web/assets/{vendor-echarts-wordcloud-CmDVLUv0.js → vendor-echarts-wordcloud-uVdwYsYa.js} +1 -1
- package/dist-web/assets/{vendor-nuxt-ui-D1aReMtR.js → vendor-nuxt-ui-CYMNNdeD.js} +2 -2
- package/dist-web/assets/{vendor-zrender-B6SpLIkk.js → vendor-zrender-5DcYjSZk.js} +2 -2
- package/dist-web/assets/{web-DOZKbBMQ.js → web-DImo6Kjz.js} +1 -1
- package/dist-web/index.html +3 -3
- package/package.json +1 -8
- package/CHANGELOG.md +0 -51
- package/LICENSE +0 -661
- package/dist/chunk-DL3BFEAH.mjs.map +0 -1
- package/dist/chunk-JMRDNHIR.mjs.map +0 -1
- package/dist/chunk-PF4QFHC6.mjs.map +0 -1
- package/dist/chunk-YAOFJ73U.mjs.map +0 -1
- package/dist/mcp-4XCDZ5PU.mjs.map +0 -1
- package/dist-web/assets/DebugToolsPanel.vue_vue_type_script_setup_true_lang-BuOhHrGp.js +0 -155
- package/dist-web/assets/electron-BHjOx6B6.js +0 -1
- package/dist-web/assets/electron-DvcSSc-D.js +0 -1
- package/dist-web/assets/fetch-CJ8Q4qM2.js +0 -1
- package/dist-web/assets/fetch-DgQkWW73.js +0 -3
- package/dist-web/assets/index-68UnLyFT.js +0 -173
- package/dist-web/assets/index-CCE1zef2.js +0 -2
- package/dist-web/assets/index-CLGv8Den.css +0 -1
- package/dist-web/assets/index-CUTe0Sc3.js +0 -16
- package/dist-web/assets/index-CzR1sElx.css +0 -1
- package/dist-web/assets/index-n2XUUgti.js +0 -7
- /package/dist/{chunk-OC73KSMV.mjs.map → chunk-OQVKZTVD.mjs.map} +0 -0
- /package/dist/{chunk-P4N5GEFF.mjs.map → chunk-OZWXLRJL.mjs.map} +0 -0
- /package/dist/{http-PQEOFGJK.mjs.map → http-DHQI4G5J.mjs.map} +0 -0
- /package/dist/{import-UR2IE2O7.mjs.map → import-CQWGIMZQ.mjs.map} +0 -0
- /package/dist/{src-ZELOFE7P.mjs.map → src-QBT5SYW5.mjs.map} +0 -0
- /package/dist/{stream-import-RP3XB6NM.mjs.map → stream-import-VG7RP6KJ.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-OQVKZTVD.mjs";
|
|
11
11
|
import {
|
|
12
12
|
analyzeIncrementalImport,
|
|
13
13
|
analyzeNewImport,
|
|
14
14
|
incrementalImport,
|
|
15
15
|
streamImport
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-AIR7EAF4.mjs";
|
|
17
17
|
import {
|
|
18
18
|
AGENT_TOOL_REGISTRY,
|
|
19
19
|
CoreDataProvider
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-4GI2UPCS.mjs";
|
|
21
21
|
import {
|
|
22
22
|
getVersion
|
|
23
23
|
} from "./chunk-2AIOU3PW.mjs";
|
|
@@ -46,12 +46,11 @@ import {
|
|
|
46
46
|
checkAndCompress,
|
|
47
47
|
checkConflictsFromSources,
|
|
48
48
|
cleanupTempDatabases,
|
|
49
|
-
clearSessionIndex,
|
|
50
|
-
completeSimple,
|
|
51
49
|
computeWordFrequency,
|
|
52
50
|
createActivateSkillTool,
|
|
53
51
|
createAiTranslate,
|
|
54
52
|
createCompressionLlmAdapter,
|
|
53
|
+
createDatabaseManagerAdapter,
|
|
55
54
|
createJiebaNlpProvider,
|
|
56
55
|
deleteDict,
|
|
57
56
|
deleteTempDatabase,
|
|
@@ -61,18 +60,15 @@ import {
|
|
|
61
60
|
ensureDefaultDict,
|
|
62
61
|
executeReadonlySql,
|
|
63
62
|
executeSql,
|
|
64
|
-
exportFilterResultToMarkdown,
|
|
65
63
|
exportSessionToJson,
|
|
64
|
+
export_service_exports,
|
|
66
65
|
fetchRemoteModels,
|
|
66
|
+
findEntryFileInDirectory,
|
|
67
67
|
formatAIError,
|
|
68
|
-
generateSessionIndex,
|
|
69
|
-
generateSessionSummary,
|
|
70
68
|
getAvailableYears,
|
|
71
69
|
getBuiltinModelsByProvider,
|
|
72
70
|
getCatchphraseAnalysis,
|
|
73
71
|
getChatOverview,
|
|
74
|
-
getChatSessionList,
|
|
75
|
-
getChatSessionSummary,
|
|
76
72
|
getClusterGraph,
|
|
77
73
|
getDailyActivity,
|
|
78
74
|
getDictList,
|
|
@@ -82,8 +78,6 @@ import {
|
|
|
82
78
|
getMemberActivity,
|
|
83
79
|
getMembers,
|
|
84
80
|
getMembersDetailed,
|
|
85
|
-
getMembersPaginated,
|
|
86
|
-
getMembersWithAliases,
|
|
87
81
|
getMentionAnalysis,
|
|
88
82
|
getMentionGraph,
|
|
89
83
|
getMessageLengthDistribution,
|
|
@@ -92,40 +86,39 @@ import {
|
|
|
92
86
|
getRelationshipStats,
|
|
93
87
|
getSchemaDetailed,
|
|
94
88
|
getSessionInfo,
|
|
95
|
-
getSessionMessages,
|
|
96
89
|
getSessionMeta,
|
|
97
90
|
getSessionOverview,
|
|
98
91
|
getSupportedFormats,
|
|
99
92
|
getTimeRange,
|
|
100
93
|
getWeekdayActivity,
|
|
101
94
|
getYearlyActivity,
|
|
102
|
-
hasFtsTable,
|
|
103
95
|
hasPendingElectronDataWarning,
|
|
104
96
|
initNlpDir,
|
|
105
97
|
isDictDownloaded,
|
|
106
98
|
loadConfig,
|
|
99
|
+
member_service_exports,
|
|
107
100
|
openBetterSqliteDatabase,
|
|
108
101
|
parseAssistantFile,
|
|
109
102
|
queryMessages,
|
|
110
|
-
rebuildFtsIndex,
|
|
111
103
|
resolveApiKey,
|
|
112
104
|
runAgentCore,
|
|
113
105
|
runSimpleLlmStream,
|
|
114
|
-
saveChatSessionSummary,
|
|
115
106
|
scanMultiChatFile,
|
|
116
|
-
searchByFts,
|
|
117
107
|
segmentText,
|
|
118
108
|
serializeChatLabToJsonl,
|
|
109
|
+
session_index_service_exports,
|
|
110
|
+
session_service_exports,
|
|
119
111
|
streamParseFileInfo,
|
|
112
|
+
summary_service_exports,
|
|
120
113
|
validateApiKey,
|
|
121
114
|
verifyCliDataPath,
|
|
122
115
|
writeAuthProfile,
|
|
123
116
|
writeConfigField
|
|
124
|
-
} from "./chunk-
|
|
117
|
+
} from "./chunk-7HQWGLX6.mjs";
|
|
125
118
|
|
|
126
119
|
// src/http/index.ts
|
|
127
|
-
import * as
|
|
128
|
-
import * as
|
|
120
|
+
import * as fs14 from "fs";
|
|
121
|
+
import * as path15 from "path";
|
|
129
122
|
import * as crypto5 from "crypto";
|
|
130
123
|
|
|
131
124
|
// src/http/server.ts
|
|
@@ -497,10 +490,9 @@ function registerSessionRoutes(server2, dbManager2) {
|
|
|
497
490
|
});
|
|
498
491
|
}
|
|
499
492
|
|
|
500
|
-
// src/http/routes/web.ts
|
|
501
|
-
import * as
|
|
502
|
-
import * as
|
|
503
|
-
import * as path3 from "path";
|
|
493
|
+
// src/http/routes/web/index.ts
|
|
494
|
+
import * as os3 from "os";
|
|
495
|
+
import * as path7 from "path";
|
|
504
496
|
|
|
505
497
|
// src/merger/merge-cache.ts
|
|
506
498
|
import * as fs from "fs";
|
|
@@ -576,17 +568,326 @@ var MergeSessionCache = class {
|
|
|
576
568
|
}
|
|
577
569
|
};
|
|
578
570
|
|
|
579
|
-
// src/
|
|
571
|
+
// src/http/routes/web/sessions.ts
|
|
572
|
+
function registerSessionRoutes2(server2, adapter) {
|
|
573
|
+
server2.get("/_web/sessions", async () => {
|
|
574
|
+
return session_service_exports.listAnalysisSessions(adapter);
|
|
575
|
+
});
|
|
576
|
+
server2.get("/_web/sessions/:id", async (request) => {
|
|
577
|
+
const session = session_service_exports.getAnalysisSession(adapter, request.params.id);
|
|
578
|
+
if (!session) {
|
|
579
|
+
throw Object.assign(new Error(`Session not found: ${request.params.id}`), { statusCode: 404 });
|
|
580
|
+
}
|
|
581
|
+
return session;
|
|
582
|
+
});
|
|
583
|
+
server2.delete("/_web/sessions/:id", async (request, reply) => {
|
|
584
|
+
const { id } = request.params;
|
|
585
|
+
try {
|
|
586
|
+
const deleted = session_service_exports.deleteSession(adapter, id);
|
|
587
|
+
if (!deleted) {
|
|
588
|
+
return reply.code(404).send({ success: false, error: "File not found" });
|
|
589
|
+
}
|
|
590
|
+
return { success: true };
|
|
591
|
+
} catch (err) {
|
|
592
|
+
return reply.code(500).send({ success: false, error: String(err) });
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
server2.patch("/_web/sessions/:id/name", async (request) => {
|
|
596
|
+
session_service_exports.renameSession(adapter, request.params.id, request.body.name);
|
|
597
|
+
return { success: true };
|
|
598
|
+
});
|
|
599
|
+
server2.patch(
|
|
600
|
+
"/_web/sessions/:id/owner",
|
|
601
|
+
async (request) => {
|
|
602
|
+
session_service_exports.updateSessionOwnerId(adapter, request.params.id, request.body.ownerId ?? null);
|
|
603
|
+
return { success: true };
|
|
604
|
+
}
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// src/http/routes/web/members.ts
|
|
609
|
+
function registerMemberRoutes(server2, adapter) {
|
|
610
|
+
server2.get("/_web/sessions/:id/members", async (request) => {
|
|
611
|
+
return member_service_exports.getMembers(adapter, request.params.id);
|
|
612
|
+
});
|
|
613
|
+
server2.get("/_web/sessions/:id/members/paginated", async (request) => {
|
|
614
|
+
return member_service_exports.getMembersPaginated(adapter, request.params.id, {
|
|
615
|
+
page: parseInt(request.query.page || "1", 10),
|
|
616
|
+
pageSize: parseInt(request.query.pageSize || "20", 10),
|
|
617
|
+
search: request.query.search,
|
|
618
|
+
sortOrder: request.query.sortOrder === "asc" ? "asc" : "desc"
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
server2.patch(
|
|
622
|
+
"/_web/sessions/:id/members/:memberId/aliases",
|
|
623
|
+
async (request) => {
|
|
624
|
+
const memberId = parseInt(request.params.memberId, 10);
|
|
625
|
+
member_service_exports.updateMemberAliases(adapter, request.params.id, memberId, request.body.aliases);
|
|
626
|
+
return { success: true };
|
|
627
|
+
}
|
|
628
|
+
);
|
|
629
|
+
server2.delete(
|
|
630
|
+
"/_web/sessions/:id/members/:memberId",
|
|
631
|
+
async (request) => {
|
|
632
|
+
const memberId = parseInt(request.params.memberId, 10);
|
|
633
|
+
member_service_exports.deleteMember(adapter, request.params.id, memberId);
|
|
634
|
+
return { success: true };
|
|
635
|
+
}
|
|
636
|
+
);
|
|
637
|
+
server2.post(
|
|
638
|
+
"/_web/sessions/:id/members/merge",
|
|
639
|
+
async (request) => {
|
|
640
|
+
const { memberId1, memberId2 } = request.body;
|
|
641
|
+
member_service_exports.mergeMembers(adapter, request.params.id, memberId1, memberId2);
|
|
642
|
+
return { success: true };
|
|
643
|
+
}
|
|
644
|
+
);
|
|
645
|
+
server2.get(
|
|
646
|
+
"/_web/sessions/:id/members/:memberId/history",
|
|
647
|
+
async (request) => {
|
|
648
|
+
const memberId = parseInt(request.params.memberId, 10);
|
|
649
|
+
return member_service_exports.getMemberNameHistory(adapter, request.params.id, memberId);
|
|
650
|
+
}
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/http/routes/web/helpers.ts
|
|
580
655
|
import * as fs2 from "fs";
|
|
581
656
|
import * as path2 from "path";
|
|
657
|
+
function parseTimeFilter(query) {
|
|
658
|
+
const { startTs, endTs, memberId } = query;
|
|
659
|
+
if (!startTs && !endTs && !memberId) return void 0;
|
|
660
|
+
const filter = {};
|
|
661
|
+
if (startTs) filter.startTs = parseInt(startTs, 10);
|
|
662
|
+
if (endTs) filter.endTs = parseInt(endTs, 10);
|
|
663
|
+
if (memberId) filter.memberId = parseInt(memberId, 10);
|
|
664
|
+
return filter;
|
|
665
|
+
}
|
|
666
|
+
function resolveNativeBinding() {
|
|
667
|
+
if (process.versions.electron) return void 0;
|
|
668
|
+
const nativePath = path2.resolve(__dirname, "../../../../native/better_sqlite3.node");
|
|
669
|
+
if (fs2.existsSync(nativePath)) return nativePath;
|
|
670
|
+
return void 0;
|
|
671
|
+
}
|
|
672
|
+
function getAiDataDir(dbManager2) {
|
|
673
|
+
const pathProvider = dbManager2["pathProvider"];
|
|
674
|
+
if (!pathProvider) {
|
|
675
|
+
throw Object.assign(new Error("PathProvider not available"), { statusCode: 500 });
|
|
676
|
+
}
|
|
677
|
+
return pathProvider.getAiDataDir();
|
|
678
|
+
}
|
|
679
|
+
function ensureDb2(dbManager2, sessionId) {
|
|
680
|
+
const db = dbManager2.open(sessionId);
|
|
681
|
+
if (!db) {
|
|
682
|
+
throw Object.assign(new Error(`Session not found: ${sessionId}`), { statusCode: 404 });
|
|
683
|
+
}
|
|
684
|
+
return db;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// src/http/routes/web/analytics.ts
|
|
688
|
+
function registerAnalyticsRoutes(server2, _dbManager, adapter) {
|
|
689
|
+
server2.get("/_web/sessions/:id/years", async (request) => {
|
|
690
|
+
const db = adapter.ensureReadonly(request.params.id);
|
|
691
|
+
return getAvailableYears(db);
|
|
692
|
+
});
|
|
693
|
+
server2.get("/_web/sessions/:id/time-range", async (request) => {
|
|
694
|
+
const db = adapter.ensureReadonly(request.params.id);
|
|
695
|
+
return getTimeRange(db);
|
|
696
|
+
});
|
|
697
|
+
server2.get(
|
|
698
|
+
"/_web/sessions/:id/stats/member-activity",
|
|
699
|
+
async (request) => {
|
|
700
|
+
const db = adapter.ensureReadonly(request.params.id);
|
|
701
|
+
return getMemberActivity(db, parseTimeFilter(request.query));
|
|
702
|
+
}
|
|
703
|
+
);
|
|
704
|
+
server2.get(
|
|
705
|
+
"/_web/sessions/:id/stats/hourly",
|
|
706
|
+
async (request) => {
|
|
707
|
+
const db = adapter.ensureReadonly(request.params.id);
|
|
708
|
+
return getHourlyActivity(db, parseTimeFilter(request.query));
|
|
709
|
+
}
|
|
710
|
+
);
|
|
711
|
+
server2.get(
|
|
712
|
+
"/_web/sessions/:id/stats/daily",
|
|
713
|
+
async (request) => {
|
|
714
|
+
const db = adapter.ensureReadonly(request.params.id);
|
|
715
|
+
return getDailyActivity(db, parseTimeFilter(request.query));
|
|
716
|
+
}
|
|
717
|
+
);
|
|
718
|
+
server2.get(
|
|
719
|
+
"/_web/sessions/:id/stats/weekday",
|
|
720
|
+
async (request) => {
|
|
721
|
+
const db = adapter.ensureReadonly(request.params.id);
|
|
722
|
+
return getWeekdayActivity(db, parseTimeFilter(request.query));
|
|
723
|
+
}
|
|
724
|
+
);
|
|
725
|
+
server2.get(
|
|
726
|
+
"/_web/sessions/:id/stats/message-types",
|
|
727
|
+
async (request) => {
|
|
728
|
+
const db = adapter.ensureReadonly(request.params.id);
|
|
729
|
+
return getMessageTypeStats(db, parseTimeFilter(request.query));
|
|
730
|
+
}
|
|
731
|
+
);
|
|
732
|
+
server2.get(
|
|
733
|
+
"/_web/sessions/:id/analytics/relationship",
|
|
734
|
+
async (request) => {
|
|
735
|
+
const db = adapter.ensureReadonly(request.params.id);
|
|
736
|
+
return getRelationshipStats(db, parseTimeFilter(request.query));
|
|
737
|
+
}
|
|
738
|
+
);
|
|
739
|
+
server2.get(
|
|
740
|
+
"/_web/sessions/:id/analytics/catchphrase",
|
|
741
|
+
async (request) => {
|
|
742
|
+
const db = adapter.ensureReadonly(request.params.id);
|
|
743
|
+
return getCatchphraseAnalysis(db, parseTimeFilter(request.query));
|
|
744
|
+
}
|
|
745
|
+
);
|
|
746
|
+
server2.get(
|
|
747
|
+
"/_web/sessions/:id/analytics/mention",
|
|
748
|
+
async (request) => {
|
|
749
|
+
const db = adapter.ensureReadonly(request.params.id);
|
|
750
|
+
return getMentionAnalysis(db, parseTimeFilter(request.query));
|
|
751
|
+
}
|
|
752
|
+
);
|
|
753
|
+
server2.get(
|
|
754
|
+
"/_web/sessions/:id/analytics/mention-graph",
|
|
755
|
+
async (request) => {
|
|
756
|
+
const db = adapter.ensureReadonly(request.params.id);
|
|
757
|
+
return getMentionGraph(db, parseTimeFilter(request.query));
|
|
758
|
+
}
|
|
759
|
+
);
|
|
760
|
+
server2.get(
|
|
761
|
+
"/_web/sessions/:id/analytics/laugh",
|
|
762
|
+
async (request) => {
|
|
763
|
+
const db = adapter.ensureReadonly(request.params.id);
|
|
764
|
+
return getLaughAnalysis(db, parseTimeFilter(request.query));
|
|
765
|
+
}
|
|
766
|
+
);
|
|
767
|
+
server2.get(
|
|
768
|
+
"/_web/sessions/:id/analytics/cluster",
|
|
769
|
+
async (request) => {
|
|
770
|
+
const db = adapter.ensureReadonly(request.params.id);
|
|
771
|
+
const filter = parseTimeFilter(request.query);
|
|
772
|
+
const topEdges = request.query.topEdges ? parseInt(request.query.topEdges, 10) : void 0;
|
|
773
|
+
return getClusterGraph(db, filter, topEdges ? { topEdges } : void 0);
|
|
774
|
+
}
|
|
775
|
+
);
|
|
776
|
+
server2.get(
|
|
777
|
+
"/_web/sessions/:id/analytics/language-preference",
|
|
778
|
+
async (request) => {
|
|
779
|
+
const db = adapter.ensureReadonly(request.params.id);
|
|
780
|
+
const filter = parseTimeFilter(request.query);
|
|
781
|
+
const locale = request.query.locale || "zh-CN";
|
|
782
|
+
const nlpProvider = createJiebaNlpProvider();
|
|
783
|
+
return getLanguagePreferenceAnalysis(db, { locale, timeFilter: filter, nlpProvider });
|
|
784
|
+
}
|
|
785
|
+
);
|
|
786
|
+
server2.get(
|
|
787
|
+
"/_web/sessions/:id/analytics/monthly-activity",
|
|
788
|
+
async (request) => {
|
|
789
|
+
const db = adapter.ensureReadonly(request.params.id);
|
|
790
|
+
return getMonthlyActivity(db, parseTimeFilter(request.query));
|
|
791
|
+
}
|
|
792
|
+
);
|
|
793
|
+
server2.get(
|
|
794
|
+
"/_web/sessions/:id/analytics/yearly-activity",
|
|
795
|
+
async (request) => {
|
|
796
|
+
const db = adapter.ensureReadonly(request.params.id);
|
|
797
|
+
return getYearlyActivity(db, parseTimeFilter(request.query));
|
|
798
|
+
}
|
|
799
|
+
);
|
|
800
|
+
server2.get(
|
|
801
|
+
"/_web/sessions/:id/analytics/message-length-distribution",
|
|
802
|
+
async (request) => {
|
|
803
|
+
const db = adapter.ensureReadonly(request.params.id);
|
|
804
|
+
return getMessageLengthDistribution(db, parseTimeFilter(request.query));
|
|
805
|
+
}
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// src/http/routes/web/sql.ts
|
|
810
|
+
function registerSqlRoutes(server2, adapter) {
|
|
811
|
+
server2.post("/_web/sessions/:id/sql", async (request, reply) => {
|
|
812
|
+
const db = adapter.ensureReadonly(request.params.id);
|
|
813
|
+
const { sql } = request.body || {};
|
|
814
|
+
if (!sql || typeof sql !== "string") {
|
|
815
|
+
return reply.code(400).send({ error: "Missing sql parameter" });
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
return executeSql(db, sql, { timing: true });
|
|
819
|
+
} catch (err) {
|
|
820
|
+
const message = err instanceof Error ? err.message : "SQL execution error";
|
|
821
|
+
return reply.code(400).send({ error: message });
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
server2.get("/_web/sessions/:id/schema", async (request) => {
|
|
825
|
+
const db = adapter.ensureReadonly(request.params.id);
|
|
826
|
+
return getSchemaDetailed(db);
|
|
827
|
+
});
|
|
828
|
+
server2.post("/_web/sessions/:id/query", async (request) => {
|
|
829
|
+
const db = adapter.ensureReadonly(request.params.id);
|
|
830
|
+
const { sql, params = [] } = request.body;
|
|
831
|
+
if (!sql || typeof sql !== "string") {
|
|
832
|
+
throw Object.assign(new Error('Missing or invalid "sql" field'), { statusCode: 400 });
|
|
833
|
+
}
|
|
834
|
+
const stmt = db.prepare(sql.trim());
|
|
835
|
+
if (!stmt.readonly) {
|
|
836
|
+
throw Object.assign(new Error("Only READ-ONLY statements are allowed"), { statusCode: 403 });
|
|
837
|
+
}
|
|
838
|
+
if (Array.isArray(params)) {
|
|
839
|
+
return stmt.all(...params);
|
|
840
|
+
}
|
|
841
|
+
return stmt.all(params);
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// src/http/routes/web/session-index.ts
|
|
846
|
+
function registerSessionIndexRoutes(server2, adapter) {
|
|
847
|
+
server2.post("/_web/sessions/:id/generate-index", async (request) => {
|
|
848
|
+
const gapThreshold = request.body?.gapThreshold ?? 1800;
|
|
849
|
+
const sessionCount = session_index_service_exports.generateIndex(adapter, request.params.id, gapThreshold);
|
|
850
|
+
return { sessionCount };
|
|
851
|
+
});
|
|
852
|
+
server2.post("/_web/sessions/:id/generate-incremental-index", async (request) => {
|
|
853
|
+
const gapThreshold = request.body?.gapThreshold ?? 1800;
|
|
854
|
+
const sessionCount = session_index_service_exports.generateIncrementalIndex(adapter, request.params.id, gapThreshold);
|
|
855
|
+
return { sessionCount };
|
|
856
|
+
});
|
|
857
|
+
server2.post("/_web/sessions/:id/clear-index", async (request) => {
|
|
858
|
+
session_index_service_exports.clearIndex(adapter, request.params.id);
|
|
859
|
+
return { success: true };
|
|
860
|
+
});
|
|
861
|
+
server2.get("/_web/sessions/:id/search/fts", async (request, reply) => {
|
|
862
|
+
if (!session_index_service_exports.getFtsStatus(adapter, request.params.id)) {
|
|
863
|
+
return reply.code(400).send({ error: "FTS index not built for this session" });
|
|
864
|
+
}
|
|
865
|
+
const keywords = request.query.keywords.split(/\s+/).filter(Boolean);
|
|
866
|
+
if (keywords.length === 0) return { rowids: [], total: 0 };
|
|
867
|
+
const limit = parseInt(request.query.limit || "100", 10);
|
|
868
|
+
const offset = parseInt(request.query.offset || "0", 10);
|
|
869
|
+
return session_index_service_exports.searchFts(adapter, request.params.id, keywords, limit, offset);
|
|
870
|
+
});
|
|
871
|
+
server2.get("/_web/sessions/:id/fts/status", async (request) => {
|
|
872
|
+
return { hasFtsIndex: session_index_service_exports.getFtsStatus(adapter, request.params.id) };
|
|
873
|
+
});
|
|
874
|
+
server2.post("/_web/sessions/:id/fts/rebuild", async (request) => {
|
|
875
|
+
const result = session_index_service_exports.rebuildFts(adapter, request.params.id);
|
|
876
|
+
return { success: true, indexed: result.indexed };
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// src/ai/llm-config.ts
|
|
881
|
+
import * as fs3 from "fs";
|
|
882
|
+
import * as path3 from "path";
|
|
582
883
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
583
884
|
function loadLlmConfig(aiDataDir) {
|
|
584
|
-
const configPath =
|
|
585
|
-
if (!
|
|
885
|
+
const configPath = path3.join(aiDataDir, "llm-config.json");
|
|
886
|
+
if (!fs3.existsSync(configPath)) {
|
|
586
887
|
return { configs: [], defaultAssistant: null, fastModel: null };
|
|
587
888
|
}
|
|
588
889
|
try {
|
|
589
|
-
const data = JSON.parse(
|
|
890
|
+
const data = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
|
|
590
891
|
const configs = (data.configs || []).map((c) => {
|
|
591
892
|
const provider = c.provider || "";
|
|
592
893
|
const authProfile = c.authProfile;
|
|
@@ -623,7 +924,7 @@ function getDefaultAssistantConfig(aiDataDir) {
|
|
|
623
924
|
}
|
|
624
925
|
var MAX_CONFIG_COUNT = 99;
|
|
625
926
|
function saveLlmConfig(aiDataDir, store) {
|
|
626
|
-
const configPath =
|
|
927
|
+
const configPath = path3.join(aiDataDir, "llm-config.json");
|
|
627
928
|
const toSave = {
|
|
628
929
|
...store,
|
|
629
930
|
configs: store.configs.map((c) => {
|
|
@@ -632,7 +933,7 @@ function saveLlmConfig(aiDataDir, store) {
|
|
|
632
933
|
}),
|
|
633
934
|
schemaVersion: 3
|
|
634
935
|
};
|
|
635
|
-
|
|
936
|
+
fs3.writeFileSync(configPath, JSON.stringify(toSave, null, 2), "utf-8");
|
|
636
937
|
}
|
|
637
938
|
function addLlmConfig(aiDataDir, config) {
|
|
638
939
|
const store = loadLlmConfig(aiDataDir);
|
|
@@ -727,12 +1028,12 @@ function setFastModelSlot(aiDataDir, slot) {
|
|
|
727
1028
|
return { success: true };
|
|
728
1029
|
}
|
|
729
1030
|
function loadRawConfigStore(aiDataDir) {
|
|
730
|
-
const configPath =
|
|
731
|
-
if (!
|
|
1031
|
+
const configPath = path3.join(aiDataDir, "llm-config.json");
|
|
1032
|
+
if (!fs3.existsSync(configPath)) {
|
|
732
1033
|
return { configs: [], defaultAssistant: null, fastModel: null };
|
|
733
1034
|
}
|
|
734
1035
|
try {
|
|
735
|
-
const data = JSON.parse(
|
|
1036
|
+
const data = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
|
|
736
1037
|
return {
|
|
737
1038
|
configs: data.configs || [],
|
|
738
1039
|
defaultAssistant: data.defaultAssistant ?? null,
|
|
@@ -746,554 +1047,111 @@ function buildPiModel2(config) {
|
|
|
746
1047
|
return buildPiModel(config);
|
|
747
1048
|
}
|
|
748
1049
|
|
|
749
|
-
// src/http/routes/web.ts
|
|
750
|
-
function
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1050
|
+
// src/http/routes/web/summaries.ts
|
|
1051
|
+
function createSummaryDeps(dbManager2) {
|
|
1052
|
+
const aiDataDir = getAiDataDir(dbManager2);
|
|
1053
|
+
return {
|
|
1054
|
+
getLlmConfig() {
|
|
1055
|
+
return getDefaultAssistantConfig(aiDataDir);
|
|
1056
|
+
},
|
|
1057
|
+
buildPiModel(config) {
|
|
1058
|
+
return buildPiModel2(config);
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
755
1061
|
}
|
|
756
|
-
function
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1062
|
+
function registerSummaryRoutes(server2, dbManager2, adapter) {
|
|
1063
|
+
const deps = createSummaryDeps(dbManager2);
|
|
1064
|
+
server2.post("/_web/sessions/:id/summaries/generate", async (request, reply) => {
|
|
1065
|
+
const { chatSessionId, locale, forceRegenerate } = request.body;
|
|
1066
|
+
const result = await summary_service_exports.generateSummary(adapter, request.params.id, chatSessionId, deps, {
|
|
1067
|
+
locale,
|
|
1068
|
+
forceRegenerate
|
|
1069
|
+
});
|
|
1070
|
+
if ("error" in result && !result.success) {
|
|
1071
|
+
return reply.code(400).send({ error: result.error });
|
|
1072
|
+
}
|
|
1073
|
+
return result;
|
|
1074
|
+
});
|
|
1075
|
+
server2.post("/_web/sessions/:id/summaries/generate-all", async (request, reply) => {
|
|
1076
|
+
const { locale, forceRegenerate } = request.body;
|
|
1077
|
+
const result = await summary_service_exports.generateAllSummaries(adapter, request.params.id, deps, {
|
|
1078
|
+
locale,
|
|
1079
|
+
forceRegenerate
|
|
1080
|
+
});
|
|
1081
|
+
if (result.error) {
|
|
1082
|
+
return reply.code(400).send({ error: result.error });
|
|
1083
|
+
}
|
|
1084
|
+
return result;
|
|
1085
|
+
});
|
|
762
1086
|
}
|
|
763
|
-
function ensureWritableDb(dbManager2, sessionId) {
|
|
764
|
-
dbManager2.close(sessionId);
|
|
765
|
-
const db = dbManager2.open(sessionId, { readonly: false });
|
|
766
|
-
if (!db) {
|
|
767
|
-
throw Object.assign(new Error(`Session not found: ${sessionId}`), { statusCode: 404 });
|
|
768
|
-
}
|
|
769
|
-
return db;
|
|
770
|
-
}
|
|
771
|
-
function parseTimeFilter(query) {
|
|
772
|
-
const { startTs, endTs, memberId } = query;
|
|
773
|
-
if (!startTs && !endTs && !memberId) return void 0;
|
|
774
|
-
const filter = {};
|
|
775
|
-
if (startTs) filter.startTs = parseInt(startTs, 10);
|
|
776
|
-
if (endTs) filter.endTs = parseInt(endTs, 10);
|
|
777
|
-
if (memberId) filter.memberId = parseInt(memberId, 10);
|
|
778
|
-
return filter;
|
|
779
|
-
}
|
|
780
|
-
function getAiDataDir(dbManager2) {
|
|
781
|
-
const pathProvider = dbManager2["pathProvider"];
|
|
782
|
-
if (!pathProvider) {
|
|
783
|
-
throw Object.assign(new Error("PathProvider not available"), { statusCode: 500 });
|
|
784
|
-
}
|
|
785
|
-
return pathProvider.getAiDataDir();
|
|
786
|
-
}
|
|
787
|
-
function buildSummaryDeps(db, llmConfig, _aiDataDir) {
|
|
788
|
-
const piModel = buildPiModel2(llmConfig);
|
|
789
|
-
return {
|
|
790
|
-
loadMessages(chatSessionId, limit = 500) {
|
|
791
|
-
const data = getSessionMessages(db, chatSessionId, limit);
|
|
792
|
-
if (!data) return null;
|
|
793
|
-
return data.messages.map((m) => ({ senderName: m.senderName, content: m.content }));
|
|
794
|
-
},
|
|
795
|
-
saveSummary(chatSessionId, summary) {
|
|
796
|
-
saveChatSessionSummary(db, chatSessionId, summary);
|
|
797
|
-
},
|
|
798
|
-
getSummary(chatSessionId) {
|
|
799
|
-
return getChatSessionSummary(db, chatSessionId);
|
|
800
|
-
},
|
|
801
|
-
async llmComplete(systemPrompt, userPrompt, options) {
|
|
802
|
-
const result = await completeSimple(
|
|
803
|
-
piModel,
|
|
804
|
-
{
|
|
805
|
-
systemPrompt,
|
|
806
|
-
messages: [{ role: "user", content: [{ type: "text", text: userPrompt }], timestamp: Date.now() }]
|
|
807
|
-
},
|
|
808
|
-
{ apiKey: llmConfig.apiKey, maxTokens: options?.maxTokens, temperature: options?.temperature }
|
|
809
|
-
);
|
|
810
|
-
return result.content.filter((item) => item.type === "text").map((item) => item.text).join("");
|
|
811
|
-
},
|
|
812
|
-
t: (key) => key
|
|
813
|
-
};
|
|
814
|
-
}
|
|
815
|
-
function registerWebRoutes(server2, dbManager2, options) {
|
|
816
|
-
const mergeCache = options?.pathProvider ? new MergeSessionCache(options.pathProvider, { nativeBinding: options.nativeBinding }) : null;
|
|
817
|
-
mergeCache?.cleanupOrphans();
|
|
818
|
-
server2.get("/_web/sessions", async () => {
|
|
819
|
-
const sessionIds = dbManager2.listSessionIds();
|
|
820
|
-
return sessionIds.map((id) => {
|
|
821
|
-
const db = dbManager2.open(id);
|
|
822
|
-
if (!db) return null;
|
|
823
|
-
const info = getSessionInfo(db);
|
|
824
|
-
if (!info) return null;
|
|
825
|
-
return {
|
|
826
|
-
...info,
|
|
827
|
-
id,
|
|
828
|
-
dbPath: "",
|
|
829
|
-
memberAvatar: null,
|
|
830
|
-
aiConversationCount: 0
|
|
831
|
-
};
|
|
832
|
-
}).filter(Boolean);
|
|
833
|
-
});
|
|
834
|
-
server2.get("/_web/sessions/:id", async (request) => {
|
|
835
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
836
|
-
const info = getSessionInfo(db);
|
|
837
|
-
if (!info) return null;
|
|
838
|
-
return {
|
|
839
|
-
...info,
|
|
840
|
-
id: request.params.id,
|
|
841
|
-
dbPath: "",
|
|
842
|
-
memberAvatar: null,
|
|
843
|
-
aiConversationCount: 0
|
|
844
|
-
};
|
|
845
|
-
});
|
|
846
|
-
server2.delete("/_web/sessions/:id", async (request, reply) => {
|
|
847
|
-
const { id } = request.params;
|
|
848
|
-
try {
|
|
849
|
-
dbManager2.close(id);
|
|
850
|
-
const dbPath = dbManager2.getDbPath(id);
|
|
851
|
-
const fs12 = await import("fs");
|
|
852
|
-
if (fs12.existsSync(dbPath)) {
|
|
853
|
-
fs12.unlinkSync(dbPath);
|
|
854
|
-
return { success: true };
|
|
855
|
-
}
|
|
856
|
-
return reply.code(404).send({ success: false, error: "File not found" });
|
|
857
|
-
} catch (err) {
|
|
858
|
-
return reply.code(500).send({ success: false, error: String(err) });
|
|
859
|
-
}
|
|
860
|
-
});
|
|
861
|
-
server2.patch("/_web/sessions/:id/name", async (request) => {
|
|
862
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
863
|
-
const { name } = request.body;
|
|
864
|
-
db.prepare("UPDATE meta SET name = ?").run(name);
|
|
865
|
-
return { success: true };
|
|
866
|
-
});
|
|
867
|
-
server2.patch(
|
|
868
|
-
"/_web/sessions/:id/owner",
|
|
869
|
-
async (request) => {
|
|
870
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
871
|
-
const { ownerId } = request.body;
|
|
872
|
-
db.prepare("UPDATE meta SET owner_id = ?").run(ownerId ?? null);
|
|
873
|
-
return { success: true };
|
|
874
|
-
}
|
|
875
|
-
);
|
|
876
|
-
server2.get("/_web/sessions/:id/years", async (request) => {
|
|
877
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
878
|
-
return getAvailableYears(db);
|
|
879
|
-
});
|
|
880
|
-
server2.get("/_web/sessions/:id/time-range", async (request) => {
|
|
881
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
882
|
-
return getTimeRange(db);
|
|
883
|
-
});
|
|
884
|
-
server2.get("/_web/sessions/:id/stats/member-activity", async (request) => {
|
|
885
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
886
|
-
const filter = parseTimeFilter(request.query);
|
|
887
|
-
return getMemberActivity(db, filter);
|
|
888
|
-
});
|
|
889
|
-
server2.get("/_web/sessions/:id/stats/hourly", async (request) => {
|
|
890
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
891
|
-
const filter = parseTimeFilter(request.query);
|
|
892
|
-
return getHourlyActivity(db, filter);
|
|
893
|
-
});
|
|
894
|
-
server2.get("/_web/sessions/:id/stats/daily", async (request) => {
|
|
895
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
896
|
-
const filter = parseTimeFilter(request.query);
|
|
897
|
-
return getDailyActivity(db, filter);
|
|
898
|
-
});
|
|
899
|
-
server2.get("/_web/sessions/:id/stats/weekday", async (request) => {
|
|
900
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
901
|
-
const filter = parseTimeFilter(request.query);
|
|
902
|
-
return getWeekdayActivity(db, filter);
|
|
903
|
-
});
|
|
904
|
-
server2.get("/_web/sessions/:id/stats/message-types", async (request) => {
|
|
905
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
906
|
-
const filter = parseTimeFilter(request.query);
|
|
907
|
-
return getMessageTypeStats(db, filter);
|
|
908
|
-
});
|
|
909
|
-
server2.get("/_web/sessions/:id/members", async (request) => {
|
|
910
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
911
|
-
return getMembersWithAliases(db);
|
|
912
|
-
});
|
|
913
|
-
server2.get("/_web/sessions/:id/members/paginated", async (request) => {
|
|
914
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
915
|
-
const result = getMembersPaginated(db, {
|
|
916
|
-
page: parseInt(request.query.page || "1", 10),
|
|
917
|
-
pageSize: parseInt(request.query.pageSize || "20", 10),
|
|
918
|
-
search: request.query.search,
|
|
919
|
-
sortOrder: request.query.sortOrder === "asc" ? "asc" : "desc"
|
|
920
|
-
});
|
|
921
|
-
return {
|
|
922
|
-
items: result.members,
|
|
923
|
-
total: result.total,
|
|
924
|
-
page: result.page,
|
|
925
|
-
pageSize: result.pageSize,
|
|
926
|
-
totalPages: result.totalPages
|
|
927
|
-
};
|
|
928
|
-
});
|
|
929
|
-
server2.patch(
|
|
930
|
-
"/_web/sessions/:id/members/:memberId/aliases",
|
|
931
|
-
async (request) => {
|
|
932
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
933
|
-
const memberId = parseInt(request.params.memberId, 10);
|
|
934
|
-
const { aliases } = request.body;
|
|
935
|
-
db.prepare("UPDATE member SET aliases = ? WHERE id = ?").run(JSON.stringify(aliases), memberId);
|
|
936
|
-
return { success: true };
|
|
937
|
-
}
|
|
938
|
-
);
|
|
939
|
-
server2.delete(
|
|
940
|
-
"/_web/sessions/:id/members/:memberId",
|
|
941
|
-
async (request) => {
|
|
942
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
943
|
-
const memberId = parseInt(request.params.memberId, 10);
|
|
944
|
-
db.prepare("DELETE FROM message WHERE sender_id = ?").run(memberId);
|
|
945
|
-
db.prepare("DELETE FROM member WHERE id = ?").run(memberId);
|
|
946
|
-
return { success: true };
|
|
947
|
-
}
|
|
948
|
-
);
|
|
949
|
-
server2.post(
|
|
950
|
-
"/_web/sessions/:id/members/merge",
|
|
951
|
-
async (request) => {
|
|
952
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
953
|
-
const { memberId1, memberId2 } = request.body;
|
|
954
|
-
db.prepare("UPDATE message SET sender_id = ? WHERE sender_id = ?").run(memberId1, memberId2);
|
|
955
|
-
db.prepare("DELETE FROM member WHERE id = ?").run(memberId2);
|
|
956
|
-
return { success: true };
|
|
957
|
-
}
|
|
958
|
-
);
|
|
959
|
-
server2.get(
|
|
960
|
-
"/_web/sessions/:id/members/:memberId/history",
|
|
961
|
-
async (request) => {
|
|
962
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
963
|
-
const memberId = parseInt(request.params.memberId, 10);
|
|
964
|
-
const rows = db.prepare(
|
|
965
|
-
`SELECT
|
|
966
|
-
sender_account_name as accountName,
|
|
967
|
-
sender_group_nickname as groupNickname,
|
|
968
|
-
MIN(ts) as startTs,
|
|
969
|
-
MAX(ts) as endTs
|
|
970
|
-
FROM message
|
|
971
|
-
WHERE sender_id = ?
|
|
972
|
-
GROUP BY sender_account_name, sender_group_nickname
|
|
973
|
-
ORDER BY startTs`
|
|
974
|
-
).all(memberId);
|
|
975
|
-
const history = [];
|
|
976
|
-
for (const row of rows) {
|
|
977
|
-
if (row.accountName) {
|
|
978
|
-
history.push({ nameType: "account_name", name: row.accountName, startTs: row.startTs, endTs: row.endTs });
|
|
979
|
-
}
|
|
980
|
-
if (row.groupNickname) {
|
|
981
|
-
history.push({ nameType: "group_nickname", name: row.groupNickname, startTs: row.startTs, endTs: row.endTs });
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
return history;
|
|
985
|
-
}
|
|
986
|
-
);
|
|
987
|
-
server2.get("/_web/sessions/:id/analytics/relationship", async (request) => {
|
|
988
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
989
|
-
const filter = parseTimeFilter(request.query);
|
|
990
|
-
return getRelationshipStats(db, filter);
|
|
991
|
-
});
|
|
992
|
-
server2.get("/_web/sessions/:id/analytics/catchphrase", async (request) => {
|
|
993
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
994
|
-
const filter = parseTimeFilter(request.query);
|
|
995
|
-
return getCatchphraseAnalysis(db, filter);
|
|
996
|
-
});
|
|
997
|
-
server2.get("/_web/sessions/:id/analytics/mention", async (request) => {
|
|
998
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
999
|
-
const filter = parseTimeFilter(request.query);
|
|
1000
|
-
return getMentionAnalysis(db, filter);
|
|
1001
|
-
});
|
|
1002
|
-
server2.get("/_web/sessions/:id/analytics/mention-graph", async (request) => {
|
|
1003
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
1004
|
-
const filter = parseTimeFilter(request.query);
|
|
1005
|
-
return getMentionGraph(db, filter);
|
|
1006
|
-
});
|
|
1007
|
-
server2.get("/_web/sessions/:id/analytics/laugh", async (request) => {
|
|
1008
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
1009
|
-
const filter = parseTimeFilter(request.query);
|
|
1010
|
-
return getLaughAnalysis(db, filter);
|
|
1011
|
-
});
|
|
1012
|
-
server2.get("/_web/sessions/:id/analytics/cluster", async (request) => {
|
|
1013
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
1014
|
-
const filter = parseTimeFilter(request.query);
|
|
1015
|
-
const topEdges = request.query.topEdges ? parseInt(request.query.topEdges, 10) : void 0;
|
|
1016
|
-
return getClusterGraph(db, filter, topEdges ? { topEdges } : void 0);
|
|
1017
|
-
});
|
|
1018
|
-
server2.get("/_web/sessions/:id/analytics/language-preference", async (request) => {
|
|
1019
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
1020
|
-
const filter = parseTimeFilter(request.query);
|
|
1021
|
-
const locale = request.query.locale || "zh-CN";
|
|
1022
|
-
const nlpProvider = createJiebaNlpProvider();
|
|
1023
|
-
return getLanguagePreferenceAnalysis(db, { locale, timeFilter: filter, nlpProvider });
|
|
1024
|
-
});
|
|
1025
|
-
server2.get("/_web/sessions/:id/analytics/monthly-activity", async (request) => {
|
|
1026
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
1027
|
-
const filter = parseTimeFilter(request.query);
|
|
1028
|
-
return getMonthlyActivity(db, filter);
|
|
1029
|
-
});
|
|
1030
|
-
server2.get("/_web/sessions/:id/analytics/yearly-activity", async (request) => {
|
|
1031
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
1032
|
-
const filter = parseTimeFilter(request.query);
|
|
1033
|
-
return getYearlyActivity(db, filter);
|
|
1034
|
-
});
|
|
1035
|
-
server2.get("/_web/sessions/:id/analytics/message-length-distribution", async (request) => {
|
|
1036
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
1037
|
-
const filter = parseTimeFilter(request.query);
|
|
1038
|
-
return getMessageLengthDistribution(db, filter);
|
|
1039
|
-
});
|
|
1040
|
-
server2.post("/_web/sessions/:id/sql", async (request, reply) => {
|
|
1041
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
1042
|
-
const { sql } = request.body || {};
|
|
1043
|
-
if (!sql || typeof sql !== "string") {
|
|
1044
|
-
return reply.code(400).send({ error: "Missing sql parameter" });
|
|
1045
|
-
}
|
|
1046
|
-
try {
|
|
1047
|
-
return executeSql(db, sql, { timing: true });
|
|
1048
|
-
} catch (err) {
|
|
1049
|
-
const message = err instanceof Error ? err.message : "SQL execution error";
|
|
1050
|
-
return reply.code(400).send({ error: message });
|
|
1051
|
-
}
|
|
1052
|
-
});
|
|
1053
|
-
server2.get("/_web/sessions/:id/schema", async (request) => {
|
|
1054
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
1055
|
-
return getSchemaDetailed(db);
|
|
1056
|
-
});
|
|
1057
|
-
server2.post("/_web/sessions/:id/query", async (request) => {
|
|
1058
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
1059
|
-
const { sql, params = [] } = request.body;
|
|
1060
|
-
if (!sql || typeof sql !== "string") {
|
|
1061
|
-
throw Object.assign(new Error('Missing or invalid "sql" field'), { statusCode: 400 });
|
|
1062
|
-
}
|
|
1063
|
-
const stmt = db.prepare(sql.trim());
|
|
1064
|
-
if (!stmt.readonly) {
|
|
1065
|
-
throw Object.assign(new Error("Only READ-ONLY statements are allowed"), { statusCode: 403 });
|
|
1066
|
-
}
|
|
1067
|
-
if (Array.isArray(params)) {
|
|
1068
|
-
return stmt.all(...params);
|
|
1069
|
-
}
|
|
1070
|
-
return stmt.all(params);
|
|
1071
|
-
});
|
|
1072
|
-
server2.post("/_web/sessions/:id/export/markdown", async (request, reply) => {
|
|
1073
|
-
const { id } = request.params;
|
|
1074
|
-
const body = request.body;
|
|
1075
|
-
const sessionName = body?.sessionName || id;
|
|
1076
|
-
const chunks = [];
|
|
1077
|
-
const result = exportFilterResultToMarkdown(
|
|
1078
|
-
{
|
|
1079
|
-
sessionId: id,
|
|
1080
|
-
sessionName,
|
|
1081
|
-
filterMode: body.filterMode || "condition",
|
|
1082
|
-
keywords: body.keywords,
|
|
1083
|
-
timeFilter: body.timeFilter,
|
|
1084
|
-
senderIds: body.senderIds,
|
|
1085
|
-
contextSize: body.contextSize,
|
|
1086
|
-
chatSessionIds: body.chatSessionIds
|
|
1087
|
-
},
|
|
1088
|
-
{
|
|
1089
|
-
openDatabase(sessionId) {
|
|
1090
|
-
return dbManager2.open(sessionId) ?? null;
|
|
1091
|
-
}
|
|
1092
|
-
},
|
|
1093
|
-
{
|
|
1094
|
-
write(chunk) {
|
|
1095
|
-
chunks.push(chunk);
|
|
1096
|
-
},
|
|
1097
|
-
end() {
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
);
|
|
1101
|
-
if (!result.success) {
|
|
1102
|
-
return reply.code(500).send({ error: result.error });
|
|
1103
|
-
}
|
|
1104
|
-
reply.header("Content-Type", "text/markdown; charset=utf-8");
|
|
1105
|
-
reply.header("Content-Disposition", `attachment; filename="${encodeURIComponent(sessionName)}_export.md"`);
|
|
1106
|
-
return reply.send(chunks.join(""));
|
|
1107
|
-
});
|
|
1108
|
-
server2.post("/_web/sessions/:id/generate-index", async (request) => {
|
|
1109
|
-
const db = ensureWritableDb(dbManager2, request.params.id);
|
|
1110
|
-
const gapThreshold = request.body?.gapThreshold ?? 1800;
|
|
1111
|
-
const sessionCount = generateSessionIndex(db, gapThreshold);
|
|
1112
|
-
return { sessionCount };
|
|
1113
|
-
});
|
|
1114
|
-
server2.post("/_web/sessions/:id/clear-index", async (request) => {
|
|
1115
|
-
const db = ensureWritableDb(dbManager2, request.params.id);
|
|
1116
|
-
clearSessionIndex(db);
|
|
1117
|
-
return { success: true };
|
|
1118
|
-
});
|
|
1119
|
-
server2.get("/_web/sessions/:id/search/fts", async (request, reply) => {
|
|
1120
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
1121
|
-
if (!hasFtsTable(db)) {
|
|
1122
|
-
return reply.code(400).send({ error: "FTS index not built for this session" });
|
|
1123
|
-
}
|
|
1124
|
-
const keywords = request.query.keywords.split(/\s+/).filter(Boolean);
|
|
1125
|
-
if (keywords.length === 0) return { rowids: [], total: 0 };
|
|
1126
|
-
const limit = parseInt(request.query.limit || "100", 10);
|
|
1127
|
-
const offset = parseInt(request.query.offset || "0", 10);
|
|
1128
|
-
return searchByFts(db, keywords, limit, offset);
|
|
1129
|
-
});
|
|
1130
|
-
server2.get("/_web/sessions/:id/fts/status", async (request) => {
|
|
1131
|
-
const db = ensureDb2(dbManager2, request.params.id);
|
|
1132
|
-
return { hasFtsIndex: hasFtsTable(db) };
|
|
1133
|
-
});
|
|
1134
|
-
server2.post("/_web/sessions/:id/fts/rebuild", async (request) => {
|
|
1135
|
-
const db = ensureWritableDb(dbManager2, request.params.id);
|
|
1136
|
-
const result = rebuildFtsIndex(db);
|
|
1137
|
-
return { success: true, indexed: result.indexed };
|
|
1138
|
-
});
|
|
1139
|
-
server2.post("/_web/sessions/:id/summaries/generate", async (request, reply) => {
|
|
1140
|
-
const sessionId = request.params.id;
|
|
1141
|
-
const { chatSessionId, locale, forceRegenerate } = request.body;
|
|
1142
|
-
const aiDataDir = getAiDataDir(dbManager2);
|
|
1143
|
-
const llmConfig = getDefaultAssistantConfig(aiDataDir);
|
|
1144
|
-
if (!llmConfig) {
|
|
1145
|
-
return reply.code(400).send({ error: "No LLM configuration available" });
|
|
1146
|
-
}
|
|
1147
|
-
const db = ensureWritableDb(dbManager2, sessionId);
|
|
1148
|
-
const deps = buildSummaryDeps(db, llmConfig, aiDataDir);
|
|
1149
|
-
const result = await generateSessionSummary(deps, chatSessionId, { locale, forceRegenerate });
|
|
1150
|
-
return result;
|
|
1151
|
-
});
|
|
1152
|
-
server2.post("/_web/sessions/:id/summaries/generate-all", async (request, reply) => {
|
|
1153
|
-
const sessionId = request.params.id;
|
|
1154
|
-
const { locale, forceRegenerate } = request.body;
|
|
1155
|
-
const aiDataDir = getAiDataDir(dbManager2);
|
|
1156
|
-
const llmConfig = getDefaultAssistantConfig(aiDataDir);
|
|
1157
|
-
if (!llmConfig) {
|
|
1158
|
-
return reply.code(400).send({ error: "No LLM configuration available" });
|
|
1159
|
-
}
|
|
1160
|
-
const db = ensureWritableDb(dbManager2, sessionId);
|
|
1161
|
-
const chatSessions = getChatSessionList(db);
|
|
1162
|
-
const deps = buildSummaryDeps(db, llmConfig, aiDataDir);
|
|
1163
|
-
let success = 0;
|
|
1164
|
-
let failed = 0;
|
|
1165
|
-
for (const cs of chatSessions) {
|
|
1166
|
-
const result = await generateSessionSummary(deps, cs.id, { locale, forceRegenerate });
|
|
1167
|
-
if (result.success) success++;
|
|
1168
|
-
else failed++;
|
|
1169
|
-
}
|
|
1170
|
-
return { success, failed, total: chatSessions.length };
|
|
1171
|
-
});
|
|
1172
|
-
const DEMO_BASE_URL = "https://chatlab.fun/assets/demo";
|
|
1173
|
-
server2.post("/_web/demo/import", async (request, reply) => {
|
|
1174
|
-
const locale = request.body?.locale || "en";
|
|
1175
|
-
const nativeBinding = resolveNativeBinding();
|
|
1176
|
-
reply.raw.writeHead(200, {
|
|
1177
|
-
"Content-Type": "text/event-stream",
|
|
1178
|
-
"Cache-Control": "no-cache",
|
|
1179
|
-
Connection: "keep-alive"
|
|
1180
|
-
});
|
|
1181
|
-
function sendEvent(event, data) {
|
|
1182
|
-
reply.raw.write(`event: ${event}
|
|
1183
|
-
data: ${JSON.stringify(data)}
|
|
1184
1087
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1088
|
+
// src/http/routes/web/import.ts
|
|
1089
|
+
import * as fs4 from "fs";
|
|
1090
|
+
import * as os from "os";
|
|
1091
|
+
import * as path4 from "path";
|
|
1092
|
+
var DEMO_BASE_URL = "https://chatlab.fun/assets/demo";
|
|
1093
|
+
function cleanupTemp(...paths) {
|
|
1094
|
+
for (const p of paths) {
|
|
1190
1095
|
try {
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
fs3.writeFileSync(groupPath, Buffer.from(await groupResp.arrayBuffer()));
|
|
1197
|
-
sendEvent("progress", { stage: "downloading", current: 2, total: 2 });
|
|
1198
|
-
const privateResp = await fetch(`${DEMO_BASE_URL}/${locale}/demo-private.json`, {
|
|
1199
|
-
signal: AbortSignal.timeout(6e4)
|
|
1200
|
-
});
|
|
1201
|
-
if (!privateResp.ok) throw new Error(`Download private demo failed: ${privateResp.status}`);
|
|
1202
|
-
fs3.writeFileSync(privatePath, Buffer.from(await privateResp.arrayBuffer()));
|
|
1203
|
-
sendEvent("progress", { stage: "importing", current: 1, total: 2 });
|
|
1204
|
-
const groupResult = await streamImport(dbManager2, groupPath, { nativeBinding });
|
|
1205
|
-
if (!groupResult.success) throw new Error(groupResult.error || "Failed to import group demo");
|
|
1206
|
-
sendEvent("progress", { stage: "importing", current: 2, total: 2 });
|
|
1207
|
-
const privateResult = await streamImport(dbManager2, privatePath, { nativeBinding });
|
|
1208
|
-
if (!privateResult.success) throw new Error(privateResult.error || "Failed to import private demo");
|
|
1209
|
-
sendEvent("progress", { stage: "done", current: 2, total: 2 });
|
|
1210
|
-
sendEvent("result", {
|
|
1211
|
-
success: true,
|
|
1212
|
-
groupSessionId: groupResult.sessionId,
|
|
1213
|
-
privateSessionId: privateResult.sessionId
|
|
1214
|
-
});
|
|
1215
|
-
} catch (err) {
|
|
1216
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1217
|
-
sendEvent("progress", { stage: "error", current: 0, total: 2, message });
|
|
1218
|
-
sendEvent("result", { success: false, error: message });
|
|
1219
|
-
} finally {
|
|
1220
|
-
try {
|
|
1221
|
-
fs3.unlinkSync(groupPath);
|
|
1222
|
-
} catch {
|
|
1223
|
-
}
|
|
1224
|
-
try {
|
|
1225
|
-
fs3.unlinkSync(privatePath);
|
|
1226
|
-
} catch {
|
|
1227
|
-
}
|
|
1228
|
-
try {
|
|
1229
|
-
fs3.rmdirSync(tmpDir);
|
|
1230
|
-
} catch {
|
|
1096
|
+
const stat = fs4.statSync(p);
|
|
1097
|
+
if (stat.isDirectory()) {
|
|
1098
|
+
fs4.rmSync(p, { recursive: true, force: true });
|
|
1099
|
+
} else {
|
|
1100
|
+
fs4.unlinkSync(p);
|
|
1231
1101
|
}
|
|
1232
|
-
|
|
1102
|
+
} catch {
|
|
1233
1103
|
}
|
|
1234
|
-
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
function registerImportRoutes(server2, dbManager2) {
|
|
1235
1107
|
server2.get("/_web/supported-formats", async () => {
|
|
1236
1108
|
return getSupportedFormats();
|
|
1237
1109
|
});
|
|
1238
1110
|
server2.post("/_web/detect-format", async (request, reply) => {
|
|
1239
1111
|
const data = await request.file();
|
|
1240
1112
|
if (!data) return reply.code(400).send({ error: "No file uploaded" });
|
|
1241
|
-
const tmpDir =
|
|
1242
|
-
const tmpPath =
|
|
1113
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "chatlab-detect-"));
|
|
1114
|
+
const tmpPath = path4.join(tmpDir, data.filename || "upload");
|
|
1243
1115
|
try {
|
|
1244
1116
|
const chunks = [];
|
|
1245
1117
|
for await (const chunk of data.file) {
|
|
1246
1118
|
chunks.push(chunk);
|
|
1247
1119
|
}
|
|
1248
|
-
|
|
1120
|
+
fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
1249
1121
|
const format = detectFormat(tmpPath);
|
|
1250
1122
|
const allFormats = detectAllFormats(tmpPath);
|
|
1251
1123
|
return { format, allFormats };
|
|
1252
1124
|
} finally {
|
|
1253
|
-
|
|
1254
|
-
fs3.unlinkSync(tmpPath);
|
|
1255
|
-
} catch {
|
|
1256
|
-
}
|
|
1257
|
-
try {
|
|
1258
|
-
fs3.rmdirSync(tmpDir);
|
|
1259
|
-
} catch {
|
|
1260
|
-
}
|
|
1125
|
+
cleanupTemp(tmpPath, tmpDir);
|
|
1261
1126
|
}
|
|
1262
1127
|
});
|
|
1263
1128
|
server2.post("/_web/scan-multi-chat", async (request, reply) => {
|
|
1264
1129
|
const data = await request.file();
|
|
1265
1130
|
if (!data) return reply.code(400).send({ error: "No file uploaded" });
|
|
1266
|
-
const tmpDir =
|
|
1267
|
-
const tmpPath =
|
|
1131
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "chatlab-scan-"));
|
|
1132
|
+
const tmpPath = path4.join(tmpDir, data.filename || "upload");
|
|
1268
1133
|
try {
|
|
1269
1134
|
const chunks = [];
|
|
1270
1135
|
for await (const chunk of data.file) {
|
|
1271
1136
|
chunks.push(chunk);
|
|
1272
1137
|
}
|
|
1273
|
-
|
|
1138
|
+
fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
1274
1139
|
const chats = await scanMultiChatFile(tmpPath);
|
|
1275
1140
|
return { chats };
|
|
1276
1141
|
} finally {
|
|
1277
|
-
|
|
1278
|
-
fs3.unlinkSync(tmpPath);
|
|
1279
|
-
} catch {
|
|
1280
|
-
}
|
|
1281
|
-
try {
|
|
1282
|
-
fs3.rmdirSync(tmpDir);
|
|
1283
|
-
} catch {
|
|
1284
|
-
}
|
|
1142
|
+
cleanupTemp(tmpPath, tmpDir);
|
|
1285
1143
|
}
|
|
1286
1144
|
});
|
|
1287
1145
|
server2.post("/_web/import", async (request, reply) => {
|
|
1288
1146
|
const data = await request.file();
|
|
1289
1147
|
if (!data) return reply.code(400).send({ error: "No file uploaded" });
|
|
1290
|
-
const tmpDir =
|
|
1291
|
-
const tmpPath =
|
|
1148
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "chatlab-import-"));
|
|
1149
|
+
const tmpPath = path4.join(tmpDir, data.filename || "upload");
|
|
1292
1150
|
const chunks = [];
|
|
1293
1151
|
for await (const chunk of data.file) {
|
|
1294
1152
|
chunks.push(chunk);
|
|
1295
1153
|
}
|
|
1296
|
-
|
|
1154
|
+
fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
1297
1155
|
const formatId = data.fields?.formatId?.value;
|
|
1298
1156
|
const chatIndexStr = data.fields?.chatIndex?.value;
|
|
1299
1157
|
const chatIndex = chatIndexStr !== void 0 ? parseInt(chatIndexStr, 10) : void 0;
|
|
@@ -1302,57 +1160,123 @@ data: ${JSON.stringify(data)}
|
|
|
1302
1160
|
"Cache-Control": "no-cache",
|
|
1303
1161
|
Connection: "keep-alive"
|
|
1304
1162
|
});
|
|
1305
|
-
function sendEvent(event,
|
|
1163
|
+
function sendEvent(event, eventData) {
|
|
1306
1164
|
reply.raw.write(`event: ${event}
|
|
1307
|
-
data: ${JSON.stringify(
|
|
1165
|
+
data: ${JSON.stringify(eventData)}
|
|
1308
1166
|
|
|
1309
1167
|
`);
|
|
1310
1168
|
}
|
|
1311
1169
|
try {
|
|
1312
1170
|
const nativeBinding = resolveNativeBinding();
|
|
1313
|
-
const result = await streamImport(dbManager2, tmpPath, {
|
|
1314
|
-
formatId,
|
|
1315
|
-
chatIndex,
|
|
1171
|
+
const result = await streamImport(dbManager2, tmpPath, {
|
|
1172
|
+
formatId,
|
|
1173
|
+
chatIndex,
|
|
1174
|
+
nativeBinding,
|
|
1175
|
+
onProgress: (p) => sendEvent("progress", p)
|
|
1176
|
+
});
|
|
1177
|
+
if (result.success) {
|
|
1178
|
+
sendEvent("done", {
|
|
1179
|
+
success: true,
|
|
1180
|
+
sessionId: result.sessionId,
|
|
1181
|
+
messageCount: result.diagnostics?.messagesWritten ?? 0,
|
|
1182
|
+
memberCount: 0
|
|
1183
|
+
});
|
|
1184
|
+
} else {
|
|
1185
|
+
sendEvent("error", { success: false, error: result.error });
|
|
1186
|
+
}
|
|
1187
|
+
} catch (err) {
|
|
1188
|
+
sendEvent("error", { success: false, error: err instanceof Error ? err.message : String(err) });
|
|
1189
|
+
} finally {
|
|
1190
|
+
reply.raw.end();
|
|
1191
|
+
cleanupTemp(tmpPath, tmpDir);
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1194
|
+
server2.post("/_web/import-directory", async (request, reply) => {
|
|
1195
|
+
const parts = request.parts();
|
|
1196
|
+
if (!parts) return reply.code(400).send({ error: "No files uploaded" });
|
|
1197
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "chatlab-dir-import-"));
|
|
1198
|
+
const relativePaths = [];
|
|
1199
|
+
const fileBuffers = [];
|
|
1200
|
+
try {
|
|
1201
|
+
let sendEvent2 = function(event, eventData) {
|
|
1202
|
+
reply.raw.write(`event: ${event}
|
|
1203
|
+
data: ${JSON.stringify(eventData)}
|
|
1204
|
+
|
|
1205
|
+
`);
|
|
1206
|
+
};
|
|
1207
|
+
var sendEvent = sendEvent2;
|
|
1208
|
+
for await (const part of parts) {
|
|
1209
|
+
if (part.type === "field" && part.fieldname === "relativePaths") {
|
|
1210
|
+
relativePaths.push(String(part.value));
|
|
1211
|
+
} else if (part.type === "file") {
|
|
1212
|
+
const chunks = [];
|
|
1213
|
+
for await (const chunk of part.file) {
|
|
1214
|
+
chunks.push(chunk);
|
|
1215
|
+
}
|
|
1216
|
+
fileBuffers.push({ data: Buffer.concat(chunks), filename: part.filename || "" });
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
for (let i = 0; i < fileBuffers.length; i++) {
|
|
1220
|
+
let relPath = relativePaths[i] || fileBuffers[i].filename || `file_${i}`;
|
|
1221
|
+
const segments = relPath.split("/");
|
|
1222
|
+
if (segments.length > 1) {
|
|
1223
|
+
relPath = segments.slice(1).join("/");
|
|
1224
|
+
}
|
|
1225
|
+
const targetPath = path4.resolve(tmpDir, relPath);
|
|
1226
|
+
if (!targetPath.startsWith(tmpDir + path4.sep)) continue;
|
|
1227
|
+
fs4.mkdirSync(path4.dirname(targetPath), { recursive: true });
|
|
1228
|
+
fs4.writeFileSync(targetPath, fileBuffers[i].data);
|
|
1229
|
+
}
|
|
1230
|
+
const entryPath = findEntryFileInDirectory(tmpDir);
|
|
1231
|
+
if (!entryPath) {
|
|
1232
|
+
return reply.code(400).send({ error: "No recognizable import format found in directory" });
|
|
1233
|
+
}
|
|
1234
|
+
reply.raw.writeHead(200, {
|
|
1235
|
+
"Content-Type": "text/event-stream",
|
|
1236
|
+
"Cache-Control": "no-cache",
|
|
1237
|
+
Connection: "keep-alive"
|
|
1238
|
+
});
|
|
1239
|
+
const nativeBinding = resolveNativeBinding();
|
|
1240
|
+
const result = await streamImport(dbManager2, entryPath, {
|
|
1316
1241
|
nativeBinding,
|
|
1317
|
-
onProgress: (p) =>
|
|
1318
|
-
sendEvent("progress", p);
|
|
1319
|
-
}
|
|
1242
|
+
onProgress: (p) => sendEvent2("progress", p)
|
|
1320
1243
|
});
|
|
1321
1244
|
if (result.success) {
|
|
1322
|
-
|
|
1245
|
+
sendEvent2("done", {
|
|
1323
1246
|
success: true,
|
|
1324
1247
|
sessionId: result.sessionId,
|
|
1325
1248
|
messageCount: result.diagnostics?.messagesWritten ?? 0,
|
|
1326
1249
|
memberCount: 0
|
|
1327
1250
|
});
|
|
1328
1251
|
} else {
|
|
1329
|
-
|
|
1252
|
+
sendEvent2("error", { success: false, error: result.error });
|
|
1330
1253
|
}
|
|
1331
1254
|
} catch (err) {
|
|
1332
|
-
|
|
1255
|
+
if (!reply.raw.headersSent) {
|
|
1256
|
+
return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
|
|
1257
|
+
}
|
|
1258
|
+
reply.raw.write(
|
|
1259
|
+
`event: error
|
|
1260
|
+
data: ${JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) })}
|
|
1261
|
+
|
|
1262
|
+
`
|
|
1263
|
+
);
|
|
1333
1264
|
} finally {
|
|
1334
1265
|
reply.raw.end();
|
|
1335
|
-
|
|
1336
|
-
fs3.unlinkSync(tmpPath);
|
|
1337
|
-
} catch {
|
|
1338
|
-
}
|
|
1339
|
-
try {
|
|
1340
|
-
fs3.rmdirSync(tmpDir);
|
|
1341
|
-
} catch {
|
|
1342
|
-
}
|
|
1266
|
+
cleanupTemp(tmpDir);
|
|
1343
1267
|
}
|
|
1344
1268
|
});
|
|
1345
1269
|
server2.post("/_web/sessions/:id/import/incremental", async (request, reply) => {
|
|
1346
1270
|
const sessionId = request.params.id;
|
|
1347
1271
|
const data = await request.file();
|
|
1348
1272
|
if (!data) return reply.code(400).send({ error: "No file uploaded" });
|
|
1349
|
-
const tmpDir =
|
|
1350
|
-
const tmpPath =
|
|
1273
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "chatlab-inc-"));
|
|
1274
|
+
const tmpPath = path4.join(tmpDir, data.filename || "upload");
|
|
1351
1275
|
const chunks = [];
|
|
1352
1276
|
for await (const chunk of data.file) {
|
|
1353
1277
|
chunks.push(chunk);
|
|
1354
1278
|
}
|
|
1355
|
-
|
|
1279
|
+
fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
1356
1280
|
reply.raw.writeHead(200, {
|
|
1357
1281
|
"Content-Type": "text/event-stream",
|
|
1358
1282
|
"Cache-Control": "no-cache",
|
|
@@ -1366,9 +1290,7 @@ data: ${JSON.stringify(eventData)}
|
|
|
1366
1290
|
}
|
|
1367
1291
|
try {
|
|
1368
1292
|
const result = await incrementalImport(dbManager2, sessionId, tmpPath, {
|
|
1369
|
-
onProgress: (p) =>
|
|
1370
|
-
sendEvent("progress", p);
|
|
1371
|
-
}
|
|
1293
|
+
onProgress: (p) => sendEvent("progress", p)
|
|
1372
1294
|
});
|
|
1373
1295
|
if (result.success) {
|
|
1374
1296
|
sendEvent("done", result);
|
|
@@ -1379,237 +1301,299 @@ data: ${JSON.stringify(eventData)}
|
|
|
1379
1301
|
sendEvent("error", { success: false, error: err instanceof Error ? err.message : String(err) });
|
|
1380
1302
|
} finally {
|
|
1381
1303
|
reply.raw.end();
|
|
1382
|
-
|
|
1383
|
-
fs3.unlinkSync(tmpPath);
|
|
1384
|
-
} catch {
|
|
1385
|
-
}
|
|
1386
|
-
try {
|
|
1387
|
-
fs3.rmdirSync(tmpDir);
|
|
1388
|
-
} catch {
|
|
1389
|
-
}
|
|
1304
|
+
cleanupTemp(tmpPath, tmpDir);
|
|
1390
1305
|
}
|
|
1391
1306
|
});
|
|
1392
1307
|
server2.post("/_web/sessions/:id/import/incremental/analyze", async (request, reply) => {
|
|
1393
1308
|
const sessionId = request.params.id;
|
|
1394
1309
|
const data = await request.file();
|
|
1395
1310
|
if (!data) return reply.code(400).send({ error: "No file uploaded" });
|
|
1396
|
-
const tmpDir =
|
|
1397
|
-
const tmpPath =
|
|
1311
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "chatlab-analyze-"));
|
|
1312
|
+
const tmpPath = path4.join(tmpDir, data.filename || "upload");
|
|
1398
1313
|
const chunks = [];
|
|
1399
1314
|
for await (const chunk of data.file) {
|
|
1400
1315
|
chunks.push(chunk);
|
|
1401
1316
|
}
|
|
1402
|
-
|
|
1317
|
+
fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
1403
1318
|
try {
|
|
1404
|
-
|
|
1405
|
-
return result;
|
|
1319
|
+
return await analyzeIncrementalImport(dbManager2, sessionId, tmpPath);
|
|
1406
1320
|
} catch (err) {
|
|
1407
1321
|
return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
|
|
1408
1322
|
} finally {
|
|
1409
|
-
|
|
1410
|
-
fs3.unlinkSync(tmpPath);
|
|
1411
|
-
} catch {
|
|
1412
|
-
}
|
|
1413
|
-
try {
|
|
1414
|
-
fs3.rmdirSync(tmpDir);
|
|
1415
|
-
} catch {
|
|
1416
|
-
}
|
|
1323
|
+
cleanupTemp(tmpPath, tmpDir);
|
|
1417
1324
|
}
|
|
1418
1325
|
});
|
|
1419
1326
|
server2.post("/_web/import/analyze", async (request, reply) => {
|
|
1420
1327
|
const data = await request.file();
|
|
1421
1328
|
if (!data) return reply.code(400).send({ error: "No file uploaded" });
|
|
1422
|
-
const tmpDir =
|
|
1423
|
-
const tmpPath =
|
|
1329
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "chatlab-analyze-"));
|
|
1330
|
+
const tmpPath = path4.join(tmpDir, data.filename || "upload");
|
|
1424
1331
|
const chunks = [];
|
|
1425
1332
|
for await (const chunk of data.file) {
|
|
1426
1333
|
chunks.push(chunk);
|
|
1427
1334
|
}
|
|
1428
|
-
|
|
1335
|
+
fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
1429
1336
|
try {
|
|
1430
|
-
|
|
1431
|
-
return result;
|
|
1337
|
+
return await analyzeNewImport(tmpPath);
|
|
1432
1338
|
} catch (err) {
|
|
1433
1339
|
return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
|
|
1340
|
+
} finally {
|
|
1341
|
+
cleanupTemp(tmpPath, tmpDir);
|
|
1342
|
+
}
|
|
1343
|
+
});
|
|
1344
|
+
server2.post("/_web/demo/import", async (request, reply) => {
|
|
1345
|
+
const locale = request.body?.locale || "en";
|
|
1346
|
+
const nativeBinding = resolveNativeBinding();
|
|
1347
|
+
reply.raw.writeHead(200, {
|
|
1348
|
+
"Content-Type": "text/event-stream",
|
|
1349
|
+
"Cache-Control": "no-cache",
|
|
1350
|
+
Connection: "keep-alive"
|
|
1351
|
+
});
|
|
1352
|
+
function sendEvent(event, eventData) {
|
|
1353
|
+
reply.raw.write(`event: ${event}
|
|
1354
|
+
data: ${JSON.stringify(eventData)}
|
|
1355
|
+
|
|
1356
|
+
`);
|
|
1357
|
+
}
|
|
1358
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "chatlab-demo-"));
|
|
1359
|
+
const groupPath = path4.join(tmpDir, "demo-group.json");
|
|
1360
|
+
const privatePath = path4.join(tmpDir, "demo-private.json");
|
|
1361
|
+
try {
|
|
1362
|
+
sendEvent("progress", { stage: "downloading", current: 1, total: 2 });
|
|
1363
|
+
const groupResp = await fetch(`${DEMO_BASE_URL}/${locale}/demo-group.json`, {
|
|
1364
|
+
signal: AbortSignal.timeout(6e4)
|
|
1365
|
+
});
|
|
1366
|
+
if (!groupResp.ok) throw new Error(`Download group demo failed: ${groupResp.status}`);
|
|
1367
|
+
fs4.writeFileSync(groupPath, Buffer.from(await groupResp.arrayBuffer()));
|
|
1368
|
+
sendEvent("progress", { stage: "downloading", current: 2, total: 2 });
|
|
1369
|
+
const privateResp = await fetch(`${DEMO_BASE_URL}/${locale}/demo-private.json`, {
|
|
1370
|
+
signal: AbortSignal.timeout(6e4)
|
|
1371
|
+
});
|
|
1372
|
+
if (!privateResp.ok) throw new Error(`Download private demo failed: ${privateResp.status}`);
|
|
1373
|
+
fs4.writeFileSync(privatePath, Buffer.from(await privateResp.arrayBuffer()));
|
|
1374
|
+
sendEvent("progress", { stage: "importing", current: 1, total: 2 });
|
|
1375
|
+
const groupResult = await streamImport(dbManager2, groupPath, { nativeBinding });
|
|
1376
|
+
if (!groupResult.success) throw new Error(groupResult.error || "Failed to import group demo");
|
|
1377
|
+
sendEvent("progress", { stage: "importing", current: 2, total: 2 });
|
|
1378
|
+
const privateResult = await streamImport(dbManager2, privatePath, { nativeBinding });
|
|
1379
|
+
if (!privateResult.success) throw new Error(privateResult.error || "Failed to import private demo");
|
|
1380
|
+
sendEvent("progress", { stage: "done", current: 2, total: 2 });
|
|
1381
|
+
sendEvent("result", {
|
|
1382
|
+
success: true,
|
|
1383
|
+
groupSessionId: groupResult.sessionId,
|
|
1384
|
+
privateSessionId: privateResult.sessionId
|
|
1385
|
+
});
|
|
1386
|
+
} catch (err) {
|
|
1387
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1388
|
+
sendEvent("progress", { stage: "error", current: 0, total: 2, message });
|
|
1389
|
+
sendEvent("result", { success: false, error: message });
|
|
1390
|
+
} finally {
|
|
1391
|
+
reply.raw.end();
|
|
1392
|
+
cleanupTemp(tmpDir);
|
|
1393
|
+
}
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// src/http/routes/web/merge.ts
|
|
1398
|
+
import * as fs5 from "fs";
|
|
1399
|
+
import * as os2 from "os";
|
|
1400
|
+
import * as path5 from "path";
|
|
1401
|
+
function registerMergeRoutes(server2, dbManager2, mergeCache) {
|
|
1402
|
+
server2.post("/_web/merge/parse", async (request, reply) => {
|
|
1403
|
+
const data = await request.file();
|
|
1404
|
+
if (!data) return reply.code(400).send({ error: "No file uploaded" });
|
|
1405
|
+
const tmpDir = fs5.mkdtempSync(path5.join(os2.tmpdir(), "chatlab-merge-"));
|
|
1406
|
+
const tmpPath = path5.join(tmpDir, data.filename || "upload");
|
|
1407
|
+
const chunks = [];
|
|
1408
|
+
for await (const chunk of data.file) {
|
|
1409
|
+
chunks.push(chunk);
|
|
1410
|
+
}
|
|
1411
|
+
fs5.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
1412
|
+
try {
|
|
1413
|
+
const result = await streamParseFileInfo(tmpPath, {
|
|
1414
|
+
createTempDatabase(sourceFilePath) {
|
|
1415
|
+
return mergeCache.createTempDatabase(path5.basename(sourceFilePath));
|
|
1416
|
+
},
|
|
1417
|
+
onProgress() {
|
|
1418
|
+
}
|
|
1419
|
+
});
|
|
1420
|
+
const handle = mergeCache.store(data.filename || "upload", result.tempDbPath);
|
|
1421
|
+
return {
|
|
1422
|
+
handle,
|
|
1423
|
+
name: result.name,
|
|
1424
|
+
format: result.format,
|
|
1425
|
+
platform: result.platform,
|
|
1426
|
+
messageCount: result.messageCount,
|
|
1427
|
+
memberCount: result.memberCount,
|
|
1428
|
+
fileSize: result.fileSize
|
|
1429
|
+
};
|
|
1434
1430
|
} finally {
|
|
1435
1431
|
try {
|
|
1436
|
-
|
|
1432
|
+
fs5.unlinkSync(tmpPath);
|
|
1437
1433
|
} catch {
|
|
1438
1434
|
}
|
|
1439
1435
|
try {
|
|
1440
|
-
|
|
1436
|
+
fs5.rmdirSync(tmpDir);
|
|
1441
1437
|
} catch {
|
|
1442
1438
|
}
|
|
1443
1439
|
}
|
|
1444
1440
|
});
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
createTempDatabase(sourceFilePath) {
|
|
1459
|
-
return mergeCache.createTempDatabase(path3.basename(sourceFilePath));
|
|
1460
|
-
},
|
|
1461
|
-
onProgress() {
|
|
1462
|
-
}
|
|
1463
|
-
});
|
|
1464
|
-
const handle = mergeCache.store(data.filename || "upload", result.tempDbPath);
|
|
1465
|
-
return {
|
|
1466
|
-
handle,
|
|
1467
|
-
name: result.name,
|
|
1468
|
-
format: result.format,
|
|
1469
|
-
platform: result.platform,
|
|
1470
|
-
messageCount: result.messageCount,
|
|
1471
|
-
memberCount: result.memberCount,
|
|
1472
|
-
fileSize: result.fileSize
|
|
1473
|
-
};
|
|
1474
|
-
} finally {
|
|
1475
|
-
try {
|
|
1476
|
-
fs3.unlinkSync(tmpPath);
|
|
1477
|
-
} catch {
|
|
1478
|
-
}
|
|
1479
|
-
try {
|
|
1480
|
-
fs3.rmdirSync(tmpDir);
|
|
1481
|
-
} catch {
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
});
|
|
1485
|
-
server2.post("/_web/merge/conflicts", async (request, reply) => {
|
|
1486
|
-
const { handles } = request.body;
|
|
1487
|
-
if (!handles || !Array.isArray(handles) || handles.length === 0) {
|
|
1488
|
-
return reply.code(400).send({ error: "Missing or empty handles array" });
|
|
1489
|
-
}
|
|
1490
|
-
const readers = [];
|
|
1491
|
-
try {
|
|
1492
|
-
const dataSources = [];
|
|
1493
|
-
for (const handle of handles) {
|
|
1494
|
-
const entry = mergeCache.openReader(handle);
|
|
1495
|
-
if (!entry) return reply.code(404).send({ error: `Handle not found: ${handle}` });
|
|
1496
|
-
readers.push(entry.reader);
|
|
1497
|
-
dataSources.push({ source: entry.reader.toDataSource(), filename: entry.filename });
|
|
1498
|
-
}
|
|
1499
|
-
return checkConflictsFromSources(dataSources);
|
|
1500
|
-
} finally {
|
|
1501
|
-
for (const r of readers) r.close();
|
|
1502
|
-
}
|
|
1503
|
-
});
|
|
1504
|
-
server2.post("/_web/merge/execute", async (request, reply) => {
|
|
1505
|
-
const { handles, outputName, format = "json", andImport } = request.body;
|
|
1506
|
-
if (!handles || !Array.isArray(handles) || handles.length === 0) {
|
|
1507
|
-
return reply.code(400).send({ error: "Missing handles" });
|
|
1441
|
+
server2.post("/_web/merge/conflicts", async (request, reply) => {
|
|
1442
|
+
const { handles } = request.body;
|
|
1443
|
+
if (!handles || !Array.isArray(handles) || handles.length === 0) {
|
|
1444
|
+
return reply.code(400).send({ error: "Missing or empty handles array" });
|
|
1445
|
+
}
|
|
1446
|
+
const readers = [];
|
|
1447
|
+
try {
|
|
1448
|
+
const dataSources = [];
|
|
1449
|
+
for (const handle of handles) {
|
|
1450
|
+
const entry = mergeCache.openReader(handle);
|
|
1451
|
+
if (!entry) return reply.code(404).send({ error: `Handle not found: ${handle}` });
|
|
1452
|
+
readers.push(entry.reader);
|
|
1453
|
+
dataSources.push({ source: entry.reader.toDataSource(), filename: entry.filename });
|
|
1508
1454
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1455
|
+
return checkConflictsFromSources(dataSources);
|
|
1456
|
+
} finally {
|
|
1457
|
+
for (const r of readers) r.close();
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
1460
|
+
server2.post("/_web/merge/execute", async (request, reply) => {
|
|
1461
|
+
const { handles, outputName, format = "json", andImport } = request.body;
|
|
1462
|
+
if (!handles || !Array.isArray(handles) || handles.length === 0) {
|
|
1463
|
+
return reply.code(400).send({ error: "Missing handles" });
|
|
1464
|
+
}
|
|
1465
|
+
if (!outputName) {
|
|
1466
|
+
return reply.code(400).send({ error: "Missing outputName" });
|
|
1467
|
+
}
|
|
1468
|
+
const readers = [];
|
|
1469
|
+
try {
|
|
1470
|
+
const dataSources = [];
|
|
1471
|
+
for (const handle of handles) {
|
|
1472
|
+
const entry = mergeCache.openReader(handle);
|
|
1473
|
+
if (!entry) return reply.code(404).send({ error: `Handle not found: ${handle}` });
|
|
1474
|
+
readers.push(entry.reader);
|
|
1475
|
+
dataSources.push({ source: entry.reader.toDataSource(), filename: entry.filename });
|
|
1511
1476
|
}
|
|
1512
|
-
const
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
const { streamImport: serverStreamImport } = await import("./stream-import-RP3XB6NM.mjs");
|
|
1525
|
-
const jsonData = JSON.stringify(merged.chatLabData);
|
|
1526
|
-
const tmpDir = fs3.mkdtempSync(path3.join(os.tmpdir(), "chatlab-merged-"));
|
|
1527
|
-
const tmpPath = path3.join(tmpDir, `${outputName}.json`);
|
|
1528
|
-
fs3.writeFileSync(tmpPath, jsonData, "utf-8");
|
|
1477
|
+
const merged = buildMergedOutput(dataSources, outputName);
|
|
1478
|
+
let sessionId;
|
|
1479
|
+
if (andImport) {
|
|
1480
|
+
const { streamImport: serverStreamImport } = await import("./stream-import-VG7RP6KJ.mjs");
|
|
1481
|
+
const jsonData = JSON.stringify(merged.chatLabData);
|
|
1482
|
+
const tmpDir = fs5.mkdtempSync(path5.join(os2.tmpdir(), "chatlab-merged-"));
|
|
1483
|
+
const tmpPath = path5.join(tmpDir, `${outputName}.json`);
|
|
1484
|
+
fs5.writeFileSync(tmpPath, jsonData, "utf-8");
|
|
1485
|
+
try {
|
|
1486
|
+
const importResult = await serverStreamImport(dbManager2, tmpPath);
|
|
1487
|
+
sessionId = importResult.sessionId;
|
|
1488
|
+
} finally {
|
|
1529
1489
|
try {
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
} finally {
|
|
1533
|
-
try {
|
|
1534
|
-
fs3.unlinkSync(tmpPath);
|
|
1535
|
-
} catch {
|
|
1536
|
-
}
|
|
1537
|
-
try {
|
|
1538
|
-
fs3.rmdirSync(tmpDir);
|
|
1539
|
-
} catch {
|
|
1540
|
-
}
|
|
1490
|
+
fs5.unlinkSync(tmpPath);
|
|
1491
|
+
} catch {
|
|
1541
1492
|
}
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
}
|
|
1546
|
-
if (format === "jsonl") {
|
|
1547
|
-
const lines = [];
|
|
1548
|
-
for (const line of serializeChatLabToJsonl(merged.chatLabData)) {
|
|
1549
|
-
lines.push(line);
|
|
1493
|
+
try {
|
|
1494
|
+
fs5.rmdirSync(tmpDir);
|
|
1495
|
+
} catch {
|
|
1550
1496
|
}
|
|
1551
|
-
return { success: true, sessionId, data: lines.join("\n") };
|
|
1552
1497
|
}
|
|
1553
|
-
return { success: true, sessionId, data: merged.chatLabData };
|
|
1554
|
-
} finally {
|
|
1555
|
-
for (const r of readers) r.close();
|
|
1556
1498
|
}
|
|
1557
|
-
|
|
1558
|
-
server2.post("/_web/merge/clear", async (request) => {
|
|
1559
|
-
const { handle } = request.body || {};
|
|
1560
|
-
if (handle) {
|
|
1499
|
+
for (const handle of handles) {
|
|
1561
1500
|
mergeCache.delete(handle);
|
|
1562
|
-
} else {
|
|
1563
|
-
mergeCache.clear();
|
|
1564
|
-
}
|
|
1565
|
-
return { success: true };
|
|
1566
|
-
});
|
|
1567
|
-
server2.post("/_web/sessions/export-for-merge", async (request, reply) => {
|
|
1568
|
-
const { sessionIds } = request.body;
|
|
1569
|
-
if (!sessionIds || !Array.isArray(sessionIds) || sessionIds.length === 0) {
|
|
1570
|
-
return reply.code(400).send({ error: "Missing sessionIds" });
|
|
1571
1501
|
}
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
const
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
const { TempDbWriter } = await import("./src-ZELOFE7P.mjs");
|
|
1579
|
-
const writer = new TempDbWriter(tempDb);
|
|
1580
|
-
writer.writeMeta({
|
|
1581
|
-
name: exported.meta.name,
|
|
1582
|
-
platform: exported.meta.platform,
|
|
1583
|
-
type: exported.meta.type,
|
|
1584
|
-
groupId: exported.meta.groupId,
|
|
1585
|
-
groupAvatar: exported.meta.groupAvatar
|
|
1586
|
-
});
|
|
1587
|
-
writer.writeMembers(
|
|
1588
|
-
exported.members.map((m) => ({
|
|
1589
|
-
platformId: m.platformId,
|
|
1590
|
-
accountName: m.accountName,
|
|
1591
|
-
groupNickname: m.groupNickname,
|
|
1592
|
-
avatar: m.avatar
|
|
1593
|
-
}))
|
|
1594
|
-
);
|
|
1595
|
-
writer.writeMessages(
|
|
1596
|
-
exported.messages.map((msg) => ({
|
|
1597
|
-
senderPlatformId: msg.sender,
|
|
1598
|
-
senderAccountName: msg.accountName,
|
|
1599
|
-
senderGroupNickname: msg.groupNickname,
|
|
1600
|
-
timestamp: msg.timestamp,
|
|
1601
|
-
type: msg.type,
|
|
1602
|
-
content: msg.content
|
|
1603
|
-
}))
|
|
1604
|
-
);
|
|
1605
|
-
writer.finish();
|
|
1606
|
-
const handle = mergeCache.store(exported.meta.name, tempDbPath);
|
|
1607
|
-
handles.push({ sessionId: sid, handle });
|
|
1502
|
+
if (format === "jsonl") {
|
|
1503
|
+
const lines = [];
|
|
1504
|
+
for (const line of serializeChatLabToJsonl(merged.chatLabData)) {
|
|
1505
|
+
lines.push(line);
|
|
1506
|
+
}
|
|
1507
|
+
return { success: true, sessionId, data: lines.join("\n") };
|
|
1608
1508
|
}
|
|
1609
|
-
return { success: true,
|
|
1509
|
+
return { success: true, sessionId, data: merged.chatLabData };
|
|
1510
|
+
} finally {
|
|
1511
|
+
for (const r of readers) r.close();
|
|
1512
|
+
}
|
|
1513
|
+
});
|
|
1514
|
+
server2.post("/_web/merge/clear", async (request) => {
|
|
1515
|
+
const { handle } = request.body || {};
|
|
1516
|
+
if (handle) {
|
|
1517
|
+
mergeCache.delete(handle);
|
|
1518
|
+
} else {
|
|
1519
|
+
mergeCache.clear();
|
|
1520
|
+
}
|
|
1521
|
+
return { success: true };
|
|
1522
|
+
});
|
|
1523
|
+
server2.post("/_web/sessions/export-for-merge", async (request, reply) => {
|
|
1524
|
+
const { sessionIds } = request.body;
|
|
1525
|
+
if (!sessionIds || !Array.isArray(sessionIds) || sessionIds.length === 0) {
|
|
1526
|
+
return reply.code(400).send({ error: "Missing sessionIds" });
|
|
1527
|
+
}
|
|
1528
|
+
const handles = [];
|
|
1529
|
+
for (const sid of sessionIds) {
|
|
1530
|
+
const db = ensureDb2(dbManager2, sid);
|
|
1531
|
+
const exported = exportSessionToJson(db);
|
|
1532
|
+
const { db: tempDb, tempDbPath } = mergeCache.createTempDatabase(exported.meta.name);
|
|
1533
|
+
const { TempDbWriter } = await import("./src-QBT5SYW5.mjs");
|
|
1534
|
+
const writer = new TempDbWriter(tempDb);
|
|
1535
|
+
writer.writeMeta({
|
|
1536
|
+
name: exported.meta.name,
|
|
1537
|
+
platform: exported.meta.platform,
|
|
1538
|
+
type: exported.meta.type,
|
|
1539
|
+
groupId: exported.meta.groupId,
|
|
1540
|
+
groupAvatar: exported.meta.groupAvatar
|
|
1541
|
+
});
|
|
1542
|
+
writer.writeMembers(
|
|
1543
|
+
exported.members.map((m) => ({
|
|
1544
|
+
platformId: m.platformId,
|
|
1545
|
+
accountName: m.accountName,
|
|
1546
|
+
groupNickname: m.groupNickname,
|
|
1547
|
+
avatar: m.avatar
|
|
1548
|
+
}))
|
|
1549
|
+
);
|
|
1550
|
+
writer.writeMessages(
|
|
1551
|
+
exported.messages.map((msg) => ({
|
|
1552
|
+
senderPlatformId: msg.sender,
|
|
1553
|
+
senderAccountName: msg.accountName,
|
|
1554
|
+
senderGroupNickname: msg.groupNickname,
|
|
1555
|
+
timestamp: msg.timestamp,
|
|
1556
|
+
type: msg.type,
|
|
1557
|
+
content: msg.content
|
|
1558
|
+
}))
|
|
1559
|
+
);
|
|
1560
|
+
writer.finish();
|
|
1561
|
+
const handle = mergeCache.store(exported.meta.name, tempDbPath);
|
|
1562
|
+
handles.push({ sessionId: sid, handle });
|
|
1563
|
+
}
|
|
1564
|
+
return { success: true, handles };
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// src/http/routes/web/export.ts
|
|
1569
|
+
function registerExportRoutes(server2, adapter) {
|
|
1570
|
+
server2.post("/_web/sessions/:id/export/markdown", async (request, reply) => {
|
|
1571
|
+
const { id } = request.params;
|
|
1572
|
+
const body = request.body;
|
|
1573
|
+
const sessionName = body?.sessionName || id;
|
|
1574
|
+
const { result, content } = export_service_exports.exportMarkdown(adapter, {
|
|
1575
|
+
sessionId: id,
|
|
1576
|
+
sessionName,
|
|
1577
|
+
filterMode: body.filterMode || "condition",
|
|
1578
|
+
keywords: body.keywords,
|
|
1579
|
+
timeFilter: body.timeFilter,
|
|
1580
|
+
senderIds: body.senderIds,
|
|
1581
|
+
contextSize: body.contextSize,
|
|
1582
|
+
chatSessionIds: body.chatSessionIds
|
|
1610
1583
|
});
|
|
1611
|
-
|
|
1612
|
-
|
|
1584
|
+
if (!result.success) {
|
|
1585
|
+
return reply.code(500).send({ error: result.error });
|
|
1586
|
+
}
|
|
1587
|
+
reply.header("Content-Type", "text/markdown; charset=utf-8");
|
|
1588
|
+
reply.header("Content-Disposition", `attachment; filename="${encodeURIComponent(sessionName)}_export.md"`);
|
|
1589
|
+
return reply.send(content);
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// src/http/routes/web/cache.ts
|
|
1594
|
+
import * as fs6 from "fs";
|
|
1595
|
+
import * as path6 from "path";
|
|
1596
|
+
function registerCacheRoutes(server2, downloadsDir) {
|
|
1613
1597
|
server2.post("/_web/cache/save-to-downloads", async (request) => {
|
|
1614
1598
|
const { filename, dataUrl } = request.body;
|
|
1615
1599
|
if (!filename || !dataUrl) {
|
|
@@ -1618,11 +1602,11 @@ data: ${JSON.stringify(eventData)}
|
|
|
1618
1602
|
const base64Prefix = dataUrl.indexOf(",");
|
|
1619
1603
|
const base64Data = base64Prefix >= 0 ? dataUrl.slice(base64Prefix + 1) : dataUrl;
|
|
1620
1604
|
const buffer = Buffer.from(base64Data, "base64");
|
|
1621
|
-
if (!
|
|
1622
|
-
|
|
1605
|
+
if (!fs6.existsSync(downloadsDir)) {
|
|
1606
|
+
fs6.mkdirSync(downloadsDir, { recursive: true });
|
|
1623
1607
|
}
|
|
1624
|
-
const filePath =
|
|
1625
|
-
|
|
1608
|
+
const filePath = path6.join(downloadsDir, filename);
|
|
1609
|
+
fs6.writeFileSync(filePath, buffer);
|
|
1626
1610
|
return { success: true, filePath };
|
|
1627
1611
|
});
|
|
1628
1612
|
server2.post("/_web/cache/show-in-folder", async (request) => {
|
|
@@ -1632,7 +1616,7 @@ data: ${JSON.stringify(eventData)}
|
|
|
1632
1616
|
}
|
|
1633
1617
|
const { exec } = await import("child_process");
|
|
1634
1618
|
const platform = process.platform;
|
|
1635
|
-
const dir =
|
|
1619
|
+
const dir = path6.dirname(filePath);
|
|
1636
1620
|
if (platform === "darwin") {
|
|
1637
1621
|
exec(`open -R "${filePath}"`);
|
|
1638
1622
|
} else if (platform === "win32") {
|
|
@@ -1644,8 +1628,28 @@ data: ${JSON.stringify(eventData)}
|
|
|
1644
1628
|
});
|
|
1645
1629
|
}
|
|
1646
1630
|
|
|
1631
|
+
// src/http/routes/web/index.ts
|
|
1632
|
+
function registerWebRoutes(server2, dbManager2, options) {
|
|
1633
|
+
const adapter = createDatabaseManagerAdapter(dbManager2);
|
|
1634
|
+
const mergeCache = options?.pathProvider ? new MergeSessionCache(options.pathProvider, { nativeBinding: options.nativeBinding }) : null;
|
|
1635
|
+
mergeCache?.cleanupOrphans();
|
|
1636
|
+
const downloadsDir = options?.pathProvider?.getDownloadsDir() || path7.join(os3.homedir(), "Downloads");
|
|
1637
|
+
registerSessionRoutes2(server2, adapter);
|
|
1638
|
+
registerMemberRoutes(server2, adapter);
|
|
1639
|
+
registerAnalyticsRoutes(server2, dbManager2, adapter);
|
|
1640
|
+
registerSqlRoutes(server2, adapter);
|
|
1641
|
+
registerSessionIndexRoutes(server2, adapter);
|
|
1642
|
+
registerSummaryRoutes(server2, dbManager2, adapter);
|
|
1643
|
+
registerImportRoutes(server2, dbManager2);
|
|
1644
|
+
if (mergeCache) {
|
|
1645
|
+
registerMergeRoutes(server2, dbManager2, mergeCache);
|
|
1646
|
+
}
|
|
1647
|
+
registerExportRoutes(server2, adapter);
|
|
1648
|
+
registerCacheRoutes(server2, downloadsDir);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1647
1651
|
// src/http/routes/nlp.ts
|
|
1648
|
-
import * as
|
|
1652
|
+
import * as path8 from "path";
|
|
1649
1653
|
function ensureDb3(dbManager2, sessionId) {
|
|
1650
1654
|
const db = dbManager2.open(sessionId);
|
|
1651
1655
|
if (!db) {
|
|
@@ -1656,7 +1660,7 @@ function ensureDb3(dbManager2, sessionId) {
|
|
|
1656
1660
|
function registerNlpRoutes(server2, dbManager2) {
|
|
1657
1661
|
const pathProvider = dbManager2["pathProvider"];
|
|
1658
1662
|
if (pathProvider) {
|
|
1659
|
-
const nlpDir =
|
|
1663
|
+
const nlpDir = path8.join(pathProvider.getUserDataDir(), "nlp");
|
|
1660
1664
|
initNlpDir(nlpDir);
|
|
1661
1665
|
ensureDefaultDict(nlpDir).catch((err) => console.warn("[NLP] Auto-download zh-CN dict failed:", err));
|
|
1662
1666
|
}
|
|
@@ -1694,12 +1698,12 @@ function registerNlpRoutes(server2, dbManager2) {
|
|
|
1694
1698
|
}
|
|
1695
1699
|
function resolveNlpDir(dbManager2) {
|
|
1696
1700
|
const pathProvider = dbManager2["pathProvider"];
|
|
1697
|
-
return
|
|
1701
|
+
return path8.join(pathProvider.getUserDataDir(), "nlp");
|
|
1698
1702
|
}
|
|
1699
1703
|
|
|
1700
1704
|
// src/http/routes/ai.ts
|
|
1701
|
-
import * as
|
|
1702
|
-
import * as
|
|
1705
|
+
import * as fs10 from "fs";
|
|
1706
|
+
import * as path12 from "path";
|
|
1703
1707
|
|
|
1704
1708
|
// src/ai/logger.ts
|
|
1705
1709
|
var logger = null;
|
|
@@ -1722,7 +1726,6 @@ function closeServerAiLogger() {
|
|
|
1722
1726
|
// src/ai/tool-adapter.ts
|
|
1723
1727
|
var DEFAULT_MAX_TOOL_RESULT_TOKENS = 8e3;
|
|
1724
1728
|
var TOOL_TRUNCATION_STRATEGY = {
|
|
1725
|
-
search_keyword: "keep_first",
|
|
1726
1729
|
search_messages: "keep_first",
|
|
1727
1730
|
deep_search_messages: "keep_first",
|
|
1728
1731
|
get_recent_messages: "keep_last",
|
|
@@ -1780,13 +1783,13 @@ function adaptToolsForAgent(tools, getContext, options) {
|
|
|
1780
1783
|
}
|
|
1781
1784
|
|
|
1782
1785
|
// src/ai/assistant-loader.ts
|
|
1783
|
-
import * as
|
|
1784
|
-
import * as
|
|
1786
|
+
import * as fs7 from "fs";
|
|
1787
|
+
import * as path9 from "path";
|
|
1785
1788
|
function loadAssistantConfig(aiDataDir, assistantId) {
|
|
1786
|
-
const filePath =
|
|
1787
|
-
if (!
|
|
1789
|
+
const filePath = path9.join(aiDataDir, "assistants", `${assistantId}.md`);
|
|
1790
|
+
if (!fs7.existsSync(filePath)) return null;
|
|
1788
1791
|
try {
|
|
1789
|
-
const content =
|
|
1792
|
+
const content = fs7.readFileSync(filePath, "utf-8");
|
|
1790
1793
|
return parseAssistantFile(content, filePath);
|
|
1791
1794
|
} catch {
|
|
1792
1795
|
return null;
|
|
@@ -1794,31 +1797,31 @@ function loadAssistantConfig(aiDataDir, assistantId) {
|
|
|
1794
1797
|
}
|
|
1795
1798
|
|
|
1796
1799
|
// src/ai/manager-factory.ts
|
|
1797
|
-
import * as
|
|
1798
|
-
import * as
|
|
1800
|
+
import * as fs8 from "fs";
|
|
1801
|
+
import * as path10 from "path";
|
|
1799
1802
|
import { createHash, randomUUID as randomUUID3 } from "crypto";
|
|
1800
1803
|
var nodeFs = {
|
|
1801
1804
|
ensureDir(dir) {
|
|
1802
|
-
if (!
|
|
1805
|
+
if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
|
|
1803
1806
|
},
|
|
1804
1807
|
listFiles(dir, ext) {
|
|
1805
|
-
if (!
|
|
1806
|
-
return
|
|
1808
|
+
if (!fs8.existsSync(dir)) return [];
|
|
1809
|
+
return fs8.readdirSync(dir).filter((f) => f.endsWith(ext));
|
|
1807
1810
|
},
|
|
1808
1811
|
readFile(filePath) {
|
|
1809
|
-
return
|
|
1812
|
+
return fs8.readFileSync(filePath, "utf-8");
|
|
1810
1813
|
},
|
|
1811
1814
|
writeFile(filePath, content) {
|
|
1812
|
-
|
|
1815
|
+
fs8.writeFileSync(filePath, content, "utf-8");
|
|
1813
1816
|
},
|
|
1814
1817
|
deleteFile(filePath) {
|
|
1815
|
-
if (
|
|
1818
|
+
if (fs8.existsSync(filePath)) fs8.unlinkSync(filePath);
|
|
1816
1819
|
},
|
|
1817
1820
|
fileExists(filePath) {
|
|
1818
|
-
return
|
|
1821
|
+
return fs8.existsSync(filePath);
|
|
1819
1822
|
},
|
|
1820
1823
|
joinPath(...parts) {
|
|
1821
|
-
return
|
|
1824
|
+
return path10.join(...parts);
|
|
1822
1825
|
}
|
|
1823
1826
|
};
|
|
1824
1827
|
var managerCache = /* @__PURE__ */ new Map();
|
|
@@ -1828,13 +1831,13 @@ function getAssistantManager(aiDataDir) {
|
|
|
1828
1831
|
cached = {
|
|
1829
1832
|
assistant: new AssistantManager({
|
|
1830
1833
|
fs: nodeFs,
|
|
1831
|
-
assistantsDir:
|
|
1834
|
+
assistantsDir: path10.join(aiDataDir, "assistants"),
|
|
1832
1835
|
builtinRawConfigs: [],
|
|
1833
1836
|
generateId: () => `custom_${randomUUID3().replace(/-/g, "").slice(0, 12)}`
|
|
1834
1837
|
}),
|
|
1835
1838
|
skill: new SkillManagerCore({
|
|
1836
1839
|
fs: nodeFs,
|
|
1837
|
-
skillsDir:
|
|
1840
|
+
skillsDir: path10.join(aiDataDir, "skills"),
|
|
1838
1841
|
builtinRawSkills: [],
|
|
1839
1842
|
contentHash: (content) => createHash("md5").update(content).digest("hex")
|
|
1840
1843
|
})
|
|
@@ -1976,24 +1979,24 @@ async function runServerAgent(options) {
|
|
|
1976
1979
|
}
|
|
1977
1980
|
|
|
1978
1981
|
// src/ai/custom-store.ts
|
|
1979
|
-
import * as
|
|
1980
|
-
import * as
|
|
1982
|
+
import * as fs9 from "fs";
|
|
1983
|
+
import * as path11 from "path";
|
|
1981
1984
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
1982
1985
|
function readProviders(aiDataDir) {
|
|
1983
|
-
const storePath =
|
|
1984
|
-
if (!
|
|
1986
|
+
const storePath = path11.join(aiDataDir, "custom-providers.json");
|
|
1987
|
+
if (!fs9.existsSync(storePath)) return [];
|
|
1985
1988
|
try {
|
|
1986
|
-
const content =
|
|
1989
|
+
const content = fs9.readFileSync(storePath, "utf-8");
|
|
1987
1990
|
return JSON.parse(content);
|
|
1988
1991
|
} catch {
|
|
1989
1992
|
return [];
|
|
1990
1993
|
}
|
|
1991
1994
|
}
|
|
1992
1995
|
function writeProviders(aiDataDir, providers) {
|
|
1993
|
-
const storePath =
|
|
1994
|
-
const dir =
|
|
1995
|
-
if (!
|
|
1996
|
-
|
|
1996
|
+
const storePath = path11.join(aiDataDir, "custom-providers.json");
|
|
1997
|
+
const dir = path11.dirname(storePath);
|
|
1998
|
+
if (!fs9.existsSync(dir)) fs9.mkdirSync(dir, { recursive: true });
|
|
1999
|
+
fs9.writeFileSync(storePath, JSON.stringify(providers, null, 2), "utf-8");
|
|
1997
2000
|
}
|
|
1998
2001
|
function addCustomProvider(aiDataDir, input) {
|
|
1999
2002
|
const providers = readProviders(aiDataDir);
|
|
@@ -2024,20 +2027,20 @@ function deleteCustomProvider(aiDataDir, id) {
|
|
|
2024
2027
|
return { success: true };
|
|
2025
2028
|
}
|
|
2026
2029
|
function readModels(aiDataDir) {
|
|
2027
|
-
const storePath =
|
|
2028
|
-
if (!
|
|
2030
|
+
const storePath = path11.join(aiDataDir, "custom-models.json");
|
|
2031
|
+
if (!fs9.existsSync(storePath)) return [];
|
|
2029
2032
|
try {
|
|
2030
|
-
const content =
|
|
2033
|
+
const content = fs9.readFileSync(storePath, "utf-8");
|
|
2031
2034
|
return JSON.parse(content);
|
|
2032
2035
|
} catch {
|
|
2033
2036
|
return [];
|
|
2034
2037
|
}
|
|
2035
2038
|
}
|
|
2036
2039
|
function writeModels(aiDataDir, models) {
|
|
2037
|
-
const storePath =
|
|
2038
|
-
const dir =
|
|
2039
|
-
if (!
|
|
2040
|
-
|
|
2040
|
+
const storePath = path11.join(aiDataDir, "custom-models.json");
|
|
2041
|
+
const dir = path11.dirname(storePath);
|
|
2042
|
+
if (!fs9.existsSync(dir)) fs9.mkdirSync(dir, { recursive: true });
|
|
2043
|
+
fs9.writeFileSync(storePath, JSON.stringify(models, null, 2), "utf-8");
|
|
2041
2044
|
}
|
|
2042
2045
|
function addCustomModel(aiDataDir, input) {
|
|
2043
2046
|
const models = readModels(aiDataDir);
|
|
@@ -2151,10 +2154,10 @@ function registerAiRoutes(server2, dbManager2, convManager2) {
|
|
|
2151
2154
|
return mgr.getBuiltinCatalog();
|
|
2152
2155
|
});
|
|
2153
2156
|
server2.get("/_web/ai/llm/has-config", async () => {
|
|
2154
|
-
const configPath =
|
|
2155
|
-
if (!
|
|
2157
|
+
const configPath = path12.join(getAiDir(dbManager2), "llm-config.json");
|
|
2158
|
+
if (!fs10.existsSync(configPath)) return false;
|
|
2156
2159
|
try {
|
|
2157
|
-
const data = JSON.parse(
|
|
2160
|
+
const data = JSON.parse(fs10.readFileSync(configPath, "utf-8"));
|
|
2158
2161
|
if (!data.configs || !Array.isArray(data.configs) || data.configs.length === 0) return false;
|
|
2159
2162
|
return data.defaultAssistant != null;
|
|
2160
2163
|
} catch {
|
|
@@ -2162,10 +2165,10 @@ function registerAiRoutes(server2, dbManager2, convManager2) {
|
|
|
2162
2165
|
}
|
|
2163
2166
|
});
|
|
2164
2167
|
server2.get("/_web/ai/llm/configs", async () => {
|
|
2165
|
-
const configPath =
|
|
2166
|
-
if (!
|
|
2168
|
+
const configPath = path12.join(getAiDir(dbManager2), "llm-config.json");
|
|
2169
|
+
if (!fs10.existsSync(configPath)) return { configs: [], defaultAssistant: null, fastModel: null };
|
|
2167
2170
|
try {
|
|
2168
|
-
const data = JSON.parse(
|
|
2171
|
+
const data = JSON.parse(fs10.readFileSync(configPath, "utf-8"));
|
|
2169
2172
|
const configs = (data.configs || []).map((c) => {
|
|
2170
2173
|
const { apiKey: _k, ...rest } = c;
|
|
2171
2174
|
return { ...rest, apiKey: "" };
|
|
@@ -2191,11 +2194,11 @@ function registerAiRoutes(server2, dbManager2, convManager2) {
|
|
|
2191
2194
|
});
|
|
2192
2195
|
});
|
|
2193
2196
|
server2.get("/_web/ai/llm/provider-registry", async () => {
|
|
2194
|
-
const customPath =
|
|
2197
|
+
const customPath = path12.join(getAiDir(dbManager2), "custom-providers.json");
|
|
2195
2198
|
let custom = [];
|
|
2196
|
-
if (
|
|
2199
|
+
if (fs10.existsSync(customPath)) {
|
|
2197
2200
|
try {
|
|
2198
|
-
custom = JSON.parse(
|
|
2201
|
+
custom = JSON.parse(fs10.readFileSync(customPath, "utf-8"));
|
|
2199
2202
|
if (!Array.isArray(custom)) custom = [];
|
|
2200
2203
|
} catch {
|
|
2201
2204
|
custom = [];
|
|
@@ -2204,11 +2207,11 @@ function registerAiRoutes(server2, dbManager2, convManager2) {
|
|
|
2204
2207
|
return [...BUILTIN_PROVIDERS, ...custom];
|
|
2205
2208
|
});
|
|
2206
2209
|
server2.get("/_web/ai/llm/model-catalog", async () => {
|
|
2207
|
-
const customPath =
|
|
2210
|
+
const customPath = path12.join(getAiDir(dbManager2), "custom-models.json");
|
|
2208
2211
|
let custom = [];
|
|
2209
|
-
if (
|
|
2212
|
+
if (fs10.existsSync(customPath)) {
|
|
2210
2213
|
try {
|
|
2211
|
-
custom = JSON.parse(
|
|
2214
|
+
custom = JSON.parse(fs10.readFileSync(customPath, "utf-8"));
|
|
2212
2215
|
if (!Array.isArray(custom)) custom = [];
|
|
2213
2216
|
} catch {
|
|
2214
2217
|
custom = [];
|
|
@@ -2217,20 +2220,20 @@ function registerAiRoutes(server2, dbManager2, convManager2) {
|
|
|
2217
2220
|
return [...BUILTIN_MODELS, ...custom];
|
|
2218
2221
|
});
|
|
2219
2222
|
server2.get("/_web/ai/llm/default-assistant-slot", async () => {
|
|
2220
|
-
const configPath =
|
|
2221
|
-
if (!
|
|
2223
|
+
const configPath = path12.join(getAiDir(dbManager2), "llm-config.json");
|
|
2224
|
+
if (!fs10.existsSync(configPath)) return null;
|
|
2222
2225
|
try {
|
|
2223
|
-
const data = JSON.parse(
|
|
2226
|
+
const data = JSON.parse(fs10.readFileSync(configPath, "utf-8"));
|
|
2224
2227
|
return data.defaultAssistant ?? null;
|
|
2225
2228
|
} catch {
|
|
2226
2229
|
return null;
|
|
2227
2230
|
}
|
|
2228
2231
|
});
|
|
2229
2232
|
server2.get("/_web/ai/llm/fast-model-slot", async () => {
|
|
2230
|
-
const configPath =
|
|
2231
|
-
if (!
|
|
2233
|
+
const configPath = path12.join(getAiDir(dbManager2), "llm-config.json");
|
|
2234
|
+
if (!fs10.existsSync(configPath)) return null;
|
|
2232
2235
|
try {
|
|
2233
|
-
const data = JSON.parse(
|
|
2236
|
+
const data = JSON.parse(fs10.readFileSync(configPath, "utf-8"));
|
|
2234
2237
|
return data.fastModel ?? null;
|
|
2235
2238
|
} catch {
|
|
2236
2239
|
return null;
|
|
@@ -2583,8 +2586,8 @@ var noop = () => {
|
|
|
2583
2586
|
var NOOP_LOGGER = { info: noop, warn: noop, error: noop };
|
|
2584
2587
|
|
|
2585
2588
|
// ../../packages/sync/src/data-source-manager.ts
|
|
2586
|
-
import * as
|
|
2587
|
-
import * as
|
|
2589
|
+
import * as fs11 from "fs";
|
|
2590
|
+
import * as path13 from "path";
|
|
2588
2591
|
import * as crypto2 from "crypto";
|
|
2589
2592
|
var CONFIG_FILE = "data-sources.json";
|
|
2590
2593
|
function generateId(prefix = "ds") {
|
|
@@ -2612,17 +2615,17 @@ var DataSourceManager = class {
|
|
|
2612
2615
|
this.logger = logger2 ?? NOOP_LOGGER;
|
|
2613
2616
|
}
|
|
2614
2617
|
getConfigPath() {
|
|
2615
|
-
return
|
|
2618
|
+
return path13.join(this.configDir, CONFIG_FILE);
|
|
2616
2619
|
}
|
|
2617
2620
|
ensureDir() {
|
|
2618
|
-
|
|
2621
|
+
fs11.mkdirSync(this.configDir, { recursive: true });
|
|
2619
2622
|
}
|
|
2620
2623
|
// ==================== Load / Save ====================
|
|
2621
2624
|
loadAll() {
|
|
2622
2625
|
try {
|
|
2623
2626
|
const filePath = this.getConfigPath();
|
|
2624
|
-
if (
|
|
2625
|
-
const raw =
|
|
2627
|
+
if (fs11.existsSync(filePath)) {
|
|
2628
|
+
const raw = fs11.readFileSync(filePath, "utf-8");
|
|
2626
2629
|
const parsed = JSON.parse(raw);
|
|
2627
2630
|
if (!isValidDataSourceArray(parsed)) {
|
|
2628
2631
|
this.logger.warn("[DataSource] Incompatible config format detected, returning empty.");
|
|
@@ -2641,7 +2644,7 @@ var DataSourceManager = class {
|
|
|
2641
2644
|
saveAll(sources) {
|
|
2642
2645
|
try {
|
|
2643
2646
|
this.ensureDir();
|
|
2644
|
-
|
|
2647
|
+
fs11.writeFileSync(this.getConfigPath(), JSON.stringify(sources, null, 2), "utf-8");
|
|
2645
2648
|
} catch (err) {
|
|
2646
2649
|
this.logger.error("[DataSource] Failed to save config", err);
|
|
2647
2650
|
}
|
|
@@ -2715,12 +2718,12 @@ var DataSourceManager = class {
|
|
|
2715
2718
|
removeSession(sourceId, sessionId) {
|
|
2716
2719
|
const sources = this.loadAll();
|
|
2717
2720
|
const ds = sources.find((s) => s.id === sourceId);
|
|
2718
|
-
if (!ds) return
|
|
2719
|
-
const
|
|
2721
|
+
if (!ds) return null;
|
|
2722
|
+
const removed = ds.sessions.find((s) => s.id === sessionId);
|
|
2723
|
+
if (!removed) return null;
|
|
2720
2724
|
ds.sessions = ds.sessions.filter((s) => s.id !== sessionId);
|
|
2721
|
-
if (ds.sessions.length === before) return false;
|
|
2722
2725
|
this.saveAll(sources);
|
|
2723
|
-
return
|
|
2726
|
+
return removed;
|
|
2724
2727
|
}
|
|
2725
2728
|
updateSession(sourceId, sessionId, updates) {
|
|
2726
2729
|
const sources = this.loadAll();
|
|
@@ -2766,16 +2769,15 @@ function parseRemoteSessionsResponse(body) {
|
|
|
2766
2769
|
}
|
|
2767
2770
|
|
|
2768
2771
|
// ../../packages/sync/src/pull-engine.ts
|
|
2769
|
-
import * as
|
|
2772
|
+
import * as fs12 from "fs";
|
|
2770
2773
|
import * as crypto3 from "crypto";
|
|
2771
|
-
var MAX_PAGES_PER_PULL =
|
|
2774
|
+
var MAX_PAGES_PER_PULL = 5e3;
|
|
2772
2775
|
var PULL_OVERLAP_SECONDS = 60;
|
|
2773
2776
|
function buildPullUrl(baseUrl, remoteSessionId, params) {
|
|
2774
2777
|
const base = `${baseUrl}/sessions/${remoteSessionId}/messages`;
|
|
2775
2778
|
const qs = ["format=chatlab"];
|
|
2776
2779
|
if (params.since !== void 0 && params.since > 0) qs.push(`since=${params.since}`);
|
|
2777
2780
|
if (params.offset !== void 0 && params.offset > 0) qs.push(`offset=${params.offset}`);
|
|
2778
|
-
if (params.end !== void 0 && params.end > 0) qs.push(`end=${params.end}`);
|
|
2779
2781
|
if (params.limit !== void 0 && params.limit > 0) qs.push(`limit=${params.limit}`);
|
|
2780
2782
|
return base + "?" + qs.join("&");
|
|
2781
2783
|
}
|
|
@@ -2787,7 +2789,7 @@ function parseSyncFromFile(filePath) {
|
|
|
2787
2789
|
try {
|
|
2788
2790
|
const isJsonl = filePath.endsWith(".jsonl");
|
|
2789
2791
|
if (isJsonl) {
|
|
2790
|
-
const content =
|
|
2792
|
+
const content = fs12.readFileSync(filePath, "utf-8");
|
|
2791
2793
|
const lines = content.trimEnd().split("\n");
|
|
2792
2794
|
for (let i = lines.length - 1; i >= Math.max(0, lines.length - 5); i--) {
|
|
2793
2795
|
try {
|
|
@@ -2806,7 +2808,7 @@ function parseSyncFromFile(filePath) {
|
|
|
2806
2808
|
}
|
|
2807
2809
|
return null;
|
|
2808
2810
|
}
|
|
2809
|
-
const raw =
|
|
2811
|
+
const raw = fs12.readFileSync(filePath, "utf-8");
|
|
2810
2812
|
const parsed = JSON.parse(raw);
|
|
2811
2813
|
if (parsed.sync && typeof parsed.sync === "object") {
|
|
2812
2814
|
const s = parsed.sync;
|
|
@@ -2817,9 +2819,32 @@ function parseSyncFromFile(filePath) {
|
|
|
2817
2819
|
return null;
|
|
2818
2820
|
}
|
|
2819
2821
|
}
|
|
2822
|
+
function fileContainsMessages(filePath) {
|
|
2823
|
+
try {
|
|
2824
|
+
if (filePath.endsWith(".jsonl")) {
|
|
2825
|
+
const content = fs12.readFileSync(filePath, "utf-8");
|
|
2826
|
+
for (const line of content.split("\n")) {
|
|
2827
|
+
const trimmed = line.trim();
|
|
2828
|
+
if (!trimmed) continue;
|
|
2829
|
+
try {
|
|
2830
|
+
const obj = JSON.parse(trimmed);
|
|
2831
|
+
if (obj._type === "message") return true;
|
|
2832
|
+
} catch {
|
|
2833
|
+
continue;
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
return false;
|
|
2837
|
+
}
|
|
2838
|
+
const raw = fs12.readFileSync(filePath, "utf-8");
|
|
2839
|
+
const parsed = JSON.parse(raw);
|
|
2840
|
+
return Array.isArray(parsed.messages) && parsed.messages.length > 0;
|
|
2841
|
+
} catch {
|
|
2842
|
+
return false;
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2820
2845
|
function cleanupTempFile(filePath) {
|
|
2821
2846
|
try {
|
|
2822
|
-
if (
|
|
2847
|
+
if (fs12.existsSync(filePath)) fs12.unlinkSync(filePath);
|
|
2823
2848
|
} catch {
|
|
2824
2849
|
}
|
|
2825
2850
|
}
|
|
@@ -2830,6 +2855,8 @@ var PullEngine = class {
|
|
|
2830
2855
|
dsManager;
|
|
2831
2856
|
logger;
|
|
2832
2857
|
isImporting;
|
|
2858
|
+
pullingSourceIds = /* @__PURE__ */ new Set();
|
|
2859
|
+
progressMap = /* @__PURE__ */ new Map();
|
|
2833
2860
|
constructor(options) {
|
|
2834
2861
|
this.fetcher = options.fetcher;
|
|
2835
2862
|
this.importer = options.importer;
|
|
@@ -2838,6 +2865,9 @@ var PullEngine = class {
|
|
|
2838
2865
|
this.logger = options.logger ?? NOOP_LOGGER;
|
|
2839
2866
|
this.isImporting = options.isImporting ?? (() => false);
|
|
2840
2867
|
}
|
|
2868
|
+
getProgress() {
|
|
2869
|
+
return Array.from(this.progressMap.values());
|
|
2870
|
+
}
|
|
2841
2871
|
async importTempFile(baseUrl, sess, tempFile) {
|
|
2842
2872
|
let targetId = sess.targetSessionId;
|
|
2843
2873
|
if (!targetId) {
|
|
@@ -2851,6 +2881,11 @@ var PullEngine = class {
|
|
|
2851
2881
|
return this.importer.importFile(tempFile, targetId || void 0, externalId);
|
|
2852
2882
|
}
|
|
2853
2883
|
async executePullSession(sourceId, ds, sess) {
|
|
2884
|
+
const currentDs = this.dsManager.get(sourceId);
|
|
2885
|
+
if (!currentDs || !currentDs.sessions.some((s) => s.id === sess.id)) {
|
|
2886
|
+
this.logger.info(`[Pull] Skipping "${sess.name}": session no longer exists`);
|
|
2887
|
+
return { success: true, newMessageCount: 0 };
|
|
2888
|
+
}
|
|
2854
2889
|
if (this.isImporting()) {
|
|
2855
2890
|
this.logger.info(`[Pull] Skipping "${sess.name}": import in progress`);
|
|
2856
2891
|
return { success: false, newMessageCount: 0, error: "Import in progress" };
|
|
@@ -2858,34 +2893,114 @@ var PullEngine = class {
|
|
|
2858
2893
|
this.logger.info(`[Pull] Pulling "${sess.name}" from ${ds.baseUrl}`);
|
|
2859
2894
|
let totalNewMessages = 0;
|
|
2860
2895
|
let since = sess.lastPullAt;
|
|
2861
|
-
let offset = 0;
|
|
2862
|
-
let end;
|
|
2863
2896
|
let pageCount = 0;
|
|
2864
2897
|
let resyncAttempted = false;
|
|
2898
|
+
this.progressMap.set(sess.id, { sessionId: sess.id, sessionName: sess.name, current: 0, pages: 0, done: false });
|
|
2865
2899
|
try {
|
|
2866
2900
|
while (pageCount < MAX_PAGES_PER_PULL) {
|
|
2867
2901
|
pageCount++;
|
|
2868
2902
|
const tempFile = await this.fetcher.fetchToTempFile(ds.baseUrl, sess.remoteSessionId, ds.token, {
|
|
2869
2903
|
since,
|
|
2870
|
-
offset,
|
|
2871
|
-
end,
|
|
2872
2904
|
limit: ds.pullLimit
|
|
2873
2905
|
});
|
|
2874
2906
|
try {
|
|
2875
|
-
const stat =
|
|
2907
|
+
const stat = fs12.statSync(tempFile);
|
|
2876
2908
|
this.logger.info(`[Pull] "${sess.name}" page ${pageCount}: fetched ${stat.size} bytes`);
|
|
2909
|
+
const sync0 = parseSyncFromFile(tempFile);
|
|
2910
|
+
if (stat.size < 1024) {
|
|
2911
|
+
if (!fileContainsMessages(tempFile) && sync0?.hasMore === false) {
|
|
2912
|
+
cleanupTempFile(tempFile);
|
|
2913
|
+
const retryDelays = [2e3, 3e3, 5e3];
|
|
2914
|
+
let retrySuccess = false;
|
|
2915
|
+
let retryHasMore = false;
|
|
2916
|
+
for (let ri = 0; ri < retryDelays.length; ri++) {
|
|
2917
|
+
this.logger.info(
|
|
2918
|
+
`[Pull] "${sess.name}" page ${pageCount} got empty response, retry ${ri + 1}/${retryDelays.length} after ${retryDelays[ri]}ms`
|
|
2919
|
+
);
|
|
2920
|
+
await new Promise((r) => setTimeout(r, retryDelays[ri]));
|
|
2921
|
+
const retryFile = await this.fetcher.fetchToTempFile(ds.baseUrl, sess.remoteSessionId, ds.token, {
|
|
2922
|
+
since,
|
|
2923
|
+
limit: ds.pullLimit
|
|
2924
|
+
});
|
|
2925
|
+
const retryStat = fs12.statSync(retryFile);
|
|
2926
|
+
this.logger.info(`[Pull] "${sess.name}" retry ${ri + 1}: fetched ${retryStat.size} bytes`);
|
|
2927
|
+
const retrySync = parseSyncFromFile(retryFile);
|
|
2928
|
+
if (retryStat.size < 1024 && !fileContainsMessages(retryFile) && retrySync?.hasMore === false) {
|
|
2929
|
+
cleanupTempFile(retryFile);
|
|
2930
|
+
continue;
|
|
2931
|
+
}
|
|
2932
|
+
const retryResult = await this.importTempFile(ds.baseUrl, sess, retryFile);
|
|
2933
|
+
cleanupTempFile(retryFile);
|
|
2934
|
+
if (retryResult.needFullResync && !resyncAttempted) {
|
|
2935
|
+
resyncAttempted = true;
|
|
2936
|
+
this.logger.info(`[Pull] Resetting since=0 for "${sess.name}" full resync`);
|
|
2937
|
+
since = 0;
|
|
2938
|
+
pageCount = 0;
|
|
2939
|
+
sess.targetSessionId = "";
|
|
2940
|
+
sess.lastPullAt = 0;
|
|
2941
|
+
this.dsManager.updateSession(sourceId, sess.id, { targetSessionId: "", lastPullAt: 0 });
|
|
2942
|
+
retrySuccess = true;
|
|
2943
|
+
retryHasMore = true;
|
|
2944
|
+
break;
|
|
2945
|
+
}
|
|
2946
|
+
if (retryResult.needFullResync) {
|
|
2947
|
+
const errMsg = "Full resync failed";
|
|
2948
|
+
this.logger.error(`[Pull] Full resync already attempted for "${sess.name}", aborting`);
|
|
2949
|
+
this.dsManager.updateSession(sourceId, sess.id, {
|
|
2950
|
+
lastPullAt: Math.floor(Date.now() / 1e3),
|
|
2951
|
+
lastStatus: "error",
|
|
2952
|
+
lastError: errMsg
|
|
2953
|
+
});
|
|
2954
|
+
this.notifier.onPullResult(sourceId, sess.id, "error", errMsg);
|
|
2955
|
+
this.markProgressDone(sess.id);
|
|
2956
|
+
return { success: false, newMessageCount: 0, error: errMsg };
|
|
2957
|
+
}
|
|
2958
|
+
if (!retryResult.success) {
|
|
2959
|
+
const errMsg = retryResult.error || "Import failed";
|
|
2960
|
+
this.dsManager.updateSession(sourceId, sess.id, {
|
|
2961
|
+
lastPullAt: Math.floor(Date.now() / 1e3),
|
|
2962
|
+
lastStatus: "error",
|
|
2963
|
+
lastError: errMsg
|
|
2964
|
+
});
|
|
2965
|
+
this.notifier.onPullResult(sourceId, sess.id, "error", errMsg);
|
|
2966
|
+
this.markProgressDone(sess.id);
|
|
2967
|
+
return { success: false, newMessageCount: 0, error: errMsg };
|
|
2968
|
+
}
|
|
2969
|
+
if (retryResult.success && retryResult.sessionId && !sess.targetSessionId) {
|
|
2970
|
+
sess.targetSessionId = retryResult.sessionId;
|
|
2971
|
+
this.dsManager.updateSession(sourceId, sess.id, { targetSessionId: retryResult.sessionId });
|
|
2972
|
+
}
|
|
2973
|
+
totalNewMessages += retryResult.newMessageCount;
|
|
2974
|
+
this.progressMap.set(sess.id, {
|
|
2975
|
+
sessionId: sess.id,
|
|
2976
|
+
sessionName: sess.name,
|
|
2977
|
+
current: totalNewMessages,
|
|
2978
|
+
pages: pageCount,
|
|
2979
|
+
done: false
|
|
2980
|
+
});
|
|
2981
|
+
if (retrySync?.hasMore && retrySync.nextSince !== void 0) {
|
|
2982
|
+
since = retrySync.nextSince;
|
|
2983
|
+
}
|
|
2984
|
+
retryHasMore = !!retrySync?.hasMore;
|
|
2985
|
+
retrySuccess = true;
|
|
2986
|
+
break;
|
|
2987
|
+
}
|
|
2988
|
+
if (!retrySuccess) break;
|
|
2989
|
+
if (!retryHasMore) break;
|
|
2990
|
+
continue;
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2877
2993
|
if (stat.size === 0) {
|
|
2878
2994
|
cleanupTempFile(tempFile);
|
|
2879
2995
|
break;
|
|
2880
2996
|
}
|
|
2881
|
-
const sync =
|
|
2997
|
+
const sync = sync0;
|
|
2882
2998
|
const result = await this.importTempFile(ds.baseUrl, sess, tempFile);
|
|
2883
2999
|
cleanupTempFile(tempFile);
|
|
2884
3000
|
if (result.needFullResync && !resyncAttempted) {
|
|
2885
3001
|
resyncAttempted = true;
|
|
2886
3002
|
this.logger.info(`[Pull] Resetting since=0 for "${sess.name}" full resync`);
|
|
2887
3003
|
since = 0;
|
|
2888
|
-
offset = 0;
|
|
2889
3004
|
pageCount = 0;
|
|
2890
3005
|
sess.targetSessionId = "";
|
|
2891
3006
|
sess.lastPullAt = 0;
|
|
@@ -2901,6 +3016,7 @@ var PullEngine = class {
|
|
|
2901
3016
|
lastError: errMsg
|
|
2902
3017
|
});
|
|
2903
3018
|
this.notifier.onPullResult(sourceId, sess.id, "error", errMsg);
|
|
3019
|
+
this.markProgressDone(sess.id);
|
|
2904
3020
|
return { success: false, newMessageCount: 0, error: errMsg };
|
|
2905
3021
|
}
|
|
2906
3022
|
if (!result.success) {
|
|
@@ -2911,6 +3027,7 @@ var PullEngine = class {
|
|
|
2911
3027
|
lastError: errMsg
|
|
2912
3028
|
});
|
|
2913
3029
|
this.notifier.onPullResult(sourceId, sess.id, "error", errMsg);
|
|
3030
|
+
this.markProgressDone(sess.id);
|
|
2914
3031
|
return { success: false, newMessageCount: 0, error: errMsg };
|
|
2915
3032
|
}
|
|
2916
3033
|
if (!sess.targetSessionId && result.sessionId) {
|
|
@@ -2918,11 +3035,15 @@ var PullEngine = class {
|
|
|
2918
3035
|
this.dsManager.updateSession(sourceId, sess.id, { targetSessionId: result.sessionId });
|
|
2919
3036
|
}
|
|
2920
3037
|
totalNewMessages += result.newMessageCount;
|
|
3038
|
+
this.progressMap.set(sess.id, {
|
|
3039
|
+
sessionId: sess.id,
|
|
3040
|
+
sessionName: sess.name,
|
|
3041
|
+
current: totalNewMessages,
|
|
3042
|
+
pages: pageCount,
|
|
3043
|
+
done: false
|
|
3044
|
+
});
|
|
2921
3045
|
if (!sync || !sync.hasMore) break;
|
|
2922
3046
|
if (sync.nextSince !== void 0) since = sync.nextSince;
|
|
2923
|
-
if (sync.nextOffset !== void 0) offset = sync.nextOffset;
|
|
2924
|
-
else offset = 0;
|
|
2925
|
-
if (sync.watermark !== void 0 && !end) end = sync.watermark;
|
|
2926
3047
|
} catch (importErr) {
|
|
2927
3048
|
cleanupTempFile(tempFile);
|
|
2928
3049
|
throw importErr;
|
|
@@ -2939,6 +3060,7 @@ var PullEngine = class {
|
|
|
2939
3060
|
});
|
|
2940
3061
|
if (totalNewMessages > 0) this.notifier.onSessionListChanged();
|
|
2941
3062
|
this.notifier.onPullResult(sourceId, sess.id, "success", `+${totalNewMessages} messages`);
|
|
3063
|
+
this.markProgressDone(sess.id);
|
|
2942
3064
|
return { success: true, newMessageCount: totalNewMessages };
|
|
2943
3065
|
} catch (error) {
|
|
2944
3066
|
const errMsg = error.message || "Pull failed";
|
|
@@ -2949,12 +3071,29 @@ var PullEngine = class {
|
|
|
2949
3071
|
lastError: errMsg
|
|
2950
3072
|
});
|
|
2951
3073
|
this.notifier.onPullResult(sourceId, sess.id, "error", errMsg);
|
|
3074
|
+
this.markProgressDone(sess.id);
|
|
2952
3075
|
return { success: false, newMessageCount: 0, error: errMsg };
|
|
2953
3076
|
}
|
|
2954
3077
|
}
|
|
3078
|
+
markProgressDone(sessionId) {
|
|
3079
|
+
const p = this.progressMap.get(sessionId);
|
|
3080
|
+
if (p) {
|
|
3081
|
+
p.done = true;
|
|
3082
|
+
setTimeout(() => this.progressMap.delete(sessionId), 5e3);
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
2955
3085
|
async pullAllSessions(ds) {
|
|
2956
|
-
|
|
2957
|
-
|
|
3086
|
+
if (this.pullingSourceIds.has(ds.id)) {
|
|
3087
|
+
this.logger.info(`[Pull] Skipping pullAllSessions for "${ds.baseUrl}": pull already in progress`);
|
|
3088
|
+
return;
|
|
3089
|
+
}
|
|
3090
|
+
this.pullingSourceIds.add(ds.id);
|
|
3091
|
+
try {
|
|
3092
|
+
for (const sess of ds.sessions) {
|
|
3093
|
+
await this.executePullSession(ds.id, ds, sess);
|
|
3094
|
+
}
|
|
3095
|
+
} finally {
|
|
3096
|
+
this.pullingSourceIds.delete(ds.id);
|
|
2958
3097
|
}
|
|
2959
3098
|
}
|
|
2960
3099
|
async triggerPull(sourceId, sessionId) {
|
|
@@ -2966,15 +3105,24 @@ var PullEngine = class {
|
|
|
2966
3105
|
const result = await this.executePullSession(sourceId, ds, sess);
|
|
2967
3106
|
return { success: result.success, error: result.error };
|
|
2968
3107
|
}
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
if (!result.success && result.error) errors.push(`${sess.name}: ${result.error}`);
|
|
3108
|
+
if (this.pullingSourceIds.has(sourceId)) {
|
|
3109
|
+
this.logger.info(`[Pull] Skipping triggerPull for source ${sourceId}: pull already in progress`);
|
|
3110
|
+
return { success: true };
|
|
2973
3111
|
}
|
|
2974
|
-
|
|
2975
|
-
|
|
3112
|
+
this.pullingSourceIds.add(sourceId);
|
|
3113
|
+
try {
|
|
3114
|
+
const errors = [];
|
|
3115
|
+
for (const sess of ds.sessions) {
|
|
3116
|
+
const result = await this.executePullSession(sourceId, ds, sess);
|
|
3117
|
+
if (!result.success && result.error) errors.push(`${sess.name}: ${result.error}`);
|
|
3118
|
+
}
|
|
3119
|
+
if (errors.length > 0) {
|
|
3120
|
+
return { success: false, error: errors.join("; ") };
|
|
3121
|
+
}
|
|
3122
|
+
return { success: true };
|
|
3123
|
+
} finally {
|
|
3124
|
+
this.pullingSourceIds.delete(sourceId);
|
|
2976
3125
|
}
|
|
2977
|
-
return { success: true };
|
|
2978
3126
|
}
|
|
2979
3127
|
async triggerPullAll(sourceId) {
|
|
2980
3128
|
return this.triggerPull(sourceId);
|
|
@@ -3048,19 +3196,19 @@ function reloadTimer(dsId, skipInitialPull = false) {
|
|
|
3048
3196
|
}
|
|
3049
3197
|
|
|
3050
3198
|
// src/sync/adapters.ts
|
|
3051
|
-
import * as
|
|
3052
|
-
import * as
|
|
3053
|
-
import * as
|
|
3199
|
+
import * as fs13 from "fs";
|
|
3200
|
+
import * as os4 from "os";
|
|
3201
|
+
import * as path14 from "path";
|
|
3054
3202
|
import * as crypto4 from "crypto";
|
|
3055
3203
|
function getTempFilePath(ext) {
|
|
3056
3204
|
const id = crypto4.randomBytes(8).toString("hex");
|
|
3057
|
-
return
|
|
3205
|
+
return path14.join(os4.tmpdir(), `chatlab-pull-${id}${ext}`);
|
|
3058
3206
|
}
|
|
3059
3207
|
var NodeFetcher = class {
|
|
3060
3208
|
async fetchToTempFile(baseUrl, remoteSessionId, token, params) {
|
|
3061
3209
|
const url = buildPullUrl(baseUrl, remoteSessionId, params);
|
|
3062
3210
|
const headers = {
|
|
3063
|
-
Accept: "application/json
|
|
3211
|
+
Accept: "application/json"
|
|
3064
3212
|
};
|
|
3065
3213
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
3066
3214
|
const response = await fetch(url, { headers, signal: AbortSignal.timeout(12e4) });
|
|
@@ -3071,14 +3219,14 @@ var NodeFetcher = class {
|
|
|
3071
3219
|
const isJsonl = contentType.includes("ndjson") || contentType.includes("jsonl");
|
|
3072
3220
|
const tempFile = getTempFilePath(isJsonl ? ".jsonl" : ".json");
|
|
3073
3221
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
3074
|
-
|
|
3222
|
+
fs13.writeFileSync(tempFile, buffer);
|
|
3075
3223
|
return tempFile;
|
|
3076
3224
|
}
|
|
3077
3225
|
};
|
|
3078
3226
|
function resolveNativeBinding2() {
|
|
3079
3227
|
if (process.versions.electron) return void 0;
|
|
3080
|
-
const nativePath =
|
|
3081
|
-
if (
|
|
3228
|
+
const nativePath = path14.resolve(__dirname, "../../native/better_sqlite3.node");
|
|
3229
|
+
if (fs13.existsSync(nativePath)) return nativePath;
|
|
3082
3230
|
return void 0;
|
|
3083
3231
|
}
|
|
3084
3232
|
var DirectImporter = class {
|
|
@@ -3092,12 +3240,37 @@ var DirectImporter = class {
|
|
|
3092
3240
|
}
|
|
3093
3241
|
sessionExists(sessionId) {
|
|
3094
3242
|
const dbPath = this.dbManager.getDbPath(sessionId);
|
|
3095
|
-
|
|
3243
|
+
if (!fs13.existsSync(dbPath)) return false;
|
|
3244
|
+
try {
|
|
3245
|
+
const db = openBetterSqliteDatabase(dbPath, { readonly: true, nativeBinding: this.nativeBinding });
|
|
3246
|
+
const row = db.prepare("SELECT COUNT(*) as cnt FROM sqlite_master WHERE type='table' AND name='message'").get();
|
|
3247
|
+
db.close();
|
|
3248
|
+
if (row.cnt === 0) {
|
|
3249
|
+
this.logger.warn(`[DirectImporter] DB file exists but has no message table: ${sessionId}, removing`);
|
|
3250
|
+
try {
|
|
3251
|
+
fs13.unlinkSync(dbPath);
|
|
3252
|
+
} catch {
|
|
3253
|
+
}
|
|
3254
|
+
return false;
|
|
3255
|
+
}
|
|
3256
|
+
return true;
|
|
3257
|
+
} catch {
|
|
3258
|
+
this.logger.warn(`[DirectImporter] Cannot validate DB file: ${sessionId}, removing`);
|
|
3259
|
+
try {
|
|
3260
|
+
fs13.unlinkSync(dbPath);
|
|
3261
|
+
} catch {
|
|
3262
|
+
}
|
|
3263
|
+
return false;
|
|
3264
|
+
}
|
|
3096
3265
|
}
|
|
3097
3266
|
async importFile(tempFile, targetSessionId, externalId) {
|
|
3098
3267
|
if (targetSessionId && this.sessionExists(targetSessionId)) {
|
|
3099
3268
|
return this.incrementalImportFile(targetSessionId, tempFile);
|
|
3100
3269
|
}
|
|
3270
|
+
if (targetSessionId) {
|
|
3271
|
+
this.logger.info(`[DirectImporter] Session ${targetSessionId} not found locally, need full resync`);
|
|
3272
|
+
return { success: false, newMessageCount: 0, needFullResync: true };
|
|
3273
|
+
}
|
|
3101
3274
|
return this.fullImportFile(tempFile, externalId);
|
|
3102
3275
|
}
|
|
3103
3276
|
async incrementalImportFile(sessionId, tempFile) {
|
|
@@ -3118,7 +3291,7 @@ var DirectImporter = class {
|
|
|
3118
3291
|
sessionId
|
|
3119
3292
|
};
|
|
3120
3293
|
}
|
|
3121
|
-
if (result.error?.includes("not found") || result.error?.includes("session_not_found")) {
|
|
3294
|
+
if (result.error?.includes("not found") || result.error?.includes("session_not_found") || result.error?.includes("no such table")) {
|
|
3122
3295
|
return { success: false, newMessageCount: 0, sessionId, needFullResync: true };
|
|
3123
3296
|
}
|
|
3124
3297
|
return { success: false, newMessageCount: 0, sessionId, error: result.error };
|
|
@@ -3156,7 +3329,7 @@ var NoopNotifier = class {
|
|
|
3156
3329
|
|
|
3157
3330
|
// src/sync/routes.ts
|
|
3158
3331
|
function registerAutomationRoutes(server2, ctx) {
|
|
3159
|
-
const { dsManager: dsManager2, pullEngine: pullEngine2, serverInfo } = ctx;
|
|
3332
|
+
const { dsManager: dsManager2, pullEngine: pullEngine2, dbManager: dbManager2, serverInfo } = ctx;
|
|
3160
3333
|
server2.get("/_web/automation/config", async () => {
|
|
3161
3334
|
return {
|
|
3162
3335
|
enabled: true,
|
|
@@ -3191,12 +3364,22 @@ function registerAutomationRoutes(server2, ctx) {
|
|
|
3191
3364
|
return reply.code(404).send({ error: "Data source not found" });
|
|
3192
3365
|
}
|
|
3193
3366
|
reloadTimer(request.params.id, true);
|
|
3367
|
+
for (const sess of added) {
|
|
3368
|
+
pullEngine2.triggerPull(request.params.id, sess.id).catch(() => {
|
|
3369
|
+
});
|
|
3370
|
+
}
|
|
3194
3371
|
return added;
|
|
3195
3372
|
});
|
|
3196
3373
|
server2.delete("/_web/automation/data-sources/:id/sessions/:sessId", async (request, reply) => {
|
|
3197
|
-
const
|
|
3198
|
-
if (!
|
|
3374
|
+
const removed = dsManager2.removeSession(request.params.id, request.params.sessId);
|
|
3375
|
+
if (!removed) return reply.code(404).send({ error: "Session not found" });
|
|
3199
3376
|
reloadTimer(request.params.id);
|
|
3377
|
+
if (request.query.deleteData === "true" && removed.targetSessionId) {
|
|
3378
|
+
dbManager2.close(removed.targetSessionId);
|
|
3379
|
+
const dbPath = dbManager2.getDbPath(removed.targetSessionId);
|
|
3380
|
+
const fs15 = await import("fs");
|
|
3381
|
+
if (fs15.existsSync(dbPath)) fs15.unlinkSync(dbPath);
|
|
3382
|
+
}
|
|
3200
3383
|
return { success: true };
|
|
3201
3384
|
});
|
|
3202
3385
|
server2.post(
|
|
@@ -3208,6 +3391,9 @@ function registerAutomationRoutes(server2, ctx) {
|
|
|
3208
3391
|
server2.post("/_web/automation/data-sources/:id/pull-all", async (request) => {
|
|
3209
3392
|
return pullEngine2.triggerPullAll(request.params.id);
|
|
3210
3393
|
});
|
|
3394
|
+
server2.get("/_web/automation/sync-progress", async () => {
|
|
3395
|
+
return pullEngine2.getProgress();
|
|
3396
|
+
});
|
|
3211
3397
|
server2.get("/_web/automation/remote-sessions", async (request, reply) => {
|
|
3212
3398
|
const { baseUrl, token, keyword, limit, cursor } = request.query;
|
|
3213
3399
|
if (!baseUrl) return reply.code(400).send({ error: "baseUrl is required" });
|
|
@@ -3251,7 +3437,7 @@ function initSync(server2, dbManager2, pathProvider, serverInfo) {
|
|
|
3251
3437
|
dsManager,
|
|
3252
3438
|
logger: syncLogger
|
|
3253
3439
|
});
|
|
3254
|
-
registerAutomationRoutes(server2, { dsManager, pullEngine, serverInfo });
|
|
3440
|
+
registerAutomationRoutes(server2, { dsManager, pullEngine, dbManager: dbManager2, serverInfo });
|
|
3255
3441
|
initScheduler({
|
|
3256
3442
|
dsManager,
|
|
3257
3443
|
pullEngine,
|
|
@@ -3270,8 +3456,8 @@ var dbManager = null;
|
|
|
3270
3456
|
var convManager = null;
|
|
3271
3457
|
function resolveNativeBinding3() {
|
|
3272
3458
|
if (process.versions.electron) return void 0;
|
|
3273
|
-
const nativePath =
|
|
3274
|
-
if (
|
|
3459
|
+
const nativePath = path15.resolve(__dirname, "../../native/better_sqlite3.node");
|
|
3460
|
+
if (fs14.existsSync(nativePath)) return nativePath;
|
|
3275
3461
|
return void 0;
|
|
3276
3462
|
}
|
|
3277
3463
|
function ensureToken(config) {
|
|
@@ -3327,7 +3513,7 @@ async function startHttpServer(options) {
|
|
|
3327
3513
|
registerWebRoutes(server, dbManager, { pathProvider, nativeBinding });
|
|
3328
3514
|
registerPreferencesRoutes(server, pathProvider);
|
|
3329
3515
|
initSync(server, dbManager, pathProvider, { port, host, token });
|
|
3330
|
-
if (options?.webRoot &&
|
|
3516
|
+
if (options?.webRoot && fs14.existsSync(options.webRoot)) {
|
|
3331
3517
|
setWebMode(true);
|
|
3332
3518
|
const fastifyStatic = await import("@fastify/static");
|
|
3333
3519
|
await server.register(fastifyStatic.default, {
|
|
@@ -3372,4 +3558,4 @@ export {
|
|
|
3372
3558
|
startHttpServer,
|
|
3373
3559
|
stopHttpServer
|
|
3374
3560
|
};
|
|
3375
|
-
//# sourceMappingURL=chunk-
|
|
3561
|
+
//# sourceMappingURL=chunk-LI4X52E7.mjs.map
|