chatlab-cli 0.34.2-beta.2 → 0.34.3

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 (117) hide show
  1. package/dist/{chat-command-6IOH2QRF.mjs → chat-command-QH7DXCAJ.mjs} +8 -8
  2. package/dist/{chunk-JX6MFS6F.mjs → chunk-2Q3HHVTS.mjs} +15 -15
  3. package/dist/{chunk-INBJ7SND.mjs → chunk-2WYXD5ZP.mjs} +117 -416
  4. package/dist/chunk-2WYXD5ZP.mjs.map +1 -0
  5. package/dist/{chunk-U6MVJSXX.mjs → chunk-3X26JJIP.mjs} +15 -9
  6. package/dist/chunk-3X26JJIP.mjs.map +1 -0
  7. package/dist/{chunk-UMBEHBIZ.mjs → chunk-6U2QZO3N.mjs} +2 -2
  8. package/dist/{chunk-52P5ZRBD.mjs → chunk-AWGXYFIC.mjs} +2 -2
  9. package/dist/{chunk-4LW6443F.mjs → chunk-AWXHYTD5.mjs} +795 -876
  10. package/dist/chunk-AWXHYTD5.mjs.map +1 -0
  11. package/dist/{chunk-4RLZFGO5.mjs → chunk-C2PD3BS7.mjs} +2 -2
  12. package/dist/{chunk-H6D4DZOC.mjs → chunk-JYBPJWE2.mjs} +7 -5
  13. package/dist/{chunk-H6D4DZOC.mjs.map → chunk-JYBPJWE2.mjs.map} +1 -1
  14. package/dist/{chunk-652KEAX3.mjs → chunk-SWUTRDGZ.mjs} +2 -2
  15. package/dist/{chunk-72RWABYJ.mjs → chunk-WJVE5Q4Y.mjs} +3 -3
  16. package/dist/chunk-WJVE5Q4Y.mjs.map +1 -0
  17. package/dist/{chunk-2XCNTG4O.mjs → chunk-XIWVJAXH.mjs} +670 -54
  18. package/dist/chunk-XIWVJAXH.mjs.map +1 -0
  19. package/dist/{chunk-MENKFEPC.mjs → chunk-ZVGDOSGG.mjs} +29 -41
  20. package/dist/chunk-ZVGDOSGG.mjs.map +1 -0
  21. package/dist/cli.mjs +9 -9
  22. package/dist/contacts-worker.mjs +2 -2
  23. package/dist/global-insight-worker.mjs +1 -1
  24. package/dist/{http-HSK46XEU.mjs → http-D2F4QTQW.mjs} +12 -12
  25. package/dist/{import-DF2WD2HE.mjs → import-H7X2JU2O.mjs} +7 -7
  26. package/dist/index.mjs +12 -12
  27. package/dist/{mcp-A2RND7KN.mjs → mcp-UURXRGDF.mjs} +8 -8
  28. package/dist/people-relationships-worker.mjs +2 -2
  29. package/dist/semantic-index-worker.mjs +1 -1
  30. package/dist/{src-SHFHW2QB.mjs → src-MHCO4HYY.mjs} +22 -38
  31. package/dist/{stream-import-GZ2QDE3Z.mjs → stream-import-BHXACUCA.mjs} +7 -7
  32. package/dist/{worker-runtime-OWN56GG5.mjs → worker-runtime-JOUCP2L4.mjs} +5 -5
  33. package/dist-cli-web/assets/ActionToolsPanel.vue_vue_type_script_setup_true_lang-kN3uxTW8.js +110 -0
  34. package/dist-cli-web/assets/{BufferResource-B-LHxriL.js → BufferResource-Dak23r3B.js} +1 -1
  35. package/dist-cli-web/assets/{CanvasRenderer-W3h_GGHM.js → CanvasRenderer-Cmjtvski.js} +1 -1
  36. package/dist-cli-web/assets/DataDirCleanupNotice-B5tyDzK-.js +1 -0
  37. package/dist-cli-web/assets/{EChartWordcloud-Dt6rCzHf.js → EChartWordcloud-Bd3L3G8I.js} +1 -1
  38. package/dist-cli-web/assets/{Filter-BpSopVKd.js → Filter-CHKZVYB3.js} +1 -1
  39. package/dist-cli-web/assets/LoadingState.vue_vue_type_script_setup_true_lang-CKf_gC1k.js +1 -0
  40. package/dist-cli-web/assets/{MessageExportModal-BoK-_h25.js → MessageExportModal-ODYCcSnk.js} +1 -1
  41. package/dist-cli-web/assets/{PageHeader-CD8B7Emf.js → PageHeader-Bn7Jn8JW.js} +1 -1
  42. package/dist-cli-web/assets/{RelationshipGalaxyCanvas-BO3mo-Re.js → RelationshipGalaxyCanvas-mJhEPzsY.js} +3 -3
  43. package/dist-cli-web/assets/{RelationshipGalaxyThreeCanvas-Brnpz6Ig.js → RelationshipGalaxyThreeCanvas-Bf76yzmQ.js} +1 -1
  44. package/dist-cli-web/assets/{RenderTargetSystem-CNP4JLfK.js → RenderTargetSystem-1rM7OF-M.js} +1 -1
  45. package/dist-cli-web/assets/{ThemeCard.vue_vue_type_script_setup_true_lang-Na3LIoSJ.js → ThemeCard.vue_vue_type_script_setup_true_lang-CADZpPia.js} +1 -1
  46. package/dist-cli-web/assets/TimeSelect.vue_vue_type_script_setup_true_lang-I3FYjhfs.js +1 -0
  47. package/dist-cli-web/assets/{WebGLRenderer-CIR5cTQN.js → WebGLRenderer-Y0jQtBvK.js} +1 -1
  48. package/dist-cli-web/assets/{WebGPURenderer-Dz3pCrzX.js → WebGPURenderer-BcUvCPhd.js} +1 -1
  49. package/dist-cli-web/assets/{annual-summary-time-range-QhoA3uC6.js → annual-summary-time-range-BIADd8qE.js} +1 -1
  50. package/dist-cli-web/assets/{base-p9yfnTpk.js → base-BpxxC5Z5.js} +1 -1
  51. package/dist-cli-web/assets/{browserAll-DMaXjf8N.js → browserAll-BsR1UaCC.js} +1 -1
  52. package/dist-cli-web/assets/{cli-web-Dvy2aHxG.js → cli-web-BiWUOncc.js} +1 -1
  53. package/dist-cli-web/assets/{dateFormat-5x3JKlIk.js → dateFormat-B6JnNCxp.js} +1 -1
  54. package/dist-cli-web/assets/{fetch-BtJWW2-O.js → fetch-BAJ_VjWE.js} +1 -1
  55. package/dist-cli-web/assets/{fetch-B0p6QRQ8.js → fetch-BNcHe2-O.js} +1 -1
  56. package/dist-cli-web/assets/{fetch-BI-Se2p7.js → fetch-CPloHijF.js} +1 -1
  57. package/dist-cli-web/assets/{fetch-CBaZNOpK.js → fetch-Clj_RVUD.js} +1 -1
  58. package/dist-cli-web/assets/{fetch-BderOzzL.js → fetch-D7vgaLpy.js} +1 -1
  59. package/dist-cli-web/assets/{fetch-DRGykJYb.js → fetch-DGvqctIb.js} +1 -1
  60. package/dist-cli-web/assets/fetch-DSxgaVBO.js +1 -0
  61. package/dist-cli-web/assets/{fetch-QLurhC_z.js → fetch-DtnhbqTN.js} +1 -1
  62. package/dist-cli-web/assets/{fetch-Cne-uL1m.js → fetch-bdY0RMM3.js} +1 -1
  63. package/dist-cli-web/assets/{fetch-CH9Hm20R.js → fetch-bhHY8rO7.js} +1 -1
  64. package/dist-cli-web/assets/index-B6Gq11lw.css +1 -0
  65. package/dist-cli-web/assets/index-BJ2lywiG.js +1 -0
  66. package/dist-cli-web/assets/{index-CXh01dqh.js → index-BT3_fSP6.js} +2 -2
  67. package/dist-cli-web/assets/{index-Dp2uglLH.js → index-BVZhP1J5.js} +1 -1
  68. package/dist-cli-web/assets/index-Br8r4pu8.js +1 -0
  69. package/dist-cli-web/assets/index-Byok4VaA.css +1 -0
  70. package/dist-cli-web/assets/index-CKnnxOci.js +1 -0
  71. package/dist-cli-web/assets/index-CXnYfvTv.js +137 -0
  72. package/dist-cli-web/assets/index-CsFLLk3T.css +1 -0
  73. package/dist-cli-web/assets/index-CzoOQ7zg.js +1 -0
  74. package/dist-cli-web/assets/{index-DF2Dijfg.js → index-D-dTTUGj.js} +1 -1
  75. package/dist-cli-web/assets/index-DEbvEKzi.js +2 -0
  76. package/dist-cli-web/assets/{index-IvxuxPNt.js → index-DMYFpctK.js} +1 -1
  77. package/dist-cli-web/assets/index-KO7uPd8W.js +64 -0
  78. package/dist-cli-web/assets/index-lkmJ6dI7.js +1 -0
  79. package/dist-cli-web/assets/{index-CumZ5uQ4.js → index-y7xPDpsW.js} +4 -4
  80. package/dist-cli-web/assets/{people-page-header-BRb9BrgP.js → people-page-header-gjTyODWs.js} +1 -1
  81. package/dist-cli-web/assets/{webworkerAll-BoY19vZt.js → webworkerAll-CfZWlukP.js} +1 -1
  82. package/dist-cli-web/index.html +2 -2
  83. package/package.json +1 -1
  84. package/dist/chunk-2XCNTG4O.mjs.map +0 -1
  85. package/dist/chunk-4LW6443F.mjs.map +0 -1
  86. package/dist/chunk-72RWABYJ.mjs.map +0 -1
  87. package/dist/chunk-INBJ7SND.mjs.map +0 -1
  88. package/dist/chunk-MENKFEPC.mjs.map +0 -1
  89. package/dist/chunk-U6MVJSXX.mjs.map +0 -1
  90. package/dist-cli-web/assets/ActionToolsPanel.vue_vue_type_script_setup_true_lang-yr_T4ttU.js +0 -110
  91. package/dist-cli-web/assets/DataDirCleanupNotice-C6CMNhfs.js +0 -1
  92. package/dist-cli-web/assets/LoadingState.vue_vue_type_script_setup_true_lang-Bi_Uag3T.js +0 -1
  93. package/dist-cli-web/assets/TimeSelect.vue_vue_type_script_setup_true_lang-CKe1YZrV.js +0 -1
  94. package/dist-cli-web/assets/fetch-CbpKKkdO.js +0 -1
  95. package/dist-cli-web/assets/index-4NL2pYn0.js +0 -1
  96. package/dist-cli-web/assets/index-B-KdENnr.js +0 -1
  97. package/dist-cli-web/assets/index-BDyixm5n.js +0 -1
  98. package/dist-cli-web/assets/index-BaCd1avL.js +0 -1
  99. package/dist-cli-web/assets/index-CVBBDI3B.js +0 -64
  100. package/dist-cli-web/assets/index-CioAsn_2.js +0 -137
  101. package/dist-cli-web/assets/index-Ct90lmPb.js +0 -1
  102. package/dist-cli-web/assets/index-DYCPQMOl.js +0 -2
  103. package/dist-cli-web/assets/index-DrjoMv9s.css +0 -1
  104. package/dist-cli-web/assets/index-Jhwca9kg.css +0 -1
  105. package/dist-cli-web/assets/index-vtF05fh7.css +0 -1
  106. /package/dist/{chat-command-6IOH2QRF.mjs.map → chat-command-QH7DXCAJ.mjs.map} +0 -0
  107. /package/dist/{chunk-JX6MFS6F.mjs.map → chunk-2Q3HHVTS.mjs.map} +0 -0
  108. /package/dist/{chunk-UMBEHBIZ.mjs.map → chunk-6U2QZO3N.mjs.map} +0 -0
  109. /package/dist/{chunk-52P5ZRBD.mjs.map → chunk-AWGXYFIC.mjs.map} +0 -0
  110. /package/dist/{chunk-4RLZFGO5.mjs.map → chunk-C2PD3BS7.mjs.map} +0 -0
  111. /package/dist/{chunk-652KEAX3.mjs.map → chunk-SWUTRDGZ.mjs.map} +0 -0
  112. /package/dist/{http-HSK46XEU.mjs.map → http-D2F4QTQW.mjs.map} +0 -0
  113. /package/dist/{import-DF2WD2HE.mjs.map → import-H7X2JU2O.mjs.map} +0 -0
  114. /package/dist/{mcp-A2RND7KN.mjs.map → mcp-UURXRGDF.mjs.map} +0 -0
  115. /package/dist/{src-SHFHW2QB.mjs.map → src-MHCO4HYY.mjs.map} +0 -0
  116. /package/dist/{stream-import-GZ2QDE3Z.mjs.map → stream-import-BHXACUCA.mjs.map} +0 -0
  117. /package/dist/{worker-runtime-OWN56GG5.mjs.map → worker-runtime-JOUCP2L4.mjs.map} +0 -0
