chatlab-cli 0.30.2 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/dist/{chat-command-VBDMWOKL.mjs → chat-command-CHV7RLBH.mjs} +9 -8
  2. package/dist/{chat-command-VBDMWOKL.mjs.map → chat-command-CHV7RLBH.mjs.map} +1 -1
  3. package/dist/{chunk-326OCJOT.mjs → chunk-DA5T64RY.mjs} +2 -2
  4. package/dist/{chunk-M2PXDX4G.mjs → chunk-DBBL74RK.mjs} +31 -22
  5. package/dist/chunk-DBBL74RK.mjs.map +1 -0
  6. package/dist/{chunk-KBSAVUFV.mjs → chunk-EXD2SXWL.mjs} +89 -15
  7. package/dist/chunk-EXD2SXWL.mjs.map +1 -0
  8. package/dist/{chunk-D3DPCDII.mjs → chunk-JH5RAJT2.mjs} +83 -34
  9. package/dist/chunk-JH5RAJT2.mjs.map +1 -0
  10. package/dist/{chunk-5EE2EFPE.mjs → chunk-RD335EMA.mjs} +4 -4
  11. package/dist/{chunk-WV5GSRQM.mjs → chunk-RDCUGC24.mjs} +5 -15
  12. package/dist/chunk-RDCUGC24.mjs.map +1 -0
  13. package/dist/{chunk-TEX44HNL.mjs → chunk-SNCXU2QL.mjs} +5 -15
  14. package/dist/chunk-SNCXU2QL.mjs.map +1 -0
  15. package/dist/{chunk-36JGKW6K.mjs → chunk-T64GA5HP.mjs} +1134 -364
  16. package/dist/chunk-T64GA5HP.mjs.map +1 -0
  17. package/dist/{chunk-LVEJHB3X.mjs → chunk-UWWFOEGF.mjs} +494 -97
  18. package/dist/chunk-UWWFOEGF.mjs.map +1 -0
  19. package/dist/{chunk-ULNRUS55.mjs → chunk-WQCY45U2.mjs} +3 -3
  20. package/dist/chunk-YAQLYL52.mjs +14 -0
  21. package/dist/chunk-YAQLYL52.mjs.map +1 -0
  22. package/dist/{chunk-TCZHG3G7.mjs → chunk-YPQ72HMX.mjs} +2 -2
  23. package/dist/cli.mjs +8 -7
  24. package/dist/contacts-worker.mjs +2 -2
  25. package/dist/global-insight-worker.mjs +179 -0
  26. package/dist/global-insight-worker.mjs.map +1 -0
  27. package/dist/{http-L4AMELPZ.mjs → http-3B5ARN7A.mjs} +12 -11
  28. package/dist/{import-WWCSXGVS.mjs → import-2DZGNSEX.mjs} +10 -7
  29. package/dist/index.mjs +12 -11
  30. package/dist/{mcp-A4M3LT5O.mjs → mcp-44I6FUUQ.mjs} +12 -11
  31. package/dist/{mcp-A4M3LT5O.mjs.map → mcp-44I6FUUQ.mjs.map} +1 -1
  32. package/dist/people-relationships-worker.mjs +2 -2
  33. package/dist/semantic-index-worker.mjs +1 -1
  34. package/dist/{src-MPNBTFOE.mjs → src-R3QX6L3L.mjs} +26 -6
  35. package/dist/{stream-import-HIE7PNL4.mjs → stream-import-VUEL2TTU.mjs} +10 -7
  36. package/dist/{worker-runtime-BJXO24XX.mjs → worker-runtime-GJAOIIZW.mjs} +6 -5
  37. package/dist/{worker-runtime-BJXO24XX.mjs.map → worker-runtime-GJAOIIZW.mjs.map} +1 -1
  38. package/dist-web/assets/ActionToolsPanel-fAio70bI.css +1 -0
  39. package/dist-web/assets/ActionToolsPanel.vue_vue_type_script_setup_true_lang-CdJT0t2f.js +110 -0
  40. package/dist-web/assets/{BufferResource-q3rzaWWd.js → BufferResource-BPGkshPa.js} +1 -1
  41. package/dist-web/assets/{CanvasRenderer-BRk1y8tp.js → CanvasRenderer-DTKkZhq4.js} +1 -1
  42. package/dist-web/assets/{EChartWordcloud-Clg60FMo.js → EChartWordcloud-DU3ZYdgz.js} +1 -1
  43. package/dist-web/assets/{Filter-BJwode73.js → Filter-CHXKzS2l.js} +1 -1
  44. 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
  45. package/dist-web/assets/MessageExportModal-h-zl0dx1.js +1 -0
  46. package/dist-web/assets/PageHeader-CK0hTsdx.js +1 -0
  47. package/dist-web/assets/PageHeader-XIXMTomn.css +1 -0
  48. package/dist-web/assets/{RelationshipGalaxyCanvas-CORMD7vL.js → RelationshipGalaxyCanvas-Dwdy5flh.js} +3 -3
  49. package/dist-web/assets/{RelationshipGalaxyThreeCanvas-B_w_P4ob.js → RelationshipGalaxyThreeCanvas-CtY4TGen.js} +1 -1
  50. package/dist-web/assets/{RenderTargetSystem-CGhV9R9E.js → RenderTargetSystem-BE56mKRG.js} +1 -1
  51. package/dist-web/assets/ThemeCard.vue_vue_type_script_setup_true_lang-BoZyssJF.js +1 -0
  52. package/dist-web/assets/TimeSelect.vue_vue_type_script_setup_true_lang-27hMyAUE.js +1 -0
  53. package/dist-web/assets/{WebGLRenderer-CAokTE5X.js → WebGLRenderer-Dm18dxOf.js} +1 -1
  54. package/dist-web/assets/{WebGPURenderer-DzP4ggSy.js → WebGPURenderer-BF6TF-is.js} +1 -1
  55. package/dist-web/assets/annual-summary-time-range-DVLXK1r4.js +1 -0
  56. package/dist-web/assets/base-CFPyD9Gy.js +1 -0
  57. package/dist-web/assets/{browserAll-CAZCK6dd.js → browserAll-Cn_CfkXl.js} +1 -1
  58. package/dist-web/assets/dateFormat-BcqNKTQ1.js +1 -0
  59. package/dist-web/assets/{electron-CkPDbAaS.js → electron-B6xOabTq.js} +1 -1
  60. package/dist-web/assets/electron-hcWiB7Gg.js +1 -0
  61. package/dist-web/assets/fetch-BPkgnY0R.js +1 -0
  62. package/dist-web/assets/{fetch-LX9jrJCC.js → fetch-BRXmDI_2.js} +1 -1
  63. package/dist-web/assets/{fetch-Apd5OLJw.js → fetch-BUm1isWe.js} +1 -1
  64. package/dist-web/assets/{fetch-B8rik0TM.js → fetch-CBOJwwJp.js} +3 -3
  65. package/dist-web/assets/{fetch-wqMSiaK3.js → fetch-CEhUu1Yu.js} +1 -1
  66. package/dist-web/assets/fetch-C_EfVFJi.js +3 -0
  67. package/dist-web/assets/{fetch-DQ9UNNAy.js → fetch-DL_u25Ui.js} +1 -1
  68. package/dist-web/assets/{fetch-BIEXkvDq.js → fetch-DMLq3xPG.js} +1 -1
  69. package/dist-web/assets/{fetch--UbRoBT7.js → fetch-DylpNKCR.js} +1 -1
  70. package/dist-web/assets/fetch-etgwVKIw.js +1 -0
  71. package/dist-web/assets/index-0x1QPzKZ.js +1 -0
  72. package/dist-web/assets/index-B5S9FnfP.js +1 -0
  73. package/dist-web/assets/index-BS6loLHW.js +1 -0
  74. package/dist-web/assets/index-BfO_LpDv.css +1 -0
  75. package/dist-web/assets/index-BrTlAl8b.js +1 -0
  76. package/dist-web/assets/index-ByUaiVQK.css +1 -0
  77. package/dist-web/assets/{index-S-YQRH1E.js → index-C3TnydSt.js} +1 -1
  78. package/dist-web/assets/index-CAWO10k1.js +2 -0
  79. package/dist-web/assets/index-C_0GHEcb.css +1 -0
  80. package/dist-web/assets/index-Chm17wNe.js +2 -0
  81. package/dist-web/assets/index-DAr_2DlV.js +173 -0
  82. package/dist-web/assets/index-Em6upenM.js +64 -0
  83. package/dist-web/assets/index-FoTy2Cce.js +1 -0
  84. package/dist-web/assets/index-YADL4XIp.js +22 -0
  85. package/dist-web/assets/index-ZsZwPLWy.css +1 -0
  86. package/dist-web/assets/index-_6lck7Bv.css +1 -0
  87. package/dist-web/assets/index-aMIgqj43.js +1 -0
  88. package/dist-web/assets/index-y7cxH1Et.css +1 -0
  89. package/dist-web/assets/index-zZVsV5HB.js +1 -0
  90. package/dist-web/assets/{people-page-header-Mupn-U_b.js → people-page-header-BdKM1qhX.js} +1 -1
  91. package/dist-web/assets/types-CZnACqaB.js +1 -0
  92. package/dist-web/assets/{vendor-echarts-hJVnODyU.js → vendor-echarts-B-i_56Av.js} +1 -1
  93. package/dist-web/assets/{vendor-echarts-wordcloud-k5ih2Jl4.js → vendor-echarts-wordcloud-Ca8n6qS6.js} +1 -1
  94. package/dist-web/assets/{vendor-nuxt-ui-DaLxYaC4.js → vendor-nuxt-ui-CS1uxE9C.js} +2 -2
  95. package/dist-web/assets/{vendor-reka-ui-CzM4kDQ7.js → vendor-reka-ui-Cta6JPvN.js} +1 -1
  96. package/dist-web/assets/{web-DbKs6-1b.js → web-BUZ-dZy0.js} +1 -1
  97. package/dist-web/assets/{webworkerAll-Bhv-hBqR.js → webworkerAll-BKGP--A4.js} +1 -1
  98. package/dist-web/index.html +4 -4
  99. package/package.json +1 -1
  100. package/dist/chunk-36JGKW6K.mjs.map +0 -1
  101. package/dist/chunk-D3DPCDII.mjs.map +0 -1
  102. package/dist/chunk-KBSAVUFV.mjs.map +0 -1
  103. package/dist/chunk-LVEJHB3X.mjs.map +0 -1
  104. package/dist/chunk-M2PXDX4G.mjs.map +0 -1
  105. package/dist/chunk-TEX44HNL.mjs.map +0 -1
  106. package/dist/chunk-WV5GSRQM.mjs.map +0 -1
  107. package/dist-web/assets/DebugToolsPanel-D_lFIW6j.css +0 -1
  108. package/dist-web/assets/DebugToolsPanel.vue_vue_type_script_setup_true_lang-F4n0zNX2.js +0 -110
  109. package/dist-web/assets/MessageExportModal-CGcdMOyw.js +0 -1
  110. package/dist-web/assets/PageHeader-Bbn87QZS.css +0 -1
  111. package/dist-web/assets/PageHeader-CZaqOdBQ.js +0 -1
  112. package/dist-web/assets/electron-D9vE5t3H.js +0 -1
  113. package/dist-web/assets/fetch-1pB-HbYw.js +0 -3
  114. package/dist-web/assets/fetch-CF_3ZqRa.js +0 -1
  115. package/dist-web/assets/fetch-FSNxSnAp.js +0 -1
  116. package/dist-web/assets/index---rXG1hZ.css +0 -1
  117. package/dist-web/assets/index-B01uUoHE.js +0 -1
  118. package/dist-web/assets/index-B4l3rHie.css +0 -1
  119. package/dist-web/assets/index-BSIyyt0z.js +0 -1
  120. package/dist-web/assets/index-Bh8z441f.js +0 -2
  121. package/dist-web/assets/index-C0rya8v4.js +0 -1
  122. package/dist-web/assets/index-C1p9CbmN.css +0 -1
  123. package/dist-web/assets/index-COpza1A4.css +0 -1
  124. package/dist-web/assets/index-CZbZv2fh.js +0 -64
  125. package/dist-web/assets/index-CqUuHQBQ.js +0 -22
  126. package/dist-web/assets/index-CzeTP5MJ.js +0 -173
  127. package/dist-web/assets/index-DgNxTM_8.js +0 -2
  128. package/dist-web/assets/index-DosG7LmJ.css +0 -1
  129. /package/dist/{chunk-326OCJOT.mjs.map → chunk-DA5T64RY.mjs.map} +0 -0
  130. /package/dist/{chunk-5EE2EFPE.mjs.map → chunk-RD335EMA.mjs.map} +0 -0
  131. /package/dist/{chunk-ULNRUS55.mjs.map → chunk-WQCY45U2.mjs.map} +0 -0
  132. /package/dist/{chunk-TCZHG3G7.mjs.map → chunk-YPQ72HMX.mjs.map} +0 -0
  133. /package/dist/{http-L4AMELPZ.mjs.map → http-3B5ARN7A.mjs.map} +0 -0
  134. /package/dist/{import-WWCSXGVS.mjs.map → import-2DZGNSEX.mjs.map} +0 -0
  135. /package/dist/{src-MPNBTFOE.mjs.map → src-R3QX6L3L.mjs.map} +0 -0
  136. /package/dist/{stream-import-HIE7PNL4.mjs.map → stream-import-VUEL2TTU.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-SNCXU2QL.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-RDCUGC24.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-UWWFOEGF.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 };
@@ -5502,6 +5518,14 @@ function extractNameFromFilePath5(filePath) {
5502
5518
  if (matchZhTw) return matchZhTw[1].trim();
5503
5519
  return basename13.replace(/\.txt$/i, "") || "\u672A\u77E5\u804A\u5929";
5504
5520
  }
