chatlab-cli 0.36.2 → 0.37.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 (201) hide show
  1. package/dist/{chat-command-SEV6N3AR.mjs → chat-command-WBAJBKIE.mjs} +15 -16
  2. package/dist/{chat-command-SEV6N3AR.mjs.map → chat-command-WBAJBKIE.mjs.map} +1 -1
  3. package/dist/chunk-2TV7M4CC.mjs +101 -0
  4. package/dist/chunk-2TV7M4CC.mjs.map +1 -0
  5. package/dist/{chunk-TRGR3KKJ.mjs → chunk-4J4MA3RC.mjs} +2605 -672
  6. package/dist/chunk-4J4MA3RC.mjs.map +1 -0
  7. package/dist/{chunk-TJ3NOBVI.mjs → chunk-5YOHLJFB.mjs} +98 -284
  8. package/dist/chunk-5YOHLJFB.mjs.map +1 -0
  9. package/dist/{chunk-WRBFV2GX.mjs → chunk-CR2MU7LN.mjs} +49 -55
  10. package/dist/chunk-CR2MU7LN.mjs.map +1 -0
  11. package/dist/{chunk-WJQOXOMR.mjs → chunk-DKIYHA3O.mjs} +281 -30
  12. package/dist/chunk-DKIYHA3O.mjs.map +1 -0
  13. package/dist/{chunk-VREVDZ4Q.mjs → chunk-DQR2YS3I.mjs} +1930 -33
  14. package/dist/chunk-DQR2YS3I.mjs.map +1 -0
  15. package/dist/{chunk-VUYWUHWW.mjs → chunk-EJBBVLUN.mjs} +3 -3
  16. package/dist/chunk-EJBBVLUN.mjs.map +1 -0
  17. package/dist/{chunk-HWIXK7SU.mjs → chunk-FM3GXNEO.mjs} +1643 -3
  18. package/dist/chunk-FM3GXNEO.mjs.map +1 -0
  19. package/dist/{chunk-YUH3H3QW.mjs → chunk-MSMDLDPB.mjs} +96 -5
  20. package/dist/chunk-MSMDLDPB.mjs.map +1 -0
  21. package/dist/{chunk-6EZPD3XJ.mjs → chunk-N2WGG3MS.mjs} +2 -2
  22. package/dist/{chunk-TWH422HC.mjs → chunk-RXMSZIYD.mjs} +3 -3
  23. package/dist/{chunk-CH5YAZ4Q.mjs → chunk-SQWN4GTQ.mjs} +2 -2
  24. package/dist/cli.mjs +9 -9
  25. package/dist/contacts-worker.mjs +2 -2
  26. package/dist/global-insight-worker.mjs +1 -1
  27. package/dist/{http-237XYNRT.mjs → http-X6OEOOSH.mjs} +12 -12
  28. package/dist/{import-R2E2TFAK.mjs → import-WBJ6YIQ7.mjs} +7 -7
  29. package/dist/index.mjs +12 -12
  30. package/dist/{mcp-BED4AARV.mjs → mcp-FR6OKWKL.mjs} +8 -8
  31. package/dist/people-relationships-worker.mjs +2 -2
  32. package/dist/semantic-index-worker.mjs +1 -1
  33. package/dist/{src-AYMALWU5.mjs → src-WE5HNN2O.mjs} +44 -14
  34. package/dist/{stream-import-RXNSOOYH.mjs → stream-import-YYQVZDGQ.mjs} +7 -7
  35. package/dist/{worker-runtime-JGAZU7MC.mjs → worker-runtime-YGB5ZDE4.mjs} +5 -5
  36. package/dist-cli-web/assets/AISettingsTab-Co2sM1iY.js +1 -0
  37. package/dist-cli-web/assets/{AboutTab-DCM1-glA.js → AboutTab-DQ8r6mYE.js} +1 -1
  38. package/dist-cli-web/assets/ActionToolsPanel-CmYpw8Mt.css +1 -0
  39. package/dist-cli-web/assets/ActionToolsPanel.vue_vue_type_script_setup_true_lang--RPlmfIu.js +75 -0
  40. package/dist-cli-web/assets/{Alert-D-egBWcb.js → Alert-DU1tLAAg.js} +1 -1
  41. package/dist-cli-web/assets/ApiSettingsTab-D5g5b79L.js +2 -0
  42. package/dist-cli-web/assets/BatchManageTab-BFUyVaGU.js +1 -0
  43. package/dist-cli-web/assets/{BufferResource-BE2_2bZK.js → BufferResource-C-RfYwQW.js} +1 -1
  44. package/dist-cli-web/assets/{CanvasRenderer-DDKdvEYQ.js → CanvasRenderer-Dz7JAMrR.js} +1 -1
  45. package/dist-cli-web/assets/CardDecoration-D28DI1G9.js +1 -0
  46. package/dist-cli-web/assets/ChatRecordDrawer-Cph819yY.js +1 -0
  47. package/dist-cli-web/assets/{ChatRecordWorkspace-DqUI4QiU.js → ChatRecordWorkspace-BAfERdPc.js} +1 -1
  48. package/dist-cli-web/assets/Checkbox-B8uqt13j.js +1 -0
  49. package/dist-cli-web/assets/DataDirCleanupNotice-3sGBE5vU.js +1 -0
  50. package/dist-cli-web/assets/{DatePicker.vue_vue_type_script_setup_true_lang-sPDoPzZ9.js → DatePicker.vue_vue_type_script_setup_true_lang-CQuub8-f.js} +1 -1
  51. package/dist-cli-web/assets/{DebugToolsPanel-BIHA6jw4.js → DebugToolsPanel-Cwkr0BOY.js} +1 -1
  52. package/dist-cli-web/assets/EChart.vue_vue_type_script_setup_true_lang-BuLujQCF.js +1 -0
  53. package/dist-cli-web/assets/EChartBar.vue_vue_type_script_setup_true_lang-DuA5xbcC.js +1 -0
  54. package/dist-cli-web/assets/EChartLine.vue_vue_type_script_setup_true_lang-CvmMEfYq.js +1 -0
  55. package/dist-cli-web/assets/EChartWordcloud-CHVtyPR7.js +1 -0
  56. package/dist-cli-web/assets/FileDropZone.vue_vue_type_script_setup_true_lang-BKPy9Qv-.js +1 -0
  57. package/dist-cli-web/assets/{Filter-kO4dIDJ2.js → Filter-CMfRHIoK.js} +1 -1
  58. package/dist-cli-web/assets/GlobalTaskBar-Jz6MASCu.js +1 -0
  59. package/dist-cli-web/assets/GlobalTaskBar-haLzezqY.css +1 -0
  60. package/dist-cli-web/assets/GroupRelationshipGalaxyView-Ch4obfEq.js +1 -0
  61. package/dist-cli-web/assets/Label-CuryuymO.js +1 -0
  62. package/dist-cli-web/assets/LoadingState.vue_vue_type_script_setup_true_lang-CoeQsaiL.js +1 -0
  63. package/dist-cli-web/assets/MessageExportModal-JqGZ-iLr.js +1 -0
  64. package/dist-cli-web/assets/PageHeader-CPLgiKT8.css +1 -0
  65. package/dist-cli-web/assets/PageHeader-D4up77TB.js +1 -0
  66. package/dist-cli-web/assets/{PageTabs.vue_vue_type_script_setup_true_lang-Cgungbjl.js → PageTabs.vue_vue_type_script_setup_true_lang-DYQRFk9s.js} +1 -1
  67. package/dist-cli-web/assets/{RelationshipGalaxyCanvas-Cod1JX9m.js → RelationshipGalaxyCanvas-Cn7Eta5U.js} +3 -3
  68. package/dist-cli-web/assets/{RelationshipGalaxyThreeCanvas-CkefzeFJ.js → RelationshipGalaxyThreeCanvas-LlaX57z6.js} +1 -1
  69. package/dist-cli-web/assets/{RenderTargetSystem-SuQ7cbze.js → RenderTargetSystem-JhA2PUFi.js} +1 -1
  70. package/dist-cli-web/assets/{RovingFocusItem-psoFYxrC.js → RovingFocusItem-DHaiTb4j.js} +1 -1
  71. package/dist-cli-web/assets/{ScreenCaptureModal-D4r9ixKh.js → ScreenCaptureModal-Bxhyt5g1.js} +1 -1
  72. package/dist-cli-web/assets/SectionTabs.vue_vue_type_script_setup_true_lang-BlAje3Tf.js +1 -0
  73. package/dist-cli-web/assets/Select-Beeqv0wU.js +1 -0
  74. package/dist-cli-web/assets/SettingsModal-DV8yDBJs.js +2 -0
  75. package/dist-cli-web/assets/{StorageTab-DhSJ-0Ir.js → StorageTab-B7GHadia.js} +1 -1
  76. package/dist-cli-web/assets/Switch-u4zlvUMa.js +1 -0
  77. package/dist-cli-web/assets/{Tabs-Bnm_Q7yW.js → Tabs-B7-Koq3x.js} +1 -1
  78. package/dist-cli-web/assets/Tabs-RWKHwtfM.js +1 -0
  79. package/dist-cli-web/assets/ThemeCard.vue_vue_type_script_setup_true_lang-ndsa3-aT.js +1 -0
  80. package/dist-cli-web/assets/{TimeSelect.vue_vue_type_script_setup_true_lang-Cjz8Wpg-.js → TimeSelect.vue_vue_type_script_setup_true_lang-DKa1flT-.js} +1 -1
  81. package/dist-cli-web/assets/{VisuallyHiddenInput-Bo_XwaLu.js → VisuallyHiddenInput-DGDqcvUy.js} +1 -1
  82. package/dist-cli-web/assets/{WebGLRenderer-D1x9WZRj.js → WebGLRenderer-ifvgA1fe.js} +1 -1
  83. package/dist-cli-web/assets/{WebGPURenderer-37Nw-tLm.js → WebGPURenderer-DwzmuWF0.js} +1 -1
  84. package/dist-cli-web/assets/aiChat-BY4C3UQe.js +8 -0
  85. package/dist-cli-web/assets/base-BWDX-164.js +1 -0
  86. package/dist-cli-web/assets/{browserAll-CWrz_-4U.js → browserAll-CSIIETeE.js} +1 -1
  87. package/dist-cli-web/assets/chatMessages--r0obAVw.css +1 -0
  88. package/dist-cli-web/assets/chatMessages-Zywjea1n.js +147 -0
  89. package/dist-cli-web/assets/cli-web-BRY49NWe.js +1 -0
  90. package/dist-cli-web/assets/{fetch-Du-wU0OO.js → fetch-Bf4cqeM3.js} +1 -1
  91. package/dist-cli-web/assets/{fetch-DPSWt6yX.js → fetch-BiXab0IF.js} +1 -1
  92. package/dist-cli-web/assets/{fetch-B69OkB6B.js → fetch-C8CcoTWY.js} +1 -1
  93. package/dist-cli-web/assets/{fetch-qd2FgBWQ.js → fetch-Co2vLmiE.js} +1 -1
  94. package/dist-cli-web/assets/{fetch-BGmCCyzz.js → fetch-D3nLtFBi.js} +1 -1
  95. package/dist-cli-web/assets/{fetch-CFpLXWTe.js → fetch-DNSDHSqQ.js} +1 -1
  96. package/dist-cli-web/assets/{fetch-DZ6OF-xW.js → fetch-DeHqF6yL.js} +1 -1
  97. package/dist-cli-web/assets/{fetch-CkXgOxcu.js → fetch-jx1VdM9j.js} +3 -3
  98. package/dist-cli-web/assets/{fetch-Gu4qQjUB.js → fetch-mYSeL4Ki.js} +1 -1
  99. package/dist-cli-web/assets/fetch-qBaUXqFt.js +1 -0
  100. package/dist-cli-web/assets/{fetch-6pcVxB9h.js → fetch-r6mhEGrP.js} +1 -1
  101. package/dist-cli-web/assets/{fetch-CgA1peJD.js → fetch-wzv71PYy.js} +1 -1
  102. package/dist-cli-web/assets/{host-CWsAWKgy.js → host-1emQf9_4.js} +1 -1
  103. package/dist-cli-web/assets/{host-D8zlA62K.js → host-jXJHyyOX.js} +1 -1
  104. package/dist-cli-web/assets/index-2dJCozDq.js +64 -0
  105. package/dist-cli-web/assets/index-6ejRbH3h.js +2 -0
  106. package/dist-cli-web/assets/index-B4mJkX_d.js +1 -0
  107. package/dist-cli-web/assets/index-B7biznha.js +1 -0
  108. package/dist-cli-web/assets/index-BOcFyPCa.js +1 -0
  109. package/dist-cli-web/assets/index-BQ463TS7.js +1 -0
  110. package/dist-cli-web/assets/index-BZpR015e.css +1 -0
  111. package/dist-cli-web/assets/index-BgelKQTW.css +1 -0
  112. package/dist-cli-web/assets/index-C1A1FwpO.css +1 -0
  113. package/dist-cli-web/assets/index-CIIyg_o9.css +1 -0
  114. package/dist-cli-web/assets/{index-a7CT8DQm.js → index-CYc_VLgK.js} +1 -1
  115. package/dist-cli-web/assets/index-ClHMs0-a.js +2 -0
  116. package/dist-cli-web/assets/{index-Dy0fRMDf.js → index-Cw0IhKZ6.js} +10 -10
  117. package/dist-cli-web/assets/{index-v6DlL4ue.js → index-DOqbUvhA.js} +1 -1
  118. package/dist-cli-web/assets/index-DnpnIyle.js +135 -0
  119. package/dist-cli-web/assets/index-LV9LfFNu.js +1 -0
  120. package/dist-cli-web/assets/index-kePiJ3ca.js +137 -0
  121. package/dist-cli-web/assets/index-slDUxib2.js +1 -0
  122. package/dist-cli-web/assets/{people-page-header-Czt9e1jv.js → people-page-header-Cx5r9Bk2.js} +1 -1
  123. package/dist-cli-web/assets/{prompt-DGazz143.js → prompt-BggEDxxi.js} +1 -1
  124. package/dist-cli-web/assets/{semantic-index-config-builder-BkFHzIUy.js → semantic-index-config-builder-1sO0aEc1.js} +1 -1
  125. package/dist-cli-web/assets/{service-CAp0YJoS.js → service-Bvq1fRkC.js} +1 -1
  126. package/dist-cli-web/assets/service-DkPheDt8.js +1 -0
  127. package/dist-cli-web/assets/service-DvI8O_7K.js +1 -0
  128. package/dist-cli-web/assets/useFormControl-B0aShtLp.js +1 -0
  129. package/dist-cli-web/assets/useTable-DC7oxmBl.js +1 -0
  130. package/dist-cli-web/assets/{useToast-YAHDkKDN.js → useToast-CCPBE-Sy.js} +1 -1
  131. package/dist-cli-web/assets/{vendor-echarts-hTLhTs52.js → vendor-echarts-C9ugTmum.js} +1 -1
  132. package/dist-cli-web/assets/{vendor-echarts-wordcloud-B8CF_NgP.js → vendor-echarts-wordcloud-DNiU8Psq.js} +1 -1
  133. package/dist-cli-web/assets/{webworkerAll-CvSp2Y8j.js → webworkerAll-FCJk6reG.js} +1 -1
  134. package/dist-cli-web/index.html +2 -2
  135. package/package.json +1 -1
  136. package/dist/chunk-6E46U4H2.mjs +0 -41
  137. package/dist/chunk-6E46U4H2.mjs.map +0 -1
  138. package/dist/chunk-HWIXK7SU.mjs.map +0 -1
  139. package/dist/chunk-TJ3NOBVI.mjs.map +0 -1
  140. package/dist/chunk-TRGR3KKJ.mjs.map +0 -1
  141. package/dist/chunk-VREVDZ4Q.mjs.map +0 -1
  142. package/dist/chunk-VUYWUHWW.mjs.map +0 -1
  143. package/dist/chunk-WJQOXOMR.mjs.map +0 -1
  144. package/dist/chunk-WRBFV2GX.mjs.map +0 -1
  145. package/dist/chunk-YUH3H3QW.mjs.map +0 -1
  146. package/dist-cli-web/assets/AISettingsTab-VgI7PiJd.js +0 -1
  147. package/dist-cli-web/assets/ActionToolsPanel-BTGgwXVD.css +0 -1
  148. package/dist-cli-web/assets/ActionToolsPanel.vue_vue_type_script_setup_true_lang-a7RMjjve.js +0 -112
  149. package/dist-cli-web/assets/ApiSettingsTab-Co3LWf1f.js +0 -2
  150. package/dist-cli-web/assets/BatchManageTab-2LrckmQm.js +0 -1
  151. package/dist-cli-web/assets/ChatRecordDrawer-DRhztqLG.js +0 -1
  152. package/dist-cli-web/assets/Checkbox-EfpQmv4A.js +0 -1
  153. package/dist-cli-web/assets/DataDirCleanupNotice-BYIiR58J.js +0 -1
  154. package/dist-cli-web/assets/EChart.vue_vue_type_script_setup_true_lang-DryMSoCz.js +0 -1
  155. package/dist-cli-web/assets/EChartBar.vue_vue_type_script_setup_true_lang-BdFW-hvt.js +0 -1
  156. package/dist-cli-web/assets/EChartLine.vue_vue_type_script_setup_true_lang-C69Kdw9X.js +0 -1
  157. package/dist-cli-web/assets/EChartWordcloud-DM7UEHdK.js +0 -1
  158. package/dist-cli-web/assets/GlobalTaskBar-BZS_h0zC.js +0 -1
  159. package/dist-cli-web/assets/GlobalTaskBar-COW_uBX6.css +0 -1
  160. package/dist-cli-web/assets/GroupRelationshipGalaxyView--WIaHx_F.js +0 -1
  161. package/dist-cli-web/assets/Label-CIuDXyeM.js +0 -1
  162. package/dist-cli-web/assets/LoadingState.vue_vue_type_script_setup_true_lang-B_7VHxTx.js +0 -1
  163. package/dist-cli-web/assets/MessageExportModal-BQsIpyxv.js +0 -1
  164. package/dist-cli-web/assets/PageHeader-DxhQHF_0.js +0 -1
  165. package/dist-cli-web/assets/PageHeader-XIXMTomn.css +0 -1
  166. package/dist-cli-web/assets/SectionTabs.vue_vue_type_script_setup_true_lang-v_7aRBgM.js +0 -1
  167. package/dist-cli-web/assets/Select-CxurgslB.js +0 -1
  168. package/dist-cli-web/assets/SettingsModal-DBZucuf6.js +0 -2
  169. package/dist-cli-web/assets/Switch-3D81G6uV.js +0 -1
  170. package/dist-cli-web/assets/Tabs-DB603EEu.js +0 -1
  171. package/dist-cli-web/assets/ThemeCard.vue_vue_type_script_setup_true_lang-BDTdkH5i.js +0 -1
  172. package/dist-cli-web/assets/aiChat-BCzYQeZq.js +0 -8
  173. package/dist-cli-web/assets/cli-web-C-8N0CJh.js +0 -1
  174. package/dist-cli-web/assets/fetch-z9M9FcIC.js +0 -1
  175. package/dist-cli-web/assets/index-BGd4ZfhO.js +0 -2
  176. package/dist-cli-web/assets/index-BYZF1ngN.js +0 -1
  177. package/dist-cli-web/assets/index-BcuwlCin.css +0 -1
  178. package/dist-cli-web/assets/index-BnaLj2dx.css +0 -1
  179. package/dist-cli-web/assets/index-Bq5q3kSU.js +0 -36
  180. package/dist-cli-web/assets/index-CPR3jdC2.js +0 -137
  181. package/dist-cli-web/assets/index-D3AjvaOX.js +0 -1
  182. package/dist-cli-web/assets/index-DAitScUA.js +0 -1
  183. package/dist-cli-web/assets/index-DBSMWAJ0.js +0 -1
  184. package/dist-cli-web/assets/index-DTn9htw-.js +0 -64
  185. package/dist-cli-web/assets/index-DV_rWxcH.js +0 -1
  186. package/dist-cli-web/assets/index-DxhAGqYM.css +0 -1
  187. package/dist-cli-web/assets/index-I0mIcDpE.css +0 -1
  188. package/dist-cli-web/assets/index-onwNShRw.js +0 -2
  189. package/dist-cli-web/assets/service-BMqwB74f.js +0 -1
  190. package/dist-cli-web/assets/service-CUQ8iHdS.js +0 -1
  191. package/dist-cli-web/assets/useFormControl-DcSMUc2d.js +0 -1
  192. package/dist-cli-web/assets/useTable-Clmq3sge.js +0 -1
  193. /package/dist/{chunk-6EZPD3XJ.mjs.map → chunk-N2WGG3MS.mjs.map} +0 -0
  194. /package/dist/{chunk-TWH422HC.mjs.map → chunk-RXMSZIYD.mjs.map} +0 -0
  195. /package/dist/{chunk-CH5YAZ4Q.mjs.map → chunk-SQWN4GTQ.mjs.map} +0 -0
  196. /package/dist/{http-237XYNRT.mjs.map → http-X6OEOOSH.mjs.map} +0 -0
  197. /package/dist/{import-R2E2TFAK.mjs.map → import-WBJ6YIQ7.mjs.map} +0 -0
  198. /package/dist/{mcp-BED4AARV.mjs.map → mcp-FR6OKWKL.mjs.map} +0 -0
  199. /package/dist/{src-AYMALWU5.mjs.map → src-WE5HNN2O.mjs.map} +0 -0
  200. /package/dist/{stream-import-RXNSOOYH.mjs.map → stream-import-YYQVZDGQ.mjs.map} +0 -0
  201. /package/dist/{worker-runtime-JGAZU7MC.mjs.map → worker-runtime-YGB5ZDE4.mjs.map} +0 -0
@@ -4118,6 +4118,14 @@ function buildMsgConditions(options) {
4118
4118
  if (options?.senderId != null) {
4119
4119
  conds.push("msg.sender_id = ?");
4120
4120
  params.push(options.senderId);
4121
+ } else if (options?.senderIds) {
4122
+ const senderIds = [...new Set(options.senderIds)];
4123
+ if (senderIds.length === 0) {
4124
+ conds.push("1 = 0");
4125
+ } else {
4126
+ conds.push(`msg.sender_id IN (${senderIds.map(() => "?").join(", ")})`);
4127
+ params.push(...senderIds);
4128
+ }
4121
4129
  }
4122
4130
  if (options?.memberId != null) {
4123
4131
  conds.push("msg.sender_id = ?");
@@ -4332,6 +4340,18 @@ function getLaughAnalysis(db, filter, keywords) {
4332
4340
  };
4333
4341
  }
4334
4342
  var DEFAULT_CLUSTER_OPTIONS = { lookAhead: 3, decaySeconds: 120, topEdges: 100 };
4343
+ var MAX_CLUSTER_LOOK_AHEAD = 10;
4344
+ var DEFAULT_SELECTED_PROXIMITY_MAX_GAP_SECONDS = 1800;
4345
+ var CO_OCCURRENCE_MESSAGE_BATCH_SIZE = 2e4;
4346
+ function resolveClusterOptions(options) {
4347
+ const requestedLookAhead = options?.lookAhead ?? DEFAULT_CLUSTER_OPTIONS.lookAhead;
4348
+ const lookAhead = Number.isFinite(requestedLookAhead) ? Math.min(MAX_CLUSTER_LOOK_AHEAD, Math.max(0, Math.ceil(requestedLookAhead))) : DEFAULT_CLUSTER_OPTIONS.lookAhead;
4349
+ return {
4350
+ lookAhead,
4351
+ decaySeconds: options?.decaySeconds ?? DEFAULT_CLUSTER_OPTIONS.decaySeconds,
4352
+ topEdges: options?.topEdges ?? DEFAULT_CLUSTER_OPTIONS.topEdges
4353
+ };
4354
+ }
4335
4355
  function roundNum(value, digits = 4) {
4336
4356
  const factor = 10 ** digits;
4337
4357
  return Math.round(value * factor) / factor;
@@ -4340,44 +4360,222 @@ function clusterPairKey(aId, bId) {
4340
4360
  return aId < bId ? `${aId}-${bId}` : `${bId}-${aId}`;
4341
4361
  }
4342
4362
  function accumulateCoOccurrencePairs(messages, options) {
4343
- const opts = { ...DEFAULT_CLUSTER_OPTIONS, ...options };
4344
- const pairRawScore = /* @__PURE__ */ new Map();
4345
- const pairCoOccurrence = /* @__PURE__ */ new Map();
4346
- const pairLastOccurrenceTs = /* @__PURE__ */ new Map();
4347
- for (let i = 0; i < messages.length - 1; i++) {
4348
- const anchor = messages[i];
4363
+ const opts = resolveClusterOptions(options);
4364
+ const pairStatsBySource = /* @__PURE__ */ new Map();
4365
+ const pairs = [];
4366
+ for (let index = 0; index < messages.length - 1; index++) {
4367
+ const anchor = messages[index];
4349
4368
  const seenPartners = /* @__PURE__ */ new Set();
4350
4369
  let partnersFound = 0;
4351
- for (let j = i + 1; j < messages.length && partnersFound < opts.lookAhead; j++) {
4352
- const candidate = messages[j];
4370
+ for (let nextIndex = index + 1; nextIndex < messages.length && partnersFound < opts.lookAhead; nextIndex++) {
4371
+ const candidate = messages[nextIndex];
4353
4372
  if (candidate.senderId === anchor.senderId || seenPartners.has(candidate.senderId)) continue;
4354
4373
  seenPartners.add(candidate.senderId);
4355
4374
  partnersFound++;
4356
- const deltaSeconds = candidate.ts - anchor.ts;
4357
- const decayWeight = Math.exp(-deltaSeconds / opts.decaySeconds);
4375
+ const decayWeight = Math.exp(-(candidate.ts - anchor.ts) / opts.decaySeconds);
4358
4376
  const positionWeight = 1 - (partnersFound - 1) * 0.2;
4359
- const weight = decayWeight * positionWeight;
4360
- const key = clusterPairKey(anchor.senderId, candidate.senderId);
4361
- pairRawScore.set(key, (pairRawScore.get(key) || 0) + weight);
4362
- pairCoOccurrence.set(key, (pairCoOccurrence.get(key) || 0) + 1);
4363
- pairLastOccurrenceTs.set(key, Math.max(pairLastOccurrenceTs.get(key) ?? 0, candidate.ts));
4377
+ applyCoOccurrencePair(
4378
+ anchor.senderId,
4379
+ candidate.senderId,
4380
+ candidate.ts,
4381
+ decayWeight * positionWeight,
4382
+ pairStatsBySource,
4383
+ pairs
4384
+ );
4364
4385
  }
4365
4386
  }
4387
+ return pairs;
4388
+ }
4389
+ function accumulateCoOccurrencePairBatches(batches, options) {
4390
+ const senderChunks = [];
4391
+ const timestampChunks = [];
4392
+ let messageCount = 0;
4393
+ for (const batch of batches) {
4394
+ if (batch.length === 0) continue;
4395
+ const senderIds2 = new Float64Array(batch.length);
4396
+ const timestamps2 = new Float64Array(batch.length);
4397
+ for (let index = 0; index < batch.length; index++) {
4398
+ senderIds2[index] = batch[index].senderId;
4399
+ timestamps2[index] = batch[index].ts;
4400
+ }
4401
+ senderChunks.push(senderIds2);
4402
+ timestampChunks.push(timestamps2);
4403
+ messageCount += batch.length;
4404
+ }
4405
+ const senderIds = mergeNumberChunks(senderChunks, messageCount);
4406
+ const timestamps = mergeNumberChunks(timestampChunks, messageCount);
4407
+ senderChunks.length = 0;
4408
+ timestampChunks.length = 0;
4409
+ return accumulateCoOccurrenceColumns(senderIds, timestamps, options);
4410
+ }
4411
+ function mergeNumberChunks(chunks, totalLength) {
4412
+ if (chunks.length === 1) return chunks[0];
4413
+ const merged = new Float64Array(totalLength);
4414
+ let offset = 0;
4415
+ for (const chunk of chunks) {
4416
+ merged.set(chunk, offset);
4417
+ offset += chunk.length;
4418
+ }
4419
+ return merged;
4420
+ }
4421
+ function accumulateCoOccurrenceColumns(senderIds, timestamps, options) {
4422
+ const opts = resolveClusterOptions(options);
4423
+ const lookAhead = opts.lookAhead;
4424
+ const candidateIndexes = buildDistinctSpeakerLookAhead(senderIds.length, (index) => senderIds[index], lookAhead);
4425
+ const pairStatsBySource = /* @__PURE__ */ new Map();
4366
4426
  const pairs = [];
4367
- for (const [key, rawScore] of pairRawScore) {
4368
- const [sourceIdStr, targetIdStr] = key.split("-");
4369
- pairs.push({
4370
- sourceId: parseInt(sourceIdStr),
4371
- targetId: parseInt(targetIdStr),
4372
- rawScore,
4373
- coOccurrenceCount: pairCoOccurrence.get(key) || 0,
4374
- lastOccurrenceTs: pairLastOccurrenceTs.get(key) ?? 0
4375
- });
4427
+ for (let index = 0; index < senderIds.length - 1; index++) {
4428
+ for (let partnerIndex = 0; partnerIndex < lookAhead; partnerIndex++) {
4429
+ const candidateIndex = candidateIndexes[index * lookAhead + partnerIndex];
4430
+ if (candidateIndex < 0) break;
4431
+ const decayWeight = Math.exp(-(timestamps[candidateIndex] - timestamps[index]) / opts.decaySeconds);
4432
+ const positionWeight = 1 - partnerIndex * 0.2;
4433
+ applyCoOccurrencePair(
4434
+ senderIds[index],
4435
+ senderIds[candidateIndex],
4436
+ timestamps[candidateIndex],
4437
+ decayWeight * positionWeight,
4438
+ pairStatsBySource,
4439
+ pairs
4440
+ );
4441
+ }
4376
4442
  }
4377
4443
  return pairs;
4378
4444
  }
4445
+ function applyCoOccurrencePair(leftSenderId, rightSenderId, timestamp, weight, pairStatsBySource, pairs) {
4446
+ const sourceId = Math.min(leftSenderId, rightSenderId);
4447
+ const targetId = Math.max(leftSenderId, rightSenderId);
4448
+ let pairStatsByTarget = pairStatsBySource.get(sourceId);
4449
+ if (!pairStatsByTarget) {
4450
+ pairStatsByTarget = /* @__PURE__ */ new Map();
4451
+ pairStatsBySource.set(sourceId, pairStatsByTarget);
4452
+ }
4453
+ let pair = pairStatsByTarget.get(targetId);
4454
+ if (!pair) {
4455
+ pair = { sourceId, targetId, rawScore: 0, coOccurrenceCount: 0, lastOccurrenceTs: 0 };
4456
+ pairStatsByTarget.set(targetId, pair);
4457
+ pairs.push(pair);
4458
+ }
4459
+ pair.rawScore += weight;
4460
+ pair.coOccurrenceCount++;
4461
+ pair.lastOccurrenceTs = Math.max(pair.lastOccurrenceTs, timestamp);
4462
+ }
4463
+ function accumulateSelectedCoOccurrencePairs(messages, selectedPairs, options) {
4464
+ const opts = resolveClusterOptions(options);
4465
+ const lookAhead = opts.lookAhead;
4466
+ const maxAnchorsPerPair = Math.max(0, Math.floor(options?.maxAnchorsPerPair ?? 2));
4467
+ const maxGapSeconds = Math.max(0, options?.maxGapSeconds ?? DEFAULT_SELECTED_PROXIMITY_MAX_GAP_SECONDS);
4468
+ const selectedKeys = new Set(selectedPairs.map(([left, right]) => clusterPairKey(left, right)));
4469
+ const candidateIndexes = buildDistinctSpeakerLookAhead(
4470
+ messages.length,
4471
+ (index) => messages[index].senderId,
4472
+ lookAhead
4473
+ );
4474
+ const stats = /* @__PURE__ */ new Map();
4475
+ for (let i = 0; i < messages.length - 1; i++) {
4476
+ const anchor = messages[i];
4477
+ for (let partnerIndex = 0; partnerIndex < lookAhead; partnerIndex++) {
4478
+ const candidateIndex = candidateIndexes[i * lookAhead + partnerIndex];
4479
+ if (candidateIndex < 0) break;
4480
+ const candidate = messages[candidateIndex];
4481
+ const deltaSeconds = candidate.ts - anchor.ts;
4482
+ if (deltaSeconds > maxGapSeconds) break;
4483
+ const key = clusterPairKey(anchor.senderId, candidate.senderId);
4484
+ if (!selectedKeys.has(key)) continue;
4485
+ const decayWeight = Math.exp(-deltaSeconds / opts.decaySeconds);
4486
+ const positionWeight = 1 - partnerIndex * 0.2;
4487
+ const weight = decayWeight * positionWeight;
4488
+ const sourceId = Math.min(anchor.senderId, candidate.senderId);
4489
+ const targetId = Math.max(anchor.senderId, candidate.senderId);
4490
+ const current = stats.get(key) ?? {
4491
+ sourceId,
4492
+ targetId,
4493
+ rawScore: 0,
4494
+ coOccurrenceCount: 0,
4495
+ lastOccurrenceTs: 0,
4496
+ anchors: []
4497
+ };
4498
+ current.rawScore += weight;
4499
+ current.coOccurrenceCount++;
4500
+ current.lastOccurrenceTs = Math.max(current.lastOccurrenceTs, candidate.ts);
4501
+ retainBestSelectedAnchor(
4502
+ current.anchors,
4503
+ {
4504
+ messageId: candidate.messageId,
4505
+ relatedMessageId: anchor.messageId,
4506
+ fromMemberId: anchor.senderId,
4507
+ toMemberId: candidate.senderId,
4508
+ timestamp: candidate.ts,
4509
+ weight
4510
+ },
4511
+ maxAnchorsPerPair
4512
+ );
4513
+ stats.set(key, current);
4514
+ }
4515
+ }
4516
+ return [...stats.values()].map((item) => ({
4517
+ sourceId: item.sourceId,
4518
+ targetId: item.targetId,
4519
+ rawScore: item.rawScore,
4520
+ coOccurrenceCount: item.coOccurrenceCount,
4521
+ lastOccurrenceTs: item.lastOccurrenceTs,
4522
+ anchors: item.anchors.sort((left, right) => right.weight - left.weight || right.timestamp - left.timestamp).slice(0, maxAnchorsPerPair)
4523
+ }));
4524
+ }
4525
+ function buildDistinctSpeakerLookAhead(messageCount, senderIdAt, lookAhead) {
4526
+ const candidateIndexes = new Int32Array(messageCount * lookAhead);
4527
+ candidateIndexes.fill(-1);
4528
+ if (lookAhead === 0 || messageCount === 0) return candidateIndexes;
4529
+ const previousIndexes = new Int32Array(messageCount);
4530
+ const nextIndexes = new Int32Array(messageCount);
4531
+ previousIndexes.fill(-1);
4532
+ nextIndexes.fill(-1);
4533
+ const activeIndexBySender = /* @__PURE__ */ new Map();
4534
+ let headIndex = -1;
4535
+ for (let index = messageCount - 1; index >= 0; index--) {
4536
+ const senderId = senderIdAt(index);
4537
+ let candidateIndex = headIndex;
4538
+ let found = 0;
4539
+ while (candidateIndex >= 0 && found < lookAhead) {
4540
+ if (senderIdAt(candidateIndex) !== senderId) {
4541
+ candidateIndexes[index * lookAhead + found] = candidateIndex;
4542
+ found++;
4543
+ }
4544
+ candidateIndex = nextIndexes[candidateIndex];
4545
+ }
4546
+ const previousOccurrence = activeIndexBySender.get(senderId);
4547
+ if (previousOccurrence !== void 0) {
4548
+ const previousIndex = previousIndexes[previousOccurrence];
4549
+ const nextIndex = nextIndexes[previousOccurrence];
4550
+ if (previousIndex >= 0) nextIndexes[previousIndex] = nextIndex;
4551
+ else headIndex = nextIndex;
4552
+ if (nextIndex >= 0) previousIndexes[nextIndex] = previousIndex;
4553
+ }
4554
+ previousIndexes[index] = -1;
4555
+ nextIndexes[index] = headIndex;
4556
+ if (headIndex >= 0) previousIndexes[headIndex] = index;
4557
+ headIndex = index;
4558
+ activeIndexBySender.set(senderId, index);
4559
+ }
4560
+ return candidateIndexes;
4561
+ }
4562
+ function retainBestSelectedAnchor(anchors, candidate, limit) {
4563
+ if (limit === 0) return;
4564
+ if (anchors.length < limit) {
4565
+ anchors.push(candidate);
4566
+ return;
4567
+ }
4568
+ let worstIndex = 0;
4569
+ for (let index = 1; index < anchors.length; index++) {
4570
+ if (compareSelectedAnchorQuality(anchors[index], anchors[worstIndex]) < 0) worstIndex = index;
4571
+ }
4572
+ if (compareSelectedAnchorQuality(candidate, anchors[worstIndex]) > 0) anchors[worstIndex] = candidate;
4573
+ }
4574
+ function compareSelectedAnchorQuality(left, right) {
4575
+ return left.weight - right.weight || left.timestamp - right.timestamp;
4576
+ }
4379
4577
  function getClusterGraph(db, filter, options) {
4380
- const opts = { ...DEFAULT_CLUSTER_OPTIONS, ...options };
4578
+ const opts = resolveClusterOptions(options);
4381
4579
  const emptyResult2 = {
4382
4580
  nodes: [],
4383
4581
  links: [],
@@ -4394,28 +4592,26 @@ function getClusterGraph(db, filter, options) {
4394
4592
  const memberInfo = /* @__PURE__ */ new Map();
4395
4593
  for (const m of members)
4396
4594
  memberInfo.set(m.id, { name: m.name, platformId: m.platformId, messageCount: m.messageCount });
4397
- const { clause, params } = buildTimeFilter(filter);
4398
- let whereClause = clause;
4399
- if (whereClause.includes("WHERE")) {
4400
- whereClause += " AND COALESCE(m.account_name, '') != '\u7CFB\u7EDF\u6D88\u606F'";
4401
- } else {
4402
- whereClause = " WHERE COALESCE(m.account_name, '') != '\u7CFB\u7EDF\u6D88\u606F'";
4403
- }
4404
- const messages = db.prepare(
4405
- `SELECT msg.sender_id as senderId, msg.ts as ts FROM message msg JOIN member m ON msg.sender_id = m.id
4406
- ${whereClause} ORDER BY msg.ts ASC, msg.id ASC`
4407
- ).all(...params);
4408
- if (messages.length < 2)
4595
+ const memberMsgCount = /* @__PURE__ */ new Map();
4596
+ let totalMessages = 0;
4597
+ const trackedMessageBatches = (function* () {
4598
+ for (const batch of queryClusterMessageBatches(db, filter)) {
4599
+ for (const message of batch) {
4600
+ memberMsgCount.set(message.senderId, (memberMsgCount.get(message.senderId) || 0) + 1);
4601
+ totalMessages++;
4602
+ }
4603
+ yield batch;
4604
+ }
4605
+ })();
4606
+ const coOccurrencePairs = accumulateCoOccurrencePairBatches(trackedMessageBatches, opts);
4607
+ if (totalMessages < 2)
4409
4608
  return {
4410
4609
  ...emptyResult2,
4411
- stats: { ...emptyResult2.stats, totalMembers: members.length, totalMessages: messages.length }
4610
+ stats: { ...emptyResult2.stats, totalMembers: members.length, totalMessages }
4412
4611
  };
4413
- const memberMsgCount = /* @__PURE__ */ new Map();
4414
- for (const msg of messages) memberMsgCount.set(msg.senderId, (memberMsgCount.get(msg.senderId) || 0) + 1);
4415
- const totalMessages = messages.length;
4416
4612
  const lookAheadFactor = opts.lookAhead * 0.8;
4417
4613
  const rawEdges = [];
4418
- for (const pair of accumulateCoOccurrencePairs(messages, opts)) {
4614
+ for (const pair of coOccurrencePairs) {
4419
4615
  const aId = pair.sourceId;
4420
4616
  const bId = pair.targetId;
4421
4617
  const aMsgCount = memberMsgCount.get(aId) || 0;
@@ -4448,7 +4644,7 @@ function getClusterGraph(db, filter, options) {
4448
4644
  if (keptEdges.length === 0)
4449
4645
  return {
4450
4646
  ...emptyResult2,
4451
- stats: { ...emptyResult2.stats, totalMembers: members.length, totalMessages: messages.length }
4647
+ stats: { ...emptyResult2.stats, totalMembers: members.length, totalMessages }
4452
4648
  };
4453
4649
  const involvedIds = /* @__PURE__ */ new Set();
4454
4650
  for (const edge of keptEdges) {
@@ -4508,13 +4704,44 @@ function getClusterGraph(db, filter, options) {
4508
4704
  communities: [],
4509
4705
  stats: {
4510
4706
  totalMembers: members.length,
4511
- totalMessages: messages.length,
4707
+ totalMessages,
4512
4708
  involvedMembers: involvedIds.size,
4513
4709
  edgeCount: keptEdges.length,
4514
4710
  communityCount: 0
4515
4711
  }
4516
4712
  };
4517
4713
  }
4714
+ function* queryClusterMessageBatches(db, filter) {
4715
+ const { clause, params } = buildTimeFilter(filter, "msg");
4716
+ const systemMemberCondition = "COALESCE(m.account_name, '') != '\u7CFB\u7EDF\u6D88\u606F'";
4717
+ const firstWhere = appendSocialConditions(clause, [systemMemberCondition]);
4718
+ const nextWhere = appendSocialConditions(clause, [systemMemberCondition, "(msg.ts, msg.id) > (?, ?)"]);
4719
+ const firstPage = db.prepare(
4720
+ `SELECT msg.id, msg.sender_id as senderId, msg.ts
4721
+ FROM message msg JOIN member m ON msg.sender_id = m.id
4722
+ ${firstWhere}
4723
+ ORDER BY msg.ts ASC, msg.id ASC
4724
+ LIMIT ?`
4725
+ );
4726
+ const nextPage = db.prepare(
4727
+ `SELECT msg.id, msg.sender_id as senderId, msg.ts
4728
+ FROM message msg JOIN member m ON msg.sender_id = m.id
4729
+ ${nextWhere}
4730
+ ORDER BY msg.ts ASC, msg.id ASC
4731
+ LIMIT ?`
4732
+ );
4733
+ let rows = firstPage.all(...params, CO_OCCURRENCE_MESSAGE_BATCH_SIZE);
4734
+ while (rows.length > 0) {
4735
+ yield rows;
4736
+ if (rows.length < CO_OCCURRENCE_MESSAGE_BATCH_SIZE) break;
4737
+ const last = rows[rows.length - 1];
4738
+ rows = nextPage.all(...params, last.ts, last.id, CO_OCCURRENCE_MESSAGE_BATCH_SIZE);
4739
+ }
4740
+ }
4741
+ function appendSocialConditions(clause, conditions) {
4742
+ const existing = clause.trim().replace(/^WHERE\s+/i, "");
4743
+ return `WHERE ${[existing, ...conditions].filter(Boolean).join(" AND ")}`;
4744
+ }
4518
4745
 
4519
4746
  // ../../packages/core/src/query/contact-queries.ts
4520
4747
  var SYSTEM_MESSAGE_TYPES = [80, 81];
@@ -4526,6 +4753,10 @@ function isValidContactPlatformId(platformId) {
4526
4753
  function resolveOwnerMember(db) {
4527
4754
  const meta = db.prepare("SELECT owner_id FROM meta LIMIT 1").get();
4528
4755
  if (!isValidContactPlatformId(meta?.owner_id)) return null;
4756
+ return resolveContactMember(db, meta.owner_id);
4757
+ }
4758
+ function resolveContactMember(db, platformId) {
4759
+ if (!isValidContactPlatformId(platformId)) return null;
4529
4760
  const aliasesSelect = hasColumn(db, "member", "aliases") ? "aliases" : "NULL as aliases";
4530
4761
  const row = db.prepare(
4531
4762
  `SELECT
@@ -4537,7 +4768,7 @@ function resolveOwnerMember(db) {
4537
4768
  FROM member m
4538
4769
  WHERE platform_id = ? AND ${nonSystemContactMemberCondition(db, "m")}
4539
4770
  LIMIT 1`
4540
- ).get(meta.owner_id);
4771
+ ).get(platformId);
4541
4772
  return row ? mapContactMemberRow(row) : null;
4542
4773
  }
4543
4774
  function getNonSystemMembersForContacts(db) {
@@ -4564,6 +4795,261 @@ function getLatestContactMessageTs(db) {
4564
4795
  ).get();
4565
4796
  return row?.ts ?? null;
4566
4797
  }
4798
+ function getParticipantSessionFacts(db, memberId, options = {}) {
4799
+ const timeFilter = createBoundedMessageTimeFilter("msg", options);
4800
+ const row = db.prepare(
4801
+ `SELECT
4802
+ COUNT(*) as sessionMessageCount,
4803
+ COALESCE(SUM(CASE WHEN msg.sender_id = ? THEN 1 ELSE 0 END), 0) as ownMessageCount,
4804
+ MIN(CASE WHEN msg.sender_id = ? THEN msg.ts END) as firstOwnMessageTs,
4805
+ MAX(CASE WHEN msg.sender_id = ? THEN msg.ts END) as lastOwnMessageTs,
4806
+ COUNT(DISTINCT CASE
4807
+ WHEN msg.sender_id = ? THEN strftime('%Y-%m-%d', msg.ts, 'unixepoch', 'localtime')
4808
+ END) as activeDays
4809
+ FROM message msg
4810
+ JOIN member m ON msg.sender_id = m.id
4811
+ WHERE ${nonSystemMessageCondition(db, "msg", "m")}${timeFilter.sql}`
4812
+ ).get(memberId, memberId, memberId, memberId, ...timeFilter.params);
4813
+ const dataRange = db.prepare(
4814
+ `SELECT MIN(msg.ts) as firstMessageTs, MAX(msg.ts) as lastMessageTs
4815
+ FROM message msg
4816
+ JOIN member m ON msg.sender_id = m.id
4817
+ WHERE ${nonSystemMessageCondition(db, "msg", "m")}`
4818
+ ).get();
4819
+ return {
4820
+ memberId,
4821
+ ownMessageCount: row?.ownMessageCount ?? 0,
4822
+ sessionMessageCount: row?.sessionMessageCount ?? 0,
4823
+ firstOwnMessageTs: row?.firstOwnMessageTs ?? null,
4824
+ lastOwnMessageTs: row?.lastOwnMessageTs ?? null,
4825
+ activeDays: row?.activeDays ?? 0,
4826
+ memberCount: getNonSystemMembersForContacts(db).length,
4827
+ sessionFirstMessageTs: dataRange?.firstMessageTs ?? null,
4828
+ sessionLastMessageTs: dataRange?.lastMessageTs ?? null
4829
+ };
4830
+ }
4831
+ function getParticipantSetInteractionFacts(db, rawMemberIds, options = {}) {
4832
+ const memberIds = [...new Set(rawMemberIds)];
4833
+ if (memberIds.length < 2) throw new Error("At least two distinct member IDs are required");
4834
+ const placeholders = memberIds.map(() => "?").join(", ");
4835
+ const timeFilter = createBoundedMessageTimeFilter("msg", options);
4836
+ const activityRows = db.prepare(
4837
+ `SELECT
4838
+ msg.sender_id as memberId,
4839
+ COUNT(*) as messageCount,
4840
+ MIN(msg.ts) as firstMessageTs,
4841
+ MAX(msg.ts) as lastMessageTs,
4842
+ COUNT(DISTINCT strftime('%Y-%m-%d', msg.ts, 'unixepoch', 'localtime')) as activeDays
4843
+ FROM message msg
4844
+ JOIN member m ON msg.sender_id = m.id
4845
+ WHERE ${nonSystemMessageCondition(db, "msg", "m")}
4846
+ AND msg.sender_id IN (${placeholders})${timeFilter.sql}
4847
+ GROUP BY msg.sender_id`
4848
+ ).all(...memberIds, ...timeFilter.params);
4849
+ const activityByMemberId = new Map(activityRows.map((row) => [row.memberId, row]));
4850
+ const participants = memberIds.map((memberId) => {
4851
+ const row = activityByMemberId.get(memberId);
4852
+ return {
4853
+ memberId,
4854
+ messageCount: row?.messageCount ?? 0,
4855
+ firstMessageTs: row?.firstMessageTs ?? null,
4856
+ lastMessageTs: row?.lastMessageTs ?? null,
4857
+ activeDays: row?.activeDays ?? 0
4858
+ };
4859
+ });
4860
+ const activeDayRows = db.prepare(
4861
+ `SELECT
4862
+ msg.sender_id as memberId,
4863
+ strftime('%Y-%m-%d', msg.ts, 'unixepoch', 'localtime') as activeDay
4864
+ FROM message msg
4865
+ JOIN member m ON msg.sender_id = m.id
4866
+ WHERE ${nonSystemMessageCondition(db, "msg", "m")}
4867
+ AND msg.sender_id IN (${placeholders})${timeFilter.sql}
4868
+ GROUP BY msg.sender_id, activeDay`
4869
+ ).all(...memberIds, ...timeFilter.params);
4870
+ const activeDaysByMemberId = new Map(memberIds.map((memberId) => [memberId, /* @__PURE__ */ new Set()]));
4871
+ for (const row of activeDayRows) activeDaysByMemberId.get(row.memberId)?.add(row.activeDay);
4872
+ const pairMap = /* @__PURE__ */ new Map();
4873
+ for (let sourceIndex = 0; sourceIndex < memberIds.length - 1; sourceIndex++) {
4874
+ for (let targetIndex = sourceIndex + 1; targetIndex < memberIds.length; targetIndex++) {
4875
+ const sourceMemberId = memberIds[sourceIndex];
4876
+ const targetMemberId = memberIds[targetIndex];
4877
+ pairMap.set(contactPairKey(sourceMemberId, targetMemberId), {
4878
+ sourceMemberId,
4879
+ targetMemberId,
4880
+ directReplyCount: 0,
4881
+ repliesFromSourceToTarget: 0,
4882
+ repliesFromTargetToSource: 0,
4883
+ lastDirectReplyTs: null,
4884
+ coOccurrenceCount: 0,
4885
+ coOccurrenceRawScore: 0,
4886
+ lastProximityTs: null,
4887
+ coActiveDays: countSetIntersection(
4888
+ activeDaysByMemberId.get(sourceMemberId) ?? /* @__PURE__ */ new Set(),
4889
+ activeDaysByMemberId.get(targetMemberId) ?? /* @__PURE__ */ new Set()
4890
+ ),
4891
+ anchors: [],
4892
+ anchorsTruncated: false
4893
+ });
4894
+ }
4895
+ }
4896
+ const replyStatsRows = db.prepare(
4897
+ `SELECT
4898
+ msg.sender_id as replySenderId,
4899
+ target.sender_id as targetSenderId,
4900
+ COUNT(*) as directReplyCount,
4901
+ MAX(msg.ts) as lastDirectReplyTs
4902
+ FROM message msg
4903
+ JOIN message target ON msg.reply_to_message_id = target.platform_message_id
4904
+ JOIN member sender ON msg.sender_id = sender.id
4905
+ JOIN member targetMember ON target.sender_id = targetMember.id
4906
+ WHERE msg.reply_to_message_id IS NOT NULL
4907
+ AND ${nonSystemMessageCondition(db, "msg", "sender")}
4908
+ AND ${nonSystemMessageCondition(db, "target", "targetMember")}
4909
+ AND msg.sender_id IN (${placeholders})
4910
+ AND +target.sender_id IN (${placeholders})
4911
+ AND msg.sender_id <> target.sender_id${timeFilter.sql}
4912
+ GROUP BY msg.sender_id, target.sender_id`
4913
+ ).all(...memberIds, ...memberIds, ...timeFilter.params);
4914
+ const maxAnchorsPerPair = Math.max(0, Math.floor(options.maxAnchorsPerPair ?? 4));
4915
+ const replyDirectionCountsByPair = /* @__PURE__ */ new Map();
4916
+ for (const row of replyStatsRows) {
4917
+ const pair = pairMap.get(contactPairKey(row.replySenderId, row.targetSenderId));
4918
+ if (!pair) continue;
4919
+ pair.directReplyCount += row.directReplyCount;
4920
+ if (row.replySenderId === pair.sourceMemberId) pair.repliesFromSourceToTarget += row.directReplyCount;
4921
+ else pair.repliesFromTargetToSource += row.directReplyCount;
4922
+ if (row.lastDirectReplyTs !== null) {
4923
+ pair.lastDirectReplyTs = Math.max(pair.lastDirectReplyTs ?? 0, row.lastDirectReplyTs);
4924
+ }
4925
+ const pairKey = contactPairKey(row.replySenderId, row.targetSenderId);
4926
+ replyDirectionCountsByPair.set(pairKey, (replyDirectionCountsByPair.get(pairKey) ?? 0) + 1);
4927
+ }
4928
+ if (maxAnchorsPerPair > 0 && replyStatsRows.length > 0) {
4929
+ const replyAnchorStatement = db.prepare(
4930
+ `SELECT
4931
+ msg.id as messageId,
4932
+ target.id as relatedMessageId,
4933
+ msg.sender_id as replySenderId,
4934
+ target.sender_id as targetSenderId,
4935
+ msg.ts as replyTs
4936
+ FROM message msg
4937
+ JOIN message target ON msg.reply_to_message_id = target.platform_message_id
4938
+ JOIN member sender ON msg.sender_id = sender.id
4939
+ JOIN member targetMember ON target.sender_id = targetMember.id
4940
+ WHERE msg.reply_to_message_id IS NOT NULL
4941
+ AND ${nonSystemMessageCondition(db, "msg", "sender")}
4942
+ AND ${nonSystemMessageCondition(db, "target", "targetMember")}
4943
+ AND msg.sender_id = ?
4944
+ AND +target.sender_id = ?${timeFilter.sql}
4945
+ ORDER BY msg.ts DESC, msg.id DESC
4946
+ LIMIT 1`
4947
+ );
4948
+ const anchorsByPair = /* @__PURE__ */ new Map();
4949
+ for (const row of replyStatsRows) {
4950
+ const anchor = replyAnchorStatement.get(row.replySenderId, row.targetSenderId, ...timeFilter.params);
4951
+ if (!anchor) continue;
4952
+ const pairKey = contactPairKey(anchor.replySenderId, anchor.targetSenderId);
4953
+ const pairAnchors = anchorsByPair.get(pairKey) ?? [];
4954
+ pairAnchors.push({
4955
+ messageId: anchor.messageId,
4956
+ relatedMessageId: anchor.relatedMessageId,
4957
+ timestamp: anchor.replyTs,
4958
+ signal: "direct_reply",
4959
+ fromMemberId: anchor.replySenderId,
4960
+ toMemberId: anchor.targetSenderId
4961
+ });
4962
+ anchorsByPair.set(pairKey, pairAnchors);
4963
+ }
4964
+ for (const [pairKey, anchors] of anchorsByPair) {
4965
+ const pair = pairMap.get(pairKey);
4966
+ if (!pair) continue;
4967
+ anchors.sort((left, right) => right.timestamp - left.timestamp || right.messageId - left.messageId);
4968
+ pair.anchors.push(...anchors.slice(0, maxAnchorsPerPair));
4969
+ }
4970
+ }
4971
+ for (const [pairKey, directionCount] of replyDirectionCountsByPair) {
4972
+ const pair = pairMap.get(pairKey);
4973
+ if (pair && directionCount > maxAnchorsPerPair) pair.anchorsTruncated = true;
4974
+ }
4975
+ const maxProximityMessages = Math.max(0, Math.floor(options.maxProximityMessages ?? 2e5));
4976
+ let proximityStatus = "complete";
4977
+ if (maxProximityMessages === 0) {
4978
+ proximityStatus = "skipped_budget";
4979
+ for (const pair of pairMap.values()) {
4980
+ pair.coOccurrenceCount = null;
4981
+ pair.coOccurrenceRawScore = null;
4982
+ }
4983
+ } else {
4984
+ const proximityRows = db.prepare(
4985
+ `SELECT msg.id as messageId, msg.sender_id as senderId, msg.ts as ts
4986
+ FROM message msg
4987
+ JOIN member m ON msg.sender_id = m.id
4988
+ WHERE ${nonSystemMessageCondition(db, "msg", "m")}${timeFilter.sql}
4989
+ ORDER BY msg.ts ASC, msg.id ASC
4990
+ LIMIT ?`
4991
+ ).all(...timeFilter.params, maxProximityMessages + 1);
4992
+ if (proximityRows.length > maxProximityMessages) {
4993
+ proximityRows.length = maxProximityMessages;
4994
+ proximityStatus = "partial";
4995
+ }
4996
+ const selectedPairFacts = accumulateSelectedCoOccurrencePairs(
4997
+ proximityRows,
4998
+ [...pairMap.values()].map((pair) => [pair.sourceMemberId, pair.targetMemberId]),
4999
+ // 多保留一个候选,用于区分“命中上限”和“确实省略了证据”。
5000
+ { maxAnchorsPerPair: maxAnchorsPerPair + 1 }
5001
+ );
5002
+ for (const selected of selectedPairFacts) {
5003
+ const pair = pairMap.get(contactPairKey(selected.sourceId, selected.targetId));
5004
+ if (!pair) continue;
5005
+ pair.coOccurrenceCount = selected.coOccurrenceCount;
5006
+ pair.coOccurrenceRawScore = selected.rawScore;
5007
+ pair.lastProximityTs = selected.lastOccurrenceTs;
5008
+ for (const anchor of selected.anchors) {
5009
+ if (pair.anchors.length >= maxAnchorsPerPair) {
5010
+ pair.anchorsTruncated = true;
5011
+ break;
5012
+ }
5013
+ pair.anchors.push({
5014
+ messageId: anchor.messageId,
5015
+ relatedMessageId: anchor.relatedMessageId,
5016
+ timestamp: anchor.timestamp,
5017
+ signal: "proximity",
5018
+ fromMemberId: anchor.fromMemberId,
5019
+ toMemberId: anchor.toMemberId
5020
+ });
5021
+ }
5022
+ }
5023
+ if (proximityStatus === "partial") {
5024
+ for (const pair of pairMap.values()) {
5025
+ if (pair.coOccurrenceCount === 0) {
5026
+ pair.coOccurrenceCount = null;
5027
+ pair.coOccurrenceRawScore = null;
5028
+ pair.lastProximityTs = null;
5029
+ }
5030
+ }
5031
+ }
5032
+ }
5033
+ const activeDaySets = memberIds.map((memberId) => activeDaysByMemberId.get(memberId) ?? /* @__PURE__ */ new Set());
5034
+ const firstMessageTimestamps = participants.map((participant) => participant.firstMessageTs);
5035
+ const lastMessageTimestamps = participants.map((participant) => participant.lastMessageTs);
5036
+ const dataRange = db.prepare(
5037
+ `SELECT MIN(msg.ts) as firstMessageTs, MAX(msg.ts) as lastMessageTs
5038
+ FROM message msg
5039
+ JOIN member m ON msg.sender_id = m.id
5040
+ WHERE ${nonSystemMessageCondition(db, "msg", "m")}`
5041
+ ).get();
5042
+ return {
5043
+ participants,
5044
+ overlapRange: calculateOverlapRange(firstMessageTimestamps, lastMessageTimestamps),
5045
+ allParticipantsCoActiveDays: countMultipleSetIntersection(activeDaySets),
5046
+ pairs: [...pairMap.values()],
5047
+ proximityStatus,
5048
+ memberCount: getNonSystemMembersForContacts(db).length,
5049
+ sessionFirstMessageTs: dataRange?.firstMessageTs ?? null,
5050
+ sessionLastMessageTs: dataRange?.lastMessageTs ?? null
5051
+ };
5052
+ }
4567
5053
  function getPrivateContactFacts(db, ownerMemberId, options = {}) {
4568
5054
  const candidates = getNonSystemMembersForContacts(db).filter((member) => member.id !== ownerMemberId);
4569
5055
  if (candidates.length === 0) return { type: "missing" };
@@ -4799,9 +5285,52 @@ function getGroupRelationshipGraphFacts(db, ownerMemberId, options = {}) {
4799
5285
  function createMessageTimeFilter(alias, startTs) {
4800
5286
  return typeof startTs === "number" ? { sql: ` AND ${alias}.ts >= ?`, params: [startTs] } : { sql: "", params: [] };
4801
5287
  }
5288
+ function createBoundedMessageTimeFilter(alias, options) {
5289
+ const clauses = [];
5290
+ const params = [];
5291
+ if (typeof options.startTs === "number") {
5292
+ clauses.push(`${alias}.ts >= ?`);
5293
+ params.push(options.startTs);
5294
+ }
5295
+ if (typeof options.endTs === "number") {
5296
+ clauses.push(`${alias}.ts <= ?`);
5297
+ params.push(options.endTs);
5298
+ }
5299
+ return {
5300
+ sql: clauses.length > 0 ? ` AND ${clauses.join(" AND ")}` : "",
5301
+ params
5302
+ };
5303
+ }
4802
5304
  function createReplyTimeFilter(startTs) {
4803
5305
  return typeof startTs === "number" ? { sql: " AND msg.ts >= ? AND target.ts >= ?", params: [startTs, startTs] } : { sql: "", params: [] };
4804
5306
  }
5307
+ function contactPairKey(left, right) {
5308
+ return left < right ? `${left}:${right}` : `${right}:${left}`;
5309
+ }
5310
+ function countSetIntersection(left, right) {
5311
+ const smaller = left.size <= right.size ? left : right;
5312
+ const larger = smaller === left ? right : left;
5313
+ let count = 0;
5314
+ for (const value of smaller) {
5315
+ if (larger.has(value)) count++;
5316
+ }
5317
+ return count;
5318
+ }
5319
+ function countMultipleSetIntersection(sets) {
5320
+ if (sets.length === 0) return 0;
5321
+ return sets.slice(1).reduce((intersection, current) => {
5322
+ for (const value of intersection) {
5323
+ if (!current.has(value)) intersection.delete(value);
5324
+ }
5325
+ return intersection;
5326
+ }, new Set(sets[0])).size;
5327
+ }
5328
+ function calculateOverlapRange(firstTimestamps, lastTimestamps) {
5329
+ if (firstTimestamps.some((value) => value === null) || lastTimestamps.some((value) => value === null)) return null;
5330
+ const startTs = Math.max(...firstTimestamps);
5331
+ const endTs = Math.min(...lastTimestamps);
5332
+ return startTs <= endTs ? { startTs, endTs } : null;
5333
+ }
4805
5334
  function mapContactMemberRow(row) {
4806
5335
  return {
4807
5336
  id: row.id,
@@ -4938,6 +5467,83 @@ function buildContactKey(platform, platformId, sessionId) {
4938
5467
  return sessionId?.trim() ? `${normalizedPlatform}:${sessionId.trim()}:${normalizedPlatformId}` : `${normalizedPlatform}:${normalizedPlatformId}`;
4939
5468
  }
4940
5469
 
5470
+ // ../../packages/core/src/query/cross-chat-statistics.ts
5471
+ function getCrossChatSessionActivityFacts(db, options = {}) {
5472
+ const timeFilter = createTimeFilter("msg", options);
5473
+ const messageCondition = nonSystemMessageCondition(db, "msg", "m");
5474
+ const summary = db.prepare(
5475
+ `SELECT
5476
+ COUNT(*) as totalMessages,
5477
+ COUNT(DISTINCT strftime('%Y-%m-%d', msg.ts, 'unixepoch', 'localtime')) as activeDays,
5478
+ COUNT(DISTINCT msg.sender_id) as activeMembers,
5479
+ MIN(msg.ts) as firstMessageTs,
5480
+ MAX(msg.ts) as lastMessageTs
5481
+ FROM message msg
5482
+ JOIN member m ON msg.sender_id = m.id
5483
+ WHERE ${messageCondition}${timeFilter.sql}`
5484
+ ).get(...timeFilter.params);
5485
+ const memberRows = db.prepare(
5486
+ `SELECT
5487
+ msg.sender_id as memberId,
5488
+ m.platform_id as platformId,
5489
+ COALESCE(m.group_nickname, m.account_name, m.platform_id) as memberName,
5490
+ COUNT(*) as messageCount,
5491
+ COUNT(DISTINCT strftime('%Y-%m-%d', msg.ts, 'unixepoch', 'localtime')) as activeDays,
5492
+ GROUP_CONCAT(DISTINCT strftime('%Y-%m-%d', msg.ts, 'unixepoch', 'localtime')) as activeDayKeys,
5493
+ MIN(msg.ts) as firstMessageTs,
5494
+ MAX(msg.ts) as lastMessageTs
5495
+ FROM message msg
5496
+ JOIN member m ON msg.sender_id = m.id
5497
+ WHERE ${messageCondition}${timeFilter.sql}
5498
+ GROUP BY msg.sender_id, m.platform_id, m.group_nickname, m.account_name
5499
+ ORDER BY msg.sender_id`
5500
+ ).all(...timeFilter.params);
5501
+ const members = memberRows.map((row) => ({
5502
+ ...row,
5503
+ activeDayKeys: row.activeDayKeys ? row.activeDayKeys.split(",").sort() : []
5504
+ }));
5505
+ const dataRange = db.prepare(
5506
+ `SELECT MIN(msg.ts) as firstMessageTs, MAX(msg.ts) as lastMessageTs
5507
+ FROM message msg
5508
+ JOIN member m ON msg.sender_id = m.id
5509
+ WHERE ${messageCondition}`
5510
+ ).get();
5511
+ const activeDayKeys = db.prepare(
5512
+ `SELECT DISTINCT strftime('%Y-%m-%d', msg.ts, 'unixepoch', 'localtime') as activeDay
5513
+ FROM message msg
5514
+ JOIN member m ON msg.sender_id = m.id
5515
+ WHERE ${messageCondition}${timeFilter.sql}
5516
+ ORDER BY activeDay`
5517
+ ).all(...timeFilter.params).map((row) => row.activeDay);
5518
+ return {
5519
+ totalMessages: summary?.totalMessages ?? 0,
5520
+ activeDays: summary?.activeDays ?? 0,
5521
+ activeMembers: summary?.activeMembers ?? 0,
5522
+ firstMessageTs: summary?.firstMessageTs ?? null,
5523
+ lastMessageTs: summary?.lastMessageTs ?? null,
5524
+ dataEarliestMessageTs: dataRange?.firstMessageTs ?? null,
5525
+ dataLatestMessageTs: dataRange?.lastMessageTs ?? null,
5526
+ activeDayKeys,
5527
+ members
5528
+ };
5529
+ }
5530
+ function createTimeFilter(alias, options) {
5531
+ const clauses = [];
5532
+ const params = [];
5533
+ if (typeof options.startTs === "number") {
5534
+ clauses.push(`${alias}.ts >= ?`);
5535
+ params.push(options.startTs);
5536
+ }
5537
+ if (typeof options.endTs === "number") {
5538
+ clauses.push(`${alias}.ts <= ?`);
5539
+ params.push(options.endTs);
5540
+ }
5541
+ return {
5542
+ sql: clauses.length > 0 ? ` AND ${clauses.join(" AND ")}` : "",
5543
+ params
5544
+ };
5545
+ }
5546
+
4941
5547
  // ../../packages/core/src/query/global-insight.ts
4942
5548
  var SYSTEM_MESSAGE_TYPES2 = [80 /* SYSTEM */, 81 /* RECALL */];
4943
5549
  function getAnnualSummarySessionFacts(db, sessionId, range) {
@@ -5726,6 +6332,7 @@ function searchMessagesByKeywords(db, keywords, options) {
5726
6332
  startTs: options?.startTs,
5727
6333
  endTs: options?.endTs,
5728
6334
  senderId: options?.senderId,
6335
+ senderIds: options?.senderIds,
5729
6336
  keywords: cleaned.length > 0 ? cleaned : void 0,
5730
6337
  matchMode: options?.matchMode,
5731
6338
  excludeKeywords: options?.excludeKeywords,
@@ -5748,6 +6355,33 @@ function searchMessagesByKeywords(db, keywords, options) {
5748
6355
  });
5749
6356
  return { messages, hasMore, total: countRow.total };
5750
6357
  }
6358
+ function getRecentMessages(db, options) {
6359
+ const limit = options?.limit ?? 50;
6360
+ const rows = db.prepare(
6361
+ `SELECT
6362
+ msg.id as id,
6363
+ m.id as senderId,
6364
+ COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
6365
+ m.platform_id as senderPlatformId,
6366
+ msg.content as content,
6367
+ msg.ts as timestamp,
6368
+ msg.type as type
6369
+ FROM message msg
6370
+ JOIN member m ON msg.sender_id = m.id
6371
+ WHERE COALESCE(m.account_name, '') != '\u7CFB\u7EDF\u6D88\u606F'
6372
+ ORDER BY msg.ts DESC
6373
+ LIMIT ?`
6374
+ ).all(limit);
6375
+ return rows.map((row) => ({
6376
+ id: Number(row.id),
6377
+ senderId: Number(row.senderId),
6378
+ senderName: String(row.senderName || ""),
6379
+ senderPlatformId: String(row.senderPlatformId || ""),
6380
+ content: row.content != null ? String(row.content) : "",
6381
+ timestamp: Number(row.timestamp),
6382
+ type: Number(row.type)
6383
+ }));
6384
+ }
5751
6385
  function getMembers(db) {
5752
6386
  return db.prepare(
5753
6387
  `SELECT
@@ -6243,6 +6877,7 @@ var REPLY_WEIGHT = 3;
6243
6877
  var MENTION_WEIGHT = 2;
6244
6878
  var CO_OCCURRENCE_COUNT_WEIGHT = 0.05;
6245
6879
  var MIN_EDGE_WEIGHT = 1e-6;
6880
+ var MESSAGE_BATCH_SIZE = 2e4;
6246
6881
  var DEFAULT_LIMITS = {
6247
6882
  nodeLimit: 400,
6248
6883
  edgeLimit: 1200,
@@ -6264,14 +6899,16 @@ function getGroupRelationshipGalaxy(db, filter, options = {}) {
6264
6899
  const limits = normalizeLimits(options);
6265
6900
  const allMembers = getNonSystemMembersForContacts(db);
6266
6901
  const memberById = new Map(allMembers.map((member) => [member.id, member]));
6267
- const messageRows = queryRelationshipMessages(db, filter).filter((message) => memberById.has(message.senderId));
6268
- const activeMembers = buildActiveMembers(allMembers, messageRows);
6902
+ const messageStats = /* @__PURE__ */ new Map();
6903
+ const messageBatches = filterRelationshipMessageBatches(db, filter, memberById, messageStats);
6904
+ const coOccurrencePairs = accumulateCoOccurrencePairBatches(messageBatches);
6905
+ const activeMembers = buildActiveMembers(allMembers, messageStats);
6269
6906
  const activeMemberById = new Map(activeMembers.map((member) => [member.id, member]));
6270
6907
  if (activeMembers.length < 2) {
6271
6908
  return emptyResult(allMembers.length, activeMembers.length);
6272
6909
  }
6273
6910
  const edgeByPair = /* @__PURE__ */ new Map();
6274
- for (const pair of accumulateCoOccurrencePairs(messageRows)) {
6911
+ for (const pair of coOccurrencePairs) {
6275
6912
  if (!activeMemberById.has(pair.sourceId) || !activeMemberById.has(pair.targetId)) continue;
6276
6913
  const edge = ensureEdge(edgeByPair, activeMemberById, pair.sourceId, pair.targetId);
6277
6914
  edge.coOccurrenceCount += pair.coOccurrenceCount;
@@ -6389,19 +7026,44 @@ function getGroupRelationshipGalaxy(db, filter, options = {}) {
6389
7026
  algorithmVersion: GROUP_RELATIONSHIP_GALAXY_ALGORITHM_VERSION
6390
7027
  };
6391
7028
  }
6392
- function queryRelationshipMessages(db, filter) {
6393
- const { clause, params } = buildTimeFilter(filter, "msg");
6394
- const where = appendConditions(clause, ["msg.type NOT IN (80, 81)"]);
6395
- return db.prepare(`SELECT msg.sender_id as senderId, msg.ts FROM message msg ${where} ORDER BY msg.ts ASC, msg.id ASC`).all(...params);
7029
+ function* filterRelationshipMessageBatches(db, filter, memberById, messageStats) {
7030
+ for (const batch of queryRelationshipMessageBatches(db, filter)) {
7031
+ const filtered = [];
7032
+ for (const message of batch) {
7033
+ if (!memberById.has(message.senderId)) continue;
7034
+ const stats = messageStats.get(message.senderId) ?? { count: 0, lastTs: 0 };
7035
+ stats.count++;
7036
+ stats.lastTs = Math.max(stats.lastTs, message.ts);
7037
+ messageStats.set(message.senderId, stats);
7038
+ filtered.push(message);
7039
+ }
7040
+ yield filtered;
7041
+ }
6396
7042
  }
6397
- function buildActiveMembers(allMembers, messages) {
6398
- const messageStats = /* @__PURE__ */ new Map();
6399
- for (const message of messages) {
6400
- const stats = messageStats.get(message.senderId) ?? { count: 0, lastTs: 0 };
6401
- stats.count++;
6402
- stats.lastTs = Math.max(stats.lastTs, message.ts);
6403
- messageStats.set(message.senderId, stats);
7043
+ function* queryRelationshipMessageBatches(db, filter) {
7044
+ const { clause, params } = buildTimeFilter(filter, "msg");
7045
+ const conditions = ["msg.type NOT IN (80, 81)"];
7046
+ const firstPage = db.prepare(
7047
+ `SELECT msg.id, msg.sender_id as senderId, msg.ts
7048
+ FROM message msg ${appendConditions(clause, conditions)}
7049
+ ORDER BY msg.ts ASC, msg.id ASC
7050
+ LIMIT ?`
7051
+ );
7052
+ const nextPage = db.prepare(
7053
+ `SELECT msg.id, msg.sender_id as senderId, msg.ts
7054
+ FROM message msg ${appendConditions(clause, [...conditions, "(msg.ts, msg.id) > (?, ?)"])}
7055
+ ORDER BY msg.ts ASC, msg.id ASC
7056
+ LIMIT ?`
7057
+ );
7058
+ let rows = firstPage.all(...params, MESSAGE_BATCH_SIZE);
7059
+ while (rows.length > 0) {
7060
+ yield rows;
7061
+ if (rows.length < MESSAGE_BATCH_SIZE) break;
7062
+ const last = rows[rows.length - 1];
7063
+ rows = nextPage.all(...params, last.ts, last.id, MESSAGE_BATCH_SIZE);
6404
7064
  }
7065
+ }
7066
+ function buildActiveMembers(allMembers, messageStats) {
6405
7067
  const active = allMembers.filter((member) => messageStats.has(member.id)).map((member) => ({
6406
7068
  ...member,
6407
7069
  key: memberKey(member.id),
@@ -6900,8 +7562,8 @@ function getJourneyStats(db, filter) {
6900
7562
  const indexAvailable = hasSessionIndex(db) && hasTable(db, "message_context");
6901
7563
  const segmentRows = indexAvailable ? querySegmentRows(db, effectiveFilter) : [];
6902
7564
  const segmentCountByMonth = /* @__PURE__ */ new Map();
6903
- for (const segment of segmentRows) {
6904
- segmentCountByMonth.set(segment.startMonth, (segmentCountByMonth.get(segment.startMonth) ?? 0) + 1);
7565
+ for (const segment2 of segmentRows) {
7566
+ segmentCountByMonth.set(segment2.startMonth, (segmentCountByMonth.get(segment2.startMonth) ?? 0) + 1);
6905
7567
  }
6906
7568
  const firstRow = monthlyRows[0];
6907
7569
  const lastRow = monthlyRows[monthlyRows.length - 1];
@@ -9001,8 +9663,8 @@ var AssistantManager = class {
9001
9663
  }
9002
9664
  // ==================== Init ====================
9003
9665
  init() {
9004
- const { fs: fs18, assistantsDir } = this.deps;
9005
- fs18.ensureDir(assistantsDir);
9666
+ const { fs: fs20, assistantsDir } = this.deps;
9667
+ fs20.ensureDir(assistantsDir);
9006
9668
  const { generalCreated, generalUpdated } = this.syncGeneralAssistants();
9007
9669
  this.loadAll();
9008
9670
  this.initialized = true;
@@ -9014,25 +9676,25 @@ var AssistantManager = class {
9014
9676
  return { total: this.cache.size, generalCreated, generalUpdated };
9015
9677
  }
9016
9678
  syncGeneralAssistants() {
9017
- const { fs: fs18, assistantsDir } = this.deps;
9679
+ const { fs: fs20, assistantsDir } = this.deps;
9018
9680
  let generalCreated = false;
9019
9681
  let generalUpdated = false;
9020
9682
  for (const id of this.generalIds) {
9021
9683
  const config = this.getBuiltinConfig(id);
9022
9684
  if (!config) continue;
9023
- const filePath = fs18.joinPath(assistantsDir, `${id}.md`);
9024
- if (!fs18.fileExists(filePath)) {
9025
- fs18.writeFile(filePath, serializeAssistant(this.withBuiltinTracking(config)));
9685
+ const filePath = fs20.joinPath(assistantsDir, `${id}.md`);
9686
+ if (!fs20.fileExists(filePath)) {
9687
+ fs20.writeFile(filePath, serializeAssistant(this.withBuiltinTracking(config)));
9026
9688
  generalCreated = true;
9027
9689
  continue;
9028
9690
  }
9029
9691
  try {
9030
- const existing = parseAssistantFile(fs18.readFile(filePath), filePath);
9692
+ const existing = parseAssistantFile(fs20.readFile(filePath), filePath);
9031
9693
  if (!existing) continue;
9032
9694
  if (existing.builtinId !== id) {
9033
9695
  try {
9034
9696
  const backupId = this.backupAssistantOccupyingReservedId(existing);
9035
- fs18.writeFile(filePath, serializeAssistant(this.withBuiltinTracking(config)));
9697
+ fs20.writeFile(filePath, serializeAssistant(this.withBuiltinTracking(config)));
9036
9698
  generalCreated = true;
9037
9699
  this.deps.logger?.info("AssistantManager", "Backed up assistant occupying reserved default ID", {
9038
9700
  reservedId: id,
@@ -9048,7 +9710,7 @@ var AssistantManager = class {
9048
9710
  continue;
9049
9711
  }
9050
9712
  if (!this.shouldUpgradeBuiltin(existing, config)) continue;
9051
- fs18.writeFile(filePath, serializeAssistant(this.withBuiltinTracking(config, existing.id)));
9713
+ fs20.writeFile(filePath, serializeAssistant(this.withBuiltinTracking(config, existing.id)));
9052
9714
  generalUpdated = true;
9053
9715
  this.deps.logger?.info("AssistantManager", "Updated unmodified default assistant template", {
9054
9716
  id,
@@ -9064,14 +9726,14 @@ var AssistantManager = class {
9064
9726
  return { generalCreated, generalUpdated };
9065
9727
  }
9066
9728
  backupAssistantOccupyingReservedId(config) {
9067
- const { fs: fs18, assistantsDir } = this.deps;
9729
+ const { fs: fs20, assistantsDir } = this.deps;
9068
9730
  const baseId = this.deps.generateId?.() || `custom_${Date.now().toString(36)}`;
9069
9731
  let backupId = baseId;
9070
9732
  let suffix = 2;
9071
- while (this.generalIds.includes(backupId) || fs18.fileExists(fs18.joinPath(assistantsDir, `${backupId}.md`))) {
9733
+ while (this.generalIds.includes(backupId) || fs20.fileExists(fs20.joinPath(assistantsDir, `${backupId}.md`))) {
9072
9734
  backupId = `${baseId}_${suffix++}`;
9073
9735
  }
9074
- fs18.writeFile(fs18.joinPath(assistantsDir, `${backupId}.md`), serializeAssistant({ ...config, id: backupId }));
9736
+ fs20.writeFile(fs20.joinPath(assistantsDir, `${backupId}.md`), serializeAssistant({ ...config, id: backupId }));
9075
9737
  return backupId;
9076
9738
  }
9077
9739
  getConfigDigest(config) {
@@ -9108,13 +9770,13 @@ var AssistantManager = class {
9108
9770
  return { isUnmodified, isOutdated };
9109
9771
  }
9110
9772
  loadAll() {
9111
- const { fs: fs18, assistantsDir } = this.deps;
9773
+ const { fs: fs20, assistantsDir } = this.deps;
9112
9774
  this.cache.clear();
9113
- const files = fs18.listFiles(assistantsDir, ".md");
9775
+ const files = fs20.listFiles(assistantsDir, ".md");
9114
9776
  for (const file of files) {
9115
9777
  try {
9116
- const filePath = fs18.joinPath(assistantsDir, file);
9117
- const content = fs18.readFile(filePath);
9778
+ const filePath = fs20.joinPath(assistantsDir, file);
9779
+ const content = fs20.readFile(filePath);
9118
9780
  const config = parseAssistantFile(content, filePath);
9119
9781
  if (config) {
9120
9782
  this.cache.set(config.id, config);
@@ -9368,6 +10030,7 @@ var AIChatManager = class {
9368
10030
  CREATE TABLE IF NOT EXISTS ai_chat (
9369
10031
  id TEXT PRIMARY KEY,
9370
10032
  session_id TEXT NOT NULL,
10033
+ kind TEXT NOT NULL DEFAULT 'session',
9371
10034
  title TEXT,
9372
10035
  assistant_id TEXT DEFAULT '${DEFAULT_GENERAL_ID}',
9373
10036
  active_message_id TEXT,
@@ -9389,6 +10052,7 @@ var AIChatManager = class {
9389
10052
  branch_index INTEGER DEFAULT 0,
9390
10053
  debug_context TEXT,
9391
10054
  token_usage TEXT,
10055
+ entity_refs TEXT,
9392
10056
  FOREIGN KEY(ai_chat_id) REFERENCES ai_chat(id) ON DELETE CASCADE
9393
10057
  );
9394
10058
 
@@ -9424,9 +10088,19 @@ var AIChatManager = class {
9424
10088
  if (!messageColumns.includes("branch_index")) {
9425
10089
  db.exec(`ALTER TABLE ${tableName} ADD COLUMN branch_index INTEGER DEFAULT 0`);
9426
10090
  }
10091
+ if (!messageColumns.includes("entity_refs")) {
10092
+ db.exec(`ALTER TABLE ${tableName} ADD COLUMN entity_refs TEXT`);
10093
+ }
10094
+ }
10095
+ ensureChatMigrationColumns(db) {
10096
+ const chatColumns = this.getTableColumns(db, "ai_chat");
10097
+ if (!chatColumns.includes("kind")) {
10098
+ db.exec(`ALTER TABLE ai_chat ADD COLUMN kind TEXT NOT NULL DEFAULT 'session'`);
10099
+ }
9427
10100
  }
9428
10101
  migrateDatabase(db) {
9429
10102
  try {
10103
+ this.ensureChatMigrationColumns(db);
9430
10104
  const hasLegacyConversationTable = this.tableExists(db, "ai_conversation");
9431
10105
  if (hasLegacyConversationTable) {
9432
10106
  const convColumns = this.getTableColumns(db, "ai_conversation");
@@ -9468,15 +10142,16 @@ var AIChatManager = class {
9468
10142
  branch_index INTEGER DEFAULT 0,
9469
10143
  debug_context TEXT,
9470
10144
  token_usage TEXT,
10145
+ entity_refs TEXT,
9471
10146
  FOREIGN KEY(ai_chat_id) REFERENCES ai_chat(id) ON DELETE CASCADE
9472
10147
  );
9473
10148
 
9474
10149
  INSERT INTO ai_message (
9475
10150
  id, ai_chat_id, role, content, timestamp, data_keywords, data_message_count,
9476
- content_blocks, parent_id, sibling_group_id, branch_index, debug_context, token_usage
10151
+ content_blocks, parent_id, sibling_group_id, branch_index, debug_context, token_usage, entity_refs
9477
10152
  )
9478
10153
  SELECT id, conversation_id, role, content, timestamp, data_keywords, data_message_count,
9479
- content_blocks, parent_id, sibling_group_id, branch_index, debug_context, token_usage
10154
+ content_blocks, parent_id, sibling_group_id, branch_index, debug_context, token_usage, entity_refs
9480
10155
  FROM ai_message_legacy;
9481
10156
 
9482
10157
  DROP TABLE ai_message_legacy;
@@ -9561,7 +10236,8 @@ var AIChatManager = class {
9561
10236
  dataKeywords: row.dataKeywords ? JSON.parse(row.dataKeywords) : void 0,
9562
10237
  dataMessageCount: row.dataMessageCount ?? void 0,
9563
10238
  contentBlocks: row.contentBlocks ? JSON.parse(row.contentBlocks) : void 0,
9564
- tokenUsage: row.tokenUsage ? JSON.parse(row.tokenUsage) : void 0
10239
+ tokenUsage: row.tokenUsage ? JSON.parse(row.tokenUsage) : void 0,
10240
+ entityRefs: row.entityRefs ? JSON.parse(row.entityRefs) : void 0
9565
10241
  };
9566
10242
  }
9567
10243
  getMessageRow(messageId) {
@@ -9570,7 +10246,7 @@ var AIChatManager = class {
9570
10246
  `SELECT id, ai_chat_id as aiChatId, role, content, timestamp,
9571
10247
  parent_id as parentId, sibling_group_id as siblingGroupId, branch_index as branchIndex,
9572
10248
  data_keywords as dataKeywords, data_message_count as dataMessageCount,
9573
- content_blocks as contentBlocks, token_usage as tokenUsage
10249
+ content_blocks as contentBlocks, token_usage as tokenUsage, entity_refs as entityRefs
9574
10250
  FROM ai_message WHERE id = ?`
9575
10251
  ).get(messageId);
9576
10252
  return row ?? null;
@@ -9594,7 +10270,7 @@ var AIChatManager = class {
9594
10270
  `SELECT id, ai_chat_id as aiChatId, role, content, timestamp,
9595
10271
  parent_id as parentId, sibling_group_id as siblingGroupId, branch_index as branchIndex,
9596
10272
  data_keywords as dataKeywords, data_message_count as dataMessageCount,
9597
- content_blocks as contentBlocks, token_usage as tokenUsage
10273
+ content_blocks as contentBlocks, token_usage as tokenUsage, entity_refs as entityRefs
9598
10274
  FROM ai_message WHERE ai_chat_id = ? ORDER BY timestamp ASC, id ASC`
9599
10275
  ).all(aiChatId);
9600
10276
  }
@@ -9604,16 +10280,16 @@ var AIChatManager = class {
9604
10280
  if (allRows.length === 0) return [];
9605
10281
  const rowMap = new Map(allRows.map((row) => [row.id, row]));
9606
10282
  let currentId = leafMessageId ?? this.getActiveMessageId(aiChatId);
9607
- const path29 = [];
10283
+ const path31 = [];
9608
10284
  const seen = /* @__PURE__ */ new Set();
9609
10285
  while (currentId && !seen.has(currentId)) {
9610
10286
  seen.add(currentId);
9611
10287
  const row = rowMap.get(currentId);
9612
10288
  if (!row) break;
9613
- path29.push(row);
10289
+ path31.push(row);
9614
10290
  currentId = row.parentId;
9615
10291
  }
9616
- return path29.length > 0 ? path29.reverse() : allRows;
10292
+ return path31.length > 0 ? path31.reverse() : allRows;
9617
10293
  }
9618
10294
  // ==================== 生命周期 ====================
9619
10295
  close() {
@@ -9670,20 +10346,35 @@ var AIChatManager = class {
9670
10346
  }
9671
10347
  // ==================== 对话管理 ====================
9672
10348
  createAIChat(sessionId, title, assistantId) {
10349
+ return this.createChat("session", sessionId, title, assistantId);
10350
+ }
10351
+ createGlobalAIChat(title, assistantId) {
10352
+ return this.createChat("global", "", title, assistantId);
10353
+ }
10354
+ createChat(kind, sessionId, title, assistantId) {
9673
10355
  const db = this.getDb();
9674
10356
  const now = Math.floor(Date.now() / 1e3);
9675
10357
  const id = this.generateId("conv");
9676
10358
  db.prepare(
9677
- `INSERT INTO ai_chat (id, session_id, title, assistant_id, created_at, updated_at)
9678
- VALUES (?, ?, ?, ?, ?, ?)`
9679
- ).run(id, sessionId, title || null, assistantId, now, now);
9680
- return { id, sessionId, title: title || null, assistantId, activeMessageId: null, createdAt: now, updatedAt: now };
10359
+ `INSERT INTO ai_chat (id, session_id, kind, title, assistant_id, created_at, updated_at)
10360
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
10361
+ ).run(id, sessionId, kind, title || null, assistantId, now, now);
10362
+ return {
10363
+ id,
10364
+ sessionId,
10365
+ kind,
10366
+ title: title || null,
10367
+ assistantId,
10368
+ activeMessageId: null,
10369
+ createdAt: now,
10370
+ updatedAt: now
10371
+ };
9681
10372
  }
9682
10373
  getAIChatCountsBySession() {
9683
10374
  const result = /* @__PURE__ */ new Map();
9684
10375
  try {
9685
10376
  const db = this.getDb();
9686
- const rows = db.prepare("SELECT session_id, COUNT(*) as count FROM ai_chat GROUP BY session_id").all();
10377
+ const rows = db.prepare("SELECT session_id, COUNT(*) as count FROM ai_chat WHERE kind = 'session' GROUP BY session_id").all();
9687
10378
  for (const row of rows) {
9688
10379
  result.set(row.session_id, row.count);
9689
10380
  }
@@ -9694,16 +10385,24 @@ var AIChatManager = class {
9694
10385
  getAIChats(sessionId) {
9695
10386
  const db = this.getDb();
9696
10387
  return db.prepare(
9697
- `SELECT id, session_id as sessionId, title, assistant_id as assistantId,
10388
+ `SELECT id, session_id as sessionId, kind, title, assistant_id as assistantId,
9698
10389
  active_message_id as activeMessageId,
9699
10390
  created_at as createdAt, updated_at as updatedAt
9700
- FROM ai_chat WHERE session_id = ? ORDER BY updated_at DESC`
10391
+ FROM ai_chat WHERE kind = 'session' AND session_id = ? ORDER BY updated_at DESC`
9701
10392
  ).all(sessionId);
9702
10393
  }
10394
+ getGlobalAIChats() {
10395
+ return this.getDb().prepare(
10396
+ `SELECT id, session_id as sessionId, kind, title, assistant_id as assistantId,
10397
+ active_message_id as activeMessageId,
10398
+ created_at as createdAt, updated_at as updatedAt
10399
+ FROM ai_chat WHERE kind = 'global' ORDER BY updated_at DESC`
10400
+ ).all();
10401
+ }
9703
10402
  getAIChat(aiChatId) {
9704
10403
  const db = this.getDb();
9705
10404
  const row = db.prepare(
9706
- `SELECT id, session_id as sessionId, title, assistant_id as assistantId,
10405
+ `SELECT id, session_id as sessionId, kind, title, assistant_id as assistantId,
9707
10406
  active_message_id as activeMessageId,
9708
10407
  created_at as createdAt, updated_at as updatedAt
9709
10408
  FROM ai_chat WHERE id = ?`
@@ -9723,13 +10422,79 @@ var AIChatManager = class {
9723
10422
  return result.changes > 0;
9724
10423
  }
9725
10424
  // ==================== 消息管理 ====================
9726
- addMessage(aiChatId, role, content, dataKeywords, dataMessageCount, contentBlocks, tokenUsage) {
10425
+ addMessage(aiChatId, role, content, dataKeywords, dataMessageCount, contentBlocks, tokenUsage, entityRefs) {
10426
+ return this.addMessageWithEntityRefs(
10427
+ aiChatId,
10428
+ role,
10429
+ content,
10430
+ dataKeywords,
10431
+ dataMessageCount,
10432
+ contentBlocks,
10433
+ tokenUsage,
10434
+ role === "user" ? entityRefs : void 0
10435
+ );
10436
+ }
10437
+ addMessagePair(aiChatId, userMessage, assistantMessage) {
10438
+ const db = this.getDb();
10439
+ return db.transaction(() => ({
10440
+ userMessage: this.addMessage(
10441
+ aiChatId,
10442
+ "user",
10443
+ userMessage.content,
10444
+ void 0,
10445
+ void 0,
10446
+ void 0,
10447
+ void 0,
10448
+ userMessage.entityRefs
10449
+ ),
10450
+ assistantMessage: this.addMessage(
10451
+ aiChatId,
10452
+ "assistant",
10453
+ assistantMessage.content,
10454
+ void 0,
10455
+ void 0,
10456
+ assistantMessage.contentBlocks,
10457
+ assistantMessage.tokenUsage
10458
+ )
10459
+ }))();
10460
+ }
10461
+ /** Atomically replaces the latest user/assistant round on the active path. */
10462
+ replaceLatestMessageRound(aiChatId, input) {
10463
+ const db = this.getDb();
10464
+ return db.transaction(() => {
10465
+ const activePath = this.getActivePathRows(aiChatId);
10466
+ const latestUserIndex = activePath.findLastIndex((message) => message.role === "user");
10467
+ const userMessage = activePath[latestUserIndex];
10468
+ if (!userMessage || userMessage.id !== input.userMessageId) {
10469
+ throw new Error("Only the latest user message can be edited");
10470
+ }
10471
+ const followingMessages = activePath.slice(latestUserIndex + 1);
10472
+ if (followingMessages.length > 1 || followingMessages.length === 1 && (followingMessages[0]?.role !== "assistant" || followingMessages[0].parentId !== input.userMessageId)) {
10473
+ throw new Error("Latest user message is not a replaceable round");
10474
+ }
10475
+ const oldAssistantMessage = followingMessages[0];
10476
+ this.updateMessageContent(input.userMessageId, input.userContent);
10477
+ if (oldAssistantMessage) {
10478
+ this.deleteAndRelinkMessage(aiChatId, oldAssistantMessage.id);
10479
+ }
10480
+ return this.insertMessageAfter(
10481
+ aiChatId,
10482
+ input.userMessageId,
10483
+ "assistant",
10484
+ input.assistantMessage.content,
10485
+ input.assistantMessage.contentBlocks,
10486
+ input.assistantMessage.tokenUsage
10487
+ );
10488
+ })();
10489
+ }
10490
+ addMessageWithEntityRefs(aiChatId, role, content, dataKeywords, dataMessageCount, contentBlocks, tokenUsage, entityRefs) {
9727
10491
  const db = this.getDb();
9728
10492
  const now = Math.floor(Date.now() / 1e3);
9729
10493
  const id = this.generateId("msg");
9730
10494
  const parentId = this.getActiveMessageId(aiChatId);
9731
10495
  const siblingGroupId = id;
9732
10496
  const branchIndex = 0;
10497
+ const persistedEntityRefs = entityRefs?.length ? entityRefs : void 0;
9733
10498
  const pendingDebug = role === "assistant" ? this.pendingDebugContextMap.get(aiChatId) : void 0;
9734
10499
  if (pendingDebug) {
9735
10500
  this.pendingDebugContextMap.delete(aiChatId);
@@ -9737,9 +10502,9 @@ var AIChatManager = class {
9737
10502
  db.prepare(
9738
10503
  `INSERT INTO ai_message (
9739
10504
  id, ai_chat_id, role, content, timestamp, data_keywords, data_message_count,
9740
- content_blocks, token_usage, debug_context, parent_id, sibling_group_id, branch_index
10505
+ content_blocks, token_usage, debug_context, parent_id, sibling_group_id, branch_index, entity_refs
9741
10506
  )
9742
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
10507
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
9743
10508
  ).run(
9744
10509
  id,
9745
10510
  aiChatId,
@@ -9753,7 +10518,8 @@ var AIChatManager = class {
9753
10518
  pendingDebug ?? null,
9754
10519
  parentId,
9755
10520
  siblingGroupId,
9756
- branchIndex
10521
+ branchIndex,
10522
+ persistedEntityRefs?.length ? JSON.stringify(persistedEntityRefs) : null
9757
10523
  );
9758
10524
  db.prepare("UPDATE ai_chat SET active_message_id = ?, updated_at = ? WHERE id = ?").run(id, now, aiChatId);
9759
10525
  return {
@@ -9766,12 +10532,17 @@ var AIChatManager = class {
9766
10532
  dataKeywords,
9767
10533
  dataMessageCount,
9768
10534
  contentBlocks,
9769
- tokenUsage
10535
+ tokenUsage,
10536
+ entityRefs: persistedEntityRefs?.length ? persistedEntityRefs : void 0
9770
10537
  };
9771
10538
  }
9772
10539
  getMessages(aiChatId) {
9773
10540
  return this.getActivePathRows(aiChatId).map((row) => this.parseMessageRow(row));
9774
10541
  }
10542
+ getMessage(messageId) {
10543
+ const row = this.getMessageRow(messageId);
10544
+ return row ? this.parseMessageRow(row) : null;
10545
+ }
9775
10546
  deleteMessage(messageId) {
9776
10547
  const db = this.getDb();
9777
10548
  const result = db.prepare("DELETE FROM ai_message WHERE id = ?").run(messageId);
@@ -9811,9 +10582,9 @@ var AIChatManager = class {
9811
10582
  const newConvId = this.generateId("conv");
9812
10583
  const forkTitle = title || `${source.title || "Untitled"} (fork)`;
9813
10584
  db.prepare(
9814
- `INSERT INTO ai_chat (id, session_id, title, assistant_id, active_message_id, created_at, updated_at)
9815
- VALUES (?, ?, ?, ?, NULL, ?, ?)`
9816
- ).run(newConvId, source.sessionId, forkTitle, source.assistantId, now, now);
10585
+ `INSERT INTO ai_chat (id, session_id, kind, title, assistant_id, active_message_id, created_at, updated_at)
10586
+ VALUES (?, ?, ?, ?, ?, NULL, ?, ?)`
10587
+ ).run(newConvId, source.sessionId, source.kind, forkTitle, source.assistantId, now, now);
9817
10588
  const idMap = /* @__PURE__ */ new Map();
9818
10589
  let lastNewId = null;
9819
10590
  for (const row of messagesToCopy) {
@@ -9823,9 +10594,9 @@ var AIChatManager = class {
9823
10594
  db.prepare(
9824
10595
  `INSERT INTO ai_message (
9825
10596
  id, ai_chat_id, role, content, timestamp, data_keywords, data_message_count,
9826
- content_blocks, token_usage, debug_context, parent_id, sibling_group_id, branch_index
10597
+ content_blocks, token_usage, debug_context, parent_id, sibling_group_id, branch_index, entity_refs
9827
10598
  )
9828
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, 0)`
10599
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, 0, ?)`
9829
10600
  ).run(
9830
10601
  newMsgId,
9831
10602
  newConvId,
@@ -9837,7 +10608,8 @@ var AIChatManager = class {
9837
10608
  row.contentBlocks,
9838
10609
  row.tokenUsage,
9839
10610
  newParentId,
9840
- newMsgId
10611
+ newMsgId,
10612
+ row.role === "user" || row.role === "summary" ? row.entityRefs : null
9841
10613
  );
9842
10614
  lastNewId = newMsgId;
9843
10615
  }
@@ -9847,6 +10619,7 @@ var AIChatManager = class {
9847
10619
  return {
9848
10620
  id: newConvId,
9849
10621
  sessionId: source.sessionId,
10622
+ kind: source.kind,
9850
10623
  title: forkTitle,
9851
10624
  assistantId: source.assistantId,
9852
10625
  activeMessageId: lastNewId,
@@ -9854,9 +10627,15 @@ var AIChatManager = class {
9854
10627
  updatedAt: now
9855
10628
  };
9856
10629
  }
9857
- updateMessageContent(messageId, newContent) {
10630
+ /** Replace message text and user entity references atomically; omitted references clear the previous value. */
10631
+ updateMessageContent(messageId, newContent, entityRefs) {
9858
10632
  const db = this.getDb();
9859
- const result = db.prepare("UPDATE ai_message SET content = ? WHERE id = ?").run(newContent, messageId);
10633
+ const serializedEntityRefs = entityRefs?.length ? JSON.stringify(entityRefs) : null;
10634
+ const result = db.prepare(
10635
+ `UPDATE ai_message
10636
+ SET content = ?, entity_refs = CASE WHEN role = 'user' THEN ? ELSE NULL END
10637
+ WHERE id = ?`
10638
+ ).run(newContent, serializedEntityRefs, messageId);
9860
10639
  if (result.changes === 0) throw new Error("Message not found");
9861
10640
  }
9862
10641
  deleteAndRelinkMessage(aiChatId, messageId) {
@@ -9881,10 +10660,11 @@ var AIChatManager = class {
9881
10660
  }
9882
10661
  db.prepare("DELETE FROM ai_message WHERE id = ?").run(messageId);
9883
10662
  }
9884
- insertMessageAfter(aiChatId, afterMessageId, role, content, contentBlocks, tokenUsage) {
10663
+ insertMessageAfter(aiChatId, afterMessageId, role, content, contentBlocks, tokenUsage, entityRefs) {
9885
10664
  const db = this.getDb();
9886
10665
  const now = Math.floor(Date.now() / 1e3);
9887
10666
  const id = this.generateId("msg");
10667
+ const persistedEntityRefs = role === "user" ? entityRefs : void 0;
9888
10668
  const pendingDebug = role === "assistant" ? this.pendingDebugContextMap.get(aiChatId) : void 0;
9889
10669
  if (pendingDebug) {
9890
10670
  this.pendingDebugContextMap.delete(aiChatId);
@@ -9893,9 +10673,9 @@ var AIChatManager = class {
9893
10673
  db.prepare(
9894
10674
  `INSERT INTO ai_message (
9895
10675
  id, ai_chat_id, role, content, timestamp, data_keywords, data_message_count,
9896
- content_blocks, token_usage, debug_context, parent_id, sibling_group_id, branch_index
10676
+ content_blocks, token_usage, debug_context, parent_id, sibling_group_id, branch_index, entity_refs
9897
10677
  )
9898
- VALUES (?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?, 0)`
10678
+ VALUES (?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?, 0, ?)`
9899
10679
  ).run(
9900
10680
  id,
9901
10681
  aiChatId,
@@ -9906,7 +10686,8 @@ var AIChatManager = class {
9906
10686
  tokenUsage ? JSON.stringify(tokenUsage) : null,
9907
10687
  pendingDebug ?? null,
9908
10688
  afterMessageId,
9909
- id
10689
+ id,
10690
+ persistedEntityRefs?.length ? JSON.stringify(persistedEntityRefs) : null
9910
10691
  );
9911
10692
  if (childRow) {
9912
10693
  db.prepare("UPDATE ai_message SET parent_id = ? WHERE id = ?").run(id, childRow.id);
@@ -9922,7 +10703,8 @@ var AIChatManager = class {
9922
10703
  timestamp: now,
9923
10704
  parentId: afterMessageId,
9924
10705
  contentBlocks,
9925
- tokenUsage
10706
+ tokenUsage,
10707
+ entityRefs: persistedEntityRefs?.length ? persistedEntityRefs : void 0
9926
10708
  };
9927
10709
  }
9928
10710
  getAIChatTokenUsage(aiChatId) {
@@ -9986,11 +10768,26 @@ var AIChatManager = class {
9986
10768
  }
9987
10769
  const contextMessages = bufferBoundary ? validMessages.filter((m) => m.role !== "summary" && m.timestamp >= bufferBoundary) : [];
9988
10770
  result = [
9989
- { role: "summary", content: summaryMsg.content },
9990
- ...contextMessages.map((m) => ({ role: m.role, content: m.content, contentBlocks: m.contentBlocks }))
10771
+ {
10772
+ role: "summary",
10773
+ content: summaryMsg.content,
10774
+ contentBlocks: summaryMsg.contentBlocks,
10775
+ entityRefs: summaryMsg.entityRefs
10776
+ },
10777
+ ...contextMessages.map((m) => ({
10778
+ role: m.role,
10779
+ content: m.content,
10780
+ contentBlocks: m.contentBlocks,
10781
+ entityRefs: m.role === "user" ? m.entityRefs : void 0
10782
+ }))
9991
10783
  ];
9992
10784
  } else {
9993
- result = validMessages.map((m) => ({ role: m.role, content: m.content, contentBlocks: m.contentBlocks }));
10785
+ result = validMessages.map((m) => ({
10786
+ role: m.role,
10787
+ content: m.content,
10788
+ contentBlocks: m.contentBlocks,
10789
+ entityRefs: m.role === "user" ? m.entityRefs : void 0
10790
+ }));
9994
10791
  }
9995
10792
  if (maxMessages && result.length > maxMessages) {
9996
10793
  if (result.length > 0 && result[0].role === "summary") {
@@ -10003,7 +10800,7 @@ var AIChatManager = class {
10003
10800
  return result;
10004
10801
  }
10005
10802
  // ==================== Summary / 压缩专用 ====================
10006
- addSummaryMessage(aiChatId, content, meta) {
10803
+ addSummaryMessage(aiChatId, content, meta, entityRefs) {
10007
10804
  const contentBlocks = [
10008
10805
  {
10009
10806
  type: "summary_meta",
@@ -10011,7 +10808,16 @@ var AIChatManager = class {
10011
10808
  compressedMessageCount: meta.compressedMessageCount
10012
10809
  }
10013
10810
  ];
10014
- return this.addMessage(aiChatId, "summary", content, void 0, void 0, contentBlocks);
10811
+ return this.addMessageWithEntityRefs(
10812
+ aiChatId,
10813
+ "summary",
10814
+ content,
10815
+ void 0,
10816
+ void 0,
10817
+ contentBlocks,
10818
+ void 0,
10819
+ entityRefs
10820
+ );
10015
10821
  }
10016
10822
  getLatestSummary(aiChatId) {
10017
10823
  const row = [...this.getActivePathRows(aiChatId)].reverse().find((message) => message.role === "summary");
@@ -10028,7 +10834,8 @@ var AIChatManager = class {
10028
10834
  role: row.role,
10029
10835
  content: row.content,
10030
10836
  timestamp: row.timestamp,
10031
- contentBlocks: row.contentBlocks ? JSON.parse(row.contentBlocks) : void 0
10837
+ contentBlocks: row.contentBlocks ? JSON.parse(row.contentBlocks) : void 0,
10838
+ entityRefs: row.role === "user" && row.entityRefs ? JSON.parse(row.entityRefs) : void 0
10032
10839
  };
10033
10840
  }
10034
10841
  getMessageCountAfterSummary(aiChatId) {
@@ -10046,6 +10853,747 @@ var AIChatManager = class {
10046
10853
  }
10047
10854
  };
10048
10855
 
10856
+ // ../../packages/node-runtime/src/nlp/segmenter.ts
10857
+ import * as fs9 from "fs";
10858
+ import * as path16 from "path";
10859
+ import { createRequire } from "module";
10860
+ var _nlpDir = null;
10861
+ var jiebaInstances = /* @__PURE__ */ new Map();
10862
+ var require2 = createRequire(import.meta.url);
10863
+ function initNlpDir(nlpDir) {
10864
+ _nlpDir = nlpDir;
10865
+ }
10866
+ function getNlpDir() {
10867
+ return _nlpDir;
10868
+ }
10869
+ function existsDictOnDisk(dictId) {
10870
+ if (!_nlpDir) return false;
10871
+ return fs9.existsSync(path16.join(_nlpDir, `${dictId}.dict`));
10872
+ }
10873
+ function tryLoadDictFromDisk(dictId) {
10874
+ if (!_nlpDir) return null;
10875
+ const dictPath = path16.join(_nlpDir, `${dictId}.dict`);
10876
+ if (!fs9.existsSync(dictPath)) return null;
10877
+ try {
10878
+ return fs9.readFileSync(dictPath);
10879
+ } catch {
10880
+ return null;
10881
+ }
10882
+ }
10883
+ function getJieba(dictType = "default") {
10884
+ const effectiveType = dictType === "default" ? "zh-CN" : dictType;
10885
+ const cached = jiebaInstances.get(effectiveType);
10886
+ if (cached) {
10887
+ if (cached.fromDiskDict === existsDictOnDisk(effectiveType)) return cached.instance;
10888
+ jiebaInstances.delete(effectiveType);
10889
+ console.error(`[NLP] jieba cache invalidated (dict ${cached.fromDiskDict ? "removed" : "added"}): ${effectiveType}`);
10890
+ }
10891
+ try {
10892
+ const { Jieba } = require2("@node-rs/jieba");
10893
+ let instance2;
10894
+ const diskDict = tryLoadDictFromDisk(effectiveType);
10895
+ if (diskDict) {
10896
+ instance2 = Jieba.withDict(diskDict);
10897
+ console.error(`[NLP] jieba dict loaded: ${effectiveType} (${diskDict.length} bytes)`);
10898
+ } else {
10899
+ instance2 = new Jieba();
10900
+ console.warn(`[NLP] jieba dict missing: ${effectiveType}, fallback to built-in tokenizer`);
10901
+ }
10902
+ jiebaInstances.set(effectiveType, { instance: instance2, fromDiskDict: !!diskDict });
10903
+ return instance2;
10904
+ } catch (error) {
10905
+ console.error(`[NLP] Failed to load jieba module (dict=${effectiveType}):`, error);
10906
+ throw new Error(`jieba \u6A21\u5757\u52A0\u8F7D\u5931\u8D25 (${effectiveType})`);
10907
+ }
10908
+ }
10909
+ function clearJiebaInstance(dictType) {
10910
+ jiebaInstances.delete(dictType);
10911
+ console.error(`[NLP] jieba instance cleared: ${dictType}`);
10912
+ }
10913
+ function segmentChinese(text, options = {}) {
10914
+ const { posFilterMode = "meaningful", customPosTags, dictType = "default" } = options;
10915
+ const cleaned = cleanText(text);
10916
+ if (!cleaned) return [];
10917
+ try {
10918
+ const jieba = getJieba(dictType);
10919
+ if (posFilterMode === "all") return jieba.cut(cleaned, false);
10920
+ const tagged = jieba.tag(cleaned);
10921
+ const allowedTags = posFilterMode === "custom" && customPosTags ? new Set(customPosTags) : MEANINGFUL_POS_TAGS;
10922
+ return tagged.filter((item) => allowedTags.has(item.tag)).map((item) => item.word);
10923
+ } catch (error) {
10924
+ console.error("[NLP] Chinese segmentation failed:", error);
10925
+ try {
10926
+ return getJieba("default").cut(cleanText(text), false);
10927
+ } catch {
10928
+ return cleaned.split("");
10929
+ }
10930
+ }
10931
+ }
10932
+ function segmentEnglish(text) {
10933
+ const cleaned = cleanText(text);
10934
+ if (!cleaned) return [];
10935
+ try {
10936
+ const segmenter = new Intl.Segmenter("en", { granularity: "word" });
10937
+ return [...segmenter.segment(cleaned)].filter((s) => s.isWordLike).map((s) => s.segment.toLowerCase());
10938
+ } catch {
10939
+ return cleaned.toLowerCase().split(/\s+/).filter((w) => w.length > 0);
10940
+ }
10941
+ }
10942
+ function segmentJapanese(text) {
10943
+ const cleaned = cleanText(text);
10944
+ if (!cleaned) return [];
10945
+ try {
10946
+ const segmenter = new Intl.Segmenter("ja", { granularity: "word" });
10947
+ return [...segmenter.segment(cleaned)].filter((s) => s.isWordLike).map((s) => s.segment);
10948
+ } catch {
10949
+ return cleaned.split("").filter((ch) => ch.trim().length > 0);
10950
+ }
10951
+ }
10952
+ function segment(text, locale, options = {}) {
10953
+ const {
10954
+ minLength,
10955
+ posFilterMode = "meaningful",
10956
+ customPosTags,
10957
+ enableStopwords = true,
10958
+ dictType = "default"
10959
+ } = options;
10960
+ const isChinese2 = locale.startsWith("zh");
10961
+ const isJapanese = locale === "ja-JP";
10962
+ const defaultMinLength = isChinese2 || isJapanese ? 2 : 3;
10963
+ const effectiveMinLength = minLength ?? defaultMinLength;
10964
+ let words;
10965
+ if (isChinese2) {
10966
+ words = segmentChinese(text, { posFilterMode, customPosTags, dictType });
10967
+ } else if (isJapanese) {
10968
+ words = segmentJapanese(text);
10969
+ } else {
10970
+ words = segmentEnglish(text);
10971
+ }
10972
+ return words.filter((word) => isValidWord(word, locale, effectiveMinLength, enableStopwords, isStopword));
10973
+ }
10974
+ function batchSegmentWithFrequency(texts, locale, options = {}) {
10975
+ const {
10976
+ minLength,
10977
+ minCount = 2,
10978
+ topN = 100,
10979
+ posFilterMode,
10980
+ customPosTags,
10981
+ enableStopwords,
10982
+ dictType,
10983
+ excludeWords
10984
+ } = options;
10985
+ const wordFrequency = /* @__PURE__ */ new Map();
10986
+ const excludeSet = excludeWords?.length ? new Set(excludeWords.map((w) => w.toLowerCase())) : null;
10987
+ for (const text of texts) {
10988
+ const words = segment(text, locale, { minLength, posFilterMode, customPosTags, enableStopwords, dictType });
10989
+ for (const word of words) {
10990
+ if (excludeSet && excludeSet.has(word.toLowerCase())) continue;
10991
+ wordFrequency.set(word, (wordFrequency.get(word) || 0) + 1);
10992
+ }
10993
+ }
10994
+ const filtered = /* @__PURE__ */ new Map();
10995
+ let totalWords = 0;
10996
+ for (const [word, count] of wordFrequency) {
10997
+ if (count >= minCount) {
10998
+ filtered.set(word, count);
10999
+ totalWords += count;
11000
+ }
11001
+ }
11002
+ const sorted = [...filtered.entries()].sort((a, b) => b[1] - a[1]).slice(0, topN);
11003
+ return { words: new Map(sorted), uniqueWords: filtered.size, totalWords };
11004
+ }
11005
+ function batchSegmentChineseWithStats(texts, locale, options = {}) {
11006
+ const {
11007
+ minLength,
11008
+ minCount = 2,
11009
+ topN = 100,
11010
+ posFilterMode = "meaningful",
11011
+ customPosTags,
11012
+ enableStopwords = true,
11013
+ dictType = "default",
11014
+ excludeWords
11015
+ } = options;
11016
+ const effectiveMinLength = minLength ?? 2;
11017
+ const allowedTags = posFilterMode === "custom" && customPosTags ? new Set(customPosTags) : MEANINGFUL_POS_TAGS;
11018
+ const excludeSet = excludeWords?.length ? new Set(excludeWords.map((w) => w.toLowerCase())) : null;
11019
+ const wordFrequency = /* @__PURE__ */ new Map();
11020
+ const posTagStats = /* @__PURE__ */ new Map();
11021
+ try {
11022
+ const jieba = getJieba(dictType);
11023
+ for (const text of texts) {
11024
+ const cleaned = cleanText(text);
11025
+ if (!cleaned) continue;
11026
+ for (const { tag, word } of jieba.tag(cleaned)) {
11027
+ if (!isValidWord(word, locale, effectiveMinLength, enableStopwords, isStopword)) continue;
11028
+ posTagStats.set(tag, (posTagStats.get(tag) || 0) + 1);
11029
+ if (!allowedTags.has(tag)) continue;
11030
+ if (excludeSet && excludeSet.has(word.toLowerCase())) continue;
11031
+ wordFrequency.set(word, (wordFrequency.get(word) || 0) + 1);
11032
+ }
11033
+ }
11034
+ } catch (error) {
11035
+ console.error("[NLP] Chinese single-pass segmentation failed:", error);
11036
+ }
11037
+ const filtered = /* @__PURE__ */ new Map();
11038
+ let totalWords = 0;
11039
+ for (const [word, count] of wordFrequency) {
11040
+ if (count >= minCount) {
11041
+ filtered.set(word, count);
11042
+ totalWords += count;
11043
+ }
11044
+ }
11045
+ const sorted = [...filtered.entries()].sort((a, b) => b[1] - a[1]).slice(0, topN);
11046
+ return { words: new Map(sorted), uniqueWords: filtered.size, totalWords, posTagStats };
11047
+ }
11048
+ function collectPosTagStats(texts, minWordLength = 2, enableStopwords = true, dictType = "default") {
11049
+ const posStats = /* @__PURE__ */ new Map();
11050
+ try {
11051
+ const jieba = getJieba(dictType);
11052
+ for (const text of texts) {
11053
+ const cleaned = cleanText(text);
11054
+ if (!cleaned) continue;
11055
+ const tagged = jieba.tag(cleaned);
11056
+ for (const item of tagged) {
11057
+ if (!isValidWord(item.word, "zh-CN", minWordLength, enableStopwords, isStopword)) continue;
11058
+ posStats.set(item.tag, (posStats.get(item.tag) || 0) + 1);
11059
+ }
11060
+ }
11061
+ } catch (error) {
11062
+ console.error("[NLP] Failed to collect POS stats:", error);
11063
+ }
11064
+ return posStats;
11065
+ }
11066
+ function getPosTagDefinitions() {
11067
+ return POS_TAG_DEFINITIONS;
11068
+ }
11069
+
11070
+ // ../../packages/node-runtime/src/ai/memory.ts
11071
+ import { randomUUID as randomUUID3 } from "crypto";
11072
+ import * as fs10 from "fs";
11073
+ import * as path17 from "path";
11074
+ import Database3 from "better-sqlite3";
11075
+ var AI_MEMORY_CONTENT_MAX_CHARS = 2e3;
11076
+ var AI_MEMORY_SCHEMA_VERSION = 2;
11077
+ var AI_MEMORY_PROMPT_MAX_ENTRIES = 20;
11078
+ var AI_MEMORY_PROMPT_MAX_CONTENT_CHARS = 4e3;
11079
+ var AI_MEMORY_PROMPT_BASE_USER_ENTRIES = 5;
11080
+ var AI_MEMORY_SUBSTRING_MIN_CHARS = 3;
11081
+ var NUMERIC_MEMORY_TOKEN_REGEX = new RegExp("\\p{Number}+", "gu");
11082
+ var SYMBOL_SUFFIX_MEMORY_TOKEN_REGEX = /[\p{Letter}\p{Number}]+(?:\+\+|#)/gu;
11083
+ function rankMemoryEntries(entries, options) {
11084
+ const recentEntries = [...entries].sort(compareRecentMemoryEntries);
11085
+ const query = normalizeSearchText(options.query);
11086
+ if (!query) {
11087
+ return { entries: recentEntries, retrievalMode: "recent_fallback", matchedCount: 0 };
11088
+ }
11089
+ const locale = normalizeMemoryLocale(options.locale);
11090
+ const queryTokens = tokenizeMemoryText(options.query, locale);
11091
+ const scored = recentEntries.map((entry) => ({
11092
+ entry,
11093
+ score: scoreMemoryEntry(entry.content, query, queryTokens, locale)
11094
+ }));
11095
+ const matchedCount = scored.filter((item) => item.score > 0).length;
11096
+ if (matchedCount === 0) {
11097
+ return { entries: recentEntries, retrievalMode: "recent_fallback", matchedCount: 0 };
11098
+ }
11099
+ scored.sort((left, right) => {
11100
+ if (left.score !== right.score) return right.score - left.score;
11101
+ if (left.entry.sourceType !== right.entry.sourceType) return left.entry.sourceType === "user" ? -1 : 1;
11102
+ return compareRecentMemoryEntries(left.entry, right.entry);
11103
+ });
11104
+ return {
11105
+ entries: scored.map((item) => item.entry),
11106
+ retrievalMode: "relevance",
11107
+ matchedCount
11108
+ };
11109
+ }
11110
+ function buildGlobalMemoryPrompt(entries, locale = "zh-CN", query = "") {
11111
+ const ranking = rankMemoryEntries(entries, { query, locale });
11112
+ const rankedEntries = ranking.entries;
11113
+ const recentUserEntries = [...entries].filter((entry) => entry.sourceType === "user").sort(compareRecentMemoryEntries).slice(0, AI_MEMORY_PROMPT_BASE_USER_ENTRIES);
11114
+ const prioritizedEntries = [];
11115
+ const prioritizedIds = /* @__PURE__ */ new Set();
11116
+ const topRelevantEntries = ranking.retrievalMode === "relevance" ? rankedEntries.slice(0, 1) : [];
11117
+ for (const entry of [...topRelevantEntries, ...recentUserEntries, ...rankedEntries]) {
11118
+ if (prioritizedIds.has(entry.id)) continue;
11119
+ prioritizedIds.add(entry.id);
11120
+ prioritizedEntries.push(entry);
11121
+ }
11122
+ const selected = [];
11123
+ let contentChars = 0;
11124
+ for (const entry of prioritizedEntries) {
11125
+ if (selected.length >= AI_MEMORY_PROMPT_MAX_ENTRIES) break;
11126
+ if (contentChars + entry.content.length > AI_MEMORY_PROMPT_MAX_CONTENT_CHARS) continue;
11127
+ selected.push(entry);
11128
+ contentChars += entry.content.length;
11129
+ }
11130
+ if (selected.length === 0) return "";
11131
+ const lines = selected.map((entry) => `- [id=${entry.id}; source=${entry.sourceType}] ${entry.content}`);
11132
+ if (selected.length < prioritizedEntries.length) {
11133
+ lines.push(
11134
+ locale.startsWith("zh") ? "- \u90E8\u5206\u5168\u5C40\u8BB0\u5FC6\u672A\u6CE8\u5165\uFF1B\u9700\u8981\u65F6\u8C03\u7528 memory_read \u8BFB\u53D6\u3002" : "- Some global memories were not injected; call memory_read when needed."
11135
+ );
11136
+ }
11137
+ return lines.join("\n");
11138
+ }
11139
+ function buildEntityMemoryPrompt(entityRefs, loadEntries, locale = "zh-CN", query = "") {
11140
+ const buckets = [];
11141
+ const seenScopes = /* @__PURE__ */ new Set();
11142
+ for (const ref of entityRefs ?? []) {
11143
+ const scope = ref.type === "contact" ? { scopeType: "contact", scopeId: ref.contactKey } : ref.sessionType === "group" ? { scopeType: "group", scopeId: ref.sessionId } : null;
11144
+ if (!scope?.scopeId) continue;
11145
+ const scopeKey = `${scope.scopeType}:${scope.scopeId}`;
11146
+ if (seenScopes.has(scopeKey)) continue;
11147
+ seenScopes.add(scopeKey);
11148
+ buckets.push({
11149
+ scope,
11150
+ displayName: ref.displayName,
11151
+ entries: rankMemoryEntries(loadEntries(scope), { query, locale }).entries
11152
+ });
11153
+ }
11154
+ const candidates = [];
11155
+ const maxBucketSize = Math.max(0, ...buckets.map((bucket) => bucket.entries.length));
11156
+ for (let index = 0; index < maxBucketSize; index++) {
11157
+ for (const bucket of buckets) {
11158
+ const entry = bucket.entries[index];
11159
+ if (entry) candidates.push({ bucket, entry });
11160
+ }
11161
+ }
11162
+ const selected = [];
11163
+ let contentChars = 0;
11164
+ for (const candidate of candidates) {
11165
+ if (selected.length >= AI_MEMORY_PROMPT_MAX_ENTRIES) break;
11166
+ if (contentChars + candidate.entry.content.length > AI_MEMORY_PROMPT_MAX_CONTENT_CHARS) continue;
11167
+ selected.push(candidate);
11168
+ contentChars += candidate.entry.content.length;
11169
+ }
11170
+ if (selected.length === 0) return "";
11171
+ const lines = selected.map(({ bucket, entry }) => {
11172
+ return `- [scope=${bucket.scope.scopeType}; scope_id=${JSON.stringify(bucket.scope.scopeId)}; display_name=${JSON.stringify(bucket.displayName)}; id=${entry.id}; source=${entry.sourceType}] ${entry.content}`;
11173
+ });
11174
+ if (selected.length < candidates.length) {
11175
+ lines.push(
11176
+ locale.startsWith("zh") ? "- \u90E8\u5206\u5F53\u524D\u5B9E\u4F53\u8BB0\u5FC6\u672A\u6CE8\u5165\uFF1B\u9700\u8981\u65F6\u8C03\u7528 memory_read \u8BFB\u53D6\u3002" : "- Some memories for the current entities were not injected; call memory_read when needed."
11177
+ );
11178
+ }
11179
+ return lines.join("\n");
11180
+ }
11181
+ var AIMemoryService = class {
11182
+ db = null;
11183
+ dbPath;
11184
+ nativeBinding;
11185
+ now;
11186
+ idFactory;
11187
+ constructor(aiDataDir, options = {}) {
11188
+ this.dbPath = path17.join(aiDataDir, "memory.db");
11189
+ this.nativeBinding = options.nativeBinding;
11190
+ this.now = options.now ?? Date.now;
11191
+ this.idFactory = options.idFactory ?? (() => `memory_${randomUUID3()}`);
11192
+ }
11193
+ list(scope) {
11194
+ const db = this.getDb();
11195
+ if (!scope) {
11196
+ return db.prepare(
11197
+ `SELECT id,
11198
+ scope_type AS scopeType,
11199
+ scope_id AS scopeId,
11200
+ content,
11201
+ source_type AS sourceType,
11202
+ source_ai_chat_id AS sourceAIChatId,
11203
+ source_message_id AS sourceMessageId,
11204
+ created_at AS createdAt,
11205
+ updated_at AS updatedAt
11206
+ FROM ai_memory
11207
+ ORDER BY updated_at DESC, id ASC`
11208
+ ).all();
11209
+ }
11210
+ const normalized = normalizeScope(scope);
11211
+ if (normalized.scopeType === "global" || normalized.scopeType === "self") {
11212
+ return db.prepare(
11213
+ `SELECT id,
11214
+ scope_type AS scopeType,
11215
+ scope_id AS scopeId,
11216
+ content,
11217
+ source_type AS sourceType,
11218
+ source_ai_chat_id AS sourceAIChatId,
11219
+ source_message_id AS sourceMessageId,
11220
+ created_at AS createdAt,
11221
+ updated_at AS updatedAt
11222
+ FROM ai_memory
11223
+ WHERE scope_type = ? AND scope_id IS NULL
11224
+ ORDER BY updated_at DESC, id ASC`
11225
+ ).all(normalized.scopeType);
11226
+ }
11227
+ return db.prepare(
11228
+ `SELECT id,
11229
+ scope_type AS scopeType,
11230
+ scope_id AS scopeId,
11231
+ content,
11232
+ source_type AS sourceType,
11233
+ source_ai_chat_id AS sourceAIChatId,
11234
+ source_message_id AS sourceMessageId,
11235
+ created_at AS createdAt,
11236
+ updated_at AS updatedAt
11237
+ FROM ai_memory
11238
+ WHERE scope_type = ? AND scope_id = ?
11239
+ ORDER BY updated_at DESC, id ASC`
11240
+ ).all(normalized.scopeType, normalized.scopeId);
11241
+ }
11242
+ search(scope, query, locale = "zh-CN") {
11243
+ const startedAt = Date.now();
11244
+ const result = rankMemoryEntries(this.list(scope), { query, locale });
11245
+ appLogger.debug("ai-memory", "AI memory relevance search completed", {
11246
+ candidateCount: result.entries.length,
11247
+ matchedCount: result.matchedCount,
11248
+ retrievalMode: result.retrievalMode,
11249
+ durationMs: Date.now() - startedAt
11250
+ });
11251
+ return result;
11252
+ }
11253
+ get(id) {
11254
+ const row = this.getDb().prepare(
11255
+ `SELECT id,
11256
+ scope_type AS scopeType,
11257
+ scope_id AS scopeId,
11258
+ content,
11259
+ source_type AS sourceType,
11260
+ source_ai_chat_id AS sourceAIChatId,
11261
+ source_message_id AS sourceMessageId,
11262
+ created_at AS createdAt,
11263
+ updated_at AS updatedAt
11264
+ FROM ai_memory
11265
+ WHERE id = ?`
11266
+ ).get(id);
11267
+ return row ?? null;
11268
+ }
11269
+ create(input) {
11270
+ return this.runWrite("create", () => {
11271
+ const scope = normalizeScope(input);
11272
+ const content = normalizeContent(input.content);
11273
+ const sourceType = normalizeSourceType(input.sourceType);
11274
+ const id = this.idFactory();
11275
+ const now = this.now();
11276
+ this.getDb().prepare(
11277
+ `INSERT INTO ai_memory (
11278
+ id, scope_type, scope_id, content, source_type,
11279
+ source_ai_chat_id, source_message_id, created_at, updated_at
11280
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
11281
+ ).run(
11282
+ id,
11283
+ scope.scopeType,
11284
+ scope.scopeId,
11285
+ content,
11286
+ sourceType,
11287
+ normalizeOptionalId(input.sourceAIChatId),
11288
+ normalizeOptionalId(input.sourceMessageId),
11289
+ now,
11290
+ now
11291
+ );
11292
+ return this.requireEntry(id);
11293
+ });
11294
+ }
11295
+ update(id, input) {
11296
+ return this.runWrite("update", () => {
11297
+ const content = normalizeContent(input.content);
11298
+ const sourceType = normalizeSourceType(input.sourceType);
11299
+ const result = this.getDb().prepare(
11300
+ `UPDATE ai_memory
11301
+ SET content = ?,
11302
+ source_type = ?,
11303
+ source_ai_chat_id = ?,
11304
+ source_message_id = ?,
11305
+ updated_at = ?
11306
+ WHERE id = ?`
11307
+ ).run(
11308
+ content,
11309
+ sourceType,
11310
+ normalizeOptionalId(input.sourceAIChatId),
11311
+ normalizeOptionalId(input.sourceMessageId),
11312
+ this.now(),
11313
+ id
11314
+ );
11315
+ if (result.changes === 0) throw new Error(`AI memory not found: ${id}`);
11316
+ return this.requireEntry(id);
11317
+ });
11318
+ }
11319
+ linkSourceMessages(links) {
11320
+ if (links.length === 0) return [];
11321
+ return this.runWrite("link sources", () => {
11322
+ const db = this.getDb();
11323
+ const update = db.prepare(
11324
+ `UPDATE ai_memory
11325
+ SET source_message_id = ?
11326
+ WHERE id = ?
11327
+ AND source_ai_chat_id = ?
11328
+ AND (source_message_id IS NULL OR source_message_id = ?)`
11329
+ );
11330
+ return db.transaction(() => {
11331
+ const linkedIds = [];
11332
+ for (const link of links) {
11333
+ const result = update.run(link.sourceMessageId, link.id, link.sourceAIChatId, link.sourceMessageId);
11334
+ if (result.changes === 0) throw new Error("AI memory source changed before it could be linked");
11335
+ linkedIds.push(link.id);
11336
+ }
11337
+ return linkedIds;
11338
+ })();
11339
+ });
11340
+ }
11341
+ forget(id) {
11342
+ return this.runWrite("forget", () => this.getDb().prepare("DELETE FROM ai_memory WHERE id = ?").run(id).changes > 0);
11343
+ }
11344
+ clear(scope) {
11345
+ return this.runWrite("clear", () => {
11346
+ const db = this.getDb();
11347
+ if (!scope) return db.prepare("DELETE FROM ai_memory").run().changes;
11348
+ const normalized = normalizeScope(scope);
11349
+ if (normalized.scopeType === "global" || normalized.scopeType === "self") {
11350
+ return db.prepare("DELETE FROM ai_memory WHERE scope_type = ? AND scope_id IS NULL").run(normalized.scopeType).changes;
11351
+ }
11352
+ return db.prepare("DELETE FROM ai_memory WHERE scope_type = ? AND scope_id = ?").run(normalized.scopeType, normalized.scopeId).changes;
11353
+ });
11354
+ }
11355
+ getSchemaVersion() {
11356
+ return this.getDb().pragma("user_version", { simple: true });
11357
+ }
11358
+ close() {
11359
+ if (!this.db) return;
11360
+ this.db.close();
11361
+ this.db = null;
11362
+ appLogger.debug("ai-memory", "AI memory database closed");
11363
+ }
11364
+ getDb() {
11365
+ if (this.db) return this.db;
11366
+ fs10.mkdirSync(path17.dirname(this.dbPath), { recursive: true });
11367
+ const db = this.nativeBinding ? new Database3(this.dbPath, { nativeBinding: this.nativeBinding }) : new Database3(this.dbPath);
11368
+ try {
11369
+ db.pragma("journal_mode = WAL");
11370
+ db.pragma("busy_timeout = 5000");
11371
+ migrateMemorySchema(db);
11372
+ } catch (error) {
11373
+ db.close();
11374
+ throw error;
11375
+ }
11376
+ this.db = db;
11377
+ appLogger.info("ai-memory", "AI memory database initialized", { schemaVersion: AI_MEMORY_SCHEMA_VERSION });
11378
+ return db;
11379
+ }
11380
+ requireEntry(id) {
11381
+ const entry = this.get(id);
11382
+ if (!entry) throw new Error(`AI memory not found after write: ${id}`);
11383
+ return entry;
11384
+ }
11385
+ runWrite(operation, write) {
11386
+ try {
11387
+ return write();
11388
+ } catch (error) {
11389
+ appLogger.error("ai-memory", `AI memory ${operation} failed`, error);
11390
+ throw error;
11391
+ }
11392
+ }
11393
+ };
11394
+ function compareRecentMemoryEntries(left, right) {
11395
+ return right.updatedAt - left.updatedAt || left.id.localeCompare(right.id);
11396
+ }
11397
+ function normalizeMemoryLocale(locale) {
11398
+ if (locale?.startsWith("zh-TW")) return "zh-TW";
11399
+ if (locale?.startsWith("zh")) return "zh-CN";
11400
+ if (locale?.startsWith("ja")) return "ja-JP";
11401
+ return "en-US";
11402
+ }
11403
+ function normalizeSearchText(value) {
11404
+ return value.normalize("NFKC").toLocaleLowerCase().replace(/[\p{P}\p{S}\s]+/gu, "");
11405
+ }
11406
+ function tokenizeMemoryText(value, locale) {
11407
+ const words = locale.startsWith("zh") ? segmentChineseMemoryText(value, locale) : segment(value, locale, { posFilterMode: "all", enableStopwords: true });
11408
+ const tokens = new Set(words.map(normalizeSearchText).filter((token) => token.length >= 2));
11409
+ for (const token of extractNumericMemoryTokens(value)) tokens.add(token);
11410
+ for (const token of extractSymbolSuffixMemoryTokens(value)) tokens.add(token);
11411
+ if (locale.startsWith("zh")) {
11412
+ for (const token of extractCjkBigrams(value)) tokens.add(token);
11413
+ }
11414
+ return tokens;
11415
+ }
11416
+ function extractNumericMemoryTokens(value) {
11417
+ return value.normalize("NFKC").match(NUMERIC_MEMORY_TOKEN_REGEX) ?? [];
11418
+ }
11419
+ function extractSymbolSuffixMemoryTokens(value) {
11420
+ return value.normalize("NFKC").toLocaleLowerCase().match(SYMBOL_SUFFIX_MEMORY_TOKEN_REGEX) ?? [];
11421
+ }
11422
+ function segmentChineseMemoryText(value, locale) {
11423
+ try {
11424
+ const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
11425
+ return [...segmenter.segment(value)].filter((item) => item.isWordLike).map((item) => item.segment);
11426
+ } catch {
11427
+ return value.match(/[\p{Letter}\p{Number}]+/gu) ?? [];
11428
+ }
11429
+ }
11430
+ function extractCjkBigrams(value) {
11431
+ const runs = value.normalize("NFKC").match(/[\p{Script=Han}]+/gu) ?? [];
11432
+ return runs.flatMap(
11433
+ (run) => Array.from({ length: Math.max(0, run.length - 1) }, (_, index) => run.slice(index, index + 2))
11434
+ );
11435
+ }
11436
+ function scoreMemoryEntry(content, normalizedQuery, queryTokens, locale) {
11437
+ const normalizedContent = normalizeSearchText(content);
11438
+ let score = 0;
11439
+ const matchesFullQuery = normalizedQuery.length >= AI_MEMORY_SUBSTRING_MIN_CHARS && normalizedContent.includes(normalizedQuery);
11440
+ const isMeaningfulReverseMatch = normalizedContent.length >= AI_MEMORY_SUBSTRING_MIN_CHARS && normalizedQuery.includes(normalizedContent);
11441
+ if (matchesFullQuery || isMeaningfulReverseMatch) score += 3;
11442
+ if (queryTokens.size === 0) return score;
11443
+ const contentTokens = tokenizeMemoryText(content, locale);
11444
+ if (contentTokens.size === 0) return score;
11445
+ let overlap = 0;
11446
+ for (const token of queryTokens) {
11447
+ if (contentTokens.has(token)) overlap += 1;
11448
+ }
11449
+ if (overlap === 0) return score;
11450
+ return score + overlap / queryTokens.size * 2 + overlap / contentTokens.size;
11451
+ }
11452
+ function normalizeScope(scope) {
11453
+ const scopeType = scope.scopeType;
11454
+ if (scopeType !== "global" && scopeType !== "self" && scopeType !== "contact" && scopeType !== "group") {
11455
+ throw new Error(`Unsupported AI memory scope type: ${String(scopeType)}`);
11456
+ }
11457
+ const scopeId = normalizeOptionalId(scope.scopeId);
11458
+ if (scopeType === "global" || scopeType === "self") {
11459
+ if (scopeId !== null) throw new Error(`${scopeType} AI memory scopeId must be null`);
11460
+ return { scopeType, scopeId: null };
11461
+ }
11462
+ if (!scopeId) throw new Error(`${scopeType} AI memory scopeId is required`);
11463
+ return { scopeType, scopeId };
11464
+ }
11465
+ function normalizeSourceType(sourceType) {
11466
+ if (sourceType !== "user" && sourceType !== "ai") {
11467
+ throw new Error(`Unsupported AI memory source type: ${String(sourceType)}`);
11468
+ }
11469
+ return sourceType;
11470
+ }
11471
+ function normalizeContent(content) {
11472
+ const normalized = typeof content === "string" ? content.trim() : "";
11473
+ if (!normalized || normalized.length > AI_MEMORY_CONTENT_MAX_CHARS) {
11474
+ throw new Error(`AI memory content must be between 1 and ${AI_MEMORY_CONTENT_MAX_CHARS} characters`);
11475
+ }
11476
+ return normalized;
11477
+ }
11478
+ function normalizeOptionalId(value) {
11479
+ if (typeof value !== "string") return null;
11480
+ const normalized = value.trim();
11481
+ return normalized || null;
11482
+ }
11483
+ function migrateMemorySchema(db) {
11484
+ const version = db.pragma("user_version", { simple: true });
11485
+ if (version > AI_MEMORY_SCHEMA_VERSION) {
11486
+ throw new Error(`Unsupported AI memory schema version: ${version}`);
11487
+ }
11488
+ if (version === AI_MEMORY_SCHEMA_VERSION) return;
11489
+ if (version !== 0) {
11490
+ throw new Error(`Unsupported AI memory schema version: ${version}`);
11491
+ }
11492
+ const migrate = db.transaction(() => {
11493
+ db.exec(`
11494
+ CREATE TABLE ai_memory (
11495
+ id TEXT PRIMARY KEY,
11496
+ scope_type TEXT NOT NULL CHECK (scope_type IN ('global', 'self', 'contact', 'group')),
11497
+ scope_id TEXT,
11498
+ content TEXT NOT NULL,
11499
+ source_type TEXT NOT NULL CHECK (source_type IN ('user', 'ai')),
11500
+ source_ai_chat_id TEXT,
11501
+ source_message_id TEXT,
11502
+ created_at INTEGER NOT NULL,
11503
+ updated_at INTEGER NOT NULL,
11504
+ CHECK (
11505
+ (scope_type IN ('global', 'self') AND scope_id IS NULL) OR
11506
+ (scope_type IN ('contact', 'group') AND scope_id IS NOT NULL)
11507
+ )
11508
+ );
11509
+ `);
11510
+ db.exec(`
11511
+ CREATE INDEX idx_ai_memory_scope_updated
11512
+ ON ai_memory(scope_type, scope_id, updated_at DESC, id ASC);
11513
+ `);
11514
+ db.pragma(`user_version = ${AI_MEMORY_SCHEMA_VERSION}`);
11515
+ });
11516
+ migrate();
11517
+ appLogger.info("ai-memory", "AI memory database schema migrated", {
11518
+ fromVersion: version,
11519
+ toVersion: AI_MEMORY_SCHEMA_VERSION
11520
+ });
11521
+ }
11522
+
11523
+ // ../../packages/node-runtime/src/ai/memory-provenance.ts
11524
+ function listAIMemoriesWithSourceStatus(memoryService, aiChatManager, scope) {
11525
+ return memoryService.list(scope).map((entry) => ({
11526
+ ...entry,
11527
+ sourceStatus: resolveAIMemorySourceStatus(entry, aiChatManager)
11528
+ }));
11529
+ }
11530
+ function linkAIMemorySources(memoryService, aiChatManager, input, options) {
11531
+ const aiChatId = requireId(input.aiChatId, "aiChatId");
11532
+ const conversation = aiChatManager.getAIChat(aiChatId);
11533
+ if (!conversation || conversation.kind !== "global") {
11534
+ throw new Error("Global AI conversation not found");
11535
+ }
11536
+ const userMessage = requireSourceMessage(aiChatManager, input.userMessageId, aiChatId, "user");
11537
+ const assistantMessage = requireSourceMessage(aiChatManager, input.assistantMessageId, aiChatId, "assistant");
11538
+ if (assistantMessage.parentId !== userMessage.id) {
11539
+ throw new Error("Assistant source message must directly reply to the user source message");
11540
+ }
11541
+ if (options && options.expectedParentMessageId !== void 0 && userMessage.parentId !== options.expectedParentMessageId) {
11542
+ throw new Error("User source message does not belong to the requested agent turn");
11543
+ }
11544
+ const memoryIds = normalizeMemoryIds(input.memoryIds);
11545
+ const memories = memoryIds.map((id) => ({ id, entry: memoryService.get(id) }));
11546
+ for (const { entry } of memories) {
11547
+ if (entry && entry.sourceAIChatId !== aiChatId) {
11548
+ throw new Error("AI memory does not belong to the requested conversation");
11549
+ }
11550
+ if (entry?.sourceMessageId) {
11551
+ const expectedSourceMessageId = entry.sourceType === "user" ? userMessage.id : assistantMessage.id;
11552
+ if (entry.sourceMessageId !== expectedSourceMessageId) {
11553
+ throw new Error("AI memory is already linked to another source message");
11554
+ }
11555
+ }
11556
+ }
11557
+ const skippedMemoryIds = [];
11558
+ const links = [];
11559
+ for (const { id, entry } of memories) {
11560
+ if (!entry) {
11561
+ skippedMemoryIds.push(id);
11562
+ continue;
11563
+ }
11564
+ const sourceMessage = entry.sourceType === "user" ? userMessage : assistantMessage;
11565
+ links.push({ id, sourceAIChatId: aiChatId, sourceMessageId: sourceMessage.id });
11566
+ }
11567
+ const linkedMemoryIds = memoryService.linkSourceMessages(links);
11568
+ return { linkedMemoryIds, skippedMemoryIds };
11569
+ }
11570
+ function resolveAIMemorySourceStatus(entry, aiChatManager) {
11571
+ if (!entry.sourceAIChatId) return "none";
11572
+ const conversation = aiChatManager.getAIChat(entry.sourceAIChatId);
11573
+ if (!conversation || conversation.kind !== "global") return "unavailable";
11574
+ if (!entry.sourceMessageId) return "conversation";
11575
+ const message = aiChatManager.getMessage(entry.sourceMessageId);
11576
+ const expectedRole = entry.sourceType === "user" ? "user" : "assistant";
11577
+ return message?.aiChatId === entry.sourceAIChatId && message.role === expectedRole ? "message" : "unavailable";
11578
+ }
11579
+ function requireSourceMessage(aiChatManager, messageId, aiChatId, role) {
11580
+ const normalizedMessageId = requireId(messageId, `${role}MessageId`);
11581
+ const message = aiChatManager.getMessage(normalizedMessageId);
11582
+ if (!message || message.aiChatId !== aiChatId || message.role !== role) {
11583
+ throw new Error(`${role} source message does not belong to the requested conversation`);
11584
+ }
11585
+ return message;
11586
+ }
11587
+ function normalizeMemoryIds(memoryIds) {
11588
+ if (!Array.isArray(memoryIds)) throw new Error("memoryIds must be an array");
11589
+ return [...new Set(memoryIds.map((id) => requireId(id, "memoryId")))];
11590
+ }
11591
+ function requireId(value, field) {
11592
+ const normalized = typeof value === "string" ? value.trim() : "";
11593
+ if (!normalized) throw new Error(`${field} is required`);
11594
+ return normalized;
11595
+ }
11596
+
10049
11597
  // ../../packages/node-runtime/src/ai/tokenizer.ts
10050
11598
  import { Tiktoken } from "js-tiktoken/lite";
10051
11599
  var encoder = null;
@@ -10183,21 +11731,21 @@ var SkillManagerCore = class {
10183
11731
  }
10184
11732
  // ==================== Init ====================
10185
11733
  init() {
10186
- const { fs: fs18, skillsDir } = this.deps;
10187
- fs18.ensureDir(skillsDir);
11734
+ const { fs: fs20, skillsDir } = this.deps;
11735
+ fs20.ensureDir(skillsDir);
10188
11736
  this.loadAll();
10189
11737
  this.initialized = true;
10190
11738
  this.deps.logger?.info("SkillManager", "Initialized", { total: this.cache.size });
10191
11739
  return { total: this.cache.size };
10192
11740
  }
10193
11741
  loadAll() {
10194
- const { fs: fs18, skillsDir } = this.deps;
11742
+ const { fs: fs20, skillsDir } = this.deps;
10195
11743
  this.cache.clear();
10196
- const files = fs18.listFiles(skillsDir, ".md");
11744
+ const files = fs20.listFiles(skillsDir, ".md");
10197
11745
  for (const file of files) {
10198
11746
  try {
10199
- const filePath = fs18.joinPath(skillsDir, file);
10200
- const content = fs18.readFile(filePath);
11747
+ const filePath = fs20.joinPath(skillsDir, file);
11748
+ const content = fs20.readFile(filePath);
10201
11749
  const def = parseSkillFile(content, filePath);
10202
11750
  if (def) {
10203
11751
  this.cache.set(def.id, def);
@@ -10349,8 +11897,8 @@ var SkillManagerCore = class {
10349
11897
  };
10350
11898
 
10351
11899
  // ../../packages/node-runtime/src/ai/skill-manager.ts
10352
- import * as fs9 from "fs";
10353
- import * as path16 from "path";
11900
+ import * as fs11 from "fs";
11901
+ import * as path18 from "path";
10354
11902
  var SKILLS_DIR_NAME = "skills";
10355
11903
  var defaultLogger2 = {
10356
11904
  info: () => {
@@ -10360,26 +11908,26 @@ var defaultLogger2 = {
10360
11908
  };
10361
11909
  var nodeFs = {
10362
11910
  ensureDir(dir) {
10363
- fs9.mkdirSync(dir, { recursive: true });
11911
+ fs11.mkdirSync(dir, { recursive: true });
10364
11912
  },
10365
11913
  listFiles(dir, ext) {
10366
- if (!fs9.existsSync(dir)) return [];
10367
- return fs9.readdirSync(dir).filter((file) => file.endsWith(ext));
11914
+ if (!fs11.existsSync(dir)) return [];
11915
+ return fs11.readdirSync(dir).filter((file) => file.endsWith(ext));
10368
11916
  },
10369
11917
  readFile(filePath) {
10370
- return fs9.readFileSync(filePath, "utf-8");
11918
+ return fs11.readFileSync(filePath, "utf-8");
10371
11919
  },
10372
11920
  writeFile(filePath, content) {
10373
- fs9.writeFileSync(filePath, content, "utf-8");
11921
+ fs11.writeFileSync(filePath, content, "utf-8");
10374
11922
  },
10375
11923
  deleteFile(filePath) {
10376
- fs9.unlinkSync(filePath);
11924
+ fs11.unlinkSync(filePath);
10377
11925
  },
10378
11926
  fileExists(filePath) {
10379
- return fs9.existsSync(filePath);
11927
+ return fs11.existsSync(filePath);
10380
11928
  },
10381
11929
  joinPath(...parts) {
10382
- return path16.join(...parts);
11930
+ return path18.join(...parts);
10383
11931
  }
10384
11932
  };
10385
11933
  var SkillManager = class {
@@ -10389,7 +11937,7 @@ var SkillManager = class {
10389
11937
  this.logger = logger ?? defaultLogger2;
10390
11938
  this.core = new SkillManagerCore({
10391
11939
  fs: nodeFs,
10392
- skillsDir: path16.join(aiDataDir, SKILLS_DIR_NAME),
11940
+ skillsDir: path18.join(aiDataDir, SKILLS_DIR_NAME),
10393
11941
  builtinRawSkills: [],
10394
11942
  logger: {
10395
11943
  info: () => {
@@ -10694,6 +12242,12 @@ var DEFAULT_CONTEXT_COMPRESSION_CONFIG = Object.freeze({
10694
12242
  });
10695
12243
 
10696
12244
  // ../../packages/node-runtime/src/ai/agent/history.ts
12245
+ function appendEntityRefsForModel(text, entityRefs) {
12246
+ if (!entityRefs?.length) return text;
12247
+ return `${text}
12248
+
12249
+ <chatlab_entity_refs>${JSON.stringify(entityRefs)}</chatlab_entity_refs>`;
12250
+ }
10697
12251
  function createEmptyPiUsage() {
10698
12252
  return {
10699
12253
  input: 0,
@@ -10767,7 +12321,7 @@ function toPiHistoryMessages(messages, options) {
10767
12321
  if (msg.role === "user") {
10768
12322
  out.push({
10769
12323
  role: "user",
10770
- content: [{ type: "text", text: msg.content || "" }],
12324
+ content: [{ type: "text", text: appendEntityRefsForModel(msg.content || "", msg.entityRefs) }],
10771
12325
  timestamp: Date.now()
10772
12326
  });
10773
12327
  continue;
@@ -10776,7 +12330,18 @@ function toPiHistoryMessages(messages, options) {
10776
12330
  if (replayable) {
10777
12331
  replayAssistantBlocks(msg.contentBlocks, out, options);
10778
12332
  } else {
10779
- out.push(makeAssistantMessage([{ type: "text", text: msg.content || "" }], "stop", options?.modelInfo));
12333
+ out.push(
12334
+ makeAssistantMessage(
12335
+ [
12336
+ {
12337
+ type: "text",
12338
+ text: appendEntityRefsForModel(msg.content || "", msg.role === "summary" ? msg.entityRefs : void 0)
12339
+ }
12340
+ ],
12341
+ "stop",
12342
+ options?.modelInfo
12343
+ )
12344
+ );
10780
12345
  }
10781
12346
  }
10782
12347
  return out;
@@ -10825,8 +12390,9 @@ var defaultLogger3 = {
10825
12390
  error: () => {
10826
12391
  }
10827
12392
  };
10828
- async function checkAndCompress(aiChatId, config, systemPrompt, llmAdapter, convManager, logger = defaultLogger3) {
12393
+ async function checkAndCompress(aiChatId, config, systemPrompt, llmAdapter, convManager, logger = defaultLogger3, options = {}) {
10829
12394
  try {
12395
+ throwIfAborted(options.signal);
10830
12396
  const contextWindow = llmAdapter.contextWindow || DEFAULT_CONTEXT_WINDOW;
10831
12397
  const thresholdTokens = Math.floor(contextWindow * (config.tokenThresholdPercent / 100) * 0.95);
10832
12398
  const summary = convManager.getLatestSummary(aiChatId);
@@ -10842,10 +12408,16 @@ async function checkAndCompress(aiChatId, config, systemPrompt, llmAdapter, conv
10842
12408
  }
10843
12409
  const historyForTokenCount = [];
10844
12410
  if (summary) {
10845
- historyForTokenCount.push({ role: "assistant", content: summary.content });
12411
+ historyForTokenCount.push({
12412
+ role: "assistant",
12413
+ content: appendEntityRefsForModel(summary.content, summary.entityRefs)
12414
+ });
10846
12415
  }
10847
12416
  for (const msg of messages) {
10848
- historyForTokenCount.push({ role: msg.role, content: msg.content });
12417
+ historyForTokenCount.push({
12418
+ role: msg.role,
12419
+ content: appendEntityRefsForModel(msg.content, msg.entityRefs)
12420
+ });
10849
12421
  for (const toolText of replayedToolResultTexts(msg.contentBlocks)) {
10850
12422
  historyForTokenCount.push({ role: "tool", content: toolText });
10851
12423
  }
@@ -10870,19 +12442,32 @@ async function checkAndCompress(aiChatId, config, systemPrompt, llmAdapter, conv
10870
12442
  const targetTokens = Math.min(Math.floor(contextWindow * 0.1), 16384);
10871
12443
  const template = isProgressive ? PROGRESSIVE_COMPRESSION_PROMPT : INITIAL_COMPRESSION_PROMPT;
10872
12444
  const prompt = template.replace("{maxTokens}", String(targetTokens)).replace("{messages}", compressInput);
10873
- let summaryText = await llmAdapter.compress(prompt, targetTokens);
12445
+ let summaryText = await llmAdapter.compress(prompt, targetTokens, options.signal);
12446
+ throwIfAborted(options.signal);
10874
12447
  if (!summaryText) {
10875
12448
  logger.warn("Compression", "LLM compression failed, falling back to truncation");
10876
12449
  summaryText = forceTruncate(compressInput, targetTokens);
10877
12450
  }
10878
12451
  const bufferBoundary = bufferMessages.length > 0 ? bufferMessages[0].timestamp : messagesToCompress[messagesToCompress.length - 1].timestamp + 1;
10879
- convManager.addSummaryMessage(aiChatId, summaryText, {
10880
- bufferBoundaryTimestamp: bufferBoundary,
10881
- compressedMessageCount: messagesToCompress.length
10882
- });
10883
- const afterTokenCount = [{ role: "assistant", content: summaryText }];
12452
+ const summaryEntityRefs = mergeEntityRefs(
12453
+ summary?.entityRefs,
12454
+ ...messagesToCompress.map((message) => message.entityRefs)
12455
+ );
12456
+ throwIfAborted(options.signal);
12457
+ convManager.addSummaryMessage(
12458
+ aiChatId,
12459
+ summaryText,
12460
+ {
12461
+ bufferBoundaryTimestamp: bufferBoundary,
12462
+ compressedMessageCount: messagesToCompress.length
12463
+ },
12464
+ summaryEntityRefs
12465
+ );
12466
+ const afterTokenCount = [
12467
+ { role: "assistant", content: appendEntityRefsForModel(summaryText, summaryEntityRefs) }
12468
+ ];
10884
12469
  for (const m of bufferMessages) {
10885
- afterTokenCount.push({ role: m.role, content: m.content });
12470
+ afterTokenCount.push({ role: m.role, content: appendEntityRefsForModel(m.content, m.entityRefs) });
10886
12471
  for (const toolText of replayedToolResultTexts(m.contentBlocks)) {
10887
12472
  afterTokenCount.push({ role: "tool", content: toolText });
10888
12473
  }
@@ -10910,10 +12495,19 @@ async function checkAndCompress(aiChatId, config, systemPrompt, llmAdapter, conv
10910
12495
  summaryContent: summaryText
10911
12496
  };
10912
12497
  } catch (error) {
12498
+ if (options.signal?.aborted) {
12499
+ return { compressed: false, reason: "error", error: "Compression aborted" };
12500
+ }
10913
12501
  logger.error("Compression", "Compression failed", { error: String(error) });
10914
12502
  return { compressed: false, reason: "error", error: String(error) };
10915
12503
  }
10916
12504
  }
12505
+ function throwIfAborted(signal) {
12506
+ if (!signal?.aborted) return;
12507
+ const error = new Error("Compression aborted");
12508
+ error.name = "AbortError";
12509
+ throw error;
12510
+ }
10917
12511
  async function manualCompress(aiChatId, config, systemPrompt, llmAdapter, convManager, logger) {
10918
12512
  const messageCount = convManager.getMessageCountAfterSummary(aiChatId);
10919
12513
  if (messageCount < 5) {
@@ -10927,7 +12521,7 @@ function replayedToolResultTexts(blocks) {
10927
12521
  return blocks.filter(isReplayableToolBlock).map((block) => truncateToolResultText(block.tool.result));
10928
12522
  }
10929
12523
  function countMessageTokensWithTools(msg) {
10930
- let tokens = countTokens(msg.content) + 4;
12524
+ let tokens = countTokens(appendEntityRefsForModel(msg.content, msg.entityRefs)) + 4;
10931
12525
  for (const toolText of replayedToolResultTexts(msg.contentBlocks)) {
10932
12526
  tokens += countTokens(toolText) + 4;
10933
12527
  }
@@ -10947,6 +12541,10 @@ function splitMessagesForCompression(messages, bufferTokenBudget) {
10947
12541
  splitIndex = 0;
10948
12542
  }
10949
12543
  }
12544
+ if (splitIndex > 0) {
12545
+ const boundaryTimestamp = messages[Math.min(splitIndex, messages.length - 1)].timestamp;
12546
+ while (splitIndex > 0 && messages[splitIndex - 1].timestamp === boundaryTimestamp) splitIndex--;
12547
+ }
10950
12548
  return {
10951
12549
  bufferMessages: messages.slice(splitIndex),
10952
12550
  messagesToCompress: messages.slice(0, splitIndex)
@@ -10955,14 +12553,16 @@ function splitMessagesForCompression(messages, bufferTokenBudget) {
10955
12553
  function buildCompressionInput(messagesToCompress, existingSummary) {
10956
12554
  const parts = [];
10957
12555
  if (existingSummary) {
10958
- parts.push(`[PREVIOUS SUMMARY \u2014 MUST PRESERVE]
10959
- ${existingSummary.content}
10960
- `);
12556
+ parts.push(
12557
+ `[PREVIOUS SUMMARY \u2014 MUST PRESERVE]
12558
+ ${appendEntityRefsForModel(existingSummary.content, existingSummary.entityRefs)}
12559
+ `
12560
+ );
10961
12561
  parts.push(`[NEW MESSAGES \u2014 SUMMARIZE AND MERGE]`);
10962
12562
  }
10963
12563
  for (const msg of messagesToCompress) {
10964
12564
  const roleLabel = msg.role === "user" ? "User" : "Assistant";
10965
- parts.push(`${roleLabel}: ${msg.content}`);
12565
+ parts.push(`${roleLabel}: ${appendEntityRefsForModel(msg.content, msg.entityRefs)}`);
10966
12566
  for (const block of (msg.contentBlocks ?? []).filter(isReplayableToolBlock)) {
10967
12567
  parts.push(`[Tool result: ${block.tool.name}]
10968
12568
  ${truncateToolResultText(block.tool.result)}`);
@@ -10970,6 +12570,16 @@ ${truncateToolResultText(block.tool.result)}`);
10970
12570
  }
10971
12571
  return parts.join("\n\n");
10972
12572
  }
12573
+ function mergeEntityRefs(...groups) {
12574
+ const refs = /* @__PURE__ */ new Map();
12575
+ for (const group of groups) {
12576
+ for (const ref of group ?? []) {
12577
+ const key = ref.type === "contact" ? `contact:${ref.contactKey}` : `session:${ref.sessionId}`;
12578
+ refs.set(key, ref);
12579
+ }
12580
+ }
12581
+ return refs.size > 0 ? [...refs.values()] : void 0;
12582
+ }
10973
12583
  function forceTruncate(input, targetTokens) {
10974
12584
  const lines = input.split("\n");
10975
12585
  const result = [];
@@ -11016,7 +12626,7 @@ function createCompressionLlmAdapter(options) {
11016
12626
  const contextWindow = options.contextWindow ?? piModel.contextWindow ?? DEFAULT_CONTEXT_WINDOW2;
11017
12627
  return {
11018
12628
  contextWindow,
11019
- compress: async (prompt, maxTokens) => {
12629
+ compress: async (prompt, maxTokens, signal) => {
11020
12630
  onCompressing?.();
11021
12631
  try {
11022
12632
  const result = await completeSimple(
@@ -11025,11 +12635,12 @@ function createCompressionLlmAdapter(options) {
11025
12635
  systemPrompt: void 0,
11026
12636
  messages: [{ role: "user", content: [{ type: "text", text: prompt }], timestamp: Date.now() }]
11027
12637
  },
11028
- { apiKey, maxTokens }
12638
+ { apiKey, maxTokens, signal }
11029
12639
  );
11030
12640
  const text = result.content.filter((item) => item.type === "text").map((item) => item.text).join("");
11031
12641
  return text || null;
11032
12642
  } catch (error) {
12643
+ if (signal?.aborted) return null;
11033
12644
  onError?.(error);
11034
12645
  return null;
11035
12646
  }
@@ -12579,288 +14190,6 @@ function buildSemanticSearchGuidance(locale) {
12579
14190
  ].join("");
12580
14191
  }
12581
14192
 
12582
- // ../../packages/node-runtime/src/ai/agent/event-handler.ts
12583
- function estimateTokensFromText(text) {
12584
- if (!text) return 0;
12585
- const normalized = text.replace(/\s+/g, " ").trim();
12586
- if (!normalized) return 0;
12587
- const cjkCount = (normalized.match(/[\u3400-\u9fff\uf900-\ufaff]/g) || []).length;
12588
- const latinCount = normalized.length - cjkCount;
12589
- return Math.max(1, Math.ceil(cjkCount * 1.15 + latinCount / 4));
12590
- }
12591
- function extractMessageText(message) {
12592
- if (message.role === "user") {
12593
- if (typeof message.content === "string") return message.content;
12594
- return message.content.map((item) => {
12595
- if (item.type === "text") return item.text;
12596
- if (item.type === "image") return "[image]";
12597
- return "";
12598
- }).join("\n");
12599
- }
12600
- if (message.role === "assistant") {
12601
- return message.content.map((item) => {
12602
- if (item.type === "text") return item.text;
12603
- if (item.type === "thinking") return item.thinking;
12604
- if (item.type === "toolCall") return `${item.name} ${JSON.stringify(item.arguments || {})}`;
12605
- return "";
12606
- }).join("\n");
12607
- }
12608
- if (message.role === "toolResult") {
12609
- return message.content.map((item) => {
12610
- if (item.type === "text") return item.text;
12611
- return "[binary]";
12612
- }).join("\n");
12613
- }
12614
- return "";
12615
- }
12616
- function estimateContextTokens(systemPrompt, messages, pendingUserMessage) {
12617
- let tokens = estimateTokensFromText(systemPrompt);
12618
- for (const message of messages) {
12619
- if (message.role === "toolResult") continue;
12620
- tokens += estimateTokensFromText(extractMessageText(message));
12621
- }
12622
- if (pendingUserMessage) {
12623
- tokens += estimateTokensFromText(pendingUserMessage);
12624
- }
12625
- return tokens;
12626
- }
12627
- var AgentEventHandler = class {
12628
- toolsUsed = [];
12629
- toolRounds = 0;
12630
- totalUsage = {
12631
- promptTokens: 0,
12632
- completionTokens: 0,
12633
- totalTokens: 0,
12634
- cacheReadTokens: 0,
12635
- cacheWriteTokens: 0
12636
- };
12637
- lastStatusAt = 0;
12638
- onChunk;
12639
- context;
12640
- systemPrompt;
12641
- activeTools = /* @__PURE__ */ new Map();
12642
- constructor(config) {
12643
- this.onChunk = config.onChunk;
12644
- this.context = config.context;
12645
- this.systemPrompt = config.systemPrompt;
12646
- }
12647
- handleCoreEvent(event, messages) {
12648
- switch (event.type) {
12649
- case "content":
12650
- this.onChunk({ type: "content", content: event.content });
12651
- this.emitStatus("responding", messages);
12652
- break;
12653
- case "thinking_start":
12654
- this.emitStatus("thinking", messages, { force: true });
12655
- break;
12656
- case "thinking_delta":
12657
- this.onChunk({ type: "think", content: event.content, thinkTag: "thinking" });
12658
- this.emitStatus("thinking", messages);
12659
- break;
12660
- case "thinking_end":
12661
- this.onChunk({ type: "think", content: "", thinkTag: "thinking", thinkDurationMs: event.durationMs });
12662
- this.emitStatus("responding", messages, { force: true });
12663
- break;
12664
- case "tool_start": {
12665
- const params = this.normalizeToolParams(event.toolName, event.toolParams);
12666
- this.toolsUsed.push(event.toolName);
12667
- this.activeTools.set(event.toolCallId, event.toolName);
12668
- this.onChunk({ type: "tool_start", toolCallId: event.toolCallId, toolName: event.toolName, toolParams: params });
12669
- this.emitStatus("tool_running", messages, { currentTool: event.toolName, force: true });
12670
- break;
12671
- }
12672
- case "tool_update":
12673
- this.onChunk({
12674
- type: "tool_update",
12675
- toolCallId: event.toolCallId,
12676
- toolName: event.toolName,
12677
- toolProgress: event.progress
12678
- });
12679
- break;
12680
- case "tool_end":
12681
- this.activeTools.delete(event.toolCallId);
12682
- this.onChunk({
12683
- type: "tool_result",
12684
- toolCallId: event.toolCallId,
12685
- toolName: event.toolName,
12686
- toolResult: event.toolResult,
12687
- toolIsError: event.isError
12688
- });
12689
- if (this.activeTools.size > 0) {
12690
- const currentTool = Array.from(this.activeTools.values()).at(-1);
12691
- this.emitStatus("tool_running", messages, { currentTool, force: true });
12692
- } else {
12693
- this.emitStatus("thinking", messages, { force: true });
12694
- }
12695
- break;
12696
- case "turn_end":
12697
- this.activeTools.clear();
12698
- this.toolRounds = event.round;
12699
- this.onChunk({ type: "turn_end", hadToolCalls: event.hadToolCalls });
12700
- this.emitStatus("thinking", messages, { force: true });
12701
- break;
12702
- case "usage_update":
12703
- this.totalUsage = { ...event.usage };
12704
- this.emitStatus("responding", messages, { force: true });
12705
- break;
12706
- }
12707
- }
12708
- cloneUsage() {
12709
- return {
12710
- promptTokens: this.totalUsage.promptTokens,
12711
- completionTokens: this.totalUsage.completionTokens,
12712
- totalTokens: this.totalUsage.totalTokens,
12713
- cacheReadTokens: this.totalUsage.cacheReadTokens,
12714
- cacheWriteTokens: this.totalUsage.cacheWriteTokens
12715
- };
12716
- }
12717
- emitStatus(phase, messages, options) {
12718
- const now = Date.now();
12719
- if (!options?.force && now - this.lastStatusAt < 240) return;
12720
- this.lastStatusAt = now;
12721
- const contextTokens = estimateContextTokens(this.systemPrompt, messages, options?.pendingUserMessage);
12722
- const status = {
12723
- phase,
12724
- round: this.toolRounds,
12725
- toolsUsed: this.toolsUsed.length,
12726
- currentTool: options?.currentTool,
12727
- contextTokens,
12728
- totalUsage: this.cloneUsage(),
12729
- updatedAt: now
12730
- };
12731
- this.onChunk({ type: "status", status });
12732
- }
12733
- normalizeToolParams(toolName, params) {
12734
- const normalized = { ...params };
12735
- const toolsWithLimit = ["search_messages", "get_recent_messages", "get_conversation_between"];
12736
- if (this.context.maxMessagesLimit && toolsWithLimit.includes(toolName)) {
12737
- normalized.limit = this.context.maxMessagesLimit;
12738
- }
12739
- if (this.context.timeFilter && (toolName === "search_messages" || toolName === "get_recent_messages")) {
12740
- normalized._timeFilter = this.context.timeFilter;
12741
- }
12742
- return normalized;
12743
- }
12744
- };
12745
-
12746
- // ../../packages/node-runtime/src/ai/agent/prompt-builder.ts
12747
- function agentT(t2, key, locale, options) {
12748
- return t2(key, { lng: locale, ...options });
12749
- }
12750
- function formatNullableTimestamp2(timestamp) {
12751
- return typeof timestamp === "number" && Number.isFinite(timestamp) ? String(timestamp) : "null";
12752
- }
12753
- function formatSharePercent2(share) {
12754
- if (!Number.isFinite(share)) return "0%";
12755
- const rounded = Math.round(share * 10) / 10;
12756
- return Number.isInteger(rounded) ? `${rounded}%` : `${rounded.toFixed(1)}%`;
12757
- }
12758
- function normalizeInlineText(text) {
12759
- return text.replace(/\s+/g, " ").trim();
12760
- }
12761
- function formatDataSnapshotContext(t2, dataSnapshot, locale) {
12762
- const memberHints = dataSnapshot.activeMemberHints ?? [];
12763
- const memberHintTitle = memberHints.length === 0 ? agentT(t2, "ai.agent.dataSnapshotMemberHintsUnavailable", locale) : dataSnapshot.totalMembers <= 10 ? agentT(t2, "ai.agent.dataSnapshotMemberHintsAll", locale) : agentT(t2, "ai.agent.dataSnapshotMemberHintsTop", locale);
12764
- const memberHintLines = memberHints.length > 0 ? memberHints.map((member, index) => {
12765
- return `${index + 1}. member_id=${member.memberId} | display_name=${normalizeInlineText(member.displayName)} | messages=${member.messageCount} | share=${formatSharePercent2(member.share)}`;
12766
- }).join("\n") : agentT(t2, "ai.agent.dataSnapshotMemberHintsEmpty", locale);
12767
- return agentT(t2, "ai.agent.dataSnapshotContext", locale, {
12768
- name: dataSnapshot.name,
12769
- platform: dataSnapshot.platform,
12770
- type: dataSnapshot.type,
12771
- totalMessages: dataSnapshot.totalMessages,
12772
- totalMembers: dataSnapshot.totalMembers,
12773
- firstMessageTs: formatNullableTimestamp2(dataSnapshot.firstMessageTs),
12774
- firstMessageTime: formatTimestamp(dataSnapshot.firstMessageTs, locale),
12775
- lastMessageTs: formatNullableTimestamp2(dataSnapshot.lastMessageTs),
12776
- lastMessageTime: formatTimestamp(dataSnapshot.lastMessageTs, locale),
12777
- segmentSummaryCount: dataSnapshot.segmentSummaries?.availableCount ?? 0,
12778
- memberHintTitle,
12779
- memberHintLines,
12780
- usageRules: agentT(t2, "ai.agent.dataSnapshotUsageRules", locale)
12781
- });
12782
- }
12783
- function getLockedPromptSection(t2, chatType, ownerInfo, locale, mentionedMembers, dataSnapshot) {
12784
- const now = /* @__PURE__ */ new Date();
12785
- const dateLocale = locale.startsWith("zh") ? "zh-CN" : "en-US";
12786
- const currentDate = now.toLocaleDateString(dateLocale, {
12787
- year: "numeric",
12788
- month: "long",
12789
- day: "numeric",
12790
- weekday: "long"
12791
- });
12792
- const isPrivate = chatType === "private";
12793
- const chatContext = agentT(t2, `ai.agent.chatContext.${chatType}`, locale);
12794
- const ownerNote = ownerInfo ? agentT(t2, "ai.agent.ownerNote", locale, {
12795
- displayName: ownerInfo.displayName,
12796
- platformId: ownerInfo.platformId,
12797
- chatContext
12798
- }) : "";
12799
- const memberNote = isPrivate ? agentT(t2, "ai.agent.memberNotePrivate", locale) : agentT(t2, "ai.agent.memberNoteGroup", locale);
12800
- const mentionedMembersNote = mentionedMembers && mentionedMembers.length > 0 ? `${agentT(t2, "ai.agent.mentionedMembersNote", locale)}
12801
- ${mentionedMembers.map((member) => {
12802
- const aliasPart = member.aliases.length > 0 ? ` | aliases=${member.aliases.join(",")}` : "";
12803
- return `- member_id=${member.memberId} | mention=${member.mentionText} | display_name=${member.displayName} | platform_id=${member.platformId}${aliasPart}`;
12804
- }).join("\n")}
12805
- ` : "";
12806
- const year = now.getFullYear();
12807
- const dataSnapshotNote = dataSnapshot ? `${formatDataSnapshotContext(t2, dataSnapshot, locale)}
12808
- ` : "";
12809
- return `${agentT(t2, "ai.agent.currentDateIs", locale)} ${currentDate}\u3002
12810
- ${ownerNote}
12811
- ${mentionedMembersNote}
12812
- ${dataSnapshotNote}
12813
- ${memberNote}
12814
- ${agentT(t2, "ai.agent.timeParamsIntro", locale)}
12815
- ${agentT(t2, "ai.agent.defaultYearNote", locale, { year })}
12816
- ${agentT(t2, "ai.agent.evidencePolicy", locale)}
12817
-
12818
- ${agentT(t2, "ai.agent.responseInstruction", locale)}`;
12819
- }
12820
- function getFallbackRoleDefinition(t2, chatType, locale) {
12821
- return agentT(t2, `ai.agent.fallbackRoleDefinition.${chatType}`, locale);
12822
- }
12823
- function formatTimestamp(timestamp, locale) {
12824
- if (!timestamp) return locale.startsWith("zh") ? "\u672A\u77E5" : "unknown";
12825
- const date = new Date(timestamp * 1e3);
12826
- if (Number.isNaN(date.getTime())) return locale.startsWith("zh") ? "\u672A\u77E5" : "unknown";
12827
- const dateLocale = locale.startsWith("zh") ? "zh-CN" : locale;
12828
- return date.toLocaleString(dateLocale, {
12829
- year: "numeric",
12830
- month: "2-digit",
12831
- day: "2-digit",
12832
- hour: "2-digit",
12833
- minute: "2-digit"
12834
- });
12835
- }
12836
- function buildSystemPrompt(options) {
12837
- const {
12838
- t: t2,
12839
- chatType = "group",
12840
- assistantSystemPrompt,
12841
- ownerInfo,
12842
- locale = "zh-CN",
12843
- skillCtx,
12844
- mentionedMembers,
12845
- dataSnapshot
12846
- } = options;
12847
- const systemPrompt = assistantSystemPrompt || getFallbackRoleDefinition(t2, chatType, locale);
12848
- const lockedSection = getLockedPromptSection(t2, chatType, ownerInfo, locale, mentionedMembers, dataSnapshot);
12849
- let skillSection = "";
12850
- if (skillCtx?.skillDef) {
12851
- skillSection = `
12852
- ## ${agentT(t2, "ai.agent.currentTask", locale)}\uFF1A${skillCtx.skillDef.name}
12853
- ${agentT(t2, "ai.agent.skillPriorityNote", locale)}
12854
- ` + skillCtx.skillDef.prompt;
12855
- } else if (skillCtx?.skillMenu) {
12856
- skillSection = `
12857
- ${skillCtx.skillMenu}`;
12858
- }
12859
- return `${systemPrompt}${skillSection}
12860
-
12861
- ${lockedSection}`;
12862
- }
12863
-
12864
14193
  // ../../packages/node-runtime/src/ai/i18n/locales/zh-CN.ts
12865
14194
  var zh_CN_default = {
12866
14195
  ai: {
@@ -14228,113 +15557,684 @@ var zh_TW_default = {
14228
15557
  - last_message_time: {{lastMessageTime}} (\u8CC7\u6599\u5EAB\u4E2D\u5DF2\u532F\u5165\u8A0A\u606F\u7684\u622A\u6B62\u6642\u9593\uFF0C\u4E0D\u4EE3\u8868\u7FA4\u7D44/\u5C0D\u8A71\u7576\u524D\u662F\u5426\u6D3B\u8E8D)
14229
15558
  - segment_summaries_available: {{segmentSummaryCount}}
14230
15559
 
14231
- {{memberHintTitle}}
14232
- {{memberHintLines}}
15560
+ {{memberHintTitle}}
15561
+ {{memberHintLines}}
15562
+
15563
+ \u4F7F\u7528\u898F\u5247\uFF1A
15564
+ {{usageRules}}`,
15565
+ dataSnapshotMemberHintsAll: "\u6D3B\u8E8D\u6210\u54E1\u67E5\u8A62\u63D0\u793A\uFF08\u5168\u90E8\u6210\u54E1\uFF09\uFF1A",
15566
+ dataSnapshotMemberHintsTop: "\u6D3B\u8E8D\u6210\u54E1\u67E5\u8A62\u63D0\u793A\uFF08\u6309\u6B77\u53F2\u7E3D\u8A0A\u606F\u91CF Top 10\uFF09\uFF1A",
15567
+ dataSnapshotMemberHintsUnavailable: "\u6D3B\u8E8D\u6210\u54E1\u67E5\u8A62\u63D0\u793A\uFF1A",
15568
+ dataSnapshotMemberHintsEmpty: "\u7121\u53EF\u7528\u6210\u54E1\u63D0\u793A\u3002",
15569
+ dataSnapshotUsageRules: `- member_id \u662F\u5DE5\u5177\u67E5\u8A62\u63D0\u793A\uFF1Bdisplay_name \u50C5\u7528\u65BC\u4EBA\u985E\u8B58\u5225\uFF0C\u53EF\u80FD\u4E0D\u552F\u4E00\u3002
15570
+ - \u4E0D\u8981\u5728\u6700\u7D42\u56DE\u7B54\u4E2D\u4E3B\u52D5\u66B4\u9732 member_id \u6216\u555F\u52D5\u4E0A\u4E0B\u6587\u672C\u8EAB\uFF0C\u9664\u975E\u4F7F\u7528\u8005\u660E\u78BA\u8981\u6C42\u6280\u8853\u7D30\u7BC0\u3002
15571
+ - \u6D3B\u8E8D\u6210\u54E1\u6392\u884C\u53EA\u4EE3\u8868\u6B77\u53F2\u7E3D\u8A0A\u606F\u91CF\uFF0C\u4E0D\u4EE3\u8868\u8FD1\u671F\u6D3B\u8E8D\u60C5\u6CC1\uFF1B\u4E5F\u4E0D\u8DB3\u4EE5\u4F5C\u70BA\u5F71\u97FF\u529B\u3001\u95DC\u4FC2\u6216\u8FD1\u671F\u8DA8\u52E2\u7D50\u8AD6\u7684\u8B49\u64DA\u3002
15572
+ - \u76F8\u5C0D\u6642\u9593\u8868\u9054\u4EE5\u771F\u5BE6\u76EE\u524D\u65E5\u671F\u70BA\u57FA\u6E96\uFF0C\u800C\u4E0D\u662F\u8CC7\u6599\u5EAB\u6700\u5F8C\u8A0A\u606F\u6642\u9593\u3002
15573
+ -\u300C\u6700\u8FD1\u4E00\u5E74/\u904E\u53BB\u4E00\u5E74\u300D\u8868\u793A\u5F9E\u771F\u5BE6\u76EE\u524D\u65E5\u671F\u56DE\u63A8\u4E00\u5E74\u5230\u4ECA\u5929\uFF1B\u300C\u53BB\u5E74\u300D\u8868\u793A\u4E0A\u4E00\u81EA\u7136\u5E74\u3002
15574
+ - \u8CC7\u6599\u5EAB\u6642\u9593\u908A\u754C\u53EA\u7528\u65BC\u8AAA\u660E\u8986\u84CB\u7BC4\u570D\uFF0C\u4E0D\u7528\u65BC\u91CD\u5B9A\u7FA9\u4F7F\u7528\u8005\u8981\u6C42\u7684\u6642\u9593\u7BC4\u570D\u3002
15575
+ - \u4F7F\u7528\u9810\u8A2D\u300C\u8FD1 N \u5929\u300D\u7684\u5DE5\u5177\u6642\uFF0C\u5148\u9078\u64C7\u8207\u8CC7\u6599\u5EAB\u6642\u9593\u908A\u754C\u6709\u4EA4\u96C6\u7684\u7BC4\u570D\uFF0C\u4E0D\u8981\u5148\u63A2\u6E2C\u771F\u5BE6\u76EE\u524D\u65E5\u671F\u8996\u7A97\u5C0E\u81F4\u7A7A\u7D50\u679C\u3002
15576
+ - \u4E0D\u8981\u53EA\u70BA\u4E86\u91CD\u65B0\u767C\u73FE min/max timestamp \u547C\u53EB\u5DE5\u5177\uFF1B\u4F46\u56DE\u7B54\u5177\u9AD4\u804A\u5929\u4E8B\u5BE6\u3001\u7D71\u8A08\u548C\u7D50\u8AD6\u4ECD\u5FC5\u9808\u547C\u53EB\u5DE5\u5177\u53D6\u5F97\u8B49\u64DA\u3002
15577
+ - last_message_time \u662F\u8CC7\u6599\u5EAB\u4E2D\u5DF2\u532F\u5165\u8A0A\u606F\u7684\u622A\u6B62\u6642\u9593\uFF0C\u4E0D\u662F\u7FA4\u7D44/\u5C0D\u8A71\u5728\u73FE\u5BE6\u4E2D\u6700\u5F8C\u4E00\u6B21\u767C\u8A00\u7684\u6642\u9593\uFF1B\u4F7F\u7528\u8005\u53EF\u80FD\u53EA\u662F\u5C1A\u672A\u532F\u5165\u66F4\u65B0\u7684\u7D00\u9304\u3002\u4E0D\u8981\u64DA\u6B64\u63A8\u65B7\u7FA4\u7D44\u300C\u591A\u4E45\u6C92\u52D5\u975C\u300D\uFF0C\u66F4\u4E0D\u8981\u4E3B\u52D5\u5EFA\u8B70\u4F7F\u7528\u8005\u53BB\u300C\u559A\u9192\u300D\u6216\u300C\u6FC0\u6D3B\u300D\u7FA4\u7D44\u3002`,
15578
+ evidencePolicy: `\u8B49\u64DA\u7B56\u7565\uFF1A
15579
+ - AI \u5C0D\u8A71\u6B77\u53F2\u3001\u6B77\u53F2 AI \u56DE\u8986\u548C\u58D3\u7E2E\u6458\u8981\u53EA\u7528\u65BC\u7406\u89E3\u4F7F\u7528\u8005\u610F\u5716\uFF0C\u4E0D\u80FD\u4F5C\u70BA\u804A\u5929\u7D00\u9304\u4E8B\u5BE6\u8B49\u64DA\u3002
15580
+ - \u53EA\u8981\u4F7F\u7528\u8005\u8A62\u554F\u804A\u5929\u7D00\u9304\u5167\u5BB9\u3001\u6700\u8FD1\u804A\u4EC0\u9EBC\u3001\u67D0\u4EBA\u8AAA\u904E\u4EC0\u9EBC\u3001\u7D71\u8A08\u6392\u884C\u3001\u662F\u5426\u51FA\u73FE\u904E\u67D0\u8A71\u984C\u6216\u8981\u6C42\u5F15\u7528\u539F\u8A71\uFF0C\u5FC5\u9808\u5148\u547C\u53EB\u5408\u9069\u7684\u8CC7\u6599\u5DE5\u5177\u6AA2\u7D22\u76EE\u524D\u8CC7\u6599\u5EAB\u3002
15581
+ - \u5982\u679C\u5DF2\u8FFD\u52A0\u65B0\u804A\u5929\u7D00\u9304\uFF0C\u76EE\u524D\u8CC7\u6599\u5EAB\u53EF\u80FD\u4E0D\u540C\u65BC\u820A\u8996\u7A97\u6B77\u53F2\uFF1B\u512A\u5148\u4F7F\u7528 get_chat_overview \u6216 get_recent_messages/search_messages \u53D6\u5F97\u6700\u65B0\u8B49\u64DA\u3002
15582
+ - \u53EA\u6709\u5DE5\u5177\u56DE\u50B3\u7684\u8A0A\u606F\u3001\u7D71\u8A08\u6216\u8CC7\u6599\u5EAB\u6982\u89BD\u53EF\u4EE5\u4F5C\u70BA\u4E8B\u5BE6\u4F9D\u64DA\uFF1B\u5DE5\u5177\u6C92\u6709\u56DE\u50B3\u8DB3\u5920\u8B49\u64DA\u6642\uFF0C\u660E\u78BA\u8AAA\u660E\u8CC7\u6599\u4E0D\u8DB3\uFF0C\u4E0D\u8981\u7DE8\u9020\u4E0D\u5B58\u5728\u7684\u804A\u5929\u7D00\u9304\u3002`,
15583
+ responseInstruction: "\u6839\u64DA\u4F7F\u7528\u8005\u7684\u554F\u984C\uFF0C\u5728\u9700\u8981\u6642\u9078\u64C7\u5408\u9069\u7684\u5DE5\u5177\u53D6\u5F97\u8CC7\u6599\uFF0C\u7136\u5F8C\u81EA\u7136\u3001\u76F4\u63A5\u5730\u7D66\u51FA\u56DE\u7B54\u3002",
15584
+ fallbackRoleDefinition: {
15585
+ group: `\u4F60\u662F ChatLab \u88E1\u7684\u7FA4\u804A\u8A18\u9304\u5206\u6790\u642D\u6A94\uFF0C\u5E6B\u52A9\u4F7F\u7528\u8005\u5F9E\u804A\u5929\u8A18\u9304\u4E2D\u7406\u89E3\u4E8B\u5BE6\u3001\u8B8A\u5316\u548C\u4E92\u52D5\u6A21\u5F0F\uFF0C\u800C\u4E0D\u662F\u6A5F\u68B0\u5730\u7522\u751F\u5206\u6790\u5831\u544A\u3002
15586
+
15587
+ ## \u4EA4\u6D41\u65B9\u5F0F
15588
+ - \u8DDF\u96A8\u4F7F\u7528\u8005\u7684\u8A9E\u8A00\u548C\u4EA4\u6D41\u65B9\u5F0F\uFF0C\u8AAA\u81EA\u7136\u3001\u76F4\u63A5\u7684\u4EBA\u8A71\uFF0C\u907F\u514D\u5BA2\u670D\u8154\u548C\u904E\u5EA6\u6B63\u5F0F\u7684\u8868\u9054\u3002
15589
+ - \u7C21\u55AE\u554F\u984C\u76F4\u63A5\u56DE\u7B54\uFF1B\u8907\u96DC\u554F\u984C\u518D\u5206\u5C64\u8AAA\u660E\u3002\u6A19\u984C\u3001\u5217\u8868\u548C\u8868\u683C\u53EA\u5728\u6709\u52A9\u65BC\u7406\u89E3\u6642\u4F7F\u7528\u3002
15590
+ - \u5728\u8B49\u64DA\u5141\u8A31\u6642\u7D66\u51FA\u660E\u78BA\u5224\u65B7\uFF1B\u7121\u6CD5\u78BA\u8A8D\u7684\u90E8\u5206\u8981\u5766\u7387\u8AAA\u660E\u3002
15591
+ - \u53EF\u4EE5\u6709\u4E00\u9EDE\u81EA\u7136\u7684\u5E7D\u9ED8\u611F\uFF0C\u4F46\u4E0D\u8981\u5F37\u884C\u73A9\u6897\u3001\u5806\u8868\u60C5\u6216\u72A7\u7272\u6E96\u78BA\u6027\u3002
15592
+
15593
+ ## \u5206\u6790\u908A\u754C
15594
+ - \u5340\u5206\u80FD\u5920\u78BA\u8A8D\u7684\u4E8B\u5BE6\u3001\u57FA\u65BC\u8B49\u64DA\u7684\u63A8\u8AD6\uFF0C\u4EE5\u53CA\u66AB\u6642\u7121\u6CD5\u5224\u65B7\u7684\u90E8\u5206\u3002
15595
+ - \u6D89\u53CA\u6027\u683C\u3001\u60C5\u7DD2\u3001\u95DC\u4FC2\u548C\u52D5\u6A5F\u6642\u5C24\u5176\u8B39\u614E\uFF0C\u4E0D\u7DE8\u9020\u5FC3\u7406\u6D3B\u52D5\uFF0C\u4E5F\u4E0D\u8F15\u6613\u7D66\u4EBA\u8CBC\u6A19\u7C64\u3002
15596
+ - \u9069\u91CF\u5F15\u7528\u6709\u4EE3\u8868\u6027\u7684\u539F\u8A71\u548C\u8CC7\u6599\uFF0C\u4E0D\u8981\u5927\u6BB5\u5806\u780C\u804A\u5929\u8A18\u9304\u3002
15597
+ - \u9664\u975E\u4F7F\u7528\u8005\u660E\u78BA\u8981\u6C42\uFF0C\u4E0D\u8AAA\u6559\u3001\u4E0D\u64C5\u81EA\u7D66\u4EBA\u751F\u5EFA\u8B70\uFF0C\u4E5F\u4E0D\u628A\u6BCF\u6B21\u56DE\u7B54\u90FD\u5BEB\u6210\u7E3D\u7D50\u5831\u544A\u3002`,
15598
+ private: `\u4F60\u662F ChatLab \u88E1\u7684\u79C1\u804A\u8A18\u9304\u5206\u6790\u642D\u6A94\uFF0C\u5E6B\u52A9\u4F7F\u7528\u8005\u5F9E\u804A\u5929\u8A18\u9304\u4E2D\u7406\u89E3\u4E8B\u5BE6\u3001\u8B8A\u5316\u548C\u4E92\u52D5\u65B9\u5F0F\uFF0C\u800C\u4E0D\u662F\u6A5F\u68B0\u5730\u7522\u751F\u5206\u6790\u5831\u544A\u3002
15599
+
15600
+ ## \u4EA4\u6D41\u65B9\u5F0F
15601
+ - \u8DDF\u96A8\u4F7F\u7528\u8005\u7684\u8A9E\u8A00\u548C\u4EA4\u6D41\u65B9\u5F0F\uFF0C\u8AAA\u81EA\u7136\u3001\u76F4\u63A5\u7684\u4EBA\u8A71\uFF0C\u907F\u514D\u5BA2\u670D\u8154\u548C\u904E\u5EA6\u6B63\u5F0F\u7684\u8868\u9054\u3002
15602
+ - \u7C21\u55AE\u554F\u984C\u76F4\u63A5\u56DE\u7B54\uFF1B\u8907\u96DC\u554F\u984C\u518D\u5206\u5C64\u8AAA\u660E\u3002\u6A19\u984C\u3001\u5217\u8868\u548C\u8868\u683C\u53EA\u5728\u6709\u52A9\u65BC\u7406\u89E3\u6642\u4F7F\u7528\u3002
15603
+ - \u5728\u8B49\u64DA\u5141\u8A31\u6642\u7D66\u51FA\u660E\u78BA\u5224\u65B7\uFF1B\u7121\u6CD5\u78BA\u8A8D\u7684\u90E8\u5206\u8981\u5766\u7387\u8AAA\u660E\u3002
15604
+ - \u53EF\u4EE5\u6709\u4E00\u9EDE\u81EA\u7136\u7684\u5E7D\u9ED8\u611F\uFF0C\u4F46\u4E0D\u8981\u5F37\u884C\u73A9\u6897\u3001\u5806\u8868\u60C5\u6216\u72A7\u7272\u6E96\u78BA\u6027\u3002
15605
+
15606
+ ## \u5206\u6790\u908A\u754C
15607
+ - \u5340\u5206\u80FD\u5920\u78BA\u8A8D\u7684\u4E8B\u5BE6\u3001\u57FA\u65BC\u8B49\u64DA\u7684\u63A8\u8AD6\uFF0C\u4EE5\u53CA\u66AB\u6642\u7121\u6CD5\u5224\u65B7\u7684\u90E8\u5206\u3002
15608
+ - \u6D89\u53CA\u6027\u683C\u3001\u60C5\u7DD2\u3001\u95DC\u4FC2\u548C\u52D5\u6A5F\u6642\u5C24\u5176\u8B39\u614E\uFF0C\u4E0D\u7DE8\u9020\u5FC3\u7406\u6D3B\u52D5\uFF0C\u4E5F\u4E0D\u8F15\u6613\u7D66\u4EBA\u8CBC\u6A19\u7C64\u3002
15609
+ - \u9069\u91CF\u5F15\u7528\u6709\u4EE3\u8868\u6027\u7684\u539F\u8A71\u548C\u8CC7\u6599\uFF0C\u4E0D\u8981\u5927\u6BB5\u5806\u780C\u804A\u5929\u8A18\u9304\u3002
15610
+ - \u9664\u975E\u4F7F\u7528\u8005\u660E\u78BA\u8981\u6C42\uFF0C\u4E0D\u8AAA\u6559\u3001\u4E0D\u64C5\u81EA\u7D66\u4EBA\u751F\u5EFA\u8B70\uFF0C\u4E5F\u4E0D\u628A\u6BCF\u6B21\u56DE\u7B54\u90FD\u5BEB\u6210\u7E3D\u7D50\u5831\u544A\u3002`
15611
+ }
15612
+ }
15613
+ },
15614
+ llm: {
15615
+ notConfigured: "LLM \u670D\u52D9\u5C1A\u672A\u8A2D\u5B9A\uFF0C\u8ACB\u5148\u5728\u8A2D\u5B9A\u4E2D\u586B\u5165 API Key",
15616
+ maxConfigs: "\u6700\u591A\u53EA\u80FD\u65B0\u589E {{count}} \u7D44\u8A2D\u5B9A",
15617
+ configNotFound: "\u627E\u4E0D\u5230\u8A2D\u5B9A",
15618
+ noActiveConfig: "\u6C92\u6709\u555F\u7528\u4E2D\u7684\u8A2D\u5B9A",
15619
+ callFailed: "LLM \u547C\u53EB\u5931\u6557\uFF0C\u8ACB\u6AA2\u67E5\u6A21\u578B\u8A2D\u5B9A\u662F\u5426\u6B63\u78BA",
15620
+ genericProviderName: "API \u670D\u52D9",
15621
+ rawErrorLabel: "\u539F\u59CB\u932F\u8AA4"
15622
+ },
15623
+ summary: {
15624
+ segmentNotFound: "\u6703\u8A71\u4E0D\u5B58\u5728\u6216\u8CC7\u6599\u5EAB\u958B\u555F\u5931\u6557",
15625
+ tooFewMessages: "\u8A0A\u606F\u6578\u91CF\u5C11\u65BC {{count}} \u689D\uFF0C\u7121\u9700\u7522\u751F\u6458\u8981",
15626
+ tooFewValidMessages: "\u6709\u6548\u8A0A\u606F\u6578\u91CF\u5C11\u65BC {{count}} \u689D\uFF0C\u7121\u9700\u7522\u751F\u6458\u8981",
15627
+ segmentNotExist: "\u627E\u4E0D\u5230\u6703\u8A71",
15628
+ messagesTooFew: "\u8A0A\u606F\u592A\u5C11",
15629
+ validMessagesTooFew: "\u6709\u6548\u8A0A\u606F\u592A\u5C11",
15630
+ systemPromptDirect: "\u4F60\u662F\u4E00\u500B\u5C0D\u8A71\u6458\u8981\u5C08\u5BB6\uFF0C\u64C5\u9577\u7528\u7C21\u6F54\u7684\u8A9E\u8A00\u7E3D\u7D50\u5C0D\u8A71\u5167\u5BB9\u3002",
15631
+ systemPromptMerge: "\u4F60\u662F\u4E00\u500B\u5C0D\u8A71\u6458\u8981\u5C08\u5BB6\uFF0C\u64C5\u9577\u5C07\u591A\u500B\u6458\u8981\u5408\u4F75\u6210\u4E00\u500B\u9023\u8CAB\u7684\u7E3D\u7D50\u3002"
15632
+ }
15633
+ };
15634
+
15635
+ // ../../packages/node-runtime/src/ai/i18n/index.ts
15636
+ var localeMap = {
15637
+ "zh-CN": zh_CN_default,
15638
+ "en-US": en_US_default,
15639
+ "ja-JP": ja_JP_default,
15640
+ "zh-TW": zh_TW_default
15641
+ };
15642
+ function getNestedValue(obj, path31) {
15643
+ let current = obj;
15644
+ for (const segment2 of path31.split(".")) {
15645
+ if (current == null || typeof current !== "object") return void 0;
15646
+ current = current[segment2];
15647
+ }
15648
+ return current;
15649
+ }
15650
+ function interpolate(template, vars) {
15651
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
15652
+ const val = vars[key];
15653
+ return val != null ? String(val) : "";
15654
+ });
15655
+ }
15656
+ function createAiTranslate(defaultLocale = "zh-CN") {
15657
+ return (key, options) => {
15658
+ const lng = options?.lng || defaultLocale;
15659
+ const locale = localeMap[lng] ? lng : "en-US";
15660
+ const value = getNestedValue(localeMap[locale], key) ?? getNestedValue(localeMap["en-US"], key);
15661
+ if (typeof value !== "string") return key;
15662
+ if (!options) return value;
15663
+ return interpolate(value, options);
15664
+ };
15665
+ }
15666
+ var aiLocales = { "zh-CN": zh_CN_default, "en-US": en_US_default, "ja-JP": ja_JP_default, "zh-TW": zh_TW_default };
15667
+
15668
+ // ../../packages/node-runtime/src/ai/agent/event-handler.ts
15669
+ function estimateTokensFromText(text) {
15670
+ if (!text) return 0;
15671
+ const normalized = text.replace(/\s+/g, " ").trim();
15672
+ if (!normalized) return 0;
15673
+ const cjkCount = (normalized.match(/[\u3400-\u9fff\uf900-\ufaff]/g) || []).length;
15674
+ const latinCount = normalized.length - cjkCount;
15675
+ return Math.max(1, Math.ceil(cjkCount * 1.15 + latinCount / 4));
15676
+ }
15677
+ function extractMessageText(message) {
15678
+ if (message.role === "user") {
15679
+ if (typeof message.content === "string") return message.content;
15680
+ return message.content.map((item) => {
15681
+ if (item.type === "text") return item.text;
15682
+ if (item.type === "image") return "[image]";
15683
+ return "";
15684
+ }).join("\n");
15685
+ }
15686
+ if (message.role === "assistant") {
15687
+ return message.content.map((item) => {
15688
+ if (item.type === "text") return item.text;
15689
+ if (item.type === "thinking") return item.thinking;
15690
+ if (item.type === "toolCall") return `${item.name} ${JSON.stringify(item.arguments || {})}`;
15691
+ return "";
15692
+ }).join("\n");
15693
+ }
15694
+ if (message.role === "toolResult") {
15695
+ return message.content.map((item) => {
15696
+ if (item.type === "text") return item.text;
15697
+ return "[binary]";
15698
+ }).join("\n");
15699
+ }
15700
+ return "";
15701
+ }
15702
+ function estimateContextTokens(systemPrompt, messages, pendingUserMessage) {
15703
+ let tokens = estimateTokensFromText(systemPrompt);
15704
+ for (const message of messages) {
15705
+ if (message.role === "toolResult") continue;
15706
+ tokens += estimateTokensFromText(extractMessageText(message));
15707
+ }
15708
+ if (pendingUserMessage) {
15709
+ tokens += estimateTokensFromText(pendingUserMessage);
15710
+ }
15711
+ return tokens;
15712
+ }
15713
+ var AgentEventHandler = class {
15714
+ toolsUsed = [];
15715
+ toolRounds = 0;
15716
+ totalUsage = {
15717
+ promptTokens: 0,
15718
+ completionTokens: 0,
15719
+ totalTokens: 0,
15720
+ cacheReadTokens: 0,
15721
+ cacheWriteTokens: 0
15722
+ };
15723
+ lastStatusAt = 0;
15724
+ onChunk;
15725
+ context;
15726
+ systemPrompt;
15727
+ activeTools = /* @__PURE__ */ new Map();
15728
+ constructor(config) {
15729
+ this.onChunk = config.onChunk;
15730
+ this.context = config.context;
15731
+ this.systemPrompt = config.systemPrompt;
15732
+ }
15733
+ handleCoreEvent(event, messages) {
15734
+ switch (event.type) {
15735
+ case "content":
15736
+ this.onChunk({ type: "content", content: event.content });
15737
+ this.emitStatus("responding", messages);
15738
+ break;
15739
+ case "thinking_start":
15740
+ this.emitStatus("thinking", messages, { force: true });
15741
+ break;
15742
+ case "thinking_delta":
15743
+ this.onChunk({ type: "think", content: event.content, thinkTag: "thinking" });
15744
+ this.emitStatus("thinking", messages);
15745
+ break;
15746
+ case "thinking_end":
15747
+ this.onChunk({ type: "think", content: "", thinkTag: "thinking", thinkDurationMs: event.durationMs });
15748
+ this.emitStatus("responding", messages, { force: true });
15749
+ break;
15750
+ case "tool_start": {
15751
+ const params = this.normalizeToolParams(event.toolName, event.toolParams);
15752
+ this.toolsUsed.push(event.toolName);
15753
+ this.activeTools.set(event.toolCallId, event.toolName);
15754
+ this.onChunk({ type: "tool_start", toolCallId: event.toolCallId, toolName: event.toolName, toolParams: params });
15755
+ this.emitStatus("tool_running", messages, { currentTool: event.toolName, force: true });
15756
+ break;
15757
+ }
15758
+ case "tool_update":
15759
+ this.onChunk({
15760
+ type: "tool_update",
15761
+ toolCallId: event.toolCallId,
15762
+ toolName: event.toolName,
15763
+ toolProgress: event.progress
15764
+ });
15765
+ break;
15766
+ case "tool_end":
15767
+ this.activeTools.delete(event.toolCallId);
15768
+ this.onChunk({
15769
+ type: "tool_result",
15770
+ toolCallId: event.toolCallId,
15771
+ toolName: event.toolName,
15772
+ toolResult: event.toolResult,
15773
+ toolIsError: event.isError
15774
+ });
15775
+ if (this.activeTools.size > 0) {
15776
+ const currentTool = Array.from(this.activeTools.values()).at(-1);
15777
+ this.emitStatus("tool_running", messages, { currentTool, force: true });
15778
+ } else {
15779
+ this.emitStatus("thinking", messages, { force: true });
15780
+ }
15781
+ break;
15782
+ case "turn_end":
15783
+ this.activeTools.clear();
15784
+ this.toolRounds = event.round;
15785
+ this.onChunk({ type: "turn_end", hadToolCalls: event.hadToolCalls });
15786
+ this.emitStatus("thinking", messages, { force: true });
15787
+ break;
15788
+ case "usage_update":
15789
+ this.totalUsage = { ...event.usage };
15790
+ this.emitStatus("responding", messages, { force: true });
15791
+ break;
15792
+ }
15793
+ }
15794
+ cloneUsage() {
15795
+ return {
15796
+ promptTokens: this.totalUsage.promptTokens,
15797
+ completionTokens: this.totalUsage.completionTokens,
15798
+ totalTokens: this.totalUsage.totalTokens,
15799
+ cacheReadTokens: this.totalUsage.cacheReadTokens,
15800
+ cacheWriteTokens: this.totalUsage.cacheWriteTokens
15801
+ };
15802
+ }
15803
+ emitStatus(phase, messages, options) {
15804
+ const now = Date.now();
15805
+ if (!options?.force && now - this.lastStatusAt < 240) return;
15806
+ this.lastStatusAt = now;
15807
+ const contextTokens = estimateContextTokens(this.systemPrompt, messages, options?.pendingUserMessage);
15808
+ const status = {
15809
+ phase,
15810
+ round: this.toolRounds,
15811
+ toolsUsed: this.toolsUsed.length,
15812
+ currentTool: options?.currentTool,
15813
+ contextTokens,
15814
+ totalUsage: this.cloneUsage(),
15815
+ updatedAt: now
15816
+ };
15817
+ this.onChunk({ type: "status", status });
15818
+ }
15819
+ normalizeToolParams(toolName, params) {
15820
+ const normalized = { ...params };
15821
+ const toolsWithLimit = ["search_messages", "get_recent_messages", "get_conversation_between"];
15822
+ if (this.context.maxMessagesLimit && toolsWithLimit.includes(toolName)) {
15823
+ normalized.limit = this.context.maxMessagesLimit;
15824
+ }
15825
+ if (this.context.timeFilter && (toolName === "search_messages" || toolName === "get_recent_messages")) {
15826
+ normalized._timeFilter = this.context.timeFilter;
15827
+ }
15828
+ return normalized;
15829
+ }
15830
+ };
15831
+
15832
+ // ../../packages/node-runtime/src/ai/cross-chat-agent.ts
15833
+ var CROSS_CHAT_MAX_TOOL_RESULT_TOKENS = 128e3;
15834
+ function resolveCrossChatToolResultTokenBudget(contextWindow) {
15835
+ const maxToolResultPercent = DEFAULT_CONTEXT_COMPRESSION_CONFIG.maxToolResultPercent ?? 50;
15836
+ const proportionalBudget = Math.floor(contextWindow * (maxToolResultPercent / 100));
15837
+ return Math.min(proportionalBudget, CROSS_CHAT_MAX_TOOL_RESULT_TOKENS);
15838
+ }
15839
+ async function runCrossChatAgent(options) {
15840
+ const {
15841
+ userMessage,
15842
+ entityRefs,
15843
+ aiChatId,
15844
+ historyLeafMessageId,
15845
+ locale = "zh-CN",
15846
+ piModel,
15847
+ apiKey,
15848
+ tools,
15849
+ aiChatManager,
15850
+ memoryService,
15851
+ onEvent,
15852
+ abortSignal,
15853
+ thinkingLevel,
15854
+ logger
15855
+ } = options;
15856
+ await initTokenizer();
15857
+ logger?.info("CrossChatAgent", "Cross-chat agent execution started", {
15858
+ aiChatId,
15859
+ entityRefCount: entityRefs?.length ?? 0,
15860
+ toolCount: tools.length
15861
+ });
15862
+ const globalMemoryPrompt = buildGlobalMemoryPrompt(
15863
+ memoryService.list({ scopeType: "global", scopeId: null }),
15864
+ locale,
15865
+ userMessage
15866
+ );
15867
+ const entityMemoryPrompt = buildEntityMemoryPrompt(
15868
+ entityRefs,
15869
+ (scope) => memoryService.list(scope),
15870
+ locale,
15871
+ userMessage
15872
+ );
15873
+ const systemPrompt = buildCrossChatSystemPrompt(locale, /* @__PURE__ */ new Date(), globalMemoryPrompt, entityMemoryPrompt);
15874
+ const handler = new AgentEventHandler({ onChunk: onEvent, context: {}, systemPrompt });
15875
+ let cachedMessages = [];
15876
+ const finishAborted = () => {
15877
+ handler.emitStatus("aborted", cachedMessages, { force: true });
15878
+ onEvent({ type: "done", isFinished: true, usage: handler.cloneUsage() });
15879
+ };
15880
+ if (abortSignal?.aborted) {
15881
+ finishAborted();
15882
+ return;
15883
+ }
15884
+ if (historyLeafMessageId === void 0) {
15885
+ const compressionResult = await checkAndCompress(
15886
+ aiChatId,
15887
+ DEFAULT_CONTEXT_COMPRESSION_CONFIG,
15888
+ systemPrompt,
15889
+ createCompressionLlmAdapter({
15890
+ piModel,
15891
+ apiKey,
15892
+ onCompressing: () => handler.emitStatus("compressing", [])
15893
+ }),
15894
+ aiChatManager,
15895
+ logger,
15896
+ { signal: abortSignal }
15897
+ );
15898
+ if (compressionResult.compressed) {
15899
+ onEvent({
15900
+ type: "compression_done",
15901
+ compressionResult: {
15902
+ summaryContent: compressionResult.summaryContent ?? "",
15903
+ tokensBefore: compressionResult.tokensBefore ?? 0,
15904
+ tokensAfter: compressionResult.tokensAfter ?? 0,
15905
+ timestamp: Date.now()
15906
+ }
15907
+ });
15908
+ }
15909
+ }
15910
+ if (abortSignal?.aborted) {
15911
+ finishAborted();
15912
+ return;
15913
+ }
15914
+ const history = aiChatManager.getHistoryForAgent(aiChatId, void 0, historyLeafMessageId);
15915
+ const modelUserMessage = appendEntityRefsForModel(userMessage, entityRefs);
15916
+ handler.emitStatus("preparing", [], { pendingUserMessage: modelUserMessage, force: true });
15917
+ try {
15918
+ const routeInput = {
15919
+ userMessage: modelUserMessage,
15920
+ chatType: "group",
15921
+ locale,
15922
+ availableTools: tools.map((tool) => tool.name),
15923
+ availableCapabilities: []
15924
+ };
15925
+ const routeDecision = await decideRequestRoute(routeInput, {
15926
+ llmRouter: createLlmRouteDecider({ piModel, apiKey, abortSignal })
15927
+ });
15928
+ onEvent({ type: "route", routeDecision });
15929
+ let effectiveSystemPrompt = systemPrompt;
15930
+ if (routeDecision.route === "planned_execution") {
15931
+ const planner = createAnalysisPlanner({
15932
+ piModel,
15933
+ apiKey,
15934
+ onPlanDelta: (delta) => onEvent({ type: "plan_delta", planDelta: delta }),
15935
+ onThinkingDelta: (delta) => onEvent({ type: "think", content: delta, thinkTag: "thinking" }),
15936
+ onThinkingEnd: (durationMs) => onEvent({ type: "think", content: "", thinkTag: "thinking", thinkDurationMs: durationMs }),
15937
+ onValidationDelta: (delta) => onEvent({ type: "think", content: delta, thinkTag: "plan_validation" }),
15938
+ onValidationEnd: (durationMs) => onEvent({ type: "think", content: "", thinkTag: "plan_validation", thinkDurationMs: durationMs })
15939
+ });
15940
+ const plan = await planner(routeInput, abortSignal);
15941
+ if (plan) {
15942
+ onEvent({ type: "plan", plan: createPlanContentBlock(plan) });
15943
+ effectiveSystemPrompt = `${systemPrompt}
15944
+
15945
+ ${buildPlanGuidance(plan)}`;
15946
+ } else {
15947
+ onEvent({ type: "plan_skipped" });
15948
+ }
15949
+ }
15950
+ const result = await runAgentCore({
15951
+ piModel,
15952
+ apiKey,
15953
+ systemPrompt: effectiveSystemPrompt,
15954
+ tools,
15955
+ history,
15956
+ userMessage: modelUserMessage,
15957
+ maxToolRounds: DEFAULT_MAX_TOOL_ROUNDS,
15958
+ abortSignal,
15959
+ providerSessionId: aiChatId,
15960
+ steerMessage: createAiTranslate(locale)("ai.agent.answerWithoutTools"),
15961
+ thinkingLevel,
15962
+ onConvertToLlm: (messages) => {
15963
+ cachedMessages = messages;
15964
+ },
15965
+ onEvent: (event) => handler.handleCoreEvent(event, cachedMessages),
15966
+ onDebugContext: (messages) => {
15967
+ try {
15968
+ aiChatManager.setPendingDebugContext(aiChatId, JSON.stringify(messages, null, 2));
15969
+ } catch {
15970
+ }
15971
+ }
15972
+ });
15973
+ if (abortSignal?.aborted) {
15974
+ finishAborted();
15975
+ return;
15976
+ }
15977
+ if (result.error) {
15978
+ onEvent({ type: "error", error: { name: "AgentError", message: formatAIError(result.error) } });
15979
+ }
15980
+ handler.emitStatus("completed", cachedMessages, { force: true });
15981
+ logger?.info("CrossChatAgent", "Cross-chat agent execution completed", {
15982
+ aiChatId,
15983
+ toolRounds: result.toolRounds,
15984
+ toolsUsed: result.toolsUsed.length
15985
+ });
15986
+ onEvent({ type: "done", isFinished: true, usage: result.usage });
15987
+ } catch (error) {
15988
+ if (abortSignal?.aborted) {
15989
+ finishAborted();
15990
+ return;
15991
+ }
15992
+ logger?.error("CrossChatAgent", "Cross-chat agent execution failed", error);
15993
+ handler.emitStatus("error", cachedMessages, { force: true });
15994
+ onEvent({ type: "error", error: { name: "AgentError", message: formatAIError(error) } });
15995
+ onEvent({ type: "done", isFinished: true, usage: handler.cloneUsage() });
15996
+ }
15997
+ }
15998
+ function buildCrossChatSystemPrompt(locale = "zh-CN", now = /* @__PURE__ */ new Date(), globalMemoryPrompt = "", entityMemoryPrompt = "") {
15999
+ const dateLocale = locale.startsWith("zh") ? "zh-CN" : "en-US";
16000
+ const currentDate = now.toLocaleDateString(dateLocale, {
16001
+ year: "numeric",
16002
+ month: "long",
16003
+ day: "numeric",
16004
+ weekday: "long"
16005
+ });
16006
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || "local";
16007
+ if (locale.startsWith("zh")) {
16008
+ return `\u4F60\u662F ChatLab \u7684\u8DE8\u5BF9\u8BDD\u5206\u6790\u52A9\u624B\u3002\u4F60\u53EF\u4EE5\u6309\u7528\u6237\u5F53\u524D\u95EE\u9898\uFF0C\u5728\u5176\u672C\u5730\u804A\u5929\u6570\u636E\u5E93\u4E2D\u6309\u9700\u68C0\u7D22\u591A\u4E2A\u8054\u7CFB\u4EBA\u548C\u7FA4\u804A\u3002
16009
+
16010
+ \u6570\u636E\u4E0E\u8303\u56F4\u89C4\u5219\uFF1A
16011
+ - \u5F53\u524D\u65E5\u671F\u662F ${currentDate}\uFF08\u65F6\u533A\uFF1A${timeZone}\uFF09\u3002\u7528\u6237\u7684\u76F8\u5BF9\u65F6\u95F4\u8868\u8FBE\u4EE5\u771F\u5B9E\u5F53\u524D\u65F6\u95F4\u4E3A\u57FA\u51C6\uFF1B\u6570\u636E\u5E93\u622A\u6B62\u65F6\u95F4\u53EA\u8BF4\u660E\u5DF2\u5BFC\u5165\u6570\u636E\u7684\u8986\u76D6\u8303\u56F4\u3002
16012
+ - \u7528\u6237\u5DF2\u6388\u6743\u4F60\u67E5\u8BE2\u5168\u90E8\u672C\u5730\u804A\u5929\u6570\u636E\uFF0C\u4F46\u53EA\u80FD\u4E3A\u56DE\u7B54\u5F53\u524D\u95EE\u9898\u6309\u9700\u8C03\u7528\u5DE5\u5177\uFF0C\u7981\u6B62\u65E0\u76EE\u7684\u904D\u5386\u3002
16013
+ - <chatlab_entity_refs> \u662F\u754C\u9762\u9009\u62E9\u5668\u5199\u5165\u7684\u7A33\u5B9A\u5B9E\u4F53\u5F15\u7528\uFF0C\u6D89\u53CA\u8FD9\u4E9B\u5B9E\u4F53\u65F6\u76F4\u63A5\u8C03\u7528 resolve_chat_entities\u3002\u7528\u6237\u53EA\u8F93\u5165\u8054\u7CFB\u4EBA\u540D\u79F0\u65F6\uFF0C\u4E5F\u8981\u7528 resolve_chat_entities \u67E5\u8BE2\u8054\u7CFB\u4EBA\u76EE\u5F55\uFF1A\u552F\u4E00\u5019\u9009\u81EA\u52A8\u7EE7\u7EED\uFF0C\u591A\u4E2A\u5019\u9009\u5FC5\u987B\u505C\u4E0B\u6765\u8BF7\u7528\u6237\u786E\u8BA4\uFF0C\u6CA1\u6709\u5019\u9009\u65F6\u518D\u63D0\u793A\u8865\u5145\u4FE1\u606F\uFF1B\u7981\u6B62\u5728\u591A\u4E2A\u5019\u9009\u4E2D\u81EA\u884C\u731C\u6D4B\u3002
16014
+ - \u5F53\u524D\u7528\u6237\u6D88\u606F\u4E2D\u7684 <chatlab_entity_refs> \u662F\u672C\u8F6E\u6700\u65B0\u9009\u62E9\u3002\u7528\u6237\u8BF4\u201C\u5F53\u524D\u5BF9\u8C61\u201D\u201C\u6240\u9009\u5B9E\u4F53\u201D\u7B49\u6307\u4EE3\u65F6\uFF0C\u53EA\u4F7F\u7528\u672C\u8F6E\u5F15\u7528\uFF0C\u4E0D\u5F97\u590D\u7528\u5386\u53F2\u6D88\u606F\u4E2D\u7684\u65E7\u5B9E\u4F53\uFF1B\u672C\u8F6E\u540C\u65F6\u9009\u62E9\u591A\u4E2A\u5B9E\u4F53\u65F6\u53EF\u4EE5\u6309\u95EE\u9898\u4F7F\u7528\u5176\u4E2D\u4EFB\u610F\u4E00\u4E2A\u3002
16015
+ - \u5BF9\u8BDD\u5386\u53F2\u4E2D\u7684\u5B9E\u4F53\u5F15\u7528\u53EA\u5E2E\u52A9\u7406\u89E3\u4E0A\u4E0B\u6587\uFF0C\u4E0D\u6784\u6210\u6C38\u4E45\u9501\u5B9A\u8303\u56F4\u3002\u6BCF\u4E00\u8F6E\u6839\u636E\u7528\u6237\u8BED\u4E49\u51B3\u5B9A\u7EE7\u7EED\u539F\u5BF9\u8C61\u3001\u5207\u6362\u5BF9\u8C61\u6216\u6267\u884C\u5168\u5C40\u53D1\u73B0\u3002
16016
+ - \u8054\u7CFB\u4EBA\u9ED8\u8BA4\u8986\u76D6\u5176\u5B9E\u9645\u53C2\u4E0E\u7684\u79C1\u804A\u548C\u7FA4\u804A\u3002\u591A\u4E2A\u5BF9\u8C61\u65E2\u53EF\u80FD\u662F\u4EA4\u96C6\u3001\u5E76\u96C6\uFF0C\u4E5F\u53EF\u80FD\u9700\u8981\u5206\u522B\u68C0\u7D22\u6BD4\u8F83\uFF1B\u4EE5\u7528\u6237\u8BED\u4E49\u4E3A\u51C6\uFF0C\u4E0D\u8981\u673A\u68B0\u5957\u7528\u4E00\u79CD\u96C6\u5408\u89C4\u5219\u3002
16017
+ - \u5F53\u7528\u6237\u95EE\u201C\u6211\u548C\u67D0\u4EBA\u6700\u8FD1\u804A\u4E86\u4EC0\u4E48\u201D\u6216\u660E\u786E\u8981\u6C42\u56DE\u987E\u4E24\u4EBA\u7684\u79C1\u804A\u5185\u5BB9\u65F6\uFF0C\u53EA\u4F7F\u7528 resolve_chat_entities \u8FD4\u56DE\u7684\u5BF9\u5E94\u79C1\u804A session\uFF0C\u4E0D\u8981\u6DF7\u5165\u8BE5\u8054\u7CFB\u4EBA\u6240\u5728\u7684\u7FA4\u804A\u3002\u7528\u6237\u6CA1\u6709\u7ED9\u51FA\u660E\u786E\u65E5\u671F\u6216\u5929\u6570\u65F6\uFF0C\u8FD9\u91CC\u7684\u201C\u6700\u8FD1\u201D\u6307\u5DF2\u5BFC\u5165\u79C1\u804A\u4E2D\u6700\u65B0\u4E00\u6BB5\u53EF\u7528\u5185\u5BB9\uFF1A\u89E3\u6790\u5230\u552F\u4E00\u79C1\u804A\u540E\u76F4\u63A5\u8C03\u7528 read_recent_session\uFF0C\u4E0D\u8981\u5148\u7528 search_messages_globally \u586B\u6EE1\u8DE8\u5BF9\u8BDD\u8BC1\u636E\u9884\u7B97\u3002\u53EA\u6709\u660E\u786E\u8BF4\u201C\u6700\u8FD1 N \u5929\u201D\u3001\u7ED9\u51FA\u65E5\u671F\u8303\u56F4\u3001\u8981\u6C42\u66F4\u65E9\u5185\u5BB9\u6216\u6DF1\u5EA6\u8C03\u67E5\u65F6\u624D\u6539\u7528 search_messages_globally\u3002\u6CA1\u6709\u5BF9\u5E94\u79C1\u804A\u65F6\u5148\u660E\u786E\u8BF4\u660E\uFF0C\u518D\u8BE2\u95EE\u662F\u5426\u6539\u67E5\u5171\u540C\u7FA4\u3002
16018
+ - \u7528\u6237\u8BE2\u95EE\u67D0\u4E2A\u8054\u7CFB\u4EBA\u51FA\u73B0\u5728\u54EA\u4E9B\u5DF2\u5BFC\u5165\u4F1A\u8BDD\u3001\u5206\u522B\u8BF4\u4E86\u591A\u5C11\u6216\u6700\u8FD1\u5728\u54EA\u91CC\u6D3B\u8DC3\u65F6\uFF0C\u5148\u7528 inspect_contact_sessions \u76D8\u70B9\u6765\u6E90\u3002\u4E0D\u8981\u4EC5\u56E0\u4E3A\u6D88\u606F\u4E2D\u51FA\u73B0\u4E86 @\u8054\u7CFB\u4EBA\u5C31\u673A\u68B0\u8C03\u7528\uFF1B\u666E\u901A\u4E8B\u5B9E\u9898\u4ECD\u6309\u95EE\u9898\u4F7F\u7528\u641C\u7D22\u4E0E\u4E0A\u4E0B\u6587\u5DE5\u5177\u3002
16019
+ - \u7528\u6237\u8BE2\u95EE 2\u20145 \u4EBA\u7684\u5173\u7CFB\u3001\u76F8\u8BC6\u80CC\u666F\u3001\u5171\u540C\u5708\u5B50\u6216\u4E92\u52A8\u53D8\u5316\u65F6\uFF0C\u5148\u7528 inspect_shared_interactions \u627E\u5168\u5458\u5171\u540C\u4F1A\u8BDD\u3001\u9010\u4EBA\u6D3B\u8DC3\u3001\u76F4\u63A5\u56DE\u590D\u3001\u76F8\u90BB\u53D1\u8A00\u548C\u6D88\u606F\u951A\u70B9\uFF1B\u4E0D\u8981\u4E3A\u4E86\u591A\u4EBA\u95EE\u9898\u673A\u68B0\u5730\u5148\u5BF9\u6BCF\u4E2A\u4EBA\u8C03\u7528 inspect_contact_sessions\u3002
16020
+ - \u7528\u6237\u8BE2\u95EE\u201C\u6211\u548C\u8C01\u79C1\u804A\u6700\u591A\u201D\u201C\u79C1\u804A\u9891\u7387\u6392\u884C\u201D\u6216\u201C\u548C\u54EA\u4E9B\u8054\u7CFB\u4EBA\u804A\u5929\u7684\u6D3B\u8DC3\u5929\u6570\u6700\u591A\u201D\u65F6\uFF0C\u76F4\u63A5\u8C03\u7528 rank_private_contacts\uFF0C\u5E76\u6309\u7528\u6237\u7ED9\u51FA\u7684\u81EA\u7136\u5E74\u3001\u65E5\u671F\u6216 recent_days \u7EDF\u8BA1\u3002\u4E0D\u8981\u628A\u201C\u6211\u201D\u89E3\u6790\u6210\u8054\u7CFB\u4EBA\uFF0C\u4E5F\u4E0D\u8981\u7528 search_messages_globally \u7684\u5173\u952E\u8BCD\u547D\u4E2D\u91CF\u3001\u62BD\u6837\u6D88\u606F\u6216 Contacts \u7EFC\u5408\u5206\u6570\u4EE3\u66FF\u5168\u5C40\u6392\u884C\u3002
16021
+ - \u7528\u6237\u8BE2\u95EE\u201C\u6211\u5728\u54EA\u4E9B\u7FA4\u6700\u6D3B\u8DC3\u201D\u65F6\u8C03\u7528 rank_group_sessions(mode=owner_activity)\uFF1B\u8BE2\u95EE\u201C\u54EA\u4E9B\u7FA4\u6700\u70ED\u95F9\u3001\u603B\u6D88\u606F\u6700\u591A\u201D\u65F6\u8C03\u7528 mode=total_activity\u3002\u4E24\u79CD\u53E3\u5F84\u4E0D\u80FD\u4E92\u6362\uFF1B\u53E5\u5B50\u4ECD\u65E0\u6CD5\u5224\u65AD\u662F\u5728\u95EE\u672C\u4EBA\u8FD8\u662F\u7FA4\u6574\u4F53\u65F6\uFF0C\u5148\u7528\u4E00\u53E5\u8BDD\u786E\u8BA4\u3002
16022
+ - \u7528\u6237\u8BE2\u95EE\u81EA\u7136\u5E74\u6216\u6700\u8FD1 365 \u5929\u5185\u672C\u4EBA\u603B\u53D1\u8A00\u91CF\u3001\u6D3B\u8DC3\u5929\u6570\u3001\u79C1\u804A\u8054\u7CFB\u4EBA\u6570\u3001\u6708\u5EA6/\u6BCF\u65E5\u8D8B\u52BF\u3001\u6D88\u606F\u7C7B\u578B\u6216\u6587\u672C\u957F\u5EA6\u65F6\uFF0C\u76F4\u63A5\u8C03\u7528 get_global_activity_summary\u3002\u5B83\u4E0D\u63D0\u4F9B\u8054\u7CFB\u4EBA\u6216\u7FA4\u804A\u6392\u884C\uFF1B\u7528\u6237\u8BE2\u95EE\u6700\u8FD1 30 \u5929\u7B49\u5176\u4ED6\u603B\u4F53\u7A97\u53E3\u65F6\uFF0C\u660E\u786E\u8BF4\u660E\u9996\u7248\u53EA\u652F\u6301\u81EA\u7136\u5E74\u548C\u6700\u8FD1 365 \u5929\uFF0C\u4E0D\u8981\u7528\u5173\u952E\u8BCD\u641C\u7D22\u4F30\u7B97\u603B\u91CF\u3002
16023
+ - \u4E09\u4EBA\u53CA\u4EE5\u4E0A\u7684 inspect_shared_interactions \u7ED3\u679C\u662F\u201C\u6240\u6709\u53C2\u4E0E\u8005\u90FD\u51FA\u73B0\u201D\u7684\u4E25\u683C\u4EA4\u96C6\uFF1B\u5982\u679C\u7528\u6237\u8981\u6BD4\u8F83\u591A\u7EC4\u4E24\u4E24\u5173\u7CFB\uFF0C\u5E94\u6309\u6210\u5458\u5BF9\u5206\u522B\u8C03\u67E5\uFF0C\u4E0D\u8981\u628A\u53EA\u542B\u90E8\u5206\u4EBA\u7684\u4F1A\u8BDD\u6DF7\u8FDB\u5168\u5458\u5171\u540C\u6765\u6E90\u3002
16024
+ - \u53EA\u6709\u7528\u6237\u660E\u786E\u8868\u8FBE\u201C\u5FD8\u4E86\u548C\u8C01\u804A\u8FC7\u201D\u201C\u5728\u6240\u6709\u804A\u5929\u91CC\u627E\u201D\u7B49\u5168\u5C40\u53D1\u73B0\u610F\u56FE\u65F6\uFF0C\u624D\u5141\u8BB8\u4E0D\u5E26 scopes \u8C03\u7528 search_messages_globally\u3002\u5168\u5C40\u53D1\u73B0\u5FC5\u987B\u63D0\u4F9B\u81F3\u5C11\u4E00\u4E2A\u5173\u952E\u8BCD\uFF1B\u5DF2\u7ECF\u9650\u5B9A scopes \u65F6\uFF0C\u53EF\u4EE5\u4E0D\u63D0\u4F9B\u5173\u952E\u8BCD\u6765\u62BD\u53D6\u8FD1\u671F\u6D88\u606F\u6837\u672C\u3002
16025
+ - \u9664\u4E0A\u8FF0\u53CC\u4EBA\u79C1\u804A\u5185\u5BB9\u56DE\u987E\u5916\uFF0C\u7528\u6237\u4F7F\u7528\u201C\u6700\u8FD1\u201D\u201C\u8FD1\u671F\u201D\u7B49\u76F8\u5BF9\u65F6\u95F4\u4F46\u6CA1\u6709\u7ED9\u51FA\u5177\u4F53\u8303\u56F4\u65F6\uFF0C\u7B2C\u4E00\u6B21\u652F\u6301\u65F6\u95F4\u8303\u56F4\u7684\u641C\u7D22\u6216\u7ED3\u6784\u8C03\u67E5\u5DE5\u5177\u8C03\u7528\u5FC5\u987B\u4F20 recent_days=30\uFF0C\u5E76\u5728\u56DE\u7B54\u4E2D\u8BF4\u660E\u6309\u6700\u8FD1 30 \u5929\u7EDF\u8BA1\uFF1B\u7981\u6B62\u6839\u636E\u6570\u636E\u5E93\u622A\u6B62\u65F6\u95F4\u6216 lastMessageTs \u624B\u5DE5\u8BA1\u7B97 start_time\uFF0C\u4E5F\u7981\u6B62\u5148\u626B\u63CF\u5168\u90E8\u5386\u53F2\u518D\u4E8B\u540E\u4E3B\u89C2\u622A\u53D6\u3002\u53EA\u6709 30 \u5929\u5185\u6CA1\u6709\u7ED3\u679C\u65F6\uFF0C\u624D\u80FD\u8BE2\u95EE\u7528\u6237\u662F\u5426\u6269\u5927\u8303\u56F4\u3002
16026
+ - \u7528\u6237\u628A\u81EA\u5DF1\u4F5C\u4E3A\u4E8B\u4EF6\u4E3B\u4F53\uFF08\u4F8B\u5982\u201C\u6211\u6700\u8FD1\u8DDF\u591A\u5C11\u4EBA\u804A\u8FC7\u6211\u4E70\u623F\u201D\uFF09\u65F6\uFF0C\u7B2C\u4E00\u6B21\u641C\u7D22\u5FC5\u987B\u4F20 sender=owner\uFF0C\u628A\u672C\u4EBA\u53D1\u8A00\u4F5C\u4E3A\u68C0\u7D22\u79CD\u5B50\uFF1B\u4E0D\u8981\u5148\u641C\u7D22\u6240\u6709\u4EBA\u7684\u540C\u7C7B\u8BDD\u9898\u3002\u641C\u7D22\u5DE5\u5177\u4F1A\u81EA\u52A8\u9644\u5E26\u5468\u8FB9\u6D88\u606F\u4EE5\u8BC6\u522B\u771F\u6B63\u53C2\u4E0E\u5BF9\u8BDD\u7684\u4EBA\uFF1B\u53EA\u6709\u9700\u8981\u66F4\u6DF1\u539F\u6587\u65F6\u624D\u7EE7\u7EED\u4F7F\u7528 get_cross_chat_message_context\uFF0C\u4E0A\u4E0B\u6587\u4E0D\u9650\u5236\u4E3A\u672C\u4EBA\u53D1\u8A00\u3002
14233
16027
 
14234
- \u4F7F\u7528\u898F\u5247\uFF1A
14235
- {{usageRules}}`,
14236
- dataSnapshotMemberHintsAll: "\u6D3B\u8E8D\u6210\u54E1\u67E5\u8A62\u63D0\u793A\uFF08\u5168\u90E8\u6210\u54E1\uFF09\uFF1A",
14237
- dataSnapshotMemberHintsTop: "\u6D3B\u8E8D\u6210\u54E1\u67E5\u8A62\u63D0\u793A\uFF08\u6309\u6B77\u53F2\u7E3D\u8A0A\u606F\u91CF Top 10\uFF09\uFF1A",
14238
- dataSnapshotMemberHintsUnavailable: "\u6D3B\u8E8D\u6210\u54E1\u67E5\u8A62\u63D0\u793A\uFF1A",
14239
- dataSnapshotMemberHintsEmpty: "\u7121\u53EF\u7528\u6210\u54E1\u63D0\u793A\u3002",
14240
- dataSnapshotUsageRules: `- member_id \u662F\u5DE5\u5177\u67E5\u8A62\u63D0\u793A\uFF1Bdisplay_name \u50C5\u7528\u65BC\u4EBA\u985E\u8B58\u5225\uFF0C\u53EF\u80FD\u4E0D\u552F\u4E00\u3002
14241
- - \u4E0D\u8981\u5728\u6700\u7D42\u56DE\u7B54\u4E2D\u4E3B\u52D5\u66B4\u9732 member_id \u6216\u555F\u52D5\u4E0A\u4E0B\u6587\u672C\u8EAB\uFF0C\u9664\u975E\u4F7F\u7528\u8005\u660E\u78BA\u8981\u6C42\u6280\u8853\u7D30\u7BC0\u3002
14242
- - \u6D3B\u8E8D\u6210\u54E1\u6392\u884C\u53EA\u4EE3\u8868\u6B77\u53F2\u7E3D\u8A0A\u606F\u91CF\uFF0C\u4E0D\u4EE3\u8868\u8FD1\u671F\u6D3B\u8E8D\u60C5\u6CC1\uFF1B\u4E5F\u4E0D\u8DB3\u4EE5\u4F5C\u70BA\u5F71\u97FF\u529B\u3001\u95DC\u4FC2\u6216\u8FD1\u671F\u8DA8\u52E2\u7D50\u8AD6\u7684\u8B49\u64DA\u3002
14243
- - \u76F8\u5C0D\u6642\u9593\u8868\u9054\u4EE5\u771F\u5BE6\u76EE\u524D\u65E5\u671F\u70BA\u57FA\u6E96\uFF0C\u800C\u4E0D\u662F\u8CC7\u6599\u5EAB\u6700\u5F8C\u8A0A\u606F\u6642\u9593\u3002
14244
- -\u300C\u6700\u8FD1\u4E00\u5E74/\u904E\u53BB\u4E00\u5E74\u300D\u8868\u793A\u5F9E\u771F\u5BE6\u76EE\u524D\u65E5\u671F\u56DE\u63A8\u4E00\u5E74\u5230\u4ECA\u5929\uFF1B\u300C\u53BB\u5E74\u300D\u8868\u793A\u4E0A\u4E00\u81EA\u7136\u5E74\u3002
14245
- - \u8CC7\u6599\u5EAB\u6642\u9593\u908A\u754C\u53EA\u7528\u65BC\u8AAA\u660E\u8986\u84CB\u7BC4\u570D\uFF0C\u4E0D\u7528\u65BC\u91CD\u5B9A\u7FA9\u4F7F\u7528\u8005\u8981\u6C42\u7684\u6642\u9593\u7BC4\u570D\u3002
14246
- - \u4F7F\u7528\u9810\u8A2D\u300C\u8FD1 N \u5929\u300D\u7684\u5DE5\u5177\u6642\uFF0C\u5148\u9078\u64C7\u8207\u8CC7\u6599\u5EAB\u6642\u9593\u908A\u754C\u6709\u4EA4\u96C6\u7684\u7BC4\u570D\uFF0C\u4E0D\u8981\u5148\u63A2\u6E2C\u771F\u5BE6\u76EE\u524D\u65E5\u671F\u8996\u7A97\u5C0E\u81F4\u7A7A\u7D50\u679C\u3002
14247
- - \u4E0D\u8981\u53EA\u70BA\u4E86\u91CD\u65B0\u767C\u73FE min/max timestamp \u547C\u53EB\u5DE5\u5177\uFF1B\u4F46\u56DE\u7B54\u5177\u9AD4\u804A\u5929\u4E8B\u5BE6\u3001\u7D71\u8A08\u548C\u7D50\u8AD6\u4ECD\u5FC5\u9808\u547C\u53EB\u5DE5\u5177\u53D6\u5F97\u8B49\u64DA\u3002
14248
- - last_message_time \u662F\u8CC7\u6599\u5EAB\u4E2D\u5DF2\u532F\u5165\u8A0A\u606F\u7684\u622A\u6B62\u6642\u9593\uFF0C\u4E0D\u662F\u7FA4\u7D44/\u5C0D\u8A71\u5728\u73FE\u5BE6\u4E2D\u6700\u5F8C\u4E00\u6B21\u767C\u8A00\u7684\u6642\u9593\uFF1B\u4F7F\u7528\u8005\u53EF\u80FD\u53EA\u662F\u5C1A\u672A\u532F\u5165\u66F4\u65B0\u7684\u7D00\u9304\u3002\u4E0D\u8981\u64DA\u6B64\u63A8\u65B7\u7FA4\u7D44\u300C\u591A\u4E45\u6C92\u52D5\u975C\u300D\uFF0C\u66F4\u4E0D\u8981\u4E3B\u52D5\u5EFA\u8B70\u4F7F\u7528\u8005\u53BB\u300C\u559A\u9192\u300D\u6216\u300C\u6FC0\u6D3B\u300D\u7FA4\u7D44\u3002`,
14249
- evidencePolicy: `\u8B49\u64DA\u7B56\u7565\uFF1A
14250
- - AI \u5C0D\u8A71\u6B77\u53F2\u3001\u6B77\u53F2 AI \u56DE\u8986\u548C\u58D3\u7E2E\u6458\u8981\u53EA\u7528\u65BC\u7406\u89E3\u4F7F\u7528\u8005\u610F\u5716\uFF0C\u4E0D\u80FD\u4F5C\u70BA\u804A\u5929\u7D00\u9304\u4E8B\u5BE6\u8B49\u64DA\u3002
14251
- - \u53EA\u8981\u4F7F\u7528\u8005\u8A62\u554F\u804A\u5929\u7D00\u9304\u5167\u5BB9\u3001\u6700\u8FD1\u804A\u4EC0\u9EBC\u3001\u67D0\u4EBA\u8AAA\u904E\u4EC0\u9EBC\u3001\u7D71\u8A08\u6392\u884C\u3001\u662F\u5426\u51FA\u73FE\u904E\u67D0\u8A71\u984C\u6216\u8981\u6C42\u5F15\u7528\u539F\u8A71\uFF0C\u5FC5\u9808\u5148\u547C\u53EB\u5408\u9069\u7684\u8CC7\u6599\u5DE5\u5177\u6AA2\u7D22\u76EE\u524D\u8CC7\u6599\u5EAB\u3002
14252
- - \u5982\u679C\u5DF2\u8FFD\u52A0\u65B0\u804A\u5929\u7D00\u9304\uFF0C\u76EE\u524D\u8CC7\u6599\u5EAB\u53EF\u80FD\u4E0D\u540C\u65BC\u820A\u8996\u7A97\u6B77\u53F2\uFF1B\u512A\u5148\u4F7F\u7528 get_chat_overview \u6216 get_recent_messages/search_messages \u53D6\u5F97\u6700\u65B0\u8B49\u64DA\u3002
14253
- - \u53EA\u6709\u5DE5\u5177\u56DE\u50B3\u7684\u8A0A\u606F\u3001\u7D71\u8A08\u6216\u8CC7\u6599\u5EAB\u6982\u89BD\u53EF\u4EE5\u4F5C\u70BA\u4E8B\u5BE6\u4F9D\u64DA\uFF1B\u5DE5\u5177\u6C92\u6709\u56DE\u50B3\u8DB3\u5920\u8B49\u64DA\u6642\uFF0C\u660E\u78BA\u8AAA\u660E\u8CC7\u6599\u4E0D\u8DB3\uFF0C\u4E0D\u8981\u7DE8\u9020\u4E0D\u5B58\u5728\u7684\u804A\u5929\u7D00\u9304\u3002`,
14254
- responseInstruction: "\u6839\u64DA\u4F7F\u7528\u8005\u7684\u554F\u984C\uFF0C\u5728\u9700\u8981\u6642\u9078\u64C7\u5408\u9069\u7684\u5DE5\u5177\u53D6\u5F97\u8CC7\u6599\uFF0C\u7136\u5F8C\u81EA\u7136\u3001\u76F4\u63A5\u5730\u7D66\u51FA\u56DE\u7B54\u3002",
14255
- fallbackRoleDefinition: {
14256
- group: `\u4F60\u662F ChatLab \u88E1\u7684\u7FA4\u804A\u8A18\u9304\u5206\u6790\u642D\u6A94\uFF0C\u5E6B\u52A9\u4F7F\u7528\u8005\u5F9E\u804A\u5929\u8A18\u9304\u4E2D\u7406\u89E3\u4E8B\u5BE6\u3001\u8B8A\u5316\u548C\u4E92\u52D5\u6A21\u5F0F\uFF0C\u800C\u4E0D\u662F\u6A5F\u68B0\u5730\u7522\u751F\u5206\u6790\u5831\u544A\u3002
16028
+ \u5DE5\u5177\u4E0E\u7ED3\u8BBA\u89C4\u5219\uFF1A
16029
+ - \u4F60\u53EA\u6709 resolve_chat_entities\u3001read_recent_session\u3001rank_private_contacts\u3001rank_group_sessions\u3001get_global_activity_summary\u3001inspect_contact_sessions\u3001inspect_shared_interactions\u3001search_messages_globally\u3001get_cross_chat_message_context\u3001get_cross_chat_overview\u3001memory_read\u3001memory_write\u3001memory_forget \u5341\u4E09\u4E2A\u5DE5\u5177\u3002\u4E0D\u8981\u58F0\u79F0\u53EF\u4EE5\u4F7F\u7528\u5355\u4F1A\u8BDD SQL\u3001\u8BED\u4E49\u7D22\u5F15\u3001\u6280\u80FD\u3001\u56FE\u8868\u6216\u70ED\u529B\u56FE\u3002
16030
+ - read_recent_session \u662F\u5355\u4E2A\u5DF2\u89E3\u6790\u4F1A\u8BDD\u7684\u8F7B\u91CF\u8FD1\u671F\u56DE\u987E\uFF1A\u5B83\u5185\u90E8\u8BFB\u53D6\u6700\u65B0\u4E00\u5C0F\u6BB5\u539F\u6587\u548C\u6700\u591A 5 \u4E2A\u5DF2\u6709\u6BB5\u843D\u6458\u8981\uFF0C\u6A21\u578B\u4E0D\u80FD\u6307\u5B9A\u6D88\u606F\u9884\u7B97\u3002\u4E0D\u8981\u4EC5\u56E0\u4E3A selection.hasEarlierMessages=true \u5C31\u81EA\u52A8\u6269\u5927\u641C\u7D22\uFF1B\u5148\u56DE\u7B54\u7528\u6237\u5F53\u524D\u7684\u666E\u901A\u8FD1\u671F\u56DE\u987E\uFF0C\u53EA\u6709\u7528\u6237\u8981\u6C42\u5177\u4F53\u65F6\u95F4\u3001\u5173\u952E\u8BCD\u3001\u66F4\u65E9\u5185\u5BB9\u6216\u6DF1\u6316\u65F6\u518D\u4F7F\u7528 search_messages_globally\u3002
16031
+ - inspect_contact_sessions \u8FD4\u56DE\u7684\u662F\u5DF2\u5BFC\u5165\u4F1A\u8BDD\u4E2D\u7684\u672C\u4EBA\u53D1\u8A00\u548C\u6210\u5458\u8868\u7ED3\u6784\u4E8B\u5B9E\uFF0C\u4E0D\u8FD4\u56DE\u804A\u5929\u6B63\u6587\uFF0C\u4E5F\u4E0D\u80FD\u8BC1\u660E\u73B0\u5B9E\u4E16\u754C\u4E2D\u7684\u5168\u90E8\u7FA4\u804A\u3002roster_only \u53EA\u8BF4\u660E\u5BFC\u5165\u6210\u5458\u8868\u8BB0\u5F55\u8FC7\u6B64\u4EBA\uFF1B\u5B58\u5728 nextCursor \u6216 coverage \u4E0D\u5B8C\u6574\u65F6\u4E0D\u80FD\u8868\u8FF0\u6210\u5168\u91CF\u7ED3\u679C\u3002
16032
+ - inspect_shared_interactions \u7684\u7FA4\u540D\u79F0\u3001\u6210\u5458\u7ED3\u6784\u3001\u5171\u540C\u6D3B\u8DC3\u548C\u76F8\u90BB\u53D1\u8A00\u53EA\u662F\u8C03\u67E5\u5BFC\u822A\u4FE1\u53F7\uFF0C\u4E0D\u80FD\u76F4\u63A5\u8BC1\u660E\u540C\u4E8B\u3001\u540C\u5B66\u3001\u4EB2\u5C5E\u3001\u670B\u53CB\u6216\u4EB2\u5BC6\u5173\u7CFB\u3002\u4F18\u5148\u5BF9 direct_reply \u548C proximity \u951A\u70B9\u8C03\u7528 get_cross_chat_message_context \u8BFB\u53D6\u539F\u6587\uFF1B\u56DE\u590D\u76EE\u6807\u53EF\u80FD\u7531 relatedMessageId \u6307\u5411\u8F83\u8FDC\u6D88\u606F\uFF0C\u5FC5\u8981\u65F6\u5206\u522B\u5C55\u5F00\u4E24\u4E2A\u6D88\u606F ID\u3002
16033
+ - rank_private_contacts \u8FD4\u56DE\u5B8C\u6574\u5019\u9009\u626B\u63CF\u4E0B\u7684\u786E\u5B9A\u6027\u79C1\u804A\u8BA1\u6570\u3001\u53CC\u65B9\u6D88\u606F\u5206\u9879\u3001\u6D3B\u8DC3\u5929\u6570\u548C\u8EAB\u4EFD coverage\u3002\u53EA\u6709 coverage.complete=true \u65F6\u624D\u80FD\u79F0\u4E3A\u5168\u5C40\u7EDD\u5BF9\u6392\u884C\uFF1B\u7F3A\u5C11\u6216\u65E0\u6CD5\u89E3\u6790\u201C\u6211\u201D\u3001\u79C1\u804A\u5BF9\u8C61\u6B67\u4E49\u3001\u4F1A\u8BDD\u5931\u8D25\u6216 time_budget \u622A\u65AD\u90FD\u5FC5\u987B\u660E\u786E\u62AB\u9732\u3002\u8BE5\u5DE5\u5177\u4E0D\u8FD4\u56DE\u804A\u5929\u6B63\u6587\uFF0C\u7528\u6237\u7EE7\u7EED\u8FFD\u95EE\u201C\u4E3A\u4EC0\u4E48\u201D\u65F6\u518D\u89E3\u6790\u5BF9\u5E94\u8054\u7CFB\u4EBA\u5E76\u8BFB\u53D6\u5FC5\u8981\u539F\u6587\u3002
16034
+ - rank_group_sessions \u7684 owner_activity \u53EA\u6309\u672C\u4EBA\u6D88\u606F\u91CF\u6392\u5E8F\uFF0C\u7F3A\u5C11\u6216\u65E0\u6CD5\u89E3\u6790\u201C\u6211\u201D\u7684\u7FA4\u662F coverage \u7F3A\u53E3\uFF1Btotal_activity \u6309\u7FA4\u603B\u6D88\u606F\u91CF\u6392\u5E8F\uFF0C\u4E0D\u8981\u6C42 owner \u53EF\u89E3\u6790\u3002\u4E24\u4E2A mode \u90FD\u9ED8\u8BA4\u6392\u9664\u7528\u6237\u6807\u8BB0\u201C\u5F53\u524D\u5BF9\u8BDD\u4E2D\u6CA1\u6709\u6211\u201D\u7684\u4F1A\u8BDD\uFF0C\u5E76\u4E14\u53EA\u6709 coverage.complete=true \u65F6\u624D\u80FD\u79F0\u4E3A\u5B8C\u6574\u5168\u5C40\u6392\u884C\u3002
16035
+ - get_global_activity_summary \u590D\u7528\u672C\u5730\u5E74\u5EA6\u6982\u89C8\u5FEB\u7167\uFF0C\u4E0D\u8BFB\u53D6\u804A\u5929\u6B63\u6587\u3002dataState=fresh \u53EF\u76F4\u63A5\u4F7F\u7528\uFF1Bstale \u53EF\u4F7F\u7528\u4F46\u5FC5\u987B\u62AB\u9732 cache.computedAt\uFF0C\u5E76\u6839\u636E task.status \u8BF4\u660E\u540E\u53F0\u6B63\u5728\u66F4\u65B0\u6216\u5237\u65B0\u5931\u8D25\uFF1Bpreparing \u8868\u793A\u9996\u6B21\u7EDF\u8BA1\u4ECD\u5728\u540E\u53F0\u751F\u6210\uFF0Cfailed \u8868\u793A\u751F\u6210\u5931\u8D25\uFF0C\u4E24\u8005\u90FD\u4E0D\u662F\u96F6\u6570\u636E\u3002\u4E0D\u8981\u5728\u540C\u4E00\u6B21\u56DE\u7B54\u4E2D\u8F6E\u8BE2 preparing \u72B6\u6001\u3002
16036
+ - \u6CA1\u6709\u76F4\u63A5\u56DE\u590D\u6216\u539F\u6587\u8BC1\u636E\u65F6\uFF0C\u53EA\u80FD\u786E\u8BA4\u5171\u540C\u6765\u6E90\u6216\u540C\u573A\u6D3B\u8DC3\uFF0C\u5E76\u660E\u786E\u5177\u4F53\u5173\u7CFB\u4ECD\u4E0D\u786E\u5B9A\u3002\u53EA\u6709 coverage \u5B8C\u6574\u4E14\u65F6\u95F4\u8303\u56F4\u660E\u786E\uFF0C\u624D\u5141\u8BB8\u7ED9\u51FA\u201C\u6CA1\u6709\u53D1\u751F\u8FC7\u201D\u6216\u201C\u6CA1\u6709\u4E92\u52A8\u201D\u7684\u5F3A\u8D1F\u9762\u5224\u65AD\uFF1Bpartial/skipped_budget \u4E0D\u662F\u96F6\u3002
16037
+ - search_messages_globally \u63D0\u4F9B\u5173\u952E\u8BCD\u65F6\u662F\u5B57\u9762 LIKE \u68C0\u7D22\uFF0C\u4E0D\u662F\u8BED\u4E49\u641C\u7D22\u3002\u7528\u6237\u95EE\u201C\u804A\u8FC7\u4EC0\u4E48\u201D\u7B49\u5F00\u653E\u95EE\u9898\u65F6\uFF0C\u5148\u9650\u5B9A\u8054\u7CFB\u4EBA\u6216\u7FA4\u804A scopes\uFF0C\u518D\u65E0\u5173\u952E\u8BCD\u62BD\u53D6\u6307\u5B9A\u65F6\u95F4\u8303\u56F4\u5185\u7684\u6D88\u606F\uFF1B\u5DE5\u5177\u81EA\u884C\u63A7\u5236\u8BC1\u636E\u91CF\uFF0C\u4F18\u5148\u8FD4\u56DE\u79C1\u804A\u8BC1\u636E\uFF0C\u5269\u4F59\u5BB9\u91CF\u6309\u76EE\u6807\u4EBA\u7269\u5728\u7FA4\u804A\u4E2D\u7684\u5339\u914D\u6D3B\u8DC3\u5EA6\u5206\u914D\uFF0C\u5E76\u81EA\u52A8\u9644\u5E26\u547D\u4E2D\u6D88\u606F\u5468\u56F4\u7684\u4E0A\u4E0B\u6587\u3002\u4E0D\u8981\u4F20\u5165\u6D88\u606F\u6570\u91CF\u3001\u4F1A\u8BDD\u6570\u91CF\u6216\u6267\u884C\u65F6\u957F\u9884\u7B97\uFF0C\u4E5F\u4E0D\u8981\u5BF9\u5168\u90E8\u4F1A\u8BDD\u505A\u65E0\u5173\u952E\u8BCD\u626B\u63CF\u3002
16038
+ - \u6BD4\u8F83\u5DF2\u7ECF\u89E3\u6790\u51FA\u7684\u8054\u7CFB\u4EBA\u6216\u7FA4\u804A\u65F6\uFF0C\u7528 get_cross_chat_overview \u4F20\u5165\u4E0E\u95EE\u9898\u4E00\u81F4\u7684\u660E\u786E\u65F6\u95F4\u8303\u56F4\uFF0C\u8BFB\u53D6\u7CBE\u786E\u6D88\u606F\u6570\u3001\u6D3B\u8DC3\u5929\u6570\u3001\u6210\u5458\u6D3B\u52A8\u4E0E owner \u8986\u76D6\uFF1B\u5B83\u4E0D\u8D1F\u8D23\u53D1\u73B0\u6216\u5168\u5C40\u6392\u540D\u3002\u9700\u8981\u89E3\u91CA\u5DEE\u5F02\u65F6\uFF0C\u518D\u68C0\u7D22\u6709\u6765\u6E90\u7684\u539F\u6587\u8BC1\u636E\uFF1B\u9700\u8981\u7406\u89E3\u547D\u4E2D\u6D88\u606F\u65F6\uFF0C\u7528 session_id + message_id \u83B7\u53D6\u4E0A\u4E0B\u6587\u3002
16039
+ - \u5DE5\u5177\u8FD4\u56DE coverage \u548C truncated\u3002\u8986\u76D6\u4E0D\u5B8C\u6574\u6216\u88AB\u622A\u65AD\u65F6\u5FC5\u987B\u660E\u786E\u8BF4\u660E\u62BD\u6837\u8303\u56F4\uFF0C\u7981\u6B62\u628A\u6837\u672C\u8868\u8FF0\u6210\u5168\u91CF\u7ED3\u8BBA\u3002
16040
+ - \u6D89\u53CA\u5355\u4E2A\u8054\u7CFB\u4EBA\u7684\u9650\u65F6\u641C\u7D22\u8FD4\u56DE 0 \u6761\u65F6\uFF0C\u4E0D\u8981\u53EA\u8BF4\u201C\u4EC0\u4E48\u90FD\u6CA1\u6709\u201D\u3002\u5FC5\u987B\u4ECE resolve_chat_entities \u8FD4\u56DE\u7684\u79C1\u804A session \u5143\u6570\u636E\u4E2D\u8BF4\u660E\u6700\u8FD1\u4E00\u6761\u5DF2\u5BFC\u5165\u79C1\u804A\u7684\u5177\u4F53\u65F6\u95F4\uFF0C\u4EE5\u53CA\u5B83\u662F\u5426\u843D\u5728\u67E5\u8BE2\u7A97\u53E3\u4E4B\u5916\uFF1B\u6CA1\u6709\u79C1\u804A session \u6216\u6CA1\u6709\u53EF\u7528\u65F6\u95F4\u65F6\u4E5F\u8981\u660E\u786E\u8BF4\u660E\u3002\u4E4B\u540E\u518D\u8BE2\u95EE\u7528\u6237\u662F\u5426\u6269\u5927\u8303\u56F4\u3002
16041
+ - sender=owner \u65F6\u8FD8\u8981\u68C0\u67E5 coverage.ownerResolution\uFF1B\u5B58\u5728\u672A\u8BBE\u7F6E\u6216\u65E0\u6CD5\u89E3\u6790\u201C\u6211\u201D\u7684\u4F1A\u8BDD\u65F6\uFF0C\u5FC5\u987B\u8BF4\u660E\u5BF9\u5E94\u8986\u76D6\u7F3A\u53E3\uFF0C\u7981\u6B62\u901A\u8FC7\u6635\u79F0\u731C\u6D4B\u672C\u4EBA\u3002
16042
+ - \u5F15\u7528\u8BC1\u636E\u65F6\u8BF4\u660E\u6765\u6E90\u4F1A\u8BDD\uFF1B\u4E0D\u8981\u6CC4\u9732\u5DE5\u5177\u672A\u8FD4\u56DE\u7684\u4FE1\u606F\uFF0C\u4E5F\u4E0D\u8981\u7F16\u9020\u8054\u7CFB\u4EBA\u3001\u7FA4\u804A\u6216\u6D88\u606F\u3002
16043
+ - \u5982\u679C\u95EE\u9898\u4E0D\u9700\u8981\u804A\u5929\u6570\u636E\uFF0C\u76F4\u63A5\u56DE\u7B54\uFF1B\u5982\u679C\u73B0\u6709\u5341\u4E09\u4E2A\u5DE5\u5177\u4E0D\u8DB3\uFF0C\u5766\u8BDA\u8BF4\u660E\u5F53\u524D\u80FD\u529B\u8FB9\u754C\u3002
14257
16044
 
14258
- ## \u4EA4\u6D41\u65B9\u5F0F
14259
- - \u8DDF\u96A8\u4F7F\u7528\u8005\u7684\u8A9E\u8A00\u548C\u4EA4\u6D41\u65B9\u5F0F\uFF0C\u8AAA\u81EA\u7136\u3001\u76F4\u63A5\u7684\u4EBA\u8A71\uFF0C\u907F\u514D\u5BA2\u670D\u8154\u548C\u904E\u5EA6\u6B63\u5F0F\u7684\u8868\u9054\u3002
14260
- - \u7C21\u55AE\u554F\u984C\u76F4\u63A5\u56DE\u7B54\uFF1B\u8907\u96DC\u554F\u984C\u518D\u5206\u5C64\u8AAA\u660E\u3002\u6A19\u984C\u3001\u5217\u8868\u548C\u8868\u683C\u53EA\u5728\u6709\u52A9\u65BC\u7406\u89E3\u6642\u4F7F\u7528\u3002
14261
- - \u5728\u8B49\u64DA\u5141\u8A31\u6642\u7D66\u51FA\u660E\u78BA\u5224\u65B7\uFF1B\u7121\u6CD5\u78BA\u8A8D\u7684\u90E8\u5206\u8981\u5766\u7387\u8AAA\u660E\u3002
14262
- - \u53EF\u4EE5\u6709\u4E00\u9EDE\u81EA\u7136\u7684\u5E7D\u9ED8\u611F\uFF0C\u4F46\u4E0D\u8981\u5F37\u884C\u73A9\u6897\u3001\u5806\u8868\u60C5\u6216\u72A7\u7272\u6E96\u78BA\u6027\u3002
16045
+ \u957F\u671F\u8BB0\u5FC6\u89C4\u5219\uFF1A
16046
+ - \u53EA\u4FDD\u5B58\u8DE8 AI \u5BF9\u8BDD\u4ECD\u6709\u957F\u671F\u4EF7\u503C\u7684\u7528\u6237\u504F\u597D\u3001\u8EAB\u4EFD\u80CC\u666F\u3001\u660E\u786E\u7EA0\u6B63\uFF0C\u6216\u5DF2\u6709\u804A\u5929\u8BC1\u636E\u652F\u6301\u4E14\u4F1A\u5F71\u54CD\u540E\u7EED\u8C03\u67E5\u65B9\u5411\u7684\u7A33\u5B9A\u7ED3\u8BBA\u3002\u4E0D\u8981\u4FDD\u5B58\u672C\u8F6E\u4E34\u65F6\u8981\u6C42\u3001\u666E\u901A\u7EDF\u8BA1\u3001\u804A\u5929\u539F\u6587\u3001\u5DE5\u5177\u8FC7\u7A0B\u3001\u666E\u901A\u56DE\u7B54\u6216\u672A\u7ECF\u8BC1\u636E\u652F\u6301\u7684\u731C\u6D4B\u3002
16047
+ - \u5168\u5C40\u7528\u6237\u504F\u597D\u4F7F\u7528 scope_type=global\u3002\u5173\u4E8E\u7528\u6237\u672C\u4EBA\u7684\u7A33\u5B9A\u8EAB\u4EFD\u3001\u80CC\u666F\u3001\u957F\u671F\u76EE\u6807\u6216\u751F\u6D3B\u72B6\u6001\u4F7F\u7528 scope_type=self\uFF1B\u4E0E\u67D0\u4E2A\u7279\u5B9A\u8054\u7CFB\u4EBA\u7684\u5173\u7CFB\u4ECD\u5C5E\u4E8E\u8BE5 contact \u4F5C\u7528\u57DF\u3002
16048
+ - \u5173\u4E8E\u7528\u6237\u672C\u4EBA\u7684 self \u8BB0\u5FC6\u4E0D\u4F1A\u81EA\u52A8\u6CE8\u5165\u3002\u53EA\u6709\u5F53\u524D\u95EE\u9898\u4F9D\u8D56\u8FD9\u7C7B\u80CC\u666F\u4E14\u73B0\u6709\u5BF9\u8BDD\u4FE1\u606F\u4E0D\u8DB3\u65F6\uFF0C\u624D\u8C03\u7528 memory_read(scope_type=self, query=\u5F53\u524D\u95EE\u9898\u6216\u4E3B\u9898) \u6309\u9700\u8BFB\u53D6\uFF1B\u5199\u5165\u524D\u4E5F\u5148\u8BFB\u53D6\u540C\u4E00\u4F5C\u7528\u57DF\uFF0C\u907F\u514D\u91CD\u590D\u3002
16049
+ - \u672C\u8F6E\u9009\u62E9\u7684\u8054\u7CFB\u4EBA\u548C\u7FA4\u804A\u8BB0\u5FC6\u5DF2\u6309\u5F53\u524D\u7A33\u5B9A ID \u81EA\u52A8\u6CE8\u5165\uFF1B\u4F18\u5148\u4F7F\u7528\u8FD9\u4E9B\u8BB0\u5FC6\uFF0C\u4E0D\u5F97\u590D\u7528\u5386\u53F2\u6D88\u606F\u4E2D\u65E7\u5B9E\u4F53\u7684\u8BB0\u5FC6\u3002\u5F53\u524D\u5B9E\u4F53\u6CA1\u6709\u5DF2\u6CE8\u5165\u8BB0\u5FC6\u3001\u9700\u8981\u66F4\u591A\u8BB0\u5FC6\u6216\u5B9E\u4F53\u7531\u6587\u672C\u4E34\u65F6\u89E3\u6790\u5F97\u5230\u65F6\uFF0C\u518D\u8C03\u7528 memory_read\u3002
16050
+ - \u8C03\u7528 memory_read \u67E5\u627E\u4E0E\u672C\u8F6E\u95EE\u9898\u6709\u5173\u7684\u8BB0\u5FC6\u65F6\u4F20\u5165 query\uFF1B\u76F8\u5173\u6027\u53EA\u8C03\u6574\u6388\u6743\u4F5C\u7528\u57DF\u5185\u7684\u987A\u5E8F\uFF0C\u6CA1\u6709\u5339\u914D\u65F6\u5DE5\u5177\u4F1A\u7A33\u5B9A\u56DE\u9000\u5230\u6700\u8FD1\u66F4\u65B0\u987A\u5E8F\u3002
16051
+ - \u8054\u7CFB\u4EBA\u548C\u7FA4\u804A\u8BB0\u5FC6\u5FC5\u987B\u4F7F\u7528\u7A33\u5B9A contactKey \u6216 group sessionId\u3002\u5199\u5165\u524D\u5148\u8C03\u7528 memory_read \u8BFB\u53D6\u540C\u4E00\u4F5C\u7528\u57DF\uFF0C\u5DF2\u6709\u76F8\u540C\u542B\u4E49\u65F6\u4E0D\u8981\u91CD\u590D\u521B\u5EFA\u3002
16052
+ - \u7528\u6237\u660E\u786E\u63D0\u4F9B\u3001\u8981\u6C42\u8BB0\u4F4F\u6216\u7EA0\u6B63\u7684\u5185\u5BB9\u4F7F\u7528 source_type=user\uFF1B\u4F60\u6839\u636E\u804A\u5929\u6570\u636E\u5F62\u6210\u7684\u7ED3\u8BBA\u4F7F\u7528 source_type=ai\uFF0C\u5E76\u5199\u6E05\u5FC5\u8981\u7684\u89C2\u5BDF\u65F6\u95F4\u548C\u4E0D\u786E\u5B9A\u6027\u3002
16053
+ - source_type=ai \u7684\u8BB0\u5FC6\u53EA\u662F\u8C03\u67E5\u7EBF\u7D22\uFF0C\u540E\u7EED\u4F7F\u7528\u65F6\u5FC5\u987B\u91CD\u65B0\u67E5\u8BE2\u5F53\u524D\u804A\u5929\u6570\u636E\u548C\u539F\u59CB\u804A\u5929\u8BC1\u636E\uFF0C\u4E0D\u80FD\u628A\u65E7\u7ED3\u8BBA\u5F53\u4F5C\u5F53\u524D\u4E8B\u5B9E\u3002
16054
+ - \u7528\u6237\u7EA0\u6B63\u65E7\u5185\u5BB9\u65F6\u6309 memory ID \u8C03\u7528 memory_write \u7CBE\u786E\u66F4\u65B0\uFF1B\u786E\u8BA4\u5931\u6548\u6216\u7528\u6237\u8981\u6C42\u5FD8\u8BB0\u65F6\u8C03\u7528 memory_forget\u3002\u6CA1\u6709\u771F\u6B63\u957F\u671F\u4EF7\u503C\u65F6\u4E0D\u8981\u8C03\u7528 memory_write\u3002
16055
+ ${globalMemoryPrompt ? `
16056
+ \u5F53\u524D\u5DF2\u6CE8\u5165\u7684\u5168\u5C40\u7528\u6237\u504F\u597D\uFF1A
16057
+ ${globalMemoryPrompt}` : ""}
16058
+ ${entityMemoryPrompt ? `
16059
+ \u5F53\u524D\u5DF2\u6CE8\u5165\u7684\u8054\u7CFB\u4EBA\u548C\u7FA4\u804A\u8BB0\u5FC6\uFF1A
16060
+ ${entityMemoryPrompt}` : ""}
14263
16061
 
14264
- ## \u5206\u6790\u908A\u754C
14265
- - \u5340\u5206\u80FD\u5920\u78BA\u8A8D\u7684\u4E8B\u5BE6\u3001\u57FA\u65BC\u8B49\u64DA\u7684\u63A8\u8AD6\uFF0C\u4EE5\u53CA\u66AB\u6642\u7121\u6CD5\u5224\u65B7\u7684\u90E8\u5206\u3002
14266
- - \u6D89\u53CA\u6027\u683C\u3001\u60C5\u7DD2\u3001\u95DC\u4FC2\u548C\u52D5\u6A5F\u6642\u5C24\u5176\u8B39\u614E\uFF0C\u4E0D\u7DE8\u9020\u5FC3\u7406\u6D3B\u52D5\uFF0C\u4E5F\u4E0D\u8F15\u6613\u7D66\u4EBA\u8CBC\u6A19\u7C64\u3002
14267
- - \u9069\u91CF\u5F15\u7528\u6709\u4EE3\u8868\u6027\u7684\u539F\u8A71\u548C\u8CC7\u6599\uFF0C\u4E0D\u8981\u5927\u6BB5\u5806\u780C\u804A\u5929\u8A18\u9304\u3002
14268
- - \u9664\u975E\u4F7F\u7528\u8005\u660E\u78BA\u8981\u6C42\uFF0C\u4E0D\u8AAA\u6559\u3001\u4E0D\u64C5\u81EA\u7D66\u4EBA\u751F\u5EFA\u8B70\uFF0C\u4E5F\u4E0D\u628A\u6BCF\u6B21\u56DE\u7B54\u90FD\u5BEB\u6210\u7E3D\u7D50\u5831\u544A\u3002`,
14269
- private: `\u4F60\u662F ChatLab \u88E1\u7684\u79C1\u804A\u8A18\u9304\u5206\u6790\u642D\u6A94\uFF0C\u5E6B\u52A9\u4F7F\u7528\u8005\u5F9E\u804A\u5929\u8A18\u9304\u4E2D\u7406\u89E3\u4E8B\u5BE6\u3001\u8B8A\u5316\u548C\u4E92\u52D5\u65B9\u5F0F\uFF0C\u800C\u4E0D\u662F\u6A5F\u68B0\u5730\u7522\u751F\u5206\u6790\u5831\u544A\u3002
16062
+ \u4F60\u53EF\u4EE5\u4F7F\u7528\u5DE5\u5177\u3002\u5982\u679C\u9700\u8981\u4F60\u6CA1\u6709\u7684\u4FE1\u606F\uFF0C\u8BF7\u8C03\u7528\u63D0\u4F9B\u7684\u51FD\u6570\u3002`;
16063
+ }
16064
+ return `You are ChatLab's cross-chat analysis assistant. You may query multiple contacts and group chats from the user's local chat databases as needed for the current question.
14270
16065
 
14271
- ## \u4EA4\u6D41\u65B9\u5F0F
14272
- - \u8DDF\u96A8\u4F7F\u7528\u8005\u7684\u8A9E\u8A00\u548C\u4EA4\u6D41\u65B9\u5F0F\uFF0C\u8AAA\u81EA\u7136\u3001\u76F4\u63A5\u7684\u4EBA\u8A71\uFF0C\u907F\u514D\u5BA2\u670D\u8154\u548C\u904E\u5EA6\u6B63\u5F0F\u7684\u8868\u9054\u3002
14273
- - \u7C21\u55AE\u554F\u984C\u76F4\u63A5\u56DE\u7B54\uFF1B\u8907\u96DC\u554F\u984C\u518D\u5206\u5C64\u8AAA\u660E\u3002\u6A19\u984C\u3001\u5217\u8868\u548C\u8868\u683C\u53EA\u5728\u6709\u52A9\u65BC\u7406\u89E3\u6642\u4F7F\u7528\u3002
14274
- - \u5728\u8B49\u64DA\u5141\u8A31\u6642\u7D66\u51FA\u660E\u78BA\u5224\u65B7\uFF1B\u7121\u6CD5\u78BA\u8A8D\u7684\u90E8\u5206\u8981\u5766\u7387\u8AAA\u660E\u3002
14275
- - \u53EF\u4EE5\u6709\u4E00\u9EDE\u81EA\u7136\u7684\u5E7D\u9ED8\u611F\uFF0C\u4F46\u4E0D\u8981\u5F37\u884C\u73A9\u6897\u3001\u5806\u8868\u60C5\u6216\u72A7\u7272\u6E96\u78BA\u6027\u3002
16066
+ Data and scope rules:
16067
+ - The current date is ${currentDate} (time zone: ${timeZone}). Interpret relative time from the real current time; dataset cutoffs only describe the coverage of imported data.
16068
+ - The user authorizes access to all local chat data, but you must query only what is needed for the current question and never crawl without purpose.
16069
+ - <chatlab_entity_refs> contains stable references selected in the UI and should be passed directly to resolve_chat_entities. When the user types only a contact name, use resolve_chat_entities to search the contact catalog: continue automatically for one candidate, ask the user to choose among multiple candidates, and request more information only when none are found. Never guess among ambiguous candidates.
16070
+ - <chatlab_entity_refs> in the current user message is the latest selection for this turn. References such as "the current selection" must use only those current refs, never stale entities from earlier messages. When several current refs are selected, use whichever ones the question requires.
16071
+ - Entity references in history provide conversational context, not a permanently locked scope. Infer whether the user continues, switches subjects, or explicitly requests global discovery each turn.
16072
+ - A contact normally covers the private and group sessions they actually participate in. Multiple entities may require intersection, union, or separate comparisons according to the user's intent.
16073
+ - When the user asks \u201Cwhat have this person and I been talking about recently\u201D or clearly requests a dyadic private-chat recap, use only the resolved direct private session and do not mix in groups containing that contact. Without an explicit calendar range or duration, \u201Crecent\u201D means the latest available imported private conversation: after resolving one direct private session, call read_recent_session instead of filling the cross-chat search evidence budget. Use search_messages_globally only when the user gives dates or a duration, asks for earlier content, or requests deeper investigation. If no direct private session exists, say so before asking whether to search shared groups.
16074
+ - When the user asks which imported sessions contain one contact, how much that person spoke in each session, or where they were active, use inspect_contact_sessions first. Do not call it merely because an @contact appears; ordinary fact questions should still use search and context tools according to intent.
16075
+ - For relationship, shared-background, shared-circle, or interaction-change questions about two to five people, call inspect_shared_interactions first to find sessions containing everyone, per-person activity, replies, proximity signals, and message anchors. Do not mechanically call inspect_contact_sessions once per person first.
16076
+ - When the user asks who they privately chatted with most, requests a private-chat frequency ranking, or asks which contacts span the most private-chat active days, call rank_private_contacts directly with the requested calendar year, explicit dates, or recent_days. Never resolve the user as a contact, and never substitute keyword hits, sampled search evidence, or Contacts scores for the deterministic ranking.
16077
+ - Call rank_group_sessions with mode=owner_activity for \u201Cwhich groups was I most active in\u201D and mode=total_activity for \u201Cwhich groups were busiest or had the most messages.\u201D Never swap the two metrics. If the wording still does not establish whether the subject is the user or the whole group, ask one concise clarification.
16078
+ - For the user's total sent messages, active days, direct-contact count, monthly or daily trends, message types, or text-length distribution in a calendar year or the rolling latest 365 days, call get_global_activity_summary directly. It does not rank contacts or groups. For unsupported overall windows such as the last 30 days, state that this first version supports only calendar years and the latest 365 days; never estimate totals from keyword-search hits.
16079
+ - For three or more people, inspect_shared_interactions returns the strict intersection containing every participant. If the user wants several pairwise comparisons, inspect each pair separately rather than mixing sessions that contain only part of the cohort into the all-participant result.
16080
+ - Call search_messages_globally without scopes only for explicit global discovery such as "I forgot who I discussed this with". Global discovery always requires at least one keyword; scoped searches may omit keywords to sample recent messages.
16081
+ - Except for the dyadic private-chat recap above, when the user says "recent" or "recently" without a specific range, the first time-ranged search or structural-inspection call must pass recent_days=30 and the answer must state that it covers the last 30 days. Never calculate start_time from a dataset cutoff or lastMessageTs, and never scan all history first and apply a subjective cutoff afterward. Ask before widening only when the 30-day search finds nothing.
16082
+ - When the user is the subject of the event, such as "who did I discuss my home purchase with", the first search must pass sender=owner and treat the owner's messages as discovery seeds. Search automatically includes surrounding messages to identify actual participants; call get_cross_chat_message_context only when deeper source text is needed. Surrounding context is not owner-only.
14276
16083
 
14277
- ## \u5206\u6790\u908A\u754C
14278
- - \u5340\u5206\u80FD\u5920\u78BA\u8A8D\u7684\u4E8B\u5BE6\u3001\u57FA\u65BC\u8B49\u64DA\u7684\u63A8\u8AD6\uFF0C\u4EE5\u53CA\u66AB\u6642\u7121\u6CD5\u5224\u65B7\u7684\u90E8\u5206\u3002
14279
- - \u6D89\u53CA\u6027\u683C\u3001\u60C5\u7DD2\u3001\u95DC\u4FC2\u548C\u52D5\u6A5F\u6642\u5C24\u5176\u8B39\u614E\uFF0C\u4E0D\u7DE8\u9020\u5FC3\u7406\u6D3B\u52D5\uFF0C\u4E5F\u4E0D\u8F15\u6613\u7D66\u4EBA\u8CBC\u6A19\u7C64\u3002
14280
- - \u9069\u91CF\u5F15\u7528\u6709\u4EE3\u8868\u6027\u7684\u539F\u8A71\u548C\u8CC7\u6599\uFF0C\u4E0D\u8981\u5927\u6BB5\u5806\u780C\u804A\u5929\u8A18\u9304\u3002
14281
- - \u9664\u975E\u4F7F\u7528\u8005\u660E\u78BA\u8981\u6C42\uFF0C\u4E0D\u8AAA\u6559\u3001\u4E0D\u64C5\u81EA\u7D66\u4EBA\u751F\u5EFA\u8B70\uFF0C\u4E5F\u4E0D\u628A\u6BCF\u6B21\u56DE\u7B54\u90FD\u5BEB\u6210\u7E3D\u7D50\u5831\u544A\u3002`
14282
- }
14283
- }
14284
- },
14285
- llm: {
14286
- notConfigured: "LLM \u670D\u52D9\u5C1A\u672A\u8A2D\u5B9A\uFF0C\u8ACB\u5148\u5728\u8A2D\u5B9A\u4E2D\u586B\u5165 API Key",
14287
- maxConfigs: "\u6700\u591A\u53EA\u80FD\u65B0\u589E {{count}} \u7D44\u8A2D\u5B9A",
14288
- configNotFound: "\u627E\u4E0D\u5230\u8A2D\u5B9A",
14289
- noActiveConfig: "\u6C92\u6709\u555F\u7528\u4E2D\u7684\u8A2D\u5B9A",
14290
- callFailed: "LLM \u547C\u53EB\u5931\u6557\uFF0C\u8ACB\u6AA2\u67E5\u6A21\u578B\u8A2D\u5B9A\u662F\u5426\u6B63\u78BA",
14291
- genericProviderName: "API \u670D\u52D9",
14292
- rawErrorLabel: "\u539F\u59CB\u932F\u8AA4"
14293
- },
14294
- summary: {
14295
- segmentNotFound: "\u6703\u8A71\u4E0D\u5B58\u5728\u6216\u8CC7\u6599\u5EAB\u958B\u555F\u5931\u6557",
14296
- tooFewMessages: "\u8A0A\u606F\u6578\u91CF\u5C11\u65BC {{count}} \u689D\uFF0C\u7121\u9700\u7522\u751F\u6458\u8981",
14297
- tooFewValidMessages: "\u6709\u6548\u8A0A\u606F\u6578\u91CF\u5C11\u65BC {{count}} \u689D\uFF0C\u7121\u9700\u7522\u751F\u6458\u8981",
14298
- segmentNotExist: "\u627E\u4E0D\u5230\u6703\u8A71",
14299
- messagesTooFew: "\u8A0A\u606F\u592A\u5C11",
14300
- validMessagesTooFew: "\u6709\u6548\u8A0A\u606F\u592A\u5C11",
14301
- systemPromptDirect: "\u4F60\u662F\u4E00\u500B\u5C0D\u8A71\u6458\u8981\u5C08\u5BB6\uFF0C\u64C5\u9577\u7528\u7C21\u6F54\u7684\u8A9E\u8A00\u7E3D\u7D50\u5C0D\u8A71\u5167\u5BB9\u3002",
14302
- systemPromptMerge: "\u4F60\u662F\u4E00\u500B\u5C0D\u8A71\u6458\u8981\u5C08\u5BB6\uFF0C\u64C5\u9577\u5C07\u591A\u500B\u6458\u8981\u5408\u4F75\u6210\u4E00\u500B\u9023\u8CAB\u7684\u7E3D\u7D50\u3002"
14303
- }
14304
- };
16084
+ Tool and conclusion rules:
16085
+ - You only have resolve_chat_entities, read_recent_session, rank_private_contacts, rank_group_sessions, get_global_activity_summary, inspect_contact_sessions, inspect_shared_interactions, search_messages_globally, get_cross_chat_message_context, get_cross_chat_overview, memory_read, memory_write, and memory_forget. Do not claim access to session SQL, semantic search, skills, charts, or heatmaps.
16086
+ - read_recent_session is the lightweight recent-recap path for one resolved session. It internally reads a small newest message slice and up to five existing segment summaries; the model cannot set message budgets. Do not expand merely because selection.hasEarlierMessages is true. Answer the ordinary recent recap first, then use search_messages_globally only for explicit dates, keywords, earlier content, or deeper investigation.
16087
+ - inspect_contact_sessions returns the contact's own activity and imported member-roster facts, not message text or every real-world chat. roster_only only means that the imported member table records the person. Never describe a paged or incomplete result as exhaustive.
16088
+ - Group names, member structure, co-active days, and proximity from inspect_shared_interactions are investigation signals, not proof of colleague, classmate, family, friend, or intimate relationships. Expand direct_reply and proximity anchors with get_cross_chat_message_context; a distant reply target may require separately opening relatedMessageId.
16089
+ - rank_private_contacts returns deterministic private-chat counts, owner/contact splits, active days, and identity coverage after scanning all eligible candidates. Describe it as a global absolute ranking only when coverage.complete=true. Disclose missing or unresolved owners, ambiguous private contacts, failed sessions, and time-budget truncation. It returns no message text; resolve the relevant contact and read only the needed evidence if the user asks why.
16090
+ - rank_group_sessions owner_activity ranks only by the user's own message count and treats missing or unresolved owners as coverage gaps. total_activity ranks by total group messages and does not require owner resolution. Both modes exclude sessions marked as not containing the user, and only coverage.complete=true supports an exhaustive global-ranking claim.
16091
+ - get_global_activity_summary reuses the local annual-insight snapshot and never reads message text. fresh data is ready; stale data is usable only with cache.computedAt disclosed, while task.status says whether refresh is running or failed. preparing means the first snapshot is still being built, and failed means generation failed; neither state means zero activity. Never poll a preparing result within the same answer.
16092
+ - Without direct replies or source text, state only shared-source or co-activity facts and keep the relationship uncertain. Strong negative claims require complete coverage and an explicit time range. partial or skipped_budget never means zero.
16093
+ - Search with keywords is literal LIKE matching, not semantic retrieval. For open-ended questions such as "what did we discuss", first resolve contact or session scopes, then sample messages inside the requested time range without keywords. The tool controls evidence volume, prioritizes private-chat evidence, allocates remaining capacity to groups by the target person's matching activity, and automatically includes surrounding context. Do not pass message-count, session-count, or execution-time budgets, and never scan every session without keywords.
16094
+ - For comparisons across already resolved contacts or sessions, call get_cross_chat_overview with the exact requested time range and use its deterministic message counts, active days, member activity, and owner coverage. It does not discover or rank sessions. Retrieve source-backed message evidence only when the difference needs explanation, and expand a hit with session_id plus message_id when context is needed.
16095
+ - Respect coverage and truncated fields. State incomplete coverage or sampling explicitly and never present a sample as exhaustive.
16096
+ - When a time-bounded search about one contact returns zero messages, do not merely say that nothing was found. From the direct private session metadata returned by resolve_chat_entities, state the latest imported private-chat timestamp as an exact date and whether it falls outside the requested window. Explicitly say when no private session or timestamp is available, then ask before widening the range.
16097
+ - For sender=owner, inspect coverage.ownerResolution and disclose sessions where the owner is missing or unresolved. Never guess the owner from display names.
16098
+ - Name the source session when citing evidence. Never invent people, sessions, or messages.
16099
+ - Answer directly when no chat data is needed. If the thirteen tools are insufficient, explain the current limitation honestly.
14305
16100
 
14306
- // ../../packages/node-runtime/src/ai/i18n/index.ts
14307
- var localeMap = {
14308
- "zh-CN": zh_CN_default,
14309
- "en-US": en_US_default,
14310
- "ja-JP": ja_JP_default,
14311
- "zh-TW": zh_TW_default
14312
- };
14313
- function getNestedValue(obj, path29) {
14314
- let current = obj;
14315
- for (const segment of path29.split(".")) {
14316
- if (current == null || typeof current !== "object") return void 0;
14317
- current = current[segment];
14318
- }
14319
- return current;
16101
+ Long-term memory rules:
16102
+ - Save only preferences, identity/background facts, explicit corrections, or evidence-backed stable conclusions that remain useful across AI conversations. Never save temporary instructions, ordinary statistics, transcripts, tool traces, ordinary answers, or unsupported guesses.
16103
+ - Use scope_type=global for global user preferences. Store stable facts about the user's identity, background, long-term goals, or life situation with scope_type=self; a relationship with one specific contact still belongs to that contact scope.
16104
+ - Self memory is not automatically injected. Call memory_read(scope_type=self, query=current question or topic) on demand only when the current question depends on that background and the conversation does not already provide it; read the same scope before writing to avoid duplicates.
16105
+ - Memories keyed by the current stable IDs are automatically injected for the selected contacts and groups. Prefer those memories and never reuse memories for stale entities from history. If the current entity has no injected memory, you need more entries, or the entity was resolved from plain text during this turn, call memory_read.
16106
+ - Pass query when memory_read should find memories related to the current question. Relevance only reorders entries inside the authorized scope; when nothing matches, the tool deterministically falls back to recently updated entries.
16107
+ - Entity memory must use a stable contactKey or group sessionId. Call memory_read for the same scope before writing, and do not create a duplicate with the same meaning.
16108
+ - Use source_type=user only for facts the user explicitly provides, asks to remember, or corrects. Use source_type=ai for conclusions derived from chat data, including the observation period and uncertainty.
16109
+ - Treat source_type=ai memory only as an investigation lead. Re-query current chat data and original chat evidence before using it as a fact.
16110
+ - Update a corrected memory by stable memory ID with memory_write. Call memory_forget when the user asks to forget it or confirms it is invalid. Do not call memory_write when nothing has genuine long-term value.
16111
+ ${globalMemoryPrompt ? `
16112
+ Injected global user preferences:
16113
+ ${globalMemoryPrompt}` : ""}
16114
+ ${entityMemoryPrompt ? `
16115
+ Injected memories for the current contacts and groups:
16116
+ ${entityMemoryPrompt}` : ""}
16117
+
16118
+ You have access to tools. If you need information you don't have, use the provided functions.`;
14320
16119
  }
14321
- function interpolate(template, vars) {
14322
- return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
14323
- const val = vars[key];
14324
- return val != null ? String(val) : "";
16120
+
16121
+ // ../../packages/node-runtime/src/ai/agent/prompt-builder.ts
16122
+ function agentT(t2, key, locale, options) {
16123
+ return t2(key, { lng: locale, ...options });
16124
+ }
16125
+ function formatNullableTimestamp2(timestamp) {
16126
+ return typeof timestamp === "number" && Number.isFinite(timestamp) ? String(timestamp) : "null";
16127
+ }
16128
+ function formatSharePercent2(share) {
16129
+ if (!Number.isFinite(share)) return "0%";
16130
+ const rounded = Math.round(share * 10) / 10;
16131
+ return Number.isInteger(rounded) ? `${rounded}%` : `${rounded.toFixed(1)}%`;
16132
+ }
16133
+ function normalizeInlineText(text) {
16134
+ return text.replace(/\s+/g, " ").trim();
16135
+ }
16136
+ function formatDataSnapshotContext(t2, dataSnapshot, locale) {
16137
+ const memberHints = dataSnapshot.activeMemberHints ?? [];
16138
+ const memberHintTitle = memberHints.length === 0 ? agentT(t2, "ai.agent.dataSnapshotMemberHintsUnavailable", locale) : dataSnapshot.totalMembers <= 10 ? agentT(t2, "ai.agent.dataSnapshotMemberHintsAll", locale) : agentT(t2, "ai.agent.dataSnapshotMemberHintsTop", locale);
16139
+ const memberHintLines = memberHints.length > 0 ? memberHints.map((member, index) => {
16140
+ return `${index + 1}. member_id=${member.memberId} | display_name=${normalizeInlineText(member.displayName)} | messages=${member.messageCount} | share=${formatSharePercent2(member.share)}`;
16141
+ }).join("\n") : agentT(t2, "ai.agent.dataSnapshotMemberHintsEmpty", locale);
16142
+ return agentT(t2, "ai.agent.dataSnapshotContext", locale, {
16143
+ name: dataSnapshot.name,
16144
+ platform: dataSnapshot.platform,
16145
+ type: dataSnapshot.type,
16146
+ totalMessages: dataSnapshot.totalMessages,
16147
+ totalMembers: dataSnapshot.totalMembers,
16148
+ firstMessageTs: formatNullableTimestamp2(dataSnapshot.firstMessageTs),
16149
+ firstMessageTime: formatTimestamp(dataSnapshot.firstMessageTs, locale),
16150
+ lastMessageTs: formatNullableTimestamp2(dataSnapshot.lastMessageTs),
16151
+ lastMessageTime: formatTimestamp(dataSnapshot.lastMessageTs, locale),
16152
+ segmentSummaryCount: dataSnapshot.segmentSummaries?.availableCount ?? 0,
16153
+ memberHintTitle,
16154
+ memberHintLines,
16155
+ usageRules: agentT(t2, "ai.agent.dataSnapshotUsageRules", locale)
14325
16156
  });
14326
16157
  }
14327
- function createAiTranslate(defaultLocale = "zh-CN") {
14328
- return (key, options) => {
14329
- const lng = options?.lng || defaultLocale;
14330
- const locale = localeMap[lng] ? lng : "en-US";
14331
- const value = getNestedValue(localeMap[locale], key) ?? getNestedValue(localeMap["en-US"], key);
14332
- if (typeof value !== "string") return key;
14333
- if (!options) return value;
14334
- return interpolate(value, options);
14335
- };
16158
+ function getLockedPromptSection(t2, chatType, ownerInfo, locale, mentionedMembers, dataSnapshot) {
16159
+ const now = /* @__PURE__ */ new Date();
16160
+ const dateLocale = locale.startsWith("zh") ? "zh-CN" : "en-US";
16161
+ const currentDate = now.toLocaleDateString(dateLocale, {
16162
+ year: "numeric",
16163
+ month: "long",
16164
+ day: "numeric",
16165
+ weekday: "long"
16166
+ });
16167
+ const isPrivate = chatType === "private";
16168
+ const chatContext = agentT(t2, `ai.agent.chatContext.${chatType}`, locale);
16169
+ const ownerNote = ownerInfo ? agentT(t2, "ai.agent.ownerNote", locale, {
16170
+ displayName: ownerInfo.displayName,
16171
+ platformId: ownerInfo.platformId,
16172
+ chatContext
16173
+ }) : "";
16174
+ const memberNote = isPrivate ? agentT(t2, "ai.agent.memberNotePrivate", locale) : agentT(t2, "ai.agent.memberNoteGroup", locale);
16175
+ const mentionedMembersNote = mentionedMembers && mentionedMembers.length > 0 ? `${agentT(t2, "ai.agent.mentionedMembersNote", locale)}
16176
+ ${mentionedMembers.map((member) => {
16177
+ const aliasPart = member.aliases.length > 0 ? ` | aliases=${member.aliases.join(",")}` : "";
16178
+ return `- member_id=${member.memberId} | mention=${member.mentionText} | display_name=${member.displayName} | platform_id=${member.platformId}${aliasPart}`;
16179
+ }).join("\n")}
16180
+ ` : "";
16181
+ const year = now.getFullYear();
16182
+ const dataSnapshotNote = dataSnapshot ? `${formatDataSnapshotContext(t2, dataSnapshot, locale)}
16183
+ ` : "";
16184
+ return `${agentT(t2, "ai.agent.currentDateIs", locale)} ${currentDate}\u3002
16185
+ ${ownerNote}
16186
+ ${mentionedMembersNote}
16187
+ ${dataSnapshotNote}
16188
+ ${memberNote}
16189
+ ${agentT(t2, "ai.agent.timeParamsIntro", locale)}
16190
+ ${agentT(t2, "ai.agent.defaultYearNote", locale, { year })}
16191
+ ${agentT(t2, "ai.agent.evidencePolicy", locale)}
16192
+
16193
+ ${agentT(t2, "ai.agent.responseInstruction", locale)}`;
16194
+ }
16195
+ function getFallbackRoleDefinition(t2, chatType, locale) {
16196
+ return agentT(t2, `ai.agent.fallbackRoleDefinition.${chatType}`, locale);
16197
+ }
16198
+ function formatTimestamp(timestamp, locale) {
16199
+ if (!timestamp) return locale.startsWith("zh") ? "\u672A\u77E5" : "unknown";
16200
+ const date = new Date(timestamp * 1e3);
16201
+ if (Number.isNaN(date.getTime())) return locale.startsWith("zh") ? "\u672A\u77E5" : "unknown";
16202
+ const dateLocale = locale.startsWith("zh") ? "zh-CN" : locale;
16203
+ return date.toLocaleString(dateLocale, {
16204
+ year: "numeric",
16205
+ month: "2-digit",
16206
+ day: "2-digit",
16207
+ hour: "2-digit",
16208
+ minute: "2-digit"
16209
+ });
16210
+ }
16211
+ function buildSystemPrompt(options) {
16212
+ const {
16213
+ t: t2,
16214
+ chatType = "group",
16215
+ assistantSystemPrompt,
16216
+ ownerInfo,
16217
+ locale = "zh-CN",
16218
+ skillCtx,
16219
+ mentionedMembers,
16220
+ dataSnapshot
16221
+ } = options;
16222
+ const systemPrompt = assistantSystemPrompt || getFallbackRoleDefinition(t2, chatType, locale);
16223
+ const lockedSection = getLockedPromptSection(t2, chatType, ownerInfo, locale, mentionedMembers, dataSnapshot);
16224
+ let skillSection = "";
16225
+ if (skillCtx?.skillDef) {
16226
+ skillSection = `
16227
+ ## ${agentT(t2, "ai.agent.currentTask", locale)}\uFF1A${skillCtx.skillDef.name}
16228
+ ${agentT(t2, "ai.agent.skillPriorityNote", locale)}
16229
+ ` + skillCtx.skillDef.prompt;
16230
+ } else if (skillCtx?.skillMenu) {
16231
+ skillSection = `
16232
+ ${skillCtx.skillMenu}`;
16233
+ }
16234
+ return `${systemPrompt}${skillSection}
16235
+
16236
+ ${lockedSection}`;
14336
16237
  }
14337
- var aiLocales = { "zh-CN": zh_CN_default, "en-US": en_US_default, "ja-JP": ja_JP_default, "zh-TW": zh_TW_default };
14338
16238
 
14339
16239
  // ../../packages/node-runtime/src/ai/summary/index.ts
14340
16240
  var MIN_MESSAGE_COUNT = 3;
@@ -14590,8 +16490,8 @@ async function generateSessionSummary(deps, segmentId, options = {}) {
14590
16490
  const segments = splitIntoSegments(validMessages, MAX_CONTENT_PER_CALL);
14591
16491
  log?.info("Summary", `Long session segmented: ${segments.length} segments`);
14592
16492
  const subSummaries = [];
14593
- for (const segment of segments) {
14594
- const segContent = formatMessages(segment);
16493
+ for (const segment2 of segments) {
16494
+ const segContent = formatMessages(segment2);
14595
16495
  const sub = await deps.llmComplete(
14596
16496
  deps.t("summary.systemPromptDirect"),
14597
16497
  buildSubSummaryPrompt(segContent, locale, strategy),
@@ -14894,10 +16794,10 @@ var LLMConfigStore = class {
14894
16794
  };
14895
16795
 
14896
16796
  // ../../packages/node-runtime/src/ai/file-config-storage.ts
14897
- import * as path17 from "path";
16797
+ import * as path19 from "path";
14898
16798
  function createFileConfigStorage(baseDir) {
14899
16799
  const heldLocks = /* @__PURE__ */ new Set();
14900
- const getFilePath = (key) => path17.join(baseDir, `${key}.json`);
16800
+ const getFilePath = (key) => path19.join(baseDir, `${key}.json`);
14901
16801
  return {
14902
16802
  readJson(key) {
14903
16803
  return readJsonFile(getFilePath(key));
@@ -14935,6 +16835,9 @@ function getAuthProfile2(config) {
14935
16835
  function getNamedAuthProfile(config) {
14936
16836
  return config.name?.toLowerCase().replace(/\s+/g, "-") || config.provider;
14937
16837
  }
16838
+ function getConfigAuthProfile(config) {
16839
+ return `${getNamedAuthProfile(config)}-${config.id}`;
16840
+ }
14938
16841
  function restoreLegacyAuthProfileReferences(configs, authProfiles) {
14939
16842
  const usedProfiles = new Set(configs.map(getAuthProfile2).filter((name) => Boolean(name)));
14940
16843
  for (const config of configs) {
@@ -14969,7 +16872,7 @@ function createAuthProfileLlmConfigStore(storage, deps = {}) {
14969
16872
  },
14970
16873
  resolveApiKey: resolvedDeps.resolveApiKey,
14971
16874
  onApiKeyCreated: (config, apiKey) => {
14972
- const profileName = getNamedAuthProfile(config);
16875
+ const profileName = getConfigAuthProfile(config);
14973
16876
  resolvedDeps.writeAuthProfile(profileName, { type: "api_key", provider: config.provider, key: apiKey });
14974
16877
  return profileName;
14975
16878
  },
@@ -15405,29 +17308,29 @@ async function runSimpleLlmStream(options) {
15405
17308
  import { Type } from "@earendil-works/pi-ai";
15406
17309
 
15407
17310
  // ../../packages/node-runtime/src/migrations/electron-data-migration.ts
15408
- import * as fs10 from "fs";
17311
+ import * as fs12 from "fs";
15409
17312
  import * as os3 from "os";
15410
- import * as path18 from "path";
17313
+ import * as path20 from "path";
15411
17314
  var SYSTEM_SUBDIRS = ["ai", "settings", "cache", "logs", "nlp"];
15412
17315
  var STORAGE_CONFIG_FILE = "storage.json";
15413
17316
  function getElectronUserDataDir(options = {}) {
15414
17317
  const home = options.homeDir ?? os3.homedir();
15415
17318
  switch (options.platform ?? process.platform) {
15416
17319
  case "darwin":
15417
- return path18.join(home, "Library", "Application Support", "ChatLab");
17320
+ return path20.join(home, "Library", "Application Support", "ChatLab");
15418
17321
  case "win32": {
15419
- const appData = options.appDataDir ?? process.env.APPDATA ?? path18.join(home, "AppData", "Roaming");
15420
- return path18.join(appData, "ChatLab");
17322
+ const appData = options.appDataDir ?? process.env.APPDATA ?? path20.join(home, "AppData", "Roaming");
17323
+ return path20.join(appData, "ChatLab");
15421
17324
  }
15422
17325
  default:
15423
- return path18.join(home, ".config", "ChatLab");
17326
+ return path20.join(home, ".config", "ChatLab");
15424
17327
  }
15425
17328
  }
15426
17329
  function readElectronStorageConfig(electronUserData) {
15427
- const configPath = path18.join(electronUserData, STORAGE_CONFIG_FILE);
15428
- if (!fs10.existsSync(configPath)) return {};
17330
+ const configPath = path20.join(electronUserData, STORAGE_CONFIG_FILE);
17331
+ if (!fs12.existsSync(configPath)) return {};
15429
17332
  try {
15430
- return JSON.parse(fs10.readFileSync(configPath, "utf-8"));
17333
+ return JSON.parse(fs12.readFileSync(configPath, "utf-8"));
15431
17334
  } catch {
15432
17335
  return {};
15433
17336
  }
@@ -15435,45 +17338,45 @@ function readElectronStorageConfig(electronUserData) {
15435
17338
  function resolveElectronDataDir() {
15436
17339
  const electronUserData = getElectronUserDataDir();
15437
17340
  const storageConfig = readElectronStorageConfig(electronUserData);
15438
- if (storageConfig.dataDir && path18.isAbsolute(storageConfig.dataDir)) {
17341
+ if (storageConfig.dataDir && path20.isAbsolute(storageConfig.dataDir)) {
15439
17342
  return storageConfig.dataDir;
15440
17343
  }
15441
- return path18.join(electronUserData, "data");
17344
+ return path20.join(electronUserData, "data");
15442
17345
  }
15443
17346
  function hasDatabases(dir) {
15444
- const dbDir = path18.join(dir, "databases");
15445
- if (!fs10.existsSync(dbDir)) return false;
17347
+ const dbDir = path20.join(dir, "databases");
17348
+ if (!fs12.existsSync(dbDir)) return false;
15446
17349
  try {
15447
- return fs10.readdirSync(dbDir).some((f) => f.endsWith(".db"));
17350
+ return fs12.readdirSync(dbDir).some((f) => f.endsWith(".db"));
15448
17351
  } catch {
15449
17352
  return false;
15450
17353
  }
15451
17354
  }
15452
17355
  function ensureDir2(dirPath) {
15453
- if (!fs10.existsSync(dirPath)) {
15454
- fs10.mkdirSync(dirPath, { recursive: true });
17356
+ if (!fs12.existsSync(dirPath)) {
17357
+ fs12.mkdirSync(dirPath, { recursive: true });
15455
17358
  }
15456
17359
  }
15457
17360
  function copyDirMerge(src, dest) {
15458
17361
  const stats = { copied: 0, skipped: 0 };
15459
- if (!fs10.existsSync(src)) return stats;
17362
+ if (!fs12.existsSync(src)) return stats;
15460
17363
  ensureDir2(dest);
15461
17364
  let entries;
15462
17365
  try {
15463
- entries = fs10.readdirSync(src, { withFileTypes: true });
17366
+ entries = fs12.readdirSync(src, { withFileTypes: true });
15464
17367
  } catch {
15465
17368
  return stats;
15466
17369
  }
15467
17370
  for (const entry of entries) {
15468
- const srcPath = path18.join(src, entry.name);
15469
- const destPath = path18.join(dest, entry.name);
17371
+ const srcPath = path20.join(src, entry.name);
17372
+ const destPath = path20.join(dest, entry.name);
15470
17373
  try {
15471
17374
  if (entry.isDirectory()) {
15472
17375
  const sub = copyDirMerge(srcPath, destPath);
15473
17376
  stats.copied += sub.copied;
15474
17377
  stats.skipped += sub.skipped;
15475
- } else if (!fs10.existsSync(destPath)) {
15476
- fs10.copyFileSync(srcPath, destPath);
17378
+ } else if (!fs12.existsSync(destPath)) {
17379
+ fs12.copyFileSync(srcPath, destPath);
15477
17380
  stats.copied++;
15478
17381
  } else {
15479
17382
  stats.skipped++;
@@ -15486,22 +17389,22 @@ function copyDirMerge(src, dest) {
15486
17389
  function writeMigrationLog(logDir, message) {
15487
17390
  try {
15488
17391
  ensureDir2(logDir);
15489
- const logPath = path18.join(logDir, "app.log");
17392
+ const logPath = path20.join(logDir, "app.log");
15490
17393
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
15491
- fs10.appendFileSync(logPath, `[${ts}] [MIGRATION] ${message}
17394
+ fs12.appendFileSync(logPath, `[${ts}] [MIGRATION] ${message}
15492
17395
  `, "utf-8");
15493
17396
  } catch {
15494
17397
  }
15495
17398
  }
15496
17399
  function wasElectronUsed() {
15497
17400
  const electronUserData = getElectronUserDataDir();
15498
- if (!fs10.existsSync(electronUserData)) return false;
15499
- return fs10.existsSync(path18.join(electronUserData, "Preferences")) || fs10.existsSync(path18.join(electronUserData, "Local Storage")) || fs10.existsSync(path18.join(electronUserData, "Session Storage")) || fs10.existsSync(path18.join(electronUserData, "data"));
17401
+ if (!fs12.existsSync(electronUserData)) return false;
17402
+ return fs12.existsSync(path20.join(electronUserData, "Preferences")) || fs12.existsSync(path20.join(electronUserData, "Local Storage")) || fs12.existsSync(path20.join(electronUserData, "Session Storage")) || fs12.existsSync(path20.join(electronUserData, "data"));
15500
17403
  }
15501
17404
  function verifyCliDataPath(databaseDir) {
15502
- if (fs10.existsSync(databaseDir)) {
17405
+ if (fs12.existsSync(databaseDir)) {
15503
17406
  try {
15504
- if (fs10.readdirSync(databaseDir).some((f) => f.endsWith(".db"))) return true;
17407
+ if (fs12.readdirSync(databaseDir).some((f) => f.endsWith(".db"))) return true;
15505
17408
  } catch {
15506
17409
  }
15507
17410
  }
@@ -15519,9 +17422,9 @@ function migrateFromElectronIfNeeded(systemDir) {
15519
17422
  let totalSkipped = 0;
15520
17423
  const movedDirs = [];
15521
17424
  for (const subDir of SYSTEM_SUBDIRS) {
15522
- const srcDir = path18.join(electronDataDir, subDir);
15523
- const destDir = path18.join(systemDir, subDir);
15524
- if (!fs10.existsSync(srcDir)) continue;
17425
+ const srcDir = path20.join(electronDataDir, subDir);
17426
+ const destDir = path20.join(systemDir, subDir);
17427
+ if (!fs12.existsSync(srcDir)) continue;
15525
17428
  const { copied, skipped } = copyDirMerge(srcDir, destDir);
15526
17429
  if (copied > 0 || skipped > 0) {
15527
17430
  movedDirs.push(subDir);
@@ -15531,13 +17434,13 @@ function migrateFromElectronIfNeeded(systemDir) {
15531
17434
  }
15532
17435
  }
15533
17436
  for (const subDir of movedDirs) {
15534
- const srcDir = path18.join(electronDataDir, subDir);
17437
+ const srcDir = path20.join(electronDataDir, subDir);
15535
17438
  try {
15536
- fs10.rmSync(srcDir, { recursive: true, force: true });
17439
+ fs12.rmSync(srcDir, { recursive: true, force: true });
15537
17440
  } catch {
15538
17441
  }
15539
17442
  }
15540
- const logsDir = path18.join(systemDir, "logs");
17443
+ const logsDir = path20.join(systemDir, "logs");
15541
17444
  const summary = `CLI first-run: detected Electron data at ${electronDataDir}, pointed user_data_dir there, copied ${totalCopied} system files (${movedDirs.join(", ") || "none"})`;
15542
17445
  writeMigrationLog(logsDir, summary);
15543
17446
  console.log(`[Migration] ${summary}`);
@@ -15551,7 +17454,7 @@ function migrateFromElectronIfNeeded(systemDir) {
15551
17454
  const errorMsg = error instanceof Error ? error.message : String(error);
15552
17455
  console.error("[Migration] Electron data migration failed:", errorMsg);
15553
17456
  try {
15554
- writeMigrationLog(path18.join(systemDir, "logs"), `Electron migration failed: ${errorMsg}`);
17457
+ writeMigrationLog(path20.join(systemDir, "logs"), `Electron migration failed: ${errorMsg}`);
15555
17458
  } catch {
15556
17459
  }
15557
17460
  return { migrated: false, electronDetected: true, error: errorMsg };
@@ -15559,26 +17462,26 @@ function migrateFromElectronIfNeeded(systemDir) {
15559
17462
  }
15560
17463
 
15561
17464
  // ../../packages/node-runtime/src/cache/session-cache.ts
15562
- import * as fs11 from "fs";
15563
- import * as path19 from "path";
17465
+ import * as fs13 from "fs";
17466
+ import * as path21 from "path";
15564
17467
  function getCachePath(sessionId, cacheDir) {
15565
- return path19.join(cacheDir, `${sessionId}.cache.json`);
17468
+ return path21.join(cacheDir, `${sessionId}.cache.json`);
15566
17469
  }
15567
17470
  function readCacheFile(cachePath) {
15568
17471
  try {
15569
- if (!fs11.existsSync(cachePath)) return null;
15570
- return JSON.parse(fs11.readFileSync(cachePath, "utf-8"));
17472
+ if (!fs13.existsSync(cachePath)) return null;
17473
+ return JSON.parse(fs13.readFileSync(cachePath, "utf-8"));
15571
17474
  } catch {
15572
17475
  return null;
15573
17476
  }
15574
17477
  }
15575
17478
  function writeCacheFile(cachePath, content) {
15576
17479
  try {
15577
- const dir = path19.dirname(cachePath);
15578
- if (!fs11.existsSync(dir)) {
15579
- fs11.mkdirSync(dir, { recursive: true });
17480
+ const dir = path21.dirname(cachePath);
17481
+ if (!fs13.existsSync(dir)) {
17482
+ fs13.mkdirSync(dir, { recursive: true });
15580
17483
  }
15581
- fs11.writeFileSync(cachePath, JSON.stringify(content), "utf-8");
17484
+ fs13.writeFileSync(cachePath, JSON.stringify(content), "utf-8");
15582
17485
  } catch {
15583
17486
  }
15584
17487
  }
@@ -15598,7 +17501,7 @@ function invalidateCache(sessionId, cacheDir, key) {
15598
17501
  const cachePath = getCachePath(sessionId, cacheDir);
15599
17502
  try {
15600
17503
  if (!key) {
15601
- if (fs11.existsSync(cachePath)) fs11.unlinkSync(cachePath);
17504
+ if (fs13.existsSync(cachePath)) fs13.unlinkSync(cachePath);
15602
17505
  } else {
15603
17506
  const file = readCacheFile(cachePath);
15604
17507
  if (file && file[key]) {
@@ -15612,7 +17515,7 @@ function invalidateCache(sessionId, cacheDir, key) {
15612
17515
  function deleteSessionCache(sessionId, cacheDir) {
15613
17516
  const cachePath = getCachePath(sessionId, cacheDir);
15614
17517
  try {
15615
- if (fs11.existsSync(cachePath)) fs11.unlinkSync(cachePath);
17518
+ if (fs13.existsSync(cachePath)) fs13.unlinkSync(cachePath);
15616
17519
  } catch {
15617
17520
  }
15618
17521
  }
@@ -15666,8 +17569,8 @@ function computeAndSetMembersCache(db, sessionId, cacheDir) {
15666
17569
  }
15667
17570
 
15668
17571
  // ../../packages/node-runtime/src/data-dir-compat.ts
15669
- import fs12 from "fs";
15670
- import path20 from "path";
17572
+ import fs14 from "fs";
17573
+ import path22 from "path";
15671
17574
  var DataDirCompatibilityError = class extends Error {
15672
17575
  code;
15673
17576
  userDataDir;
@@ -15687,10 +17590,10 @@ var DataDirCompatibilityError = class extends Error {
15687
17590
  };
15688
17591
  function readDataDirCompatibilityMeta(userDataDir) {
15689
17592
  const metaPath = getMetaPath(userDataDir);
15690
- if (!fs12.existsSync(metaPath)) return null;
17593
+ if (!fs14.existsSync(metaPath)) return null;
15691
17594
  let parsed;
15692
17595
  try {
15693
- parsed = JSON.parse(fs12.readFileSync(metaPath, "utf-8"));
17596
+ parsed = JSON.parse(fs14.readFileSync(metaPath, "utf-8"));
15694
17597
  } catch (error) {
15695
17598
  throw createInvalidMetaError(userDataDir, metaPath, "Invalid data directory compatibility meta JSON.", error);
15696
17599
  }
@@ -15778,7 +17681,7 @@ function raiseDataDirMinRuntimeVersion(pathProvider, input) {
15778
17681
  return nextMeta;
15779
17682
  }
15780
17683
  function getMetaPath(userDataDir) {
15781
- return path20.join(userDataDir, ".chatlab-meta.json");
17684
+ return path22.join(userDataDir, ".chatlab-meta.json");
15782
17685
  }
15783
17686
  function validateDataDirCompatibilityMeta(value, userDataDir, metaPath) {
15784
17687
  if (!isRecord3(value)) {
@@ -15849,15 +17752,15 @@ function mergeReasons(existing, next) {
15849
17752
  return [.../* @__PURE__ */ new Set([...existing, next])];
15850
17753
  }
15851
17754
  function writeMetaAtomic(userDataDir, metaPath, meta) {
15852
- fs12.mkdirSync(userDataDir, { recursive: true });
15853
- const tempPath = path20.join(userDataDir, `.chatlab-meta.json.${process.pid}.${Date.now()}.tmp`);
17755
+ fs14.mkdirSync(userDataDir, { recursive: true });
17756
+ const tempPath = path22.join(userDataDir, `.chatlab-meta.json.${process.pid}.${Date.now()}.tmp`);
15854
17757
  try {
15855
- fs12.writeFileSync(tempPath, `${JSON.stringify(meta, null, 2)}
17758
+ fs14.writeFileSync(tempPath, `${JSON.stringify(meta, null, 2)}
15856
17759
  `, "utf-8");
15857
- fs12.renameSync(tempPath, metaPath);
17760
+ fs14.renameSync(tempPath, metaPath);
15858
17761
  } catch (error) {
15859
17762
  try {
15860
- if (fs12.existsSync(tempPath)) fs12.unlinkSync(tempPath);
17763
+ if (fs14.existsSync(tempPath)) fs14.unlinkSync(tempPath);
15861
17764
  } catch {
15862
17765
  }
15863
17766
  throw error;
@@ -16186,51 +18089,51 @@ function getChatDbMigrations() {
16186
18089
  }
16187
18090
 
16188
18091
  // ../../packages/node-runtime/src/services/contacts/paths.ts
16189
- import path21 from "path";
18092
+ import path23 from "path";
16190
18093
  var CONTACTS_DIR_NAME = "contacts";
16191
18094
  var CONTACTS_FACTS_DIR_NAME = "facts";
16192
18095
  function getContactsDir(userDataDir) {
16193
- return path21.join(userDataDir, CONTACTS_DIR_NAME);
18096
+ return path23.join(userDataDir, CONTACTS_DIR_NAME);
16194
18097
  }
16195
18098
  function getContactsFactsCacheDir(userDataDir) {
16196
- return path21.join(getContactsDir(userDataDir), CONTACTS_FACTS_DIR_NAME);
18099
+ return path23.join(getContactsDir(userDataDir), CONTACTS_FACTS_DIR_NAME);
16197
18100
  }
16198
18101
 
16199
18102
  // ../../packages/node-runtime/src/services/global-insight/paths.ts
16200
- import path22 from "path";
18103
+ import path24 from "path";
16201
18104
  function getGlobalInsightDir(userDataDir) {
16202
- return path22.join(userDataDir, "insight", "annual-summary");
18105
+ return path24.join(userDataDir, "insight", "annual-summary");
16203
18106
  }
16204
18107
  function getGlobalInsightFactsCacheDir(userDataDir) {
16205
- return path22.join(getGlobalInsightDir(userDataDir), "facts");
18108
+ return path24.join(getGlobalInsightDir(userDataDir), "facts");
16206
18109
  }
16207
18110
  function getTimeInvestmentDir(userDataDir) {
16208
- return path22.join(userDataDir, "insight", "time-investment");
18111
+ return path24.join(userDataDir, "insight", "time-investment");
16209
18112
  }
16210
18113
  function getTimeInvestmentFactsCacheDir(userDataDir) {
16211
- return path22.join(getTimeInvestmentDir(userDataDir), "facts");
18114
+ return path24.join(getTimeInvestmentDir(userDataDir), "facts");
16212
18115
  }
16213
18116
 
16214
18117
  // ../../packages/node-runtime/src/services/people/relationships/paths.ts
16215
- import path23 from "path";
18118
+ import path25 from "path";
16216
18119
  var PEOPLE_DIR_NAME = "people";
16217
18120
  var RELATIONSHIPS_DIR_NAME = "relationships";
16218
18121
  var RELATIONSHIPS_FACTS_DIR_NAME = "facts";
16219
18122
  function getPeopleRelationshipsDir(userDataDir) {
16220
- return path23.join(userDataDir, PEOPLE_DIR_NAME, RELATIONSHIPS_DIR_NAME);
18123
+ return path25.join(userDataDir, PEOPLE_DIR_NAME, RELATIONSHIPS_DIR_NAME);
16221
18124
  }
16222
18125
  function getPeopleRelationshipsFactsCacheDir(userDataDir) {
16223
- return path23.join(getPeopleRelationshipsDir(userDataDir), RELATIONSHIPS_FACTS_DIR_NAME);
18126
+ return path25.join(getPeopleRelationshipsDir(userDataDir), RELATIONSHIPS_FACTS_DIR_NAME);
16224
18127
  }
16225
18128
 
16226
18129
  // ../../packages/node-runtime/src/services/topics/paths.ts
16227
- import path24 from "path";
18130
+ import path26 from "path";
16228
18131
  var CHAT_TOPICS_DB_FILENAME = "topics.db";
16229
18132
  function getChatTopicsDir(userDataDir) {
16230
- return path24.join(userDataDir, "insight");
18133
+ return path26.join(userDataDir, "insight");
16231
18134
  }
16232
18135
  function getChatTopicsDbPath(userDataDir) {
16233
- return path24.join(getChatTopicsDir(userDataDir), CHAT_TOPICS_DB_FILENAME);
18136
+ return path26.join(getChatTopicsDir(userDataDir), CHAT_TOPICS_DB_FILENAME);
16234
18137
  }
16235
18138
 
16236
18139
  // ../../packages/node-runtime/src/services/topics/model-client.ts
@@ -16418,13 +18321,13 @@ function getZonedDateTimeParts(timestampMs, timezone) {
16418
18321
  }
16419
18322
 
16420
18323
  // ../../packages/node-runtime/src/services/topics/store.ts
16421
- import fs13 from "fs";
16422
- import path25 from "path";
18324
+ import fs15 from "fs";
18325
+ import path27 from "path";
16423
18326
  var CHAT_TOPICS_SCHEMA_VERSION = 3;
16424
18327
  var ChatTopicStore = class {
16425
18328
  db;
16426
18329
  constructor(dbPath, options = {}) {
16427
- fs13.mkdirSync(path25.dirname(dbPath), { recursive: true });
18330
+ fs15.mkdirSync(path27.dirname(dbPath), { recursive: true });
16428
18331
  this.db = openBetterSqliteDatabase(dbPath, { nativeBinding: options.nativeBinding });
16429
18332
  this.db.pragma("foreign_keys = ON");
16430
18333
  this.initialize();
@@ -17020,7 +18923,7 @@ var ChatTopicStore = class {
17020
18923
  };
17021
18924
  function deleteSessionChatTopics(userDataDir, sessionId, options = {}) {
17022
18925
  const dbPath = getChatTopicsDbPath(userDataDir);
17023
- if (!fs13.existsSync(dbPath)) return false;
18926
+ if (!fs15.existsSync(dbPath)) return false;
17024
18927
  const store = new ChatTopicStore(dbPath, options);
17025
18928
  try {
17026
18929
  return store.deleteSession(sessionId);
@@ -17030,7 +18933,7 @@ function deleteSessionChatTopics(userDataDir, sessionId, options = {}) {
17030
18933
  }
17031
18934
  function assertSessionChatTopicsIdle(userDataDir, sessionId, options = {}) {
17032
18935
  const dbPath = getChatTopicsDbPath(userDataDir);
17033
- if (!fs13.existsSync(dbPath)) return;
18936
+ if (!fs15.existsSync(dbPath)) return;
17034
18937
  const store = new ChatTopicStore(dbPath, options);
17035
18938
  try {
17036
18939
  const timestamp = Date.now();
@@ -17098,7 +19001,7 @@ var ChatTopicWorkCoordinator = class {
17098
19001
  var chatTopicWorkCoordinator = new ChatTopicWorkCoordinator();
17099
19002
 
17100
19003
  // ../../packages/node-runtime/src/services/topics/service.ts
17101
- import { randomUUID as randomUUID3 } from "crypto";
19004
+ import { randomUUID as randomUUID4 } from "crypto";
17102
19005
 
17103
19006
  // ../../packages/node-runtime/src/services/topics/ledger.ts
17104
19007
  import { createHash as createHash3 } from "crypto";
@@ -17736,8 +19639,8 @@ var CHAT_TOPICS_MIN_RUNTIME_VERSION = "0.35.1";
17736
19639
  var CHAT_TOPICS_DATA_COMPATIBILITY_VERSION = 2;
17737
19640
  function createChatTopicService(deps) {
17738
19641
  const now = deps.now ?? Date.now;
17739
- const generateId = deps.generateId ?? randomUUID3;
17740
- const runtimeId = deps.runtimeId ?? randomUUID3();
19642
+ const generateId = deps.generateId ?? randomUUID4;
19643
+ const runtimeId = deps.runtimeId ?? randomUUID4();
17741
19644
  const store = new ChatTopicStore(getChatTopicsDbPath(deps.pathProvider.getUserDataDir()), {
17742
19645
  nativeBinding: deps.nativeBinding
17743
19646
  });
@@ -18351,12 +20254,12 @@ function validationErrorMessage(error) {
18351
20254
  }
18352
20255
 
18353
20256
  // ../../packages/node-runtime/src/database-manager.ts
18354
- import * as fs16 from "fs";
18355
- import * as path28 from "path";
20257
+ import * as fs18 from "fs";
20258
+ import * as path30 from "path";
18356
20259
 
18357
20260
  // ../../packages/node-runtime/src/services/global-insight/snapshot.ts
18358
- import fs14 from "fs";
18359
- import path26 from "path";
20261
+ import fs16 from "fs";
20262
+ import path28 from "path";
18360
20263
 
18361
20264
  // ../../packages/node-runtime/src/services/global-insight/time-range.ts
18362
20265
  function normalizeAnnualSummaryRange(input, nowInput = /* @__PURE__ */ new Date()) {
@@ -18407,17 +20310,17 @@ function toAnnualSummaryLocalDate(range) {
18407
20310
  // ../../packages/node-runtime/src/services/global-insight/snapshot.ts
18408
20311
  var SNAPSHOT_TMP_PREFIX = "annual-summary.tmp-";
18409
20312
  function getAnnualSummarySnapshotPath(snapshotDir, range) {
18410
- return path26.join(snapshotDir, `annual-summary-${toAnnualSummaryRangeKey(range)}.json`);
20313
+ return path28.join(snapshotDir, `annual-summary-${toAnnualSummaryRangeKey(range)}.json`);
18411
20314
  }
18412
20315
  function readAnnualSummarySnapshot(snapshotDir, range, options = {}) {
18413
20316
  const snapshotPath = getAnnualSummarySnapshotPath(snapshotDir, range);
18414
- if (!fs14.existsSync(snapshotPath)) return null;
20317
+ if (!fs16.existsSync(snapshotPath)) return null;
18415
20318
  try {
18416
- return JSON.parse(fs14.readFileSync(snapshotPath, "utf-8"));
20319
+ return JSON.parse(fs16.readFileSync(snapshotPath, "utf-8"));
18417
20320
  } catch (error) {
18418
- const backupPath = path26.join(snapshotDir, `annual-summary.corrupt-${options.now?.() ?? Date.now()}.json`);
20321
+ const backupPath = path28.join(snapshotDir, `annual-summary.corrupt-${options.now?.() ?? Date.now()}.json`);
18419
20322
  try {
18420
- fs14.renameSync(snapshotPath, backupPath);
20323
+ fs16.renameSync(snapshotPath, backupPath);
18421
20324
  } catch (renameError) {
18422
20325
  appLogger.warn("global-insight", "failed to backup corrupt annual summary snapshot", renameError);
18423
20326
  }
@@ -18426,28 +20329,28 @@ function readAnnualSummarySnapshot(snapshotDir, range, options = {}) {
18426
20329
  }
18427
20330
  }
18428
20331
  function writeAnnualSummarySnapshot(snapshotDir, snapshot) {
18429
- fs14.mkdirSync(snapshotDir, { recursive: true });
18430
- const tmpPath = path26.join(snapshotDir, `${SNAPSHOT_TMP_PREFIX}${process.pid}-${Date.now()}`);
18431
- fs14.writeFileSync(tmpPath, JSON.stringify(snapshot, null, 2), "utf-8");
18432
- fs14.renameSync(tmpPath, getAnnualSummarySnapshotPath(snapshotDir, snapshot.range));
20332
+ fs16.mkdirSync(snapshotDir, { recursive: true });
20333
+ const tmpPath = path28.join(snapshotDir, `${SNAPSHOT_TMP_PREFIX}${process.pid}-${Date.now()}`);
20334
+ fs16.writeFileSync(tmpPath, JSON.stringify(snapshot, null, 2), "utf-8");
20335
+ fs16.renameSync(tmpPath, getAnnualSummarySnapshotPath(snapshotDir, snapshot.range));
18433
20336
  }
18434
20337
  function cleanupAnnualSummarySnapshotTempFiles(snapshotDir) {
18435
- if (!fs14.existsSync(snapshotDir)) return;
18436
- for (const name of fs14.readdirSync(snapshotDir)) {
20338
+ if (!fs16.existsSync(snapshotDir)) return;
20339
+ for (const name of fs16.readdirSync(snapshotDir)) {
18437
20340
  if (!name.startsWith(SNAPSHOT_TMP_PREFIX)) continue;
18438
20341
  try {
18439
- fs14.rmSync(path26.join(snapshotDir, name), { force: true });
20342
+ fs16.rmSync(path28.join(snapshotDir, name), { force: true });
18440
20343
  } catch (error) {
18441
20344
  appLogger.warn("global-insight", "failed to remove annual summary snapshot temp file", error);
18442
20345
  }
18443
20346
  }
18444
20347
  }
18445
20348
  function deleteAnnualSummarySnapshots(snapshotDir) {
18446
- if (!fs14.existsSync(snapshotDir)) return;
18447
- for (const name of fs14.readdirSync(snapshotDir)) {
20349
+ if (!fs16.existsSync(snapshotDir)) return;
20350
+ for (const name of fs16.readdirSync(snapshotDir)) {
18448
20351
  if (!name.startsWith("annual-summary-") && !name.startsWith("annual-summary.")) continue;
18449
20352
  try {
18450
- fs14.rmSync(path26.join(snapshotDir, name), { force: true });
20353
+ fs16.rmSync(path28.join(snapshotDir, name), { force: true });
18451
20354
  } catch (error) {
18452
20355
  appLogger.warn("global-insight", "failed to remove annual summary snapshot", error);
18453
20356
  }
@@ -18455,21 +20358,21 @@ function deleteAnnualSummarySnapshots(snapshotDir) {
18455
20358
  }
18456
20359
 
18457
20360
  // ../../packages/node-runtime/src/services/global-insight/time-investment-snapshot.ts
18458
- import fs15 from "fs";
18459
- import path27 from "path";
20361
+ import fs17 from "fs";
20362
+ import path29 from "path";
18460
20363
  var SNAPSHOT_TMP_PREFIX2 = "time-investment.tmp-";
18461
20364
  function getTimeInvestmentSnapshotPath(snapshotDir, range) {
18462
- return path27.join(snapshotDir, `time-investment-${toAnnualSummaryRangeKey(range)}.json`);
20365
+ return path29.join(snapshotDir, `time-investment-${toAnnualSummaryRangeKey(range)}.json`);
18463
20366
  }
18464
20367
  function readTimeInvestmentSnapshot(snapshotDir, range, options = {}) {
18465
20368
  const snapshotPath = getTimeInvestmentSnapshotPath(snapshotDir, range);
18466
- if (!fs15.existsSync(snapshotPath)) return null;
20369
+ if (!fs17.existsSync(snapshotPath)) return null;
18467
20370
  try {
18468
- return JSON.parse(fs15.readFileSync(snapshotPath, "utf-8"));
20371
+ return JSON.parse(fs17.readFileSync(snapshotPath, "utf-8"));
18469
20372
  } catch (error) {
18470
- const backupPath = path27.join(snapshotDir, `time-investment.corrupt-${options.now?.() ?? Date.now()}.json`);
20373
+ const backupPath = path29.join(snapshotDir, `time-investment.corrupt-${options.now?.() ?? Date.now()}.json`);
18471
20374
  try {
18472
- fs15.renameSync(snapshotPath, backupPath);
20375
+ fs17.renameSync(snapshotPath, backupPath);
18473
20376
  } catch (renameError) {
18474
20377
  appLogger.warn("global-insight", "failed to backup corrupt time investment snapshot", renameError);
18475
20378
  }
@@ -18478,28 +20381,28 @@ function readTimeInvestmentSnapshot(snapshotDir, range, options = {}) {
18478
20381
  }
18479
20382
  }
18480
20383
  function writeTimeInvestmentSnapshot(snapshotDir, snapshot) {
18481
- fs15.mkdirSync(snapshotDir, { recursive: true });
18482
- const tmpPath = path27.join(snapshotDir, `${SNAPSHOT_TMP_PREFIX2}${process.pid}-${Date.now()}`);
18483
- fs15.writeFileSync(tmpPath, JSON.stringify(snapshot, null, 2), "utf-8");
18484
- fs15.renameSync(tmpPath, getTimeInvestmentSnapshotPath(snapshotDir, snapshot.range));
20384
+ fs17.mkdirSync(snapshotDir, { recursive: true });
20385
+ const tmpPath = path29.join(snapshotDir, `${SNAPSHOT_TMP_PREFIX2}${process.pid}-${Date.now()}`);
20386
+ fs17.writeFileSync(tmpPath, JSON.stringify(snapshot, null, 2), "utf-8");
20387
+ fs17.renameSync(tmpPath, getTimeInvestmentSnapshotPath(snapshotDir, snapshot.range));
18485
20388
  }
18486
20389
  function cleanupTimeInvestmentSnapshotTempFiles(snapshotDir) {
18487
- if (!fs15.existsSync(snapshotDir)) return;
18488
- for (const name of fs15.readdirSync(snapshotDir)) {
20390
+ if (!fs17.existsSync(snapshotDir)) return;
20391
+ for (const name of fs17.readdirSync(snapshotDir)) {
18489
20392
  if (!name.startsWith(SNAPSHOT_TMP_PREFIX2)) continue;
18490
20393
  try {
18491
- fs15.rmSync(path27.join(snapshotDir, name), { force: true });
20394
+ fs17.rmSync(path29.join(snapshotDir, name), { force: true });
18492
20395
  } catch (error) {
18493
20396
  appLogger.warn("global-insight", "failed to remove time investment snapshot temp file", error);
18494
20397
  }
18495
20398
  }
18496
20399
  }
18497
20400
  function deleteTimeInvestmentSnapshots(snapshotDir) {
18498
- if (!fs15.existsSync(snapshotDir)) return;
18499
- for (const name of fs15.readdirSync(snapshotDir)) {
20401
+ if (!fs17.existsSync(snapshotDir)) return;
20402
+ for (const name of fs17.readdirSync(snapshotDir)) {
18500
20403
  if (!name.startsWith("time-investment-") && !name.startsWith("time-investment.")) continue;
18501
20404
  try {
18502
- fs15.rmSync(path27.join(snapshotDir, name), { force: true });
20405
+ fs17.rmSync(path29.join(snapshotDir, name), { force: true });
18503
20406
  } catch (error) {
18504
20407
  appLogger.warn("global-insight", "failed to remove time investment snapshot", error);
18505
20408
  }
@@ -18508,8 +20411,8 @@ function deleteTimeInvestmentSnapshots(snapshotDir) {
18508
20411
 
18509
20412
  // ../../packages/node-runtime/src/database-manager.ts
18510
20413
  function listDatabaseCandidateIds(databaseDir) {
18511
- if (!fs16.existsSync(databaseDir)) return [];
18512
- return fs16.readdirSync(databaseDir).filter((fileName) => fileName.endsWith(".db") && !fileName.startsWith("._")).map((fileName) => fileName.slice(0, -3));
20414
+ if (!fs18.existsSync(databaseDir)) return [];
20415
+ return fs18.readdirSync(databaseDir).filter((fileName) => fileName.endsWith(".db") && !fileName.startsWith("._")).map((fileName) => fileName.slice(0, -3));
18513
20416
  }
18514
20417
  var DatabaseManager = class {
18515
20418
  constructor(pathProvider, options = {}) {
@@ -18533,7 +20436,7 @@ var DatabaseManager = class {
18533
20436
  return this.cache.get(sessionId);
18534
20437
  }
18535
20438
  const dbPath = this.getDbPath(sessionId);
18536
- if (!fs16.existsSync(dbPath)) return null;
20439
+ if (!fs18.existsSync(dbPath)) return null;
18537
20440
  this.migrateIfNeeded(dbPath);
18538
20441
  this.assertCompatible();
18539
20442
  const adapter = openBetterSqliteDatabase(dbPath, {
@@ -18579,7 +20482,7 @@ var DatabaseManager = class {
18579
20482
  this.cache.delete(sessionId);
18580
20483
  }
18581
20484
  const dbPath = this.getDbPath(sessionId);
18582
- if (!fs16.existsSync(dbPath)) return null;
20485
+ if (!fs18.existsSync(dbPath)) return null;
18583
20486
  const adapter = openBetterSqliteDatabase(dbPath, {
18584
20487
  readonly: false,
18585
20488
  nativeBinding: this.nativeBinding
@@ -18617,14 +20520,20 @@ var DatabaseManager = class {
18617
20520
  * 列举数据库目录下的所有聊天会话 ID
18618
20521
  */
18619
20522
  listSessionIds() {
18620
- this.assertCompatible();
18621
- const dbDir = this.pathProvider.getDatabaseDir();
18622
- return listDatabaseCandidateIds(dbDir).filter((id) => {
20523
+ return this.listSessionCandidateIds().filter((id) => {
18623
20524
  const db = this.open(id);
18624
20525
  if (!db) return false;
18625
20526
  return isChatSessionDb(db);
18626
20527
  });
18627
20528
  }
20529
+ /**
20530
+ * 仅列出数据库文件候选,不同步打开所有数据库。
20531
+ * 需要逐库检查、取消和时间预算的调用方应使用此入口后自行验证。
20532
+ */
20533
+ listSessionCandidateIds() {
20534
+ this.assertCompatible();
20535
+ return listDatabaseCandidateIds(this.pathProvider.getDatabaseDir());
20536
+ }
18628
20537
  /**
18629
20538
  * 只读列举聊天会话 ID,不触发数据库迁移或兼容门禁提升。
18630
20539
  */
@@ -18647,17 +20556,17 @@ var DatabaseManager = class {
18647
20556
  * 获取数据库文件路径
18648
20557
  */
18649
20558
  getDbPath(sessionId) {
18650
- return path28.join(this.pathProvider.getDatabaseDir(), `${sessionId}.db`);
20559
+ return path30.join(this.pathProvider.getDatabaseDir(), `${sessionId}.db`);
18651
20560
  }
18652
20561
  openRawSessionDatabase(sessionId, options = {}) {
18653
20562
  this.assertCompatible();
18654
20563
  const dbPath = this.getDbPath(sessionId);
18655
- if (!options.create && !fs16.existsSync(dbPath)) {
20564
+ if (!options.create && !fs18.existsSync(dbPath)) {
18656
20565
  throw new Error(`Session database not found: ${sessionId}`);
18657
20566
  }
18658
20567
  if (!options.readonly) {
18659
20568
  this.close(sessionId);
18660
- fs16.mkdirSync(path28.dirname(dbPath), { recursive: true });
20569
+ fs18.mkdirSync(path30.dirname(dbPath), { recursive: true });
18661
20570
  }
18662
20571
  const adapter = openBetterSqliteDatabase(dbPath, {
18663
20572
  readonly: options.readonly ?? false,
@@ -18676,19 +20585,19 @@ var DatabaseManager = class {
18676
20585
  assertSessionChatTopicsIdle(this.pathProvider.getUserDataDir(), sessionId, { nativeBinding: this.nativeBinding });
18677
20586
  this.close(sessionId);
18678
20587
  const dbPath = this.getDbPath(sessionId);
18679
- const existed = ["", "-wal", "-shm"].some((suffix) => fs16.existsSync(dbPath + suffix));
18680
- if (fs16.existsSync(dbPath)) fs16.unlinkSync(dbPath);
20588
+ const existed = ["", "-wal", "-shm"].some((suffix) => fs18.existsSync(dbPath + suffix));
20589
+ if (fs18.existsSync(dbPath)) fs18.unlinkSync(dbPath);
18681
20590
  for (const suffix of ["-wal", "-shm"]) {
18682
20591
  try {
18683
20592
  const filePath = dbPath + suffix;
18684
- if (fs16.existsSync(filePath)) fs16.unlinkSync(filePath);
20593
+ if (fs18.existsSync(filePath)) fs18.unlinkSync(filePath);
18685
20594
  } catch {
18686
20595
  }
18687
20596
  }
18688
20597
  deleteSessionChatTopics(this.pathProvider.getUserDataDir(), sessionId, { nativeBinding: this.nativeBinding });
18689
20598
  const cacheDir = this.pathProvider.getCacheDir();
18690
20599
  deleteSessionCache(sessionId, cacheDir);
18691
- deleteSessionCache(sessionId, path28.join(cacheDir, "query"));
20600
+ deleteSessionCache(sessionId, path30.join(cacheDir, "query"));
18692
20601
  deleteSessionCache(sessionId, getContactsFactsCacheDir(this.pathProvider.getUserDataDir()));
18693
20602
  deleteSessionCache(sessionId, getGlobalInsightFactsCacheDir(this.pathProvider.getUserDataDir()));
18694
20603
  deleteAnnualSummarySnapshots(getGlobalInsightDir(this.pathProvider.getUserDataDir()));
@@ -18731,12 +20640,12 @@ function shouldRepairCompatibilityGate(schemaVersion) {
18731
20640
  }
18732
20641
 
18733
20642
  // ../../packages/node-runtime/src/cache/analytics-cache.ts
18734
- import * as fs17 from "fs";
20643
+ import * as fs19 from "fs";
18735
20644
  function getDbFileVersion(dbPath) {
18736
20645
  const parts = [];
18737
20646
  for (const p of [dbPath, `${dbPath}-wal`]) {
18738
20647
  try {
18739
- const st = fs17.statSync(p);
20648
+ const st = fs19.statSync(p);
18740
20649
  parts.push(`${Math.floor(st.mtimeMs)}:${st.size}`);
18741
20650
  } catch {
18742
20651
  parts.push("-");
@@ -18810,6 +20719,7 @@ var StaticPathProvider = class {
18810
20719
  function createDatabaseManagerAdapter(dbManager) {
18811
20720
  return {
18812
20721
  listSessionIds: () => dbManager.listSessionIds(),
20722
+ listSessionCandidateIds: () => dbManager.listSessionCandidateIds(),
18813
20723
  openReadonly: (sessionId) => dbManager.open(sessionId),
18814
20724
  openWritable: (sessionId) => dbManager.openWritable(sessionId),
18815
20725
  closeSession: (sessionId) => dbManager.close(sessionId),
@@ -18890,14 +20800,16 @@ export {
18890
20800
  POS_TAG_DEFINITIONS,
18891
20801
  MEANINGFUL_POS_TAGS,
18892
20802
  isStopword,
18893
- cleanText,
18894
- isValidWord,
18895
20803
  buildExcludeKeywordsConditions,
18896
20804
  getMentionAnalysis,
18897
20805
  getLaughAnalysis,
18898
20806
  getClusterGraph,
18899
20807
  resolveOwnerMember,
20808
+ resolveContactMember,
20809
+ getNonSystemMembersForContacts,
18900
20810
  getLatestContactMessageTs,
20811
+ getParticipantSessionFacts,
20812
+ getParticipantSetInteractionFacts,
18901
20813
  getPrivateContactFacts,
18902
20814
  getGroupContactFacts,
18903
20815
  getGroupRelationshipGraphFacts,
@@ -18906,6 +20818,7 @@ export {
18906
20818
  matchOwnerProfile,
18907
20819
  shouldScopeContactToSession,
18908
20820
  buildContactKey,
20821
+ getCrossChatSessionActivityFacts,
18909
20822
  CHATLAB_FORMAT_VERSION,
18910
20823
  CHATLAB_SUPPORTED_FORMAT_VERSIONS,
18911
20824
  isSupportedChatLabFormatVersion,
@@ -18926,6 +20839,7 @@ export {
18926
20839
  queryMessages,
18927
20840
  searchMessagesLike,
18928
20841
  searchMessagesByKeywords,
20842
+ getRecentMessages,
18929
20843
  getMembers,
18930
20844
  executeReadonlySql,
18931
20845
  executeSql,
@@ -18988,6 +20902,21 @@ export {
18988
20902
  parseSkillFile,
18989
20903
  extractSkillId,
18990
20904
  AIChatManager,
20905
+ initNlpDir,
20906
+ getNlpDir,
20907
+ getJieba,
20908
+ clearJiebaInstance,
20909
+ segment,
20910
+ batchSegmentWithFrequency,
20911
+ batchSegmentChineseWithStats,
20912
+ collectPosTagStats,
20913
+ getPosTagDefinitions,
20914
+ AI_MEMORY_CONTENT_MAX_CHARS,
20915
+ buildGlobalMemoryPrompt,
20916
+ AIMemoryService,
20917
+ listAIMemoriesWithSourceStatus,
20918
+ linkAIMemorySources,
20919
+ resolveAIMemorySourceStatus,
18991
20920
  initTokenizer,
18992
20921
  countTokens,
18993
20922
  countMessagesTokens,
@@ -19042,11 +20971,15 @@ export {
19042
20971
  createPlanContentBlock,
19043
20972
  buildPlanGuidance,
19044
20973
  buildSemanticSearchGuidance,
20974
+ createAiTranslate,
20975
+ aiLocales,
19045
20976
  estimateTokensFromText,
19046
20977
  AgentEventHandler,
20978
+ CROSS_CHAT_MAX_TOOL_RESULT_TOKENS,
20979
+ resolveCrossChatToolResultTokenBudget,
20980
+ runCrossChatAgent,
20981
+ buildCrossChatSystemPrompt,
19047
20982
  buildSystemPrompt,
19048
- createAiTranslate,
19049
- aiLocales,
19050
20983
  isValidMessage,
19051
20984
  filterValidMessages,
19052
20985
  splitIntoSegments,
@@ -19125,4 +21058,4 @@ export {
19125
21058
  StaticPathProvider,
19126
21059
  createDatabaseManagerAdapter
19127
21060
  };
19128
- //# sourceMappingURL=chunk-TRGR3KKJ.mjs.map
21061
+ //# sourceMappingURL=chunk-4J4MA3RC.mjs.map