@@ -40,46 +40,49 @@ import {
40
40
  composeChunkId,
41
41
  computeChunkerConfigHash,
42
42
  computeDbPathHash,
43
- createChatDbFtsSearcher,
44
43
  createChatDbMessageRangeReader,
45
44
  createChatDbMessageSource,
46
45
  createChatLabTempDir,
47
46
  createEmbedder,
48
47
  createLocalEmbeddingRuntimeManager,
48
+ createMessageDedupState,
49
49
  createSemanticIndexService,
50
50
  defaultSemanticIndexConfig,
51
51
  deriveParentId,
52
52
  estimateTokens,
53
- extractFtsKeywords,
54
53
  formatEvidenceMessages,
54
+ generateFallbackMessageKey,
55
55
  getChatLabTempScopeDir,
56
56
  getLocalProfileByModelId,
57
57
  getLocalProfilesForLocale,
58
- hybridSearch,
59
58
  isKeylessSemanticIndexApiBaseUrl,
60
59
  isSemanticIndexConfigured,
61
60
  isSemanticVoid,
62
61
  parseParentBounds,
63
62
  persistSemanticIndexConfig,
64
63
  rebaseChatLabDemoDocuments,
65
- reciprocalRankFusion,
64
+ registerMessageAndCheckDuplicate,
66
65
  removeChatLabTempDir,
67
66
  resolveModelId,
68
67
  resolveSemanticIndexApiKeySet,
69
68
  runWarmup,
69
+ semanticSearch,
70
70
  vecTableName,
71
71
  vecTableSchema
72
- } from "./chunk-4LW6443F.mjs";
72
+ } from "./chunk-AWXHYTD5.mjs";
73
73
  import {
74
74
  CHATLAB_FORMAT_VERSION,
75
75
  MEANINGFUL_POS_TAGS,
76
+ POS_TAG_DEFINITIONS,
76
77
  StaticPathProvider,
77
78
  appLogger,
79
+ buildExcludeKeywordsConditions,
80
+ cleanText,
78
81
  detectConflictsInMessages,
79
- generateMessageKey,
80
82
  getCollidingPlatformIds,
81
- getJieba,
83
+ getCollidingPlatformIdsFromMessages,
82
84
  isStopword,
85
+ isValidWord,
83
86
  loadConfig,
84
87
  mergeMembers2 as mergeMembers,
85
88
  migrateFromElectronIfNeeded,
@@ -90,7 +93,7 @@ import {
90
93
  withFileLock,
91
94
  writeConfigField,
92
95
  writeJsonFileAtomically
93
- } from "./chunk-INBJ7SND.mjs";
96
+ } from "./chunk-2WYXD5ZP.mjs";
94
97
  import {
95
98
  __export
96
99
  } from "./chunk-VSOWNKPA.mjs";
