chatlab-cli 0.30.2 → 0.31.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 (141) hide show
  1. package/dist/{chat-command-VBDMWOKL.mjs → chat-command-MOETAGU5.mjs} +9 -8
  2. package/dist/{chat-command-VBDMWOKL.mjs.map → chat-command-MOETAGU5.mjs.map} +1 -1
  3. package/dist/{chunk-M2PXDX4G.mjs → chunk-2IXPWPLR.mjs} +168 -77
  4. package/dist/chunk-2IXPWPLR.mjs.map +1 -0
  5. package/dist/{chunk-TCZHG3G7.mjs → chunk-3VMKS7PU.mjs} +44 -5
  6. package/dist/chunk-3VMKS7PU.mjs.map +1 -0
  7. package/dist/{chunk-TEX44HNL.mjs → chunk-7HBVGVOB.mjs} +5 -15
  8. package/dist/chunk-7HBVGVOB.mjs.map +1 -0
  9. package/dist/{chunk-LVEJHB3X.mjs → chunk-DQ45LDKZ.mjs} +512 -98
  10. package/dist/chunk-DQ45LDKZ.mjs.map +1 -0
  11. package/dist/{chunk-ULNRUS55.mjs → chunk-E6W2RURS.mjs} +3 -3
  12. package/dist/{chunk-36JGKW6K.mjs → chunk-KCPGEILH.mjs} +1503 -530
  13. package/dist/chunk-KCPGEILH.mjs.map +1 -0
  14. package/dist/{chunk-WV5GSRQM.mjs → chunk-LJXSUN25.mjs} +5 -15
  15. package/dist/chunk-LJXSUN25.mjs.map +1 -0
  16. package/dist/chunk-N5AGDT7I.mjs +268 -0
  17. package/dist/chunk-N5AGDT7I.mjs.map +1 -0
  18. package/dist/{chunk-D3DPCDII.mjs → chunk-SZEBCNBB.mjs} +193 -79
  19. package/dist/chunk-SZEBCNBB.mjs.map +1 -0
  20. package/dist/{chunk-5EE2EFPE.mjs → chunk-VOVLPSG6.mjs} +29 -52
  21. package/dist/chunk-VOVLPSG6.mjs.map +1 -0
  22. package/dist/chunk-YAQLYL52.mjs +14 -0
  23. package/dist/chunk-YAQLYL52.mjs.map +1 -0
  24. package/dist/{chunk-326OCJOT.mjs → chunk-ZVWUR7ET.mjs} +2 -2
  25. package/dist/cli.mjs +10 -7
  26. package/dist/contacts-worker.mjs +2 -2
  27. package/dist/global-insight-worker.mjs +179 -0
  28. package/dist/global-insight-worker.mjs.map +1 -0
  29. package/dist/{http-L4AMELPZ.mjs → http-M6NHWJZD.mjs} +12 -11
  30. package/dist/{import-WWCSXGVS.mjs → import-SEYHFXS5.mjs} +12 -7
  31. package/dist/index.mjs +13 -12
  32. package/dist/{mcp-A4M3LT5O.mjs → mcp-MIPFYRLV.mjs} +12 -11
  33. package/dist/{mcp-A4M3LT5O.mjs.map → mcp-MIPFYRLV.mjs.map} +1 -1
  34. package/dist/people-relationships-worker.mjs +2 -2
  35. package/dist/semantic-index-worker.mjs +1 -1
  36. package/dist/{src-MPNBTFOE.mjs → src-7OD6RD5V.mjs} +38 -6
  37. package/dist/{stream-import-HIE7PNL4.mjs → stream-import-AWEHGQJY.mjs} +12 -7
  38. package/dist/{worker-runtime-BJXO24XX.mjs → worker-runtime-TGYMASIE.mjs} +6 -5
  39. package/dist/{worker-runtime-BJXO24XX.mjs.map → worker-runtime-TGYMASIE.mjs.map} +1 -1
  40. package/dist-web/assets/ActionToolsPanel-fAio70bI.css +1 -0
  41. package/dist-web/assets/ActionToolsPanel.vue_vue_type_script_setup_true_lang-CkDGVRXe.js +110 -0
  42. package/dist-web/assets/{BufferResource-q3rzaWWd.js → BufferResource-CHI62pIA.js} +1 -1
  43. package/dist-web/assets/{CanvasRenderer-BRk1y8tp.js → CanvasRenderer-OZRoiF0j.js} +1 -1
  44. package/dist-web/assets/{EChartWordcloud-Clg60FMo.js → EChartWordcloud-DU3ZYdgz.js} +1 -1
  45. package/dist-web/assets/{Filter-BJwode73.js → Filter-DNb5VErW.js} +1 -1
  46. package/dist-web/assets/{LoadingState.vue_vue_type_script_setup_true_lang-D_dJuLWJ.js → LoadingState.vue_vue_type_script_setup_true_lang-DACMroOe.js} +1 -1
  47. package/dist-web/assets/MessageExportModal-CqP-w7HP.js +1 -0
  48. package/dist-web/assets/PageHeader-Ck8ULyZH.js +1 -0
  49. package/dist-web/assets/PageHeader-XIXMTomn.css +1 -0
  50. package/dist-web/assets/{RelationshipGalaxyCanvas-CORMD7vL.js → RelationshipGalaxyCanvas-ioRNObb5.js} +3 -3
  51. package/dist-web/assets/{RelationshipGalaxyThreeCanvas-B_w_P4ob.js → RelationshipGalaxyThreeCanvas-L1Y43p1A.js} +1 -1
  52. package/dist-web/assets/{RenderTargetSystem-CGhV9R9E.js → RenderTargetSystem-fJ7D7Chv.js} +1 -1
  53. package/dist-web/assets/ThemeCard.vue_vue_type_script_setup_true_lang-BoZyssJF.js +1 -0
  54. package/dist-web/assets/TimeSelect.vue_vue_type_script_setup_true_lang-7SEaCxDL.js +1 -0
  55. package/dist-web/assets/{WebGLRenderer-CAokTE5X.js → WebGLRenderer-BD3ZUtxH.js} +1 -1
  56. package/dist-web/assets/{WebGPURenderer-DzP4ggSy.js → WebGPURenderer-DZXYsT6D.js} +1 -1
  57. package/dist-web/assets/annual-summary-time-range-DVLXK1r4.js +1 -0
  58. package/dist-web/assets/base-DTElkJcC.js +1 -0
  59. package/dist-web/assets/{browserAll-CAZCK6dd.js → browserAll-ChT6N6a9.js} +1 -1
  60. package/dist-web/assets/dateFormat-D9WibwOu.js +1 -0
  61. package/dist-web/assets/{electron-CkPDbAaS.js → electron-CIzyGxH_.js} +1 -1
  62. package/dist-web/assets/electron-hcWiB7Gg.js +1 -0
  63. package/dist-web/assets/fetch-Avtnh8cJ.js +3 -0
  64. package/dist-web/assets/{fetch-DQ9UNNAy.js → fetch-BFnrBahy.js} +1 -1
  65. package/dist-web/assets/fetch-C9rx2isC.js +31 -0
  66. package/dist-web/assets/fetch-Ccq_0ecb.js +1 -0
  67. package/dist-web/assets/{fetch-BIEXkvDq.js → fetch-CuiQUcO_.js} +1 -1
  68. package/dist-web/assets/{fetch-LX9jrJCC.js → fetch-CyKILLnB.js} +1 -1
  69. package/dist-web/assets/fetch-Dd6t3p1r.js +6 -0
  70. package/dist-web/assets/{fetch--UbRoBT7.js → fetch-DnGx-1_t.js} +1 -1
  71. package/dist-web/assets/{fetch-wqMSiaK3.js → fetch-qC7om4QG.js} +1 -1
  72. package/dist-web/assets/fetch-wSSfDBdV.js +1 -0
  73. package/dist-web/assets/index-BH9jI5kV.js +173 -0
  74. package/dist-web/assets/index-BWtNqcYK.js +22 -0
  75. package/dist-web/assets/index-BfO_LpDv.css +1 -0
  76. package/dist-web/assets/index-Bkq7-GBp.js +1 -0
  77. package/dist-web/assets/index-Bqr-LkS1.css +1 -0
  78. package/dist-web/assets/index-ByUaiVQK.css +1 -0
  79. package/dist-web/assets/index-C6qtsaP7.js +1 -0
  80. package/dist-web/assets/index-C_0GHEcb.css +1 -0
  81. package/dist-web/assets/index-ChFGuu70.js +1 -0
  82. package/dist-web/assets/{index-S-YQRH1E.js → index-CiS20iAR.js} +1 -1
  83. package/dist-web/assets/index-CqD6u57u.js +1 -0
  84. package/dist-web/assets/index-CzDG8LVr.js +2 -0
  85. package/dist-web/assets/index-D0Euk-vQ.js +1 -0
  86. package/dist-web/assets/index-D26BWhHz.js +1 -0
  87. package/dist-web/assets/index-DXHYzIIj.js +1 -0
  88. package/dist-web/assets/index-Dq8u7gMb.js +2 -0
  89. package/dist-web/assets/index-ZsZwPLWy.css +1 -0
  90. package/dist-web/assets/index-u_UH6rDY.js +64 -0
  91. package/dist-web/assets/index-y7cxH1Et.css +1 -0
  92. package/dist-web/assets/{people-page-header-Mupn-U_b.js → people-page-header-BdKM1qhX.js} +1 -1
  93. package/dist-web/assets/types-CZnACqaB.js +1 -0
  94. package/dist-web/assets/{vendor-echarts-hJVnODyU.js → vendor-echarts-B-i_56Av.js} +1 -1
  95. package/dist-web/assets/{vendor-echarts-wordcloud-k5ih2Jl4.js → vendor-echarts-wordcloud-Ca8n6qS6.js} +1 -1
  96. package/dist-web/assets/{vendor-nuxt-ui-DaLxYaC4.js → vendor-nuxt-ui-CS1uxE9C.js} +2 -2
  97. package/dist-web/assets/{vendor-reka-ui-CzM4kDQ7.js → vendor-reka-ui-Cta6JPvN.js} +1 -1
  98. package/dist-web/assets/{web-DbKs6-1b.js → web-BDBdpdK7.js} +1 -1
  99. package/dist-web/assets/{webworkerAll-Bhv-hBqR.js → webworkerAll-C3dAOQj4.js} +1 -1
  100. package/dist-web/index.html +4 -4
  101. package/package.json +1 -1
  102. package/dist/chunk-36JGKW6K.mjs.map +0 -1
  103. package/dist/chunk-5EE2EFPE.mjs.map +0 -1
  104. package/dist/chunk-D3DPCDII.mjs.map +0 -1
  105. package/dist/chunk-KBSAVUFV.mjs +0 -161
  106. package/dist/chunk-KBSAVUFV.mjs.map +0 -1
  107. package/dist/chunk-LVEJHB3X.mjs.map +0 -1
  108. package/dist/chunk-M2PXDX4G.mjs.map +0 -1
  109. package/dist/chunk-TCZHG3G7.mjs.map +0 -1
  110. package/dist/chunk-TEX44HNL.mjs.map +0 -1
  111. package/dist/chunk-WV5GSRQM.mjs.map +0 -1
  112. package/dist-web/assets/DebugToolsPanel-D_lFIW6j.css +0 -1
  113. package/dist-web/assets/DebugToolsPanel.vue_vue_type_script_setup_true_lang-F4n0zNX2.js +0 -110
  114. package/dist-web/assets/MessageExportModal-CGcdMOyw.js +0 -1
  115. package/dist-web/assets/PageHeader-Bbn87QZS.css +0 -1
  116. package/dist-web/assets/PageHeader-CZaqOdBQ.js +0 -1
  117. package/dist-web/assets/electron-D9vE5t3H.js +0 -1
  118. package/dist-web/assets/fetch-1pB-HbYw.js +0 -3
  119. package/dist-web/assets/fetch-Apd5OLJw.js +0 -5
  120. package/dist-web/assets/fetch-B8rik0TM.js +0 -17
  121. package/dist-web/assets/fetch-CF_3ZqRa.js +0 -1
  122. package/dist-web/assets/fetch-FSNxSnAp.js +0 -1
  123. package/dist-web/assets/index---rXG1hZ.css +0 -1
  124. package/dist-web/assets/index-B01uUoHE.js +0 -1
  125. package/dist-web/assets/index-B4l3rHie.css +0 -1
  126. package/dist-web/assets/index-BSIyyt0z.js +0 -1
  127. package/dist-web/assets/index-Bh8z441f.js +0 -2
  128. package/dist-web/assets/index-C0rya8v4.js +0 -1
  129. package/dist-web/assets/index-C1p9CbmN.css +0 -1
  130. package/dist-web/assets/index-COpza1A4.css +0 -1
  131. package/dist-web/assets/index-CZbZv2fh.js +0 -64
  132. package/dist-web/assets/index-CqUuHQBQ.js +0 -22
  133. package/dist-web/assets/index-CzeTP5MJ.js +0 -173
  134. package/dist-web/assets/index-DgNxTM_8.js +0 -2
  135. package/dist-web/assets/index-DosG7LmJ.css +0 -1
  136. /package/dist/{chunk-ULNRUS55.mjs.map → chunk-E6W2RURS.mjs.map} +0 -0
  137. /package/dist/{chunk-326OCJOT.mjs.map → chunk-ZVWUR7ET.mjs.map} +0 -0
  138. /package/dist/{http-L4AMELPZ.mjs.map → http-M6NHWJZD.mjs.map} +0 -0
  139. /package/dist/{import-WWCSXGVS.mjs.map → import-SEYHFXS5.mjs.map} +0 -0
  140. /package/dist/{src-MPNBTFOE.mjs.map → src-7OD6RD5V.mjs.map} +0 -0
  141. /package/dist/{stream-import-HIE7PNL4.mjs.map → stream-import-AWEHGQJY.mjs.map} +0 -0