5521
+ function shouldInferPrivateChatName(filePath, fallbackName) {
5522
+ const basename13 = path8.basename(filePath);
5523
+ return /^_chat\.txt$/i.test(basename13) || /^whatsapp(?:[-_\s].*)?$/i.test(fallbackName);
5524
+ }
5525
+ function inferPrivateChatName(memberMap, fallbackName) {
5526
+ const member = Array.from(memberMap.values()).find((item) => item.platformId !== "system");
5527
+ return member?.nickname || fallbackName;
5528
+ }
5505
5529
  var feature10 = {
5506
5530
  id: "whatsapp-native-txt",
5507
5531
  name: "WhatsApp \u5B98\u65B9\u5BFC\u51FA (TXT)",
@@ -5520,9 +5544,9 @@ var feature10 = {
5520
5544
  // 英文加密提示(WhatsApp 独有)
5521
5545
  /你发送给自己的消息已进行端到端加密/,
5522
5546
  // 简体中文自己对话提示(WhatsApp 独有)
5523
- /\d{1,4}\/\d{1,2}\/\d{2,4},?\s+\d{1,2}:\d{2} - /,
5547
+ /\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
5548
  // 消息行格式特征(无方括号,含 " - " 分隔符,WhatsApp 独有)
5525
- /\[\d{1,4}\/\d{1,2}\/\d{2,4}[\s,].*\d{1,2}:\d{2}:\d{2}.*\] /
5549
+ /\[\d{1,4}\/\d{1,2}\/\d{1,4}[\s,].*\d{1,2}:\d{2}(?::\d{2})?.*\] /
5526
5550
  // 消息行格式特征(方括号 + 含日期和时间的时间戳,兼容各地区变体)
5527
5551
  ],
5528
5552
  // 文件名特征:与xxx的 WhatsApp 聊天.txt
@@ -5532,9 +5556,9 @@ var feature10 = {
5532
5556
  function cleanLine(line) {
5533
5557
  return line.replace(/^(?:\uFEFF|\u200E|\u200F|\u200B|\u200C|\u200D|\u2060)+/, "").trim();
5534
5558
  }
5535
- var MESSAGE_LINE_REGEX_V1 = /^(\d{1,4}\/\d{1,2}\/\d{2,4},?\s+\d{1,2}:\d{2}) - (.+)$/;
5559
+ 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
5560
  var MESSAGE_LINE_REGEX_V2 = /^\[([^\]]+)\] (.+)$/;
5537
- var SENDER_CONTENT_REGEX = /^(.+?):[\s\u200E]+(.*)$/;
5561
+ var SENDER_CONTENT_REGEX = /^(.+?):[\s\u200E]*(.*)$/;
5538
5562
  var SYSTEM_MESSAGE_PATTERNS = [
5539
5563
  // 简体中文系统消息
5540
5564
  /消息和通话已进行端到端加密/,
@@ -5586,12 +5610,12 @@ function detectMessageType2(content) {
5586
5610
  }
5587
5611
  var AMPM_MARKERS = [
5588
5612
  [/\bPM\b/i, true],
5589
- [/\bP\.M\.\b/i, true],
5613
+ [/\bP\.M\.(?!\w)/i, true],
5590
5614
  [/下午/, true],
5591
5615
  [/午後/, true],
5592
5616
  [/오후/, true],
5593
5617
  [/\bAM\b/i, false],
5594
- [/\bA\.M\.\b/i, false],
5618
+ [/\bA\.M\.(?!\w)/i, false],
5595
5619
  [/上午/, false],
5596
5620
  [/午前/, false],
5597
5621
  [/오전/, false]
@@ -5607,7 +5631,7 @@ function parseFlexibleV2Timestamp(raw) {
5607
5631
  }
5608
5632
  }
5609
5633
  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})?)$/);
5634
+ const match = str.match(/^(\d{1,4}\/\d{1,2}\/\d{1,4})\s+(\d{1,2}:\d{2}(?::\d{2})?)$/);
5611
5635
  if (!match) return null;
5612
5636
  const [, datePart, timePart] = match;
5613
5637
  const dateParts = datePart.split("/").map((s) => parseInt(s, 10));
@@ -5636,32 +5660,7 @@ function parseFlexibleV2Timestamp(raw) {
5636
5660
  return isNaN(ts) ? null : ts;
5637
5661
  }
5638
5662
  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;
5663
+ return parseFlexibleV2Timestamp(timeStr) ?? 0;
5665
5664
  }
