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.
Files changed (54) hide show
  1. package/dist/{chunk-YAOFJ73U.mjs → chunk-3DIWXYMJ.mjs} +80 -129
  2. package/dist/chunk-3DIWXYMJ.mjs.map +1 -0
  3. package/dist/{chunk-JMRDNHIR.mjs → chunk-EIRLEPXP.mjs} +824 -280
  4. package/dist/chunk-EIRLEPXP.mjs.map +1 -0
  5. package/dist/{chunk-DL3BFEAH.mjs → chunk-EWCSGYVT.mjs} +2 -2
  6. package/dist/chunk-EWCSGYVT.mjs.map +1 -0
  7. package/dist/{chunk-PF4QFHC6.mjs → chunk-FBTHZU55.mjs} +849 -824
  8. package/dist/chunk-FBTHZU55.mjs.map +1 -0
  9. package/dist/{chunk-P4N5GEFF.mjs → chunk-NB4BIAYM.mjs} +9 -9
  10. package/dist/{chunk-OC73KSMV.mjs → chunk-XUHPCRVT.mjs} +2 -2
  11. package/dist/cli.mjs +2 -2
  12. package/dist/{http-PQEOFGJK.mjs → http-QVYJ5HER.mjs} +6 -6
  13. package/dist/{import-UR2IE2O7.mjs → import-AJKL6JPR.mjs} +6 -4
  14. package/dist/index.mjs +6 -6
  15. package/dist/{mcp-4XCDZ5PU.mjs → mcp-FRF3YAN5.mjs} +5 -6
  16. package/dist/mcp-FRF3YAN5.mjs.map +1 -0
  17. package/dist/{src-ZELOFE7P.mjs → src-KOIGBZJX.mjs} +14 -2
  18. package/dist/{stream-import-RP3XB6NM.mjs → stream-import-URGOCXUH.mjs} +5 -3
  19. 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
  20. package/dist-web/assets/{EChartWordcloud-DcC12j9S.js → EChartWordcloud-BYLFxPAX.js} +1 -1
  21. package/dist-web/assets/{MessageExportModal-CUS9nnwi.js → MessageExportModal-BAf68-wB.js} +1 -1
  22. package/dist-web/assets/electron-2m4Vdk2z.js +1 -0
  23. package/dist-web/assets/{fetch-DvfIZxuz.js → fetch-Ayp337Ng.js} +1 -1
  24. package/dist-web/assets/{fetch-CJ8Q4qM2.js → fetch-BWtpQwBZ.js} +1 -1
  25. package/dist-web/assets/fetch-o5NJ2uWE.js +3 -0
  26. package/dist-web/assets/{index-D3siJhmm.js → index-Bd5r2oFG.js} +4 -4
  27. package/dist-web/assets/index-BfZM-q0T.js +7 -0
  28. package/dist-web/assets/{index-CCE1zef2.js → index-CLYHD_f4.js} +2 -2
  29. package/dist-web/assets/{index-CUTe0Sc3.js → index-CaTRpNBK.js} +12 -12
  30. package/dist-web/assets/index-Ksa--o23.js +173 -0
  31. package/dist-web/assets/{vendor-echarts-BXkS9r5W.js → vendor-echarts-C13vDgZW.js} +5 -5
  32. package/dist-web/assets/{vendor-echarts-wordcloud-CmDVLUv0.js → vendor-echarts-wordcloud-uVdwYsYa.js} +1 -1
  33. package/dist-web/assets/{vendor-nuxt-ui-D1aReMtR.js → vendor-nuxt-ui-CYMNNdeD.js} +2 -2
  34. package/dist-web/assets/{vendor-zrender-B6SpLIkk.js → vendor-zrender-5DcYjSZk.js} +2 -2
  35. package/dist-web/assets/{web-DOZKbBMQ.js → web-C5yfcwjn.js} +1 -1
  36. package/dist-web/index.html +2 -2
  37. package/package.json +1 -2
  38. package/CHANGELOG.md +0 -51
  39. package/LICENSE +0 -661
  40. package/dist/chunk-DL3BFEAH.mjs.map +0 -1
  41. package/dist/chunk-JMRDNHIR.mjs.map +0 -1
  42. package/dist/chunk-PF4QFHC6.mjs.map +0 -1
  43. package/dist/chunk-YAOFJ73U.mjs.map +0 -1
  44. package/dist/mcp-4XCDZ5PU.mjs.map +0 -1
  45. package/dist-web/assets/electron-DvcSSc-D.js +0 -1
  46. package/dist-web/assets/fetch-DgQkWW73.js +0 -3
  47. package/dist-web/assets/index-68UnLyFT.js +0 -173
  48. package/dist-web/assets/index-n2XUUgti.js +0 -7
  49. /package/dist/{chunk-P4N5GEFF.mjs.map → chunk-NB4BIAYM.mjs.map} +0 -0
  50. /package/dist/{chunk-OC73KSMV.mjs.map → chunk-XUHPCRVT.mjs.map} +0 -0
  51. /package/dist/{http-PQEOFGJK.mjs.map → http-QVYJ5HER.mjs.map} +0 -0
  52. /package/dist/{import-UR2IE2O7.mjs.map → import-AJKL6JPR.mjs.map} +0 -0
  53. /package/dist/{src-ZELOFE7P.mjs.map → src-KOIGBZJX.mjs.map} +0 -0
  54. /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-OC73KSMV.mjs";