@@ -9,7 +9,7 @@ import {
9
9
  createEmptyContactsDiagnostics,
10
10
  normalizeContactsTimeRangePreset,
11
11
  resolveContactsTimeRange
12
- } from "./chunk-TEX44HNL.mjs";
12
+ } from "./chunk-7HBVGVOB.mjs";
13
13
  import {
14
14
  PEOPLE_RELATIONSHIPS_ALGORITHM_VERSION,
15
15
  buildPeopleRelationshipsNeighborhoodGraph,
@@ -18,7 +18,10 @@ import {
18
18
  computePeopleRelationshipsSnapshot,
19
19
  normalizePeopleRelationshipsTimeRangePreset,
20
20
  resolvePeopleRelationshipsTimeRange
21
- } from "./chunk-WV5GSRQM.mjs";
21
+ } from "./chunk-LJXSUN25.mjs";
22
+ import {
23
+ ANNUAL_SUMMARY_ALGORITHM_VERSION
24
+ } from "./chunk-YAQLYL52.mjs";
22
25
  import {
23
26
  BUILTIN_MODELS,
24
27
  BUILTIN_PROVIDERS,
@@ -34,18 +37,23 @@ import {
34
37
  batchSegmentWithFrequency,
35
38
  buildExcludeKeywordsConditions,
36
39
  buildSessionInfo,
40
+ cleanupAnnualSummarySnapshotTempFiles,
37
41
  clearJiebaInstance,
38
42
  clearSessionIndex,
39
43
  collectPosTagStats,
44
+ deleteAnnualSummarySnapshots,
40
45
  deleteMember,
41
46
  generateIncrementalSessionIndex,
42
47
  generateMessageKey,
43
48
  generateSessionIndex,
49
+ getAnnualSummarySnapshotPath,
44
50
  getBuiltinModelsByProvider,
45
51
  getChatSessionList,
46
52
  getContactsDir,
47
53
  getContactsFactsCacheDir,
48
54
  getDbFileVersion,
55
+ getGlobalInsightDir,
56
+ getGlobalInsightFactsCacheDir,
49
57
  getJieba,
50
58
  getMemberNameHistory,
51
59
  getMembersPaginated,
@@ -67,20 +75,25 @@ import {
67
75
  mergeConfirmedNames,
68
76
  mergeMembers,
69
77
  needsStreamingThinkParsing,
78
+ normalizeAnnualSummaryRange,
70
79
  normalizeBuiltinToolNames,
71
80
  openBetterSqliteDatabase,
81
+ readAnnualSummarySnapshot,
72
82
  renameSession,
73
83
  resolveApiKey,
74
84
  saveSegmentSummary,
75
85
  segment,
76
86
  snapshotPathProvider,
87
+ toAnnualSummaryLocalDate,
88
+ toAnnualSummaryRangeKey,
77
89
  tokenizeForFts,
78
90
  tokenizeQueryForFts,
79
91
  truncateToolResultText,
80
92
  updateMemberAliases,
81
93
  updateSessionOwnerId,
94
+ writeAnnualSummarySnapshot,
82
95
  writeAuthProfile
83
- } from "./chunk-LVEJHB3X.mjs";
96
+ } from "./chunk-DQ45LDKZ.mjs";
84
97
  import {
85
98
  __commonJS,
86
99
  __export,
@@ -696,8 +709,8 @@ var require_FilterBase = __commonJS({
696
709
  var FilterBase = class _FilterBase extends Transform {
697
710
  static stringFilter(string, separator) {
698
711
  return (stack) => {
699
- const path30 = stack.join(separator);
700
- return path30.length === string.length && path30 === string || path30.length > string.length && path30.substr(0, string.length) === string && path30.substr(string.length, separator.length) === separator;
712
+ const path31 = stack.join(separator);
713
+ return path31.length === string.length && path31 === string || path31.length > string.length && path31.substr(0, string.length) === string && path31.substr(string.length, separator.length) === separator;
701
714
  };
702
715
  }
703
716
  static regExpFilter(regExp, separator) {
@@ -1151,12 +1164,12 @@ var require_Assembler = __commonJS({
1151
1164
  return (this.stack.length >> 1) + (this.done ? 0 : 1);
1152
1165
  }
1153
1166
  get path() {
1154
- const path30 = [];
1167
+ const path31 = [];
1155
1168
  for (let i = 0; i < this.stack.length; i += 2) {
1156
1169
  const key = this.stack[i + 1];
1157
- path30.push(key === null ? this.stack[i].length : key);
1170
+ path31.push(key === null ? this.stack[i].length : key);
1158
1171
  }
1159
- return path30;
1172
+ return path31;
1160
1173
  }
1161
1174
  dropToLevel(level) {
1162
1175
  if (level < this.depth) {
@@ -1438,7 +1451,7 @@ var require_pend = __commonJS({
1438
1451
  var require_fd_slicer = __commonJS({
1439
1452
  "../../packages/node-runtime/node_modules/yauzl/fd-slicer.js"(exports) {
1440
1453
  "use strict";
1441
- var fs33 = __require("fs");
1454
+ var fs35 = __require("fs");
1442
1455
  var util = __require("util");
1443
1456
  var stream = __require("stream");
1444
1457
  var Readable = stream.Readable;
@@ -1458,7 +1471,7 @@ var require_fd_slicer = __commonJS({
1458
1471
  FdSlicer.prototype.read = function(buffer, offset, length, position, callback) {
1459
1472
  var self = this;
1460
1473
  self.pend.go(function(cb) {
1461
- fs33.read(self.fd, buffer, offset, length, position, function(err, bytesRead, buffer2) {
1474
+ fs35.read(self.fd, buffer, offset, length, position, function(err, bytesRead, buffer2) {
1462
1475
  cb();
1463
1476
  callback(err, bytesRead, buffer2);
1464
1477
  });
@@ -1475,7 +1488,7 @@ var require_fd_slicer = __commonJS({
1475
1488
  self.refCount -= 1;
1476
1489
  if (self.refCount < 0) throw new Error("invalid unref");
1477
1490
  if (self.refCount > 0) return;
1478
- fs33.close(self.fd, onCloseDone);
1491
+ fs35.close(self.fd, onCloseDone);
1479
1492
  function onCloseDone(err) {
1480
1493
  if (err) {
1481
1494
  self.emit("error", err);
@@ -1507,7 +1520,7 @@ var require_fd_slicer = __commonJS({
1507
1520
  }
1508
1521
  self.context.pend.go(function(cb) {
1509
1522
  var buffer = Buffer.allocUnsafe(toRead);
1510
- fs33.read(self.context.fd, buffer, 0, toRead, self.pos, function(err, bytesRead) {
1523
+ fs35.read(self.context.fd, buffer, 0, toRead, self.pos, function(err, bytesRead) {
1511
1524
  if (err) {
1512
1525
  self.destroy(err);
1513
1526
  } else if (bytesRead === 0) {
@@ -1869,7 +1882,7 @@ var require_crc32 = __commonJS({
1869
1882
  var require_yauzl = __commonJS({
1870
1883
  "../../packages/node-runtime/node_modules/yauzl/index.js"(exports) {
1871
1884
  "use strict";
1872
- var fs33 = __require("fs");
1885
+ var fs35 = __require("fs");
1873
1886
  var zlib = __require("zlib");
1874
1887
  var fd_slicer = require_fd_slicer();
1875
1888
  var util = __require("util");
@@ -1894,9 +1907,9 @@ var require_yauzl = __commonJS({
1894
1907
  exports.Entry = Entry;
1895
1908
  exports.LocalFileHeader = LocalFileHeader;
1896
1909
  exports.RandomAccessReader = RandomAccessReader;
1897
- function openPromise2(path30, options2) {
1910
+ function openPromise2(path31, options2) {
1898
1911
  return new Promise((resolve2, reject) => {
1899
- open(path30, { ...options2, lazyEntries: true }, function(err, zipfile) {
1912
+ open(path31, { ...options2, lazyEntries: true }, function(err, zipfile) {
1900
1913
  if (err) return reject(err);
1901
1914
  resolve2(zipfile);
1902
1915
  });
@@ -1926,7 +1939,7 @@ var require_yauzl = __commonJS({
1926
1939
  });
1927
1940
  });
1928
1941
  }
1929
- function open(path30, options2, callback) {
1942
+ function open(path31, options2, callback) {
1930
1943
  if (typeof options2 === "function") {
1931
1944
  callback = options2;
1932
1945
  options2 = null;
@@ -1938,10 +1951,10 @@ var require_yauzl = __commonJS({
1938
1951
  if (options2.validateEntrySizes == null) options2.validateEntrySizes = true;
1939
1952
  if (options2.strictFileNames == null) options2.strictFileNames = false;
1940
1953
  if (callback == null) callback = defaultCallback;
1941
- fs33.open(path30, "r", function(err, fd) {
1954
+ fs35.open(path31, "r", function(err, fd) {
1942
1955
  if (err) return callback(err);
1943
1956
  fromFd(fd, options2, function(err2, zipfile) {
1944
- if (err2) fs33.close(fd, defaultCallback);
1957
+ if (err2) fs35.close(fd, defaultCallback);
1945
1958
  callback(err2, zipfile);
1946
1959
  });
1947
1960
  });
@@ -1958,7 +1971,7 @@ var require_yauzl = __commonJS({
1958
1971
  if (options2.validateEntrySizes == null) options2.validateEntrySizes = true;
1959
1972
  if (options2.strictFileNames == null) options2.strictFileNames = false;
1960
1973
  if (callback == null) callback = defaultCallback;
1961
- fs33.fstat(fd, function(err, stats) {
1974
+ fs35.fstat(fd, function(err, stats) {
1962
1975
  if (err) return callback(err);
1963
1976
  var reader = new fd_slicer.FdSlicer(fd);
1964
1977
  fromRandomAccessReader(reader, stats.size, options2, callback);
@@ -2743,7 +2756,7 @@ function writeParseResultToDb(db, meta, members, messages) {
2743
2756
  meta.ownerId || null
2744
2757
  );
2745
2758
  const insertMember = db.prepare(
2746
- `INSERT OR IGNORE INTO member (platform_id, account_name, group_nickname, avatar, roles) VALUES (?, ?, ?, ?, ?)`
2759
+ `INSERT OR IGNORE INTO member (platform_id, account_name, group_nickname, aliases, avatar, roles) VALUES (?, ?, ?, ?, ?, ?)`
2747
2760
  );
2748
2761
  const getMemberId = db.prepare(`SELECT id FROM member WHERE platform_id = ?`);
2749
2762
  const memberIdMap = /* @__PURE__ */ new Map();
@@ -2752,6 +2765,7 @@ function writeParseResultToDb(db, meta, members, messages) {
2752
2765
  member.platformId,
2753
2766
  member.accountName || null,
2754
2767
  member.groupNickname || null,
2768
+ member.aliases ? JSON.stringify(member.aliases) : "[]",
2755
2769
  member.avatar || null,
2756
2770
  member.roles ? JSON.stringify(member.roles) : "[]"
2757
2771
  );
@@ -3178,6 +3192,7 @@ function toParsedMember(member, fromHead) {
3178
3192
  platformId: member.platformId,
3179
3193
  accountName: member.accountName,
3180
3194
  groupNickname: member.groupNickname,
3195
+ aliases: member.aliases,
3181
3196
  avatar: member.avatar,
3182
3197
  // Role objects pass through with their original key set ({id} or {id, name}).
3183
3198
  roles: member.roles?.map((role) => role.name !== void 0 ? { id: role.id, name: role.name } : { id: role.id })
@@ -3193,7 +3208,9 @@ var chatlabAdapter = {
3193
3208
  platform: meta.platform,
3194
3209
  type: meta.chatType,
3195
3210
  groupId: meta.groupId ?? void 0,
3196
- groupAvatar: meta.groupAvatar ?? void 0
3211
+ groupAvatar: meta.groupAvatar ?? void 0,
3212
+ ownerId: meta.ownerId ?? void 0,
3213
+ sourceSessionId: meta.sourceSessionId ?? void 0
3197
3214
  };
3198
3215
  },
3199
3216
  mapMembers(members, metaJson) {
@@ -3285,7 +3302,9 @@ async function* parseChatLab(options2) {
3285
3302
  platform: metaObj.platform || KNOWN_PLATFORMS.UNKNOWN,
3286
3303
  type: metaObj.type || "group" /* GROUP */,
3287
3304
  groupId: metaObj.groupId,
3288
- groupAvatar: metaObj.groupAvatar
3305
+ groupAvatar: metaObj.groupAvatar,
3306
+ ownerId: metaObj.ownerId,
3307
+ sourceSessionId: metaObj.sourceSessionId
3289
3308
  };
3290
3309
  }
3291
3310
  }
@@ -3296,22 +3315,6 @@ async function* parseChatLab(options2) {
3296
3315
  }
3297
3316
  yield { type: "meta", data: meta };
3298
3317
  const members = [];
3299
- try {
3300
- const membersMatch = headContent.match(/"members"\s*:\s*\[([\s\S]*?)\]/);
3301
- if (membersMatch) {
3302
- const membersJson = JSON.parse(`[${membersMatch[1]}]`);
3303
- for (const m of membersJson) {
3304
- members.push({
3305
- platformId: m.platformId,
3306
- accountName: m.accountName,
3307
- groupNickname: m.groupNickname,
3308
- avatar: m.avatar,
3309
- roles: m.roles
3310
- });
3311
- }
3312
- }
3313
- } catch {
3314
- }
3315
3318
  const memberMapFromMessages = /* @__PURE__ */ new Map();
3316
3319
  const messageBatch = [];
3317
3320
  await new Promise((resolve2, reject) => {
@@ -3319,9 +3322,20 @@ async function* parseChatLab(options2) {
3319
3322
  readStream.on("data", (chunk) => {
3320
3323
  bytesRead += typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.length;
3321
3324
  });
3322
- const pipeline2 = chain([readStream, parser(), pick({ filter: /^messages\.\d+$/ }), streamValues()]);
3325
+ const pipeline2 = chain([readStream, parser(), pick({ filter: /^(?:members|messages)\.\d+$/ }), streamValues()]);
3323
3326
  const batchCollector = [];
3324
3327
  pipeline2.on("data", ({ value }) => {
3328
+ if (!("sender" in value)) {
3329
+ members.push({
3330
+ platformId: value.platformId,
3331
+ accountName: value.accountName,
3332
+ groupNickname: value.groupNickname,
3333
+ aliases: value.aliases,
3334
+ avatar: value.avatar,
3335
+ roles: value.roles
3336
+ });
3337
+ return;
3338
+ }
3325
3339
  const msg = value;
3326
3340
  if (members.length === 0) {
3327
3341
  memberMapFromMessages.set(msg.sender, {
@@ -3460,7 +3474,9 @@ async function* parseChatLabJsonl(options2) {
3460
3474
  platform: parsed.meta.platform || KNOWN_PLATFORMS.UNKNOWN,
3461
3475
  type: parsed.meta.type || "group" /* GROUP */,
3462
3476
  groupId: parsed.meta.groupId,
3463
- groupAvatar: parsed.meta.groupAvatar
3477
+ groupAvatar: parsed.meta.groupAvatar,
3478
+ ownerId: parsed.meta.ownerId,
3479
+ sourceSessionId: parsed.meta.sourceSessionId
3464
3480
  };
3465
3481
  headerParsed = true;
3466
3482
  yield { type: "meta", data: meta };
@@ -3865,6 +3881,9 @@ function extractNameFromFilePath3(filePath) {
3865
3881
  const basename13 = path5.basename(filePath);
3866
3882
  return basename13.replace(/\.json$/i, "") || "\u672A\u77E5\u7FA4\u804A";
3867
3883
  }
3884
+ function isPlaceholderSenderId(id) {
3885
+ return id === "0" || id === "\u672A\u77E5";
3886
+ }
3868
3887
  function countSenders(content) {
3869
3888
  const sendersStart = content.indexOf('"senders"');
3870
3889
  if (sendersStart === -1) return 0;
@@ -3879,8 +3898,19 @@ function countSenders(content) {
3879
3898
  }
3880
3899
  if (depth !== 0) return 0;
3881
3900
  const sendersContent = content.slice(arrayStart + 1, i - 1);
3882
- const uidMatches = sendersContent.match(/"uid"\s*:/g);
3883
- return uidMatches ? uidMatches.length : 0;
3901
+ const uidMatches = sendersContent.matchAll(/"uid"\s*:\s*"([^"]*)"/g);
3902
+ let count = 0;
3903
+ for (const match of uidMatches) {
3904
+ const uid = match[1];
3905
+ if (uid && !isPlaceholderSenderId(uid)) count++;
3906
+ }
3907
+ return count;
3908
+ }
3909
+ function resolveChatType(chatInfoType, headContent) {
3910
+ if (chatInfoType === "group") return "group" /* GROUP */;
3911
+ const sendersCount = countSenders(headContent);
3912
+ if (chatInfoType === "private") return sendersCount > 2 ? "group" /* GROUP */ : "private" /* PRIVATE */;
3913
+ return sendersCount > 2 ? "group" /* GROUP */ : sendersCount > 0 ? "private" /* PRIVATE */ : "group" /* GROUP */;
3884
3914
  }
3885
3915
  function convertMessageType(messageType, content, isRecalled) {
3886
3916
  if (isRecalled) return 81 /* RECALL */;
@@ -3934,8 +3964,7 @@ async function* parseV4(options2) {
3934
3964
  }
3935
3965
  } catch {
3936
3966
  }
3937
- const sendersCount = countSenders(headContent);
3938
- const chatType = sendersCount > 2 ? "group" /* GROUP */ : sendersCount > 0 ? "private" /* PRIVATE */ : "group" /* GROUP */;
3967
+ const chatType = resolveChatType(chatInfo.type, headContent);
3939
3968
  const meta = {
3940
3969
  name: chatInfo.name || extractNameFromFilePath3(filePath),
3941
3970
  platform: KNOWN_PLATFORMS.QQ,
@@ -3954,7 +3983,7 @@ async function* parseV4(options2) {
3954
3983
  const pipeline2 = chain3([readStream, parser4(), pick3({ filter: /^messages\.\d+$/ }), streamValues3()]);
3955
3984
  pipeline2.on("data", ({ value }) => {
3956
3985
  const platformId = value.sender.uin || value.sender.uid || value.rawMessage?.senderUin || value.rawMessage?.senderUid;
3957
- if (!platformId) {
3986
+ if (!platformId || isPlaceholderSenderId(platformId)) {
3958
3987
  skippedMessages++;
3959
3988
  return;
3960
3989
  }
@@ -3973,9 +4002,11 @@ async function* parseV4(options2) {
3973
4002
  skippedMessages++;
3974
4003
  return;
3975
4004
  }
3976
- const type = value.isSystemMessage ? 80 /* SYSTEM */ : convertMessageType(value.messageType, value.content, value.isRecalled);
4005
+ const isSystem = value.system ?? value.isSystemMessage;
4006
+ const isRecalled = value.recalled ?? value.isRecalled;
4007
+ const type = isSystem ? 80 /* SYSTEM */ : convertMessageType(value.messageType, value.content, isRecalled);
3977
4008
  let textContent = value.content?.text || "";
3978
- if (value.isRecalled) textContent = "[\u5DF2\u64A4\u56DE] " + textContent;
4009
+ if (isRecalled) textContent = "[\u5DF2\u64A4\u56DE] " + textContent;
3979
4010
  messageBatch.push({
3980
4011
  platformMessageId: value.messageId,
3981
4012
  // 消息的平台原始 ID
@@ -5502,6 +5533,14 @@ function extractNameFromFilePath5(filePath) {
5502
5533
  if (matchZhTw) return matchZhTw[1].trim();
5503
5534
  return basename13.replace(/\.txt$/i, "") || "\u672A\u77E5\u804A\u5929";
5504
5535
  }
5536
+ function shouldInferPrivateChatName(filePath, fallbackName) {
5537
+ const basename13 = path8.basename(filePath);
5538
+ return /^_chat\.txt$/i.test(basename13) || /^whatsapp(?:[-_\s].*)?$/i.test(fallbackName);
5539
+ }
5540
+ function inferPrivateChatName(memberMap, fallbackName) {
5541
+ const member = Array.from(memberMap.values()).find((item) => item.platformId !== "system");
5542
+ return member?.nickname || fallbackName;
5543
+ }
5505
5544
  var feature10 = {
5506
5545
  id: "whatsapp-native-txt",
5507
5546
  name: "WhatsApp \u5B98\u65B9\u5BFC\u51FA (TXT)",
@@ -5520,9 +5559,9 @@ var feature10 = {
5520
5559
  // 英文加密提示(WhatsApp 独有)
5521
5560
  /你发送给自己的消息已进行端到端加密/,
5522
5561
  // 简体中文自己对话提示(WhatsApp 独有)
5523
- /\d{1,4}\/\d{1,2}\/\d{2,4},?\s+\d{1,2}:\d{2} - /,
5562
+ /\d{1,4}\/\d{1,2}\/\d{2,4},?\s+\d{1,2}:\d{2}(?::\d{2})?(?:[\s\u2009\u202F]*(?:AM|PM|A\.M\.|P\.M\.|上午|下午|午前|午後|오전|오후))?\s+- /i,
5524
5563
  // 消息行格式特征(无方括号,含 " - " 分隔符,WhatsApp 独有)
5525
- /\[\d{1,4}\/\d{1,2}\/\d{2,4}[\s,].*\d{1,2}:\d{2}:\d{2}.*\] /
5564
+ /\[\d{1,4}\/\d{1,2}\/\d{1,4}[\s,].*\d{1,2}:\d{2}(?::\d{2})?.*\] /
5526
5565
  // 消息行格式特征(方括号 + 含日期和时间的时间戳,兼容各地区变体)
5527
5566
  ],
5528
5567
  // 文件名特征:与xxx的 WhatsApp 聊天.txt
@@ -5532,9 +5571,9 @@ var feature10 = {
5532
5571
  function cleanLine(line) {
5533
5572
  return line.replace(/^(?:\uFEFF|\u200E|\u200F|\u200B|\u200C|\u200D|\u2060)+/, "").trim();
5534
5573
  }
5535
- var MESSAGE_LINE_REGEX_V1 = /^(\d{1,4}\/\d{1,2}\/\d{2,4},?\s+\d{1,2}:\d{2}) - (.+)$/;
5574
+ var MESSAGE_LINE_REGEX_V1 = /^(\d{1,4}\/\d{1,2}\/\d{2,4},?\s+\d{1,2}:\d{2}(?::\d{2})?(?:[\s\u2009\u202F]*(?:AM|PM|A\.M\.|P\.M\.|上午|下午|午前|午後|오전|오후))?) - (.+)$/i;
5536
5575
  var MESSAGE_LINE_REGEX_V2 = /^\[([^\]]+)\] (.+)$/;
5537
- var SENDER_CONTENT_REGEX = /^(.+?):[\s\u200E]+(.*)$/;
5576
+ var SENDER_CONTENT_REGEX = /^(.+?):[\s\u200E]*(.*)$/;
5538
5577
  var SYSTEM_MESSAGE_PATTERNS = [
5539
5578
  // 简体中文系统消息
5540
5579
  /消息和通话已进行端到端加密/,
@@ -5586,12 +5625,12 @@ function detectMessageType2(content) {
5586
5625
  }
5587
5626
  var AMPM_MARKERS = [
5588
5627
  [/\bPM\b/i, true],
5589
- [/\bP\.M\.\b/i, true],
5628
+ [/\bP\.M\.(?!\w)/i, true],
5590
5629
  [/下午/, true],
5591
5630
  [/午後/, true],
5592
5631
  [/오후/, true],
5593
5632
  [/\bAM\b/i, false],
5594
- [/\bA\.M\.\b/i, false],
5633
+ [/\bA\.M\.(?!\w)/i, false],
5595
5634
  [/上午/, false],
5596
5635
  [/午前/, false],
5597
5636
  [/오전/, false]
@@ -5607,7 +5646,7 @@ function parseFlexibleV2Timestamp(raw) {
5607
5646
  }
5608
5647
  }
5609
5648
  str = str.replace(/,/g, " ").replace(/\s+/g, " ").trim();
5610
- const match = str.match(/^(\d{1,4}\/\d{1,2}\/\d{2,4})\s+(\d{1,2}:\d{2}(?::\d{2})?)$/);
5649
+ const match = str.match(/^(\d{1,4}\/\d{1,2}\/\d{1,4})\s+(\d{1,2}:\d{2}(?::\d{2})?)$/);
5611
5650
  if (!match) return null;
5612
5651
  const [, datePart, timePart] = match;
5613
5652
  const dateParts = datePart.split("/").map((s) => parseInt(s, 10));
@@ -5636,32 +5675,7 @@ function parseFlexibleV2Timestamp(raw) {
5636
5675
  return isNaN(ts) ? null : ts;
5637
5676
  }
5638
5677
  function parseV1Timestamp(timeStr) {
5639
- const str = timeStr.replace(/,/g, "").replace(/\s+/g, " ").trim();
5640
- const match = str.match(/^(\d{1,4})\/(\d{1,2})\/(\d{2,4})\s+(\d{1,2}):(\d{2})$/);
5641
- if (!match) {
5642
- const normalized = timeStr.replace(/,/g, "").replace(/\//g, "-").replace(/\s+/, "T") + ":00";
5643
- return Math.floor(new Date(normalized).getTime() / 1e3);
5644
- }
5645
- const parts = [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10)];
5646
- const hour = parseInt(match[4], 10);
5647
- const minute = parseInt(match[5], 10);
5648
- let year, month, day;
5649
- if (parts[0] > 31) {
5650
- year = parts[0];
5651
- month = parts[1];
5652
- day = parts[2];
5653
- } else if (parts[2] > 31) {
5654
- day = parts[0];
5655
- month = parts[1];
5656
- year = parts[2];
5657
- } else {
5658
- month = parts[0];
5659
- day = parts[1];
5660
- year = 2e3 + parts[2];
5661
- }
5662
- const date = new Date(year, month - 1, day, hour, minute, 0);
5663
- const ts = Math.floor(date.getTime() / 1e3);
5664
- return isNaN(ts) ? 0 : ts;
5678
+ return parseFlexibleV2Timestamp(timeStr) ?? 0;
5665
5679
  }
5666
5680
  async function* parseWhatsApp(options2) {
5667
5681
  const { filePath, batchSize = 5e3, onProgress, onLog } = options2;
@@ -5673,7 +5687,7 @@ async function* parseWhatsApp(options2) {
5673
5687
  yield { type: "progress", data: initialProgress };
5674
5688
  onProgress?.(initialProgress);
5675
5689
  onLog?.("info", `\u5F00\u59CB\u89E3\u6790 WhatsApp TXT \u6587\u4EF6\uFF0C\u5927\u5C0F: ${(totalBytes / 1024 / 1024).toFixed(2)} MB`);
5676
- const chatName = extractNameFromFilePath5(filePath);
5690
+ const fileChatName = extractNameFromFilePath5(filePath);
5677
5691
  const memberMap = /* @__PURE__ */ new Map();
5678
5692
  const messages = [];
5679
5693
  let currentMessage = null;
@@ -5772,6 +5786,7 @@ async function* parseWhatsApp(options2) {
5772
5786
  const hasSystemMember = memberMap.has("system");
5773
5787
  const realMemberCount = hasSystemMember ? memberMap.size - 1 : memberMap.size;
5774
5788
  const chatType = realMemberCount > 2 ? "group" /* GROUP */ : "private" /* PRIVATE */;
5789
+ const chatName = chatType === "private" /* PRIVATE */ && shouldInferPrivateChatName(filePath, fileChatName) ? inferPrivateChatName(memberMap, fileChatName) : fileChatName;
5775
5790
  const meta = {
5776
5791
  name: chatName,
5777
5792
  platform: KNOWN_PLATFORMS.WHATSAPP,
@@ -7410,6 +7425,35 @@ function searchByFts(db, keywords, limit = 1e3, offset = 0) {
7410
7425
 
7411
7426
  // ../../packages/node-runtime/src/import/streaming-importer.ts
7412
7427
  import * as fs20 from "fs";
7428
+
7429
+ // ../../packages/node-runtime/src/import/message-deduplicator.ts
7430
+ function generateFallbackMessageKey(message) {
7431
+ const contentKey = generateMessageKey(message.timestamp, message.senderPlatformId, message.content);
7432
+ return JSON.stringify([contentKey, message.type, message.replyToMessageId || null]);
7433
+ }
7434
+ function createMessageDedupState(platformMessageIds = [], fallbackKeys = [], fallbackOnlyKeys = []) {
7435
+ return {
7436
+ platformMessageIds: new Set(platformMessageIds),
7437
+ fallbackKeys: new Set(fallbackKeys),
7438
+ fallbackOnlyKeys: new Set(fallbackOnlyKeys)
7439
+ };
7440
+ }
7441
+ function registerMessageAndCheckDuplicate(message, state) {
7442
+ const key = generateFallbackMessageKey(message);
7443
+ if (message.platformMessageId) {
7444
+ if (state.platformMessageIds.has(message.platformMessageId)) return true;
7445
+ state.platformMessageIds.add(message.platformMessageId);
7446
+ if (state.fallbackOnlyKeys.delete(key)) return true;
7447
+ state.fallbackKeys.add(key);
7448
+ return false;
7449
+ }
7450
+ if (state.fallbackKeys.has(key)) return true;
7451
+ state.fallbackKeys.add(key);
7452
+ state.fallbackOnlyKeys.add(key);
7453
+ return false;
7454
+ }
7455
+
7456
+ // ../../packages/node-runtime/src/import/streaming-importer.ts
7413
7457
  var BATCH_COMMIT_SIZE = 5e4;
7414
7458
  var CHECKPOINT_INTERVAL = 2e5;
7415
7459
  var yieldToEventLoop = () => new Promise((resolve2) => setImmediate(resolve2));
@@ -7418,6 +7462,28 @@ function defaultGenerateSessionId() {
7418
7462
  const rand = Math.random().toString(36).substring(2, 8);
7419
7463
  return `chat_${ts}_${rand}`;
7420
7464
  }
7465
+ function prepareMessageForCreate(message) {
7466
+ if (!message.senderPlatformId) return { skipCounter: "skippedNoSenderId" };
7467
+ if (!message.senderAccountName) return { skipCounter: "skippedNoAccountName" };
7468
+ if (message.timestamp === void 0 || message.timestamp === null || isNaN(message.timestamp)) {
7469
+ return { skipCounter: "skippedInvalidTimestamp" };
7470
+ }
7471
+ if (message.type === void 0 || message.type === null) return { skipCounter: "skippedNoType" };
7472
+ let content = null;
7473
+ if (message.content != null) {
7474
+ content = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
7475
+ }
7476
+ return {
7477
+ message: {
7478
+ platformMessageId: message.platformMessageId,
7479
+ timestamp: message.timestamp,
7480
+ senderPlatformId: message.senderPlatformId,
7481
+ type: message.type,
7482
+ content,
7483
+ replyToMessageId: message.replyToMessageId
7484
+ }
7485
+ };
7486
+ }
7421
7487
  async function streamingImport(filePath, deps, formatOptions, externalSessionId) {
7422
7488
  if (formatOptions?.formatId) {
7423
7489
  const formatId = formatOptions.formatId;
@@ -7494,11 +7560,12 @@ async function streamImportSingle(filePath, deps, formatFeature, formatOptions,
7494
7560
  `INSERT INTO meta (name, platform, type, imported_at, group_id, group_avatar, owner_id) VALUES (?, ?, ?, ?, ?, ?, ?)`
7495
7561
  );
7496
7562
  const insertMember = db.prepare(
7497
- `INSERT INTO member (platform_id, account_name, group_nickname, avatar, roles)
7498
- VALUES (?, ?, ?, ?, ?)
7563
+ `INSERT INTO member (platform_id, account_name, group_nickname, aliases, avatar, roles)
7564
+ VALUES (?, ?, ?, ?, ?, ?)
7499
7565
  ON CONFLICT(platform_id) DO UPDATE SET
7500
7566
  account_name = COALESCE(NULLIF(excluded.account_name, ''), account_name),
7501
7567
  group_nickname = COALESCE(NULLIF(excluded.group_nickname, ''), group_nickname),
7568
+ aliases = CASE WHEN excluded.aliases != '[]' THEN excluded.aliases ELSE aliases END,
7502
7569
  avatar = COALESCE(NULLIF(excluded.avatar, ''), avatar),
7503
7570
  roles = CASE WHEN excluded.roles != '[]' THEN excluded.roles ELSE roles END`
7504
7571
  );
@@ -7518,6 +7585,7 @@ async function streamImportSingle(filePath, deps, formatFeature, formatOptions,
7518
7585
  let metaInserted = false;
7519
7586
  let messageCountInBatch = 0;
7520
7587
  let totalMessageCount = 0;
7588
+ let duplicateCount = 0;
7521
7589
  let lastCheckpointCount = 0;
7522
7590
  let inTransaction = false;
7523
7591
  const beginTransaction = () => {
@@ -7569,6 +7637,7 @@ async function streamImportSingle(filePath, deps, formatFeature, formatOptions,
7569
7637
  skippedInvalidTimestamp: 0,
7570
7638
  skippedNoType: 0
7571
7639
  };
7640
+ const dedupState = createMessageDedupState();
7572
7641
  logger?.info("Starting streamParseFile...");
7573
7642
  try {
7574
7643
  await streamParseFile(
@@ -7613,6 +7682,7 @@ async function streamImportSingle(filePath, deps, formatFeature, formatOptions,
7613
7682
  member.platformId,
7614
7683
  member.accountName || null,
7615
7684
  member.groupNickname || null,
7685
+ member.aliases ? JSON.stringify(member.aliases) : "[]",
7616
7686
  member.avatar || null,
7617
7687
  member.roles ? JSON.stringify(member.roles) : "[]"
7618
7688
  );
@@ -7634,20 +7704,14 @@ async function streamImportSingle(filePath, deps, formatFeature, formatOptions,
7634
7704
  let memberInsertCount = 0;
7635
7705
  let nicknameChangeCount = 0;
7636
7706
  for (const msg of messages) {
7637
- if (!msg.senderPlatformId) {
7638
- callbackStats.skippedNoSenderId++;
7639
- continue;
7640
- }
7641
- if (!msg.senderAccountName) {
7642
- callbackStats.skippedNoAccountName++;
7643
- continue;
7644
- }
7645
- if (msg.timestamp === void 0 || msg.timestamp === null || isNaN(msg.timestamp)) {
7646
- callbackStats.skippedInvalidTimestamp++;
7707
+ const prepared = prepareMessageForCreate(msg);
7708
+ if ("skipCounter" in prepared) {
7709
+ callbackStats[prepared.skipCounter]++;
7647
7710
  continue;
7648
7711
  }
7649
- if (msg.type === void 0 || msg.type === null) {
7650
- callbackStats.skippedNoType++;
7712
+ const dedupMessage = prepared.message;
7713
+ if (registerMessageAndCheckDuplicate(dedupMessage, dedupState)) {
7714
+ duplicateCount++;
7651
7715
  continue;
7652
7716
  }
7653
7717
  let t0 = Date.now();
@@ -7656,6 +7720,7 @@ async function streamImportSingle(filePath, deps, formatFeature, formatOptions,
7656
7720
  msg.senderPlatformId,
7657
7721
  msg.senderAccountName || null,
7658
7722
  msg.senderGroupNickname || null,
7723
+ "[]",
7659
7724
  null,
7660
7725
  "[]"
7661
7726
  );
@@ -7669,18 +7734,14 @@ async function streamImportSingle(filePath, deps, formatFeature, formatOptions,
7669
7734
  }
7670
7735
  const senderId = memberIdMap.get(msg.senderPlatformId);
7671
7736
  if (senderId === void 0) continue;
7672
- let safeContent = null;
7673
- if (msg.content != null) {
7674
- safeContent = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
7675
- }
7676
7737
  t0 = Date.now();
7677
7738
  insertMessage.run(
7678
7739
  senderId,
7679
7740
  msg.senderAccountName || null,
7680
7741
  msg.senderGroupNickname || null,
7681
- msg.timestamp,
7682
- msg.type,
7683
- safeContent,
7742
+ dedupMessage.timestamp,
7743
+ dedupMessage.type,
7744
+ dedupMessage.content,
7684
7745
  msg.replyToMessageId || null,
7685
7746
  msg.platformMessageId || null
7686
7747
  );
@@ -7797,6 +7858,7 @@ async function streamImportSingle(filePath, deps, formatFeature, formatOptions,
7797
7858
  logger?.info(` invalid timestamp: ${callbackStats.skippedInvalidTimestamp}`);
7798
7859
  if (callbackStats.skippedNoType > 0) logger?.info(` missing type: ${callbackStats.skippedNoType}`);
7799
7860
  }
7861
+ if (duplicateCount > 0) logger?.info(`Duplicate messages skipped: ${duplicateCount}`);
7800
7862
  logger?.summary(totalMessageCount, memberIdMap.size);
7801
7863
  if (totalMessageCount === 0) {
7802
7864
  logger?.error(
@@ -7829,6 +7891,7 @@ async function streamImportSingle(filePath, deps, formatFeature, formatOptions,
7829
7891
  detectedFormat: formatFeature ? `${formatFeature.name} (${formatFeature.id})` : null,
7830
7892
  messagesReceived: callbackStats.totalMessagesReceived,
7831
7893
  messagesWritten: totalMessageCount,
7894
+ duplicateCount,
7832
7895
  messagesSkipped: callbackStats.skippedNoSenderId + callbackStats.skippedNoAccountName + callbackStats.skippedInvalidTimestamp + callbackStats.skippedNoType,
7833
7896
  skipReasons: {
7834
7897
  noSenderId: callbackStats.skippedNoSenderId,
@@ -7842,32 +7905,54 @@ async function streamImportSingle(filePath, deps, formatFeature, formatOptions,
7842
7905
  }
7843
7906
  return { success: true, sessionId, diagnostics };
7844
7907
  }
7845
- async function analyzeNewImport(filePath, onProgress) {
7846
- const formatFeature = detectFormat(filePath);
7908
+ async function analyzeNewImport(filePath, onProgress, options2) {
7909
+ const formatFeature = options2?.formatId ? getFormatFeatureById(options2.formatId) : detectFormat(filePath);
7847
7910
  if (!formatFeature) {
7848
- return { totalMessages: 0, totalMembers: 0, meta: null, error: "error.unrecognized_format" };
7911
+ return {
7912
+ totalMessages: 0,
7913
+ newMessageCount: 0,
7914
+ duplicateCount: 0,
7915
+ totalMembers: 0,
7916
+ meta: null,
7917
+ error: "error.unrecognized_format"
7918
+ };
7849
7919
  }
7850
7920
  let meta = null;
7851
7921
  const memberSet = /* @__PURE__ */ new Set();
7922
+ const dedupState = createMessageDedupState();
7852
7923
  let totalMessages = 0;
7853
- await streamParseFile(filePath, {
7854
- onMeta: (parsedMeta) => {
7855
- meta = { name: parsedMeta.name, platform: parsedMeta.platform, type: parsedMeta.type };
7856
- },
7857
- onMembers: (members) => {
7858
- for (const m of members) memberSet.add(m.platformId);
7859
- },
7860
- onProgress: (progress) => {
7861
- onProgress(progress);
7862
- },
7863
- onMessageBatch: (batch) => {
7864
- for (const msg of batch) {
7865
- totalMessages++;
7866
- if (!memberSet.has(msg.senderPlatformId)) memberSet.add(msg.senderPlatformId);
7924
+ let newMessageCount = 0;
7925
+ let duplicateCount = 0;
7926
+ await streamParseFile(
7927
+ filePath,
7928
+ {
7929
+ formatOptions: options2?.chatIndex === void 0 ? void 0 : { chatIndex: options2.chatIndex },
7930
+ onMeta: (parsedMeta) => {
7931
+ meta = { name: parsedMeta.name, platform: parsedMeta.platform, type: parsedMeta.type };
7932
+ },
7933
+ onMembers: (members) => {
7934
+ for (const m of members) memberSet.add(m.platformId);
7935
+ },
7936
+ onProgress: (progress) => {
7937
+ onProgress(progress);
7938
+ },
7939
+ onMessageBatch: (batch) => {
7940
+ for (const msg of batch) {
7941
+ totalMessages++;
7942
+ if (!memberSet.has(msg.senderPlatformId)) memberSet.add(msg.senderPlatformId);
7943
+ const prepared = prepareMessageForCreate(msg);
7944
+ if ("skipCounter" in prepared) continue;
7945
+ if (registerMessageAndCheckDuplicate(prepared.message, dedupState)) {
7946
+ duplicateCount++;
7947
+ } else {
7948
+ newMessageCount++;
7949
+ }
7950
+ }
7867
7951
  }
7868
- }
7869
- });
7870
- return { totalMessages, totalMembers: memberSet.size, meta };
7952
+ },
7953
+ options2?.formatId
7954
+ );
7955
+ return { totalMessages, newMessageCount, duplicateCount, totalMembers: memberSet.size, meta };
7871
7956
  }
7872
7957
  async function streamParseFileInfo(filePath, deps) {
7873
7958
  const formatFeature = detectFormat(filePath);
@@ -8028,37 +8113,43 @@ function countHistory(tracker) {
8028
8113
  function loadExistingDedup(db) {
8029
8114
  const existingPlatformMsgIds = /* @__PURE__ */ new Set();
8030
8115
  const existingKeys = /* @__PURE__ */ new Set();
8116
+ const existingFallbackOnlyKeys = /* @__PURE__ */ new Set();
8031
8117
  const pmidRows = db.prepare("SELECT platform_message_id FROM message WHERE platform_message_id IS NOT NULL").all();
8032
8118
  for (const row of pmidRows) {
8033
8119
  existingPlatformMsgIds.add(row.platform_message_id);
8034
8120
  }
8035
8121
  const hashRows = db.prepare(
8036
- `SELECT ts, m.platform_id as sender_platform_id, content
8122
+ `SELECT ts, m.platform_id as sender_platform_id, msg.type, content, reply_to_message_id,
8123
+ platform_message_id
8037
8124
  FROM message msg
8038
8125
  JOIN member m ON msg.sender_id = m.id`
8039
8126
  ).all();
8040
8127
  for (const row of hashRows) {
8041
- existingKeys.add(generateMessageKey(row.ts, row.sender_platform_id, row.content));
8128
+ const key = generateFallbackMessageKey({
8129
+ timestamp: row.ts,
8130
+ senderPlatformId: row.sender_platform_id,
8131
+ type: row.type,
8132
+ content: row.content,
8133
+ replyToMessageId: row.reply_to_message_id ?? void 0
8134
+ });
8135
+ existingKeys.add(key);
8136
+ if (!row.platform_message_id) existingFallbackOnlyKeys.add(key);
8042
8137
  }
8043
- return { existingPlatformMsgIds, existingKeys };
8138
+ return { existingPlatformMsgIds, existingKeys, existingFallbackOnlyKeys };
8044
8139
  }
8045
- function isDuplicate(msg, existingPlatformMsgIds, existingKeys) {
8046
- if (msg.platformMessageId) {
8047
- if (existingPlatformMsgIds.has(msg.platformMessageId)) return true;
8048
- existingPlatformMsgIds.add(msg.platformMessageId);
8049
- return false;
8050
- }
8051
- const key = generateMessageKey(msg.timestamp, msg.senderPlatformId, msg.content);
8052
- if (existingKeys.has(key)) return true;
8053
- existingKeys.add(key);
8054
- return false;
8140
+ function isDuplicate(msg, existingPlatformMsgIds, existingKeys, existingFallbackOnlyKeys) {
8141
+ return registerMessageAndCheckDuplicate(msg, {
8142
+ platformMessageIds: existingPlatformMsgIds,
8143
+ fallbackKeys: existingKeys,
8144
+ fallbackOnlyKeys: existingFallbackOnlyKeys
8145
+ });
8055
8146
  }
8056
- function normalizeTimestamp(timestamp) {
8147
+ function normalizeImportTimestamp(timestamp) {
8057
8148
  const value = typeof timestamp === "string" && timestamp.trim() !== "" ? Number(timestamp) : timestamp;
8058
8149
  return typeof value === "number" && value > 0 && Number.isFinite(value) ? value : null;
8059
8150
  }
8060
- async function analyzeIncrementalImport(sessionId, filePath, deps) {
8061
- const formatFeature = detectFormat(filePath);
8151
+ async function analyzeIncrementalImport(sessionId, filePath, deps, options2) {
8152
+ const formatFeature = options2?.formatId ? getFormatFeatureById(options2.formatId) : detectFormat(filePath);
8062
8153
  if (!formatFeature) {
8063
8154
  return { error: "error.unrecognized_format", newMessageCount: 0, duplicateCount: 0, totalInFile: 0 };
8064
8155
  }
@@ -8068,36 +8159,41 @@ async function analyzeIncrementalImport(sessionId, filePath, deps) {
8068
8159
  } catch {
8069
8160
  return { error: "error.session_not_found", newMessageCount: 0, duplicateCount: 0, totalInFile: 0 };
8070
8161
  }
8071
- const { existingPlatformMsgIds, existingKeys } = loadExistingDedup(db);
8162
+ const { existingPlatformMsgIds, existingKeys, existingFallbackOnlyKeys } = loadExistingDedup(db);
8072
8163
  db.close();
8073
8164
  let totalInFile = 0;
8074
8165
  let newMessageCount = 0;
8075
8166
  let duplicateCount = 0;
8076
- await streamParseFile(filePath, {
8077
- onMeta: () => {
8078
- },
8079
- onMembers: () => {
8080
- },
8081
- onProgress: (progress) => {
8082
- deps.onProgress(progress);
8083
- },
8084
- onMessageBatch: (batch) => {
8085
- for (const msg of batch) {
8086
- totalInFile++;
8087
- const timestamp = normalizeTimestamp(msg.timestamp);
8088
- if (timestamp === null) continue;
8089
- if (isDuplicate({ ...msg, timestamp }, existingPlatformMsgIds, existingKeys)) {
8090
- duplicateCount++;
8091
- } else {
8092
- newMessageCount++;
8167
+ await streamParseFile(
8168
+ filePath,
8169
+ {
8170
+ formatOptions: options2?.chatIndex === void 0 ? void 0 : { chatIndex: options2.chatIndex },
8171
+ onMeta: () => {
8172
+ },
8173
+ onMembers: () => {
8174
+ },
8175
+ onProgress: (progress) => {
8176
+ deps.onProgress(progress);
8177
+ },
8178
+ onMessageBatch: (batch) => {
8179
+ for (const msg of batch) {
8180
+ totalInFile++;
8181
+ const timestamp = normalizeImportTimestamp(msg.timestamp);
8182
+ if (timestamp === null) continue;
8183
+ if (isDuplicate({ ...msg, timestamp }, existingPlatformMsgIds, existingKeys, existingFallbackOnlyKeys)) {
8184
+ duplicateCount++;
8185
+ } else {
8186
+ newMessageCount++;
8187
+ }
8093
8188
  }
8094
8189
  }
8095
- }
8096
- });
8190
+ },
8191
+ options2?.formatId
8192
+ );
8097
8193
  return { newMessageCount, duplicateCount, totalInFile };
8098
8194
  }
8099
8195
  async function incrementalImport(sessionId, filePath, deps, options2) {
8100
- const formatFeature = detectFormat(filePath);
8196
+ const formatFeature = options2?.formatId ? getFormatFeatureById(options2.formatId) : detectFormat(filePath);
8101
8197
  if (!formatFeature) {
8102
8198
  return { success: false, newMessageCount: 0, error: "error.unrecognized_format" };
8103
8199
  }
@@ -8118,18 +8214,19 @@ async function incrementalImport(sessionId, filePath, deps, options2) {
8118
8214
  }
8119
8215
  };
8120
8216
  var trackError = trackError2;
8121
- const { existingPlatformMsgIds, existingKeys } = loadExistingDedup(db);
8217
+ const { existingPlatformMsgIds, existingKeys, existingFallbackOnlyKeys } = loadExistingDedup(db);
8122
8218
  const memberIdMap = /* @__PURE__ */ new Map();
8123
8219
  const existingMembers = db.prepare("SELECT id, platform_id FROM member").all();
8124
8220
  for (const m of existingMembers) {
8125
8221
  memberIdMap.set(m.platform_id, m.id);
8126
8222
  }
8127
8223
  const upsertMember = db.prepare(`
8128
- INSERT INTO member (platform_id, account_name, group_nickname, avatar, roles)
8129
- VALUES (?, ?, ?, ?, ?)
8224
+ INSERT INTO member (platform_id, account_name, group_nickname, aliases, avatar, roles)
8225
+ VALUES (?, ?, ?, ?, ?, ?)
8130
8226
  ON CONFLICT(platform_id) DO UPDATE SET
8131
8227
  account_name = COALESCE(NULLIF(excluded.account_name, ''), account_name),
8132
8228
  group_nickname = COALESCE(NULLIF(excluded.group_nickname, ''), group_nickname),
8229
+ aliases = CASE WHEN excluded.aliases != '[]' THEN excluded.aliases ELSE aliases END,
8133
8230
  avatar = COALESCE(NULLIF(excluded.avatar, ''), avatar),
8134
8231
  roles = CASE WHEN excluded.roles != '[]' THEN excluded.roles ELSE roles END
8135
8232
  `);
@@ -8165,106 +8262,112 @@ async function incrementalImport(sessionId, filePath, deps, options2) {
8165
8262
  const MAX_ERROR_SAMPLES = 5;
8166
8263
  const BATCH_SIZE2 = 5e3;
8167
8264
  const newFtsEntries = [];
8168
- await streamParseFile(filePath, {
8169
- onMeta: (meta) => {
8170
- if (metaUpdateMode === "none") return;
8171
- updateMeta.run(
8172
- meta.name || "",
8173
- meta.groupId || "",
8174
- meta.groupAvatar || "",
8175
- meta.ownerId || "",
8176
- Math.floor(Date.now() / 1e3)
8177
- );
8178
- metaUpdated = true;
8179
- },
8180
- onMembers: (members) => {
8181
- if (memberUpdateMode === "none") return;
8182
- for (const m of members) {
8183
- const existed = memberIdMap.has(m.platformId);
8184
- upsertMember.run(
8185
- m.platformId,
8186
- m.accountName || null,
8187
- m.groupNickname || null,
8188
- m.avatar || null,
8189
- m.roles ? JSON.stringify(m.roles) : "[]"
8265
+ await streamParseFile(
8266
+ filePath,
8267
+ {
8268
+ formatOptions: options2?.chatIndex === void 0 ? void 0 : { chatIndex: options2.chatIndex },
8269
+ onMeta: (meta) => {
8270
+ if (metaUpdateMode === "none") return;
8271
+ updateMeta.run(
8272
+ meta.name || "",
8273
+ meta.groupId || "",
8274
+ meta.groupAvatar || "",
8275
+ meta.ownerId || "",
8276
+ Math.floor(Date.now() / 1e3)
8190
8277
  );
8191
- if (!existed) {
8192
- const row = getMemberId.get(m.platformId);
8193
- if (row) memberIdMap.set(m.platformId, row.id);
8194
- membersAdded++;
8195
- } else {
8196
- membersUpdated++;
8197
- }
8198
- }
8199
- },
8200
- onProgress: (progress) => {
8201
- deps.onProgress(progress);
8202
- },
8203
- onMessageBatch: (batch) => {
8204
- for (const msg of batch) {
8205
- processedCount++;
8206
- if (!msg.senderPlatformId) {
8207
- trackError2(processedCount, "MISSING_SENDER", "sender field is empty");
8208
- continue;
8209
- }
8210
- if (msg.timestamp === void 0 || msg.timestamp === null) {
8211
- trackError2(processedCount, "MISSING_TIMESTAMP", "timestamp field is missing");
8212
- continue;
8213
- }
8214
- const timestamp = normalizeTimestamp(msg.timestamp);
8215
- if (timestamp === null) {
8216
- trackError2(processedCount, "INVALID_TIMESTAMP", `timestamp value: ${msg.timestamp}`);
8217
- continue;
8218
- }
8219
- if (isDuplicate({ ...msg, timestamp }, existingPlatformMsgIds, existingKeys)) {
8220
- duplicateCount++;
8221
- continue;
8278
+ metaUpdated = true;
8279
+ },
8280
+ onMembers: (members) => {
8281
+ if (memberUpdateMode === "none") return;
8282
+ for (const m of members) {
8283
+ const existed = memberIdMap.has(m.platformId);
8284
+ upsertMember.run(
8285
+ m.platformId,
8286
+ m.accountName || null,
8287
+ m.groupNickname || null,
8288
+ m.aliases ? JSON.stringify(m.aliases) : "[]",
8289
+ m.avatar || null,
8290
+ m.roles ? JSON.stringify(m.roles) : "[]"
8291
+ );
8292
+ if (!existed) {
8293
+ const row = getMemberId.get(m.platformId);
8294
+ if (row) memberIdMap.set(m.platformId, row.id);
8295
+ membersAdded++;
8296
+ } else {
8297
+ membersUpdated++;
8298
+ }
8222
8299
  }
8223
- let memberId = memberIdMap.get(msg.senderPlatformId);
8224
- if (!memberId) {
8225
- insertMemberMinimal.run(
8226
- msg.senderPlatformId,
8300
+ },
8301
+ onProgress: (progress) => {
8302
+ deps.onProgress(progress);
8303
+ },
8304
+ onMessageBatch: (batch) => {
8305
+ for (const msg of batch) {
8306
+ processedCount++;
8307
+ if (!msg.senderPlatformId) {
8308
+ trackError2(processedCount, "MISSING_SENDER", "sender field is empty");
8309
+ continue;
8310
+ }
8311
+ if (msg.timestamp === void 0 || msg.timestamp === null) {
8312
+ trackError2(processedCount, "MISSING_TIMESTAMP", "timestamp field is missing");
8313
+ continue;
8314
+ }
8315
+ const timestamp = normalizeImportTimestamp(msg.timestamp);
8316
+ if (timestamp === null) {
8317
+ trackError2(processedCount, "INVALID_TIMESTAMP", `timestamp value: ${msg.timestamp}`);
8318
+ continue;
8319
+ }
8320
+ if (isDuplicate({ ...msg, timestamp }, existingPlatformMsgIds, existingKeys, existingFallbackOnlyKeys)) {
8321
+ duplicateCount++;
8322
+ continue;
8323
+ }
8324
+ let memberId = memberIdMap.get(msg.senderPlatformId);
8325
+ if (!memberId) {
8326
+ insertMemberMinimal.run(
8327
+ msg.senderPlatformId,
8328
+ msg.senderAccountName || null,
8329
+ msg.senderGroupNickname || null,
8330
+ null
8331
+ );
8332
+ const row = getMemberId.get(msg.senderPlatformId);
8333
+ if (row) {
8334
+ memberId = row.id;
8335
+ memberIdMap.set(msg.senderPlatformId, memberId);
8336
+ membersAdded++;
8337
+ }
8338
+ }
8339
+ if (!memberId) continue;
8340
+ const msgResult = insertMessage.run(
8341
+ memberId,
8227
8342
  msg.senderAccountName || null,
8228
8343
  msg.senderGroupNickname || null,
8229
- null
8344
+ timestamp,
8345
+ msg.type,
8346
+ msg.content || null,
8347
+ msg.replyToMessageId || null,
8348
+ msg.platformMessageId || null
8230
8349
  );
8231
- const row = getMemberId.get(msg.senderPlatformId);
8232
- if (row) {
8233
- memberId = row.id;
8234
- memberIdMap.set(msg.senderPlatformId, memberId);
8235
- membersAdded++;
8236
- }
8350
+ newFtsEntries.push({
8351
+ id: Number(msgResult.lastInsertRowid ?? 0),
8352
+ content: msg.content || null
8353
+ });
8354
+ if (timestamp < minWrittenTs) minWrittenTs = timestamp;
8355
+ newMessageCount++;
8356
+ }
8357
+ if (processedCount % BATCH_SIZE2 === 0) {
8358
+ deps.onProgress({
8359
+ stage: "saving",
8360
+ bytesRead: 0,
8361
+ totalBytes: 0,
8362
+ messagesProcessed: processedCount,
8363
+ percentage: 50,
8364
+ message: `Processed ${processedCount}, added ${newMessageCount}`
8365
+ });
8237
8366
  }
8238
- if (!memberId) continue;
8239
- const msgResult = insertMessage.run(
8240
- memberId,
8241
- msg.senderAccountName || null,
8242
- msg.senderGroupNickname || null,
8243
- timestamp,
8244
- msg.type,
8245
- msg.content || null,
8246
- msg.replyToMessageId || null,
8247
- msg.platformMessageId || null
8248
- );
8249
- newFtsEntries.push({
8250
- id: Number(msgResult.lastInsertRowid ?? 0),
8251
- content: msg.content || null
8252
- });
8253
- if (timestamp < minWrittenTs) minWrittenTs = timestamp;
8254
- newMessageCount++;
8255
- }
8256
- if (processedCount % BATCH_SIZE2 === 0) {
8257
- deps.onProgress({
8258
- stage: "saving",
8259
- bytesRead: 0,
8260
- totalBytes: 0,
8261
- messagesProcessed: processedCount,
8262
- percentage: 50,
8263
- message: `Processed ${processedCount}, added ${newMessageCount}`
8264
- });
8265
8367
  }
8266
- }
8267
- });
8368
+ },
8369
+ options2?.formatId
8370
+ );
8268
8371
  db.exec("COMMIT");
8269
8372
  if (!metaUpdated) {
8270
8373
  db.prepare("UPDATE meta SET imported_at = ?").run(Math.floor(Date.now() / 1e3));
@@ -8340,56 +8443,408 @@ async function incrementalImport(sessionId, filePath, deps, options2) {
8340
8443
  }
8341
8444
  }
8342
8445
 
8343
- // ../../packages/node-runtime/src/import/archive/errors.ts
8344
- var ArchiveImportError = class extends Error {
8345
- constructor(code, message, options2) {
8346
- super(message, options2);
8347
- this.code = code;
8348
- this.name = "ArchiveImportError";
8349
- }
8350
- code;
8351
- };
8446
+ // ../../packages/node-runtime/src/import/session-id.ts
8447
+ var SAFE_IMPORT_SESSION_ID_RE = /^[A-Za-z0-9_@-][A-Za-z0-9._@-]{0,127}$/;
8448
+ function isValidImportSessionId(sessionId) {
8449
+ return SAFE_IMPORT_SESSION_ID_RE.test(sessionId) && !sessionId.includes("..");
8450
+ }
8352
8451
 
8353
- // ../../packages/node-runtime/src/import/archive/archive-reader.ts
8354
- var import_yauzl = __toESM(require_yauzl(), 1);
8355
- import * as fs21 from "fs";
8356
- import * as path16 from "path";
8357
- import { pipeline } from "stream/promises";
8358
- var DEFAULT_MAX_ENTRIES = 1e5;
8359
- var DEFAULT_MAX_ENTRY_BYTES = 20 * 1024 * 1024 * 1024;
8360
- var DEFAULT_MAX_COMPRESSION_RATIO = 1e4;
8361
- function validateArchiveEntryName(fileName) {
8362
- if (fileName.startsWith("/") || /^[A-Za-z]:\//.test(fileName) || fileName.includes("\\") || fileName.split("/").includes("..")) {
8363
- throw new ArchiveImportError("error.archive_unsafe_path", `Unsafe archive entry path: ${fileName}`);
8452
+ // ../../packages/node-runtime/src/import/auto-import-matcher.ts
8453
+ var MATCH_WINDOW_SIZE = 5;
8454
+ function buildPrivateIdentity(ownerId, memberIds) {
8455
+ if (!ownerId) return null;
8456
+ const ids = [...new Set(memberIds)].filter((id) => id && id.toLowerCase() !== "system").sort();
8457
+ return ids.length > 0 ? `${ownerId}\0${ids.join("\0")}` : null;
8458
+ }
8459
+ function isBusinessMessage(type, content) {
8460
+ return type !== 80 /* SYSTEM */ && type !== 81 /* RECALL */ && Boolean(content?.trim());
8461
+ }
8462
+ function addMessageWindow(recentKeys, windows, message) {
8463
+ if (!message.senderPlatformId || !isBusinessMessage(message.type, message.content)) return;
8464
+ const timestamp = normalizeImportTimestamp(message.timestamp);
8465
+ if (timestamp === null) return;
8466
+ recentKeys.push(generateMessageKey(timestamp, message.senderPlatformId, message.content));
8467
+ if (recentKeys.length > MATCH_WINDOW_SIZE) recentKeys.shift();
8468
+ if (recentKeys.length === MATCH_WINDOW_SIZE) windows.add(recentKeys.join(""));
8469
+ }
8470
+ async function resolveAutoImportTarget(filePath, deps, formatOptions) {
8471
+ let sourceMeta = null;
8472
+ const sourceMemberIds = /* @__PURE__ */ new Set();
8473
+ const sourceWindows = /* @__PURE__ */ new Set();
8474
+ const recentSourceKeys = [];
8475
+ const { formatId, ...parserOptions } = formatOptions ?? {};
8476
+ await streamParseFile(
8477
+ filePath,
8478
+ {
8479
+ formatOptions: parserOptions,
8480
+ onProgress: deps.onProgress ?? (() => {
8481
+ }),
8482
+ onMeta: (meta2) => {
8483
+ sourceMeta = meta2;
8484
+ },
8485
+ onMembers: (members) => {
8486
+ for (const member of members) sourceMemberIds.add(member.platformId);
8487
+ },
8488
+ onMessageBatch: (messages) => {
8489
+ for (const message of messages) {
8490
+ sourceMemberIds.add(message.senderPlatformId);
8491
+ addMessageWindow(recentSourceKeys, sourceWindows, message);
8492
+ }
8493
+ }
8494
+ },
8495
+ typeof formatId === "string" ? formatId : void 0
8496
+ );
8497
+ if (!sourceMeta) throw new Error("Import source did not provide metadata");
8498
+ const meta = sourceMeta;
8499
+ const privateIdentity = meta.type === "private" ? buildPrivateIdentity(meta.ownerId, sourceMemberIds) : null;
8500
+ const hasStableIdentity = Boolean(meta.groupId || privateIdentity);
8501
+ const stableMatches = [];
8502
+ const trailingMatches = [];
8503
+ for (const sessionId of deps.listSessionIds()) {
8504
+ let db = null;
8505
+ try {
8506
+ db = deps.openReadonly(sessionId);
8507
+ if (!isChatSessionDb(db)) continue;
8508
+ const candidate = getSessionMeta(db);
8509
+ if (candidate?.platform !== meta.platform || candidate.type !== meta.type) continue;
8510
+ const groupMatches = Boolean(meta.groupId && candidate.groupId === meta.groupId);
8511
+ const memberRows = privateIdentity ? db.prepare("SELECT platform_id FROM member WHERE LOWER(platform_id) != 'system'").all() : [];
8512
+ const candidatePrivateIdentity = privateIdentity ? buildPrivateIdentity(
8513
+ candidate.ownerId,
8514
+ memberRows.map((row) => row.platform_id)
8515
+ ) : null;
8516
+ if (groupMatches || privateIdentity && candidatePrivateIdentity === privateIdentity) {
8517
+ stableMatches.push(sessionId);
8518
+ }
8519
+ if (sourceWindows.size > 0) {
8520
+ const rows = db.prepare(
8521
+ `SELECT msg.ts, member.platform_id, msg.type, msg.content
8522
+ FROM message msg
8523
+ JOIN member ON member.id = msg.sender_id
8524
+ WHERE msg.type NOT IN (?, ?)
8525
+ AND NULLIF(TRIM(msg.content), '') IS NOT NULL
8526
+ ORDER BY msg.ts DESC, msg.id DESC
8527
+ LIMIT ?`
8528
+ ).all(80 /* SYSTEM */, 81 /* RECALL */, MATCH_WINDOW_SIZE);
8529
+ if (rows.length === MATCH_WINDOW_SIZE) {
8530
+ const signature = rows.reverse().map((row) => generateMessageKey(row.ts, row.platform_id, row.content)).join("");
8531
+ if (sourceWindows.has(signature)) trailingMatches.push(sessionId);
8532
+ }
8533
+ }
8534
+ } catch (error) {
8535
+ const detail = error instanceof Error ? error.message : String(error);
8536
+ throw new Error(`Failed to inspect import candidate ${JSON.stringify(sessionId)}: ${detail}`, {
8537
+ cause: error
8538
+ });
8539
+ } finally {
8540
+ db?.close();
8541
+ }
8542
+ }
8543
+ if (hasStableIdentity) {
8544
+ if (meta.sourceSessionId && stableMatches.includes(meta.sourceSessionId)) {
8545
+ return { action: "incremental", sessionId: meta.sourceSessionId, matchedBy: "source-session-id" };
8546
+ }
8547
+ if (stableMatches.length === 1) {
8548
+ return { action: "incremental", sessionId: stableMatches[0], matchedBy: "stable-id" };
8549
+ }
8364
8550
  }
8551
+ if (meta.sourceSessionId && trailingMatches.includes(meta.sourceSessionId)) {
8552
+ return { action: "incremental", sessionId: meta.sourceSessionId, matchedBy: "source-session-id" };
8553
+ }
8554
+ if (trailingMatches.length === 1) {
8555
+ return { action: "incremental", sessionId: trailingMatches[0], matchedBy: "trailing-messages" };
8556
+ }
8557
+ const ambiguous = stableMatches.length > 1 || trailingMatches.length > 1;
8558
+ return { action: "create", reason: ambiguous ? "ambiguous" : "no-match" };
8365
8559
  }
8366
- function mapArchiveError(error) {
8367
- if (error instanceof ArchiveImportError) return error;
8368
- const message = error instanceof Error ? error.message : String(error);
8369
- return new ArchiveImportError("error.archive_corrupt", `Invalid ZIP archive: ${message}`, {
8370
- cause: error
8371
- });
8560
+
8561
+ // ../../packages/node-runtime/src/import/auto-importer.ts
8562
+ async function planAutoImport(filePath, deps, options2) {
8563
+ if (options2.explicitSessionId) {
8564
+ if (!isValidImportSessionId(options2.explicitSessionId)) {
8565
+ throw new Error("sessionId contains invalid characters");
8566
+ }
8567
+ return deps.sessionExists(options2.explicitSessionId) ? { action: "incremental", sessionId: options2.explicitSessionId } : { action: "create", sessionId: options2.explicitSessionId };
8568
+ }
8569
+ const decision = await (deps.resolveTarget ?? resolveAutoImportTarget)(
8570
+ filePath,
8571
+ deps,
8572
+ options2.formatOptions
8573
+ );
8574
+ return decision.action === "incremental" ? { action: "incremental", sessionId: decision.sessionId, matchedBy: decision.matchedBy } : { action: "create", reason: decision.reason };
8372
8575
  }
8373
- function toSummary(entry) {
8576
+ function mapCreateResult(result, createReason) {
8577
+ if (!result.success || !result.sessionId) {
8578
+ return { success: false, error: result.error, diagnostics: result.diagnostics };
8579
+ }
8374
8580
  return {
8375
- name: entry.fileName,
8376
- compressedSize: entry.compressedSize,
8377
- uncompressedSize: entry.uncompressedSize,
8378
- compressionMethod: entry.compressionMethod,
8379
- isDirectory: entry.fileName.endsWith("/")
8581
+ success: true,
8582
+ sessionId: result.sessionId,
8583
+ importMode: "created",
8584
+ ...createReason ? { createReason } : {},
8585
+ newMessageCount: result.diagnostics?.messagesWritten ?? 0,
8586
+ duplicateCount: result.diagnostics?.duplicateCount ?? 0,
8587
+ batch: result.diagnostics ? {
8588
+ receivedCount: result.diagnostics.messagesReceived,
8589
+ writtenCount: result.diagnostics.messagesWritten,
8590
+ duplicateCount: result.diagnostics.duplicateCount
8591
+ } : void 0,
8592
+ diagnostics: result.diagnostics
8380
8593
  };
8381
8594
  }
8382
- function validateEntry(entry, options2) {
8383
- validateArchiveEntryName(entry.fileName);
8384
- if (entry.isEncrypted()) {
8385
- throw new ArchiveImportError("error.archive_encrypted", `Encrypted ZIP entry is not supported: ${entry.fileName}`);
8386
- }
8387
- if (!entry.canDecodeFileData() || entry.compressionMethod !== 0 && entry.compressionMethod !== 8) {
8388
- throw new ArchiveImportError(
8389
- "error.archive_unsupported",
8390
- `Unsupported ZIP compression method for entry: ${entry.fileName}`
8595
+ function mapIncrementalResult(sessionId, result, matchedBy) {
8596
+ if (!result.success) return { success: false, sessionId, error: result.error };
8597
+ return {
8598
+ success: true,
8599
+ sessionId,
8600
+ importMode: "incremental",
8601
+ ...matchedBy ? { matchedBy } : {},
8602
+ newMessageCount: result.newMessageCount,
8603
+ duplicateCount: result.batch?.duplicateCount ?? 0,
8604
+ batch: result.batch,
8605
+ session: result.session,
8606
+ updates: result.updates
8607
+ };
8608
+ }
8609
+ async function autoImportFile(filePath, deps, options2 = {}) {
8610
+ try {
8611
+ if (!options2.explicitSessionId) {
8612
+ appLogger.info("import", "Automatic session matching started", {
8613
+ candidateCount: deps.listSessionIds().length
8614
+ });
8615
+ }
8616
+ const plan = await planAutoImport(filePath, deps, options2);
8617
+ if (plan.action === "incremental") {
8618
+ const result2 = mapIncrementalResult(
8619
+ plan.sessionId,
8620
+ await deps.appendSession(plan.sessionId, filePath, options2.formatOptions),
8621
+ plan.matchedBy
8622
+ );
8623
+ appLogger.info("import", "Incremental import completed", {
8624
+ sessionId: plan.sessionId,
8625
+ matchedBy: plan.matchedBy,
8626
+ success: result2.success,
8627
+ newMessageCount: result2.newMessageCount,
8628
+ duplicateCount: result2.duplicateCount
8629
+ });
8630
+ return result2;
8631
+ }
8632
+ const result = mapCreateResult(
8633
+ await deps.createSession(filePath, options2.formatOptions, plan.sessionId),
8634
+ plan.reason
8391
8635
  );
8392
- }
8636
+ appLogger.info("import", "Import created a new session", {
8637
+ reason: plan.reason,
8638
+ sessionId: result.sessionId,
8639
+ success: result.success
8640
+ });
8641
+ return result;
8642
+ } catch (error) {
8643
+ appLogger.error("import", "Automatic import failed", error);
8644
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
8645
+ }
8646
+ }
8647
+ async function analyzeAutoImportFile(filePath, deps, options2 = {}) {
8648
+ try {
8649
+ const plan = await planAutoImport(filePath, deps, options2);
8650
+ if (plan.action === "incremental") {
8651
+ const analysis2 = await deps.analyzeAppendSession(plan.sessionId, filePath, options2.formatOptions);
8652
+ if (analysis2.error) return { success: false, error: analysis2.error };
8653
+ const result2 = {
8654
+ success: true,
8655
+ importMode: "incremental",
8656
+ sessionId: plan.sessionId,
8657
+ ...plan.matchedBy ? { matchedBy: plan.matchedBy } : {},
8658
+ totalMessageCount: analysis2.totalInFile,
8659
+ newMessageCount: analysis2.newMessageCount,
8660
+ duplicateCount: analysis2.duplicateCount
8661
+ };
8662
+ appLogger.info("import", "Incremental import analysis completed", {
8663
+ sessionId: plan.sessionId,
8664
+ matchedBy: plan.matchedBy,
8665
+ newMessageCount: analysis2.newMessageCount,
8666
+ duplicateCount: analysis2.duplicateCount
8667
+ });
8668
+ return result2;
8669
+ }
8670
+ const analysis = await deps.analyzeCreateSession(filePath, options2.formatOptions);
8671
+ if (analysis.error) return { success: false, error: analysis.error };
8672
+ const result = {
8673
+ success: true,
8674
+ importMode: "created",
8675
+ ...plan.sessionId ? { sessionId: plan.sessionId } : {},
8676
+ ...plan.reason ? { createReason: plan.reason } : {},
8677
+ totalMessageCount: analysis.totalMessages,
8678
+ newMessageCount: analysis.newMessageCount,
8679
+ duplicateCount: analysis.duplicateCount,
8680
+ totalMemberCount: analysis.totalMembers,
8681
+ meta: analysis.meta
8682
+ };
8683
+ appLogger.info("import", "New session import analysis completed", {
8684
+ reason: plan.reason,
8685
+ messageCount: analysis.totalMessages,
8686
+ memberCount: analysis.totalMembers
8687
+ });
8688
+ return result;
8689
+ } catch (error) {
8690
+ appLogger.error("import", "Automatic import analysis failed", error);
8691
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
8692
+ }
8693
+ }
8694
+
8695
+ // ../../packages/node-runtime/src/import/import-lock.ts
8696
+ import * as fs21 from "fs";
8697
+ import * as path16 from "path";
8698
+ import { randomUUID } from "crypto";
8699
+ var IMPORT_LOCK_FILENAME = ".chatlab-import.lock";
8700
+ var IMPORT_IN_PROGRESS_ERROR_KEY = "error.import_in_progress";
8701
+ var ImportInProgressError = class extends Error {
8702
+ code = "IMPORT_IN_PROGRESS";
8703
+ errorKey = IMPORT_IN_PROGRESS_ERROR_KEY;
8704
+ constructor() {
8705
+ super("Another import is already in progress");
8706
+ this.name = "ImportInProgressError";
8707
+ }
8708
+ };
8709
+ function parseOwner(lockPath) {
8710
+ try {
8711
+ const parsed = JSON.parse(fs21.readFileSync(lockPath, "utf-8"));
8712
+ if (typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0 || typeof parsed.token !== "string" || parsed.token.length === 0 || typeof parsed.startedAt !== "number") {
8713
+ return null;
8714
+ }
8715
+ return parsed;
8716
+ } catch {
8717
+ return null;
8718
+ }
8719
+ }
8720
+ function isProcessRunning(pid) {
8721
+ try {
8722
+ process.kill(pid, 0);
8723
+ return true;
8724
+ } catch (error) {
8725
+ return error.code !== "ESRCH";
8726
+ }
8727
+ }
8728
+ function removeStaleLock(lockPath) {
8729
+ const owner = parseOwner(lockPath);
8730
+ if (owner) {
8731
+ if (isProcessRunning(owner.pid)) return false;
8732
+ } else {
8733
+ try {
8734
+ if (Date.now() - fs21.statSync(lockPath).mtimeMs < 5e3) return false;
8735
+ } catch {
8736
+ return true;
8737
+ }
8738
+ }
8739
+ try {
8740
+ fs21.unlinkSync(lockPath);
8741
+ appLogger.warn("import-lock", "Removed stale import lock", { ownerPid: owner?.pid });
8742
+ return true;
8743
+ } catch {
8744
+ return false;
8745
+ }
8746
+ }
8747
+ function releaseLock(lockPath, fd, owner) {
8748
+ try {
8749
+ fs21.closeSync(fd);
8750
+ } catch (error) {
8751
+ appLogger.error("import-lock", "Failed to close import lock", error);
8752
+ }
8753
+ try {
8754
+ if (parseOwner(lockPath)?.token !== owner.token) {
8755
+ appLogger.warn("import-lock", "Import lock ownership changed before release");
8756
+ return;
8757
+ }
8758
+ fs21.unlinkSync(lockPath);
8759
+ appLogger.info("import-lock", "Import lock released");
8760
+ } catch (error) {
8761
+ appLogger.error("import-lock", "Failed to release import lock", error);
8762
+ }
8763
+ }
8764
+ async function withDataDirImportLock(userDataDir, task) {
8765
+ fs21.mkdirSync(userDataDir, { recursive: true });
8766
+ const lockPath = path16.join(userDataDir, IMPORT_LOCK_FILENAME);
8767
+ let fd = null;
8768
+ for (let attempt = 0; attempt < 2; attempt++) {
8769
+ try {
8770
+ fd = fs21.openSync(lockPath, "wx");
8771
+ break;
8772
+ } catch (error) {
8773
+ const code = error.code;
8774
+ if (code === "EEXIST" && attempt === 0 && removeStaleLock(lockPath)) continue;
8775
+ if (code === "EEXIST") {
8776
+ appLogger.warn("import-lock", "Rejected concurrent import");
8777
+ throw new ImportInProgressError();
8778
+ }
8779
+ throw error;
8780
+ }
8781
+ }
8782
+ if (fd === null) throw new ImportInProgressError();
8783
+ const owner = {
8784
+ pid: process.pid,
8785
+ token: randomUUID(),
8786
+ startedAt: Date.now()
8787
+ };
8788
+ try {
8789
+ fs21.writeFileSync(fd, JSON.stringify(owner), "utf-8");
8790
+ fs21.fsyncSync(fd);
8791
+ appLogger.info("import-lock", "Import lock acquired");
8792
+ return await task();
8793
+ } finally {
8794
+ releaseLock(lockPath, fd, owner);
8795
+ }
8796
+ }
8797
+
8798
+ // ../../packages/node-runtime/src/import/archive/errors.ts
8799
+ var ArchiveImportError = class extends Error {
8800
+ constructor(code, message, options2) {
8801
+ super(message, options2);
8802
+ this.code = code;
8803
+ this.name = "ArchiveImportError";
8804
+ }
8805
+ code;
8806
+ };
8807
+
8808
+ // ../../packages/node-runtime/src/import/archive/archive-reader.ts
8809
+ var import_yauzl = __toESM(require_yauzl(), 1);
8810
+ import * as fs22 from "fs";
8811
+ import * as path17 from "path";
8812
+ import { pipeline } from "stream/promises";
8813
+ var DEFAULT_MAX_ENTRIES = 1e5;
8814
+ var DEFAULT_MAX_ENTRY_BYTES = 20 * 1024 * 1024 * 1024;
8815
+ var DEFAULT_MAX_COMPRESSION_RATIO = 1e4;
8816
+ function validateArchiveEntryName(fileName) {
8817
+ if (fileName.startsWith("/") || /^[A-Za-z]:\//.test(fileName) || fileName.includes("\\") || fileName.split("/").includes("..")) {
8818
+ throw new ArchiveImportError("error.archive_unsafe_path", `Unsafe archive entry path: ${fileName}`);
8819
+ }
8820
+ }
8821
+ function mapArchiveError(error) {
8822
+ if (error instanceof ArchiveImportError) return error;
8823
+ const message = error instanceof Error ? error.message : String(error);
8824
+ return new ArchiveImportError("error.archive_corrupt", `Invalid ZIP archive: ${message}`, {
8825
+ cause: error
8826
+ });
8827
+ }
8828
+ function toSummary(entry) {
8829
+ return {
8830
+ name: entry.fileName,
8831
+ compressedSize: entry.compressedSize,
8832
+ uncompressedSize: entry.uncompressedSize,
8833
+ compressionMethod: entry.compressionMethod,
8834
+ isDirectory: entry.fileName.endsWith("/")
8835
+ };
8836
+ }
8837
+ function validateEntry(entry, options2) {
8838
+ validateArchiveEntryName(entry.fileName);
8839
+ if (entry.isEncrypted()) {
8840
+ throw new ArchiveImportError("error.archive_encrypted", `Encrypted ZIP entry is not supported: ${entry.fileName}`);
8841
+ }
8842
+ if (!entry.canDecodeFileData() || entry.compressionMethod !== 0 && entry.compressionMethod !== 8) {
8843
+ throw new ArchiveImportError(
8844
+ "error.archive_unsupported",
8845
+ `Unsupported ZIP compression method for entry: ${entry.fileName}`
8846
+ );
8847
+ }
8393
8848
  if (entry.uncompressedSize > options2.maxEntryBytes) {
8394
8849
  throw new ArchiveImportError("error.archive_limit_exceeded", `ZIP entry exceeds the size limit: ${entry.fileName}`);
8395
8850
  }
@@ -8472,8 +8927,8 @@ var ZipArchiveReader = class {
8472
8927
  await this.visitEntries(async (entry, openStream) => {
8473
8928
  const destination = targets.get(entry.name);
8474
8929
  if (!destination || entry.isDirectory) return;
8475
- fs21.mkdirSync(path16.dirname(destination), { recursive: true });
8476
- await pipeline(await openStream(), fs21.createWriteStream(destination, { flags: "wx" }));
8930
+ fs22.mkdirSync(path17.dirname(destination), { recursive: true });
8931
+ await pipeline(await openStream(), fs22.createWriteStream(destination, { flags: "wx" }));
8477
8932
  remaining.delete(entry.name);
8478
8933
  });
8479
8934
  if (remaining.size > 0) {
@@ -8490,8 +8945,8 @@ var import_stream_chain9 = __toESM(require_stream_chain(), 1);
8490
8945
  var import_stream_json9 = __toESM(require_stream_json(), 1);
8491
8946
  var import_Pick8 = __toESM(require_Pick(), 1);
8492
8947
  var import_StreamValues9 = __toESM(require_StreamValues(), 1);
8493
- import * as fs22 from "fs";
8494
- import * as path17 from "path";
8948
+ import * as fs23 from "fs";
8949
+ import * as path18 from "path";
8495
8950
  var { chain: chain9 } = import_stream_chain9.default;
8496
8951
  var { parser: parser10 } = import_stream_json9.default;
8497
8952
  var { pick: pick8 } = import_Pick8.default;
@@ -8620,16 +9075,16 @@ var GoogleChatTakeoutResolver = class {
8620
9075
  `Google Chat conversation is missing required files: ${chat.chatId}`
8621
9076
  );
8622
9077
  }
8623
- fs22.mkdirSync(targetDir, { recursive: true });
9078
+ fs23.mkdirSync(targetDir, { recursive: true });
8624
9079
  await reader.pipeEntries(
8625
9080
  /* @__PURE__ */ new Map([
8626
- [userInfoEntry, path17.join(targetDir, "user_info.json")],
8627
- [groupInfoEntry, path17.join(targetDir, "group_info.json")],
8628
- [messagesEntry, path17.join(targetDir, "messages.json")]
9081
+ [userInfoEntry, path18.join(targetDir, "user_info.json")],
9082
+ [groupInfoEntry, path18.join(targetDir, "group_info.json")],
9083
+ [messagesEntry, path18.join(targetDir, "messages.json")]
8629
9084
  ])
8630
9085
  );
8631
- const manifestPath = path17.join(targetDir, "google-chat-import.json");
8632
- fs22.writeFileSync(
9086
+ const manifestPath = path18.join(targetDir, "google-chat-import.json");
9087
+ fs23.writeFileSync(
8633
9088
  manifestPath,
8634
9089
  JSON.stringify(
8635
9090
  {
@@ -8652,9 +9107,9 @@ var GoogleChatTakeoutResolver = class {
8652
9107
  };
8653
9108
 
8654
9109
  // ../../packages/node-runtime/src/import/archive/source-manager.ts
8655
- import * as fs23 from "fs";
8656
- import * as path18 from "path";
8657
- import { randomUUID } from "crypto";
9110
+ import * as fs24 from "fs";
9111
+ import * as path19 from "path";
9112
+ import { randomUUID as randomUUID2 } from "crypto";
8658
9113
  var DEFAULT_TTL_MS = 30 * 60 * 1e3;
8659
9114
  var ArchiveImportSourceManager = class {
8660
9115
  records = /* @__PURE__ */ new Map();
@@ -8664,11 +9119,11 @@ var ArchiveImportSourceManager = class {
8664
9119
  now;
8665
9120
  resolvers;
8666
9121
  constructor(options2 = {}) {
8667
- this.tempRoot = options2.tempRoot ?? path18.join(process.cwd(), ".chatlab-import-sources");
9122
+ this.tempRoot = options2.tempRoot ?? path19.join(process.cwd(), ".chatlab-import-sources");
8668
9123
  this.ttlMs = options2.ttlMs ?? DEFAULT_TTL_MS;
8669
9124
  this.now = options2.now ?? Date.now;
8670
9125
  this.resolvers = options2.resolvers ?? [new GoogleChatTakeoutResolver()];
8671
- fs23.mkdirSync(this.tempRoot, { recursive: true });
9126
+ fs24.mkdirSync(this.tempRoot, { recursive: true });
8672
9127
  }
8673
9128
  prepareLocalArchive(archivePath) {
8674
9129
  return this.prepareArchive(archivePath, false);
@@ -8691,7 +9146,7 @@ var ArchiveImportSourceManager = class {
8691
9146
  const resolver = matches[0];
8692
9147
  const chats = await resolver.scan(reader);
8693
9148
  const createdAt = this.now();
8694
- const sourceId = randomUUID();
9149
+ const sourceId = randomUUID2();
8695
9150
  const descriptor = {
8696
9151
  sourceId,
8697
9152
  formatId: resolver.id,
@@ -8725,7 +9180,7 @@ var ArchiveImportSourceManager = class {
8725
9180
  `Archive conversation was not found: ${chatId}`
8726
9181
  );
8727
9182
  }
8728
- const materializedDir = fs23.mkdtempSync(path18.join(this.tempRoot, `materialized-${sourceId}-`));
9183
+ const materializedDir = fs24.mkdtempSync(path19.join(this.tempRoot, `materialized-${sourceId}-`));
8729
9184
  try {
8730
9185
  const reader = new ZipArchiveReader(record.archivePath);
8731
9186
  const materialized = await record.resolver.materialize(reader, chat, materializedDir);
@@ -8776,7 +9231,7 @@ var ArchiveImportSourceManager = class {
8776
9231
  }
8777
9232
  removePath(targetPath) {
8778
9233
  try {
8779
- fs23.rmSync(targetPath, { recursive: true, force: true });
9234
+ fs24.rmSync(targetPath, { recursive: true, force: true });
8780
9235
  } catch {
8781
9236
  }
8782
9237
  }
@@ -8846,12 +9301,12 @@ ${rawErrorLabel}: ${details}`;
8846
9301
  }
8847
9302
 
8848
9303
  // ../../packages/node-runtime/src/ai/assistant-parser.ts
8849
- import * as path19 from "path";
9304
+ import * as path20 from "path";
8850
9305
  import matter from "gray-matter";
8851
9306
  function parseAssistantFile(content, filePath) {
8852
9307
  try {
8853
9308
  const { data: fm, content: body } = matter(content);
8854
- const id = fm.id ?? path19.basename(filePath, ".md");
9309
+ const id = fm.id ?? path20.basename(filePath, ".md");
8855
9310
  const name = fm.name;
8856
9311
  if (!name) return null;
8857
9312
  return {
@@ -8930,8 +9385,8 @@ var AssistantManager = class {
8930
9385
  }
8931
9386
  // ==================== Init ====================
8932
9387
  init() {
8933
- const { fs: fs33, assistantsDir } = this.deps;
8934
- fs33.ensureDir(assistantsDir);
9388
+ const { fs: fs35, assistantsDir } = this.deps;
9389
+ fs35.ensureDir(assistantsDir);
8935
9390
  const generalCreated = this.ensureGeneralAssistants();
8936
9391
  this.loadAll();
8937
9392
  this.initialized = true;
@@ -8942,26 +9397,26 @@ var AssistantManager = class {
8942
9397
  return { total: this.cache.size, generalCreated };
8943
9398
  }
8944
9399
  ensureGeneralAssistants() {
8945
- const { fs: fs33, assistantsDir } = this.deps;
9400
+ const { fs: fs35, assistantsDir } = this.deps;
8946
9401
  let anyCreated = false;
8947
9402
  for (const id of this.generalIds) {
8948
9403
  const config = this.getBuiltinConfig(id);
8949
9404
  if (!config) continue;
8950
- const filePath = fs33.joinPath(assistantsDir, `${id}.md`);
8951
- if (fs33.fileExists(filePath)) continue;
8952
- fs33.writeFile(filePath, serializeAssistant({ ...config, builtinId: config.id }));
9405
+ const filePath = fs35.joinPath(assistantsDir, `${id}.md`);
9406
+ if (fs35.fileExists(filePath)) continue;
9407
+ fs35.writeFile(filePath, serializeAssistant({ ...config, builtinId: config.id }));
8953
9408
  anyCreated = true;
8954
9409
  }
8955
9410
  return anyCreated;
8956
9411
  }
8957
9412
  loadAll() {
8958
- const { fs: fs33, assistantsDir } = this.deps;
9413
+ const { fs: fs35, assistantsDir } = this.deps;
8959
9414
  this.cache.clear();
8960
- const files = fs33.listFiles(assistantsDir, ".md");
9415
+ const files = fs35.listFiles(assistantsDir, ".md");
8961
9416
  for (const file of files) {
8962
9417
  try {
8963
- const filePath = fs33.joinPath(assistantsDir, file);
8964
- const content = fs33.readFile(filePath);
9418
+ const filePath = fs35.joinPath(assistantsDir, file);
9419
+ const content = fs35.readFile(filePath);
8965
9420
  const config = parseAssistantFile(content, filePath);
8966
9421
  if (config) {
8967
9422
  this.cache.set(config.id, config);
@@ -9073,12 +9528,12 @@ var AssistantManager = class {
9073
9528
  };
9074
9529
 
9075
9530
  // ../../packages/node-runtime/src/ai/skill-parser.ts
9076
- import * as path20 from "path";
9531
+ import * as path21 from "path";
9077
9532
  import matter2 from "gray-matter";
9078
9533
  function parseSkillFile(content, filePath) {
9079
9534
  try {
9080
9535
  const { data: fm, content: prompt } = matter2(content);
9081
- const id = fm.id ?? path20.basename(filePath, ".md");
9536
+ const id = fm.id ?? path21.basename(filePath, ".md");
9082
9537
  const name = fm.name;
9083
9538
  if (!name) return null;
9084
9539
  return {
@@ -9097,7 +9552,7 @@ function parseSkillFile(content, filePath) {
9097
9552
  function extractSkillId(content, filePath) {
9098
9553
  try {
9099
9554
  const { data: fm } = matter2(content);
9100
- return fm.id ?? path20.basename(filePath, ".md");
9555
+ return fm.id ?? path21.basename(filePath, ".md");
9101
9556
  } catch {
9102
9557
  return null;
9103
9558
  }
@@ -9116,8 +9571,8 @@ function validateChatScope(raw) {
9116
9571
 
9117
9572
  // ../../packages/node-runtime/src/ai/chats.ts
9118
9573
  import Database from "better-sqlite3";
9119
- import * as fs24 from "fs";
9120
- import * as path21 from "path";
9574
+ import * as fs25 from "fs";
9575
+ import * as path22 from "path";
9121
9576
  var DEFAULT_GENERAL_ID = "general_cn";
9122
9577
  var defaultLogger = {
9123
9578
  warn(_category, message, extra) {
@@ -9136,14 +9591,14 @@ var AIChatManager = class {
9136
9591
  this.nativeBinding = options2?.nativeBinding;
9137
9592
  }
9138
9593
  ensureDir(dir) {
9139
- if (!fs24.existsSync(dir)) {
9140
- fs24.mkdirSync(dir, { recursive: true });
9594
+ if (!fs25.existsSync(dir)) {
9595
+ fs25.mkdirSync(dir, { recursive: true });
9141
9596
  }
9142
9597
  }
9143
9598
  getDb() {
9144
9599
  if (this.db) return this.db;
9145
9600
  this.ensureDir(this.aiDataDir);
9146
- const dbPath = path21.join(this.aiDataDir, "conversations.db");
9601
+ const dbPath = path22.join(this.aiDataDir, "conversations.db");
9147
9602
  this.db = this.nativeBinding ? new Database(dbPath, { nativeBinding: this.nativeBinding }) : new Database(dbPath);
9148
9603
  this.db.pragma("journal_mode = WAL");
9149
9604
  this.db.exec(`
@@ -9386,16 +9841,16 @@ var AIChatManager = class {
9386
9841
  if (allRows.length === 0) return [];
9387
9842
  const rowMap = new Map(allRows.map((row) => [row.id, row]));
9388
9843
  let currentId = leafMessageId ?? this.getActiveMessageId(aiChatId);
9389
- const path30 = [];
9844
+ const path31 = [];
9390
9845
  const seen = /* @__PURE__ */ new Set();
9391
9846
  while (currentId && !seen.has(currentId)) {
9392
9847
  seen.add(currentId);
9393
9848
  const row = rowMap.get(currentId);
9394
9849
  if (!row) break;
9395
- path30.push(row);
9850
+ path31.push(row);
9396
9851
  currentId = row.parentId;
9397
9852
  }
9398
- return path30.length > 0 ? path30.reverse() : allRows;
9853
+ return path31.length > 0 ? path31.reverse() : allRows;
9399
9854
  }
9400
9855
  // ==================== 生命周期 ====================
9401
9856
  close() {
@@ -9965,21 +10420,21 @@ var SkillManagerCore = class {
9965
10420
  }
9966
10421
  // ==================== Init ====================
9967
10422
  init() {
9968
- const { fs: fs33, skillsDir } = this.deps;
9969
- fs33.ensureDir(skillsDir);
10423
+ const { fs: fs35, skillsDir } = this.deps;
10424
+ fs35.ensureDir(skillsDir);
9970
10425
  this.loadAll();
9971
10426
  this.initialized = true;
9972
10427
  this.deps.logger?.info("SkillManager", "Initialized", { total: this.cache.size });
9973
10428
  return { total: this.cache.size };
9974
10429
  }
9975
10430
  loadAll() {
9976
- const { fs: fs33, skillsDir } = this.deps;
10431
+ const { fs: fs35, skillsDir } = this.deps;
9977
10432
  this.cache.clear();
9978
- const files = fs33.listFiles(skillsDir, ".md");
10433
+ const files = fs35.listFiles(skillsDir, ".md");
9979
10434
  for (const file of files) {
9980
10435
  try {
9981
- const filePath = fs33.joinPath(skillsDir, file);
9982
- const content = fs33.readFile(filePath);
10436
+ const filePath = fs35.joinPath(skillsDir, file);
10437
+ const content = fs35.readFile(filePath);
9983
10438
  const def = parseSkillFile(content, filePath);
9984
10439
  if (def) {
9985
10440
  this.cache.set(def.id, def);
@@ -10131,8 +10586,8 @@ var SkillManagerCore = class {
10131
10586
  };
10132
10587
 
10133
10588
  // ../../packages/node-runtime/src/ai/skill-manager.ts
10134
- import * as fs25 from "fs";
10135
- import * as path22 from "path";
10589
+ import * as fs26 from "fs";
10590
+ import * as path23 from "path";
10136
10591
  var SKILLS_DIR_NAME = "skills";
10137
10592
  var defaultLogger2 = {
10138
10593
  info: () => {
@@ -10142,26 +10597,26 @@ var defaultLogger2 = {
10142
10597
  };
10143
10598
  var nodeFs = {
10144
10599
  ensureDir(dir) {
10145
- fs25.mkdirSync(dir, { recursive: true });
10600
+ fs26.mkdirSync(dir, { recursive: true });
10146
10601
  },
10147
10602
  listFiles(dir, ext) {
10148
- if (!fs25.existsSync(dir)) return [];
10149
- return fs25.readdirSync(dir).filter((file) => file.endsWith(ext));
10603
+ if (!fs26.existsSync(dir)) return [];
10604
+ return fs26.readdirSync(dir).filter((file) => file.endsWith(ext));
10150
10605
  },
10151
10606
  readFile(filePath) {
10152
- return fs25.readFileSync(filePath, "utf-8");
10607
+ return fs26.readFileSync(filePath, "utf-8");
10153
10608
  },
10154
10609
  writeFile(filePath, content) {
10155
- fs25.writeFileSync(filePath, content, "utf-8");
10610
+ fs26.writeFileSync(filePath, content, "utf-8");
10156
10611
  },
10157
10612
  deleteFile(filePath) {
10158
- fs25.unlinkSync(filePath);
10613
+ fs26.unlinkSync(filePath);
10159
10614
  },
10160
10615
  fileExists(filePath) {
10161
- return fs25.existsSync(filePath);
10616
+ return fs26.existsSync(filePath);
10162
10617
  },
10163
10618
  joinPath(...parts) {
10164
- return path22.join(...parts);
10619
+ return path23.join(...parts);
10165
10620
  }
10166
10621
  };
10167
10622
  var SkillManager = class {
@@ -10171,7 +10626,7 @@ var SkillManager = class {
10171
10626
  this.logger = logger ?? defaultLogger2;
10172
10627
  this.core = new SkillManagerCore({
10173
10628
  fs: nodeFs,
10174
- skillsDir: path22.join(aiDataDir, SKILLS_DIR_NAME),
10629
+ skillsDir: path23.join(aiDataDir, SKILLS_DIR_NAME),
10175
10630
  builtinRawSkills: [],
10176
10631
  logger: {
10177
10632
  info: () => {
@@ -14001,9 +14456,9 @@ var localeMap = {
14001
14456
  "ja-JP": ja_JP_default,
14002
14457
  "zh-TW": zh_TW_default
14003
14458
  };
14004
- function getNestedValue(obj, path30) {
14459
+ function getNestedValue(obj, path31) {
14005
14460
  let current = obj;
14006
- for (const segment2 of path30.split(".")) {
14461
+ for (const segment2 of path31.split(".")) {
14007
14462
  if (current == null || typeof current !== "object") return void 0;
14008
14463
  current = current[segment2];
14009
14464
  }
@@ -15156,7 +15611,13 @@ function queryMessages(db, timeFilter) {
15156
15611
  const sql = `
15157
15612
  SELECT msg.ts,
15158
15613
  COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
15159
- msg.content
15614
+ m.platform_id as sender,
15615
+ COALESCE(msg.sender_account_name, m.account_name, m.platform_id) as accountName,
15616
+ COALESCE(msg.sender_group_nickname, m.group_nickname) as groupNickname,
15617
+ msg.type,
15618
+ msg.content,
15619
+ msg.platform_message_id as platformMessageId,
15620
+ msg.reply_to_message_id as replyToMessageId
15160
15621
  FROM message msg
15161
15622
  JOIN member m ON msg.sender_id = m.id
15162
15623
  ${hasFilter ? "WHERE msg.ts >= ? AND msg.ts <= ?" : ""}
@@ -15168,9 +15629,6 @@ function queryMessages(db, timeFilter) {
15168
15629
  }
15169
15630
  return db.prepare(sql).all(...params);
15170
15631
  }
15171
- function formatTime(ts) {
15172
- return new Date(ts * 1e3).toLocaleString();
15173
- }
15174
15632
  function formatTimeShort(ts) {
15175
15633
  return new Date(ts * 1e3).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
15176
15634
  }
@@ -15190,16 +15648,69 @@ function exportAsTxt(messages, sessionName) {
15190
15648
  }
15191
15649
  return lines.join("\n");
15192
15650
  }
15193
- function exportAsJson(messages, sessionName) {
15651
+ function parseOptionalArray(value) {
15652
+ if (!value) return void 0;
15653
+ try {
15654
+ const parsed = JSON.parse(value);
15655
+ return Array.isArray(parsed) && parsed.length > 0 ? parsed : void 0;
15656
+ } catch {
15657
+ return void 0;
15658
+ }
15659
+ }
15660
+ function exportAsJson(db, messages, sessionName, sourceSessionId) {
15661
+ const meta = db.prepare(
15662
+ `SELECT name,
15663
+ platform,
15664
+ type,
15665
+ group_id as groupId,
15666
+ group_avatar as groupAvatar,
15667
+ owner_id as ownerId
15668
+ FROM meta
15669
+ LIMIT 1`
15670
+ ).get();
15671
+ if (!meta) throw new Error("Cannot read session metadata");
15672
+ const members = db.prepare(
15673
+ `SELECT platform_id as platformId,
15674
+ account_name as accountName,
15675
+ group_nickname as groupNickname,
15676
+ aliases,
15677
+ avatar,
15678
+ roles
15679
+ FROM member
15680
+ ORDER BY id ASC`
15681
+ ).all();
15194
15682
  const data = {
15195
- sessionName,
15196
- exportTime: (/* @__PURE__ */ new Date()).toISOString(),
15197
- totalMessages: messages.length,
15683
+ chatlab: {
15684
+ version: "0.0.2",
15685
+ exportedAt: Math.floor(Date.now() / 1e3),
15686
+ generator: "ChatLab"
15687
+ },
15688
+ meta: {
15689
+ name: meta.name || sessionName,
15690
+ platform: meta.platform,
15691
+ type: meta.type,
15692
+ groupId: meta.groupId || void 0,
15693
+ groupAvatar: meta.groupAvatar || void 0,
15694
+ ownerId: meta.ownerId || void 0,
15695
+ sourceSessionId
15696
+ },
15697
+ members: members.map((member) => ({
15698
+ platformId: member.platformId,
15699
+ accountName: member.accountName || member.platformId,
15700
+ groupNickname: member.groupNickname || void 0,
15701
+ aliases: parseOptionalArray(member.aliases),
15702
+ avatar: member.avatar || void 0,
15703
+ roles: parseOptionalArray(member.roles)
15704
+ })),
15198
15705
  messages: messages.map((msg) => ({
15706
+ sender: msg.sender,
15707
+ accountName: msg.accountName,
15708
+ groupNickname: msg.groupNickname || void 0,
15199
15709
  timestamp: msg.ts,
15200
- time: formatTime(msg.ts),
15201
- sender: msg.senderName,
15202
- content: msg.content || ""
15710
+ type: msg.type,
15711
+ content: msg.content,
15712
+ platformMessageId: msg.platformMessageId || void 0,
15713
+ replyToMessageId: msg.replyToMessageId || void 0
15203
15714
  }))
15204
15715
  };
15205
15716
  return JSON.stringify(data, null, 2);
@@ -15250,7 +15761,7 @@ function exportWithFormat(params, openDatabase) {
15250
15761
  content = exportAsTxt(messages, params.sessionName);
15251
15762
  break;
15252
15763
  case "json":
15253
- content = exportAsJson(messages, params.sessionName);
15764
+ content = exportAsJson(db, messages, params.sessionName, params.sessionId);
15254
15765
  break;
15255
15766
  case "markdown":
15256
15767
  content = exportAsMarkdown(messages, params.sessionName);
@@ -15258,8 +15769,15 @@ function exportWithFormat(params, openDatabase) {
15258
15769
  }
15259
15770
  const timestamp = Date.now();
15260
15771
  const filename = `${params.sessionName}_export_${timestamp}.${ext}`;
15772
+ appLogger.info("export", "chat export generated", {
15773
+ sessionId: params.sessionId,
15774
+ format: params.format,
15775
+ totalMessages: messages.length,
15776
+ filteredByTime: !!params.timeFilter
15777
+ });
15261
15778
  return { success: true, totalMessages: messages.length, content, filename, mimeType: mime };
15262
15779
  } catch (error) {
15780
+ appLogger.error("export", "chat export failed", error);
15263
15781
  return {
15264
15782
  success: false,
15265
15783
  error: String(error),
@@ -15272,8 +15790,8 @@ function exportWithFormat(params, openDatabase) {
15272
15790
  }
15273
15791
 
15274
15792
  // ../../packages/node-runtime/src/merger/temp-db.ts
15275
- import * as fs26 from "fs";
15276
- import * as path23 from "path";
15793
+ import * as fs27 from "fs";
15794
+ import * as path24 from "path";
15277
15795
  var TEMP_DB_SCHEMA = `
15278
15796
  CREATE TABLE IF NOT EXISTS meta (
15279
15797
  name TEXT NOT NULL,
@@ -15518,21 +16036,21 @@ function exportSessionToJson(db) {
15518
16036
  }
15519
16037
  function deleteTempDatabase(dbPath) {
15520
16038
  try {
15521
- if (fs26.existsSync(dbPath)) fs26.unlinkSync(dbPath);
16039
+ if (fs27.existsSync(dbPath)) fs27.unlinkSync(dbPath);
15522
16040
  const walPath = dbPath + "-wal";
15523
16041
  const shmPath = dbPath + "-shm";
15524
- if (fs26.existsSync(walPath)) fs26.unlinkSync(walPath);
15525
- if (fs26.existsSync(shmPath)) fs26.unlinkSync(shmPath);
16042
+ if (fs27.existsSync(walPath)) fs27.unlinkSync(walPath);
16043
+ if (fs27.existsSync(shmPath)) fs27.unlinkSync(shmPath);
15526
16044
  } catch {
15527
16045
  }
15528
16046
  }
15529
16047
  function cleanupTempDatabases(tempDir) {
15530
16048
  try {
15531
- if (!fs26.existsSync(tempDir)) return;
15532
- const files = fs26.readdirSync(tempDir);
16049
+ if (!fs27.existsSync(tempDir)) return;
16050
+ const files = fs27.readdirSync(tempDir);
15533
16051
  for (const file of files) {
15534
16052
  if (file.startsWith("merge_") && file.endsWith(".db")) {
15535
- deleteTempDatabase(path23.join(tempDir, file));
16053
+ deleteTempDatabase(path24.join(tempDir, file));
15536
16054
  }
15537
16055
  }
15538
16056
  } catch {
@@ -15558,7 +16076,21 @@ function buildSession(db, id, dbPath, options2) {
15558
16076
  if (meta.type === "private") {
15559
16077
  memberAvatar = getPrivateChatMemberAvatar(db, meta.name, meta.ownerId);
15560
16078
  }
15561
- let dto = { ...info, id, dbPath, memberAvatar, aiConversationCount: 0 };
16079
+ const ownerMember = meta.ownerId ? db.prepare(
16080
+ `SELECT COALESCE(NULLIF(group_nickname, ''), NULLIF(account_name, ''), platform_id) as displayName
16081
+ FROM member
16082
+ WHERE platform_id = ?
16083
+ LIMIT 1`
16084
+ ).get(meta.ownerId) : void 0;
16085
+ let dto = {
16086
+ ...info,
16087
+ id,
16088
+ dbPath,
16089
+ memberAvatar,
16090
+ aiConversationCount: 0,
16091
+ ownerName: ownerMember?.displayName ?? null,
16092
+ ownerStatus: !meta.ownerId ? "missing" : ownerMember ? "resolved" : "unresolved"
16093
+ };
15562
16094
  if (options2?.enrichSession) {
15563
16095
  dto = options2.enrichSession(dto);
15564
16096
  }
@@ -15898,8 +16430,8 @@ function clearSessionOwner(adapter, sessionId) {
15898
16430
  }
15899
16431
 
15900
16432
  // ../../packages/node-runtime/src/services/contacts/overrides.ts
15901
- import fs27 from "fs";
15902
- import path24 from "path";
16433
+ import fs28 from "fs";
16434
+ import path25 from "path";
15903
16435
  var CONTACT_OVERRIDES_FILE_NAME = "contact-overrides.json";
15904
16436
  var CONTACT_OVERRIDES_TMP_PREFIX = "contact-overrides.tmp-";
15905
16437
  var CONTACT_OVERRIDES_VERSION = 1;
@@ -15910,23 +16442,23 @@ function createEmptyContactOverridesFile() {
15910
16442
  };
15911
16443
  }
15912
16444
  function getContactOverridesPath(snapshotDir) {
15913
- return path24.join(snapshotDir, CONTACT_OVERRIDES_FILE_NAME);
16445
+ return path25.join(snapshotDir, CONTACT_OVERRIDES_FILE_NAME);
15914
16446
  }
15915
16447
  function readContactOverrides(snapshotDir) {
15916
16448
  const filePath = getContactOverridesPath(snapshotDir);
15917
- if (!fs27.existsSync(filePath)) return createEmptyContactOverridesFile();
16449
+ if (!fs28.existsSync(filePath)) return createEmptyContactOverridesFile();
15918
16450
  try {
15919
- return normalizeContactOverrides(JSON.parse(fs27.readFileSync(filePath, "utf-8")));
16451
+ return normalizeContactOverrides(JSON.parse(fs28.readFileSync(filePath, "utf-8")));
15920
16452
  } catch (error) {
15921
16453
  appLogger.warn("contacts", "contact overrides file is unreadable", error);
15922
16454
  return createEmptyContactOverridesFile();
15923
16455
  }
15924
16456
  }
15925
16457
  function writeContactOverrides(snapshotDir, overrides) {
15926
- if (!fs27.existsSync(snapshotDir)) fs27.mkdirSync(snapshotDir, { recursive: true });
15927
- const tmpPath = path24.join(snapshotDir, `${CONTACT_OVERRIDES_TMP_PREFIX}${process.pid}-${Date.now()}`);
15928
- fs27.writeFileSync(tmpPath, JSON.stringify(normalizeContactOverrides(overrides), null, 2), "utf-8");
15929
- fs27.renameSync(tmpPath, getContactOverridesPath(snapshotDir));
16458
+ if (!fs28.existsSync(snapshotDir)) fs28.mkdirSync(snapshotDir, { recursive: true });
16459
+ const tmpPath = path25.join(snapshotDir, `${CONTACT_OVERRIDES_TMP_PREFIX}${process.pid}-${Date.now()}`);
16460
+ fs28.writeFileSync(tmpPath, JSON.stringify(normalizeContactOverrides(overrides), null, 2), "utf-8");
16461
+ fs28.renameSync(tmpPath, getContactOverridesPath(snapshotDir));
15930
16462
  }
15931
16463
  function normalizeContactOverrides(value) {
15932
16464
  const input = isRecord2(value) ? value : {};
@@ -15961,23 +16493,23 @@ function buildContactsSignature(adapter, timeRangePreset) {
15961
16493
  }
15962
16494
 
15963
16495
  // ../../packages/node-runtime/src/services/contacts/snapshot.ts
15964
- import fs28 from "fs";
15965
- import path25 from "path";
16496
+ import fs29 from "fs";
16497
+ import path26 from "path";
15966
16498
  var CONTACTS_SNAPSHOT_TMP_PREFIX = "contacts-snapshot.tmp-";
15967
16499
  function getContactsSnapshotPath(snapshotDir, timeRangePreset) {
15968
16500
  const preset = normalizeContactsTimeRangePreset(timeRangePreset);
15969
- return path25.join(snapshotDir, `contacts-snapshot-${preset}.json`);
16501
+ return path26.join(snapshotDir, `contacts-snapshot-${preset}.json`);
15970
16502
  }
15971
16503
  function readContactsSnapshot(snapshotDir, timeRangePreset, options2 = {}) {
15972
16504
  const snapshotPath = getContactsSnapshotPath(snapshotDir, timeRangePreset);
15973
- if (!fs28.existsSync(snapshotPath)) return null;
16505
+ if (!fs29.existsSync(snapshotPath)) return null;
15974
16506
  try {
15975
- return JSON.parse(fs28.readFileSync(snapshotPath, "utf-8"));
16507
+ return JSON.parse(fs29.readFileSync(snapshotPath, "utf-8"));
15976
16508
  } catch (error) {
15977
16509
  const ts = options2.now?.() ?? Date.now();
15978
- const backupPath = path25.join(snapshotDir, `contacts-snapshot.corrupt-${ts}.json`);
16510
+ const backupPath = path26.join(snapshotDir, `contacts-snapshot.corrupt-${ts}.json`);
15979
16511
  try {
15980
- fs28.renameSync(snapshotPath, backupPath);
16512
+ fs29.renameSync(snapshotPath, backupPath);
15981
16513
  } catch (renameError) {
15982
16514
  appLogger.warn("contacts", "failed to backup corrupt contacts snapshot", renameError);
15983
16515
  }
@@ -15986,17 +16518,17 @@ function readContactsSnapshot(snapshotDir, timeRangePreset, options2 = {}) {
15986
16518
  }
15987
16519
  }
15988
16520
  function writeContactsSnapshot(snapshotDir, snapshot) {
15989
- if (!fs28.existsSync(snapshotDir)) fs28.mkdirSync(snapshotDir, { recursive: true });
15990
- const tmpPath = path25.join(snapshotDir, `${CONTACTS_SNAPSHOT_TMP_PREFIX}${process.pid}-${Date.now()}`);
15991
- fs28.writeFileSync(tmpPath, JSON.stringify(snapshot, null, 2), "utf-8");
15992
- fs28.renameSync(tmpPath, getContactsSnapshotPath(snapshotDir, snapshot.timeRange.preset));
16521
+ if (!fs29.existsSync(snapshotDir)) fs29.mkdirSync(snapshotDir, { recursive: true });
16522
+ const tmpPath = path26.join(snapshotDir, `${CONTACTS_SNAPSHOT_TMP_PREFIX}${process.pid}-${Date.now()}`);
16523
+ fs29.writeFileSync(tmpPath, JSON.stringify(snapshot, null, 2), "utf-8");
16524
+ fs29.renameSync(tmpPath, getContactsSnapshotPath(snapshotDir, snapshot.timeRange.preset));
15993
16525
  }
15994
16526
  function cleanupContactsSnapshotTempFiles(snapshotDir) {
15995
- if (!fs28.existsSync(snapshotDir)) return;
15996
- for (const name of fs28.readdirSync(snapshotDir)) {
16527
+ if (!fs29.existsSync(snapshotDir)) return;
16528
+ for (const name of fs29.readdirSync(snapshotDir)) {
15997
16529
  if (!name.startsWith(CONTACTS_SNAPSHOT_TMP_PREFIX)) continue;
15998
16530
  try {
15999
- fs28.rmSync(path25.join(snapshotDir, name), { force: true });
16531
+ fs29.rmSync(path26.join(snapshotDir, name), { force: true });
16000
16532
  } catch (error) {
16001
16533
  appLogger.warn("contacts", "failed to remove contacts snapshot temp file", error);
16002
16534
  }
@@ -16494,23 +17026,23 @@ function buildPeopleRelationshipsSignature(adapter, timeRangePreset) {
16494
17026
  }
16495
17027
 
16496
17028
  // ../../packages/node-runtime/src/services/people/relationships/snapshot.ts
16497
- import fs29 from "fs";
16498
- import path26 from "path";
17029
+ import fs30 from "fs";
17030
+ import path27 from "path";
16499
17031
  var PEOPLE_RELATIONSHIPS_SNAPSHOT_TMP_PREFIX = "people-relationships-snapshot.tmp-";
16500
17032
  function getPeopleRelationshipsSnapshotPath(snapshotDir, timeRangePreset) {
16501
17033
  const preset = normalizePeopleRelationshipsTimeRangePreset(timeRangePreset);
16502
- return path26.join(snapshotDir, `graph-snapshot-${preset}.json`);
17034
+ return path27.join(snapshotDir, `graph-snapshot-${preset}.json`);
16503
17035
  }
16504
17036
  function readPeopleRelationshipsSnapshot(snapshotDir, timeRangePreset, options2 = {}) {
16505
17037
  const snapshotPath = getPeopleRelationshipsSnapshotPath(snapshotDir, timeRangePreset);
16506
- if (!fs29.existsSync(snapshotPath)) return null;
17038
+ if (!fs30.existsSync(snapshotPath)) return null;
16507
17039
  try {
16508
- return JSON.parse(fs29.readFileSync(snapshotPath, "utf-8"));
17040
+ return JSON.parse(fs30.readFileSync(snapshotPath, "utf-8"));
16509
17041
  } catch (error) {
16510
17042
  const ts = options2.now?.() ?? Date.now();
16511
- const backupPath = path26.join(snapshotDir, `graph-snapshot.corrupt-${ts}.json`);
17043
+ const backupPath = path27.join(snapshotDir, `graph-snapshot.corrupt-${ts}.json`);
16512
17044
  try {
16513
- fs29.renameSync(snapshotPath, backupPath);
17045
+ fs30.renameSync(snapshotPath, backupPath);
16514
17046
  } catch (renameError) {
16515
17047
  appLogger.warn("people-relationships", "failed to backup corrupt people relationships snapshot", renameError);
16516
17048
  }
@@ -16519,17 +17051,17 @@ function readPeopleRelationshipsSnapshot(snapshotDir, timeRangePreset, options2
16519
17051
  }
16520
17052
  }
16521
17053
  function writePeopleRelationshipsSnapshot(snapshotDir, snapshot) {
16522
- if (!fs29.existsSync(snapshotDir)) fs29.mkdirSync(snapshotDir, { recursive: true });
16523
- const tmpPath = path26.join(snapshotDir, `${PEOPLE_RELATIONSHIPS_SNAPSHOT_TMP_PREFIX}${process.pid}-${Date.now()}`);
16524
- fs29.writeFileSync(tmpPath, JSON.stringify(snapshot, null, 2), "utf-8");
16525
- fs29.renameSync(tmpPath, getPeopleRelationshipsSnapshotPath(snapshotDir, snapshot.timeRange.preset));
17054
+ if (!fs30.existsSync(snapshotDir)) fs30.mkdirSync(snapshotDir, { recursive: true });
17055
+ const tmpPath = path27.join(snapshotDir, `${PEOPLE_RELATIONSHIPS_SNAPSHOT_TMP_PREFIX}${process.pid}-${Date.now()}`);
17056
+ fs30.writeFileSync(tmpPath, JSON.stringify(snapshot, null, 2), "utf-8");
17057
+ fs30.renameSync(tmpPath, getPeopleRelationshipsSnapshotPath(snapshotDir, snapshot.timeRange.preset));
16526
17058
  }
16527
17059
  function cleanupPeopleRelationshipsSnapshotTempFiles(snapshotDir) {
16528
- if (!fs29.existsSync(snapshotDir)) return;
16529
- for (const name of fs29.readdirSync(snapshotDir)) {
17060
+ if (!fs30.existsSync(snapshotDir)) return;
17061
+ for (const name of fs30.readdirSync(snapshotDir)) {
16530
17062
  if (!name.startsWith(PEOPLE_RELATIONSHIPS_SNAPSHOT_TMP_PREFIX)) continue;
16531
17063
  try {
16532
- fs29.rmSync(path26.join(snapshotDir, name), { force: true });
17064
+ fs30.rmSync(path27.join(snapshotDir, name), { force: true });
16533
17065
  } catch (error) {
16534
17066
  appLogger.warn("people-relationships", "failed to remove people relationships snapshot temp file", error);
16535
17067
  }
@@ -17050,9 +17582,310 @@ __export(relationships_exports, {
17050
17582
  getPeopleRelationshipsFactsCacheDir: () => getPeopleRelationshipsFactsCacheDir
17051
17583
  });
17052
17584
 
17585
+ // ../../packages/node-runtime/src/services/global-insight/service.ts
17586
+ import fs31 from "fs";
17587
+
17588
+ // ../../packages/node-runtime/src/services/global-insight/signature.ts
17589
+ function buildAnnualSummarySignature(adapter, range) {
17590
+ const parts = [
17591
+ `algorithm:${ANNUAL_SUMMARY_ALGORITHM_VERSION}`,
17592
+ `range:${toAnnualSummaryRangeKey(range)}`,
17593
+ `start:${range.startTs}`,
17594
+ `day:${toAnnualSummaryLocalDate(range)}`
17595
+ ];
17596
+ for (const sessionId of [...adapter.listSessionIds()].sort()) {
17597
+ parts.push(`${sessionId}:${getDbFileVersion(adapter.getDbPath(sessionId))}`);
17598
+ }
17599
+ return parts.join("|");
17600
+ }
17601
+
17602
+ // ../../packages/node-runtime/src/services/global-insight/worker-runner.ts
17603
+ import { existsSync as existsSync11 } from "fs";
17604
+ import { Worker as Worker3 } from "worker_threads";
17605
+ function resolveDefaultAnnualSummaryWorkerEntryUrl(currentModuleUrl = import.meta.url, entryExists = (url) => existsSync11(url)) {
17606
+ const moduleUrl = typeof currentModuleUrl === "string" ? currentModuleUrl : currentModuleUrl.href;
17607
+ if (moduleUrl.endsWith(".ts")) return new URL("./worker-entry.ts", moduleUrl);
17608
+ if (moduleUrl.endsWith(".mjs")) return new URL("./global-insight-worker.mjs", moduleUrl);
17609
+ const sibling = new URL("./worker-entry.js", moduleUrl);
17610
+ return entryExists(sibling) ? sibling : new URL("./global-insight-worker.js", moduleUrl);
17611
+ }
17612
+ function createAnnualSummaryWorkerRunner(options2) {
17613
+ return ({ signature, range, signal, onProgress }) => new Promise((resolve2, reject) => {
17614
+ if (signal.aborted) {
17615
+ reject(new Error("annual summary worker aborted"));
17616
+ return;
17617
+ }
17618
+ const worker = createWorker3(
17619
+ {
17620
+ paths: snapshotPathProvider(options2.pathProvider),
17621
+ runtimeIdentity: options2.runtimeIdentity,
17622
+ nativeBinding: options2.nativeBinding,
17623
+ signature,
17624
+ range
17625
+ },
17626
+ options2.workerEntryUrl
17627
+ );
17628
+ let settled = false;
17629
+ const abort = () => {
17630
+ if (settled) return;
17631
+ settled = true;
17632
+ void worker.terminate();
17633
+ reject(new Error("annual summary worker aborted"));
17634
+ };
17635
+ signal.addEventListener("abort", abort, { once: true });
17636
+ worker.on("message", (message) => {
17637
+ if (message.type === "progress" && message.progress) {
17638
+ onProgress(message.progress);
17639
+ return;
17640
+ }
17641
+ if (message.type === "success" && message.snapshot) {
17642
+ settled = true;
17643
+ signal.removeEventListener("abort", abort);
17644
+ resolve2(message.snapshot);
17645
+ void worker.terminate();
17646
+ return;
17647
+ }
17648
+ if (message.type === "error") {
17649
+ settled = true;
17650
+ signal.removeEventListener("abort", abort);
17651
+ reject(new Error(message.error ?? "annual summary worker failed"));
17652
+ void worker.terminate();
17653
+ }
17654
+ });
17655
+ worker.on("error", (error) => {
17656
+ if (settled) return;
17657
+ settled = true;
17658
+ signal.removeEventListener("abort", abort);
17659
+ reject(error);
17660
+ });
17661
+ worker.on("exit", (code) => {
17662
+ if (settled || code === 0) return;
17663
+ settled = true;
17664
+ signal.removeEventListener("abort", abort);
17665
+ reject(new Error(`annual summary worker exited with code ${code}`));
17666
+ });
17667
+ });
17668
+ }
17669
+ function createWorker3(workerData, entryUrlInput) {
17670
+ const entryUrl = entryUrlInput ? typeof entryUrlInput === "string" ? new URL(entryUrlInput) : entryUrlInput : resolveDefaultAnnualSummaryWorkerEntryUrl();
17671
+ if (!entryUrl.href.endsWith(".ts")) return new Worker3(entryUrl, { workerData });
17672
+ const bootstrap = `
17673
+ import { register } from 'tsx/esm/api';
17674
+ register();
17675
+ await import(${JSON.stringify(entryUrl.href)});
17676
+ `;
17677
+ const workerOptions = { eval: true, type: "module", workerData, execArgv: [] };
17678
+ return new Worker3(bootstrap, workerOptions);
17679
+ }
17680
+
17681
+ // ../../packages/node-runtime/src/services/global-insight/service.ts
17682
+ function createGlobalInsightService(deps) {
17683
+ return new DefaultGlobalInsightService(deps);
17684
+ }
17685
+ var DefaultGlobalInsightService = class {
17686
+ constructor(deps) {
17687
+ this.deps = deps;
17688
+ this.snapshotDir = deps.userDataDir ? getGlobalInsightDir(deps.userDataDir) : getGlobalInsightDir(requirePathProvider3(deps).getUserDataDir());
17689
+ cleanupAnnualSummarySnapshotTempFiles(this.snapshotDir);
17690
+ this.runner = deps.runner ?? createAnnualSummaryWorkerRunner({
17691
+ pathProvider: requirePathProvider3(deps),
17692
+ runtimeIdentity: deps.runtimeIdentity,
17693
+ nativeBinding: deps.nativeBinding,
17694
+ workerEntryUrl: deps.workerEntryUrl
17695
+ });
17696
+ }
17697
+ deps;
17698
+ snapshots = /* @__PURE__ */ new Map();
17699
+ snapshotDir;
17700
+ runner;
17701
+ inFlight = null;
17702
+ task = createIdleTaskState3();
17703
+ getAnnualSummary(options2 = {}) {
17704
+ const range = this.normalizeRange(options2);
17705
+ const signature = buildAnnualSummarySignature(this.deps.adapter, range);
17706
+ const status = this.getCacheStatus(signature, range);
17707
+ if (this.shouldStartTask(options2, status)) this.ensureTaskStarted(signature, range);
17708
+ return this.toResponse(signature, range, options2.acceptStale === true);
17709
+ }
17710
+ startRecompute(options2 = {}) {
17711
+ const range = this.normalizeRange(options2);
17712
+ const signature = buildAnnualSummarySignature(this.deps.adapter, range);
17713
+ this.ensureTaskStarted(signature, range);
17714
+ return this.toResponse(signature, range, true);
17715
+ }
17716
+ invalidateCache() {
17717
+ this.snapshots.clear();
17718
+ }
17719
+ normalizeRange(input = {}) {
17720
+ return normalizeAnnualSummaryRange(input, new Date(this.now()));
17721
+ }
17722
+ replaceSnapshotForTests(snapshot) {
17723
+ writeAnnualSummarySnapshot(this.snapshotDir, snapshot);
17724
+ this.snapshots.set(toAnnualSummaryRangeKey(snapshot.range), snapshot);
17725
+ }
17726
+ async close() {
17727
+ const inFlight = this.inFlight;
17728
+ if (!inFlight) return;
17729
+ this.inFlight = null;
17730
+ inFlight.abortController.abort();
17731
+ this.task = {
17732
+ ...this.task,
17733
+ status: "failed",
17734
+ finishedAt: this.now(),
17735
+ lastError: "annual summary task aborted"
17736
+ };
17737
+ }
17738
+ shouldStartTask(options2, status) {
17739
+ if (options2.forceRecompute) return true;
17740
+ if (status === "fresh") return false;
17741
+ return this.task.status !== "failed";
17742
+ }
17743
+ ensureTaskStarted(signature, range) {
17744
+ if (this.inFlight) return;
17745
+ const id = `annual_summary_${this.now()}_${Math.random().toString(36).slice(2)}`;
17746
+ this.task = {
17747
+ id,
17748
+ status: "running",
17749
+ startedAt: this.now(),
17750
+ finishedAt: null,
17751
+ processedSessions: 0,
17752
+ totalSessions: this.deps.adapter.listSessionIds().length
17753
+ };
17754
+ const abortController = new AbortController();
17755
+ const promise = this.runner({
17756
+ signature,
17757
+ range,
17758
+ signal: abortController.signal,
17759
+ onProgress: (progress) => {
17760
+ if (this.task.id !== id || this.task.status !== "running") return;
17761
+ this.task = { ...this.task, ...progress };
17762
+ }
17763
+ });
17764
+ this.inFlight = { id, signature, promise, abortController };
17765
+ promise.then((snapshot) => this.handleTaskSuccess(id, signature, snapshot)).catch((error) => this.handleTaskFailure(id, error));
17766
+ }
17767
+ handleTaskSuccess(id, inputSignature, snapshot) {
17768
+ if (this.inFlight?.id !== id) return;
17769
+ const latestSignature = buildAnnualSummarySignature(this.deps.adapter, snapshot.range);
17770
+ const finishedAt = this.now();
17771
+ if (inputSignature !== latestSignature || snapshot.signature !== latestSignature) {
17772
+ this.inFlight = null;
17773
+ this.task = { ...this.task, status: "superseded", finishedAt, currentSessionId: void 0 };
17774
+ appLogger.info("global-insight", "annual summary worker result discarded because signature changed");
17775
+ return;
17776
+ }
17777
+ try {
17778
+ writeAnnualSummarySnapshot(this.snapshotDir, snapshot);
17779
+ this.inFlight = null;
17780
+ this.snapshots.set(toAnnualSummaryRangeKey(snapshot.range), snapshot);
17781
+ this.task = {
17782
+ ...this.task,
17783
+ status: "succeeded",
17784
+ finishedAt,
17785
+ processedSessions: snapshot.workerStats.processedSessions,
17786
+ totalSessions: snapshot.workerStats.totalSessions,
17787
+ currentSessionId: void 0
17788
+ };
17789
+ appLogger.info("global-insight", "annual summary snapshot persisted", {
17790
+ analyzedSessions: snapshot.coverage.analyzedSessions,
17791
+ durationMs: snapshot.workerStats.durationMs
17792
+ });
17793
+ } catch (error) {
17794
+ this.handleTaskFailure(id, error);
17795
+ }
17796
+ }
17797
+ handleTaskFailure(id, error) {
17798
+ if (this.inFlight?.id !== id) return;
17799
+ this.inFlight = null;
17800
+ this.task = {
17801
+ ...this.task,
17802
+ status: "failed",
17803
+ finishedAt: this.now(),
17804
+ currentSessionId: void 0,
17805
+ lastError: error instanceof Error ? error.message : String(error)
17806
+ };
17807
+ appLogger.error("global-insight", "annual summary worker failed", error);
17808
+ }
17809
+ toResponse(signature, range, acceptStale) {
17810
+ const snapshot = this.getSnapshot(range);
17811
+ const status = this.getCacheStatus(signature, range);
17812
+ const includeSnapshot = status === "fresh" || status === "stale" && acceptStale;
17813
+ return {
17814
+ range,
17815
+ availableDataYears: includeSnapshot ? snapshot?.availableDataYears ?? [] : [],
17816
+ latestDataYear: includeSnapshot ? snapshot?.latestDataYear ?? null : null,
17817
+ metrics: includeSnapshot ? snapshot?.metrics ?? null : null,
17818
+ monthlyActivity: includeSnapshot ? snapshot?.monthlyActivity ?? [] : [],
17819
+ dailyActivity: includeSnapshot ? snapshot?.dailyActivity ?? [] : [],
17820
+ messageTypes: includeSnapshot ? snapshot?.messageTypes ?? [] : [],
17821
+ textLength: includeSnapshot ? snapshot?.textLength ?? null : null,
17822
+ coverage: includeSnapshot ? snapshot?.coverage ?? emptyCoverage() : emptyCoverage(),
17823
+ cache: {
17824
+ status,
17825
+ computedAt: snapshot?.computedAt ?? null,
17826
+ signature: snapshot?.signature,
17827
+ staleReason: status === "stale" ? "signature_changed" : void 0
17828
+ },
17829
+ task: this.task
17830
+ };
17831
+ }
17832
+ getCacheStatus(signature, range) {
17833
+ const snapshot = this.getSnapshot(range);
17834
+ if (!snapshot) return "missing";
17835
+ return snapshot.signature === signature ? "fresh" : "stale";
17836
+ }
17837
+ getSnapshot(range) {
17838
+ const key = toAnnualSummaryRangeKey(range);
17839
+ if (this.snapshots.has(key) && !fs31.existsSync(getAnnualSummarySnapshotPath(this.snapshotDir, range))) {
17840
+ this.snapshots.delete(key);
17841
+ }
17842
+ if (!this.snapshots.has(key)) {
17843
+ this.snapshots.set(key, readAnnualSummarySnapshot(this.snapshotDir, range, { now: this.deps.now }));
17844
+ }
17845
+ return this.snapshots.get(key) ?? null;
17846
+ }
17847
+ now() {
17848
+ return this.deps.now?.() ?? Date.now();
17849
+ }
17850
+ };
17851
+ function requirePathProvider3(deps) {
17852
+ if (!deps.pathProvider)
17853
+ throw new Error("GlobalInsightService requires pathProvider unless userDataDir and runner are provided");
17854
+ return deps.pathProvider;
17855
+ }
17856
+ function createIdleTaskState3() {
17857
+ return {
17858
+ id: null,
17859
+ status: "idle",
17860
+ startedAt: null,
17861
+ finishedAt: null,
17862
+ processedSessions: 0,
17863
+ totalSessions: 0
17864
+ };
17865
+ }
17866
+ function emptyCoverage() {
17867
+ return {
17868
+ totalSessions: 0,
17869
+ analyzedSessions: 0,
17870
+ missingOwnerSessions: 0,
17871
+ unresolvedOwnerSessions: 0,
17872
+ failedSessions: 0
17873
+ };
17874
+ }
17875
+
17876
+ // ../../packages/node-runtime/src/services/global-insight/index.ts
17877
+ var global_insight_exports = {};
17878
+ __export(global_insight_exports, {
17879
+ ANNUAL_SUMMARY_ALGORITHM_VERSION: () => ANNUAL_SUMMARY_ALGORITHM_VERSION,
17880
+ createGlobalInsightService: () => createGlobalInsightService,
17881
+ deleteAnnualSummarySnapshots: () => deleteAnnualSummarySnapshots,
17882
+ getGlobalInsightDir: () => getGlobalInsightDir,
17883
+ getGlobalInsightFactsCacheDir: () => getGlobalInsightFactsCacheDir
17884
+ });
17885
+
17053
17886
  // ../../packages/node-runtime/src/semantic-index/config.ts
17054
- import fs30 from "fs";
17055
- import path27 from "path";
17887
+ import fs32 from "fs";
17888
+ import path28 from "path";
17056
17889
  var DEFAULT_SEMANTIC_INDEX_MODEL_DOWNLOAD_SOURCE = "huggingface";
17057
17890
  var SEMANTIC_INDEX_CONFIG_VERSION = 1;
17058
17891
  var SEARCH_MAX_RESULTS_DEFAULT = 10;
@@ -17141,9 +17974,9 @@ var SemanticIndexConfigStore = class {
17141
17974
  return this.set({ ...this.get(), enabled });
17142
17975
  }
17143
17976
  get() {
17144
- if (!fs30.existsSync(this.filePath)) return defaultSemanticIndexConfig();
17977
+ if (!fs32.existsSync(this.filePath)) return defaultSemanticIndexConfig();
17145
17978
  try {
17146
- const raw = JSON.parse(fs30.readFileSync(this.filePath, "utf-8"));
17979
+ const raw = JSON.parse(fs32.readFileSync(this.filePath, "utf-8"));
17147
17980
  return normalizeConfig(raw);
17148
17981
  } catch {
17149
17982
  return defaultSemanticIndexConfig();
@@ -17151,9 +17984,9 @@ var SemanticIndexConfigStore = class {
17151
17984
  }
17152
17985
  set(config) {
17153
17986
  const normalized = normalizeConfig(config);
17154
- const dir = path27.dirname(this.filePath);
17155
- if (!fs30.existsSync(dir)) fs30.mkdirSync(dir, { recursive: true });
17156
- fs30.writeFileSync(this.filePath, JSON.stringify(normalized, null, 2), "utf-8");
17987
+ const dir = path28.dirname(this.filePath);
17988
+ if (!fs32.existsSync(dir)) fs32.mkdirSync(dir, { recursive: true });
17989
+ fs32.writeFileSync(this.filePath, JSON.stringify(normalized, null, 2), "utf-8");
17157
17990
  return normalized;
17158
17991
  }
17159
17992
  resolveModelId() {
@@ -17162,7 +17995,7 @@ var SemanticIndexConfigStore = class {
17162
17995
  };
17163
17996
 
17164
17997
  // ../../packages/node-runtime/src/semantic-index/service.ts
17165
- import path28 from "path";
17998
+ import path29 from "path";
17166
17999
 
17167
18000
  // ../../packages/node-runtime/src/semantic-index/chunker-config.ts
17168
18001
  import { createHash as createHash2 } from "crypto";
@@ -18143,7 +18976,7 @@ function formatDateTime(ts) {
18143
18976
  const d = new Date(ts);
18144
18977
  return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
18145
18978
  }
18146
- function formatTime2(ts) {
18979
+ function formatTime(ts) {
18147
18980
  const d = new Date(ts);
18148
18981
  return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
18149
18982
  }
@@ -18151,7 +18984,7 @@ function formatTimeRange2(startTs, endTs) {
18151
18984
  const start = new Date(startTs);
18152
18985
  const end = new Date(endTs);
18153
18986
  const sameDay = start.getFullYear() === end.getFullYear() && start.getMonth() === end.getMonth() && start.getDate() === end.getDate();
18154
- return `${formatDateTime(startTs)} - ${sameDay ? formatTime2(endTs) : formatDateTime(endTs)}`;
18987
+ return `${formatDateTime(startTs)} - ${sameDay ? formatTime(endTs) : formatDateTime(endTs)}`;
18155
18988
  }
18156
18989
  function resolveSenderNameTokenCap(nameTokenCounts, totalLimit) {
18157
18990
  const total = nameTokenCounts.reduce((sum, count) => sum + count, 0);
@@ -18693,16 +19526,16 @@ function resolveSearchToolEvidenceTokens(maxResultTokens) {
18693
19526
  return maxResultTokens && maxResultTokens > 0 ? Math.min(SEARCH_TOOL_EVIDENCE_TOKENS, maxResultTokens) : SEARCH_TOOL_EVIDENCE_TOKENS;
18694
19527
  }
18695
19528
  function sessionIdFromDbPath(dbPath) {
18696
- return path28.basename(dbPath, ".db");
19529
+ return path29.basename(dbPath, ".db");
18697
19530
  }
18698
19531
  var SEMANTIC_INDEX_DB_FILE = "embedding_index.db";
18699
19532
  var SEMANTIC_INDEX_CONFIG_FILE = "semantic-index-config.json";
18700
19533
  function createSemanticIndexService(params) {
18701
19534
  const { pathProvider } = params;
18702
19535
  return new SemanticIndexService({
18703
- vectorDbPath: path28.join(pathProvider.getVectorDir(), SEMANTIC_INDEX_DB_FILE),
18704
- configPath: path28.join(pathProvider.getAiDataDir(), SEMANTIC_INDEX_CONFIG_FILE),
18705
- modelsCacheDir: path28.join(pathProvider.getAiDataDir(), "models", "semantic-index"),
19536
+ vectorDbPath: path29.join(pathProvider.getVectorDir(), SEMANTIC_INDEX_DB_FILE),
19537
+ configPath: path29.join(pathProvider.getAiDataDir(), SEMANTIC_INDEX_CONFIG_FILE),
19538
+ modelsCacheDir: path29.join(pathProvider.getAiDataDir(), "models", "semantic-index"),
18706
19539
  modelDownloadProxyUrl: params.modelDownloadProxyUrl,
18707
19540
  sessionAdapter: params.sessionAdapter,
18708
19541
  nativeBinding: params.nativeBinding,
@@ -19252,8 +20085,8 @@ ${text}`;
19252
20085
  };
19253
20086
 
19254
20087
  // ../../packages/node-runtime/src/services/merge-cache.ts
19255
- import * as fs31 from "fs";
19256
- import * as path29 from "path";
20088
+ import * as fs33 from "fs";
20089
+ import * as path30 from "path";
19257
20090
  import * as crypto from "crypto";
19258
20091
  var MergeSessionCache = class {
19259
20092
  cache = /* @__PURE__ */ new Map();
@@ -19262,15 +20095,15 @@ var MergeSessionCache = class {
19262
20095
  constructor(pathProvider, options2) {
19263
20096
  this.tempDir = pathProvider.getTempDir();
19264
20097
  this.nativeBinding = options2?.nativeBinding;
19265
- if (!fs31.existsSync(this.tempDir)) {
19266
- fs31.mkdirSync(this.tempDir, { recursive: true });
20098
+ if (!fs33.existsSync(this.tempDir)) {
20099
+ fs33.mkdirSync(this.tempDir, { recursive: true });
19267
20100
  }
19268
20101
  }
19269
20102
  generateTempDbPath(filename) {
19270
20103
  const ts = Date.now();
19271
20104
  const rand = Math.random().toString(36).slice(2, 8);
19272
20105
  const safeName = filename.replace(/[/\\?%*:|"<>]/g, "_").slice(0, 40);
19273
- return path29.join(this.tempDir, `merge_${safeName}_${ts}_${rand}.db`);
20106
+ return path30.join(this.tempDir, `merge_${safeName}_${ts}_${rand}.db`);
19274
20107
  }
19275
20108
  /** Store a parsed file's temp DB and return a handle for subsequent requests. */
19276
20109
  store(filename, tempDbPath) {
@@ -19281,7 +20114,7 @@ var MergeSessionCache = class {
19281
20114
  /** Open a TempDbReader for a given handle. Caller must close the reader. */
19282
20115
  openReader(handle) {
19283
20116
  const entry = this.cache.get(handle);
19284
- if (!entry || !fs31.existsSync(entry.tempDbPath)) return null;
20117
+ if (!entry || !fs33.existsSync(entry.tempDbPath)) return null;
19285
20118
  const adapter = openBetterSqliteDatabase(entry.tempDbPath, {
19286
20119
  readonly: true,
19287
20120
  nativeBinding: this.nativeBinding
@@ -19326,21 +20159,44 @@ var MergeSessionCache = class {
19326
20159
  };
19327
20160
 
19328
20161
  // ../../packages/node-runtime/src/services/push-importer.ts
19329
- import * as fs32 from "fs";
19330
- var importInProgress = /* @__PURE__ */ new Set();
20162
+ import * as fs34 from "fs";
19331
20163
  var SYSTEM_SENDER_ID = "SYSTEM";
19332
20164
  var SYSTEM_MEMBER_NAME = "\u7CFB\u7EDF\u6D88\u606F";
20165
+ function isRecord3(value) {
20166
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20167
+ }
20168
+ function validateOptionalString(value, field) {
20169
+ return value !== void 0 && typeof value !== "string" ? `${field} must be a string` : null;
20170
+ }
19333
20171
  function validatePayload(payload, isNew) {
20172
+ if (!isRecord3(payload)) return "payload must be an object";
19334
20173
  const messages = payload.messages;
19335
20174
  if (!Array.isArray(messages) || messages.length === 0) return "messages is required and must be a non-empty array";
20175
+ if (payload.chatlab !== void 0) {
20176
+ if (!isRecord3(payload.chatlab)) return "chatlab must be an object";
20177
+ if (typeof payload.chatlab.version !== "string" || payload.chatlab.version.length === 0)
20178
+ return "chatlab.version must be a string";
20179
+ if (typeof payload.chatlab.exportedAt !== "number" || !Number.isFinite(payload.chatlab.exportedAt))
20180
+ return "chatlab.exportedAt must be a number";
20181
+ const generatorError = validateOptionalString(payload.chatlab.generator, "chatlab.generator");
20182
+ if (generatorError) return generatorError;
20183
+ }
19336
20184
  if (isNew) {
19337
20185
  if (!payload.chatlab) return "chatlab is required for new sessions";
19338
20186
  if (!payload.meta) return "meta is required for new sessions";
19339
- const m = payload.meta;
19340
- if (!m.name) return "meta.name is required";
19341
- if (!m.platform) return "meta.platform is required";
19342
- if (!m.type) return "meta.type is required";
19343
20187
  }
20188
+ if (payload.meta !== void 0) {
20189
+ if (!isRecord3(payload.meta)) return "meta must be an object";
20190
+ const metaFields = ["name", "platform", "type", "groupId", "groupAvatar", "ownerId"];
20191
+ for (const field of metaFields) {
20192
+ const error = validateOptionalString(payload.meta[field], `meta.${field}`);
20193
+ if (error) return error;
20194
+ }
20195
+ if (isNew && !payload.meta.name) return "meta.name is required";
20196
+ if (isNew && !payload.meta.platform) return "meta.platform is required";
20197
+ if (isNew && !payload.meta.type) return "meta.type is required";
20198
+ }
20199
+ if (payload.options !== void 0 && !isRecord3(payload.options)) return "options must be an object";
19344
20200
  const { metaUpdateMode, memberUpdateMode } = payload.options ?? {};
19345
20201
  if (metaUpdateMode !== void 0 && metaUpdateMode !== "patch" && metaUpdateMode !== "none") {
19346
20202
  return `options.metaUpdateMode must be 'patch' or 'none'`;
@@ -19351,16 +20207,35 @@ function validatePayload(payload, isNew) {
19351
20207
  if (payload.members !== void 0 && !Array.isArray(payload.members)) return "members must be an array";
19352
20208
  if (payload.members) {
19353
20209
  for (let i = 0; i < payload.members.length; i++) {
19354
- if (typeof payload.members[i].platformId !== "string" || payload.members[i].platformId.length === 0)
20210
+ const member = payload.members[i];
20211
+ if (!isRecord3(member)) return `members[${i}] must be an object`;
20212
+ if (typeof member.platformId !== "string" || member.platformId.length === 0)
19355
20213
  return `members[${i}].platformId must be a string`;
20214
+ for (const field of ["accountName", "groupNickname", "avatar"]) {
20215
+ const error = validateOptionalString(member[field], `members[${i}].${field}`);
20216
+ if (error) return error;
20217
+ }
20218
+ if (member.roles !== void 0) {
20219
+ if (!Array.isArray(member.roles)) return `members[${i}].roles must be an array`;
20220
+ for (let j = 0; j < member.roles.length; j++) {
20221
+ const role = member.roles[j];
20222
+ if (!isRecord3(role) || typeof role.id !== "string" || role.id.length === 0)
20223
+ return `members[${i}].roles[${j}].id must be a string`;
20224
+ }
20225
+ }
19356
20226
  }
19357
20227
  }
19358
20228
  for (let i = 0; i < messages.length; i++) {
19359
20229
  const msg = messages[i];
20230
+ if (!isRecord3(msg)) return `messages[${i}] must be an object`;
19360
20231
  if (typeof msg.sender !== "string" || msg.sender.length === 0) return `messages[${i}].sender must be a string`;
19361
- if (typeof msg.timestamp !== "number" || msg.timestamp <= 0)
20232
+ if (typeof msg.timestamp !== "number" || !Number.isFinite(msg.timestamp) || msg.timestamp <= 0)
19362
20233
  return `messages[${i}].timestamp must be a positive number`;
19363
- if (typeof msg.type !== "number") return `messages[${i}].type must be a number`;
20234
+ if (typeof msg.type !== "number" || !Number.isFinite(msg.type)) return `messages[${i}].type must be a number`;
20235
+ for (const field of ["accountName", "groupNickname"]) {
20236
+ const error = validateOptionalString(msg[field], `messages[${i}].${field}`);
20237
+ if (error) return error;
20238
+ }
19364
20239
  if (msg.content !== void 0 && msg.content !== null && typeof msg.content !== "string")
19365
20240
  return `messages[${i}].content must be a string or null`;
19366
20241
  if (msg.platformMessageId !== void 0 && typeof msg.platformMessageId !== "string")
@@ -19381,47 +20256,79 @@ function normalizeSenderAccountName(sender, accountName) {
19381
20256
  function normalizeSenderGroupNickname(sender, groupNickname) {
19382
20257
  return sender === SYSTEM_SENDER_ID ? SYSTEM_MEMBER_NAME : groupNickname;
19383
20258
  }
19384
- function writeMessages(db, messages, existingPmids, existingKeys) {
20259
+ function isCountedMember(platformId) {
20260
+ return platformId !== SYSTEM_SENDER_ID;
20261
+ }
20262
+ function registerPushMessageAndCheckDuplicate(message, dedupState) {
20263
+ return registerMessageAndCheckDuplicate(
20264
+ {
20265
+ platformMessageId: message.platformMessageId,
20266
+ timestamp: message.timestamp,
20267
+ senderPlatformId: message.sender,
20268
+ type: message.type,
20269
+ content: message.content ?? null,
20270
+ replyToMessageId: message.replyToMessageId
20271
+ },
20272
+ dedupState
20273
+ );
20274
+ }
20275
+ function createExistingMessageDedupState(db) {
20276
+ const existingPmids = /* @__PURE__ */ new Set();
20277
+ const existingKeys = /* @__PURE__ */ new Set();
20278
+ const existingFallbackOnlyKeys = /* @__PURE__ */ new Set();
20279
+ const existingMessages = db.prepare(
20280
+ `SELECT msg.ts, m.platform_id, msg.type, msg.content, msg.reply_to_message_id, msg.platform_message_id
20281
+ FROM message msg
20282
+ JOIN member m ON msg.sender_id = m.id`
20283
+ ).all();
20284
+ for (const message of existingMessages) {
20285
+ if (message.platform_message_id) existingPmids.add(message.platform_message_id);
20286
+ const key = generateFallbackMessageKey({
20287
+ timestamp: message.ts,
20288
+ senderPlatformId: message.platform_id,
20289
+ type: message.type,
20290
+ content: message.content,
20291
+ replyToMessageId: message.reply_to_message_id ?? void 0
20292
+ });
20293
+ existingKeys.add(key);
20294
+ if (!message.platform_message_id) existingFallbackOnlyKeys.add(key);
20295
+ }
20296
+ return createMessageDedupState(existingPmids, existingKeys, existingFallbackOnlyKeys);
20297
+ }
20298
+ function writeMessages(db, messages, dedupState) {
19385
20299
  const insertMsg = db.prepare(
19386
20300
  `INSERT INTO message (sender_id, sender_account_name, sender_group_nickname, ts, type, content, reply_to_message_id, platform_message_id)
19387
20301
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
19388
20302
  );
19389
20303
  const getMemberId = db.prepare("SELECT id FROM member WHERE platform_id = ?");
19390
- const insertMinimalMember = db.prepare("INSERT OR IGNORE INTO member (platform_id, account_name) VALUES (?, ?)");
20304
+ const insertMinimalMember = db.prepare(
20305
+ "INSERT OR IGNORE INTO member (platform_id, account_name, group_nickname) VALUES (?, ?, ?)"
20306
+ );
19391
20307
  const memberIdCache = /* @__PURE__ */ new Map();
19392
20308
  let writtenCount = 0;
19393
20309
  let duplicateCount = 0;
20310
+ let membersAdded = 0;
19394
20311
  let minWrittenTs = Infinity;
19395
20312
  const ftsEntries = [];
19396
20313
  const sorted = [...messages].sort((a, b) => a.timestamp - b.timestamp);
19397
20314
  db.transaction(() => {
19398
20315
  for (const msg of sorted) {
19399
- const key = generateMessageKey(msg.timestamp, msg.sender, msg.content ?? null);
19400
- if (msg.platformMessageId) {
19401
- if (existingPmids.has(msg.platformMessageId)) {
19402
- duplicateCount++;
19403
- continue;
19404
- }
19405
- if (existingKeys.has(key)) {
19406
- duplicateCount++;
19407
- continue;
19408
- }
19409
- existingPmids.add(msg.platformMessageId);
19410
- existingKeys.add(key);
19411
- } else {
19412
- if (existingKeys.has(key)) {
19413
- duplicateCount++;
19414
- continue;
19415
- }
19416
- existingKeys.add(key);
20316
+ if (registerPushMessageAndCheckDuplicate(msg, dedupState)) {
20317
+ duplicateCount++;
20318
+ continue;
19417
20319
  }
19418
20320
  let memberId = memberIdCache.get(msg.sender);
19419
20321
  if (!memberId) {
19420
- insertMinimalMember.run(msg.sender, normalizeSenderAccountName(msg.sender, msg.accountName) || null);
20322
+ const insertResult = insertMinimalMember.run(
20323
+ msg.sender,
20324
+ normalizeSenderAccountName(msg.sender, msg.accountName) || null,
20325
+ normalizeSenderGroupNickname(msg.sender, msg.groupNickname) || null
20326
+ );
19421
20327
  const row = getMemberId.get(msg.sender);
19422
20328
  if (row) {
19423
20329
  memberId = row.id;
19424
20330
  memberIdCache.set(msg.sender, memberId);
20331
+ if (isCountedMember(msg.sender)) membersAdded += insertResult.changes;
19425
20332
  }
19426
20333
  }
19427
20334
  if (!memberId) continue;
@@ -19440,7 +20347,7 @@ function writeMessages(db, messages, existingPmids, existingKeys) {
19440
20347
  writtenCount++;
19441
20348
  }
19442
20349
  });
19443
- return { writtenCount, duplicateCount, minWrittenTs, ftsEntries };
20350
+ return { writtenCount, duplicateCount, membersAdded, minWrittenTs, ftsEntries };
19444
20351
  }
19445
20352
  function fullImport(db, meta, members, messages) {
19446
20353
  db.exec(CHAT_DB_SCHEMA);
@@ -19457,29 +20364,13 @@ function fullImport(db, meta, members, messages) {
19457
20364
  }
19458
20365
  }
19459
20366
  const allMembers = extraMembers.length > 0 ? [...members, ...extraMembers] : members;
19460
- const pmids = /* @__PURE__ */ new Set();
19461
- const hashKeys = /* @__PURE__ */ new Set();
20367
+ const dedupState = createMessageDedupState();
19462
20368
  let duplicateCount = 0;
19463
20369
  const dedupedMessages = [];
19464
20370
  for (const msg of messages) {
19465
- const key = generateMessageKey(msg.timestamp, msg.sender, msg.content ?? null);
19466
- if (msg.platformMessageId) {
19467
- if (pmids.has(msg.platformMessageId)) {
19468
- duplicateCount++;
19469
- continue;
19470
- }
19471
- if (hashKeys.has(key)) {
19472
- duplicateCount++;
19473
- continue;
19474
- }
19475
- pmids.add(msg.platformMessageId);
19476
- hashKeys.add(key);
19477
- } else {
19478
- if (hashKeys.has(key)) {
19479
- duplicateCount++;
19480
- continue;
19481
- }
19482
- hashKeys.add(key);
20371
+ if (registerPushMessageAndCheckDuplicate(msg, dedupState)) {
20372
+ duplicateCount++;
20373
+ continue;
19483
20374
  }
19484
20375
  dedupedMessages.push(msg);
19485
20376
  }
@@ -19507,15 +20398,12 @@ function fullImport(db, meta, members, messages) {
19507
20398
  generateSessionIndex(db);
19508
20399
  } catch {
19509
20400
  }
19510
- return { writtenCount: stats.messageCount, duplicateCount, membersAdded: members.length };
20401
+ return { writtenCount: stats.messageCount, duplicateCount };
19511
20402
  }
19512
- function incrementalImport2(db, payload) {
20403
+ function writeIncrementalImport(db, payload) {
19513
20404
  const metaUpdateMode = payload.options?.metaUpdateMode ?? "patch";
19514
20405
  const memberUpdateMode = payload.options?.memberUpdateMode ?? "upsert";
19515
- const existingPmids = /* @__PURE__ */ new Set();
19516
- const existingKeys = /* @__PURE__ */ new Set();
19517
- db.prepare("SELECT platform_message_id FROM message WHERE platform_message_id IS NOT NULL").all().forEach((r) => existingPmids.add(r.platform_message_id));
19518
- db.prepare(`SELECT msg.ts, m.platform_id, msg.content FROM message msg JOIN member m ON msg.sender_id = m.id`).all().forEach((r) => existingKeys.add(generateMessageKey(r.ts, r.platform_id, r.content)));
20406
+ const dedupState = createExistingMessageDedupState(db);
19519
20407
  let metaUpdated = false;
19520
20408
  let membersAdded = 0;
19521
20409
  let membersUpdated = 0;
@@ -19552,22 +20440,23 @@ function incrementalImport2(db, payload) {
19552
20440
  m.roles ? JSON.stringify(m.roles) : "[]"
19553
20441
  );
19554
20442
  if (!existed) {
19555
- membersAdded++;
20443
+ if (isCountedMember(m.platformId)) membersAdded++;
19556
20444
  const row = getMemberId.get(m.platformId);
19557
20445
  if (row) existingMemberIds.add(m.platformId);
19558
- } else {
20446
+ } else if (isCountedMember(m.platformId)) {
19559
20447
  membersUpdated++;
19560
20448
  }
19561
20449
  }
19562
20450
  });
19563
20451
  }
19564
20452
  const preWriteMaxTs = db.prepare("SELECT MAX(ts) as max_ts FROM message").get()?.max_ts ?? 0;
19565
- const { writtenCount, duplicateCount, minWrittenTs, ftsEntries } = writeMessages(
19566
- db,
19567
- payload.messages,
19568
- existingPmids,
19569
- existingKeys
19570
- );
20453
+ const {
20454
+ writtenCount,
20455
+ duplicateCount,
20456
+ membersAdded: autoCreatedMembersAdded,
20457
+ minWrittenTs,
20458
+ ftsEntries
20459
+ } = writeMessages(db, payload.messages, dedupState);
19571
20460
  if (ftsEntries.length > 0) {
19572
20461
  try {
19573
20462
  insertFtsEntries(db, ftsEntries);
@@ -19587,40 +20476,69 @@ function incrementalImport2(db, payload) {
19587
20476
  if (!metaUpdated) {
19588
20477
  db.prepare("UPDATE meta SET imported_at = ?").run(Math.floor(Date.now() / 1e3));
19589
20478
  }
19590
- return { writtenCount, duplicateCount, metaUpdated, membersAdded, membersUpdated };
20479
+ return {
20480
+ writtenCount,
20481
+ duplicateCount,
20482
+ metaUpdated,
20483
+ membersAdded: membersAdded + autoCreatedMembersAdded,
20484
+ membersUpdated
20485
+ };
20486
+ }
20487
+ function incrementalImport2(db, payload) {
20488
+ return db.transaction(() => writeIncrementalImport(db, payload));
19591
20489
  }
19592
- var SAFE_SESSION_ID_RE = /^[a-zA-Z0-9_@-][a-zA-Z0-9_@.-]*$/;
19593
20490
  async function pushImport(dbManager, sessionId, payload) {
19594
- if (!SAFE_SESSION_ID_RE.test(sessionId) || sessionId.includes("..")) {
19595
- return { ok: false, reason: "invalid_payload", message: "sessionId contains invalid characters" };
20491
+ try {
20492
+ return await withDataDirImportLock(dbManager.getUserDataDir(), async () => {
20493
+ const outcome = await executePushImportUnlocked(
20494
+ {
20495
+ getDbPath: (id) => dbManager.getDbPath(id),
20496
+ openDatabase: (id, options2) => dbManager.openRawSessionDatabase(id, options2),
20497
+ deleteDatabase: (id) => {
20498
+ dbManager.deleteSessionDatabaseFiles(id);
20499
+ }
20500
+ },
20501
+ sessionId,
20502
+ payload
20503
+ );
20504
+ if (!outcome.ok) return outcome;
20505
+ try {
20506
+ dbManager.raiseCurrentChatDbCompatibilityGate();
20507
+ } catch (error) {
20508
+ if (outcome.result.created) dbManager.deleteSessionDatabaseFiles(sessionId);
20509
+ throw error;
20510
+ }
20511
+ return outcome;
20512
+ });
20513
+ } catch (error) {
20514
+ if (error instanceof ImportInProgressError) {
20515
+ return {
20516
+ ok: false,
20517
+ reason: "import_in_progress",
20518
+ message: "Another import is already in progress"
20519
+ };
20520
+ }
20521
+ throw error;
19596
20522
  }
19597
- if (importInProgress.has(sessionId)) {
19598
- return {
19599
- ok: false,
19600
- reason: "import_in_progress",
19601
- message: "Another import is already in progress for this session"
19602
- };
20523
+ }
20524
+ async function executePushImportUnlocked(deps, sessionId, payload) {
20525
+ if (!isValidImportSessionId(sessionId)) {
20526
+ return { ok: false, reason: "invalid_payload", message: "sessionId contains invalid characters" };
19603
20527
  }
19604
- const dbPath = dbManager.getDbPath(sessionId);
19605
- const isNew = !fs32.existsSync(dbPath);
20528
+ const dbPath = deps.getDbPath(sessionId);
20529
+ const isNew = !fs34.existsSync(dbPath);
19606
20530
  const validationError = validatePayload(payload, isNew);
19607
20531
  if (validationError) {
19608
20532
  return { ok: false, reason: "invalid_payload", message: validationError };
19609
20533
  }
19610
- importInProgress.add(sessionId);
19611
20534
  try {
19612
20535
  if (isNew) {
19613
- const db2 = dbManager.openRawSessionDatabase(sessionId, { create: true });
20536
+ const db2 = deps.openDatabase(sessionId, { create: true });
19614
20537
  try {
19615
- const { writtenCount, duplicateCount, membersAdded } = fullImport(
19616
- db2,
19617
- payload.meta,
19618
- payload.members ?? [],
19619
- payload.messages
19620
- );
20538
+ const { writtenCount, duplicateCount } = fullImport(db2, payload.meta, payload.members ?? [], payload.messages);
19621
20539
  const session = queryStats(db2);
19622
- dbManager.raiseCurrentChatDbCompatibilityGate();
19623
- return {
20540
+ const membersAdded = session.memberCount;
20541
+ const outcome = {
19624
20542
  ok: true,
19625
20543
  result: {
19626
20544
  sessionId,
@@ -19630,16 +20548,17 @@ async function pushImport(dbManager, sessionId, payload) {
19630
20548
  updates: { metaUpdated: true, membersAdded, membersUpdated: 0 }
19631
20549
  }
19632
20550
  };
20551
+ appLogger.info("push-import", "Push import completed", outcome.result);
20552
+ return outcome;
19633
20553
  } finally {
19634
20554
  db2.close();
19635
20555
  }
19636
20556
  }
19637
- const db = dbManager.openRawSessionDatabase(sessionId, { readonly: false });
20557
+ const db = deps.openDatabase(sessionId, { readonly: false });
19638
20558
  try {
19639
20559
  const { writtenCount, duplicateCount, metaUpdated, membersAdded, membersUpdated } = incrementalImport2(db, payload);
19640
20560
  const session = queryStats(db);
19641
- dbManager.raiseCurrentChatDbCompatibilityGate();
19642
- return {
20561
+ const outcome = {
19643
20562
  ok: true,
19644
20563
  result: {
19645
20564
  sessionId,
@@ -19649,6 +20568,8 @@ async function pushImport(dbManager, sessionId, payload) {
19649
20568
  updates: { metaUpdated, membersAdded, membersUpdated }
19650
20569
  }
19651
20570
  };
20571
+ appLogger.info("push-import", "Push import completed", outcome.result);
20572
+ return outcome;
19652
20573
  } finally {
19653
20574
  db.close();
19654
20575
  }
@@ -19656,16 +20577,54 @@ async function pushImport(dbManager, sessionId, payload) {
19656
20577
  if (err instanceof DataDirCompatibilityError) throw err;
19657
20578
  if (isNew) {
19658
20579
  try {
19659
- dbManager.deleteSessionDatabaseFiles(sessionId);
20580
+ deps.deleteDatabase(sessionId);
19660
20581
  } catch {
19661
20582
  }
19662
20583
  }
20584
+ appLogger.error("push-import", "Push import failed", err);
19663
20585
  return { ok: false, reason: "import_failed", message: err instanceof Error ? err.message : String(err) };
19664
- } finally {
19665
- importInProgress.delete(sessionId);
19666
20586
  }
19667
20587
  }
19668
20588
 
20589
+ // ../../packages/node-runtime/src/services/import-idempotency.ts
20590
+ import { createHash as createHash4 } from "crypto";
20591
+ var DEFAULT_IMPORT_IDEMPOTENCY_TTL_MS = 60 * 60 * 1e3;
20592
+ function hashImportBody(body) {
20593
+ return createHash4("sha256").update(JSON.stringify(body ?? "")).digest("hex");
20594
+ }
20595
+ var ImportIdempotencyCache = class {
20596
+ constructor(ttlMs = DEFAULT_IMPORT_IDEMPOTENCY_TTL_MS) {
20597
+ this.ttlMs = ttlMs;
20598
+ }
20599
+ ttlMs;
20600
+ entries = /* @__PURE__ */ new Map();
20601
+ start(key, bodyHash, now = Date.now()) {
20602
+ this.prune(now);
20603
+ const entry = this.entries.get(key);
20604
+ if (!entry) {
20605
+ this.entries.set(key, { bodyHash, status: "pending", timestamp: now });
20606
+ return { status: "started" };
20607
+ }
20608
+ if (entry.bodyHash !== bodyHash) return { status: "conflict" };
20609
+ if (entry.status === "pending") return { status: "pending" };
20610
+ return { status: "success", response: entry.response };
20611
+ }
20612
+ success(key, response) {
20613
+ const entry = this.entries.get(key);
20614
+ if (!entry) return;
20615
+ entry.status = "success";
20616
+ entry.response = response;
20617
+ }
20618
+ fail(key) {
20619
+ this.entries.delete(key);
20620
+ }
20621
+ prune(now) {
20622
+ for (const [key, entry] of this.entries) {
20623
+ if (now - entry.timestamp > this.ttlMs) this.entries.delete(key);
20624
+ }
20625
+ }
20626
+ };
20627
+
19669
20628
  export {
19670
20629
  writeParseResultToDb,
19671
20630
  detectFormat,
@@ -19704,6 +20663,14 @@ export {
19704
20663
  streamParseFileInfo,
19705
20664
  analyzeIncrementalImport,
19706
20665
  incrementalImport,
20666
+ isValidImportSessionId,
20667
+ resolveAutoImportTarget,
20668
+ autoImportFile,
20669
+ analyzeAutoImportFile,
20670
+ IMPORT_LOCK_FILENAME,
20671
+ IMPORT_IN_PROGRESS_ERROR_KEY,
20672
+ ImportInProgressError,
20673
+ withDataDirImportLock,
19707
20674
  ArchiveImportError,
19708
20675
  validateArchiveEntryName,
19709
20676
  ZipArchiveReader,
@@ -19809,6 +20776,8 @@ export {
19809
20776
  contacts_exports,
19810
20777
  createPeopleRelationshipsService,
19811
20778
  relationships_exports,
20779
+ createGlobalInsightService,
20780
+ global_insight_exports,
19812
20781
  CHUNK_VECTOR_INDEX_TABLE,
19813
20782
  CHUNK_VECTOR_INDEX_INDEXES,
19814
20783
  EMBEDDING_INDEX_SCHEMA,
@@ -19866,6 +20835,10 @@ export {
19866
20835
  createSemanticIndexService,
19867
20836
  SemanticIndexService,
19868
20837
  MergeSessionCache,
19869
- pushImport
20838
+ pushImport,
20839
+ executePushImportUnlocked,
20840
+ DEFAULT_IMPORT_IDEMPOTENCY_TTL_MS,
20841
+ hashImportBody,
20842
+ ImportIdempotencyCache
19870
20843
  };
19871
- //# sourceMappingURL=chunk-36JGKW6K.mjs.map
20844
+ //# sourceMappingURL=chunk-KCPGEILH.mjs.map