@@ -556,6 +559,220 @@ function expandHome(filePath) {
556
559
  return filePath;
557
560
  }
558
561
 
562
+ // ../../packages/node-runtime/src/nlp/segmenter.ts
563
+ import * as fs3 from "fs";
564
+ import * as path3 from "path";
565
+ import { createRequire } from "module";
566
+ var _nlpDir = null;
567
+ var jiebaInstances = /* @__PURE__ */ new Map();
568
+ var require2 = createRequire(import.meta.url);
569
+ function initNlpDir(nlpDir) {
570
+ _nlpDir = nlpDir;
571
+ }
572
+ function getNlpDir() {
573
+ return _nlpDir;
574
+ }
575
+ function existsDictOnDisk(dictId) {
576
+ if (!_nlpDir) return false;
577
+ return fs3.existsSync(path3.join(_nlpDir, `${dictId}.dict`));
578
+ }
579
+ function tryLoadDictFromDisk(dictId) {
580
+ if (!_nlpDir) return null;
581
+ const dictPath = path3.join(_nlpDir, `${dictId}.dict`);
582
+ if (!fs3.existsSync(dictPath)) return null;
583
+ try {
584
+ return fs3.readFileSync(dictPath);
585
+ } catch {
586
+ return null;
587
+ }
588
+ }
589
+ function getJieba(dictType = "default") {
590
+ const effectiveType = dictType === "default" ? "zh-CN" : dictType;
591
+ const cached = jiebaInstances.get(effectiveType);
592
+ if (cached) {
593
+ if (cached.fromDiskDict === existsDictOnDisk(effectiveType)) return cached.instance;
594
+ jiebaInstances.delete(effectiveType);
595
+ console.error(`[NLP] jieba cache invalidated (dict ${cached.fromDiskDict ? "removed" : "added"}): ${effectiveType}`);
596
+ }
597
+ try {
598
+ const { Jieba } = require2("@node-rs/jieba");
599
+ let instance;
600
+ const diskDict = tryLoadDictFromDisk(effectiveType);
601
+ if (diskDict) {
602
+ instance = Jieba.withDict(diskDict);
603
+ console.error(`[NLP] jieba dict loaded: ${effectiveType} (${diskDict.length} bytes)`);
604
+ } else {
605
+ instance = new Jieba();
606
+ console.warn(`[NLP] jieba dict missing: ${effectiveType}, fallback to built-in tokenizer`);
607
+ }
608
+ jiebaInstances.set(effectiveType, { instance, fromDiskDict: !!diskDict });
609
+ return instance;
610
+ } catch (error) {
611
+ console.error(`[NLP] Failed to load jieba module (dict=${effectiveType}):`, error);
612
+ throw new Error(`jieba \u6A21\u5757\u52A0\u8F7D\u5931\u8D25 (${effectiveType})`);
613
+ }
614
+ }
615
+ function clearJiebaInstance(dictType) {
616
+ jiebaInstances.delete(dictType);
617
+ console.error(`[NLP] jieba instance cleared: ${dictType}`);
618
+ }
619
+ function segmentChinese(text, options = {}) {
620
+ const { posFilterMode = "meaningful", customPosTags, dictType = "default" } = options;
621
+ const cleaned = cleanText(text);
622
+ if (!cleaned) return [];
623
+ try {
624
+ const jieba = getJieba(dictType);
625
+ if (posFilterMode === "all") return jieba.cut(cleaned, false);
626
+ const tagged = jieba.tag(cleaned);
627
+ const allowedTags = posFilterMode === "custom" && customPosTags ? new Set(customPosTags) : MEANINGFUL_POS_TAGS;
628
+ return tagged.filter((item) => allowedTags.has(item.tag)).map((item) => item.word);
629
+ } catch (error) {
630
+ console.error("[NLP] Chinese segmentation failed:", error);
631
+ try {
632
+ return getJieba("default").cut(cleanText(text), false);
633
+ } catch {
634
+ return cleaned.split("");
635
+ }
636
+ }
637
+ }
638
+ function segmentEnglish(text) {
639
+ const cleaned = cleanText(text);
640
+ if (!cleaned) return [];
641
+ try {
642
+ const segmenter = new Intl.Segmenter("en", { granularity: "word" });
643
+ return [...segmenter.segment(cleaned)].filter((s) => s.isWordLike).map((s) => s.segment.toLowerCase());
644
+ } catch {
645
+ return cleaned.toLowerCase().split(/\s+/).filter((w) => w.length > 0);
646
+ }
647
+ }
648
+ function segmentJapanese(text) {
649
+ const cleaned = cleanText(text);
650
+ if (!cleaned) return [];
651
+ try {
652
+ const segmenter = new Intl.Segmenter("ja", { granularity: "word" });
653
+ return [...segmenter.segment(cleaned)].filter((s) => s.isWordLike).map((s) => s.segment);
654
+ } catch {
655
+ return cleaned.split("").filter((ch) => ch.trim().length > 0);
656
+ }
657
+ }
658
+ function segment(text, locale, options = {}) {
659
+ const {
660
+ minLength,
661
+ posFilterMode = "meaningful",
662
+ customPosTags,
663
+ enableStopwords = true,
664
+ dictType = "default"
665
+ } = options;
666
+ const isChinese = locale.startsWith("zh");
667
+ const isJapanese = locale === "ja-JP";
668
+ const defaultMinLength = isChinese || isJapanese ? 2 : 3;
669
+ const effectiveMinLength = minLength ?? defaultMinLength;
670
+ let words;
671
+ if (isChinese) {
672
+ words = segmentChinese(text, { posFilterMode, customPosTags, dictType });
673
+ } else if (isJapanese) {
674
+ words = segmentJapanese(text);
675
+ } else {
676
+ words = segmentEnglish(text);
677
+ }
678
+ return words.filter((word) => isValidWord(word, locale, effectiveMinLength, enableStopwords, isStopword));
679
+ }
680
+ function batchSegmentWithFrequency(texts, locale, options = {}) {
681
+ const {
682
+ minLength,
683
+ minCount = 2,
684
+ topN = 100,
685
+ posFilterMode,
686
+ customPosTags,
687
+ enableStopwords,
688
+ dictType,
689
+ excludeWords
690
+ } = options;
691
+ const wordFrequency = /* @__PURE__ */ new Map();
692
+ const excludeSet = excludeWords?.length ? new Set(excludeWords.map((w) => w.toLowerCase())) : null;
693
+ for (const text of texts) {
694
+ const words = segment(text, locale, { minLength, posFilterMode, customPosTags, enableStopwords, dictType });
695
+ for (const word of words) {
696
+ if (excludeSet && excludeSet.has(word.toLowerCase())) continue;
697
+ wordFrequency.set(word, (wordFrequency.get(word) || 0) + 1);
698
+ }
699
+ }
700
+ const filtered = /* @__PURE__ */ new Map();
701
+ let totalWords = 0;
702
+ for (const [word, count] of wordFrequency) {
703
+ if (count >= minCount) {
704
+ filtered.set(word, count);
705
+ totalWords += count;
706
+ }
707
+ }
708
+ const sorted = [...filtered.entries()].sort((a, b) => b[1] - a[1]).slice(0, topN);
709
+ return { words: new Map(sorted), uniqueWords: filtered.size, totalWords };
710
+ }
711
+ function batchSegmentChineseWithStats(texts, locale, options = {}) {
712
+ const {
713
+ minLength,
714
+ minCount = 2,
715
+ topN = 100,
716
+ posFilterMode = "meaningful",
717
+ customPosTags,
718
+ enableStopwords = true,
719
+ dictType = "default",
720
+ excludeWords
721
+ } = options;
722
+ const effectiveMinLength = minLength ?? 2;
723
+ const allowedTags = posFilterMode === "custom" && customPosTags ? new Set(customPosTags) : MEANINGFUL_POS_TAGS;
724
+ const excludeSet = excludeWords?.length ? new Set(excludeWords.map((w) => w.toLowerCase())) : null;
725
+ const wordFrequency = /* @__PURE__ */ new Map();
726
+ const posTagStats = /* @__PURE__ */ new Map();
727
+ try {
728
+ const jieba = getJieba(dictType);
729
+ for (const text of texts) {
730
+ const cleaned = cleanText(text);
731
+ if (!cleaned) continue;
732
+ for (const { tag, word } of jieba.tag(cleaned)) {
733
+ if (!isValidWord(word, locale, effectiveMinLength, enableStopwords, isStopword)) continue;
734
+ posTagStats.set(tag, (posTagStats.get(tag) || 0) + 1);
735
+ if (!allowedTags.has(tag)) continue;
736
+ if (excludeSet && excludeSet.has(word.toLowerCase())) continue;
737
+ wordFrequency.set(word, (wordFrequency.get(word) || 0) + 1);
738
+ }
739
+ }
740
+ } catch (error) {
741
+ console.error("[NLP] Chinese single-pass segmentation failed:", error);
742
+ }
743
+ const filtered = /* @__PURE__ */ new Map();
744
+ let totalWords = 0;
745
+ for (const [word, count] of wordFrequency) {
746
+ if (count >= minCount) {
747
+ filtered.set(word, count);
748
+ totalWords += count;
749
+ }
750
+ }
751
+ const sorted = [...filtered.entries()].sort((a, b) => b[1] - a[1]).slice(0, topN);
752
+ return { words: new Map(sorted), uniqueWords: filtered.size, totalWords, posTagStats };
753
+ }
754
+ function collectPosTagStats(texts, minWordLength = 2, enableStopwords = true, dictType = "default") {
755
+ const posStats = /* @__PURE__ */ new Map();
756
+ try {
757
+ const jieba = getJieba(dictType);
758
+ for (const text of texts) {
759
+ const cleaned = cleanText(text);
760
+ if (!cleaned) continue;
761
+ const tagged = jieba.tag(cleaned);
762
+ for (const item of tagged) {
763
+ if (!isValidWord(item.word, "zh-CN", minWordLength, enableStopwords, isStopword)) continue;
764
+ posStats.set(item.tag, (posStats.get(item.tag) || 0) + 1);
765
+ }
766
+ }
767
+ } catch (error) {
768
+ console.error("[NLP] Failed to collect POS stats:", error);
769
+ }
770
+ return posStats;
771
+ }
772
+ function getPosTagDefinitions() {
773
+ return POS_TAG_DEFINITIONS;
774
+ }
775
+
559
776
  // ../../packages/node-runtime/src/jieba-nlp-provider.ts