10
+ } from "./chunk-XUHPCRVT.mjs";
11
11
  import {
12
12
  analyzeIncrementalImport,
13
13
  analyzeNewImport,
14
14
  incrementalImport,
15
15
  streamImport
16
- } from "./chunk-DL3BFEAH.mjs";
16
+ } from "./chunk-EWCSGYVT.mjs";
17
17
  import {
18
18
  AGENT_TOOL_REGISTRY,
19
19
  CoreDataProvider
20
- } from "./chunk-YAOFJ73U.mjs";
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-JMRDNHIR.mjs";
117
+ } from "./chunk-EIRLEPXP.mjs";
125
118
 
126
119
  // src/http/index.ts
127
- import * as fs11 from "fs";
128
- import * as path11 from "path";
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 fs3 from "fs";
502
- import * as os from "os";
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/ai/llm-config.ts
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 = path2.join(aiDataDir, "llm-config.json");
585
- if (!fs2.existsSync(configPath)) {
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(fs2.readFileSync(configPath, "utf-8"));
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 = path2.join(aiDataDir, "llm-config.json");
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
- fs2.writeFileSync(configPath, JSON.stringify(toSave, null, 2), "utf-8");
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 = path2.join(aiDataDir, "llm-config.json");
731
- if (!fs2.existsSync(configPath)) {
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(fs2.readFileSync(configPath, "utf-8"));
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 resolveNativeBinding() {
751
- if (process.versions.electron) return void 0;
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
- loadMessages(chatSessionId, limit = 500) {
791
- const data = getSessionMessages(db, chatSessionId, limit);
792
- if (!data) return null;
793
- return data.messages.map((m) => ({ senderName: m.senderName, content: m.content }));
1049
+ getLlmConfig() {
1050
+ return getDefaultAssistantConfig(aiDataDir);
794
1051
  },
795
- saveSummary(chatSessionId, summary) {
796
- saveChatSessionSummary(db, chatSessionId, summary);
797
- },
798
- getSummary(chatSessionId) {
799
- return getChatSessionSummary(db, chatSessionId);
800
- },
801
- async llmComplete(systemPrompt, userPrompt, options) {
802
- const result = await completeSimple(
803
- piModel,
804
- {
805
- systemPrompt,
806
- messages: [{ role: "user", content: [{ type: "text", text: userPrompt }], timestamp: Date.now() }]
807
- },
808
- { apiKey: llmConfig.apiKey, maxTokens: options?.maxTokens, temperature: options?.temperature }
809
- );
810
- return result.content.filter((item) => item.type === "text").map((item) => item.text).join("");
811
- },
812
- t: (key) => key
1052
+ buildPiModel(config) {
1053
+ return buildPiModel2(config);
1054
+ }
813
1055
  };
814
1056
  }
815
- function registerWebRoutes(server2, dbManager2, options) {
816
- const mergeCache = options?.pathProvider ? new MergeSessionCache(options.pathProvider, { nativeBinding: options.nativeBinding }) : null;
817
- mergeCache?.cleanupOrphans();
818
- server2.get("/_web/sessions", async () => {
819
- const sessionIds = dbManager2.listSessionIds();
820
- return sessionIds.map((id) => {
821
- const db = dbManager2.open(id);
822
- if (!db) return null;
823
- const info = getSessionInfo(db);
824
- if (!info) return null;
825
- return {
826
- ...info,
827
- id,
828
- dbPath: "",
829
- memberAvatar: null,
830
- aiConversationCount: 0
831
- };
832
- }).filter(Boolean);
833
- });
834
- server2.get("/_web/sessions/:id", async (request) => {
835
- const db = ensureDb2(dbManager2, request.params.id);
836
- const info = getSessionInfo(db);
837
- if (!info) return null;
838
- return {
839
- ...info,
840
- id: request.params.id,
841
- dbPath: "",
842
- memberAvatar: null,
843
- aiConversationCount: 0
844
- };
845
- });
846
- server2.delete("/_web/sessions/:id", async (request, reply) => {
847
- const { id } = request.params;
848
- try {
849
- dbManager2.close(id);
850
- const dbPath = dbManager2.getDbPath(id);
851
- const fs12 = await import("fs");
852
- if (fs12.existsSync(dbPath)) {
853
- fs12.unlinkSync(dbPath);
854
- return { success: true };
855
- }
856
- return reply.code(404).send({ success: false, error: "File not found" });
857
- } catch (err) {
858
- return reply.code(500).send({ success: false, error: String(err) });
859
- }
860
- });
861
- server2.patch("/_web/sessions/:id/name", async (request) => {
862
- const db = ensureDb2(dbManager2, request.params.id);
863
- const { name } = request.body;
864
- db.prepare("UPDATE meta SET name = ?").run(name);
865
- return { success: true };
866
- });
867
- server2.patch(
868
- "/_web/sessions/:id/owner",
869
- async (request) => {
870
- const db = ensureDb2(dbManager2, request.params.id);
871
- const { ownerId } = request.body;
872
- db.prepare("UPDATE meta SET owner_id = ?").run(ownerId ?? null);
873
- return { success: true };
874
- }
875
- );
876
- server2.get("/_web/sessions/:id/years", async (request) => {
877
- const db = ensureDb2(dbManager2, request.params.id);
878
- return getAvailableYears(db);
879
- });
880
- server2.get("/_web/sessions/:id/time-range", async (request) => {
881
- const db = ensureDb2(dbManager2, request.params.id);
882
- return getTimeRange(db);
883
- });
884
- server2.get("/_web/sessions/:id/stats/member-activity", async (request) => {
885
- const db = ensureDb2(dbManager2, request.params.id);
886
- const filter = parseTimeFilter(request.query);
887
- return getMemberActivity(db, filter);
888
- });
889
- server2.get("/_web/sessions/:id/stats/hourly", async (request) => {
890
- const db = ensureDb2(dbManager2, request.params.id);
891
- const filter = parseTimeFilter(request.query);
892
- return getHourlyActivity(db, filter);
893
- });
894
- server2.get("/_web/sessions/:id/stats/daily", async (request) => {
895
- const db = ensureDb2(dbManager2, request.params.id);
896
- const filter = parseTimeFilter(request.query);
897
- return getDailyActivity(db, filter);
898
- });
899
- server2.get("/_web/sessions/:id/stats/weekday", async (request) => {
900
- const db = ensureDb2(dbManager2, request.params.id);
901
- const filter = parseTimeFilter(request.query);
902
- return getWeekdayActivity(db, filter);
903
- });
904
- server2.get("/_web/sessions/:id/stats/message-types", async (request) => {
905
- const db = ensureDb2(dbManager2, request.params.id);
906
- const filter = parseTimeFilter(request.query);
907
- return getMessageTypeStats(db, filter);
908
- });
909
- server2.get("/_web/sessions/:id/members", async (request) => {
910
- const db = ensureDb2(dbManager2, request.params.id);
911
- return getMembersWithAliases(db);
912
- });
913
- server2.get("/_web/sessions/:id/members/paginated", async (request) => {
914
- const db = ensureDb2(dbManager2, request.params.id);
915
- const result = getMembersPaginated(db, {
916
- page: parseInt(request.query.page || "1", 10),
917
- pageSize: parseInt(request.query.pageSize || "20", 10),
918
- search: request.query.search,
919
- sortOrder: request.query.sortOrder === "asc" ? "asc" : "desc"
920
- });
921
- return {
922
- items: result.members,
923
- total: result.total,
924
- page: result.page,
925
- pageSize: result.pageSize,
926
- totalPages: result.totalPages
927
- };
928
- });
929
- server2.patch(
930
- "/_web/sessions/:id/members/:memberId/aliases",
931
- async (request) => {
932
- const db = ensureDb2(dbManager2, request.params.id);
933
- const memberId = parseInt(request.params.memberId, 10);
934
- const { aliases } = request.body;
935
- db.prepare("UPDATE member SET aliases = ? WHERE id = ?").run(JSON.stringify(aliases), memberId);
936
- return { success: true };
937
- }
938
- );
939
- server2.delete(
940
- "/_web/sessions/:id/members/:memberId",
941
- async (request) => {
942
- const db = ensureDb2(dbManager2, request.params.id);
943
- const memberId = parseInt(request.params.memberId, 10);
944
- db.prepare("DELETE FROM message WHERE sender_id = ?").run(memberId);
945
- db.prepare("DELETE FROM member WHERE id = ?").run(memberId);
946
- return { success: true };
947
- }
948
- );
949
- server2.post(
950
- "/_web/sessions/:id/members/merge",
951
- async (request) => {
952
- const db = ensureDb2(dbManager2, request.params.id);
953
- const { memberId1, memberId2 } = request.body;
954
- db.prepare("UPDATE message SET sender_id = ? WHERE sender_id = ?").run(memberId1, memberId2);
955
- db.prepare("DELETE FROM member WHERE id = ?").run(memberId2);
956
- return { success: true };
957
- }
958
- );
959
- server2.get(
960
- "/_web/sessions/:id/members/:memberId/history",
961
- async (request) => {
962
- const db = ensureDb2(dbManager2, request.params.id);
963
- const memberId = parseInt(request.params.memberId, 10);
964
- const rows = db.prepare(
965
- `SELECT
966
- sender_account_name as accountName,
967
- sender_group_nickname as groupNickname,
968
- MIN(ts) as startTs,
969
- MAX(ts) as endTs
970
- FROM message
971
- WHERE sender_id = ?
972
- GROUP BY sender_account_name, sender_group_nickname
973
- ORDER BY startTs`
974
- ).all(memberId);
975
- const history = [];
976
- for (const row of rows) {
977
- if (row.accountName) {
978
- history.push({ nameType: "account_name", name: row.accountName, startTs: row.startTs, endTs: row.endTs });
979
- }
980
- if (row.groupNickname) {
981
- history.push({ nameType: "group_nickname", name: row.groupNickname, startTs: row.startTs, endTs: row.endTs });
982
- }
983
- }
984
- return history;
985
- }
986
- );
987
- server2.get("/_web/sessions/:id/analytics/relationship", async (request) => {
988
- const db = ensureDb2(dbManager2, request.params.id);
989
- const filter = parseTimeFilter(request.query);
990
- return getRelationshipStats(db, filter);
991
- });
992
- server2.get("/_web/sessions/:id/analytics/catchphrase", async (request) => {
993
- const db = ensureDb2(dbManager2, request.params.id);
994
- const filter = parseTimeFilter(request.query);
995
- return getCatchphraseAnalysis(db, filter);
996
- });
997
- server2.get("/_web/sessions/:id/analytics/mention", async (request) => {
998
- const db = ensureDb2(dbManager2, request.params.id);
999
- const filter = parseTimeFilter(request.query);
1000
- return getMentionAnalysis(db, filter);
1001
- });
1002
- server2.get("/_web/sessions/:id/analytics/mention-graph", async (request) => {
1003
- const db = ensureDb2(dbManager2, request.params.id);
1004
- const filter = parseTimeFilter(request.query);
1005
- return getMentionGraph(db, filter);
1006
- });
1007
- server2.get("/_web/sessions/:id/analytics/laugh", async (request) => {
1008
- const db = ensureDb2(dbManager2, request.params.id);
1009
- const filter = parseTimeFilter(request.query);
1010
- return getLaughAnalysis(db, filter);
1011
- });
1012
- server2.get("/_web/sessions/:id/analytics/cluster", async (request) => {
1013
- const db = ensureDb2(dbManager2, request.params.id);
1014
- const filter = parseTimeFilter(request.query);
1015
- const topEdges = request.query.topEdges ? parseInt(request.query.topEdges, 10) : void 0;
1016
- return getClusterGraph(db, filter, topEdges ? { topEdges } : void 0);
1017
- });
1018
- server2.get("/_web/sessions/:id/analytics/language-preference", async (request) => {
1019
- const db = ensureDb2(dbManager2, request.params.id);
1020
- const filter = parseTimeFilter(request.query);
1021
- const locale = request.query.locale || "zh-CN";
1022
- const nlpProvider = createJiebaNlpProvider();
1023
- return getLanguagePreferenceAnalysis(db, { locale, timeFilter: filter, nlpProvider });
1024
- });
1025
- server2.get("/_web/sessions/:id/analytics/monthly-activity", async (request) => {
1026
- const db = ensureDb2(dbManager2, request.params.id);
1027
- const filter = parseTimeFilter(request.query);
1028
- return getMonthlyActivity(db, filter);
1029
- });
1030
- server2.get("/_web/sessions/:id/analytics/yearly-activity", async (request) => {
1031
- const db = ensureDb2(dbManager2, request.params.id);
1032
- const filter = parseTimeFilter(request.query);
1033
- return getYearlyActivity(db, filter);
1034
- });
1035
- server2.get("/_web/sessions/:id/analytics/message-length-distribution", async (request) => {
1036
- const db = ensureDb2(dbManager2, request.params.id);
1037
- const filter = parseTimeFilter(request.query);
1038
- return getMessageLengthDistribution(db, filter);
1039
- });
1040
- server2.post("/_web/sessions/:id/sql", async (request, reply) => {
1041
- const db = ensureDb2(dbManager2, request.params.id);
1042
- const { sql } = request.body || {};
1043
- if (!sql || typeof sql !== "string") {
1044
- return reply.code(400).send({ error: "Missing sql parameter" });
1045
- }
1046
- try {
1047
- return executeSql(db, sql, { timing: true });
1048
- } catch (err) {
1049
- const message = err instanceof Error ? err.message : "SQL execution error";
1050
- return reply.code(400).send({ error: message });
1051
- }
1052
- });
1053
- server2.get("/_web/sessions/:id/schema", async (request) => {
1054
- const db = ensureDb2(dbManager2, request.params.id);
1055
- return getSchemaDetailed(db);
1056
- });
1057
- server2.post("/_web/sessions/:id/query", async (request) => {
1058
- const db = ensureDb2(dbManager2, request.params.id);
1059
- const { sql, params = [] } = request.body;
1060
- if (!sql || typeof sql !== "string") {
1061
- throw Object.assign(new Error('Missing or invalid "sql" field'), { statusCode: 400 });
1062
- }
1063
- const stmt = db.prepare(sql.trim());
1064
- if (!stmt.readonly) {
1065
- throw Object.assign(new Error("Only READ-ONLY statements are allowed"), { statusCode: 403 });
1066
- }
1067
- if (Array.isArray(params)) {
1068
- return stmt.all(...params);
1069
- }
1070
- return stmt.all(params);
1071
- });
1072
- server2.post("/_web/sessions/:id/export/markdown", async (request, reply) => {
1073
- const { id } = request.params;
1074
- const body = request.body;
1075
- const sessionName = body?.sessionName || id;
1076
- const chunks = [];
1077
- const result = exportFilterResultToMarkdown(
1078
- {
1079
- sessionId: id,
1080
- sessionName,
1081
- filterMode: body.filterMode || "condition",
1082
- keywords: body.keywords,
1083
- timeFilter: body.timeFilter,
1084
- senderIds: body.senderIds,
1085
- contextSize: body.contextSize,
1086
- chatSessionIds: body.chatSessionIds
1087
- },
1088
- {
1089
- openDatabase(sessionId) {
1090
- return dbManager2.open(sessionId) ?? null;
1091
- }
1092
- },
1093
- {
1094
- write(chunk) {
1095
- chunks.push(chunk);
1096
- },
1097
- end() {
1098
- }
1099
- }
1100
- );
1101
- if (!result.success) {
1102
- return reply.code(500).send({ error: result.error });
1103
- }
1104
- reply.header("Content-Type", "text/markdown; charset=utf-8");
1105
- reply.header("Content-Disposition", `attachment; filename="${encodeURIComponent(sessionName)}_export.md"`);
1106
- return reply.send(chunks.join(""));
1107
- });
1108
- server2.post("/_web/sessions/:id/generate-index", async (request) => {
1109
- const db = ensureWritableDb(dbManager2, request.params.id);
1110
- const gapThreshold = request.body?.gapThreshold ?? 1800;
1111
- const sessionCount = generateSessionIndex(db, gapThreshold);
1112
- return { sessionCount };
1113
- });
1114
- server2.post("/_web/sessions/:id/clear-index", async (request) => {
1115
- const db = ensureWritableDb(dbManager2, request.params.id);
1116
- clearSessionIndex(db);
1117
- return { success: true };
1118
- });
1119
- server2.get("/_web/sessions/:id/search/fts", async (request, reply) => {
1120
- const db = ensureDb2(dbManager2, request.params.id);
1121
- if (!hasFtsTable(db)) {
1122
- return reply.code(400).send({ error: "FTS index not built for this session" });
1123
- }
1124
- const keywords = request.query.keywords.split(/\s+/).filter(Boolean);
1125
- if (keywords.length === 0) return { rowids: [], total: 0 };
1126
- const limit = parseInt(request.query.limit || "100", 10);
1127
- const offset = parseInt(request.query.offset || "0", 10);
1128
- return searchByFts(db, keywords, limit, offset);
1129
- });
1130
- server2.get("/_web/sessions/:id/fts/status", async (request) => {
1131
- const db = ensureDb2(dbManager2, request.params.id);
1132
- return { hasFtsIndex: hasFtsTable(db) };
1133
- });
1134
- server2.post("/_web/sessions/:id/fts/rebuild", async (request) => {
1135
- const db = ensureWritableDb(dbManager2, request.params.id);
1136
- const result = rebuildFtsIndex(db);
1137
- return { success: true, indexed: result.indexed };
1138
- });
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 aiDataDir = getAiDataDir(dbManager2);
1143
- const llmConfig = getDefaultAssistantConfig(aiDataDir);
1144
- if (!llmConfig) {
1145
- return reply.code(400).send({ error: "No LLM configuration available" });
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 aiDataDir = getAiDataDir(dbManager2);
1156
- const llmConfig = getDefaultAssistantConfig(aiDataDir);
1157
- if (!llmConfig) {
1158
- return reply.code(400).send({ error: "No LLM configuration available" });
1159
- }
1160
- const db = ensureWritableDb(dbManager2, sessionId);
1161
- const chatSessions = getChatSessionList(db);
1162
- const deps = buildSummaryDeps(db, llmConfig, aiDataDir);
1163
- let success = 0;
1164
- let failed = 0;
1165
- for (const cs of chatSessions) {
1166
- const result = await generateSessionSummary(deps, cs.id, { locale, forceRegenerate });
1167
- if (result.success) success++;
1168
- else failed++;
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 { success, failed, total: chatSessions.length };
1079
+ return result;
1171
1080
  });
1172
- const DEMO_BASE_URL = "https://chatlab.fun/assets/demo";
1173
- server2.post("/_web/demo/import", async (request, reply) => {
1174
- const locale = request.body?.locale || "en";
1175
- const nativeBinding = resolveNativeBinding();
1176
- reply.raw.writeHead(200, {
1177
- "Content-Type": "text/event-stream",
1178
- "Cache-Control": "no-cache",
1179
- Connection: "keep-alive"
1180
- });
1181
- function sendEvent(event, data) {
1182
- reply.raw.write(`event: ${event}
1183
- data: ${JSON.stringify(data)}
1081
+ }
1184
1082
 
1185
- `);
1186
- }
1187
- const tmpDir = fs3.mkdtempSync(path3.join(os.tmpdir(), "chatlab-demo-"));
1188
- const groupPath = path3.join(tmpDir, "demo-group.json");
1189
- const privatePath = path3.join(tmpDir, "demo-private.json");
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
- sendEvent("progress", { stage: "downloading", current: 1, total: 2 });
1192
- const groupResp = await fetch(`${DEMO_BASE_URL}/${locale}/demo-group.json`, {
1193
- signal: AbortSignal.timeout(6e4)
1194
- });
1195
- if (!groupResp.ok) throw new Error(`Download group demo failed: ${groupResp.status}`);
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
- reply.raw.end();
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 = fs3.mkdtempSync(path3.join(os.tmpdir(), "chatlab-detect-"));
1242
- const tmpPath = path3.join(tmpDir, data.filename || "upload");
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
- fs3.writeFileSync(tmpPath, Buffer.concat(chunks));
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
- try {
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 = fs3.mkdtempSync(path3.join(os.tmpdir(), "chatlab-scan-"));
1267
- const tmpPath = path3.join(tmpDir, data.filename || "upload");
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
- fs3.writeFileSync(tmpPath, Buffer.concat(chunks));
1133
+ fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
1274
1134
  const chats = await scanMultiChatFile(tmpPath);
1275
1135
  return { chats };
1276
1136
  } finally {
1277
- try {
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 = fs3.mkdtempSync(path3.join(os.tmpdir(), "chatlab-import-"));
1291
- const tmpPath = path3.join(tmpDir, data.filename || "upload");
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
- fs3.writeFileSync(tmpPath, Buffer.concat(chunks));
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, data2) {
1158
+ function sendEvent(event, eventData) {
1306
1159
  reply.raw.write(`event: ${event}
1307
- data: ${JSON.stringify(data2)}
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
- try {
1336
- fs3.unlinkSync(tmpPath);
1337
- } catch {
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
- try {
1340
- fs3.rmdirSync(tmpDir);
1341
- } catch {
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 = fs3.mkdtempSync(path3.join(os.tmpdir(), "chatlab-inc-"));
1350
- const tmpPath = path3.join(tmpDir, data.filename || "upload");
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
- fs3.writeFileSync(tmpPath, Buffer.concat(chunks));
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
- try {
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 = fs3.mkdtempSync(path3.join(os.tmpdir(), "chatlab-analyze-"));
1397
- const tmpPath = path3.join(tmpDir, data.filename || "upload");
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
- fs3.writeFileSync(tmpPath, Buffer.concat(chunks));
1312
+ fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
1403
1313
  try {
1404
- const result = await analyzeIncrementalImport(dbManager2, sessionId, tmpPath);
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
- try {
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 = fs3.mkdtempSync(path3.join(os.tmpdir(), "chatlab-analyze-"));
1423
- const tmpPath = path3.join(tmpDir, data.filename || "upload");
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
- fs3.writeFileSync(tmpPath, Buffer.concat(chunks));
1330
+ fs4.writeFileSync(tmpPath, Buffer.concat(chunks));
1429
1331
  try {
1430
- const result = await analyzeNewImport(tmpPath);
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
- fs3.unlinkSync(tmpPath);
1427
+ fs5.unlinkSync(tmpPath);
1437
1428
  } catch {
1438
1429
  }
1439
1430
  try {
1440
- fs3.rmdirSync(tmpDir);
1431
+ fs5.rmdirSync(tmpDir);
1441
1432
  } catch {
1442
1433
  }
1443
1434
  }
1444
1435
  });
1445
- if (mergeCache) {
1446
- server2.post("/_web/merge/parse", async (request, reply) => {
1447
- const data = await request.file();
1448
- if (!data) return reply.code(400).send({ error: "No file uploaded" });
1449
- const tmpDir = fs3.mkdtempSync(path3.join(os.tmpdir(), "chatlab-merge-"));
1450
- const tmpPath = path3.join(tmpDir, data.filename || "upload");
1451
- const chunks = [];
1452
- for await (const chunk of data.file) {
1453
- chunks.push(chunk);
1454
- }
1455
- fs3.writeFileSync(tmpPath, Buffer.concat(chunks));
1456
- try {
1457
- const result = await streamParseFileInfo(tmpPath, {
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
- if (!outputName) {
1510
- return reply.code(400).send({ error: "Missing outputName" });
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 readers = [];
1513
- try {
1514
- const dataSources = [];
1515
- for (const handle of handles) {
1516
- const entry = mergeCache.openReader(handle);
1517
- if (!entry) return reply.code(404).send({ error: `Handle not found: ${handle}` });
1518
- readers.push(entry.reader);
1519
- dataSources.push({ source: entry.reader.toDataSource(), filename: entry.filename });
1520
- }
1521
- const merged = buildMergedOutput(dataSources, outputName);
1522
- let sessionId;
1523
- if (andImport) {
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
- const importResult = await serverStreamImport(dbManager2, tmpPath);
1531
- sessionId = importResult.sessionId;
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
- for (const handle of handles) {
1544
- mergeCache.delete(handle);
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
- const handles = [];
1573
- for (const sid of sessionIds) {
1574
- const db = dbManager2.open(sid);
1575
- if (!db) return reply.code(404).send({ error: `Session not found: ${sid}` });
1576
- const exported = exportSessionToJson(db);
1577
- const { db: tempDb, tempDbPath } = mergeCache.createTempDatabase(exported.meta.name);
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, handles };
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
- const downloadsDir = options?.pathProvider?.getDownloadsDir() || path3.join(os.homedir(), "Downloads");
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 (!fs3.existsSync(downloadsDir)) {
1622
- fs3.mkdirSync(downloadsDir, { recursive: true });
1600
+ if (!fs6.existsSync(downloadsDir)) {
1601
+ fs6.mkdirSync(downloadsDir, { recursive: true });
1623
1602
  }
1624
- const filePath = path3.join(downloadsDir, filename);
1625
- fs3.writeFileSync(filePath, buffer);
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 = path3.dirname(filePath);
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 path4 from "path";
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 = path4.join(pathProvider.getUserDataDir(), "nlp");
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 path4.join(pathProvider.getUserDataDir(), "nlp");
1696
+ return path8.join(pathProvider.getUserDataDir(), "nlp");
1698
1697
  }
1699
1698
 
1700
1699
  // src/http/routes/ai.ts
1701
- import * as fs7 from "fs";
1702
- import * as path8 from "path";
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 fs4 from "fs";
1784
- import * as path5 from "path";
1781
+ import * as fs7 from "fs";
1782
+ import * as path9 from "path";
1785
1783
  function loadAssistantConfig(aiDataDir, assistantId) {
1786
- const filePath = path5.join(aiDataDir, "assistants", `${assistantId}.md`);
1787
- if (!fs4.existsSync(filePath)) return null;
1784
+ const filePath = path9.join(aiDataDir, "assistants", `${assistantId}.md`);
1785
+ if (!fs7.existsSync(filePath)) return null;
1788
1786
  try {
1789
- const content = fs4.readFileSync(filePath, "utf-8");
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 fs5 from "fs";
1798
- import * as path6 from "path";
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 (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
1800
+ if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
1803
1801
  },
1804
1802
  listFiles(dir, ext) {
1805
- if (!fs5.existsSync(dir)) return [];
1806
- return fs5.readdirSync(dir).filter((f) => f.endsWith(ext));
1803
+ if (!fs8.existsSync(dir)) return [];
1804
+ return fs8.readdirSync(dir).filter((f) => f.endsWith(ext));
1807
1805
  },
1808
1806
  readFile(filePath) {
1809
- return fs5.readFileSync(filePath, "utf-8");
1807
+ return fs8.readFileSync(filePath, "utf-8");
1810
1808
  },
1811
1809
  writeFile(filePath, content) {
1812
- fs5.writeFileSync(filePath, content, "utf-8");
1810
+ fs8.writeFileSync(filePath, content, "utf-8");
1813
1811
  },
1814
1812
  deleteFile(filePath) {
1815
- if (fs5.existsSync(filePath)) fs5.unlinkSync(filePath);
1813
+ if (fs8.existsSync(filePath)) fs8.unlinkSync(filePath);
1816
1814
  },
1817
1815
  fileExists(filePath) {
1818
- return fs5.existsSync(filePath);
1816
+ return fs8.existsSync(filePath);
1819
1817
  },
1820
1818
  joinPath(...parts) {
1821
- return path6.join(...parts);
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: path6.join(aiDataDir, "assistants"),
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: path6.join(aiDataDir, "skills"),
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 fs6 from "fs";
1980
- import * as path7 from "path";
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 = path7.join(aiDataDir, "custom-providers.json");
1984
- if (!fs6.existsSync(storePath)) return [];
1981
+ const storePath = path11.join(aiDataDir, "custom-providers.json");
1982
+ if (!fs9.existsSync(storePath)) return [];
1985
1983
  try {
1986
- const content = fs6.readFileSync(storePath, "utf-8");
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 = path7.join(aiDataDir, "custom-providers.json");
1994
- const dir = path7.dirname(storePath);
1995
- if (!fs6.existsSync(dir)) fs6.mkdirSync(dir, { recursive: true });
1996
- fs6.writeFileSync(storePath, JSON.stringify(providers, null, 2), "utf-8");
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 = path7.join(aiDataDir, "custom-models.json");
2028
- if (!fs6.existsSync(storePath)) return [];
2025
+ const storePath = path11.join(aiDataDir, "custom-models.json");
2026
+ if (!fs9.existsSync(storePath)) return [];
2029
2027
  try {
2030
- const content = fs6.readFileSync(storePath, "utf-8");
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 = path7.join(aiDataDir, "custom-models.json");
2038
- const dir = path7.dirname(storePath);
2039
- if (!fs6.existsSync(dir)) fs6.mkdirSync(dir, { recursive: true });
2040
- fs6.writeFileSync(storePath, JSON.stringify(models, null, 2), "utf-8");
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 = path8.join(getAiDir(dbManager2), "llm-config.json");
2155
- if (!fs7.existsSync(configPath)) return false;
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(fs7.readFileSync(configPath, "utf-8"));
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 = path8.join(getAiDir(dbManager2), "llm-config.json");
2166
- if (!fs7.existsSync(configPath)) return { configs: [], defaultAssistant: null, fastModel: null };
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(fs7.readFileSync(configPath, "utf-8"));
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 = path8.join(getAiDir(dbManager2), "custom-providers.json");
2192
+ const customPath = path12.join(getAiDir(dbManager2), "custom-providers.json");
2195
2193
  let custom = [];
2196
- if (fs7.existsSync(customPath)) {
2194
+ if (fs10.existsSync(customPath)) {
2197
2195
  try {
2198
- custom = JSON.parse(fs7.readFileSync(customPath, "utf-8"));
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 = path8.join(getAiDir(dbManager2), "custom-models.json");
2205
+ const customPath = path12.join(getAiDir(dbManager2), "custom-models.json");
2208
2206
  let custom = [];
2209
- if (fs7.existsSync(customPath)) {
2207
+ if (fs10.existsSync(customPath)) {
2210
2208
  try {
2211
- custom = JSON.parse(fs7.readFileSync(customPath, "utf-8"));
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 = path8.join(getAiDir(dbManager2), "llm-config.json");
2221
- if (!fs7.existsSync(configPath)) return null;
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(fs7.readFileSync(configPath, "utf-8"));
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 = path8.join(getAiDir(dbManager2), "llm-config.json");
2231
- if (!fs7.existsSync(configPath)) return null;
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(fs7.readFileSync(configPath, "utf-8"));
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 fs8 from "fs";
2587
- import * as path9 from "path";
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 path9.join(this.configDir, CONFIG_FILE);
2613
+ return path13.join(this.configDir, CONFIG_FILE);
2616
2614
  }
2617
2615
  ensureDir() {
2618
- fs8.mkdirSync(this.configDir, { recursive: true });
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 (fs8.existsSync(filePath)) {
2625
- const raw = fs8.readFileSync(filePath, "utf-8");
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
- fs8.writeFileSync(this.getConfigPath(), JSON.stringify(sources, null, 2), "utf-8");
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 fs9 from "fs";
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 = fs9.readFileSync(filePath, "utf-8");
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 = fs9.readFileSync(filePath, "utf-8");
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 (fs9.existsSync(filePath)) fs9.unlinkSync(filePath);
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 = fs9.statSync(tempFile);
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
- for (const sess of ds.sessions) {
2957
- await this.executePullSession(ds.id, ds, sess);
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
- const errors = [];
2970
- for (const sess of ds.sessions) {
2971
- const result = await this.executePullSession(sourceId, ds, sess);
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
- if (errors.length > 0) {
2975
- return { success: false, error: errors.join("; ") };
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 fs10 from "fs";
3052
- import * as os2 from "os";
3053
- import * as path10 from "path";
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 path10.join(os2.tmpdir(), `chatlab-pull-${id}${ext}`);
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
- fs10.writeFileSync(tempFile, buffer);
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 = path10.resolve(__dirname, "../../native/better_sqlite3.node");
3081
- if (fs10.existsSync(nativePath)) return nativePath;
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 fs10.existsSync(dbPath);
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 = path11.resolve(__dirname, "../../native/better_sqlite3.node");
3274
- if (fs11.existsSync(nativePath)) return nativePath;
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 && fs11.existsSync(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-PF4QFHC6.mjs.map
3400
+ //# sourceMappingURL=chunk-FBTHZU55.mjs.map