chatlab-cli 0.1.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-YAOFJ73U.mjs → chunk-3DIWXYMJ.mjs} +80 -129
- package/dist/chunk-3DIWXYMJ.mjs.map +1 -0
- package/dist/{chunk-JMRDNHIR.mjs → chunk-EIRLEPXP.mjs} +824 -280
- package/dist/chunk-EIRLEPXP.mjs.map +1 -0
- package/dist/{chunk-DL3BFEAH.mjs → chunk-EWCSGYVT.mjs} +2 -2
- package/dist/chunk-EWCSGYVT.mjs.map +1 -0
- package/dist/{chunk-PF4QFHC6.mjs → chunk-FBTHZU55.mjs} +849 -824
- package/dist/chunk-FBTHZU55.mjs.map +1 -0
- package/dist/{chunk-P4N5GEFF.mjs → chunk-NB4BIAYM.mjs} +9 -9
- package/dist/{chunk-OC73KSMV.mjs → chunk-XUHPCRVT.mjs} +2 -2
- package/dist/cli.mjs +2 -2
- package/dist/{http-PQEOFGJK.mjs → http-QVYJ5HER.mjs} +6 -6
- package/dist/{import-UR2IE2O7.mjs → import-AJKL6JPR.mjs} +6 -4
- package/dist/index.mjs +6 -6
- package/dist/{mcp-4XCDZ5PU.mjs → mcp-FRF3YAN5.mjs} +5 -6
- package/dist/mcp-FRF3YAN5.mjs.map +1 -0
- package/dist/{src-ZELOFE7P.mjs → src-KOIGBZJX.mjs} +14 -2
- package/dist/{stream-import-RP3XB6NM.mjs → stream-import-URGOCXUH.mjs} +5 -3
- package/dist-web/assets/{DebugToolsPanel.vue_vue_type_script_setup_true_lang-BuOhHrGp.js → DebugToolsPanel.vue_vue_type_script_setup_true_lang-DFN1Co1H.js} +19 -19
- package/dist-web/assets/{EChartWordcloud-DcC12j9S.js → EChartWordcloud-BYLFxPAX.js} +1 -1
- package/dist-web/assets/{MessageExportModal-CUS9nnwi.js → MessageExportModal-BAf68-wB.js} +1 -1
- package/dist-web/assets/electron-2m4Vdk2z.js +1 -0
- package/dist-web/assets/{fetch-DvfIZxuz.js → fetch-Ayp337Ng.js} +1 -1
- package/dist-web/assets/{fetch-CJ8Q4qM2.js → fetch-BWtpQwBZ.js} +1 -1
- package/dist-web/assets/fetch-o5NJ2uWE.js +3 -0
- package/dist-web/assets/{index-D3siJhmm.js → index-Bd5r2oFG.js} +4 -4
- package/dist-web/assets/index-BfZM-q0T.js +7 -0
- package/dist-web/assets/{index-CCE1zef2.js → index-CLYHD_f4.js} +2 -2
- package/dist-web/assets/{index-CUTe0Sc3.js → index-CaTRpNBK.js} +12 -12
- package/dist-web/assets/index-Ksa--o23.js +173 -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-C5yfcwjn.js} +1 -1
- package/dist-web/index.html +2 -2
- package/package.json +1 -2
- 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/electron-DvcSSc-D.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-n2XUUgti.js +0 -7
- /package/dist/{chunk-P4N5GEFF.mjs.map → chunk-NB4BIAYM.mjs.map} +0 -0
- /package/dist/{chunk-OC73KSMV.mjs.map → chunk-XUHPCRVT.mjs.map} +0 -0
- /package/dist/{http-PQEOFGJK.mjs.map → http-QVYJ5HER.mjs.map} +0 -0
- /package/dist/{import-UR2IE2O7.mjs.map → import-AJKL6JPR.mjs.map} +0 -0
- /package/dist/{src-ZELOFE7P.mjs.map → src-KOIGBZJX.mjs.map} +0 -0
- /package/dist/{stream-import-RP3XB6NM.mjs.map → stream-import-URGOCXUH.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-XUHPCRVT.mjs";
|
|
11
11
|
import {
|
|
12
12
|
analyzeIncrementalImport,
|
|
13
13
|
analyzeNewImport,
|
|
14
14
|
incrementalImport,
|
|
15
15
|
streamImport
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-EWCSGYVT.mjs";
|
|
17
17
|
import {
|
|
18
18
|
AGENT_TOOL_REGISTRY,
|
|
19
19
|
CoreDataProvider
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-3DIWXYMJ.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-EIRLEPXP.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,321 @@ 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/clear-index", async (request) => {
|
|
853
|
+
session_index_service_exports.clearIndex(adapter, request.params.id);
|
|
854
|
+
return { success: true };
|
|
855
|
+
});
|
|
856
|
+
server2.get("/_web/sessions/:id/search/fts", async (request, reply) => {
|
|
857
|
+
if (!session_index_service_exports.getFtsStatus(adapter, request.params.id)) {
|
|
858
|
+
return reply.code(400).send({ error: "FTS index not built for this session" });
|
|
859
|
+
}
|
|
860
|
+
const keywords = request.query.keywords.split(/\s+/).filter(Boolean);
|
|
861
|
+
if (keywords.length === 0) return { rowids: [], total: 0 };
|
|
862
|
+
const limit = parseInt(request.query.limit || "100", 10);
|
|
863
|
+
const offset = parseInt(request.query.offset || "0", 10);
|
|
864
|
+
return session_index_service_exports.searchFts(adapter, request.params.id, keywords, limit, offset);
|
|
865
|
+
});
|
|
866
|
+
server2.get("/_web/sessions/:id/fts/status", async (request) => {
|
|
867
|
+
return { hasFtsIndex: session_index_service_exports.getFtsStatus(adapter, request.params.id) };
|
|
868
|
+
});
|
|
869
|
+
server2.post("/_web/sessions/:id/fts/rebuild", async (request) => {
|
|
870
|
+
const result = session_index_service_exports.rebuildFts(adapter, request.params.id);
|
|
871
|
+
return { success: true, indexed: result.indexed };
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// src/ai/llm-config.ts
|
|
876
|
+
import * as fs3 from "fs";
|
|
877
|
+
import * as path3 from "path";
|
|
582
878
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
583
879
|
function loadLlmConfig(aiDataDir) {
|
|
584
|
-
const configPath =
|
|
585
|
-
if (!
|
|
880
|
+
const configPath = path3.join(aiDataDir, "llm-config.json");
|
|
881
|
+
if (!fs3.existsSync(configPath)) {
|
|
586
882
|
return { configs: [], defaultAssistant: null, fastModel: null };
|
|
587
883
|
}
|
|
588
884
|
try {
|
|
589
|
-
const data = JSON.parse(
|
|
885
|
+
const data = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
|
|
590
886
|
const configs = (data.configs || []).map((c) => {
|
|
591
887
|
const provider = c.provider || "";
|
|
592
888
|
const authProfile = c.authProfile;
|
|
@@ -623,7 +919,7 @@ function getDefaultAssistantConfig(aiDataDir) {
|
|
|
623
919
|
}
|
|
624
920
|
var MAX_CONFIG_COUNT = 99;
|
|
625
921
|
function saveLlmConfig(aiDataDir, store) {
|
|
626
|
-
const configPath =
|
|
922
|
+
const configPath = path3.join(aiDataDir, "llm-config.json");
|
|
627
923
|
const toSave = {
|
|
628
924
|
...store,
|
|
629
925
|
configs: store.configs.map((c) => {
|
|
@@ -632,7 +928,7 @@ function saveLlmConfig(aiDataDir, store) {
|
|
|
632
928
|
}),
|
|
633
929
|
schemaVersion: 3
|
|
634
930
|
};
|
|
635
|
-
|
|
931
|
+
fs3.writeFileSync(configPath, JSON.stringify(toSave, null, 2), "utf-8");
|
|
636
932
|
}
|
|
637
933
|
function addLlmConfig(aiDataDir, config) {
|
|
638
934
|
const store = loadLlmConfig(aiDataDir);
|
|
@@ -727,12 +1023,12 @@ function setFastModelSlot(aiDataDir, slot) {
|
|
|
727
1023
|
return { success: true };
|
|
728
1024
|
}
|
|
729
1025
|
function loadRawConfigStore(aiDataDir) {
|
|
730
|
-
const configPath =
|
|
731
|
-
if (!
|
|
1026
|
+
const configPath = path3.join(aiDataDir, "llm-config.json");
|
|
1027
|
+
if (!fs3.existsSync(configPath)) {
|
|
732
1028
|
return { configs: [], defaultAssistant: null, fastModel: null };
|
|
733
1029
|
}
|
|
734
1030
|
try {
|
|
735
|
-
const data = JSON.parse(
|
|
1031
|
+
const data = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
|
|
736
1032
|
return {
|
|
737
1033
|
configs: data.configs || [],
|
|
738
1034
|
defaultAssistant: data.defaultAssistant ?? null,
|
|
@@ -746,554 +1042,111 @@ function buildPiModel2(config) {
|
|
|
746
1042
|
return buildPiModel(config);
|
|
747
1043
|
}
|
|
748
1044
|
|
|
749
|
-
// src/http/routes/web.ts
|
|
750
|
-
function
|
|
751
|
-
|
|
752
|
-
const nativePath = path3.resolve(__dirname, "../../../native/better_sqlite3.node");
|
|
753
|
-
if (fs3.existsSync(nativePath)) return nativePath;
|
|
754
|
-
return void 0;
|
|
755
|
-
}
|
|
756
|
-
function ensureDb2(dbManager2, sessionId) {
|
|
757
|
-
const db = dbManager2.open(sessionId);
|
|
758
|
-
if (!db) {
|
|
759
|
-
throw Object.assign(new Error(`Session not found: ${sessionId}`), { statusCode: 404 });
|
|
760
|
-
}
|
|
761
|
-
return db;
|
|
762
|
-
}
|
|
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);
|
|
1045
|
+
// src/http/routes/web/summaries.ts
|
|
1046
|
+
function createSummaryDeps(dbManager2) {
|
|
1047
|
+
const aiDataDir = getAiDataDir(dbManager2);
|
|
789
1048
|
return {
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
if (!data) return null;
|
|
793
|
-
return data.messages.map((m) => ({ senderName: m.senderName, content: m.content }));
|
|
1049
|
+
getLlmConfig() {
|
|
1050
|
+
return getDefaultAssistantConfig(aiDataDir);
|
|
794
1051
|
},
|
|
795
|
-
|
|
796
|
-
|
|
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
|
|
1052
|
+
buildPiModel(config) {
|
|
1053
|
+
return buildPiModel2(config);
|
|
1054
|
+
}
|
|
813
1055
|
};
|
|
814
1056
|
}
|
|
815
|
-
function
|
|
816
|
-
const
|
|
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
|
-
});
|
|
1057
|
+
function registerSummaryRoutes(server2, dbManager2, adapter) {
|
|
1058
|
+
const deps = createSummaryDeps(dbManager2);
|
|
1139
1059
|
server2.post("/_web/sessions/:id/summaries/generate", async (request, reply) => {
|
|
1140
|
-
const sessionId = request.params.id;
|
|
1141
1060
|
const { chatSessionId, locale, forceRegenerate } = request.body;
|
|
1142
|
-
const
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1061
|
+
const result = await summary_service_exports.generateSummary(adapter, request.params.id, chatSessionId, deps, {
|
|
1062
|
+
locale,
|
|
1063
|
+
forceRegenerate
|
|
1064
|
+
});
|
|
1065
|
+
if ("error" in result && !result.success) {
|
|
1066
|
+
return reply.code(400).send({ error: result.error });
|
|
1146
1067
|
}
|
|
1147
|
-
const db = ensureWritableDb(dbManager2, sessionId);
|
|
1148
|
-
const deps = buildSummaryDeps(db, llmConfig, aiDataDir);
|
|
1149
|
-
const result = await generateSessionSummary(deps, chatSessionId, { locale, forceRegenerate });
|
|
1150
1068
|
return result;
|
|
1151
1069
|
});
|
|
1152
1070
|
server2.post("/_web/sessions/:id/summaries/generate-all", async (request, reply) => {
|
|
1153
|
-
const sessionId = request.params.id;
|
|
1154
1071
|
const { locale, forceRegenerate } = request.body;
|
|
1155
|
-
const
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
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++;
|
|
1072
|
+
const result = await summary_service_exports.generateAllSummaries(adapter, request.params.id, deps, {
|
|
1073
|
+
locale,
|
|
1074
|
+
forceRegenerate
|
|
1075
|
+
});
|
|
1076
|
+
if (result.error) {
|
|
1077
|
+
return reply.code(400).send({ error: result.error });
|
|
1169
1078
|
}
|
|
1170
|
-
return
|
|
1079
|
+
return result;
|
|
1171
1080
|
});
|
|
1172
|
-
|
|
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)}
|
|
1081
|
+
}
|
|
1184
1082
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1083
|
+
// src/http/routes/web/import.ts
|
|
1084
|
+
import * as fs4 from "fs";
|
|
1085
|
+
import * as os from "os";
|
|
1086
|
+
import * as path4 from "path";
|
|
1087
|
+
var DEMO_BASE_URL = "https://chatlab.fun/assets/demo";
|
|
1088
|
+
function cleanupTemp(...paths) {
|
|
1089
|
+
for (const p of paths) {
|
|
1190
1090
|
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 {
|
|
1091
|
+
const stat = fs4.statSync(p);
|
|
1092
|
+
if (stat.isDirectory()) {
|
|
1093
|
+
fs4.rmSync(p, { recursive: true, force: true });
|
|
1094
|
+
} else {
|
|
1095
|
+
fs4.unlinkSync(p);
|
|
1231
1096
|
}
|
|
1232
|
-
|
|
1097
|
+
} catch {
|
|
1233
1098
|
}
|
|
1234
|
-
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
function registerImportRoutes(server2, dbManager2) {
|
|
1235
1102
|
server2.get("/_web/supported-formats", async () => {
|
|
1236
1103
|
return getSupportedFormats();
|
|
1237
1104
|
});
|
|
1238
1105
|
server2.post("/_web/detect-format", async (request, reply) => {
|
|
1239
1106
|
const data = await request.file();
|
|
1240
1107
|
if (!data) return reply.code(400).send({ error: "No file uploaded" });
|
|
1241
|
-
const tmpDir =
|
|
1242
|
-
const tmpPath =
|
|
1108
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "chatlab-detect-"));
|
|
1109
|
+
const tmpPath = path4.join(tmpDir, data.filename || "upload");
|
|
1243
1110
|
try {
|
|
1244
1111
|
const chunks = [];
|
|
1245
1112
|
for await (const chunk of data.file) {
|
|
1246
1113
|
chunks.push(chunk);
|
|
1247
1114
|
}
|
|
1248
|
-
|
|
1115
|
+
fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
1249
1116
|
const format = detectFormat(tmpPath);
|
|
1250
1117
|
const allFormats = detectAllFormats(tmpPath);
|
|
1251
1118
|
return { format, allFormats };
|
|
1252
1119
|
} finally {
|
|
1253
|
-
|
|
1254
|
-
fs3.unlinkSync(tmpPath);
|
|
1255
|
-
} catch {
|
|
1256
|
-
}
|
|
1257
|
-
try {
|
|
1258
|
-
fs3.rmdirSync(tmpDir);
|
|
1259
|
-
} catch {
|
|
1260
|
-
}
|
|
1120
|
+
cleanupTemp(tmpPath, tmpDir);
|
|
1261
1121
|
}
|
|
1262
1122
|
});
|
|
1263
1123
|
server2.post("/_web/scan-multi-chat", async (request, reply) => {
|
|
1264
1124
|
const data = await request.file();
|
|
1265
1125
|
if (!data) return reply.code(400).send({ error: "No file uploaded" });
|
|
1266
|
-
const tmpDir =
|
|
1267
|
-
const tmpPath =
|
|
1126
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "chatlab-scan-"));
|
|
1127
|
+
const tmpPath = path4.join(tmpDir, data.filename || "upload");
|
|
1268
1128
|
try {
|
|
1269
1129
|
const chunks = [];
|
|
1270
1130
|
for await (const chunk of data.file) {
|
|
1271
1131
|
chunks.push(chunk);
|
|
1272
1132
|
}
|
|
1273
|
-
|
|
1133
|
+
fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
1274
1134
|
const chats = await scanMultiChatFile(tmpPath);
|
|
1275
1135
|
return { chats };
|
|
1276
1136
|
} finally {
|
|
1277
|
-
|
|
1278
|
-
fs3.unlinkSync(tmpPath);
|
|
1279
|
-
} catch {
|
|
1280
|
-
}
|
|
1281
|
-
try {
|
|
1282
|
-
fs3.rmdirSync(tmpDir);
|
|
1283
|
-
} catch {
|
|
1284
|
-
}
|
|
1137
|
+
cleanupTemp(tmpPath, tmpDir);
|
|
1285
1138
|
}
|
|
1286
1139
|
});
|
|
1287
1140
|
server2.post("/_web/import", async (request, reply) => {
|
|
1288
1141
|
const data = await request.file();
|
|
1289
1142
|
if (!data) return reply.code(400).send({ error: "No file uploaded" });
|
|
1290
|
-
const tmpDir =
|
|
1291
|
-
const tmpPath =
|
|
1143
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "chatlab-import-"));
|
|
1144
|
+
const tmpPath = path4.join(tmpDir, data.filename || "upload");
|
|
1292
1145
|
const chunks = [];
|
|
1293
1146
|
for await (const chunk of data.file) {
|
|
1294
1147
|
chunks.push(chunk);
|
|
1295
1148
|
}
|
|
1296
|
-
|
|
1149
|
+
fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
1297
1150
|
const formatId = data.fields?.formatId?.value;
|
|
1298
1151
|
const chatIndexStr = data.fields?.chatIndex?.value;
|
|
1299
1152
|
const chatIndex = chatIndexStr !== void 0 ? parseInt(chatIndexStr, 10) : void 0;
|
|
@@ -1302,9 +1155,9 @@ data: ${JSON.stringify(data)}
|
|
|
1302
1155
|
"Cache-Control": "no-cache",
|
|
1303
1156
|
Connection: "keep-alive"
|
|
1304
1157
|
});
|
|
1305
|
-
function sendEvent(event,
|
|
1158
|
+
function sendEvent(event, eventData) {
|
|
1306
1159
|
reply.raw.write(`event: ${event}
|
|
1307
|
-
data: ${JSON.stringify(
|
|
1160
|
+
data: ${JSON.stringify(eventData)}
|
|
1308
1161
|
|
|
1309
1162
|
`);
|
|
1310
1163
|
}
|
|
@@ -1314,9 +1167,7 @@ data: ${JSON.stringify(data2)}
|
|
|
1314
1167
|
formatId,
|
|
1315
1168
|
chatIndex,
|
|
1316
1169
|
nativeBinding,
|
|
1317
|
-
onProgress: (p) =>
|
|
1318
|
-
sendEvent("progress", p);
|
|
1319
|
-
}
|
|
1170
|
+
onProgress: (p) => sendEvent("progress", p)
|
|
1320
1171
|
});
|
|
1321
1172
|
if (result.success) {
|
|
1322
1173
|
sendEvent("done", {
|
|
@@ -1332,27 +1183,95 @@ data: ${JSON.stringify(data2)}
|
|
|
1332
1183
|
sendEvent("error", { success: false, error: err instanceof Error ? err.message : String(err) });
|
|
1333
1184
|
} finally {
|
|
1334
1185
|
reply.raw.end();
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1186
|
+
cleanupTemp(tmpPath, tmpDir);
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
server2.post("/_web/import-directory", async (request, reply) => {
|
|
1190
|
+
const parts = request.parts();
|
|
1191
|
+
if (!parts) return reply.code(400).send({ error: "No files uploaded" });
|
|
1192
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "chatlab-dir-import-"));
|
|
1193
|
+
const relativePaths = [];
|
|
1194
|
+
const fileBuffers = [];
|
|
1195
|
+
try {
|
|
1196
|
+
let sendEvent2 = function(event, eventData) {
|
|
1197
|
+
reply.raw.write(`event: ${event}
|
|
1198
|
+
data: ${JSON.stringify(eventData)}
|
|
1199
|
+
|
|
1200
|
+
`);
|
|
1201
|
+
};
|
|
1202
|
+
var sendEvent = sendEvent2;
|
|
1203
|
+
for await (const part of parts) {
|
|
1204
|
+
if (part.type === "field" && part.fieldname === "relativePaths") {
|
|
1205
|
+
relativePaths.push(String(part.value));
|
|
1206
|
+
} else if (part.type === "file") {
|
|
1207
|
+
const chunks = [];
|
|
1208
|
+
for await (const chunk of part.file) {
|
|
1209
|
+
chunks.push(chunk);
|
|
1210
|
+
}
|
|
1211
|
+
fileBuffers.push({ data: Buffer.concat(chunks), filename: part.filename || "" });
|
|
1212
|
+
}
|
|
1338
1213
|
}
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1214
|
+
for (let i = 0; i < fileBuffers.length; i++) {
|
|
1215
|
+
let relPath = relativePaths[i] || fileBuffers[i].filename || `file_${i}`;
|
|
1216
|
+
const segments = relPath.split("/");
|
|
1217
|
+
if (segments.length > 1) {
|
|
1218
|
+
relPath = segments.slice(1).join("/");
|
|
1219
|
+
}
|
|
1220
|
+
const targetPath = path4.resolve(tmpDir, relPath);
|
|
1221
|
+
if (!targetPath.startsWith(tmpDir + path4.sep)) continue;
|
|
1222
|
+
fs4.mkdirSync(path4.dirname(targetPath), { recursive: true });
|
|
1223
|
+
fs4.writeFileSync(targetPath, fileBuffers[i].data);
|
|
1224
|
+
}
|
|
1225
|
+
const entryPath = findEntryFileInDirectory(tmpDir);
|
|
1226
|
+
if (!entryPath) {
|
|
1227
|
+
return reply.code(400).send({ error: "No recognizable import format found in directory" });
|
|
1228
|
+
}
|
|
1229
|
+
reply.raw.writeHead(200, {
|
|
1230
|
+
"Content-Type": "text/event-stream",
|
|
1231
|
+
"Cache-Control": "no-cache",
|
|
1232
|
+
Connection: "keep-alive"
|
|
1233
|
+
});
|
|
1234
|
+
const nativeBinding = resolveNativeBinding();
|
|
1235
|
+
const result = await streamImport(dbManager2, entryPath, {
|
|
1236
|
+
nativeBinding,
|
|
1237
|
+
onProgress: (p) => sendEvent2("progress", p)
|
|
1238
|
+
});
|
|
1239
|
+
if (result.success) {
|
|
1240
|
+
sendEvent2("done", {
|
|
1241
|
+
success: true,
|
|
1242
|
+
sessionId: result.sessionId,
|
|
1243
|
+
messageCount: result.diagnostics?.messagesWritten ?? 0,
|
|
1244
|
+
memberCount: 0
|
|
1245
|
+
});
|
|
1246
|
+
} else {
|
|
1247
|
+
sendEvent2("error", { success: false, error: result.error });
|
|
1342
1248
|
}
|
|
1249
|
+
} catch (err) {
|
|
1250
|
+
if (!reply.raw.headersSent) {
|
|
1251
|
+
return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
|
|
1252
|
+
}
|
|
1253
|
+
reply.raw.write(
|
|
1254
|
+
`event: error
|
|
1255
|
+
data: ${JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) })}
|
|
1256
|
+
|
|
1257
|
+
`
|
|
1258
|
+
);
|
|
1259
|
+
} finally {
|
|
1260
|
+
reply.raw.end();
|
|
1261
|
+
cleanupTemp(tmpDir);
|
|
1343
1262
|
}
|
|
1344
1263
|
});
|
|
1345
1264
|
server2.post("/_web/sessions/:id/import/incremental", async (request, reply) => {
|
|
1346
1265
|
const sessionId = request.params.id;
|
|
1347
1266
|
const data = await request.file();
|
|
1348
1267
|
if (!data) return reply.code(400).send({ error: "No file uploaded" });
|
|
1349
|
-
const tmpDir =
|
|
1350
|
-
const tmpPath =
|
|
1268
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "chatlab-inc-"));
|
|
1269
|
+
const tmpPath = path4.join(tmpDir, data.filename || "upload");
|
|
1351
1270
|
const chunks = [];
|
|
1352
1271
|
for await (const chunk of data.file) {
|
|
1353
1272
|
chunks.push(chunk);
|
|
1354
1273
|
}
|
|
1355
|
-
|
|
1274
|
+
fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
1356
1275
|
reply.raw.writeHead(200, {
|
|
1357
1276
|
"Content-Type": "text/event-stream",
|
|
1358
1277
|
"Cache-Control": "no-cache",
|
|
@@ -1366,9 +1285,7 @@ data: ${JSON.stringify(eventData)}
|
|
|
1366
1285
|
}
|
|
1367
1286
|
try {
|
|
1368
1287
|
const result = await incrementalImport(dbManager2, sessionId, tmpPath, {
|
|
1369
|
-
onProgress: (p) =>
|
|
1370
|
-
sendEvent("progress", p);
|
|
1371
|
-
}
|
|
1288
|
+
onProgress: (p) => sendEvent("progress", p)
|
|
1372
1289
|
});
|
|
1373
1290
|
if (result.success) {
|
|
1374
1291
|
sendEvent("done", result);
|
|
@@ -1379,237 +1296,299 @@ data: ${JSON.stringify(eventData)}
|
|
|
1379
1296
|
sendEvent("error", { success: false, error: err instanceof Error ? err.message : String(err) });
|
|
1380
1297
|
} finally {
|
|
1381
1298
|
reply.raw.end();
|
|
1382
|
-
|
|
1383
|
-
fs3.unlinkSync(tmpPath);
|
|
1384
|
-
} catch {
|
|
1385
|
-
}
|
|
1386
|
-
try {
|
|
1387
|
-
fs3.rmdirSync(tmpDir);
|
|
1388
|
-
} catch {
|
|
1389
|
-
}
|
|
1299
|
+
cleanupTemp(tmpPath, tmpDir);
|
|
1390
1300
|
}
|
|
1391
1301
|
});
|
|
1392
1302
|
server2.post("/_web/sessions/:id/import/incremental/analyze", async (request, reply) => {
|
|
1393
1303
|
const sessionId = request.params.id;
|
|
1394
1304
|
const data = await request.file();
|
|
1395
1305
|
if (!data) return reply.code(400).send({ error: "No file uploaded" });
|
|
1396
|
-
const tmpDir =
|
|
1397
|
-
const tmpPath =
|
|
1306
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "chatlab-analyze-"));
|
|
1307
|
+
const tmpPath = path4.join(tmpDir, data.filename || "upload");
|
|
1398
1308
|
const chunks = [];
|
|
1399
1309
|
for await (const chunk of data.file) {
|
|
1400
1310
|
chunks.push(chunk);
|
|
1401
1311
|
}
|
|
1402
|
-
|
|
1312
|
+
fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
1403
1313
|
try {
|
|
1404
|
-
|
|
1405
|
-
return result;
|
|
1314
|
+
return await analyzeIncrementalImport(dbManager2, sessionId, tmpPath);
|
|
1406
1315
|
} catch (err) {
|
|
1407
1316
|
return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
|
|
1408
1317
|
} finally {
|
|
1409
|
-
|
|
1410
|
-
fs3.unlinkSync(tmpPath);
|
|
1411
|
-
} catch {
|
|
1412
|
-
}
|
|
1413
|
-
try {
|
|
1414
|
-
fs3.rmdirSync(tmpDir);
|
|
1415
|
-
} catch {
|
|
1416
|
-
}
|
|
1318
|
+
cleanupTemp(tmpPath, tmpDir);
|
|
1417
1319
|
}
|
|
1418
1320
|
});
|
|
1419
1321
|
server2.post("/_web/import/analyze", async (request, reply) => {
|
|
1420
1322
|
const data = await request.file();
|
|
1421
1323
|
if (!data) return reply.code(400).send({ error: "No file uploaded" });
|
|
1422
|
-
const tmpDir =
|
|
1423
|
-
const tmpPath =
|
|
1324
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "chatlab-analyze-"));
|
|
1325
|
+
const tmpPath = path4.join(tmpDir, data.filename || "upload");
|
|
1424
1326
|
const chunks = [];
|
|
1425
1327
|
for await (const chunk of data.file) {
|
|
1426
1328
|
chunks.push(chunk);
|
|
1427
1329
|
}
|
|
1428
|
-
|
|
1330
|
+
fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
1429
1331
|
try {
|
|
1430
|
-
|
|
1431
|
-
return result;
|
|
1332
|
+
return await analyzeNewImport(tmpPath);
|
|
1432
1333
|
} catch (err) {
|
|
1433
1334
|
return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
|
|
1335
|
+
} finally {
|
|
1336
|
+
cleanupTemp(tmpPath, tmpDir);
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
server2.post("/_web/demo/import", async (request, reply) => {
|
|
1340
|
+
const locale = request.body?.locale || "en";
|
|
1341
|
+
const nativeBinding = resolveNativeBinding();
|
|
1342
|
+
reply.raw.writeHead(200, {
|
|
1343
|
+
"Content-Type": "text/event-stream",
|
|
1344
|
+
"Cache-Control": "no-cache",
|
|
1345
|
+
Connection: "keep-alive"
|
|
1346
|
+
});
|
|
1347
|
+
function sendEvent(event, eventData) {
|
|
1348
|
+
reply.raw.write(`event: ${event}
|
|
1349
|
+
data: ${JSON.stringify(eventData)}
|
|
1350
|
+
|
|
1351
|
+
`);
|
|
1352
|
+
}
|
|
1353
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "chatlab-demo-"));
|
|
1354
|
+
const groupPath = path4.join(tmpDir, "demo-group.json");
|
|
1355
|
+
const privatePath = path4.join(tmpDir, "demo-private.json");
|
|
1356
|
+
try {
|
|
1357
|
+
sendEvent("progress", { stage: "downloading", current: 1, total: 2 });
|
|
1358
|
+
const groupResp = await fetch(`${DEMO_BASE_URL}/${locale}/demo-group.json`, {
|
|
1359
|
+
signal: AbortSignal.timeout(6e4)
|
|
1360
|
+
});
|
|
1361
|
+
if (!groupResp.ok) throw new Error(`Download group demo failed: ${groupResp.status}`);
|
|
1362
|
+
fs4.writeFileSync(groupPath, Buffer.from(await groupResp.arrayBuffer()));
|
|
1363
|
+
sendEvent("progress", { stage: "downloading", current: 2, total: 2 });
|
|
1364
|
+
const privateResp = await fetch(`${DEMO_BASE_URL}/${locale}/demo-private.json`, {
|
|
1365
|
+
signal: AbortSignal.timeout(6e4)
|
|
1366
|
+
});
|
|
1367
|
+
if (!privateResp.ok) throw new Error(`Download private demo failed: ${privateResp.status}`);
|
|
1368
|
+
fs4.writeFileSync(privatePath, Buffer.from(await privateResp.arrayBuffer()));
|
|
1369
|
+
sendEvent("progress", { stage: "importing", current: 1, total: 2 });
|
|
1370
|
+
const groupResult = await streamImport(dbManager2, groupPath, { nativeBinding });
|
|
1371
|
+
if (!groupResult.success) throw new Error(groupResult.error || "Failed to import group demo");
|
|
1372
|
+
sendEvent("progress", { stage: "importing", current: 2, total: 2 });
|
|
1373
|
+
const privateResult = await streamImport(dbManager2, privatePath, { nativeBinding });
|
|
1374
|
+
if (!privateResult.success) throw new Error(privateResult.error || "Failed to import private demo");
|
|
1375
|
+
sendEvent("progress", { stage: "done", current: 2, total: 2 });
|
|
1376
|
+
sendEvent("result", {
|
|
1377
|
+
success: true,
|
|
1378
|
+
groupSessionId: groupResult.sessionId,
|
|
1379
|
+
privateSessionId: privateResult.sessionId
|
|
1380
|
+
});
|
|
1381
|
+
} catch (err) {
|
|
1382
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1383
|
+
sendEvent("progress", { stage: "error", current: 0, total: 2, message });
|
|
1384
|
+
sendEvent("result", { success: false, error: message });
|
|
1385
|
+
} finally {
|
|
1386
|
+
reply.raw.end();
|
|
1387
|
+
cleanupTemp(tmpDir);
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// src/http/routes/web/merge.ts
|
|
1393
|
+
import * as fs5 from "fs";
|
|
1394
|
+
import * as os2 from "os";
|
|
1395
|
+
import * as path5 from "path";
|
|
1396
|
+
function registerMergeRoutes(server2, dbManager2, mergeCache) {
|
|
1397
|
+
server2.post("/_web/merge/parse", async (request, reply) => {
|
|
1398
|
+
const data = await request.file();
|
|
1399
|
+
if (!data) return reply.code(400).send({ error: "No file uploaded" });
|
|
1400
|
+
const tmpDir = fs5.mkdtempSync(path5.join(os2.tmpdir(), "chatlab-merge-"));
|
|
1401
|
+
const tmpPath = path5.join(tmpDir, data.filename || "upload");
|
|
1402
|
+
const chunks = [];
|
|
1403
|
+
for await (const chunk of data.file) {
|
|
1404
|
+
chunks.push(chunk);
|
|
1405
|
+
}
|
|
1406
|
+
fs5.writeFileSync(tmpPath, Buffer.concat(chunks));
|
|
1407
|
+
try {
|
|
1408
|
+
const result = await streamParseFileInfo(tmpPath, {
|
|
1409
|
+
createTempDatabase(sourceFilePath) {
|
|
1410
|
+
return mergeCache.createTempDatabase(path5.basename(sourceFilePath));
|
|
1411
|
+
},
|
|
1412
|
+
onProgress() {
|
|
1413
|
+
}
|
|
1414
|
+
});
|
|
1415
|
+
const handle = mergeCache.store(data.filename || "upload", result.tempDbPath);
|
|
1416
|
+
return {
|
|
1417
|
+
handle,
|
|
1418
|
+
name: result.name,
|
|
1419
|
+
format: result.format,
|
|
1420
|
+
platform: result.platform,
|
|
1421
|
+
messageCount: result.messageCount,
|
|
1422
|
+
memberCount: result.memberCount,
|
|
1423
|
+
fileSize: result.fileSize
|
|
1424
|
+
};
|
|
1434
1425
|
} finally {
|
|
1435
1426
|
try {
|
|
1436
|
-
|
|
1427
|
+
fs5.unlinkSync(tmpPath);
|
|
1437
1428
|
} catch {
|
|
1438
1429
|
}
|
|
1439
1430
|
try {
|
|
1440
|
-
|
|
1431
|
+
fs5.rmdirSync(tmpDir);
|
|
1441
1432
|
} catch {
|
|
1442
1433
|
}
|
|
1443
1434
|
}
|
|
1444
1435
|
});
|
|
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" });
|
|
1436
|
+
server2.post("/_web/merge/conflicts", async (request, reply) => {
|
|
1437
|
+
const { handles } = request.body;
|
|
1438
|
+
if (!handles || !Array.isArray(handles) || handles.length === 0) {
|
|
1439
|
+
return reply.code(400).send({ error: "Missing or empty handles array" });
|
|
1440
|
+
}
|
|
1441
|
+
const readers = [];
|
|
1442
|
+
try {
|
|
1443
|
+
const dataSources = [];
|
|
1444
|
+
for (const handle of handles) {
|
|
1445
|
+
const entry = mergeCache.openReader(handle);
|
|
1446
|
+
if (!entry) return reply.code(404).send({ error: `Handle not found: ${handle}` });
|
|
1447
|
+
readers.push(entry.reader);
|
|
1448
|
+
dataSources.push({ source: entry.reader.toDataSource(), filename: entry.filename });
|
|
1508
1449
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1450
|
+
return checkConflictsFromSources(dataSources);
|
|
1451
|
+
} finally {
|
|
1452
|
+
for (const r of readers) r.close();
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1455
|
+
server2.post("/_web/merge/execute", async (request, reply) => {
|
|
1456
|
+
const { handles, outputName, format = "json", andImport } = request.body;
|
|
1457
|
+
if (!handles || !Array.isArray(handles) || handles.length === 0) {
|
|
1458
|
+
return reply.code(400).send({ error: "Missing handles" });
|
|
1459
|
+
}
|
|
1460
|
+
if (!outputName) {
|
|
1461
|
+
return reply.code(400).send({ error: "Missing outputName" });
|
|
1462
|
+
}
|
|
1463
|
+
const readers = [];
|
|
1464
|
+
try {
|
|
1465
|
+
const dataSources = [];
|
|
1466
|
+
for (const handle of handles) {
|
|
1467
|
+
const entry = mergeCache.openReader(handle);
|
|
1468
|
+
if (!entry) return reply.code(404).send({ error: `Handle not found: ${handle}` });
|
|
1469
|
+
readers.push(entry.reader);
|
|
1470
|
+
dataSources.push({ source: entry.reader.toDataSource(), filename: entry.filename });
|
|
1511
1471
|
}
|
|
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");
|
|
1472
|
+
const merged = buildMergedOutput(dataSources, outputName);
|
|
1473
|
+
let sessionId;
|
|
1474
|
+
if (andImport) {
|
|
1475
|
+
const { streamImport: serverStreamImport } = await import("./stream-import-URGOCXUH.mjs");
|
|
1476
|
+
const jsonData = JSON.stringify(merged.chatLabData);
|
|
1477
|
+
const tmpDir = fs5.mkdtempSync(path5.join(os2.tmpdir(), "chatlab-merged-"));
|
|
1478
|
+
const tmpPath = path5.join(tmpDir, `${outputName}.json`);
|
|
1479
|
+
fs5.writeFileSync(tmpPath, jsonData, "utf-8");
|
|
1480
|
+
try {
|
|
1481
|
+
const importResult = await serverStreamImport(dbManager2, tmpPath);
|
|
1482
|
+
sessionId = importResult.sessionId;
|
|
1483
|
+
} finally {
|
|
1529
1484
|
try {
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
} finally {
|
|
1533
|
-
try {
|
|
1534
|
-
fs3.unlinkSync(tmpPath);
|
|
1535
|
-
} catch {
|
|
1536
|
-
}
|
|
1537
|
-
try {
|
|
1538
|
-
fs3.rmdirSync(tmpDir);
|
|
1539
|
-
} catch {
|
|
1540
|
-
}
|
|
1485
|
+
fs5.unlinkSync(tmpPath);
|
|
1486
|
+
} catch {
|
|
1541
1487
|
}
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
}
|
|
1546
|
-
if (format === "jsonl") {
|
|
1547
|
-
const lines = [];
|
|
1548
|
-
for (const line of serializeChatLabToJsonl(merged.chatLabData)) {
|
|
1549
|
-
lines.push(line);
|
|
1488
|
+
try {
|
|
1489
|
+
fs5.rmdirSync(tmpDir);
|
|
1490
|
+
} catch {
|
|
1550
1491
|
}
|
|
1551
|
-
return { success: true, sessionId, data: lines.join("\n") };
|
|
1552
1492
|
}
|
|
1553
|
-
return { success: true, sessionId, data: merged.chatLabData };
|
|
1554
|
-
} finally {
|
|
1555
|
-
for (const r of readers) r.close();
|
|
1556
1493
|
}
|
|
1557
|
-
|
|
1558
|
-
server2.post("/_web/merge/clear", async (request) => {
|
|
1559
|
-
const { handle } = request.body || {};
|
|
1560
|
-
if (handle) {
|
|
1494
|
+
for (const handle of handles) {
|
|
1561
1495
|
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
1496
|
}
|
|
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 });
|
|
1497
|
+
if (format === "jsonl") {
|
|
1498
|
+
const lines = [];
|
|
1499
|
+
for (const line of serializeChatLabToJsonl(merged.chatLabData)) {
|
|
1500
|
+
lines.push(line);
|
|
1501
|
+
}
|
|
1502
|
+
return { success: true, sessionId, data: lines.join("\n") };
|
|
1608
1503
|
}
|
|
1609
|
-
return { success: true,
|
|
1504
|
+
return { success: true, sessionId, data: merged.chatLabData };
|
|
1505
|
+
} finally {
|
|
1506
|
+
for (const r of readers) r.close();
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
server2.post("/_web/merge/clear", async (request) => {
|
|
1510
|
+
const { handle } = request.body || {};
|
|
1511
|
+
if (handle) {
|
|
1512
|
+
mergeCache.delete(handle);
|
|
1513
|
+
} else {
|
|
1514
|
+
mergeCache.clear();
|
|
1515
|
+
}
|
|
1516
|
+
return { success: true };
|
|
1517
|
+
});
|
|
1518
|
+
server2.post("/_web/sessions/export-for-merge", async (request, reply) => {
|
|
1519
|
+
const { sessionIds } = request.body;
|
|
1520
|
+
if (!sessionIds || !Array.isArray(sessionIds) || sessionIds.length === 0) {
|
|
1521
|
+
return reply.code(400).send({ error: "Missing sessionIds" });
|
|
1522
|
+
}
|
|
1523
|
+
const handles = [];
|
|
1524
|
+
for (const sid of sessionIds) {
|
|
1525
|
+
const db = ensureDb2(dbManager2, sid);
|
|
1526
|
+
const exported = exportSessionToJson(db);
|
|
1527
|
+
const { db: tempDb, tempDbPath } = mergeCache.createTempDatabase(exported.meta.name);
|
|
1528
|
+
const { TempDbWriter } = await import("./src-KOIGBZJX.mjs");
|
|
1529
|
+
const writer = new TempDbWriter(tempDb);
|
|
1530
|
+
writer.writeMeta({
|
|
1531
|
+
name: exported.meta.name,
|
|
1532
|
+
platform: exported.meta.platform,
|
|
1533
|
+
type: exported.meta.type,
|
|
1534
|
+
groupId: exported.meta.groupId,
|
|
1535
|
+
groupAvatar: exported.meta.groupAvatar
|
|
1536
|
+
});
|
|
1537
|
+
writer.writeMembers(
|
|
1538
|
+
exported.members.map((m) => ({
|
|
1539
|
+
platformId: m.platformId,
|
|
1540
|
+
accountName: m.accountName,
|
|
1541
|
+
groupNickname: m.groupNickname,
|
|
1542
|
+
avatar: m.avatar
|
|
1543
|
+
}))
|
|
1544
|
+
);
|
|
1545
|
+
writer.writeMessages(
|
|
1546
|
+
exported.messages.map((msg) => ({
|
|
1547
|
+
senderPlatformId: msg.sender,
|
|
1548
|
+
senderAccountName: msg.accountName,
|
|
1549
|
+
senderGroupNickname: msg.groupNickname,
|
|
1550
|
+
timestamp: msg.timestamp,
|
|
1551
|
+
type: msg.type,
|
|
1552
|
+
content: msg.content
|
|
1553
|
+
}))
|
|
1554
|
+
);
|
|
1555
|
+
writer.finish();
|
|
1556
|
+
const handle = mergeCache.store(exported.meta.name, tempDbPath);
|
|
1557
|
+
handles.push({ sessionId: sid, handle });
|
|
1558
|
+
}
|
|
1559
|
+
return { success: true, handles };
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// src/http/routes/web/export.ts
|
|
1564
|
+
function registerExportRoutes(server2, adapter) {
|
|
1565
|
+
server2.post("/_web/sessions/:id/export/markdown", async (request, reply) => {
|
|
1566
|
+
const { id } = request.params;
|
|
1567
|
+
const body = request.body;
|
|
1568
|
+
const sessionName = body?.sessionName || id;
|
|
1569
|
+
const { result, content } = export_service_exports.exportMarkdown(adapter, {
|
|
1570
|
+
sessionId: id,
|
|
1571
|
+
sessionName,
|
|
1572
|
+
filterMode: body.filterMode || "condition",
|
|
1573
|
+
keywords: body.keywords,
|
|
1574
|
+
timeFilter: body.timeFilter,
|
|
1575
|
+
senderIds: body.senderIds,
|
|
1576
|
+
contextSize: body.contextSize,
|
|
1577
|
+
chatSessionIds: body.chatSessionIds
|
|
1610
1578
|
});
|
|
1611
|
-
|
|
1612
|
-
|
|
1579
|
+
if (!result.success) {
|
|
1580
|
+
return reply.code(500).send({ error: result.error });
|
|
1581
|
+
}
|
|
1582
|
+
reply.header("Content-Type", "text/markdown; charset=utf-8");
|
|
1583
|
+
reply.header("Content-Disposition", `attachment; filename="${encodeURIComponent(sessionName)}_export.md"`);
|
|
1584
|
+
return reply.send(content);
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// src/http/routes/web/cache.ts
|
|
1589
|
+
import * as fs6 from "fs";
|
|
1590
|
+
import * as path6 from "path";
|
|
1591
|
+
function registerCacheRoutes(server2, downloadsDir) {
|
|
1613
1592
|
server2.post("/_web/cache/save-to-downloads", async (request) => {
|
|
1614
1593
|
const { filename, dataUrl } = request.body;
|
|
1615
1594
|
if (!filename || !dataUrl) {
|
|
@@ -1618,11 +1597,11 @@ data: ${JSON.stringify(eventData)}
|
|
|
1618
1597
|
const base64Prefix = dataUrl.indexOf(",");
|
|
1619
1598
|
const base64Data = base64Prefix >= 0 ? dataUrl.slice(base64Prefix + 1) : dataUrl;
|
|
1620
1599
|
const buffer = Buffer.from(base64Data, "base64");
|
|
1621
|
-
if (!
|
|
1622
|
-
|
|
1600
|
+
if (!fs6.existsSync(downloadsDir)) {
|
|
1601
|
+
fs6.mkdirSync(downloadsDir, { recursive: true });
|
|
1623
1602
|
}
|
|
1624
|
-
const filePath =
|
|
1625
|
-
|
|
1603
|
+
const filePath = path6.join(downloadsDir, filename);
|
|
1604
|
+
fs6.writeFileSync(filePath, buffer);
|
|
1626
1605
|
return { success: true, filePath };
|
|
1627
1606
|
});
|
|
1628
1607
|
server2.post("/_web/cache/show-in-folder", async (request) => {
|
|
@@ -1632,7 +1611,7 @@ data: ${JSON.stringify(eventData)}
|
|
|
1632
1611
|
}
|
|
1633
1612
|
const { exec } = await import("child_process");
|
|
1634
1613
|
const platform = process.platform;
|
|
1635
|
-
const dir =
|
|
1614
|
+
const dir = path6.dirname(filePath);
|
|
1636
1615
|
if (platform === "darwin") {
|
|
1637
1616
|
exec(`open -R "${filePath}"`);
|
|
1638
1617
|
} else if (platform === "win32") {
|
|
@@ -1644,8 +1623,28 @@ data: ${JSON.stringify(eventData)}
|
|
|
1644
1623
|
});
|
|
1645
1624
|
}
|
|
1646
1625
|
|
|
1626
|
+
// src/http/routes/web/index.ts
|
|
1627
|
+
function registerWebRoutes(server2, dbManager2, options) {
|
|
1628
|
+
const adapter = createDatabaseManagerAdapter(dbManager2);
|
|
1629
|
+
const mergeCache = options?.pathProvider ? new MergeSessionCache(options.pathProvider, { nativeBinding: options.nativeBinding }) : null;
|
|
1630
|
+
mergeCache?.cleanupOrphans();
|
|
1631
|
+
const downloadsDir = options?.pathProvider?.getDownloadsDir() || path7.join(os3.homedir(), "Downloads");
|
|
1632
|
+
registerSessionRoutes2(server2, adapter);
|
|
1633
|
+
registerMemberRoutes(server2, adapter);
|
|
1634
|
+
registerAnalyticsRoutes(server2, dbManager2, adapter);
|
|
1635
|
+
registerSqlRoutes(server2, adapter);
|
|
1636
|
+
registerSessionIndexRoutes(server2, adapter);
|
|
1637
|
+
registerSummaryRoutes(server2, dbManager2, adapter);
|
|
1638
|
+
registerImportRoutes(server2, dbManager2);
|
|
1639
|
+
if (mergeCache) {
|
|
1640
|
+
registerMergeRoutes(server2, dbManager2, mergeCache);
|
|
1641
|
+
}
|
|
1642
|
+
registerExportRoutes(server2, adapter);
|
|
1643
|
+
registerCacheRoutes(server2, downloadsDir);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1647
1646
|
// src/http/routes/nlp.ts
|
|
1648
|
-
import * as
|
|
1647
|
+
import * as path8 from "path";
|
|
1649
1648
|
function ensureDb3(dbManager2, sessionId) {
|
|
1650
1649
|
const db = dbManager2.open(sessionId);
|
|
1651
1650
|
if (!db) {
|
|
@@ -1656,7 +1655,7 @@ function ensureDb3(dbManager2, sessionId) {
|
|
|
1656
1655
|
function registerNlpRoutes(server2, dbManager2) {
|
|
1657
1656
|
const pathProvider = dbManager2["pathProvider"];
|
|
1658
1657
|
if (pathProvider) {
|
|
1659
|
-
const nlpDir =
|
|
1658
|
+
const nlpDir = path8.join(pathProvider.getUserDataDir(), "nlp");
|
|
1660
1659
|
initNlpDir(nlpDir);
|
|
1661
1660
|
ensureDefaultDict(nlpDir).catch((err) => console.warn("[NLP] Auto-download zh-CN dict failed:", err));
|
|
1662
1661
|
}
|
|
@@ -1694,12 +1693,12 @@ function registerNlpRoutes(server2, dbManager2) {
|
|
|
1694
1693
|
}
|
|
1695
1694
|
function resolveNlpDir(dbManager2) {
|
|
1696
1695
|
const pathProvider = dbManager2["pathProvider"];
|
|
1697
|
-
return
|
|
1696
|
+
return path8.join(pathProvider.getUserDataDir(), "nlp");
|
|
1698
1697
|
}
|
|
1699
1698
|
|
|
1700
1699
|
// src/http/routes/ai.ts
|
|
1701
|
-
import * as
|
|
1702
|
-
import * as
|
|
1700
|
+
import * as fs10 from "fs";
|
|
1701
|
+
import * as path12 from "path";
|
|
1703
1702
|
|
|
1704
1703
|
// src/ai/logger.ts
|
|
1705
1704
|
var logger = null;
|
|
@@ -1722,7 +1721,6 @@ function closeServerAiLogger() {
|
|
|
1722
1721
|
// src/ai/tool-adapter.ts
|
|
1723
1722
|
var DEFAULT_MAX_TOOL_RESULT_TOKENS = 8e3;
|
|
1724
1723
|
var TOOL_TRUNCATION_STRATEGY = {
|
|
1725
|
-
search_keyword: "keep_first",
|
|
1726
1724
|
search_messages: "keep_first",
|
|
1727
1725
|
deep_search_messages: "keep_first",
|
|
1728
1726
|
get_recent_messages: "keep_last",
|
|
@@ -1780,13 +1778,13 @@ function adaptToolsForAgent(tools, getContext, options) {
|
|
|
1780
1778
|
}
|
|
1781
1779
|
|
|
1782
1780
|
// src/ai/assistant-loader.ts
|
|
1783
|
-
import * as
|
|
1784
|
-
import * as
|
|
1781
|
+
import * as fs7 from "fs";
|
|
1782
|
+
import * as path9 from "path";
|
|
1785
1783
|
function loadAssistantConfig(aiDataDir, assistantId) {
|
|
1786
|
-
const filePath =
|
|
1787
|
-
if (!
|
|
1784
|
+
const filePath = path9.join(aiDataDir, "assistants", `${assistantId}.md`);
|
|
1785
|
+
if (!fs7.existsSync(filePath)) return null;
|
|
1788
1786
|
try {
|
|
1789
|
-
const content =
|
|
1787
|
+
const content = fs7.readFileSync(filePath, "utf-8");
|
|
1790
1788
|
return parseAssistantFile(content, filePath);
|
|
1791
1789
|
} catch {
|
|
1792
1790
|
return null;
|
|
@@ -1794,31 +1792,31 @@ function loadAssistantConfig(aiDataDir, assistantId) {
|
|
|
1794
1792
|
}
|
|
1795
1793
|
|
|
1796
1794
|
// src/ai/manager-factory.ts
|
|
1797
|
-
import * as
|
|
1798
|
-
import * as
|
|
1795
|
+
import * as fs8 from "fs";
|
|
1796
|
+
import * as path10 from "path";
|
|
1799
1797
|
import { createHash, randomUUID as randomUUID3 } from "crypto";
|
|
1800
1798
|
var nodeFs = {
|
|
1801
1799
|
ensureDir(dir) {
|
|
1802
|
-
if (!
|
|
1800
|
+
if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
|
|
1803
1801
|
},
|
|
1804
1802
|
listFiles(dir, ext) {
|
|
1805
|
-
if (!
|
|
1806
|
-
return
|
|
1803
|
+
if (!fs8.existsSync(dir)) return [];
|
|
1804
|
+
return fs8.readdirSync(dir).filter((f) => f.endsWith(ext));
|
|
1807
1805
|
},
|
|
1808
1806
|
readFile(filePath) {
|
|
1809
|
-
return
|
|
1807
|
+
return fs8.readFileSync(filePath, "utf-8");
|
|
1810
1808
|
},
|
|
1811
1809
|
writeFile(filePath, content) {
|
|
1812
|
-
|
|
1810
|
+
fs8.writeFileSync(filePath, content, "utf-8");
|
|
1813
1811
|
},
|
|
1814
1812
|
deleteFile(filePath) {
|
|
1815
|
-
if (
|
|
1813
|
+
if (fs8.existsSync(filePath)) fs8.unlinkSync(filePath);
|
|
1816
1814
|
},
|
|
1817
1815
|
fileExists(filePath) {
|
|
1818
|
-
return
|
|
1816
|
+
return fs8.existsSync(filePath);
|
|
1819
1817
|
},
|
|
1820
1818
|
joinPath(...parts) {
|
|
1821
|
-
return
|
|
1819
|
+
return path10.join(...parts);
|
|
1822
1820
|
}
|
|
1823
1821
|
};
|
|
1824
1822
|
var managerCache = /* @__PURE__ */ new Map();
|
|
@@ -1828,13 +1826,13 @@ function getAssistantManager(aiDataDir) {
|
|
|
1828
1826
|
cached = {
|
|
1829
1827
|
assistant: new AssistantManager({
|
|
1830
1828
|
fs: nodeFs,
|
|
1831
|
-
assistantsDir:
|
|
1829
|
+
assistantsDir: path10.join(aiDataDir, "assistants"),
|
|
1832
1830
|
builtinRawConfigs: [],
|
|
1833
1831
|
generateId: () => `custom_${randomUUID3().replace(/-/g, "").slice(0, 12)}`
|
|
1834
1832
|
}),
|
|
1835
1833
|
skill: new SkillManagerCore({
|
|
1836
1834
|
fs: nodeFs,
|
|
1837
|
-
skillsDir:
|
|
1835
|
+
skillsDir: path10.join(aiDataDir, "skills"),
|
|
1838
1836
|
builtinRawSkills: [],
|
|
1839
1837
|
contentHash: (content) => createHash("md5").update(content).digest("hex")
|
|
1840
1838
|
})
|
|
@@ -1976,24 +1974,24 @@ async function runServerAgent(options) {
|
|
|
1976
1974
|
}
|
|
1977
1975
|
|
|
1978
1976
|
// src/ai/custom-store.ts
|
|
1979
|
-
import * as
|
|
1980
|
-
import * as
|
|
1977
|
+
import * as fs9 from "fs";
|
|
1978
|
+
import * as path11 from "path";
|
|
1981
1979
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
1982
1980
|
function readProviders(aiDataDir) {
|
|
1983
|
-
const storePath =
|
|
1984
|
-
if (!
|
|
1981
|
+
const storePath = path11.join(aiDataDir, "custom-providers.json");
|
|
1982
|
+
if (!fs9.existsSync(storePath)) return [];
|
|
1985
1983
|
try {
|
|
1986
|
-
const content =
|
|
1984
|
+
const content = fs9.readFileSync(storePath, "utf-8");
|
|
1987
1985
|
return JSON.parse(content);
|
|
1988
1986
|
} catch {
|
|
1989
1987
|
return [];
|
|
1990
1988
|
}
|
|
1991
1989
|
}
|
|
1992
1990
|
function writeProviders(aiDataDir, providers) {
|
|
1993
|
-
const storePath =
|
|
1994
|
-
const dir =
|
|
1995
|
-
if (!
|
|
1996
|
-
|
|
1991
|
+
const storePath = path11.join(aiDataDir, "custom-providers.json");
|
|
1992
|
+
const dir = path11.dirname(storePath);
|
|
1993
|
+
if (!fs9.existsSync(dir)) fs9.mkdirSync(dir, { recursive: true });
|
|
1994
|
+
fs9.writeFileSync(storePath, JSON.stringify(providers, null, 2), "utf-8");
|
|
1997
1995
|
}
|
|
1998
1996
|
function addCustomProvider(aiDataDir, input) {
|
|
1999
1997
|
const providers = readProviders(aiDataDir);
|
|
@@ -2024,20 +2022,20 @@ function deleteCustomProvider(aiDataDir, id) {
|
|
|
2024
2022
|
return { success: true };
|
|
2025
2023
|
}
|
|
2026
2024
|
function readModels(aiDataDir) {
|
|
2027
|
-
const storePath =
|
|
2028
|
-
if (!
|
|
2025
|
+
const storePath = path11.join(aiDataDir, "custom-models.json");
|
|
2026
|
+
if (!fs9.existsSync(storePath)) return [];
|
|
2029
2027
|
try {
|
|
2030
|
-
const content =
|
|
2028
|
+
const content = fs9.readFileSync(storePath, "utf-8");
|
|
2031
2029
|
return JSON.parse(content);
|
|
2032
2030
|
} catch {
|
|
2033
2031
|
return [];
|
|
2034
2032
|
}
|
|
2035
2033
|
}
|
|
2036
2034
|
function writeModels(aiDataDir, models) {
|
|
2037
|
-
const storePath =
|
|
2038
|
-
const dir =
|
|
2039
|
-
if (!
|
|
2040
|
-
|
|
2035
|
+
const storePath = path11.join(aiDataDir, "custom-models.json");
|
|
2036
|
+
const dir = path11.dirname(storePath);
|
|
2037
|
+
if (!fs9.existsSync(dir)) fs9.mkdirSync(dir, { recursive: true });
|
|
2038
|
+
fs9.writeFileSync(storePath, JSON.stringify(models, null, 2), "utf-8");
|
|
2041
2039
|
}
|
|
2042
2040
|
function addCustomModel(aiDataDir, input) {
|
|
2043
2041
|
const models = readModels(aiDataDir);
|
|
@@ -2151,10 +2149,10 @@ function registerAiRoutes(server2, dbManager2, convManager2) {
|
|
|
2151
2149
|
return mgr.getBuiltinCatalog();
|
|
2152
2150
|
});
|
|
2153
2151
|
server2.get("/_web/ai/llm/has-config", async () => {
|
|
2154
|
-
const configPath =
|
|
2155
|
-
if (!
|
|
2152
|
+
const configPath = path12.join(getAiDir(dbManager2), "llm-config.json");
|
|
2153
|
+
if (!fs10.existsSync(configPath)) return false;
|
|
2156
2154
|
try {
|
|
2157
|
-
const data = JSON.parse(
|
|
2155
|
+
const data = JSON.parse(fs10.readFileSync(configPath, "utf-8"));
|
|
2158
2156
|
if (!data.configs || !Array.isArray(data.configs) || data.configs.length === 0) return false;
|
|
2159
2157
|
return data.defaultAssistant != null;
|
|
2160
2158
|
} catch {
|
|
@@ -2162,10 +2160,10 @@ function registerAiRoutes(server2, dbManager2, convManager2) {
|
|
|
2162
2160
|
}
|
|
2163
2161
|
});
|
|
2164
2162
|
server2.get("/_web/ai/llm/configs", async () => {
|
|
2165
|
-
const configPath =
|
|
2166
|
-
if (!
|
|
2163
|
+
const configPath = path12.join(getAiDir(dbManager2), "llm-config.json");
|
|
2164
|
+
if (!fs10.existsSync(configPath)) return { configs: [], defaultAssistant: null, fastModel: null };
|
|
2167
2165
|
try {
|
|
2168
|
-
const data = JSON.parse(
|
|
2166
|
+
const data = JSON.parse(fs10.readFileSync(configPath, "utf-8"));
|
|
2169
2167
|
const configs = (data.configs || []).map((c) => {
|
|
2170
2168
|
const { apiKey: _k, ...rest } = c;
|
|
2171
2169
|
return { ...rest, apiKey: "" };
|
|
@@ -2191,11 +2189,11 @@ function registerAiRoutes(server2, dbManager2, convManager2) {
|
|
|
2191
2189
|
});
|
|
2192
2190
|
});
|
|
2193
2191
|
server2.get("/_web/ai/llm/provider-registry", async () => {
|
|
2194
|
-
const customPath =
|
|
2192
|
+
const customPath = path12.join(getAiDir(dbManager2), "custom-providers.json");
|
|
2195
2193
|
let custom = [];
|
|
2196
|
-
if (
|
|
2194
|
+
if (fs10.existsSync(customPath)) {
|
|
2197
2195
|
try {
|
|
2198
|
-
custom = JSON.parse(
|
|
2196
|
+
custom = JSON.parse(fs10.readFileSync(customPath, "utf-8"));
|
|
2199
2197
|
if (!Array.isArray(custom)) custom = [];
|
|
2200
2198
|
} catch {
|
|
2201
2199
|
custom = [];
|
|
@@ -2204,11 +2202,11 @@ function registerAiRoutes(server2, dbManager2, convManager2) {
|
|
|
2204
2202
|
return [...BUILTIN_PROVIDERS, ...custom];
|
|
2205
2203
|
});
|
|
2206
2204
|
server2.get("/_web/ai/llm/model-catalog", async () => {
|
|
2207
|
-
const customPath =
|
|
2205
|
+
const customPath = path12.join(getAiDir(dbManager2), "custom-models.json");
|
|
2208
2206
|
let custom = [];
|
|
2209
|
-
if (
|
|
2207
|
+
if (fs10.existsSync(customPath)) {
|
|
2210
2208
|
try {
|
|
2211
|
-
custom = JSON.parse(
|
|
2209
|
+
custom = JSON.parse(fs10.readFileSync(customPath, "utf-8"));
|
|
2212
2210
|
if (!Array.isArray(custom)) custom = [];
|
|
2213
2211
|
} catch {
|
|
2214
2212
|
custom = [];
|
|
@@ -2217,20 +2215,20 @@ function registerAiRoutes(server2, dbManager2, convManager2) {
|
|
|
2217
2215
|
return [...BUILTIN_MODELS, ...custom];
|
|
2218
2216
|
});
|
|
2219
2217
|
server2.get("/_web/ai/llm/default-assistant-slot", async () => {
|
|
2220
|
-
const configPath =
|
|
2221
|
-
if (!
|
|
2218
|
+
const configPath = path12.join(getAiDir(dbManager2), "llm-config.json");
|
|
2219
|
+
if (!fs10.existsSync(configPath)) return null;
|
|
2222
2220
|
try {
|
|
2223
|
-
const data = JSON.parse(
|
|
2221
|
+
const data = JSON.parse(fs10.readFileSync(configPath, "utf-8"));
|
|
2224
2222
|
return data.defaultAssistant ?? null;
|
|
2225
2223
|
} catch {
|
|
2226
2224
|
return null;
|
|
2227
2225
|
}
|
|
2228
2226
|
});
|
|
2229
2227
|
server2.get("/_web/ai/llm/fast-model-slot", async () => {
|
|
2230
|
-
const configPath =
|
|
2231
|
-
if (!
|
|
2228
|
+
const configPath = path12.join(getAiDir(dbManager2), "llm-config.json");
|
|
2229
|
+
if (!fs10.existsSync(configPath)) return null;
|
|
2232
2230
|
try {
|
|
2233
|
-
const data = JSON.parse(
|
|
2231
|
+
const data = JSON.parse(fs10.readFileSync(configPath, "utf-8"));
|
|
2234
2232
|
return data.fastModel ?? null;
|
|
2235
2233
|
} catch {
|
|
2236
2234
|
return null;
|
|
@@ -2583,8 +2581,8 @@ var noop = () => {
|
|
|
2583
2581
|
var NOOP_LOGGER = { info: noop, warn: noop, error: noop };
|
|
2584
2582
|
|
|
2585
2583
|
// ../../packages/sync/src/data-source-manager.ts
|
|
2586
|
-
import * as
|
|
2587
|
-
import * as
|
|
2584
|
+
import * as fs11 from "fs";
|
|
2585
|
+
import * as path13 from "path";
|
|
2588
2586
|
import * as crypto2 from "crypto";
|
|
2589
2587
|
var CONFIG_FILE = "data-sources.json";
|
|
2590
2588
|
function generateId(prefix = "ds") {
|
|
@@ -2612,17 +2610,17 @@ var DataSourceManager = class {
|
|
|
2612
2610
|
this.logger = logger2 ?? NOOP_LOGGER;
|
|
2613
2611
|
}
|
|
2614
2612
|
getConfigPath() {
|
|
2615
|
-
return
|
|
2613
|
+
return path13.join(this.configDir, CONFIG_FILE);
|
|
2616
2614
|
}
|
|
2617
2615
|
ensureDir() {
|
|
2618
|
-
|
|
2616
|
+
fs11.mkdirSync(this.configDir, { recursive: true });
|
|
2619
2617
|
}
|
|
2620
2618
|
// ==================== Load / Save ====================
|
|
2621
2619
|
loadAll() {
|
|
2622
2620
|
try {
|
|
2623
2621
|
const filePath = this.getConfigPath();
|
|
2624
|
-
if (
|
|
2625
|
-
const raw =
|
|
2622
|
+
if (fs11.existsSync(filePath)) {
|
|
2623
|
+
const raw = fs11.readFileSync(filePath, "utf-8");
|
|
2626
2624
|
const parsed = JSON.parse(raw);
|
|
2627
2625
|
if (!isValidDataSourceArray(parsed)) {
|
|
2628
2626
|
this.logger.warn("[DataSource] Incompatible config format detected, returning empty.");
|
|
@@ -2641,7 +2639,7 @@ var DataSourceManager = class {
|
|
|
2641
2639
|
saveAll(sources) {
|
|
2642
2640
|
try {
|
|
2643
2641
|
this.ensureDir();
|
|
2644
|
-
|
|
2642
|
+
fs11.writeFileSync(this.getConfigPath(), JSON.stringify(sources, null, 2), "utf-8");
|
|
2645
2643
|
} catch (err) {
|
|
2646
2644
|
this.logger.error("[DataSource] Failed to save config", err);
|
|
2647
2645
|
}
|
|
@@ -2766,7 +2764,7 @@ function parseRemoteSessionsResponse(body) {
|
|
|
2766
2764
|
}
|
|
2767
2765
|
|
|
2768
2766
|
// ../../packages/sync/src/pull-engine.ts
|
|
2769
|
-
import * as
|
|
2767
|
+
import * as fs12 from "fs";
|
|
2770
2768
|
import * as crypto3 from "crypto";
|
|
2771
2769
|
var MAX_PAGES_PER_PULL = 50;
|
|
2772
2770
|
var PULL_OVERLAP_SECONDS = 60;
|
|
@@ -2787,7 +2785,7 @@ function parseSyncFromFile(filePath) {
|
|
|
2787
2785
|
try {
|
|
2788
2786
|
const isJsonl = filePath.endsWith(".jsonl");
|
|
2789
2787
|
if (isJsonl) {
|
|
2790
|
-
const content =
|
|
2788
|
+
const content = fs12.readFileSync(filePath, "utf-8");
|
|
2791
2789
|
const lines = content.trimEnd().split("\n");
|
|
2792
2790
|
for (let i = lines.length - 1; i >= Math.max(0, lines.length - 5); i--) {
|
|
2793
2791
|
try {
|
|
@@ -2806,7 +2804,7 @@ function parseSyncFromFile(filePath) {
|
|
|
2806
2804
|
}
|
|
2807
2805
|
return null;
|
|
2808
2806
|
}
|
|
2809
|
-
const raw =
|
|
2807
|
+
const raw = fs12.readFileSync(filePath, "utf-8");
|
|
2810
2808
|
const parsed = JSON.parse(raw);
|
|
2811
2809
|
if (parsed.sync && typeof parsed.sync === "object") {
|
|
2812
2810
|
const s = parsed.sync;
|
|
@@ -2819,7 +2817,7 @@ function parseSyncFromFile(filePath) {
|
|
|
2819
2817
|
}
|
|
2820
2818
|
function cleanupTempFile(filePath) {
|
|
2821
2819
|
try {
|
|
2822
|
-
if (
|
|
2820
|
+
if (fs12.existsSync(filePath)) fs12.unlinkSync(filePath);
|
|
2823
2821
|
} catch {
|
|
2824
2822
|
}
|
|
2825
2823
|
}
|
|
@@ -2830,6 +2828,7 @@ var PullEngine = class {
|
|
|
2830
2828
|
dsManager;
|
|
2831
2829
|
logger;
|
|
2832
2830
|
isImporting;
|
|
2831
|
+
pullingSourceIds = /* @__PURE__ */ new Set();
|
|
2833
2832
|
constructor(options) {
|
|
2834
2833
|
this.fetcher = options.fetcher;
|
|
2835
2834
|
this.importer = options.importer;
|
|
@@ -2872,7 +2871,7 @@ var PullEngine = class {
|
|
|
2872
2871
|
limit: ds.pullLimit
|
|
2873
2872
|
});
|
|
2874
2873
|
try {
|
|
2875
|
-
const stat =
|
|
2874
|
+
const stat = fs12.statSync(tempFile);
|
|
2876
2875
|
this.logger.info(`[Pull] "${sess.name}" page ${pageCount}: fetched ${stat.size} bytes`);
|
|
2877
2876
|
if (stat.size === 0) {
|
|
2878
2877
|
cleanupTempFile(tempFile);
|
|
@@ -2953,8 +2952,17 @@ var PullEngine = class {
|
|
|
2953
2952
|
}
|
|
2954
2953
|
}
|
|
2955
2954
|
async pullAllSessions(ds) {
|
|
2956
|
-
|
|
2957
|
-
|
|
2955
|
+
if (this.pullingSourceIds.has(ds.id)) {
|
|
2956
|
+
this.logger.info(`[Pull] Skipping pullAllSessions for "${ds.baseUrl}": pull already in progress`);
|
|
2957
|
+
return;
|
|
2958
|
+
}
|
|
2959
|
+
this.pullingSourceIds.add(ds.id);
|
|
2960
|
+
try {
|
|
2961
|
+
for (const sess of ds.sessions) {
|
|
2962
|
+
await this.executePullSession(ds.id, ds, sess);
|
|
2963
|
+
}
|
|
2964
|
+
} finally {
|
|
2965
|
+
this.pullingSourceIds.delete(ds.id);
|
|
2958
2966
|
}
|
|
2959
2967
|
}
|
|
2960
2968
|
async triggerPull(sourceId, sessionId) {
|
|
@@ -2966,15 +2974,24 @@ var PullEngine = class {
|
|
|
2966
2974
|
const result = await this.executePullSession(sourceId, ds, sess);
|
|
2967
2975
|
return { success: result.success, error: result.error };
|
|
2968
2976
|
}
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
if (!result.success && result.error) errors.push(`${sess.name}: ${result.error}`);
|
|
2977
|
+
if (this.pullingSourceIds.has(sourceId)) {
|
|
2978
|
+
this.logger.info(`[Pull] Skipping triggerPull for source ${sourceId}: pull already in progress`);
|
|
2979
|
+
return { success: true };
|
|
2973
2980
|
}
|
|
2974
|
-
|
|
2975
|
-
|
|
2981
|
+
this.pullingSourceIds.add(sourceId);
|
|
2982
|
+
try {
|
|
2983
|
+
const errors = [];
|
|
2984
|
+
for (const sess of ds.sessions) {
|
|
2985
|
+
const result = await this.executePullSession(sourceId, ds, sess);
|
|
2986
|
+
if (!result.success && result.error) errors.push(`${sess.name}: ${result.error}`);
|
|
2987
|
+
}
|
|
2988
|
+
if (errors.length > 0) {
|
|
2989
|
+
return { success: false, error: errors.join("; ") };
|
|
2990
|
+
}
|
|
2991
|
+
return { success: true };
|
|
2992
|
+
} finally {
|
|
2993
|
+
this.pullingSourceIds.delete(sourceId);
|
|
2976
2994
|
}
|
|
2977
|
-
return { success: true };
|
|
2978
2995
|
}
|
|
2979
2996
|
async triggerPullAll(sourceId) {
|
|
2980
2997
|
return this.triggerPull(sourceId);
|
|
@@ -3048,13 +3065,13 @@ function reloadTimer(dsId, skipInitialPull = false) {
|
|
|
3048
3065
|
}
|
|
3049
3066
|
|
|
3050
3067
|
// src/sync/adapters.ts
|
|
3051
|
-
import * as
|
|
3052
|
-
import * as
|
|
3053
|
-
import * as
|
|
3068
|
+
import * as fs13 from "fs";
|
|
3069
|
+
import * as os4 from "os";
|
|
3070
|
+
import * as path14 from "path";
|
|
3054
3071
|
import * as crypto4 from "crypto";
|
|
3055
3072
|
function getTempFilePath(ext) {
|
|
3056
3073
|
const id = crypto4.randomBytes(8).toString("hex");
|
|
3057
|
-
return
|
|
3074
|
+
return path14.join(os4.tmpdir(), `chatlab-pull-${id}${ext}`);
|
|
3058
3075
|
}
|
|
3059
3076
|
var NodeFetcher = class {
|
|
3060
3077
|
async fetchToTempFile(baseUrl, remoteSessionId, token, params) {
|
|
@@ -3071,14 +3088,14 @@ var NodeFetcher = class {
|
|
|
3071
3088
|
const isJsonl = contentType.includes("ndjson") || contentType.includes("jsonl");
|
|
3072
3089
|
const tempFile = getTempFilePath(isJsonl ? ".jsonl" : ".json");
|
|
3073
3090
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
3074
|
-
|
|
3091
|
+
fs13.writeFileSync(tempFile, buffer);
|
|
3075
3092
|
return tempFile;
|
|
3076
3093
|
}
|
|
3077
3094
|
};
|
|
3078
3095
|
function resolveNativeBinding2() {
|
|
3079
3096
|
if (process.versions.electron) return void 0;
|
|
3080
|
-
const nativePath =
|
|
3081
|
-
if (
|
|
3097
|
+
const nativePath = path14.resolve(__dirname, "../../native/better_sqlite3.node");
|
|
3098
|
+
if (fs13.existsSync(nativePath)) return nativePath;
|
|
3082
3099
|
return void 0;
|
|
3083
3100
|
}
|
|
3084
3101
|
var DirectImporter = class {
|
|
@@ -3092,12 +3109,16 @@ var DirectImporter = class {
|
|
|
3092
3109
|
}
|
|
3093
3110
|
sessionExists(sessionId) {
|
|
3094
3111
|
const dbPath = this.dbManager.getDbPath(sessionId);
|
|
3095
|
-
return
|
|
3112
|
+
return fs13.existsSync(dbPath);
|
|
3096
3113
|
}
|
|
3097
3114
|
async importFile(tempFile, targetSessionId, externalId) {
|
|
3098
3115
|
if (targetSessionId && this.sessionExists(targetSessionId)) {
|
|
3099
3116
|
return this.incrementalImportFile(targetSessionId, tempFile);
|
|
3100
3117
|
}
|
|
3118
|
+
if (targetSessionId) {
|
|
3119
|
+
this.logger.info(`[DirectImporter] Session ${targetSessionId} not found locally, need full resync`);
|
|
3120
|
+
return { success: false, newMessageCount: 0, needFullResync: true };
|
|
3121
|
+
}
|
|
3101
3122
|
return this.fullImportFile(tempFile, externalId);
|
|
3102
3123
|
}
|
|
3103
3124
|
async incrementalImportFile(sessionId, tempFile) {
|
|
@@ -3191,6 +3212,10 @@ function registerAutomationRoutes(server2, ctx) {
|
|
|
3191
3212
|
return reply.code(404).send({ error: "Data source not found" });
|
|
3192
3213
|
}
|
|
3193
3214
|
reloadTimer(request.params.id, true);
|
|
3215
|
+
for (const sess of added) {
|
|
3216
|
+
pullEngine2.triggerPull(request.params.id, sess.id).catch(() => {
|
|
3217
|
+
});
|
|
3218
|
+
}
|
|
3194
3219
|
return added;
|
|
3195
3220
|
});
|
|
3196
3221
|
server2.delete("/_web/automation/data-sources/:id/sessions/:sessId", async (request, reply) => {
|
|
@@ -3270,8 +3295,8 @@ var dbManager = null;
|
|
|
3270
3295
|
var convManager = null;
|
|
3271
3296
|
function resolveNativeBinding3() {
|
|
3272
3297
|
if (process.versions.electron) return void 0;
|
|
3273
|
-
const nativePath =
|
|
3274
|
-
if (
|
|
3298
|
+
const nativePath = path15.resolve(__dirname, "../../native/better_sqlite3.node");
|
|
3299
|
+
if (fs14.existsSync(nativePath)) return nativePath;
|
|
3275
3300
|
return void 0;
|
|
3276
3301
|
}
|
|
3277
3302
|
function ensureToken(config) {
|
|
@@ -3327,7 +3352,7 @@ async function startHttpServer(options) {
|
|
|
3327
3352
|
registerWebRoutes(server, dbManager, { pathProvider, nativeBinding });
|
|
3328
3353
|
registerPreferencesRoutes(server, pathProvider);
|
|
3329
3354
|
initSync(server, dbManager, pathProvider, { port, host, token });
|
|
3330
|
-
if (options?.webRoot &&
|
|
3355
|
+
if (options?.webRoot && fs14.existsSync(options.webRoot)) {
|
|
3331
3356
|
setWebMode(true);
|
|
3332
3357
|
const fastifyStatic = await import("@fastify/static");
|
|
3333
3358
|
await server.register(fastifyStatic.default, {
|
|
@@ -3372,4 +3397,4 @@ export {
|
|
|
3372
3397
|
startHttpServer,
|
|
3373
3398
|
stopHttpServer
|
|
3374
3399
|
};
|
|
3375
|
-
//# sourceMappingURL=chunk-
|
|
3400
|
+
//# sourceMappingURL=chunk-FBTHZU55.mjs.map
|