5666
5665
  async function* parseWhatsApp(options2) {
5667
5666
  const { filePath, batchSize = 5e3, onProgress, onLog } = options2;
@@ -5673,7 +5672,7 @@ async function* parseWhatsApp(options2) {
5673
5672
  yield { type: "progress", data: initialProgress };
5674
5673
  onProgress?.(initialProgress);
5675
5674
  onLog?.("info", `\u5F00\u59CB\u89E3\u6790 WhatsApp TXT \u6587\u4EF6\uFF0C\u5927\u5C0F: ${(totalBytes / 1024 / 1024).toFixed(2)} MB`);
5676
- const chatName = extractNameFromFilePath5(filePath);
5675
+ const fileChatName = extractNameFromFilePath5(filePath);
5677
5676
  const memberMap = /* @__PURE__ */ new Map();
5678
5677
  const messages = [];
5679
5678
  let currentMessage = null;
@@ -5772,6 +5771,7 @@ async function* parseWhatsApp(options2) {
5772
5771
  const hasSystemMember = memberMap.has("system");
5773
5772
  const realMemberCount = hasSystemMember ? memberMap.size - 1 : memberMap.size;
5774
5773
  const chatType = realMemberCount > 2 ? "group" /* GROUP */ : "private" /* PRIVATE */;
5774
+ const chatName = chatType === "private" /* PRIVATE */ && shouldInferPrivateChatName(filePath, fileChatName) ? inferPrivateChatName(memberMap, fileChatName) : fileChatName;
5775
5775
  const meta = {
5776
5776
  name: chatName,
5777
5777
  platform: KNOWN_PLATFORMS.WHATSAPP,
@@ -7410,6 +7410,35 @@ function searchByFts(db, keywords, limit = 1e3, offset = 0) {
7410
7410
 
7411
7411
  // ../../packages/node-runtime/src/import/streaming-importer.ts
7412
7412
  import * as fs20 from "fs";
7413
+
7414
+ // ../../packages/node-runtime/src/import/message-deduplicator.ts
7415
+ function generateFallbackMessageKey(message) {
7416
+ const contentKey = generateMessageKey(message.timestamp, message.senderPlatformId, message.content);
7417
+ return JSON.stringify([contentKey, message.type, message.replyToMessageId || null]);
7418
+ }
7419
+ function createMessageDedupState(platformMessageIds = [], fallbackKeys = [], fallbackOnlyKeys = []) {
7420
+ return {
7421
+ platformMessageIds: new Set(platformMessageIds),
7422
+ fallbackKeys: new Set(fallbackKeys),
7423
+ fallbackOnlyKeys: new Set(fallbackOnlyKeys)
7424
+ };
7425
+ }
7426
+ function registerMessageAndCheckDuplicate(message, state) {
7427
+ const key = generateFallbackMessageKey(message);
7428
+ if (message.platformMessageId) {
7429
+ if (state.platformMessageIds.has(message.platformMessageId)) return true;
7430
+ state.platformMessageIds.add(message.platformMessageId);
7431
+ if (state.fallbackOnlyKeys.delete(key)) return true;
7432
+ state.fallbackKeys.add(key);
7433
+ return false;
7434
+ }
7435
+ if (state.fallbackKeys.has(key)) return true;
7436
+ state.fallbackKeys.add(key);
7437
+ state.fallbackOnlyKeys.add(key);
7438
+ return false;
7439
+ }
7440
+
7441
+ // ../../packages/node-runtime/src/import/streaming-importer.ts
7413
7442
  var BATCH_COMMIT_SIZE = 5e4;
7414
7443
  var CHECKPOINT_INTERVAL = 2e5;
7415
7444
  var yieldToEventLoop = () => new Promise((resolve2) => setImmediate(resolve2));
@@ -7418,6 +7447,28 @@ function defaultGenerateSessionId() {
7418
7447
  const rand = Math.random().toString(36).substring(2, 8);
7419
7448
  return `chat_${ts}_${rand}`;
7420
7449
  }
7450
+ function prepareMessageForCreate(message) {
7451
+ if (!message.senderPlatformId) return { skipCounter: "skippedNoSenderId" };
7452
+ if (!message.senderAccountName) return { skipCounter: "skippedNoAccountName" };
7453
+ if (message.timestamp === void 0 || message.timestamp === null || isNaN(message.timestamp)) {
7454
+ return { skipCounter: "skippedInvalidTimestamp" };
7455
+ }
7456
+ if (message.type === void 0 || message.type === null) return { skipCounter: "skippedNoType" };
7457
+ let content = null;
7458
+ if (message.content != null) {
7459
+ content = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
7460
+ }
7461
+ return {
7462
+ message: {
7463
+ platformMessageId: message.platformMessageId,
7464
+ timestamp: message.timestamp,
7465
+ senderPlatformId: message.senderPlatformId,
7466
+ type: message.type,
7467
+ content,
7468
+ replyToMessageId: message.replyToMessageId
7469
+ }
7470
+ };
7471
+ }
7421
7472
  async function streamingImport(filePath, deps, formatOptions, externalSessionId) {
7422
7473
  if (formatOptions?.formatId) {
7423
7474
  const formatId = formatOptions.formatId;
@@ -7494,11 +7545,12 @@ async function streamImportSingle(filePath, deps, formatFeature, formatOptions,
7494
7545
  `INSERT INTO meta (name, platform, type, imported_at, group_id, group_avatar, owner_id) VALUES (?, ?, ?, ?, ?, ?, ?)`
7495
7546
  );
7496
7547
  const insertMember = db.prepare(
7497
- `INSERT INTO member (platform_id, account_name, group_nickname, avatar, roles)
7498
- VALUES (?, ?, ?, ?, ?)
7548
+ `INSERT INTO member (platform_id, account_name, group_nickname, aliases, avatar, roles)
7549
+ VALUES (?, ?, ?, ?, ?, ?)
7499
7550
  ON CONFLICT(platform_id) DO UPDATE SET
7500
7551
  account_name = COALESCE(NULLIF(excluded.account_name, ''), account_name),
7501
7552
  group_nickname = COALESCE(NULLIF(excluded.group_nickname, ''), group_nickname),
7553
+ aliases = CASE WHEN excluded.aliases != '[]' THEN excluded.aliases ELSE aliases END,
7502
7554
  avatar = COALESCE(NULLIF(excluded.avatar, ''), avatar),
7503
7555
  roles = CASE WHEN excluded.roles != '[]' THEN excluded.roles ELSE roles END`
7504
7556
  );
@@ -7518,6 +7570,7 @@ async function streamImportSingle(filePath, deps, formatFeature, formatOptions,
7518
7570
  let metaInserted = false;
7519
7571
  let messageCountInBatch = 0;
7520
7572
  let totalMessageCount = 0;
7573
+ let duplicateCount = 0;
7521
7574
  let lastCheckpointCount = 0;
7522
7575
  let inTransaction = false;
7523
7576
  const beginTransaction = () => {
@@ -7569,6 +7622,7 @@ async function streamImportSingle(filePath, deps, formatFeature, formatOptions,
7569
7622
  skippedInvalidTimestamp: 0,
7570
7623
  skippedNoType: 0
7571
7624
  };
7625
+ const dedupState = createMessageDedupState();
7572
7626
  logger?.info("Starting streamParseFile...");
7573
7627
  try {
7574
7628
  await streamParseFile(
@@ -7613,6 +7667,7 @@ async function streamImportSingle(filePath, deps, formatFeature, formatOptions,
7613
7667
  member.platformId,
7614
7668
  member.accountName || null,
7615
7669
  member.groupNickname || null,
7670
+ member.aliases ? JSON.stringify(member.aliases) : "[]",
7616
7671
  member.avatar || null,
7617
7672
  member.roles ? JSON.stringify(member.roles) : "[]"
7618
7673
  );
@@ -7634,20 +7689,14 @@ async function streamImportSingle(filePath, deps, formatFeature, formatOptions,
7634
7689
  let memberInsertCount = 0;
7635
7690
  let nicknameChangeCount = 0;
7636
7691
  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++;
7692
+ const prepared = prepareMessageForCreate(msg);
7693
+ if ("skipCounter" in prepared) {
7694
+ callbackStats[prepared.skipCounter]++;
7647
7695
  continue;
7648
7696
  }
7649
- if (msg.type === void 0 || msg.type === null) {
7650
- callbackStats.skippedNoType++;
7697
+ const dedupMessage = prepared.message;
7698
+ if (registerMessageAndCheckDuplicate(dedupMessage, dedupState)) {
7699
+ duplicateCount++;
7651
7700
  continue;
7652
7701
  }
7653
7702
  let t0 = Date.now();
@@ -7656,6 +7705,7 @@ async function streamImportSingle(filePath, deps, formatFeature, formatOptions,
7656
7705
  msg.senderPlatformId,
7657
7706
  msg.senderAccountName || null,
7658
7707
  msg.senderGroupNickname || null,
7708
+ "[]",
7659
7709
  null,
7660
7710
  "[]"
7661
7711
  );
@@ -7669,18 +7719,14 @@ async function streamImportSingle(filePath, deps, formatFeature, formatOptions,
7669
7719
  }
7670
7720
  const senderId = memberIdMap.get(msg.senderPlatformId);
7671
7721
  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
7722
  t0 = Date.now();
7677
7723
  insertMessage.run(
7678
7724
  senderId,
7679
7725
  msg.senderAccountName || null,
7680
7726
  msg.senderGroupNickname || null,
7681
- msg.timestamp,
7682
- msg.type,
7683
- safeContent,
7727
+ dedupMessage.timestamp,
7728
+ dedupMessage.type,
7729
+ dedupMessage.content,
7684
7730
  msg.replyToMessageId || null,
7685
7731
  msg.platformMessageId || null
7686
7732
  );
@@ -7797,6 +7843,7 @@ async function streamImportSingle(filePath, deps, formatFeature, formatOptions,
7797
7843
  logger?.info(` invalid timestamp: ${callbackStats.skippedInvalidTimestamp}`);
7798
7844
  if (callbackStats.skippedNoType > 0) logger?.info(` missing type: ${callbackStats.skippedNoType}`);
7799
7845
  }
7846
+ if (duplicateCount > 0) logger?.info(`Duplicate messages skipped: ${duplicateCount}`);
7800
7847
  logger?.summary(totalMessageCount, memberIdMap.size);
7801
7848
  if (totalMessageCount === 0) {
7802
7849
  logger?.error(
@@ -7829,6 +7876,7 @@ async function streamImportSingle(filePath, deps, formatFeature, formatOptions,
7829
7876
  detectedFormat: formatFeature ? `${formatFeature.name} (${formatFeature.id})` : null,
7830
7877
  messagesReceived: callbackStats.totalMessagesReceived,
7831
7878
  messagesWritten: totalMessageCount,
7879
+ duplicateCount,
7832
7880
  messagesSkipped: callbackStats.skippedNoSenderId + callbackStats.skippedNoAccountName + callbackStats.skippedInvalidTimestamp + callbackStats.skippedNoType,
7833
7881
  skipReasons: {
7834
7882
  noSenderId: callbackStats.skippedNoSenderId,
@@ -7845,11 +7893,21 @@ async function streamImportSingle(filePath, deps, formatFeature, formatOptions,
7845
7893
  async function analyzeNewImport(filePath, onProgress) {
7846
7894
  const formatFeature = detectFormat(filePath);
7847
7895
  if (!formatFeature) {
7848
- return { totalMessages: 0, totalMembers: 0, meta: null, error: "error.unrecognized_format" };
7896
+ return {
7897
+ totalMessages: 0,
7898
+ newMessageCount: 0,
7899
+ duplicateCount: 0,
7900
+ totalMembers: 0,
7901
+ meta: null,
7902
+ error: "error.unrecognized_format"
7903
+ };
7849
7904
  }
7850
7905
  let meta = null;
7851
7906
  const memberSet = /* @__PURE__ */ new Set();
7907
+ const dedupState = createMessageDedupState();
7852
7908
  let totalMessages = 0;
7909
+ let newMessageCount = 0;
7910
+ let duplicateCount = 0;
7853
7911
  await streamParseFile(filePath, {
7854
7912
  onMeta: (parsedMeta) => {
7855
7913
  meta = { name: parsedMeta.name, platform: parsedMeta.platform, type: parsedMeta.type };
@@ -7864,10 +7922,17 @@ async function analyzeNewImport(filePath, onProgress) {
7864
7922
  for (const msg of batch) {
7865
7923
  totalMessages++;
7866
7924
  if (!memberSet.has(msg.senderPlatformId)) memberSet.add(msg.senderPlatformId);
7925
+ const prepared = prepareMessageForCreate(msg);
7926
+ if ("skipCounter" in prepared) continue;
7927
+ if (registerMessageAndCheckDuplicate(prepared.message, dedupState)) {
7928
+ duplicateCount++;
7929
+ } else {
7930
+ newMessageCount++;
7931
+ }
7867
7932
  }
7868
7933
  }
7869
7934
  });
7870
- return { totalMessages, totalMembers: memberSet.size, meta };
7935
+ return { totalMessages, newMessageCount, duplicateCount, totalMembers: memberSet.size, meta };
7871
7936
  }
7872
7937
  async function streamParseFileInfo(filePath, deps) {
7873
7938
  const formatFeature = detectFormat(filePath);
@@ -8028,32 +8093,38 @@ function countHistory(tracker) {
8028
8093
  function loadExistingDedup(db) {
8029
8094
  const existingPlatformMsgIds = /* @__PURE__ */ new Set();
8030
8095
  const existingKeys = /* @__PURE__ */ new Set();
8096
+ const existingFallbackOnlyKeys = /* @__PURE__ */ new Set();
8031
8097
  const pmidRows = db.prepare("SELECT platform_message_id FROM message WHERE platform_message_id IS NOT NULL").all();
8032
8098
  for (const row of pmidRows) {
8033
8099
  existingPlatformMsgIds.add(row.platform_message_id);
8034
8100
  }
8035
8101
  const hashRows = db.prepare(
8036
- `SELECT ts, m.platform_id as sender_platform_id, content
8102
+ `SELECT ts, m.platform_id as sender_platform_id, msg.type, content, reply_to_message_id,
8103
+ platform_message_id
8037
8104
  FROM message msg
8038
8105
  JOIN member m ON msg.sender_id = m.id`
8039
8106
  ).all();
8040
8107
  for (const row of hashRows) {
8041
- existingKeys.add(generateMessageKey(row.ts, row.sender_platform_id, row.content));
8108
+ const key = generateFallbackMessageKey({
8109
+ timestamp: row.ts,
8110
+ senderPlatformId: row.sender_platform_id,
8111
+ type: row.type,
8112
+ content: row.content,
8113
+ replyToMessageId: row.reply_to_message_id ?? void 0
8114
+ });
8115
+ existingKeys.add(key);
8116
+ if (!row.platform_message_id) existingFallbackOnlyKeys.add(key);
8042
8117
  }
8043
- return { existingPlatformMsgIds, existingKeys };
8118
+ return { existingPlatformMsgIds, existingKeys, existingFallbackOnlyKeys };
8044
8119
  }
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;
8120
+ function isDuplicate(msg, existingPlatformMsgIds, existingKeys, existingFallbackOnlyKeys) {
8121
+ return registerMessageAndCheckDuplicate(msg, {
8122
+ platformMessageIds: existingPlatformMsgIds,
8123
+ fallbackKeys: existingKeys,
8124
+ fallbackOnlyKeys: existingFallbackOnlyKeys
8125
+ });
8055
8126
  }
8056
- function normalizeTimestamp(timestamp) {
8127
+ function normalizeImportTimestamp(timestamp) {
8057
8128
  const value = typeof timestamp === "string" && timestamp.trim() !== "" ? Number(timestamp) : timestamp;
8058
8129
  return typeof value === "number" && value > 0 && Number.isFinite(value) ? value : null;
8059
8130
  }
@@ -8068,7 +8139,7 @@ async function analyzeIncrementalImport(sessionId, filePath, deps) {
8068
8139
  } catch {
8069
8140
  return { error: "error.session_not_found", newMessageCount: 0, duplicateCount: 0, totalInFile: 0 };
8070
8141
  }
8071
- const { existingPlatformMsgIds, existingKeys } = loadExistingDedup(db);
8142
+ const { existingPlatformMsgIds, existingKeys, existingFallbackOnlyKeys } = loadExistingDedup(db);
8072
8143
  db.close();
8073
8144
  let totalInFile = 0;
8074
8145
  let newMessageCount = 0;
@@ -8084,9 +8155,9 @@ async function analyzeIncrementalImport(sessionId, filePath, deps) {
8084
8155
  onMessageBatch: (batch) => {
8085
8156
  for (const msg of batch) {
8086
8157
  totalInFile++;
8087
- const timestamp = normalizeTimestamp(msg.timestamp);
8158
+ const timestamp = normalizeImportTimestamp(msg.timestamp);
8088
8159
  if (timestamp === null) continue;
8089
- if (isDuplicate({ ...msg, timestamp }, existingPlatformMsgIds, existingKeys)) {
8160
+ if (isDuplicate({ ...msg, timestamp }, existingPlatformMsgIds, existingKeys, existingFallbackOnlyKeys)) {
8090
8161
  duplicateCount++;
8091
8162
  } else {
8092
8163
  newMessageCount++;
@@ -8097,7 +8168,7 @@ async function analyzeIncrementalImport(sessionId, filePath, deps) {
8097
8168
  return { newMessageCount, duplicateCount, totalInFile };
8098
8169
  }
8099
8170
  async function incrementalImport(sessionId, filePath, deps, options2) {
8100
- const formatFeature = detectFormat(filePath);
8171
+ const formatFeature = options2?.formatId ? getFormatFeatureById(options2.formatId) : detectFormat(filePath);
8101
8172
  if (!formatFeature) {
8102
8173
  return { success: false, newMessageCount: 0, error: "error.unrecognized_format" };
8103
8174
  }
@@ -8118,18 +8189,19 @@ async function incrementalImport(sessionId, filePath, deps, options2) {
8118
8189
  }
8119
8190
  };
8120
8191
  var trackError = trackError2;
8121
- const { existingPlatformMsgIds, existingKeys } = loadExistingDedup(db);
8192
+ const { existingPlatformMsgIds, existingKeys, existingFallbackOnlyKeys } = loadExistingDedup(db);
8122
8193
  const memberIdMap = /* @__PURE__ */ new Map();
8123
8194
  const existingMembers = db.prepare("SELECT id, platform_id FROM member").all();
8124
8195
  for (const m of existingMembers) {
8125
8196
  memberIdMap.set(m.platform_id, m.id);
8126
8197
  }
8127
8198
  const upsertMember = db.prepare(`
8128
- INSERT INTO member (platform_id, account_name, group_nickname, avatar, roles)
8129
- VALUES (?, ?, ?, ?, ?)
8199
+ INSERT INTO member (platform_id, account_name, group_nickname, aliases, avatar, roles)
8200
+ VALUES (?, ?, ?, ?, ?, ?)
8130
8201
  ON CONFLICT(platform_id) DO UPDATE SET
8131
8202
  account_name = COALESCE(NULLIF(excluded.account_name, ''), account_name),
8132
8203
  group_nickname = COALESCE(NULLIF(excluded.group_nickname, ''), group_nickname),
8204
+ aliases = CASE WHEN excluded.aliases != '[]' THEN excluded.aliases ELSE aliases END,
8133
8205
  avatar = COALESCE(NULLIF(excluded.avatar, ''), avatar),
8134
8206
  roles = CASE WHEN excluded.roles != '[]' THEN excluded.roles ELSE roles END
8135
8207
  `);
@@ -8165,106 +8237,112 @@ async function incrementalImport(sessionId, filePath, deps, options2) {
8165
8237
  const MAX_ERROR_SAMPLES = 5;
8166
8238
  const BATCH_SIZE2 = 5e3;
8167
8239
  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) : "[]"
8240
+ await streamParseFile(
8241
+ filePath,
8242
+ {
8243
+ formatOptions: options2?.chatIndex === void 0 ? void 0 : { chatIndex: options2.chatIndex },
8244
+ onMeta: (meta) => {
8245
+ if (metaUpdateMode === "none") return;
8246
+ updateMeta.run(
8247
+ meta.name || "",
8248
+ meta.groupId || "",
8249
+ meta.groupAvatar || "",
8250
+ meta.ownerId || "",
8251
+ Math.floor(Date.now() / 1e3)
8190
8252
  );
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;
8253
+ metaUpdated = true;
8254
+ },
8255
+ onMembers: (members) => {
8256
+ if (memberUpdateMode === "none") return;
8257
+ for (const m of members) {
8258
+ const existed = memberIdMap.has(m.platformId);
8259
+ upsertMember.run(
8260
+ m.platformId,
8261
+ m.accountName || null,
8262
+ m.groupNickname || null,
8263
+ m.aliases ? JSON.stringify(m.aliases) : "[]",
8264
+ m.avatar || null,
8265
+ m.roles ? JSON.stringify(m.roles) : "[]"
8266
+ );
8267
+ if (!existed) {
8268
+ const row = getMemberId.get(m.platformId);
8269
+ if (row) memberIdMap.set(m.platformId, row.id);
8270
+ membersAdded++;
8271
+ } else {
8272
+ membersUpdated++;
8273
+ }
8222
8274
  }
8223
- let memberId = memberIdMap.get(msg.senderPlatformId);
8224
- if (!memberId) {
8225
- insertMemberMinimal.run(
8226
- msg.senderPlatformId,
8275
+ },
8276
+ onProgress: (progress) => {
8277
+ deps.onProgress(progress);
8278
+ },
8279
+ onMessageBatch: (batch) => {
8280
+ for (const msg of batch) {
8281
+ processedCount++;
8282
+ if (!msg.senderPlatformId) {
8283
+ trackError2(processedCount, "MISSING_SENDER", "sender field is empty");
8284
+ continue;
8285
+ }
8286
+ if (msg.timestamp === void 0 || msg.timestamp === null) {
8287
+ trackError2(processedCount, "MISSING_TIMESTAMP", "timestamp field is missing");
8288
+ continue;
8289
+ }
8290
+ const timestamp = normalizeImportTimestamp(msg.timestamp);
8291
+ if (timestamp === null) {
8292
+ trackError2(processedCount, "INVALID_TIMESTAMP", `timestamp value: ${msg.timestamp}`);
8293
+ continue;
8294
+ }
8295
+ if (isDuplicate({ ...msg, timestamp }, existingPlatformMsgIds, existingKeys, existingFallbackOnlyKeys)) {
8296
+ duplicateCount++;
8297
+ continue;
8298
+ }
8299
+ let memberId = memberIdMap.get(msg.senderPlatformId);
8300
+ if (!memberId) {
8301
+ insertMemberMinimal.run(
8302
+ msg.senderPlatformId,
8303
+ msg.senderAccountName || null,
8304
+ msg.senderGroupNickname || null,
8305
+ null
8306
+ );
8307
+ const row = getMemberId.get(msg.senderPlatformId);
8308
+ if (row) {
8309
+ memberId = row.id;
8310
+ memberIdMap.set(msg.senderPlatformId, memberId);
8311
+ membersAdded++;
8312
+ }
8313
+ }
8314
+ if (!memberId) continue;
8315
+ const msgResult = insertMessage.run(
8316
+ memberId,
8227
8317
  msg.senderAccountName || null,
8228
8318
  msg.senderGroupNickname || null,
8229
- null
8319
+ timestamp,
8320
+ msg.type,
8321
+ msg.content || null,
8322
+ msg.replyToMessageId || null,
8323
+ msg.platformMessageId || null
8230
8324
  );
8231
- const row = getMemberId.get(msg.senderPlatformId);
8232
- if (row) {
8233
- memberId = row.id;
8234
- memberIdMap.set(msg.senderPlatformId, memberId);
8235
- membersAdded++;
8236
- }
8325
+ newFtsEntries.push({
8326
+ id: Number(msgResult.lastInsertRowid ?? 0),
8327
+ content: msg.content || null
8328
+ });
8329
+ if (timestamp < minWrittenTs) minWrittenTs = timestamp;
8330
+ newMessageCount++;
8331
+ }
8332
+ if (processedCount % BATCH_SIZE2 === 0) {
8333
+ deps.onProgress({
8334
+ stage: "saving",
8335
+ bytesRead: 0,
8336
+ totalBytes: 0,
8337
+ messagesProcessed: processedCount,
8338
+ percentage: 50,
8339
+ message: `Processed ${processedCount}, added ${newMessageCount}`
8340
+ });
8237
8341
  }
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
8342
  }
8266
- }
8267
- });
8343
+ },
8344
+ options2?.formatId
8345
+ );
8268
8346
  db.exec("COMMIT");
8269
8347
  if (!metaUpdated) {
8270
8348
  db.prepare("UPDATE meta SET imported_at = ?").run(Math.floor(Date.now() / 1e3));
@@ -8340,6 +8418,306 @@ async function incrementalImport(sessionId, filePath, deps, options2) {
8340
8418
  }
8341
8419
  }
8342
8420
 
8421
+ // ../../packages/node-runtime/src/import/auto-import-matcher.ts
8422
+ var MATCH_WINDOW_SIZE = 5;
8423
+ function buildPrivateIdentity(ownerId, memberIds) {
8424
+ if (!ownerId) return null;
8425
+ const ids = [...new Set(memberIds)].filter((id) => id && id.toLowerCase() !== "system").sort();
8426
+ return ids.length > 0 ? `${ownerId}\0${ids.join("\0")}` : null;
8427
+ }
8428
+ function isBusinessMessage(type, content) {
8429
+ return type !== 80 /* SYSTEM */ && type !== 81 /* RECALL */ && Boolean(content?.trim());
8430
+ }
8431
+ function addMessageWindow(recentKeys, windows, message) {
8432
+ if (!message.senderPlatformId || !isBusinessMessage(message.type, message.content)) return;
8433
+ const timestamp = normalizeImportTimestamp(message.timestamp);
8434
+ if (timestamp === null) return;
8435
+ recentKeys.push(generateMessageKey(timestamp, message.senderPlatformId, message.content));
8436
+ if (recentKeys.length > MATCH_WINDOW_SIZE) recentKeys.shift();
8437
+ if (recentKeys.length === MATCH_WINDOW_SIZE) windows.add(recentKeys.join(""));
8438
+ }
8439
+ async function resolveAutoImportTarget(filePath, deps, formatOptions) {
8440
+ let sourceMeta = null;
8441
+ const sourceMemberIds = /* @__PURE__ */ new Set();
8442
+ const sourceWindows = /* @__PURE__ */ new Set();
8443
+ const recentSourceKeys = [];
8444
+ const { formatId, ...parserOptions } = formatOptions ?? {};
8445
+ await streamParseFile(
8446
+ filePath,
8447
+ {
8448
+ formatOptions: parserOptions,
8449
+ onProgress: deps.onProgress ?? (() => {
8450
+ }),
8451
+ onMeta: (meta2) => {
8452
+ sourceMeta = meta2;
8453
+ },
8454
+ onMembers: (members) => {
8455
+ for (const member of members) sourceMemberIds.add(member.platformId);
8456
+ },
8457
+ onMessageBatch: (messages) => {
8458
+ for (const message of messages) {
8459
+ sourceMemberIds.add(message.senderPlatformId);
8460
+ addMessageWindow(recentSourceKeys, sourceWindows, message);
8461
+ }
8462
+ }
8463
+ },
8464
+ typeof formatId === "string" ? formatId : void 0
8465
+ );
8466
+ if (!sourceMeta) throw new Error("Import source did not provide metadata");
8467
+ const meta = sourceMeta;
8468
+ const privateIdentity = meta.type === "private" ? buildPrivateIdentity(meta.ownerId, sourceMemberIds) : null;
8469
+ const hasStableIdentity = Boolean(meta.groupId || privateIdentity);
8470
+ const stableMatches = [];
8471
+ const trailingMatches = [];
8472
+ for (const sessionId of deps.listSessionIds()) {
8473
+ let db = null;
8474
+ try {
8475
+ db = deps.openReadonly(sessionId);
8476
+ if (!isChatSessionDb(db)) continue;
8477
+ const candidate = getSessionMeta(db);
8478
+ if (candidate?.platform !== meta.platform || candidate.type !== meta.type) continue;
8479
+ const groupMatches = Boolean(meta.groupId && candidate.groupId === meta.groupId);
8480
+ const memberRows = privateIdentity ? db.prepare("SELECT platform_id FROM member WHERE LOWER(platform_id) != 'system'").all() : [];
8481
+ const candidatePrivateIdentity = privateIdentity ? buildPrivateIdentity(
8482
+ candidate.ownerId,
8483
+ memberRows.map((row) => row.platform_id)
8484
+ ) : null;
8485
+ if (groupMatches || privateIdentity && candidatePrivateIdentity === privateIdentity) {
8486
+ stableMatches.push(sessionId);
8487
+ }
8488
+ if (sourceWindows.size > 0) {
8489
+ const rows = db.prepare(
8490
+ `SELECT msg.ts, member.platform_id, msg.type, msg.content
8491
+ FROM message msg
8492
+ JOIN member ON member.id = msg.sender_id
8493
+ WHERE msg.type NOT IN (?, ?)
8494
+ AND NULLIF(TRIM(msg.content), '') IS NOT NULL
8495
+ ORDER BY msg.ts DESC, msg.id DESC
8496
+ LIMIT ?`
8497
+ ).all(80 /* SYSTEM */, 81 /* RECALL */, MATCH_WINDOW_SIZE);
8498
+ if (rows.length === MATCH_WINDOW_SIZE) {
8499
+ const signature = rows.reverse().map((row) => generateMessageKey(row.ts, row.platform_id, row.content)).join("");
8500
+ if (sourceWindows.has(signature)) trailingMatches.push(sessionId);
8501
+ }
8502
+ }
8503
+ } catch (error) {
8504
+ const detail = error instanceof Error ? error.message : String(error);
8505
+ throw new Error(`Failed to inspect import candidate ${JSON.stringify(sessionId)}: ${detail}`, {
8506
+ cause: error
8507
+ });
8508
+ } finally {
8509
+ db?.close();
8510
+ }
8511
+ }
8512
+ if (hasStableIdentity) {
8513
+ if (meta.sourceSessionId && stableMatches.includes(meta.sourceSessionId)) {
8514
+ return { action: "incremental", sessionId: meta.sourceSessionId, matchedBy: "source-session-id" };
8515
+ }
8516
+ if (stableMatches.length === 1) {
8517
+ return { action: "incremental", sessionId: stableMatches[0], matchedBy: "stable-id" };
8518
+ }
8519
+ }
8520
+ if (meta.sourceSessionId && trailingMatches.includes(meta.sourceSessionId)) {
8521
+ return { action: "incremental", sessionId: meta.sourceSessionId, matchedBy: "source-session-id" };
8522
+ }
8523
+ if (trailingMatches.length === 1) {
8524
+ return { action: "incremental", sessionId: trailingMatches[0], matchedBy: "trailing-messages" };
8525
+ }
8526
+ const ambiguous = stableMatches.length > 1 || trailingMatches.length > 1;
8527
+ return { action: "create", reason: ambiguous ? "ambiguous" : "no-match" };
8528
+ }
8529
+
8530
+ // ../../packages/node-runtime/src/import/auto-importer.ts
8531
+ function mapCreateResult(result, createReason) {
8532
+ if (!result.success || !result.sessionId) {
8533
+ return { success: false, error: result.error, diagnostics: result.diagnostics };
8534
+ }
8535
+ return {
8536
+ success: true,
8537
+ sessionId: result.sessionId,
8538
+ importMode: "created",
8539
+ ...createReason ? { createReason } : {},
8540
+ newMessageCount: result.diagnostics?.messagesWritten ?? 0,
8541
+ duplicateCount: result.diagnostics?.duplicateCount ?? 0,
8542
+ diagnostics: result.diagnostics
8543
+ };
8544
+ }
8545
+ function mapIncrementalResult(sessionId, result, matchedBy) {
8546
+ if (!result.success) return { success: false, sessionId, error: result.error };
8547
+ return {
8548
+ success: true,
8549
+ sessionId,
8550
+ importMode: "incremental",
8551
+ ...matchedBy ? { matchedBy } : {},
8552
+ newMessageCount: result.newMessageCount,
8553
+ duplicateCount: result.batch?.duplicateCount ?? 0
8554
+ };
8555
+ }
8556
+ async function autoImportFile(filePath, deps, options2 = {}) {
8557
+ try {
8558
+ if (options2.explicitSessionId) {
8559
+ if (deps.sessionExists(options2.explicitSessionId)) {
8560
+ const result3 = mapIncrementalResult(
8561
+ options2.explicitSessionId,
8562
+ await deps.appendSession(options2.explicitSessionId, filePath, options2.formatOptions)
8563
+ );
8564
+ appLogger.info("import", "Explicit incremental import completed", {
8565
+ sessionId: options2.explicitSessionId,
8566
+ success: result3.success,
8567
+ newMessageCount: result3.newMessageCount,
8568
+ duplicateCount: result3.duplicateCount
8569
+ });
8570
+ return result3;
8571
+ }
8572
+ const result2 = mapCreateResult(
8573
+ await deps.createSession(filePath, options2.formatOptions, options2.explicitSessionId)
8574
+ );
8575
+ appLogger.info("import", "Explicit session import completed", {
8576
+ sessionId: result2.sessionId,
8577
+ success: result2.success,
8578
+ importMode: result2.importMode
8579
+ });
8580
+ return result2;
8581
+ }
8582
+ appLogger.info("import", "Automatic session matching started", {
8583
+ candidateCount: deps.listSessionIds().length
8584
+ });
8585
+ const decision = await (deps.resolveTarget ?? resolveAutoImportTarget)(
8586
+ filePath,
8587
+ deps,
8588
+ options2.formatOptions
8589
+ );
8590
+ if (decision.action === "incremental") {
8591
+ const result2 = mapIncrementalResult(
8592
+ decision.sessionId,
8593
+ await deps.appendSession(decision.sessionId, filePath, options2.formatOptions),
8594
+ decision.matchedBy
8595
+ );
8596
+ appLogger.info("import", "Automatic incremental import completed", {
8597
+ sessionId: decision.sessionId,
8598
+ matchedBy: decision.matchedBy,
8599
+ success: result2.success,
8600
+ newMessageCount: result2.newMessageCount,
8601
+ duplicateCount: result2.duplicateCount
8602
+ });
8603
+ return result2;
8604
+ }
8605
+ const result = mapCreateResult(await deps.createSession(filePath, options2.formatOptions), decision.reason);
8606
+ appLogger.info("import", "Automatic import created a new session", {
8607
+ reason: decision.reason,
8608
+ sessionId: result.sessionId,
8609
+ success: result.success
8610
+ });
8611
+ return result;
8612
+ } catch (error) {
8613
+ appLogger.error("import", "Automatic import failed", error);
8614
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
8615
+ }
8616
+ }
8617
+
8618
+ // ../../packages/node-runtime/src/import/import-lock.ts
8619
+ import * as fs21 from "fs";
8620
+ import * as path16 from "path";
8621
+ import { randomUUID } from "crypto";
8622
+ var IMPORT_LOCK_FILENAME = ".chatlab-import.lock";
8623
+ var IMPORT_IN_PROGRESS_ERROR_KEY = "error.import_in_progress";
8624
+ var ImportInProgressError = class extends Error {
8625
+ code = "IMPORT_IN_PROGRESS";
8626
+ errorKey = IMPORT_IN_PROGRESS_ERROR_KEY;
8627
+ constructor() {
8628
+ super("Another import is already in progress");
8629
+ this.name = "ImportInProgressError";
8630
+ }
8631
+ };
8632
+ function parseOwner(lockPath) {
8633
+ try {
8634
+ const parsed = JSON.parse(fs21.readFileSync(lockPath, "utf-8"));
8635
+ if (typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0 || typeof parsed.token !== "string" || parsed.token.length === 0 || typeof parsed.startedAt !== "number") {
8636
+ return null;
8637
+ }
8638
+ return parsed;
8639
+ } catch {
8640
+ return null;
8641
+ }
8642
+ }
8643
+ function isProcessRunning(pid) {
8644
+ try {
8645
+ process.kill(pid, 0);
8646
+ return true;
8647
+ } catch (error) {
8648
+ return error.code !== "ESRCH";
8649
+ }
8650
+ }
8651
+ function removeStaleLock(lockPath) {
8652
+ const owner = parseOwner(lockPath);
8653
+ if (owner) {
8654
+ if (isProcessRunning(owner.pid)) return false;
8655
+ } else {
8656
+ try {
8657
+ if (Date.now() - fs21.statSync(lockPath).mtimeMs < 5e3) return false;
8658
+ } catch {
8659
+ return true;
8660
+ }
8661
+ }
8662
+ try {
8663
+ fs21.unlinkSync(lockPath);
8664
+ appLogger.warn("import-lock", "Removed stale import lock", { ownerPid: owner?.pid });
8665
+ return true;
8666
+ } catch {
8667
+ return false;
8668
+ }
8669
+ }
8670
+ function releaseLock(lockPath, fd, owner) {
8671
+ try {
8672
+ fs21.closeSync(fd);
8673
+ } catch (error) {
8674
+ appLogger.error("import-lock", "Failed to close import lock", error);
8675
+ }
8676
+ try {
8677
+ if (parseOwner(lockPath)?.token !== owner.token) {
8678
+ appLogger.warn("import-lock", "Import lock ownership changed before release");
8679
+ return;
8680
+ }
8681
+ fs21.unlinkSync(lockPath);
8682
+ appLogger.info("import-lock", "Import lock released");
8683
+ } catch (error) {
8684
+ appLogger.error("import-lock", "Failed to release import lock", error);
8685
+ }
8686
+ }
8687
+ async function withDataDirImportLock(userDataDir, task) {
8688
+ fs21.mkdirSync(userDataDir, { recursive: true });
8689
+ const lockPath = path16.join(userDataDir, IMPORT_LOCK_FILENAME);
8690
+ let fd = null;
8691
+ for (let attempt = 0; attempt < 2; attempt++) {
8692
+ try {
8693
+ fd = fs21.openSync(lockPath, "wx");
8694
+ break;
8695
+ } catch (error) {
8696
+ const code = error.code;
8697
+ if (code === "EEXIST" && attempt === 0 && removeStaleLock(lockPath)) continue;
8698
+ if (code === "EEXIST") {
8699
+ appLogger.warn("import-lock", "Rejected concurrent import");
8700
+ throw new ImportInProgressError();
8701
+ }
8702
+ throw error;
8703
+ }
8704
+ }
8705
+ if (fd === null) throw new ImportInProgressError();
8706
+ const owner = {
8707
+ pid: process.pid,
8708
+ token: randomUUID(),
8709
+ startedAt: Date.now()
8710
+ };
8711
+ try {
8712
+ fs21.writeFileSync(fd, JSON.stringify(owner), "utf-8");
8713
+ fs21.fsyncSync(fd);
8714
+ appLogger.info("import-lock", "Import lock acquired");
8715
+ return await task();
8716
+ } finally {
8717
+ releaseLock(lockPath, fd, owner);
8718
+ }
8719
+ }
8720
+
8343
8721
  // ../../packages/node-runtime/src/import/archive/errors.ts
8344
8722
  var ArchiveImportError = class extends Error {
8345
8723
  constructor(code, message, options2) {
@@ -8352,8 +8730,8 @@ var ArchiveImportError = class extends Error {
8352
8730
 
8353
8731
  // ../../packages/node-runtime/src/import/archive/archive-reader.ts
8354
8732
  var import_yauzl = __toESM(require_yauzl(), 1);
8355
- import * as fs21 from "fs";
8356
- import * as path16 from "path";
8733
+ import * as fs22 from "fs";
8734
+ import * as path17 from "path";
8357
8735
  import { pipeline } from "stream/promises";
8358
8736
  var DEFAULT_MAX_ENTRIES = 1e5;
8359
8737
  var DEFAULT_MAX_ENTRY_BYTES = 20 * 1024 * 1024 * 1024;
@@ -8472,8 +8850,8 @@ var ZipArchiveReader = class {
8472
8850
  await this.visitEntries(async (entry, openStream) => {
8473
8851
  const destination = targets.get(entry.name);
8474
8852
  if (!destination || entry.isDirectory) return;
8475
- fs21.mkdirSync(path16.dirname(destination), { recursive: true });
8476
- await pipeline(await openStream(), fs21.createWriteStream(destination, { flags: "wx" }));
8853
+ fs22.mkdirSync(path17.dirname(destination), { recursive: true });
8854
+ await pipeline(await openStream(), fs22.createWriteStream(destination, { flags: "wx" }));
8477
8855
  remaining.delete(entry.name);
8478
8856
  });
8479
8857
  if (remaining.size > 0) {
@@ -8490,8 +8868,8 @@ var import_stream_chain9 = __toESM(require_stream_chain(), 1);
8490
8868
  var import_stream_json9 = __toESM(require_stream_json(), 1);
8491
8869
  var import_Pick8 = __toESM(require_Pick(), 1);
8492
8870
  var import_StreamValues9 = __toESM(require_StreamValues(), 1);
8493
- import * as fs22 from "fs";
8494
- import * as path17 from "path";
8871
+ import * as fs23 from "fs";
8872
+ import * as path18 from "path";
8495
8873
  var { chain: chain9 } = import_stream_chain9.default;
8496
8874
  var { parser: parser10 } = import_stream_json9.default;
8497
8875
  var { pick: pick8 } = import_Pick8.default;
@@ -8620,16 +8998,16 @@ var GoogleChatTakeoutResolver = class {
8620
8998
  `Google Chat conversation is missing required files: ${chat.chatId}`
8621
8999
  );
8622
9000
  }
8623
- fs22.mkdirSync(targetDir, { recursive: true });
9001
+ fs23.mkdirSync(targetDir, { recursive: true });
8624
9002
  await reader.pipeEntries(
8625
9003
  /* @__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")]
9004
+ [userInfoEntry, path18.join(targetDir, "user_info.json")],
9005
+ [groupInfoEntry, path18.join(targetDir, "group_info.json")],
9006
+ [messagesEntry, path18.join(targetDir, "messages.json")]
8629
9007
  ])
8630
9008
  );
8631
- const manifestPath = path17.join(targetDir, "google-chat-import.json");
8632
- fs22.writeFileSync(
9009
+ const manifestPath = path18.join(targetDir, "google-chat-import.json");
9010
+ fs23.writeFileSync(
8633
9011
  manifestPath,
8634
9012
  JSON.stringify(
8635
9013
  {
@@ -8652,9 +9030,9 @@ var GoogleChatTakeoutResolver = class {
8652
9030
  };
8653
9031
 
8654
9032
  // ../../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";
9033
+ import * as fs24 from "fs";
9034
+ import * as path19 from "path";
9035
+ import { randomUUID as randomUUID2 } from "crypto";
8658
9036
  var DEFAULT_TTL_MS = 30 * 60 * 1e3;
8659
9037
  var ArchiveImportSourceManager = class {
8660
9038
  records = /* @__PURE__ */ new Map();
@@ -8664,11 +9042,11 @@ var ArchiveImportSourceManager = class {
8664
9042
  now;
8665
9043
  resolvers;
8666
9044
  constructor(options2 = {}) {
8667
- this.tempRoot = options2.tempRoot ?? path18.join(process.cwd(), ".chatlab-import-sources");
9045
+ this.tempRoot = options2.tempRoot ?? path19.join(process.cwd(), ".chatlab-import-sources");
8668
9046
  this.ttlMs = options2.ttlMs ?? DEFAULT_TTL_MS;
8669
9047
  this.now = options2.now ?? Date.now;
8670
9048
  this.resolvers = options2.resolvers ?? [new GoogleChatTakeoutResolver()];
8671
- fs23.mkdirSync(this.tempRoot, { recursive: true });
9049
+ fs24.mkdirSync(this.tempRoot, { recursive: true });
8672
9050
  }
8673
9051
  prepareLocalArchive(archivePath) {
8674
9052
  return this.prepareArchive(archivePath, false);
@@ -8691,7 +9069,7 @@ var ArchiveImportSourceManager = class {
8691
9069
  const resolver = matches[0];
8692
9070
  const chats = await resolver.scan(reader);
8693
9071
  const createdAt = this.now();
8694
- const sourceId = randomUUID();
9072
+ const sourceId = randomUUID2();
8695
9073
  const descriptor = {
8696
9074
  sourceId,
8697
9075
  formatId: resolver.id,
@@ -8725,7 +9103,7 @@ var ArchiveImportSourceManager = class {
8725
9103
  `Archive conversation was not found: ${chatId}`
8726
9104
  );
8727
9105
  }
8728
- const materializedDir = fs23.mkdtempSync(path18.join(this.tempRoot, `materialized-${sourceId}-`));
9106
+ const materializedDir = fs24.mkdtempSync(path19.join(this.tempRoot, `materialized-${sourceId}-`));
8729
9107
  try {
8730
9108
  const reader = new ZipArchiveReader(record.archivePath);
8731
9109
  const materialized = await record.resolver.materialize(reader, chat, materializedDir);
@@ -8776,7 +9154,7 @@ var ArchiveImportSourceManager = class {
8776
9154
  }
8777
9155
  removePath(targetPath) {
8778
9156
  try {
8779
- fs23.rmSync(targetPath, { recursive: true, force: true });
9157
+ fs24.rmSync(targetPath, { recursive: true, force: true });
8780
9158
  } catch {
8781
9159
  }
8782
9160
  }
@@ -8846,12 +9224,12 @@ ${rawErrorLabel}: ${details}`;
8846
9224
  }
8847
9225
 
8848
9226
  // ../../packages/node-runtime/src/ai/assistant-parser.ts
8849
- import * as path19 from "path";
9227
+ import * as path20 from "path";
8850
9228
  import matter from "gray-matter";
8851
9229
  function parseAssistantFile(content, filePath) {
8852
9230
  try {
8853
9231
  const { data: fm, content: body } = matter(content);
8854
- const id = fm.id ?? path19.basename(filePath, ".md");
9232
+ const id = fm.id ?? path20.basename(filePath, ".md");
8855
9233
  const name = fm.name;
8856
9234
  if (!name) return null;
8857
9235
  return {
@@ -8930,8 +9308,8 @@ var AssistantManager = class {
8930
9308
  }
8931
9309
  // ==================== Init ====================
8932
9310
  init() {
8933
- const { fs: fs33, assistantsDir } = this.deps;
8934
- fs33.ensureDir(assistantsDir);
9311
+ const { fs: fs35, assistantsDir } = this.deps;
9312
+ fs35.ensureDir(assistantsDir);
8935
9313
  const generalCreated = this.ensureGeneralAssistants();
8936
9314
  this.loadAll();
8937
9315
  this.initialized = true;
@@ -8942,26 +9320,26 @@ var AssistantManager = class {
8942
9320
  return { total: this.cache.size, generalCreated };
8943
9321
  }
8944
9322
  ensureGeneralAssistants() {
8945
- const { fs: fs33, assistantsDir } = this.deps;
9323
+ const { fs: fs35, assistantsDir } = this.deps;
8946
9324
  let anyCreated = false;
8947
9325
  for (const id of this.generalIds) {
8948
9326
  const config = this.getBuiltinConfig(id);
8949
9327
  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 }));
9328
+ const filePath = fs35.joinPath(assistantsDir, `${id}.md`);
9329
+ if (fs35.fileExists(filePath)) continue;
9330
+ fs35.writeFile(filePath, serializeAssistant({ ...config, builtinId: config.id }));
8953
9331
  anyCreated = true;
8954
9332
  }
8955
9333
  return anyCreated;
8956
9334
  }
8957
9335
  loadAll() {
8958
- const { fs: fs33, assistantsDir } = this.deps;
9336
+ const { fs: fs35, assistantsDir } = this.deps;
8959
9337
  this.cache.clear();
8960
- const files = fs33.listFiles(assistantsDir, ".md");
9338
+ const files = fs35.listFiles(assistantsDir, ".md");
8961
9339
  for (const file of files) {
8962
9340
  try {
8963
- const filePath = fs33.joinPath(assistantsDir, file);
8964
- const content = fs33.readFile(filePath);
9341
+ const filePath = fs35.joinPath(assistantsDir, file);
9342
+ const content = fs35.readFile(filePath);
8965
9343
  const config = parseAssistantFile(content, filePath);
8966
9344
  if (config) {
8967
9345
  this.cache.set(config.id, config);
@@ -9073,12 +9451,12 @@ var AssistantManager = class {
9073
9451
  };
9074
9452
 
9075
9453
  // ../../packages/node-runtime/src/ai/skill-parser.ts
9076
- import * as path20 from "path";
9454
+ import * as path21 from "path";
9077
9455
  import matter2 from "gray-matter";
9078
9456
  function parseSkillFile(content, filePath) {
9079
9457
  try {
9080
9458
  const { data: fm, content: prompt } = matter2(content);
9081
- const id = fm.id ?? path20.basename(filePath, ".md");
9459
+ const id = fm.id ?? path21.basename(filePath, ".md");
9082
9460
  const name = fm.name;
9083
9461
  if (!name) return null;
9084
9462
  return {
@@ -9097,7 +9475,7 @@ function parseSkillFile(content, filePath) {
9097
9475
  function extractSkillId(content, filePath) {
9098
9476
  try {
9099
9477
  const { data: fm } = matter2(content);
9100
- return fm.id ?? path20.basename(filePath, ".md");
9478
+ return fm.id ?? path21.basename(filePath, ".md");
9101
9479
  } catch {
9102
9480
  return null;
9103
9481
  }
@@ -9116,8 +9494,8 @@ function validateChatScope(raw) {
9116
9494
 
9117
9495
  // ../../packages/node-runtime/src/ai/chats.ts
9118
9496
  import Database from "better-sqlite3";
9119
- import * as fs24 from "fs";
9120
- import * as path21 from "path";
9497
+ import * as fs25 from "fs";
9498
+ import * as path22 from "path";
9121
9499
  var DEFAULT_GENERAL_ID = "general_cn";
9122
9500
  var defaultLogger = {
9123
9501
  warn(_category, message, extra) {
@@ -9136,14 +9514,14 @@ var AIChatManager = class {
9136
9514
  this.nativeBinding = options2?.nativeBinding;
9137
9515
  }
9138
9516
  ensureDir(dir) {
9139
- if (!fs24.existsSync(dir)) {
9140
- fs24.mkdirSync(dir, { recursive: true });
9517
+ if (!fs25.existsSync(dir)) {
9518
+ fs25.mkdirSync(dir, { recursive: true });
9141
9519
  }
9142
9520
  }
9143
9521
  getDb() {
9144
9522
  if (this.db) return this.db;
9145
9523
  this.ensureDir(this.aiDataDir);
9146
- const dbPath = path21.join(this.aiDataDir, "conversations.db");
9524
+ const dbPath = path22.join(this.aiDataDir, "conversations.db");
9147
9525
  this.db = this.nativeBinding ? new Database(dbPath, { nativeBinding: this.nativeBinding }) : new Database(dbPath);
9148
9526
  this.db.pragma("journal_mode = WAL");
9149
9527
  this.db.exec(`
@@ -9386,16 +9764,16 @@ var AIChatManager = class {
9386
9764
  if (allRows.length === 0) return [];
9387
9765
  const rowMap = new Map(allRows.map((row) => [row.id, row]));
9388
9766
  let currentId = leafMessageId ?? this.getActiveMessageId(aiChatId);
9389
- const path30 = [];
9767
+ const path31 = [];
9390
9768
  const seen = /* @__PURE__ */ new Set();
9391
9769
  while (currentId && !seen.has(currentId)) {
9392
9770
  seen.add(currentId);
9393
9771
  const row = rowMap.get(currentId);
9394
9772
  if (!row) break;
9395
- path30.push(row);
9773
+ path31.push(row);
9396
9774
  currentId = row.parentId;
9397
9775
  }
9398
- return path30.length > 0 ? path30.reverse() : allRows;
9776
+ return path31.length > 0 ? path31.reverse() : allRows;
9399
9777
  }
9400
9778
  // ==================== 生命周期 ====================
9401
9779
  close() {
@@ -9965,21 +10343,21 @@ var SkillManagerCore = class {
9965
10343
  }
9966
10344
  // ==================== Init ====================
9967
10345
  init() {
9968
- const { fs: fs33, skillsDir } = this.deps;
9969
- fs33.ensureDir(skillsDir);
10346
+ const { fs: fs35, skillsDir } = this.deps;
10347
+ fs35.ensureDir(skillsDir);
9970
10348
  this.loadAll();
9971
10349
  this.initialized = true;
9972
10350
  this.deps.logger?.info("SkillManager", "Initialized", { total: this.cache.size });
9973
10351
  return { total: this.cache.size };
9974
10352
  }
9975
10353
  loadAll() {
9976
- const { fs: fs33, skillsDir } = this.deps;
10354
+ const { fs: fs35, skillsDir } = this.deps;
9977
10355
  this.cache.clear();
9978
- const files = fs33.listFiles(skillsDir, ".md");
10356
+ const files = fs35.listFiles(skillsDir, ".md");
9979
10357
  for (const file of files) {
9980
10358
  try {
9981
- const filePath = fs33.joinPath(skillsDir, file);
9982
- const content = fs33.readFile(filePath);
10359
+ const filePath = fs35.joinPath(skillsDir, file);
10360
+ const content = fs35.readFile(filePath);
9983
10361
  const def = parseSkillFile(content, filePath);
9984
10362
  if (def) {
9985
10363
  this.cache.set(def.id, def);
@@ -10131,8 +10509,8 @@ var SkillManagerCore = class {
10131
10509
  };
10132
10510
 
10133
10511
  // ../../packages/node-runtime/src/ai/skill-manager.ts
10134
- import * as fs25 from "fs";
10135
- import * as path22 from "path";
10512
+ import * as fs26 from "fs";
10513
+ import * as path23 from "path";
10136
10514
  var SKILLS_DIR_NAME = "skills";
10137
10515
  var defaultLogger2 = {
10138
10516
  info: () => {
@@ -10142,26 +10520,26 @@ var defaultLogger2 = {
10142
10520
  };
10143
10521
  var nodeFs = {
10144
10522
  ensureDir(dir) {
10145
- fs25.mkdirSync(dir, { recursive: true });
10523
+ fs26.mkdirSync(dir, { recursive: true });
10146
10524
  },
10147
10525
  listFiles(dir, ext) {
10148
- if (!fs25.existsSync(dir)) return [];
10149
- return fs25.readdirSync(dir).filter((file) => file.endsWith(ext));
10526
+ if (!fs26.existsSync(dir)) return [];
10527
+ return fs26.readdirSync(dir).filter((file) => file.endsWith(ext));
10150
10528
  },
10151
10529
  readFile(filePath) {
10152
- return fs25.readFileSync(filePath, "utf-8");
10530
+ return fs26.readFileSync(filePath, "utf-8");
10153
10531
  },
10154
10532
  writeFile(filePath, content) {
10155
- fs25.writeFileSync(filePath, content, "utf-8");
10533
+ fs26.writeFileSync(filePath, content, "utf-8");
10156
10534
  },
10157
10535
  deleteFile(filePath) {
10158
- fs25.unlinkSync(filePath);
10536
+ fs26.unlinkSync(filePath);
10159
10537
  },
10160
10538
  fileExists(filePath) {
10161
- return fs25.existsSync(filePath);
10539
+ return fs26.existsSync(filePath);
10162
10540
  },
10163
10541
  joinPath(...parts) {
10164
- return path22.join(...parts);
10542
+ return path23.join(...parts);
10165
10543
  }
10166
10544
  };
10167
10545
  var SkillManager = class {
@@ -10171,7 +10549,7 @@ var SkillManager = class {
10171
10549
  this.logger = logger ?? defaultLogger2;
10172
10550
  this.core = new SkillManagerCore({
10173
10551
  fs: nodeFs,
10174
- skillsDir: path22.join(aiDataDir, SKILLS_DIR_NAME),
10552
+ skillsDir: path23.join(aiDataDir, SKILLS_DIR_NAME),
10175
10553
  builtinRawSkills: [],
10176
10554
  logger: {
10177
10555
  info: () => {
@@ -14001,9 +14379,9 @@ var localeMap = {
14001
14379
  "ja-JP": ja_JP_default,
14002
14380
  "zh-TW": zh_TW_default
14003
14381
  };
14004
- function getNestedValue(obj, path30) {
14382
+ function getNestedValue(obj, path31) {
14005
14383
  let current = obj;
14006
- for (const segment2 of path30.split(".")) {
14384
+ for (const segment2 of path31.split(".")) {
14007
14385
  if (current == null || typeof current !== "object") return void 0;
14008
14386
  current = current[segment2];
14009
14387
  }
@@ -15156,7 +15534,13 @@ function queryMessages(db, timeFilter) {
15156
15534
  const sql = `
15157
15535
  SELECT msg.ts,
15158
15536
  COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
15159
- msg.content
15537
+ m.platform_id as sender,
15538
+ COALESCE(msg.sender_account_name, m.account_name, m.platform_id) as accountName,
15539
+ COALESCE(msg.sender_group_nickname, m.group_nickname) as groupNickname,
15540
+ msg.type,
15541
+ msg.content,
15542
+ msg.platform_message_id as platformMessageId,
15543
+ msg.reply_to_message_id as replyToMessageId
15160
15544
  FROM message msg
15161
15545
  JOIN member m ON msg.sender_id = m.id
15162
15546
  ${hasFilter ? "WHERE msg.ts >= ? AND msg.ts <= ?" : ""}
@@ -15168,9 +15552,6 @@ function queryMessages(db, timeFilter) {
15168
15552
  }
15169
15553
  return db.prepare(sql).all(...params);
15170
15554
  }
15171
- function formatTime(ts) {
15172
- return new Date(ts * 1e3).toLocaleString();
15173
- }
15174
15555
  function formatTimeShort(ts) {
15175
15556
  return new Date(ts * 1e3).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
15176
15557
  }
@@ -15190,16 +15571,69 @@ function exportAsTxt(messages, sessionName) {
15190
15571
  }
15191
15572
  return lines.join("\n");
15192
15573
  }
15193
- function exportAsJson(messages, sessionName) {
15574
+ function parseOptionalArray(value) {
15575
+ if (!value) return void 0;
15576
+ try {
15577
+ const parsed = JSON.parse(value);
15578
+ return Array.isArray(parsed) && parsed.length > 0 ? parsed : void 0;
15579
+ } catch {
15580
+ return void 0;
15581
+ }
15582
+ }
15583
+ function exportAsJson(db, messages, sessionName, sourceSessionId) {
15584
+ const meta = db.prepare(
15585
+ `SELECT name,
15586
+ platform,
15587
+ type,
15588
+ group_id as groupId,
15589
+ group_avatar as groupAvatar,
15590
+ owner_id as ownerId
15591
+ FROM meta
15592
+ LIMIT 1`
15593
+ ).get();
15594
+ if (!meta) throw new Error("Cannot read session metadata");
15595
+ const members = db.prepare(
15596
+ `SELECT platform_id as platformId,
15597
+ account_name as accountName,
15598
+ group_nickname as groupNickname,
15599
+ aliases,
15600
+ avatar,
15601
+ roles
15602
+ FROM member
15603
+ ORDER BY id ASC`
15604
+ ).all();
15194
15605
  const data = {
15195
- sessionName,
15196
- exportTime: (/* @__PURE__ */ new Date()).toISOString(),
15197
- totalMessages: messages.length,
15606
+ chatlab: {
15607
+ version: "0.0.2",
15608
+ exportedAt: Math.floor(Date.now() / 1e3),
15609
+ generator: "ChatLab"
15610
+ },
15611
+ meta: {
15612
+ name: meta.name || sessionName,
15613
+ platform: meta.platform,
15614
+ type: meta.type,
15615
+ groupId: meta.groupId || void 0,
15616
+ groupAvatar: meta.groupAvatar || void 0,
15617
+ ownerId: meta.ownerId || void 0,
15618
+ sourceSessionId
15619
+ },
15620
+ members: members.map((member) => ({
15621
+ platformId: member.platformId,
15622
+ accountName: member.accountName || member.platformId,
15623
+ groupNickname: member.groupNickname || void 0,
15624
+ aliases: parseOptionalArray(member.aliases),
15625
+ avatar: member.avatar || void 0,
15626
+ roles: parseOptionalArray(member.roles)
15627
+ })),
15198
15628
  messages: messages.map((msg) => ({
15629
+ sender: msg.sender,
15630
+ accountName: msg.accountName,
15631
+ groupNickname: msg.groupNickname || void 0,
15199
15632
  timestamp: msg.ts,
15200
- time: formatTime(msg.ts),
15201
- sender: msg.senderName,
15202
- content: msg.content || ""
15633
+ type: msg.type,
15634
+ content: msg.content,
15635
+ platformMessageId: msg.platformMessageId || void 0,
15636
+ replyToMessageId: msg.replyToMessageId || void 0
15203
15637
  }))
15204
15638
  };
15205
15639
  return JSON.stringify(data, null, 2);
@@ -15250,7 +15684,7 @@ function exportWithFormat(params, openDatabase) {
15250
15684
  content = exportAsTxt(messages, params.sessionName);
15251
15685
  break;
15252
15686
  case "json":
15253
- content = exportAsJson(messages, params.sessionName);
15687
+ content = exportAsJson(db, messages, params.sessionName, params.sessionId);
15254
15688
  break;
15255
15689
  case "markdown":
15256
15690
  content = exportAsMarkdown(messages, params.sessionName);
@@ -15258,8 +15692,15 @@ function exportWithFormat(params, openDatabase) {
15258
15692
  }
15259
15693
  const timestamp = Date.now();
15260
15694
  const filename = `${params.sessionName}_export_${timestamp}.${ext}`;
15695
+ appLogger.info("export", "chat export generated", {
15696
+ sessionId: params.sessionId,
15697
+ format: params.format,
15698
+ totalMessages: messages.length,
15699
+ filteredByTime: !!params.timeFilter
15700
+ });
15261
15701
  return { success: true, totalMessages: messages.length, content, filename, mimeType: mime };
15262
15702
  } catch (error) {
15703
+ appLogger.error("export", "chat export failed", error);
15263
15704
  return {
15264
15705
  success: false,
15265
15706
  error: String(error),
@@ -15272,8 +15713,8 @@ function exportWithFormat(params, openDatabase) {
15272
15713
  }
15273
15714
 
15274
15715
  // ../../packages/node-runtime/src/merger/temp-db.ts
15275
- import * as fs26 from "fs";
15276
- import * as path23 from "path";
15716
+ import * as fs27 from "fs";
15717
+ import * as path24 from "path";
15277
15718
  var TEMP_DB_SCHEMA = `
15278
15719
  CREATE TABLE IF NOT EXISTS meta (
15279
15720
  name TEXT NOT NULL,
@@ -15518,21 +15959,21 @@ function exportSessionToJson(db) {
15518
15959
  }
15519
15960
  function deleteTempDatabase(dbPath) {
15520
15961
  try {
15521
- if (fs26.existsSync(dbPath)) fs26.unlinkSync(dbPath);
15962
+ if (fs27.existsSync(dbPath)) fs27.unlinkSync(dbPath);
15522
15963
  const walPath = dbPath + "-wal";
15523
15964
  const shmPath = dbPath + "-shm";
15524
- if (fs26.existsSync(walPath)) fs26.unlinkSync(walPath);
15525
- if (fs26.existsSync(shmPath)) fs26.unlinkSync(shmPath);
15965
+ if (fs27.existsSync(walPath)) fs27.unlinkSync(walPath);
15966
+ if (fs27.existsSync(shmPath)) fs27.unlinkSync(shmPath);
15526
15967
  } catch {
15527
15968
  }
15528
15969
  }
15529
15970
  function cleanupTempDatabases(tempDir) {
15530
15971
  try {
15531
- if (!fs26.existsSync(tempDir)) return;
15532
- const files = fs26.readdirSync(tempDir);
15972
+ if (!fs27.existsSync(tempDir)) return;
15973
+ const files = fs27.readdirSync(tempDir);
15533
15974
  for (const file of files) {
15534
15975
  if (file.startsWith("merge_") && file.endsWith(".db")) {
15535
- deleteTempDatabase(path23.join(tempDir, file));
15976
+ deleteTempDatabase(path24.join(tempDir, file));
15536
15977
  }
15537
15978
  }
15538
15979
  } catch {
@@ -15558,7 +15999,21 @@ function buildSession(db, id, dbPath, options2) {
15558
15999
  if (meta.type === "private") {
15559
16000
  memberAvatar = getPrivateChatMemberAvatar(db, meta.name, meta.ownerId);
15560
16001
  }
15561
- let dto = { ...info, id, dbPath, memberAvatar, aiConversationCount: 0 };
16002
+ const ownerMember = meta.ownerId ? db.prepare(
16003
+ `SELECT COALESCE(NULLIF(group_nickname, ''), NULLIF(account_name, ''), platform_id) as displayName
16004
+ FROM member
16005
+ WHERE platform_id = ?
16006
+ LIMIT 1`
16007
+ ).get(meta.ownerId) : void 0;
16008
+ let dto = {
16009
+ ...info,
16010
+ id,
16011
+ dbPath,
16012
+ memberAvatar,
16013
+ aiConversationCount: 0,
16014
+ ownerName: ownerMember?.displayName ?? null,
16015
+ ownerStatus: !meta.ownerId ? "missing" : ownerMember ? "resolved" : "unresolved"
16016
+ };
15562
16017
  if (options2?.enrichSession) {
15563
16018
  dto = options2.enrichSession(dto);
15564
16019
  }
@@ -15898,8 +16353,8 @@ function clearSessionOwner(adapter, sessionId) {
15898
16353
  }
15899
16354
 
15900
16355
  // ../../packages/node-runtime/src/services/contacts/overrides.ts
15901
- import fs27 from "fs";
15902
- import path24 from "path";
16356
+ import fs28 from "fs";
16357
+ import path25 from "path";
15903
16358
  var CONTACT_OVERRIDES_FILE_NAME = "contact-overrides.json";
15904
16359
  var CONTACT_OVERRIDES_TMP_PREFIX = "contact-overrides.tmp-";
15905
16360
  var CONTACT_OVERRIDES_VERSION = 1;
@@ -15910,23 +16365,23 @@ function createEmptyContactOverridesFile() {
15910
16365
  };
15911
16366
  }
15912
16367
  function getContactOverridesPath(snapshotDir) {
15913
- return path24.join(snapshotDir, CONTACT_OVERRIDES_FILE_NAME);
16368
+ return path25.join(snapshotDir, CONTACT_OVERRIDES_FILE_NAME);
15914
16369
  }
15915
16370
  function readContactOverrides(snapshotDir) {
15916
16371
  const filePath = getContactOverridesPath(snapshotDir);
15917
- if (!fs27.existsSync(filePath)) return createEmptyContactOverridesFile();
16372
+ if (!fs28.existsSync(filePath)) return createEmptyContactOverridesFile();
15918
16373
  try {
15919
- return normalizeContactOverrides(JSON.parse(fs27.readFileSync(filePath, "utf-8")));
16374
+ return normalizeContactOverrides(JSON.parse(fs28.readFileSync(filePath, "utf-8")));
15920
16375
  } catch (error) {
15921
16376
  appLogger.warn("contacts", "contact overrides file is unreadable", error);
15922
16377
  return createEmptyContactOverridesFile();
15923
16378
  }
15924
16379
  }
15925
16380
  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));
16381
+ if (!fs28.existsSync(snapshotDir)) fs28.mkdirSync(snapshotDir, { recursive: true });
16382
+ const tmpPath = path25.join(snapshotDir, `${CONTACT_OVERRIDES_TMP_PREFIX}${process.pid}-${Date.now()}`);
16383
+ fs28.writeFileSync(tmpPath, JSON.stringify(normalizeContactOverrides(overrides), null, 2), "utf-8");
16384
+ fs28.renameSync(tmpPath, getContactOverridesPath(snapshotDir));
15930
16385
  }
15931
16386
  function normalizeContactOverrides(value) {
15932
16387
  const input = isRecord2(value) ? value : {};
@@ -15961,23 +16416,23 @@ function buildContactsSignature(adapter, timeRangePreset) {
15961
16416
  }
15962
16417
 
15963
16418
  // ../../packages/node-runtime/src/services/contacts/snapshot.ts
15964
- import fs28 from "fs";
15965
- import path25 from "path";
16419
+ import fs29 from "fs";
16420
+ import path26 from "path";
15966
16421
  var CONTACTS_SNAPSHOT_TMP_PREFIX = "contacts-snapshot.tmp-";
15967
16422
  function getContactsSnapshotPath(snapshotDir, timeRangePreset) {
15968
16423
  const preset = normalizeContactsTimeRangePreset(timeRangePreset);
15969
- return path25.join(snapshotDir, `contacts-snapshot-${preset}.json`);
16424
+ return path26.join(snapshotDir, `contacts-snapshot-${preset}.json`);
15970
16425
  }
15971
16426
  function readContactsSnapshot(snapshotDir, timeRangePreset, options2 = {}) {
15972
16427
  const snapshotPath = getContactsSnapshotPath(snapshotDir, timeRangePreset);
15973
- if (!fs28.existsSync(snapshotPath)) return null;
16428
+ if (!fs29.existsSync(snapshotPath)) return null;
15974
16429
  try {
15975
- return JSON.parse(fs28.readFileSync(snapshotPath, "utf-8"));
16430
+ return JSON.parse(fs29.readFileSync(snapshotPath, "utf-8"));
15976
16431
  } catch (error) {
15977
16432
  const ts = options2.now?.() ?? Date.now();
15978
- const backupPath = path25.join(snapshotDir, `contacts-snapshot.corrupt-${ts}.json`);
16433
+ const backupPath = path26.join(snapshotDir, `contacts-snapshot.corrupt-${ts}.json`);
15979
16434
  try {
15980
- fs28.renameSync(snapshotPath, backupPath);
16435
+ fs29.renameSync(snapshotPath, backupPath);
15981
16436
  } catch (renameError) {
15982
16437
  appLogger.warn("contacts", "failed to backup corrupt contacts snapshot", renameError);
15983
16438
  }
@@ -15986,17 +16441,17 @@ function readContactsSnapshot(snapshotDir, timeRangePreset, options2 = {}) {
15986
16441
  }
15987
16442
  }
15988
16443
  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));
16444
+ if (!fs29.existsSync(snapshotDir)) fs29.mkdirSync(snapshotDir, { recursive: true });
16445
+ const tmpPath = path26.join(snapshotDir, `${CONTACTS_SNAPSHOT_TMP_PREFIX}${process.pid}-${Date.now()}`);
16446
+ fs29.writeFileSync(tmpPath, JSON.stringify(snapshot, null, 2), "utf-8");
16447
+ fs29.renameSync(tmpPath, getContactsSnapshotPath(snapshotDir, snapshot.timeRange.preset));
15993
16448
  }
15994
16449
  function cleanupContactsSnapshotTempFiles(snapshotDir) {
15995
- if (!fs28.existsSync(snapshotDir)) return;
15996
- for (const name of fs28.readdirSync(snapshotDir)) {
16450
+ if (!fs29.existsSync(snapshotDir)) return;
16451
+ for (const name of fs29.readdirSync(snapshotDir)) {
15997
16452
  if (!name.startsWith(CONTACTS_SNAPSHOT_TMP_PREFIX)) continue;
15998
16453
  try {
15999
- fs28.rmSync(path25.join(snapshotDir, name), { force: true });
16454
+ fs29.rmSync(path26.join(snapshotDir, name), { force: true });
16000
16455
  } catch (error) {
16001
16456
  appLogger.warn("contacts", "failed to remove contacts snapshot temp file", error);
16002
16457
  }
@@ -16494,23 +16949,23 @@ function buildPeopleRelationshipsSignature(adapter, timeRangePreset) {
16494
16949
  }
16495
16950
 
16496
16951
  // ../../packages/node-runtime/src/services/people/relationships/snapshot.ts
16497
- import fs29 from "fs";
16498
- import path26 from "path";
16952
+ import fs30 from "fs";
16953
+ import path27 from "path";
16499
16954
  var PEOPLE_RELATIONSHIPS_SNAPSHOT_TMP_PREFIX = "people-relationships-snapshot.tmp-";
16500
16955
  function getPeopleRelationshipsSnapshotPath(snapshotDir, timeRangePreset) {
16501
16956
  const preset = normalizePeopleRelationshipsTimeRangePreset(timeRangePreset);
16502
- return path26.join(snapshotDir, `graph-snapshot-${preset}.json`);
16957
+ return path27.join(snapshotDir, `graph-snapshot-${preset}.json`);
16503
16958
  }
16504
16959
  function readPeopleRelationshipsSnapshot(snapshotDir, timeRangePreset, options2 = {}) {
16505
16960
  const snapshotPath = getPeopleRelationshipsSnapshotPath(snapshotDir, timeRangePreset);
16506
- if (!fs29.existsSync(snapshotPath)) return null;
16961
+ if (!fs30.existsSync(snapshotPath)) return null;
16507
16962
  try {
16508
- return JSON.parse(fs29.readFileSync(snapshotPath, "utf-8"));
16963
+ return JSON.parse(fs30.readFileSync(snapshotPath, "utf-8"));
16509
16964
  } catch (error) {
16510
16965
  const ts = options2.now?.() ?? Date.now();
16511
- const backupPath = path26.join(snapshotDir, `graph-snapshot.corrupt-${ts}.json`);
16966
+ const backupPath = path27.join(snapshotDir, `graph-snapshot.corrupt-${ts}.json`);
16512
16967
  try {
16513
- fs29.renameSync(snapshotPath, backupPath);
16968
+ fs30.renameSync(snapshotPath, backupPath);
16514
16969
  } catch (renameError) {
16515
16970
  appLogger.warn("people-relationships", "failed to backup corrupt people relationships snapshot", renameError);
16516
16971
  }
@@ -16519,17 +16974,17 @@ function readPeopleRelationshipsSnapshot(snapshotDir, timeRangePreset, options2
16519
16974
  }
16520
16975
  }
16521
16976
  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));
16977
+ if (!fs30.existsSync(snapshotDir)) fs30.mkdirSync(snapshotDir, { recursive: true });
16978
+ const tmpPath = path27.join(snapshotDir, `${PEOPLE_RELATIONSHIPS_SNAPSHOT_TMP_PREFIX}${process.pid}-${Date.now()}`);
16979
+ fs30.writeFileSync(tmpPath, JSON.stringify(snapshot, null, 2), "utf-8");
16980
+ fs30.renameSync(tmpPath, getPeopleRelationshipsSnapshotPath(snapshotDir, snapshot.timeRange.preset));
16526
16981
  }
16527
16982
  function cleanupPeopleRelationshipsSnapshotTempFiles(snapshotDir) {
16528
- if (!fs29.existsSync(snapshotDir)) return;
16529
- for (const name of fs29.readdirSync(snapshotDir)) {
16983
+ if (!fs30.existsSync(snapshotDir)) return;
16984
+ for (const name of fs30.readdirSync(snapshotDir)) {
16530
16985
  if (!name.startsWith(PEOPLE_RELATIONSHIPS_SNAPSHOT_TMP_PREFIX)) continue;
16531
16986
  try {
16532
- fs29.rmSync(path26.join(snapshotDir, name), { force: true });
16987
+ fs30.rmSync(path27.join(snapshotDir, name), { force: true });
16533
16988
  } catch (error) {
16534
16989
  appLogger.warn("people-relationships", "failed to remove people relationships snapshot temp file", error);
16535
16990
  }
@@ -17050,9 +17505,310 @@ __export(relationships_exports, {
17050
17505
  getPeopleRelationshipsFactsCacheDir: () => getPeopleRelationshipsFactsCacheDir
17051
17506
  });
17052
17507
 
17508
+ // ../../packages/node-runtime/src/services/global-insight/service.ts
17509
+ import fs31 from "fs";
17510
+
17511
+ // ../../packages/node-runtime/src/services/global-insight/signature.ts
17512
+ function buildAnnualSummarySignature(adapter, range) {
17513
+ const parts = [
17514
+ `algorithm:${ANNUAL_SUMMARY_ALGORITHM_VERSION}`,
17515
+ `range:${toAnnualSummaryRangeKey(range)}`,
17516
+ `start:${range.startTs}`,
17517
+ `day:${toAnnualSummaryLocalDate(range)}`
17518
+ ];
17519
+ for (const sessionId of [...adapter.listSessionIds()].sort()) {
17520
+ parts.push(`${sessionId}:${getDbFileVersion(adapter.getDbPath(sessionId))}`);
17521
+ }
17522
+ return parts.join("|");
17523
+ }
17524
+
17525
+ // ../../packages/node-runtime/src/services/global-insight/worker-runner.ts
17526
+ import { existsSync as existsSync11 } from "fs";
17527
+ import { Worker as Worker3 } from "worker_threads";
17528
+ function resolveDefaultAnnualSummaryWorkerEntryUrl(currentModuleUrl = import.meta.url, entryExists = (url) => existsSync11(url)) {
17529
+ const moduleUrl = typeof currentModuleUrl === "string" ? currentModuleUrl : currentModuleUrl.href;
17530
+ if (moduleUrl.endsWith(".ts")) return new URL("./worker-entry.ts", moduleUrl);
17531
+ if (moduleUrl.endsWith(".mjs")) return new URL("./global-insight-worker.mjs", moduleUrl);
17532
+ const sibling = new URL("./worker-entry.js", moduleUrl);
17533
+ return entryExists(sibling) ? sibling : new URL("./global-insight-worker.js", moduleUrl);
17534
+ }
17535
+ function createAnnualSummaryWorkerRunner(options2) {
17536
+ return ({ signature, range, signal, onProgress }) => new Promise((resolve2, reject) => {
17537
+ if (signal.aborted) {
17538
+ reject(new Error("annual summary worker aborted"));
17539
+ return;
17540
+ }
17541
+ const worker = createWorker3(
17542
+ {
17543
+ paths: snapshotPathProvider(options2.pathProvider),
17544
+ runtimeIdentity: options2.runtimeIdentity,
17545
+ nativeBinding: options2.nativeBinding,
17546
+ signature,
17547
+ range
17548
+ },
17549
+ options2.workerEntryUrl
17550
+ );
17551
+ let settled = false;
17552
+ const abort = () => {
17553
+ if (settled) return;
17554
+ settled = true;
17555
+ void worker.terminate();
17556
+ reject(new Error("annual summary worker aborted"));
17557
+ };
17558
+ signal.addEventListener("abort", abort, { once: true });
17559
+ worker.on("message", (message) => {
17560
+ if (message.type === "progress" && message.progress) {
17561
+ onProgress(message.progress);
17562
+ return;
17563
+ }
17564
+ if (message.type === "success" && message.snapshot) {
17565
+ settled = true;
17566
+ signal.removeEventListener("abort", abort);
17567
+ resolve2(message.snapshot);
17568
+ void worker.terminate();
17569
+ return;
17570
+ }
17571
+ if (message.type === "error") {
17572
+ settled = true;
17573
+ signal.removeEventListener("abort", abort);
17574
+ reject(new Error(message.error ?? "annual summary worker failed"));
17575
+ void worker.terminate();
17576
+ }
17577
+ });
17578
+ worker.on("error", (error) => {
17579
+ if (settled) return;
17580
+ settled = true;
17581
+ signal.removeEventListener("abort", abort);
17582
+ reject(error);
17583
+ });
17584
+ worker.on("exit", (code) => {
17585
+ if (settled || code === 0) return;
17586
+ settled = true;
17587
+ signal.removeEventListener("abort", abort);
17588
+ reject(new Error(`annual summary worker exited with code ${code}`));
17589
+ });
17590
+ });
17591
+ }
17592
+ function createWorker3(workerData, entryUrlInput) {
17593
+ const entryUrl = entryUrlInput ? typeof entryUrlInput === "string" ? new URL(entryUrlInput) : entryUrlInput : resolveDefaultAnnualSummaryWorkerEntryUrl();
17594
+ if (!entryUrl.href.endsWith(".ts")) return new Worker3(entryUrl, { workerData });
17595
+ const bootstrap = `
17596
+ import { register } from 'tsx/esm/api';
17597
+ register();
17598
+ await import(${JSON.stringify(entryUrl.href)});
17599
+ `;
17600
+ const workerOptions = { eval: true, type: "module", workerData, execArgv: [] };
17601
+ return new Worker3(bootstrap, workerOptions);
17602
+ }
17603
+
17604
+ // ../../packages/node-runtime/src/services/global-insight/service.ts
17605
+ function createGlobalInsightService(deps) {
17606
+ return new DefaultGlobalInsightService(deps);
17607
+ }
17608
+ var DefaultGlobalInsightService = class {
17609
+ constructor(deps) {
17610
+ this.deps = deps;
17611
+ this.snapshotDir = deps.userDataDir ? getGlobalInsightDir(deps.userDataDir) : getGlobalInsightDir(requirePathProvider3(deps).getUserDataDir());
17612
+ cleanupAnnualSummarySnapshotTempFiles(this.snapshotDir);
17613
+ this.runner = deps.runner ?? createAnnualSummaryWorkerRunner({
17614
+ pathProvider: requirePathProvider3(deps),
17615
+ runtimeIdentity: deps.runtimeIdentity,
17616
+ nativeBinding: deps.nativeBinding,
17617
+ workerEntryUrl: deps.workerEntryUrl
17618
+ });
17619
+ }
17620
+ deps;
17621
+ snapshots = /* @__PURE__ */ new Map();
17622
+ snapshotDir;
17623
+ runner;
17624
+ inFlight = null;
17625
+ task = createIdleTaskState3();
17626
+ getAnnualSummary(options2 = {}) {
17627
+ const range = this.normalizeRange(options2);
17628
+ const signature = buildAnnualSummarySignature(this.deps.adapter, range);
17629
+ const status = this.getCacheStatus(signature, range);
17630
+ if (this.shouldStartTask(options2, status)) this.ensureTaskStarted(signature, range);
17631
+ return this.toResponse(signature, range, options2.acceptStale === true);
17632
+ }
17633
+ startRecompute(options2 = {}) {
17634
+ const range = this.normalizeRange(options2);
17635
+ const signature = buildAnnualSummarySignature(this.deps.adapter, range);
17636
+ this.ensureTaskStarted(signature, range);
17637
+ return this.toResponse(signature, range, true);
17638
+ }
17639
+ invalidateCache() {
17640
+ this.snapshots.clear();
17641
+ }
17642
+ normalizeRange(input = {}) {
17643
+ return normalizeAnnualSummaryRange(input, new Date(this.now()));
17644
+ }
17645
+ replaceSnapshotForTests(snapshot) {
17646
+ writeAnnualSummarySnapshot(this.snapshotDir, snapshot);
17647
+ this.snapshots.set(toAnnualSummaryRangeKey(snapshot.range), snapshot);
17648
+ }
17649
+ async close() {
17650
+ const inFlight = this.inFlight;
17651
+ if (!inFlight) return;
17652
+ this.inFlight = null;
17653
+ inFlight.abortController.abort();
17654
+ this.task = {
17655
+ ...this.task,
17656
+ status: "failed",
17657
+ finishedAt: this.now(),
17658
+ lastError: "annual summary task aborted"
17659
+ };
17660
+ }
17661
+ shouldStartTask(options2, status) {
17662
+ if (options2.forceRecompute) return true;
17663
+ if (status === "fresh") return false;
17664
+ return this.task.status !== "failed";
17665
+ }
17666
+ ensureTaskStarted(signature, range) {
17667
+ if (this.inFlight) return;
17668
+ const id = `annual_summary_${this.now()}_${Math.random().toString(36).slice(2)}`;
17669
+ this.task = {
17670
+ id,
17671
+ status: "running",
17672
+ startedAt: this.now(),
17673
+ finishedAt: null,
17674
+ processedSessions: 0,
17675
+ totalSessions: this.deps.adapter.listSessionIds().length
17676
+ };
17677
+ const abortController = new AbortController();
17678
+ const promise = this.runner({
17679
+ signature,
17680
+ range,
17681
+ signal: abortController.signal,
17682
+ onProgress: (progress) => {
17683
+ if (this.task.id !== id || this.task.status !== "running") return;
17684
+ this.task = { ...this.task, ...progress };
17685
+ }
17686
+ });
17687
+ this.inFlight = { id, signature, promise, abortController };
17688
+ promise.then((snapshot) => this.handleTaskSuccess(id, signature, snapshot)).catch((error) => this.handleTaskFailure(id, error));
17689
+ }
17690
+ handleTaskSuccess(id, inputSignature, snapshot) {
17691
+ if (this.inFlight?.id !== id) return;
17692
+ const latestSignature = buildAnnualSummarySignature(this.deps.adapter, snapshot.range);
17693
+ const finishedAt = this.now();
17694
+ if (inputSignature !== latestSignature || snapshot.signature !== latestSignature) {
17695
+ this.inFlight = null;
17696
+ this.task = { ...this.task, status: "superseded", finishedAt, currentSessionId: void 0 };
17697
+ appLogger.info("global-insight", "annual summary worker result discarded because signature changed");
17698
+ return;
17699
+ }
17700
+ try {
17701
+ writeAnnualSummarySnapshot(this.snapshotDir, snapshot);
17702
+ this.inFlight = null;
17703
+ this.snapshots.set(toAnnualSummaryRangeKey(snapshot.range), snapshot);
17704
+ this.task = {
17705
+ ...this.task,
17706
+ status: "succeeded",
17707
+ finishedAt,
17708
+ processedSessions: snapshot.workerStats.processedSessions,
17709
+ totalSessions: snapshot.workerStats.totalSessions,
17710
+ currentSessionId: void 0
17711
+ };
17712
+ appLogger.info("global-insight", "annual summary snapshot persisted", {
17713
+ analyzedSessions: snapshot.coverage.analyzedSessions,
17714
+ durationMs: snapshot.workerStats.durationMs
17715
+ });
17716
+ } catch (error) {
17717
+ this.handleTaskFailure(id, error);
17718
+ }
17719
+ }
17720
+ handleTaskFailure(id, error) {
17721
+ if (this.inFlight?.id !== id) return;
17722
+ this.inFlight = null;
17723
+ this.task = {
17724
+ ...this.task,
17725
+ status: "failed",
17726
+ finishedAt: this.now(),
17727
+ currentSessionId: void 0,
17728
+ lastError: error instanceof Error ? error.message : String(error)
17729
+ };
17730
+ appLogger.error("global-insight", "annual summary worker failed", error);
17731
+ }
17732
+ toResponse(signature, range, acceptStale) {
17733
+ const snapshot = this.getSnapshot(range);
17734
+ const status = this.getCacheStatus(signature, range);
17735
+ const includeSnapshot = status === "fresh" || status === "stale" && acceptStale;
17736
+ return {
17737
+ range,
17738
+ availableDataYears: includeSnapshot ? snapshot?.availableDataYears ?? [] : [],
17739
+ latestDataYear: includeSnapshot ? snapshot?.latestDataYear ?? null : null,
17740
+ metrics: includeSnapshot ? snapshot?.metrics ?? null : null,
17741
+ monthlyActivity: includeSnapshot ? snapshot?.monthlyActivity ?? [] : [],
17742
+ dailyActivity: includeSnapshot ? snapshot?.dailyActivity ?? [] : [],
17743
+ messageTypes: includeSnapshot ? snapshot?.messageTypes ?? [] : [],
17744
+ textLength: includeSnapshot ? snapshot?.textLength ?? null : null,
17745
+ coverage: includeSnapshot ? snapshot?.coverage ?? emptyCoverage() : emptyCoverage(),
17746
+ cache: {
17747
+ status,
17748
+ computedAt: snapshot?.computedAt ?? null,
17749
+ signature: snapshot?.signature,
17750
+ staleReason: status === "stale" ? "signature_changed" : void 0
17751
+ },
17752
+ task: this.task
17753
+ };
17754
+ }
17755
+ getCacheStatus(signature, range) {
17756
+ const snapshot = this.getSnapshot(range);
17757
+ if (!snapshot) return "missing";
17758
+ return snapshot.signature === signature ? "fresh" : "stale";
17759
+ }
17760
+ getSnapshot(range) {
17761
+ const key = toAnnualSummaryRangeKey(range);
17762
+ if (this.snapshots.has(key) && !fs31.existsSync(getAnnualSummarySnapshotPath(this.snapshotDir, range))) {
17763
+ this.snapshots.delete(key);
17764
+ }
17765
+ if (!this.snapshots.has(key)) {
17766
+ this.snapshots.set(key, readAnnualSummarySnapshot(this.snapshotDir, range, { now: this.deps.now }));
17767
+ }
17768
+ return this.snapshots.get(key) ?? null;
17769
+ }
17770
+ now() {
17771
+ return this.deps.now?.() ?? Date.now();
17772
+ }
17773
+ };
17774
+ function requirePathProvider3(deps) {
17775
+ if (!deps.pathProvider)
17776
+ throw new Error("GlobalInsightService requires pathProvider unless userDataDir and runner are provided");
17777
+ return deps.pathProvider;
17778
+ }
17779
+ function createIdleTaskState3() {
17780
+ return {
17781
+ id: null,
17782
+ status: "idle",
17783
+ startedAt: null,
17784
+ finishedAt: null,
17785
+ processedSessions: 0,
17786
+ totalSessions: 0
17787
+ };
17788
+ }
17789
+ function emptyCoverage() {
17790
+ return {
17791
+ totalSessions: 0,
17792
+ analyzedSessions: 0,
17793
+ missingOwnerSessions: 0,
17794
+ unresolvedOwnerSessions: 0,
17795
+ failedSessions: 0
17796
+ };
17797
+ }
17798
+
17799
+ // ../../packages/node-runtime/src/services/global-insight/index.ts
17800
+ var global_insight_exports = {};
17801
+ __export(global_insight_exports, {
17802
+ ANNUAL_SUMMARY_ALGORITHM_VERSION: () => ANNUAL_SUMMARY_ALGORITHM_VERSION,
17803
+ createGlobalInsightService: () => createGlobalInsightService,
17804
+ deleteAnnualSummarySnapshots: () => deleteAnnualSummarySnapshots,
17805
+ getGlobalInsightDir: () => getGlobalInsightDir,
17806
+ getGlobalInsightFactsCacheDir: () => getGlobalInsightFactsCacheDir
17807
+ });
17808
+
17053
17809
  // ../../packages/node-runtime/src/semantic-index/config.ts
17054
- import fs30 from "fs";
17055
- import path27 from "path";
17810
+ import fs32 from "fs";
17811
+ import path28 from "path";
17056
17812
  var DEFAULT_SEMANTIC_INDEX_MODEL_DOWNLOAD_SOURCE = "huggingface";
17057
17813
  var SEMANTIC_INDEX_CONFIG_VERSION = 1;
17058
17814
  var SEARCH_MAX_RESULTS_DEFAULT = 10;
@@ -17141,9 +17897,9 @@ var SemanticIndexConfigStore = class {
17141
17897
  return this.set({ ...this.get(), enabled });
17142
17898
  }
17143
17899
  get() {
17144
- if (!fs30.existsSync(this.filePath)) return defaultSemanticIndexConfig();
17900
+ if (!fs32.existsSync(this.filePath)) return defaultSemanticIndexConfig();
17145
17901
  try {
17146
- const raw = JSON.parse(fs30.readFileSync(this.filePath, "utf-8"));
17902
+ const raw = JSON.parse(fs32.readFileSync(this.filePath, "utf-8"));
17147
17903
  return normalizeConfig(raw);
17148
17904
  } catch {
17149
17905
  return defaultSemanticIndexConfig();
@@ -17151,9 +17907,9 @@ var SemanticIndexConfigStore = class {
17151
17907
  }
17152
17908
  set(config) {
17153
17909
  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");
17910
+ const dir = path28.dirname(this.filePath);
17911
+ if (!fs32.existsSync(dir)) fs32.mkdirSync(dir, { recursive: true });
17912
+ fs32.writeFileSync(this.filePath, JSON.stringify(normalized, null, 2), "utf-8");
17157
17913
  return normalized;
17158
17914
  }
17159
17915
  resolveModelId() {
@@ -17162,7 +17918,7 @@ var SemanticIndexConfigStore = class {
17162
17918
  };
17163
17919
 
17164
17920
  // ../../packages/node-runtime/src/semantic-index/service.ts
17165
- import path28 from "path";
17921
+ import path29 from "path";
17166
17922
 
17167
17923
  // ../../packages/node-runtime/src/semantic-index/chunker-config.ts
17168
17924
  import { createHash as createHash2 } from "crypto";
@@ -18143,7 +18899,7 @@ function formatDateTime(ts) {
18143
18899
  const d = new Date(ts);
18144
18900
  return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
18145
18901
  }
18146
- function formatTime2(ts) {
18902
+ function formatTime(ts) {
18147
18903
  const d = new Date(ts);
18148
18904
  return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
18149
18905
  }
@@ -18151,7 +18907,7 @@ function formatTimeRange2(startTs, endTs) {
18151
18907
  const start = new Date(startTs);
18152
18908
  const end = new Date(endTs);
18153
18909
  const sameDay = start.getFullYear() === end.getFullYear() && start.getMonth() === end.getMonth() && start.getDate() === end.getDate();
18154
- return `${formatDateTime(startTs)} - ${sameDay ? formatTime2(endTs) : formatDateTime(endTs)}`;
18910
+ return `${formatDateTime(startTs)} - ${sameDay ? formatTime(endTs) : formatDateTime(endTs)}`;
18155
18911
  }
18156
18912
  function resolveSenderNameTokenCap(nameTokenCounts, totalLimit) {
18157
18913
  const total = nameTokenCounts.reduce((sum, count) => sum + count, 0);
@@ -18693,16 +19449,16 @@ function resolveSearchToolEvidenceTokens(maxResultTokens) {
18693
19449
  return maxResultTokens && maxResultTokens > 0 ? Math.min(SEARCH_TOOL_EVIDENCE_TOKENS, maxResultTokens) : SEARCH_TOOL_EVIDENCE_TOKENS;
18694
19450
  }
18695
19451
  function sessionIdFromDbPath(dbPath) {
18696
- return path28.basename(dbPath, ".db");
19452
+ return path29.basename(dbPath, ".db");
18697
19453
  }
18698
19454
  var SEMANTIC_INDEX_DB_FILE = "embedding_index.db";
18699
19455
  var SEMANTIC_INDEX_CONFIG_FILE = "semantic-index-config.json";
18700
19456
  function createSemanticIndexService(params) {
18701
19457
  const { pathProvider } = params;
18702
19458
  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"),
19459
+ vectorDbPath: path29.join(pathProvider.getVectorDir(), SEMANTIC_INDEX_DB_FILE),
19460
+ configPath: path29.join(pathProvider.getAiDataDir(), SEMANTIC_INDEX_CONFIG_FILE),
19461
+ modelsCacheDir: path29.join(pathProvider.getAiDataDir(), "models", "semantic-index"),
18706
19462
  modelDownloadProxyUrl: params.modelDownloadProxyUrl,
18707
19463
  sessionAdapter: params.sessionAdapter,
18708
19464
  nativeBinding: params.nativeBinding,
@@ -19252,8 +20008,8 @@ ${text}`;
19252
20008
  };
19253
20009
 
19254
20010
  // ../../packages/node-runtime/src/services/merge-cache.ts
19255
- import * as fs31 from "fs";
19256
- import * as path29 from "path";
20011
+ import * as fs33 from "fs";
20012
+ import * as path30 from "path";
19257
20013
  import * as crypto from "crypto";
19258
20014
  var MergeSessionCache = class {
19259
20015
  cache = /* @__PURE__ */ new Map();
@@ -19262,15 +20018,15 @@ var MergeSessionCache = class {
19262
20018
  constructor(pathProvider, options2) {
19263
20019
  this.tempDir = pathProvider.getTempDir();
19264
20020
  this.nativeBinding = options2?.nativeBinding;
19265
- if (!fs31.existsSync(this.tempDir)) {
19266
- fs31.mkdirSync(this.tempDir, { recursive: true });
20021
+ if (!fs33.existsSync(this.tempDir)) {
20022
+ fs33.mkdirSync(this.tempDir, { recursive: true });
19267
20023
  }
19268
20024
  }
19269
20025
  generateTempDbPath(filename) {
19270
20026
  const ts = Date.now();
19271
20027
  const rand = Math.random().toString(36).slice(2, 8);
19272
20028
  const safeName = filename.replace(/[/\\?%*:|"<>]/g, "_").slice(0, 40);
19273
- return path29.join(this.tempDir, `merge_${safeName}_${ts}_${rand}.db`);
20029
+ return path30.join(this.tempDir, `merge_${safeName}_${ts}_${rand}.db`);
19274
20030
  }
19275
20031
  /** Store a parsed file's temp DB and return a handle for subsequent requests. */
19276
20032
  store(filename, tempDbPath) {
@@ -19281,7 +20037,7 @@ var MergeSessionCache = class {
19281
20037
  /** Open a TempDbReader for a given handle. Caller must close the reader. */
19282
20038
  openReader(handle) {
19283
20039
  const entry = this.cache.get(handle);
19284
- if (!entry || !fs31.existsSync(entry.tempDbPath)) return null;
20040
+ if (!entry || !fs33.existsSync(entry.tempDbPath)) return null;
19285
20041
  const adapter = openBetterSqliteDatabase(entry.tempDbPath, {
19286
20042
  readonly: true,
19287
20043
  nativeBinding: this.nativeBinding
@@ -19326,8 +20082,7 @@ var MergeSessionCache = class {
19326
20082
  };
19327
20083
 
19328
20084
  // ../../packages/node-runtime/src/services/push-importer.ts
19329
- import * as fs32 from "fs";
19330
- var importInProgress = /* @__PURE__ */ new Set();
20085
+ import * as fs34 from "fs";
19331
20086
  var SYSTEM_SENDER_ID = "SYSTEM";
19332
20087
  var SYSTEM_MEMBER_NAME = "\u7CFB\u7EDF\u6D88\u606F";
19333
20088
  function validatePayload(payload, isNew) {
@@ -19594,20 +20349,29 @@ async function pushImport(dbManager, sessionId, payload) {
19594
20349
  if (!SAFE_SESSION_ID_RE.test(sessionId) || sessionId.includes("..")) {
19595
20350
  return { ok: false, reason: "invalid_payload", message: "sessionId contains invalid characters" };
19596
20351
  }
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
- };
20352
+ try {
20353
+ return await withDataDirImportLock(
20354
+ dbManager.getUserDataDir(),
20355
+ () => pushImportUnlocked(dbManager, sessionId, payload)
20356
+ );
20357
+ } catch (error) {
20358
+ if (error instanceof ImportInProgressError) {
20359
+ return {
20360
+ ok: false,
20361
+ reason: "import_in_progress",
20362
+ message: "Another import is already in progress"
20363
+ };
20364
+ }
20365
+ throw error;
19603
20366
  }
20367
+ }
20368
+ async function pushImportUnlocked(dbManager, sessionId, payload) {
19604
20369
  const dbPath = dbManager.getDbPath(sessionId);
19605
- const isNew = !fs32.existsSync(dbPath);
20370
+ const isNew = !fs34.existsSync(dbPath);
19606
20371
  const validationError = validatePayload(payload, isNew);
19607
20372
  if (validationError) {
19608
20373
  return { ok: false, reason: "invalid_payload", message: validationError };
19609
20374
  }
19610
- importInProgress.add(sessionId);
19611
20375
  try {
19612
20376
  if (isNew) {
19613
20377
  const db2 = dbManager.openRawSessionDatabase(sessionId, { create: true });
@@ -19661,8 +20425,6 @@ async function pushImport(dbManager, sessionId, payload) {
19661
20425
  }
19662
20426
  }
19663
20427
  return { ok: false, reason: "import_failed", message: err instanceof Error ? err.message : String(err) };
19664
- } finally {
19665
- importInProgress.delete(sessionId);
19666
20428
  }
19667
20429
  }
19668
20430
 
@@ -19704,6 +20466,12 @@ export {
19704
20466
  streamParseFileInfo,
19705
20467
  analyzeIncrementalImport,
19706
20468
  incrementalImport,
20469
+ resolveAutoImportTarget,
20470
+ autoImportFile,
20471
+ IMPORT_LOCK_FILENAME,
20472
+ IMPORT_IN_PROGRESS_ERROR_KEY,
20473
+ ImportInProgressError,
20474
+ withDataDirImportLock,
19707
20475
  ArchiveImportError,
19708
20476
  validateArchiveEntryName,
19709
20477
  ZipArchiveReader,
@@ -19809,6 +20577,8 @@ export {
19809
20577
  contacts_exports,
19810
20578
  createPeopleRelationshipsService,
19811
20579
  relationships_exports,
20580
+ createGlobalInsightService,
20581
+ global_insight_exports,
19812
20582
  CHUNK_VECTOR_INDEX_TABLE,
19813
20583
  CHUNK_VECTOR_INDEX_INDEXES,
19814
20584
  EMBEDDING_INDEX_SCHEMA,
@@ -19868,4 +20638,4 @@ export {
19868
20638
  MergeSessionCache,
19869
20639
  pushImport
19870
20640
  };
19871
- //# sourceMappingURL=chunk-36JGKW6K.mjs.map
20641
+ //# sourceMappingURL=chunk-T64GA5HP.mjs.map