560
777
  function createJiebaNlpProvider(dictType = "default") {
561
778
  return {
@@ -567,9 +784,233 @@ function createJiebaNlpProvider(dictType = "default") {
567
784
  };
568
785
  }
569
786
 
787
+ // ../../packages/node-runtime/src/nlp/word-frequency.ts
788
+ function buildMessageQuery(timeFilter, memberId, excludeKeywords) {
789
+ const conditions = [];
790
+ const params = [];
791
+ if (timeFilter?.startTs !== void 0) {
792
+ conditions.push("msg.ts >= ?");
793
+ params.push(timeFilter.startTs);
794
+ }
795
+ if (timeFilter?.endTs !== void 0) {
796
+ conditions.push("msg.ts <= ?");
797
+ params.push(timeFilter.endTs);
798
+ }
799
+ if (memberId !== void 0 && memberId !== null) {
800
+ conditions.push("msg.sender_id = ?");
801
+ params.push(memberId);
802
+ }
803
+ if (excludeKeywords && excludeKeywords.length > 0) {
804
+ const exclude = buildExcludeKeywordsConditions(excludeKeywords);
805
+ conditions.push(...exclude.conditions);
806
+ params.push(...exclude.params);
807
+ }
808
+ conditions.push("COALESCE(m.account_name, '') != '\u7CFB\u7EDF\u6D88\u606F'");
809
+ conditions.push("msg.type = 0");
810
+ conditions.push("msg.content IS NOT NULL");
811
+ conditions.push("TRIM(msg.content) != ''");
812
+ return {
813
+ clause: ` WHERE ${conditions.join(" AND ")}`,
814
+ params
815
+ };
816
+ }
817
+ function computeWordFrequency(db, params) {
818
+ const {
819
+ locale,
820
+ timeFilter,
821
+ memberId,
822
+ topN = 100,
823
+ minWordLength,
824
+ minCount = 2,
825
+ posFilterMode = "meaningful",
826
+ customPosTags,
827
+ enableStopwords = true,
828
+ dictType = "default",
829
+ excludeWords,
830
+ excludeKeywords
831
+ } = params;
832
+ const { clause, params: filterParams } = buildMessageQuery(timeFilter, memberId, excludeKeywords);
833
+ const messages = db.prepare(`SELECT msg.content FROM message msg JOIN member m ON msg.sender_id = m.id${clause}`).all(...filterParams);
834
+ if (messages.length === 0) {
835
+ return { words: [], totalWords: 0, totalMessages: 0, uniqueWords: 0 };
836
+ }
837
+ const texts = messages.map((m) => m.content);
838
+ const segmentOptions = {
839
+ minLength: minWordLength,
840
+ minCount,
841
+ topN,
842
+ posFilterMode,
843
+ customPosTags,
844
+ enableStopwords,
845
+ dictType,
846
+ excludeWords
847
+ };
848
+ let posTagStats;
849
+ let result;
850
+ if (locale.startsWith("zh") && posFilterMode !== "all") {
851
+ const combined = batchSegmentChineseWithStats(texts, locale, segmentOptions);
852
+ result = { words: combined.words, uniqueWords: combined.uniqueWords, totalWords: combined.totalWords };
853
+ posTagStats = [...combined.posTagStats.entries()].map(([tag, count]) => ({ tag, count }));
854
+ } else {
855
+ if (locale.startsWith("zh")) {
856
+ const posStatsMap = collectPosTagStats(texts, minWordLength ?? 2, enableStopwords, dictType);
857
+ posTagStats = [...posStatsMap.entries()].map(([tag, count]) => ({ tag, count }));
858
+ }
859
+ result = batchSegmentWithFrequency(texts, locale, segmentOptions);
860
+ }
861
+ let topNTotalWords = 0;
862
+ for (const count of result.words.values()) topNTotalWords += count;
863
+ const words = [...result.words.entries()].map(([word, count]) => ({
864
+ word,
865
+ count,
866
+ percentage: topNTotalWords > 0 ? Math.round(count / topNTotalWords * 1e4) / 100 : 0
867
+ }));
868
+ return {
869
+ words,
870
+ totalWords: result.totalWords,
871
+ totalMessages: messages.length,
872
+ uniqueWords: result.uniqueWords,
873
+ posTagStats
874
+ };
875
+ }
876
+ function segmentText(text, locale, minLength) {
877
+ return segment(text, locale, { minLength });
878
+ }
879
+
880
+ // ../../packages/node-runtime/src/nlp/dict-manager.ts
881
+ import * as fs4 from "fs";
882
+ import * as path4 from "path";
883
+ import { createHash } from "crypto";
884
+ var DICT_DOWNLOAD_URL_BASE = "https://chatlab.fun/assets/nlp";
885
+ var DICT_SHA256 = {
886
+ "zh-CN": "139519822fe8ab9e10d9d07e68ea0451045380aedaf54ecc51e2a28c6b42a13f",
887
+ "zh-TW": "a63ec7e388f16f1b486dcd948a9f1a3b492be5d9b6bdab786a95e59966786dfd"
888
+ };
889
+ var AVAILABLE_DICTS = [
890
+ { id: "zh-CN", label: "\u7B80\u4F53\u4E2D\u6587", locale: "zh-CN" },
891
+ { id: "zh-TW", label: "\u7E41\u9AD4\u4E2D\u6587", locale: "zh-TW" }
892
+ ];
893
+ function getDictFilePath(nlpDir, dictId) {
894
+ return path4.join(nlpDir, `${dictId}.dict`);
895
+ }
896
+ function isDictDownloaded(nlpDir, dictId) {
897
+ return fs4.existsSync(getDictFilePath(nlpDir, dictId));
898
+ }
899
+ function getDictList(nlpDir) {
900
+ return AVAILABLE_DICTS.map((d) => {
901
+ const filePath = getDictFilePath(nlpDir, d.id);
902
+ const downloaded = fs4.existsSync(filePath);
903
+ let fileSize;
904
+ if (downloaded) {
905
+ try {
906
+ fileSize = fs4.statSync(filePath).size;
907
+ } catch {
908
+ }
909
+ }
910
+ return { ...d, downloaded, fileSize };
911
+ });
912
+ }
913
+ function loadDictBuffer(nlpDir, dictId) {
914
+ const filePath = getDictFilePath(nlpDir, dictId);
915
+ if (!fs4.existsSync(filePath)) return null;
916
+ try {
917
+ return fs4.readFileSync(filePath);
918
+ } catch (error) {
919
+ console.error(`[NLP DictManager] Failed to read dict file: ${filePath}`, error);
920
+ return null;
921
+ }
922
+ }
923
+ async function downloadDict(nlpDir, dictId, onProgress) {
924
+ if (!AVAILABLE_DICTS.find((d) => d.id === dictId)) {
925
+ return { success: false, error: `Unknown dict: ${dictId}` };
926
+ }
927
+ fs4.mkdirSync(nlpDir, { recursive: true });
928
+ const url = `${DICT_DOWNLOAD_URL_BASE}/${dictId}.dict`;
929
+ const filePath = getDictFilePath(nlpDir, dictId);
930
+ const tmpPath = filePath + ".tmp";
931
+ try {
932
+ const response = await fetch(url, { signal: AbortSignal.timeout(12e4) });
933
+ if (!response.ok) throw new Error(`Download failed: HTTP ${response.status}`);
934
+ const total = Number(response.headers.get("content-length") || 0);
935
+ let buffer;
936
+ if (!response.body) {
937
+ buffer = Buffer.from(await response.arrayBuffer());
938
+ } else {
939
+ const reader = response.body.getReader();
940
+ const chunks = [];
941
+ let loaded = 0;
942
+ while (true) {
943
+ const { done, value } = await reader.read();
944
+ if (done) break;
945
+ const chunk = Buffer.from(value);
946
+ chunks.push(chunk);
947
+ loaded += chunk.length;
948
+ if (total > 0 && onProgress) onProgress(Math.round(loaded / total * 100));
949
+ }
950
+ buffer = Buffer.concat(chunks);
951
+ }
952
+ const MIN_DICT_SIZE = 1e6;
953
+ if (buffer.length < MIN_DICT_SIZE) {
954
+ return { success: false, error: `Downloaded file is invalid (${buffer.length} bytes)` };
955
+ }
956
+ const head = buffer.subarray(0, 50).toString("utf-8").trim();
957
+ if (head.startsWith("<!") || head.startsWith("<html")) {
958
+ return { success: false, error: "Downloaded file is HTML, not a dictionary file" };
959
+ }
960
+ const expectedSha256 = DICT_SHA256[dictId];
961
+ if (!expectedSha256) {
962
+ return { success: false, error: `Missing SHA256 checksum for dict: ${dictId}` };
963
+ }
964
+ const actualSha256 = createHash("sha256").update(buffer).digest("hex");
965
+ if (actualSha256 !== expectedSha256) {
966
+ return { success: false, error: "Dictionary integrity check failed (SHA256 mismatch)" };
967
+ }
968
+ fs4.writeFileSync(tmpPath, buffer);
969
+ if (fs4.existsSync(filePath)) fs4.unlinkSync(filePath);
970
+ fs4.renameSync(tmpPath, filePath);
971
+ clearJiebaInstance(dictId);
972
+ console.log(`[NLP DictManager] Dict downloaded: ${dictId} (${fs4.statSync(filePath).size} bytes)`);
973
+ return { success: true };
974
+ } catch (error) {
975
+ if (fs4.existsSync(tmpPath)) {
976
+ try {
977
+ fs4.unlinkSync(tmpPath);
978
+ } catch {
979
+ }
980
+ }
981
+ const msg = error instanceof Error ? error.message : String(error);
982
+ console.error(`[NLP DictManager] Download failed for ${dictId}:`, msg);
983
+ return { success: false, error: msg };
984
+ }
985
+ }
986
+ function deleteDict(nlpDir, dictId) {
987
+ const filePath = getDictFilePath(nlpDir, dictId);
988
+ if (!fs4.existsSync(filePath)) return { success: true };
989
+ try {
990
+ fs4.unlinkSync(filePath);
991
+ clearJiebaInstance(dictId);
992
+ console.log(`[NLP DictManager] Dict deleted: ${dictId}`);
993
+ return { success: true };
994
+ } catch (error) {
995
+ const msg = error instanceof Error ? error.message : String(error);
996
+ console.error(`[NLP DictManager] Delete failed for ${dictId}:`, msg);
997
+ return { success: false, error: msg };
998
+ }
999
+ }
1000
+ async function ensureDefaultDict(nlpDir) {
1001
+ if (isDictDownloaded(nlpDir, "zh-CN")) return;
1002
+ console.log("[NLP DictManager] zh-CN dict not found, starting background download...");
1003
+ const result = await downloadDict(nlpDir, "zh-CN");
1004
+ if (result.success) {
1005
+ console.log("[NLP DictManager] zh-CN dict auto-downloaded successfully");
1006
+ } else {
1007
+ console.warn("[NLP DictManager] zh-CN dict auto-download failed:", result.error);
1008
+ }
1009
+ }
1010
+
570
1011
  // ../../packages/node-runtime/src/preferences.ts
571
- import * as fs3 from "fs";
572
- import * as path3 from "path";
1012
+ import * as fs5 from "fs";
1013
+ import * as path5 from "path";
573
1014
  var DEFAULTS = {
574
1015
  pinnedSessionIds: [],
575
1016
  assistantUpgradeSkippedVersions: {},
@@ -616,13 +1057,13 @@ var PreferencesManager = class {
616
1057
  filePath;
617
1058
  cache = null;
618
1059
  constructor(systemDir) {
619
- this.filePath = path3.join(systemDir, "preferences.json");
1060
+ this.filePath = path5.join(systemDir, "preferences.json");
620
1061
  }
621
1062
  load() {
622
1063
  if (this.cache) return this.cache;
623
1064
  try {
624
- if (fs3.existsSync(this.filePath)) {
625
- const raw = fs3.readFileSync(this.filePath, "utf-8");
1065
+ if (fs5.existsSync(this.filePath)) {
1066
+ const raw = fs5.readFileSync(this.filePath, "utf-8");
626
1067
  const parsed = JSON.parse(raw);
627
1068
  const migrated = this.migrateLegacyDesensitizeRules(parsed, raw);
628
1069
  this.cache = this.mergeDefaults(migrated.preferences);
@@ -748,21 +1189,21 @@ var PreferencesManager = class {
748
1189
  };
749
1190
  }
750
1191
  backupLegacyPreferences(rawContent) {
751
- const backupDir = path3.join(path3.dirname(this.filePath), "backups");
752
- if (!fs3.existsSync(backupDir)) {
753
- fs3.mkdirSync(backupDir, { recursive: true });
1192
+ const backupDir = path5.join(path5.dirname(this.filePath), "backups");
1193
+ if (!fs5.existsSync(backupDir)) {
1194
+ fs5.mkdirSync(backupDir, { recursive: true });
754
1195
  }
755
1196
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
756
- const backupPath = path3.join(backupDir, `preferences-pre-desensitize-groups-${timestamp}.json`);
757
- fs3.writeFileSync(backupPath, rawContent, "utf-8");
1197
+ const backupPath = path5.join(backupDir, `preferences-pre-desensitize-groups-${timestamp}.json`);
1198
+ fs5.writeFileSync(backupPath, rawContent, "utf-8");
758
1199
  console.info(`[Preferences] Backed up legacy desensitize rules to ${backupPath}`);
759
1200
  }
760
1201
  writePreferences(preferences) {
761
- const dir = path3.dirname(this.filePath);
762
- if (!fs3.existsSync(dir)) {
763
- fs3.mkdirSync(dir, { recursive: true });
1202
+ const dir = path5.dirname(this.filePath);
1203
+ if (!fs5.existsSync(dir)) {
1204
+ fs5.mkdirSync(dir, { recursive: true });
764
1205
  }
765
- fs3.writeFileSync(this.filePath, JSON.stringify(preferences, null, 2), "utf-8");
1206
+ fs5.writeFileSync(this.filePath, JSON.stringify(preferences, null, 2), "utf-8");
766
1207
  }
767
1208
  /**
768
1209
  * Deep merge: arrays are replaced (not concatenated), objects are merged.
@@ -782,26 +1223,146 @@ var PreferencesManager = class {
782
1223
  };
783
1224
 
784
1225
  // ../../packages/node-runtime/src/merger/index.ts
1226
+ function createMessageScope(meta, members, sourceIndex) {
1227
+ const platform = meta?.platform || "unknown";
1228
+ const type = meta?.type || "unknown";
1229
+ if (meta?.groupId) return JSON.stringify([platform, type, "group", meta.groupId]);
1230
+ if (type === "group") return JSON.stringify([platform, type, "source", sourceIndex]);
1231
+ const memberIds = [...new Set(members.map((member) => member.platformId))].sort();
1232
+ if (memberIds.length > 0) {
1233
+ if (type === "private") {
1234
+ if (memberIds.length >= 2) return JSON.stringify([platform, type, "members", memberIds]);
1235
+ } else {
1236
+ return JSON.stringify([platform, type, "name-members", meta?.name || "", memberIds]);
1237
+ }
1238
+ }
1239
+ return JSON.stringify([platform, type, "source", sourceIndex]);
1240
+ }
1241
+ function createMessageBridgeFamily(meta) {
1242
+ if (!meta || meta.groupId || meta.type !== "group" && meta.type !== "private") return void 0;
1243
+ return JSON.stringify([meta.platform || "unknown", meta.type]);
1244
+ }
1245
+ function createMessageIdNormalizer(scopes) {
1246
+ const uniqueScopes = [...new Set(scopes)];
1247
+ const scopeIndexes = new Map(uniqueScopes.map((scope, index) => [scope, index]));
1248
+ const namespaceMessageIds = uniqueScopes.length > 1;
1249
+ return (id, scope) => {
1250
+ if (!id || !namespaceMessageIds) return id;
1251
+ return `__chatlab_message_scope__${scopeIndexes.get(scope) ?? 0}__${encodeURIComponent(id)}`;
1252
+ };
1253
+ }
1254
+ function createMessageScopeResolver(contexts) {
1255
+ const contextBySource = /* @__PURE__ */ new Map();
1256
+ const parents = /* @__PURE__ */ new Map();
1257
+ const firstSourceBySignature = /* @__PURE__ */ new Map();
1258
+ for (const context of contexts) {
1259
+ contextBySource.set(context.sourceIndex, context);
1260
+ parents.set(context.sourceIndex, context.sourceIndex);
1261
+ }
1262
+ const find = (sourceIndex) => {
1263
+ const parent = parents.get(sourceIndex) ?? sourceIndex;
1264
+ if (parent === sourceIndex) return sourceIndex;
1265
+ const root = find(parent);
1266
+ parents.set(sourceIndex, root);
1267
+ return root;
1268
+ };
1269
+ const union = (firstSourceIndex, secondSourceIndex) => {
1270
+ const firstRoot = find(firstSourceIndex);
1271
+ const secondRoot = find(secondSourceIndex);
1272
+ if (firstRoot === secondRoot) return;
1273
+ const root = Math.min(firstRoot, secondRoot);
1274
+ parents.set(firstRoot === root ? secondRoot : firstRoot, root);
1275
+ };
1276
+ return {
1277
+ addMessage(message, context) {
1278
+ if (!context.messageBridgeFamily || !message.platformMessageId) return;
1279
+ const fallbackKey = generateFallbackMessageKey({
1280
+ timestamp: message.timestamp,
1281
+ senderPlatformId: message.senderPlatformId,
1282
+ type: message.type,
1283
+ content: message.content ?? null,
1284
+ replyToMessageId: message.replyToMessageId
1285
+ });
1286
+ const signature = JSON.stringify([context.messageBridgeFamily, message.platformMessageId, fallbackKey]);
1287
+ const firstSourceIndex = firstSourceBySignature.get(signature);
1288
+ if (firstSourceIndex === void 0) {
1289
+ firstSourceBySignature.set(signature, context.sourceIndex);
1290
+ return;
1291
+ }
1292
+ union(firstSourceIndex, context.sourceIndex);
1293
+ },
1294
+ complete() {
1295
+ firstSourceBySignature.clear();
1296
+ },
1297
+ resolve(sourceIndex) {
1298
+ const rootContext = contextBySource.get(find(sourceIndex));
1299
+ return rootContext?.messageScope ?? JSON.stringify(["unknown", "source", sourceIndex]);
1300
+ }
1301
+ };
1302
+ }
785
1303
  function checkConflictsFromSources(dataSources) {
786
- const allMessages = [];
787
- for (const { source, filename } of dataSources) {
1304
+ const preparedSources = dataSources.map(({ source, filename }, sourceIndex) => {
788
1305
  const meta = source.getMeta();
789
- const platform = meta?.platform || "unknown";
1306
+ const members = source.getMembers();
1307
+ return {
1308
+ source,
1309
+ filename,
1310
+ sourceIndex,
1311
+ platform: meta?.platform || "unknown",
1312
+ messageScope: createMessageScope(meta, members, sourceIndex),
1313
+ messageBridgeFamily: createMessageBridgeFamily(meta)
1314
+ };
1315
+ });
1316
+ const allMessages = [];
1317
+ for (const { source, filename, sourceIndex, platform, messageScope, messageBridgeFamily } of preparedSources) {
790
1318
  source.streamMessages(1e4, (messages) => {
791
1319
  for (const msg of messages) {
792
- allMessages.push({ msg, source: filename, platform });
1320
+ allMessages.push({ msg, source: filename, sourceIndex, platform, messageScope, messageBridgeFamily });
793
1321
  }
794
1322
  });
795
1323
  }
796
- return detectConflictsInMessages(allMessages);
1324
+ const result = detectConflictsInMessages(allMessages);
1325
+ const collidingIds = getCollidingPlatformIdsFromMessages(allMessages);
1326
+ const scopeResolver = createMessageScopeResolver(preparedSources);
1327
+ for (const message of allMessages) scopeResolver.addMessage(message.msg, message);
1328
+ scopeResolver.complete();
1329
+ const normalizeMessageId = createMessageIdNormalizer(
1330
+ preparedSources.map(({ sourceIndex }) => scopeResolver.resolve(sourceIndex))
1331
+ );
1332
+ const dedupState = createMessageDedupState();
1333
+ let totalMessages = 0;
1334
+ for (const { msg, sourceIndex, platform } of allMessages) {
1335
+ const messageScope = scopeResolver.resolve(sourceIndex);
1336
+ const senderPlatformId = normalizePlatformId(msg.senderPlatformId, platform, collidingIds);
1337
+ const duplicate = registerMessageAndCheckDuplicate(
1338
+ {
1339
+ platformMessageId: normalizeMessageId(msg.platformMessageId, messageScope),
1340
+ timestamp: msg.timestamp,
1341
+ senderPlatformId,
1342
+ type: msg.type,
1343
+ content: msg.content ?? null,
1344
+ replyToMessageId: normalizeMessageId(msg.replyToMessageId, messageScope)
1345
+ },
1346
+ dedupState
1347
+ );
1348
+ if (!duplicate) totalMessages++;
1349
+ }
1350
+ return { ...result, totalMessages };
797
1351
  }
798
1352
  function buildMergedOutput(dataSources, outputName) {
799
- const metas = dataSources.map(({ source, filename }) => ({
800
- meta: source.getMeta(),
801
- members: source.getMembers(),
802
- filename,
803
- source
804
- }));
1353
+ const metas = dataSources.map(({ source, filename }, sourceIndex) => {
1354
+ const meta = source.getMeta();
1355
+ const members = source.getMembers();
1356
+ return {
1357
+ meta,
1358
+ members,
1359
+ filename,
1360
+ source,
1361
+ sourceIndex,
1362
+ messageScope: createMessageScope(meta, members, sourceIndex),
1363
+ messageBridgeFamily: createMessageBridgeFamily(meta)
1364
+ };
1365
+ });
805
1366
  const collidingIds = getCollidingPlatformIds(
806
1367
  metas.map(({ meta, members }) => ({
807
1368
  platform: meta?.platform || "unknown",
@@ -815,24 +1376,63 @@ function buildMergedOutput(dataSources, outputName) {
815
1376
  })),
816
1377
  collidingIds
817
1378
  );
818
- const seenKeys = /* @__PURE__ */ new Set();
1379
+ const uniquePlatforms = [...new Set(metas.map(({ meta }) => meta?.platform || "unknown"))];
1380
+ const scopeResolver = createMessageScopeResolver(metas);
1381
+ for (const context of metas) {
1382
+ if (!context.messageBridgeFamily) continue;
1383
+ context.source.streamMessages(1e4, (messages) => {
1384
+ for (const message of messages) scopeResolver.addMessage(message, context);
1385
+ });
1386
+ }
1387
+ scopeResolver.complete();
1388
+ const normalizeMessageId = createMessageIdNormalizer(
1389
+ metas.map(({ sourceIndex }) => scopeResolver.resolve(sourceIndex))
1390
+ );
1391
+ const dedupState = createMessageDedupState();
1392
+ const fallbackOnlyMessageIndexes = /* @__PURE__ */ new Map();
819
1393
  const mergedMessages = [];
820
- for (const { source, meta } of metas) {
1394
+ for (const { source, meta, sourceIndex } of metas) {
821
1395
  const platform2 = meta?.platform || "unknown";
1396
+ const messageScope = scopeResolver.resolve(sourceIndex);
822
1397
  source.streamMessages(1e4, (messages) => {
823
1398
  for (const msg of messages) {
824
1399
  const nid = normalizePlatformId(msg.senderPlatformId, platform2, collidingIds);
825
- const key = generateMessageKey(msg.timestamp, nid, msg.content ?? null);
826
- if (seenKeys.has(key)) continue;
827
- seenKeys.add(key);
1400
+ const platformMessageId = normalizeMessageId(msg.platformMessageId, messageScope);
1401
+ const replyToMessageId = normalizeMessageId(msg.replyToMessageId, messageScope);
1402
+ const dedupMessage = {
1403
+ platformMessageId,
1404
+ timestamp: msg.timestamp,
1405
+ senderPlatformId: nid,
1406
+ type: msg.type,
1407
+ content: msg.content ?? null,
1408
+ replyToMessageId
1409
+ };
1410
+ const fallbackKey = generateFallbackMessageKey(dedupMessage);
1411
+ const shouldBackfillId = Boolean(
1412
+ platformMessageId && !dedupState.platformMessageIds.has(platformMessageId) && dedupState.fallbackOnlyKeys.has(fallbackKey)
1413
+ );
1414
+ if (registerMessageAndCheckDuplicate(dedupMessage, dedupState)) {
1415
+ if (shouldBackfillId) {
1416
+ const retainedIndex2 = fallbackOnlyMessageIndexes.get(fallbackKey);
1417
+ if (retainedIndex2 !== void 0) {
1418
+ mergedMessages[retainedIndex2].platformMessageId = platformMessageId;
1419
+ fallbackOnlyMessageIndexes.delete(fallbackKey);
1420
+ }
1421
+ }
1422
+ continue;
1423
+ }
1424
+ const retainedIndex = mergedMessages.length;
828
1425
  mergedMessages.push({
1426
+ platformMessageId,
829
1427
  sender: nid,
830
1428
  accountName: msg.senderAccountName,
831
1429
  groupNickname: msg.senderGroupNickname,
832
1430
  timestamp: msg.timestamp,
833
1431
  type: msg.type,
834
- content: msg.content
1432
+ content: msg.content,
1433
+ replyToMessageId
835
1434
  });
1435
+ if (!platformMessageId) fallbackOnlyMessageIndexes.set(fallbackKey, retainedIndex);
836
1436
  }
837
1437
  });
838
1438
  }
@@ -842,11 +1442,13 @@ function buildMergedOutput(dataSources, outputName) {
842
1442
  platform: source.getMeta()?.platform || "unknown",
843
1443
  messageCount: source.getMessageCount()
844
1444
  }));
845
- const uniquePlatforms = [...new Set(metas.map(({ meta }) => meta?.platform || "unknown"))];
846
1445
  const platform = uniquePlatforms.length === 1 ? uniquePlatforms[0] : "mixed";
847
1446
  const groupIds = new Set(metas.map(({ meta }) => meta?.groupId).filter(Boolean));
848
1447
  const groupId = groupIds.size === 1 ? metas.find(({ meta }) => meta?.groupId)?.meta?.groupId : void 0;
849
1448
  const groupAvatar = groupId ? metas.filter(({ meta }) => meta?.groupId === groupId).pop()?.meta?.groupAvatar : void 0;
1449
+ const ownerIds = new Set(metas.map(({ meta }) => meta?.ownerId).filter((id) => Boolean(id)));
1450
+ const ownerCandidate = uniquePlatforms.length === 1 && ownerIds.size === 1 ? [...ownerIds][0] : void 0;
1451
+ const ownerId = ownerCandidate && memberMap.has(ownerCandidate) ? ownerCandidate : void 0;
850
1452
  const chatLabData = {
851
1453
  chatlab: {
852
1454
  version: CHATLAB_FORMAT_VERSION,
@@ -860,7 +1462,8 @@ function buildMergedOutput(dataSources, outputName) {
860
1462
  type: metas[0]?.meta?.type || "group",
861
1463
  sources,
862
1464
  groupId,
863
- groupAvatar
1465
+ groupAvatar,
1466
+ ownerId
864
1467
  },
865
1468
  members: Array.from(memberMap.values()),
866
1469
  messages: mergedMessages
@@ -882,7 +1485,7 @@ function* serializeChatLabToJsonl(data) {
882
1485
  }
883
1486
 
884
1487
  // ../../packages/node-runtime/src/services/analytics.ts
885
- import * as path4 from "path";
1488
+ import * as path6 from "path";
886
1489
  import { randomUUID as randomUUID2 } from "crypto";
887
1490
  var DEFAULT_DATA = {
888
1491
  lastReportDate: null,
@@ -994,7 +1597,7 @@ var AnalyticsService = class {
994
1597
  // Daily-active deduplication is intentionally process-local unless concurrent runtimes become supported.
995
1598
  dailyActivePromise = null;
996
1599
  constructor(systemDir, options) {
997
- this.dataPath = path4.join(systemDir, "analytics.json");
1600
+ this.dataPath = path6.join(systemDir, "analytics.json");
998
1601
  this.options = {
999
1602
  ...options,
1000
1603
  umami: options.umami ?? getUmamiConfigFromEnv()
@@ -1156,8 +1759,8 @@ var AnalyticsService = class {
1156
1759
  };
1157
1760
 
1158
1761
  // ../../packages/node-runtime/src/services/demo-import.ts
1159
- import * as fs4 from "fs";
1160
- import * as path5 from "path";
1762
+ import * as fs6 from "fs";
1763
+ import * as path7 from "path";
1161
1764
  var DEMO_BASE_URL = "https://chatlab.fun/assets/demo";
1162
1765
  var DEMO_FILES = ["demo-group.json", "demo-private-B-wukong.json"];
1163
1766
  async function importDemoSessions(options) {
@@ -1189,8 +1792,8 @@ async function importDemoSessions(options) {
1189
1792
  }
1190
1793
  const rebased = rebaseChatLabDemoDocuments(downloaded, options.now?.() ?? /* @__PURE__ */ new Date(), options.targetTimeZone);
1191
1794
  const localPaths = rebased.documents.map((document, index) => {
1192
- const localPath = path5.join(createdTempDir, DEMO_FILES[index]);
1193
- fs4.writeFileSync(localPath, document, "utf8");
1795
+ const localPath = path7.join(createdTempDir, DEMO_FILES[index]);
1796
+ fs6.writeFileSync(localPath, document, "utf8");
1194
1797
  return localPath;
1195
1798
  });
1196
1799
  appLogger.info("demo-import", "Demo timeline prepared", {
@@ -1274,7 +1877,6 @@ __export(semantic_index_exports, {
1274
1877
  composeChunkId: () => composeChunkId,
1275
1878
  computeChunkerConfigHash: () => computeChunkerConfigHash,
1276
1879
  computeDbPathHash: () => computeDbPathHash,
1277
- createChatDbFtsSearcher: () => createChatDbFtsSearcher,
1278
1880
  createChatDbMessageRangeReader: () => createChatDbMessageRangeReader,
1279
1881
  createChatDbMessageSource: () => createChatDbMessageSource,
1280
1882
  createEmbedder: () => createEmbedder,
@@ -1287,24 +1889,22 @@ __export(semantic_index_exports, {
1287
1889
  deriveParentId: () => deriveParentId,
1288
1890
  embedding: () => embedding_exports,
1289
1891
  estimateTokens: () => estimateTokens,
1290
- extractFtsKeywords: () => extractFtsKeywords,
1291
1892
  formatEvidenceMessages: () => formatEvidenceMessages,
1292
- hybridSearch: () => hybridSearch,
1293
1893
  isSemanticIndexConfigured: () => isSemanticIndexConfigured,
1294
1894
  isSemanticVoid: () => isSemanticVoid,
1295
1895
  parseParentBounds: () => parseParentBounds,
1296
1896
  persistSemanticIndexConfig: () => persistSemanticIndexConfig,
1297
- reciprocalRankFusion: () => reciprocalRankFusion,
1298
1897
  resolveModelId: () => resolveModelId,
1299
1898
  resolveSemanticIndexApiKeySet: () => resolveSemanticIndexApiKeySet,
1300
1899
  runWarmup: () => runWarmup,
1900
+ semanticSearch: () => semanticSearch,
1301
1901
  snapshotPathProvider: () => snapshotPathProvider,
1302
1902
  vecTableName: () => vecTableName,
1303
1903
  vecTableSchema: () => vecTableSchema
1304
1904
  });
1305
1905
 
1306
1906
  // ../../packages/node-runtime/src/semantic-index/worker-client.ts
1307
- import path6 from "path";
1907
+ import path8 from "path";
1308
1908
 
1309
1909
  // ../../packages/node-runtime/src/semantic-index/worker-thread-transport.ts
1310
1910
  import { Worker } from "worker_threads";
@@ -1686,7 +2286,7 @@ function createSemanticIndexWorkerClient(options) {
1686
2286
  }
1687
2287
  function createSemanticIndexWorkerRuntimeClient(options) {
1688
2288
  const configStore = new SemanticIndexConfigStore(
1689
- path6.join(options.pathProvider.getAiDataDir(), SEMANTIC_INDEX_CONFIG_FILE)
2289
+ path8.join(options.pathProvider.getAiDataDir(), SEMANTIC_INDEX_CONFIG_FILE)
1690
2290
  );
1691
2291
  return createSemanticIndexWorkerClient({
1692
2292
  configStore,
@@ -1741,7 +2341,23 @@ export {
1741
2341
  NodePathProvider,
1742
2342
  getDefaultNodeUserDataDir,
1743
2343
  getSystemLogsDir,
2344
+ initNlpDir,
2345
+ getNlpDir,
2346
+ getJieba,
2347
+ clearJiebaInstance,
2348
+ segment,
2349
+ batchSegmentWithFrequency,
2350
+ collectPosTagStats,
2351
+ getPosTagDefinitions,
1744
2352
  createJiebaNlpProvider,
2353
+ computeWordFrequency,
2354
+ segmentText,
2355
+ isDictDownloaded,
2356
+ getDictList,
2357
+ loadDictBuffer,
2358
+ downloadDict,
2359
+ deleteDict,
2360
+ ensureDefaultDict,
1745
2361
  PreferencesManager,
1746
2362
  checkConflictsFromSources,
1747
2363
  buildMergedOutput,
@@ -1751,4 +2367,4 @@ export {
1751
2367
  createSemanticIndexWorkerRuntimeClient,
1752
2368
  semantic_index_exports
1753
2369
  };
1754
- //# sourceMappingURL=chunk-2XCNTG4O.mjs.map
2370
+ //# sourceMappingURL=chunk-XIWVJAXH.mjs.map