chatlab-cli 0.1.0 → 0.21.1

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