botmux 3.7.1 → 3.8.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 (622) hide show
  1. package/dist/.runtime-build-id +1 -1
  2. package/dist/adapters/backend/capabilities.d.ts +17 -0
  3. package/dist/adapters/backend/capabilities.d.ts.map +1 -0
  4. package/dist/adapters/backend/capabilities.js +22 -0
  5. package/dist/adapters/backend/capabilities.js.map +1 -0
  6. package/dist/adapters/backend/critical-control-key.d.ts +19 -0
  7. package/dist/adapters/backend/critical-control-key.d.ts.map +1 -0
  8. package/dist/adapters/backend/critical-control-key.js +34 -0
  9. package/dist/adapters/backend/critical-control-key.js.map +1 -0
  10. package/dist/adapters/backend/herdr-backend.d.ts +9 -0
  11. package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
  12. package/dist/adapters/backend/herdr-backend.js +23 -4
  13. package/dist/adapters/backend/herdr-backend.js.map +1 -1
  14. package/dist/adapters/backend/riff-backend.d.ts.map +1 -1
  15. package/dist/adapters/backend/riff-backend.js +3 -2
  16. package/dist/adapters/backend/riff-backend.js.map +1 -1
  17. package/dist/adapters/backend/session-backend-selector.d.ts +61 -6
  18. package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -1
  19. package/dist/adapters/backend/session-backend-selector.js +173 -9
  20. package/dist/adapters/backend/session-backend-selector.js.map +1 -1
  21. package/dist/adapters/backend/tmux-backend.d.ts +10 -3
  22. package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
  23. package/dist/adapters/backend/tmux-backend.js +25 -8
  24. package/dist/adapters/backend/tmux-backend.js.map +1 -1
  25. package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
  26. package/dist/adapters/backend/tmux-pipe-backend.js +3 -2
  27. package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
  28. package/dist/adapters/backend/types.d.ts +45 -4
  29. package/dist/adapters/backend/types.d.ts.map +1 -1
  30. package/dist/adapters/backend/types.js +11 -0
  31. package/dist/adapters/backend/types.js.map +1 -1
  32. package/dist/adapters/backend/zellij-backend.d.ts.map +1 -1
  33. package/dist/adapters/backend/zellij-backend.js +3 -2
  34. package/dist/adapters/backend/zellij-backend.js.map +1 -1
  35. package/dist/adapters/backend/zmx-backend.d.ts +286 -0
  36. package/dist/adapters/backend/zmx-backend.d.ts.map +1 -0
  37. package/dist/adapters/backend/zmx-backend.js +2396 -0
  38. package/dist/adapters/backend/zmx-backend.js.map +1 -0
  39. package/dist/adapters/cli/claude-code.d.ts.map +1 -1
  40. package/dist/adapters/cli/claude-code.js +3 -0
  41. package/dist/adapters/cli/claude-code.js.map +1 -1
  42. package/dist/adapters/cli/codex-app.d.ts.map +1 -1
  43. package/dist/adapters/cli/codex-app.js +5 -1
  44. package/dist/adapters/cli/codex-app.js.map +1 -1
  45. package/dist/adapters/cli/codex.d.ts.map +1 -1
  46. package/dist/adapters/cli/codex.js +26 -1
  47. package/dist/adapters/cli/codex.js.map +1 -1
  48. package/dist/adapters/cli/fs-policy.d.ts +79 -1
  49. package/dist/adapters/cli/fs-policy.d.ts.map +1 -1
  50. package/dist/adapters/cli/fs-policy.js +290 -13
  51. package/dist/adapters/cli/fs-policy.js.map +1 -1
  52. package/dist/adapters/cli/grok.d.ts.map +1 -1
  53. package/dist/adapters/cli/grok.js +4 -1
  54. package/dist/adapters/cli/grok.js.map +1 -1
  55. package/dist/adapters/cli/oh-my-pi.d.ts.map +1 -1
  56. package/dist/adapters/cli/oh-my-pi.js +5 -3
  57. package/dist/adapters/cli/oh-my-pi.js.map +1 -1
  58. package/dist/adapters/cli/shared-hints.d.ts +7 -4
  59. package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
  60. package/dist/adapters/cli/shared-hints.js +52 -35
  61. package/dist/adapters/cli/shared-hints.js.map +1 -1
  62. package/dist/adapters/cli/strict-input-handle.d.ts +13 -0
  63. package/dist/adapters/cli/strict-input-handle.d.ts.map +1 -0
  64. package/dist/adapters/cli/strict-input-handle.js +39 -0
  65. package/dist/adapters/cli/strict-input-handle.js.map +1 -0
  66. package/dist/adapters/cli/traex.d.ts.map +1 -1
  67. package/dist/adapters/cli/traex.js +6 -4
  68. package/dist/adapters/cli/traex.js.map +1 -1
  69. package/dist/adapters/cli/types.d.ts +16 -0
  70. package/dist/adapters/cli/types.d.ts.map +1 -1
  71. package/dist/bot-registry.d.ts +137 -8
  72. package/dist/bot-registry.d.ts.map +1 -1
  73. package/dist/bot-registry.js +298 -12
  74. package/dist/bot-registry.js.map +1 -1
  75. package/dist/cli/session-list-liveness.d.ts +1 -1
  76. package/dist/cli/session-list-liveness.js +1 -1
  77. package/dist/cli/zmx-managed-attach.d.ts +41 -0
  78. package/dist/cli/zmx-managed-attach.d.ts.map +1 -0
  79. package/dist/cli/zmx-managed-attach.js +72 -0
  80. package/dist/cli/zmx-managed-attach.js.map +1 -0
  81. package/dist/cli.d.ts.map +1 -1
  82. package/dist/cli.js +780 -131
  83. package/dist/cli.js.map +1 -1
  84. package/dist/codex-app-runner.js +90 -2
  85. package/dist/codex-app-runner.js.map +1 -1
  86. package/dist/codex-rpc-engine.d.ts.map +1 -1
  87. package/dist/codex-rpc-engine.js +22 -6
  88. package/dist/codex-rpc-engine.js.map +1 -1
  89. package/dist/config.d.ts +5 -0
  90. package/dist/config.d.ts.map +1 -1
  91. package/dist/config.js +29 -1
  92. package/dist/config.js.map +1 -1
  93. package/dist/core/ask-broker.d.ts +22 -1
  94. package/dist/core/ask-broker.d.ts.map +1 -1
  95. package/dist/core/ask-broker.js +12 -5
  96. package/dist/core/ask-broker.js.map +1 -1
  97. package/dist/core/botmux-wrapper.d.ts +18 -0
  98. package/dist/core/botmux-wrapper.d.ts.map +1 -1
  99. package/dist/core/botmux-wrapper.js +25 -1
  100. package/dist/core/botmux-wrapper.js.map +1 -1
  101. package/dist/core/chat-rename.d.ts +27 -0
  102. package/dist/core/chat-rename.d.ts.map +1 -0
  103. package/dist/core/chat-rename.js +53 -0
  104. package/dist/core/chat-rename.js.map +1 -0
  105. package/dist/core/command-handler.d.ts.map +1 -1
  106. package/dist/core/command-handler.js +160 -12
  107. package/dist/core/command-handler.js.map +1 -1
  108. package/dist/core/cost-calculator.d.ts +25 -1
  109. package/dist/core/cost-calculator.d.ts.map +1 -1
  110. package/dist/core/cost-calculator.js +102 -4
  111. package/dist/core/cost-calculator.js.map +1 -1
  112. package/dist/core/dashboard-ipc-server.d.ts +16 -2
  113. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  114. package/dist/core/dashboard-ipc-server.js +611 -62
  115. package/dist/core/dashboard-ipc-server.js.map +1 -1
  116. package/dist/core/dashboard-rows.d.ts +22 -1
  117. package/dist/core/dashboard-rows.d.ts.map +1 -1
  118. package/dist/core/dashboard-rows.js +61 -0
  119. package/dist/core/dashboard-rows.js.map +1 -1
  120. package/dist/core/dashboard-url.d.ts +6 -0
  121. package/dist/core/dashboard-url.d.ts.map +1 -1
  122. package/dist/core/dashboard-url.js +11 -1
  123. package/dist/core/dashboard-url.js.map +1 -1
  124. package/dist/core/device-isolation-daemon.d.ts +28 -6
  125. package/dist/core/device-isolation-daemon.d.ts.map +1 -1
  126. package/dist/core/device-isolation-daemon.js +96 -13
  127. package/dist/core/device-isolation-daemon.js.map +1 -1
  128. package/dist/core/host-overload-alert.d.ts +75 -0
  129. package/dist/core/host-overload-alert.d.ts.map +1 -1
  130. package/dist/core/host-overload-alert.js +103 -0
  131. package/dist/core/host-overload-alert.js.map +1 -1
  132. package/dist/core/idle-worker-sweeper.js +1 -1
  133. package/dist/core/idle-worker-sweeper.js.map +1 -1
  134. package/dist/core/inflight-input-tracker.d.ts +3 -0
  135. package/dist/core/inflight-input-tracker.d.ts.map +1 -1
  136. package/dist/core/inflight-input-tracker.js +9 -0
  137. package/dist/core/inflight-input-tracker.js.map +1 -1
  138. package/dist/core/passthrough-commands.d.ts.map +1 -1
  139. package/dist/core/passthrough-commands.js +10 -0
  140. package/dist/core/passthrough-commands.js.map +1 -1
  141. package/dist/core/persistent-backend.d.ts +32 -4
  142. package/dist/core/persistent-backend.d.ts.map +1 -1
  143. package/dist/core/persistent-backend.js +75 -6
  144. package/dist/core/persistent-backend.js.map +1 -1
  145. package/dist/core/plugins/mcp/gateway.js +2 -2
  146. package/dist/core/plugins/mcp/gateway.js.map +1 -1
  147. package/dist/core/plugins/runtime.d.ts +3 -0
  148. package/dist/core/plugins/runtime.d.ts.map +1 -1
  149. package/dist/core/plugins/runtime.js.map +1 -1
  150. package/dist/core/plugins/service-manager.d.ts +4 -1
  151. package/dist/core/plugins/service-manager.d.ts.map +1 -1
  152. package/dist/core/plugins/service-manager.js +11 -6
  153. package/dist/core/plugins/service-manager.js.map +1 -1
  154. package/dist/core/role-library.d.ts +48 -1
  155. package/dist/core/role-library.d.ts.map +1 -1
  156. package/dist/core/role-library.js +111 -3
  157. package/dist/core/role-library.js.map +1 -1
  158. package/dist/core/role-resolver.d.ts +14 -2
  159. package/dist/core/role-resolver.d.ts.map +1 -1
  160. package/dist/core/role-resolver.js +47 -4
  161. package/dist/core/role-resolver.js.map +1 -1
  162. package/dist/core/session-activity.d.ts +14 -0
  163. package/dist/core/session-activity.d.ts.map +1 -1
  164. package/dist/core/session-activity.js +46 -1
  165. package/dist/core/session-activity.js.map +1 -1
  166. package/dist/core/session-board.d.ts +5 -2
  167. package/dist/core/session-board.d.ts.map +1 -1
  168. package/dist/core/session-board.js +5 -2
  169. package/dist/core/session-board.js.map +1 -1
  170. package/dist/core/session-liveness.d.ts +6 -5
  171. package/dist/core/session-liveness.d.ts.map +1 -1
  172. package/dist/core/session-liveness.js +34 -13
  173. package/dist/core/session-liveness.js.map +1 -1
  174. package/dist/core/session-manager.d.ts +3 -2
  175. package/dist/core/session-manager.d.ts.map +1 -1
  176. package/dist/core/session-manager.js +397 -103
  177. package/dist/core/session-manager.js.map +1 -1
  178. package/dist/core/session-marker.d.ts +11 -2
  179. package/dist/core/session-marker.d.ts.map +1 -1
  180. package/dist/core/session-marker.js +50 -11
  181. package/dist/core/session-marker.js.map +1 -1
  182. package/dist/core/session-message-preview.d.ts +20 -0
  183. package/dist/core/session-message-preview.d.ts.map +1 -0
  184. package/dist/core/session-message-preview.js +188 -0
  185. package/dist/core/session-message-preview.js.map +1 -0
  186. package/dist/core/session-title.d.ts +5 -1
  187. package/dist/core/session-title.d.ts.map +1 -1
  188. package/dist/core/session-title.js +20 -3
  189. package/dist/core/session-title.js.map +1 -1
  190. package/dist/core/startup-commands.d.ts +1 -1
  191. package/dist/core/startup-commands.js +1 -1
  192. package/dist/core/terminal-url.d.ts.map +1 -1
  193. package/dist/core/terminal-url.js +3 -2
  194. package/dist/core/terminal-url.js.map +1 -1
  195. package/dist/core/trigger-session.d.ts.map +1 -1
  196. package/dist/core/trigger-session.js +75 -12
  197. package/dist/core/trigger-session.js.map +1 -1
  198. package/dist/core/types.d.ts +35 -0
  199. package/dist/core/types.d.ts.map +1 -1
  200. package/dist/core/types.js +23 -0
  201. package/dist/core/types.js.map +1 -1
  202. package/dist/core/worker-ipc.d.ts +11 -0
  203. package/dist/core/worker-ipc.d.ts.map +1 -0
  204. package/dist/core/worker-ipc.js +26 -0
  205. package/dist/core/worker-ipc.js.map +1 -0
  206. package/dist/core/worker-pool.d.ts +179 -29
  207. package/dist/core/worker-pool.d.ts.map +1 -1
  208. package/dist/core/worker-pool.js +2118 -331
  209. package/dist/core/worker-pool.js.map +1 -1
  210. package/dist/daemon.d.ts +132 -3
  211. package/dist/daemon.d.ts.map +1 -1
  212. package/dist/daemon.js +1327 -449
  213. package/dist/daemon.js.map +1 -1
  214. package/dist/dashboard/auth.d.ts +2 -0
  215. package/dist/dashboard/auth.d.ts.map +1 -1
  216. package/dist/dashboard/auth.js +9 -0
  217. package/dist/dashboard/auth.js.map +1 -1
  218. package/dist/dashboard/bot-payload.d.ts +4 -0
  219. package/dist/dashboard/bot-payload.d.ts.map +1 -1
  220. package/dist/dashboard/bot-payload.js +5 -0
  221. package/dist/dashboard/bot-payload.js.map +1 -1
  222. package/dist/dashboard/federation-api.d.ts.map +1 -1
  223. package/dist/dashboard/federation-api.js +4 -0
  224. package/dist/dashboard/federation-api.js.map +1 -1
  225. package/dist/dashboard/federation-spoke-api.d.ts.map +1 -1
  226. package/dist/dashboard/federation-spoke-api.js +32 -5
  227. package/dist/dashboard/federation-spoke-api.js.map +1 -1
  228. package/dist/dashboard/public-redact.d.ts +8 -2
  229. package/dist/dashboard/public-redact.d.ts.map +1 -1
  230. package/dist/dashboard/public-redact.js +34 -7
  231. package/dist/dashboard/public-redact.js.map +1 -1
  232. package/dist/dashboard/session-card-model.d.ts.map +1 -1
  233. package/dist/dashboard/session-card-model.js +10 -2
  234. package/dist/dashboard/session-card-model.js.map +1 -1
  235. package/dist/dashboard/settings-write-applier.d.ts +35 -1
  236. package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
  237. package/dist/dashboard/settings-write-applier.js +88 -1
  238. package/dist/dashboard/settings-write-applier.js.map +1 -1
  239. package/dist/dashboard/web/app.d.ts.map +1 -1
  240. package/dist/dashboard/web/app.js +0 -1
  241. package/dist/dashboard/web/app.js.map +1 -1
  242. package/dist/dashboard/web/bot-defaults-page.d.ts +15 -0
  243. package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
  244. package/dist/dashboard/web/bot-defaults-page.js +212 -58
  245. package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
  246. package/dist/dashboard/web/bot-defaults.d.ts +3 -0
  247. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  248. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  249. package/dist/dashboard/web/bot-onboarding.d.ts +2 -1
  250. package/dist/dashboard/web/bot-onboarding.d.ts.map +1 -1
  251. package/dist/dashboard/web/bot-onboarding.js.map +1 -1
  252. package/dist/dashboard/web/clipboard.d.ts +2 -0
  253. package/dist/dashboard/web/clipboard.d.ts.map +1 -0
  254. package/dist/dashboard/web/clipboard.js +42 -0
  255. package/dist/dashboard/web/clipboard.js.map +1 -0
  256. package/dist/dashboard/web/connectors-page.d.ts.map +1 -1
  257. package/dist/dashboard/web/connectors-page.js +12 -8
  258. package/dist/dashboard/web/connectors-page.js.map +1 -1
  259. package/dist/dashboard/web/dashboard-components.d.ts +17 -16
  260. package/dist/dashboard/web/dashboard-components.d.ts.map +1 -1
  261. package/dist/dashboard/web/dashboard-components.js.map +1 -1
  262. package/dist/dashboard/web/groups-page.d.ts.map +1 -1
  263. package/dist/dashboard/web/groups-page.js +7 -3
  264. package/dist/dashboard/web/groups-page.js.map +1 -1
  265. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  266. package/dist/dashboard/web/i18n.js +205 -12
  267. package/dist/dashboard/web/i18n.js.map +1 -1
  268. package/dist/dashboard/web/icon-tooltip.d.ts.map +1 -1
  269. package/dist/dashboard/web/icon-tooltip.js +16 -1
  270. package/dist/dashboard/web/icon-tooltip.js.map +1 -1
  271. package/dist/dashboard/web/listener-filters.d.ts +23 -0
  272. package/dist/dashboard/web/listener-filters.d.ts.map +1 -0
  273. package/dist/dashboard/web/listener-filters.js +36 -0
  274. package/dist/dashboard/web/listener-filters.js.map +1 -0
  275. package/dist/dashboard/web/overview-page.d.ts.map +1 -1
  276. package/dist/dashboard/web/overview-page.js.map +1 -1
  277. package/dist/dashboard/web/plugin-page.d.ts.map +1 -1
  278. package/dist/dashboard/web/plugin-page.js.map +1 -1
  279. package/dist/dashboard/web/preview-markdown.d.ts +5 -0
  280. package/dist/dashboard/web/preview-markdown.d.ts.map +1 -0
  281. package/dist/dashboard/web/preview-markdown.js +52 -0
  282. package/dist/dashboard/web/preview-markdown.js.map +1 -0
  283. package/dist/dashboard/web/roles-page.d.ts.map +1 -1
  284. package/dist/dashboard/web/roles-page.js +535 -10
  285. package/dist/dashboard/web/roles-page.js.map +1 -1
  286. package/dist/dashboard/web/roles.d.ts +77 -0
  287. package/dist/dashboard/web/roles.d.ts.map +1 -1
  288. package/dist/dashboard/web/roles.js +98 -0
  289. package/dist/dashboard/web/roles.js.map +1 -1
  290. package/dist/dashboard/web/session-terminal.d.ts.map +1 -1
  291. package/dist/dashboard/web/session-terminal.js +6 -4
  292. package/dist/dashboard/web/session-terminal.js.map +1 -1
  293. package/dist/dashboard/web/sessions-kanban.d.ts +4 -1
  294. package/dist/dashboard/web/sessions-kanban.d.ts.map +1 -1
  295. package/dist/dashboard/web/sessions-kanban.js +1 -1
  296. package/dist/dashboard/web/sessions-kanban.js.map +1 -1
  297. package/dist/dashboard/web/sessions-page.d.ts +3 -2
  298. package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
  299. package/dist/dashboard/web/sessions-page.js +163 -28
  300. package/dist/dashboard/web/sessions-page.js.map +1 -1
  301. package/dist/dashboard/web/sessions.d.ts +25 -0
  302. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  303. package/dist/dashboard/web/sessions.js +58 -7
  304. package/dist/dashboard/web/sessions.js.map +1 -1
  305. package/dist/dashboard/web/settings-page.d.ts +28 -3
  306. package/dist/dashboard/web/settings-page.d.ts.map +1 -1
  307. package/dist/dashboard/web/settings-page.js +61 -1
  308. package/dist/dashboard/web/settings-page.js.map +1 -1
  309. package/dist/dashboard/web/skills-page.d.ts +6 -5
  310. package/dist/dashboard/web/skills-page.d.ts.map +1 -1
  311. package/dist/dashboard/web/skills-page.js +2 -0
  312. package/dist/dashboard/web/skills-page.js.map +1 -1
  313. package/dist/dashboard/web/v3-components.d.ts +3 -2
  314. package/dist/dashboard/web/v3-components.d.ts.map +1 -1
  315. package/dist/dashboard/web/v3-components.js.map +1 -1
  316. package/dist/dashboard/web/vc-consumer-profiles-section.d.ts +12 -2
  317. package/dist/dashboard/web/vc-consumer-profiles-section.d.ts.map +1 -1
  318. package/dist/dashboard/web/vc-consumer-profiles-section.js +0 -9
  319. package/dist/dashboard/web/vc-consumer-profiles-section.js.map +1 -1
  320. package/dist/dashboard/web/webhook-logs-page.d.ts +2 -1
  321. package/dist/dashboard/web/webhook-logs-page.d.ts.map +1 -1
  322. package/dist/dashboard/web/webhook-logs-page.js +4 -1
  323. package/dist/dashboard/web/webhook-logs-page.js.map +1 -1
  324. package/dist/dashboard-web/app.js +1 -1
  325. package/dist/dashboard-web/chunks/bot-defaults-page-FAPIMQV3.js +10 -0
  326. package/dist/dashboard-web/chunks/{chunk-QFMH6ITB.js → chunk-5CJMHTT4.js} +1 -1
  327. package/dist/dashboard-web/chunks/{chunk-KQB42ML5.js → chunk-DLZK52JU.js} +14 -12
  328. package/dist/dashboard-web/chunks/chunk-FDZ7WOBW.js +16 -0
  329. package/dist/dashboard-web/chunks/{chunk-3QAUJXAS.js → chunk-GTAJV5YC.js} +1 -1
  330. package/dist/dashboard-web/chunks/{chunk-FNPGOZMX.js → chunk-IHNG6KE6.js} +1 -1
  331. package/dist/dashboard-web/chunks/{chunk-FLGXZJTD.js → chunk-VA6QWSGQ.js} +1 -1
  332. package/dist/dashboard-web/chunks/chunk-XJ7BGPJH.js +1 -0
  333. package/dist/dashboard-web/chunks/chunk-ZJVTGU5X.js +1 -0
  334. package/dist/dashboard-web/chunks/connectors-page-LKWRT3GS.js +1 -0
  335. package/dist/dashboard-web/chunks/groups-page-V6654ICQ.js +8 -0
  336. package/dist/dashboard-web/chunks/insights-page-237RMFQ5.js +7 -0
  337. package/dist/dashboard-web/chunks/{monitor-room-7IVIS75W.js → monitor-room-OO5Y4FRQ.js} +1 -1
  338. package/dist/dashboard-web/chunks/{monitoring-page-5SSJRQZF.js → monitoring-page-OUEITM5P.js} +1 -1
  339. package/dist/dashboard-web/chunks/{office-page-EOND3L5D.js → office-page-4KNAHQ4Q.js} +1 -1
  340. package/dist/dashboard-web/chunks/{overview-page-NISBVBBJ.js → overview-page-QDP4YK2S.js} +1 -1
  341. package/dist/dashboard-web/chunks/{plugin-page-JXC5F5YV.js → plugin-page-VEGTCEAS.js} +1 -1
  342. package/dist/dashboard-web/chunks/roles-page-PGXNZZNP.js +1 -0
  343. package/dist/dashboard-web/chunks/{schedules-page-AVOOKLUH.js → schedules-page-BJNC5TID.js} +1 -1
  344. package/dist/dashboard-web/chunks/sessions-page-M2ZYYCMO.js +6 -0
  345. package/dist/dashboard-web/chunks/settings-page-IJFZKUUW.js +2 -0
  346. package/dist/dashboard-web/chunks/{skills-page-5MW54PCC.js → skills-page-YRAIRZOQ.js} +3 -3
  347. package/dist/dashboard-web/chunks/{team-federation-page-2SNIMVXI.js → team-federation-page-DEU27GRU.js} +1 -1
  348. package/dist/dashboard-web/chunks/{v3-page-W6SBXXQC.js → v3-page-6KHEW2YL.js} +5 -5
  349. package/dist/dashboard-web/chunks/{whiteboards-page-STC54KTP.js → whiteboards-page-VDECAALH.js} +1 -1
  350. package/dist/dashboard-web/style.css +12545 -11432
  351. package/dist/dashboard.js +215 -36
  352. package/dist/dashboard.js.map +1 -1
  353. package/dist/global-config.d.ts +41 -2
  354. package/dist/global-config.d.ts.map +1 -1
  355. package/dist/global-config.js +47 -0
  356. package/dist/global-config.js.map +1 -1
  357. package/dist/i18n/en.d.ts.map +1 -1
  358. package/dist/i18n/en.js +71 -19
  359. package/dist/i18n/en.js.map +1 -1
  360. package/dist/i18n/zh.d.ts.map +1 -1
  361. package/dist/i18n/zh.js +71 -19
  362. package/dist/i18n/zh.js.map +1 -1
  363. package/dist/im/lark/callback-button-marker.d.ts +34 -0
  364. package/dist/im/lark/callback-button-marker.d.ts.map +1 -0
  365. package/dist/im/lark/callback-button-marker.js +99 -0
  366. package/dist/im/lark/callback-button-marker.js.map +1 -0
  367. package/dist/im/lark/card-builder.d.ts +46 -10
  368. package/dist/im/lark/card-builder.d.ts.map +1 -1
  369. package/dist/im/lark/card-builder.js +299 -78
  370. package/dist/im/lark/card-builder.js.map +1 -1
  371. package/dist/im/lark/card-handler.d.ts +3 -4
  372. package/dist/im/lark/card-handler.d.ts.map +1 -1
  373. package/dist/im/lark/card-handler.js +380 -148
  374. package/dist/im/lark/card-handler.js.map +1 -1
  375. package/dist/im/lark/client.d.ts +24 -1
  376. package/dist/im/lark/client.d.ts.map +1 -1
  377. package/dist/im/lark/client.js +101 -8
  378. package/dist/im/lark/client.js.map +1 -1
  379. package/dist/im/lark/doc-comment.d.ts.map +1 -1
  380. package/dist/im/lark/doc-comment.js +6 -1
  381. package/dist/im/lark/doc-comment.js.map +1 -1
  382. package/dist/im/lark/event-dispatcher.d.ts +115 -3
  383. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  384. package/dist/im/lark/event-dispatcher.js +534 -147
  385. package/dist/im/lark/event-dispatcher.js.map +1 -1
  386. package/dist/im/lark/grant-command.d.ts +16 -12
  387. package/dist/im/lark/grant-command.d.ts.map +1 -1
  388. package/dist/im/lark/grant-command.js +48 -151
  389. package/dist/im/lark/grant-command.js.map +1 -1
  390. package/dist/im/lark/grant-pending.d.ts +12 -4
  391. package/dist/im/lark/grant-pending.d.ts.map +1 -1
  392. package/dist/im/lark/grant-pending.js +31 -8
  393. package/dist/im/lark/grant-pending.js.map +1 -1
  394. package/dist/im/lark/invite-command.d.ts +20 -0
  395. package/dist/im/lark/invite-command.d.ts.map +1 -0
  396. package/dist/im/lark/invite-command.js +239 -0
  397. package/dist/im/lark/invite-command.js.map +1 -0
  398. package/dist/im/lark/md-card.d.ts +66 -4
  399. package/dist/im/lark/md-card.d.ts.map +1 -1
  400. package/dist/im/lark/md-card.js +201 -31
  401. package/dist/im/lark/md-card.js.map +1 -1
  402. package/dist/im/lark/mention-targets.d.ts +43 -0
  403. package/dist/im/lark/mention-targets.d.ts.map +1 -0
  404. package/dist/im/lark/mention-targets.js +190 -0
  405. package/dist/im/lark/mention-targets.js.map +1 -0
  406. package/dist/im/lark/message-parser.d.ts +31 -0
  407. package/dist/im/lark/message-parser.d.ts.map +1 -1
  408. package/dist/im/lark/message-parser.js +305 -32
  409. package/dist/im/lark/message-parser.js.map +1 -1
  410. package/dist/im/lark/reply-card-footer-signature.d.ts +7 -0
  411. package/dist/im/lark/reply-card-footer-signature.d.ts.map +1 -0
  412. package/dist/im/lark/reply-card-footer-signature.js +7 -0
  413. package/dist/im/lark/reply-card-footer-signature.js.map +1 -0
  414. package/dist/im/lark/sessions-card.d.ts.map +1 -1
  415. package/dist/im/lark/sessions-card.js +4 -1
  416. package/dist/im/lark/sessions-card.js.map +1 -1
  417. package/dist/im/lark/substitute-command.d.ts.map +1 -1
  418. package/dist/im/lark/substitute-command.js +8 -0
  419. package/dist/im/lark/substitute-command.js.map +1 -1
  420. package/dist/im/lark/topic-root-context.d.ts +35 -0
  421. package/dist/im/lark/topic-root-context.d.ts.map +1 -0
  422. package/dist/im/lark/topic-root-context.js +37 -0
  423. package/dist/im/lark/topic-root-context.js.map +1 -0
  424. package/dist/im/lark/v3-blocked-card.d.ts.map +1 -1
  425. package/dist/im/lark/v3-blocked-card.js +2 -1
  426. package/dist/im/lark/v3-blocked-card.js.map +1 -1
  427. package/dist/im/lark/v3-gate-card.d.ts.map +1 -1
  428. package/dist/im/lark/v3-gate-card.js +2 -1
  429. package/dist/im/lark/v3-gate-card.js.map +1 -1
  430. package/dist/im/lark/v3-loop-grant-card.d.ts.map +1 -1
  431. package/dist/im/lark/v3-loop-grant-card.js +2 -1
  432. package/dist/im/lark/v3-loop-grant-card.js.map +1 -1
  433. package/dist/im/lark/v3-progress-card.d.ts.map +1 -1
  434. package/dist/im/lark/v3-progress-card.js +2 -1
  435. package/dist/im/lark/v3-progress-card.js.map +1 -1
  436. package/dist/im/lark/v3-revisit-grant-card.d.ts.map +1 -1
  437. package/dist/im/lark/v3-revisit-grant-card.js +2 -1
  438. package/dist/im/lark/v3-revisit-grant-card.js.map +1 -1
  439. package/dist/index-core-only.d.ts +3 -0
  440. package/dist/index-core-only.d.ts.map +1 -0
  441. package/dist/index-core-only.js +107 -0
  442. package/dist/index-core-only.js.map +1 -0
  443. package/dist/services/async-trigger-state.d.ts +8 -0
  444. package/dist/services/async-trigger-state.d.ts.map +1 -1
  445. package/dist/services/async-trigger-state.js +3 -2
  446. package/dist/services/async-trigger-state.js.map +1 -1
  447. package/dist/services/async-trigger-store.d.ts +10 -2
  448. package/dist/services/async-trigger-store.d.ts.map +1 -1
  449. package/dist/services/async-trigger-store.js +3 -2
  450. package/dist/services/async-trigger-store.js.map +1 -1
  451. package/dist/services/backend-availability.d.ts +3 -1
  452. package/dist/services/backend-availability.d.ts.map +1 -1
  453. package/dist/services/backend-availability.js +14 -1
  454. package/dist/services/backend-availability.js.map +1 -1
  455. package/dist/services/backend-type-store.d.ts.map +1 -1
  456. package/dist/services/backend-type-store.js +1 -1
  457. package/dist/services/backend-type-store.js.map +1 -1
  458. package/dist/services/bot-config-store.d.ts.map +1 -1
  459. package/dist/services/bot-config-store.js +2 -1
  460. package/dist/services/bot-config-store.js.map +1 -1
  461. package/dist/services/bridge-fallback-gate.d.ts +14 -1
  462. package/dist/services/bridge-fallback-gate.d.ts.map +1 -1
  463. package/dist/services/bridge-fallback-gate.js +65 -1
  464. package/dist/services/bridge-fallback-gate.js.map +1 -1
  465. package/dist/services/card-prefs-store.d.ts +7 -2
  466. package/dist/services/card-prefs-store.d.ts.map +1 -1
  467. package/dist/services/card-prefs-store.js +30 -4
  468. package/dist/services/card-prefs-store.js.map +1 -1
  469. package/dist/services/codex-app-runner-protocol.d.ts +9 -0
  470. package/dist/services/codex-app-runner-protocol.d.ts.map +1 -1
  471. package/dist/services/codex-app-runner-protocol.js +17 -0
  472. package/dist/services/codex-app-runner-protocol.js.map +1 -1
  473. package/dist/services/codex-app-token-usage.d.ts +93 -0
  474. package/dist/services/codex-app-token-usage.d.ts.map +1 -0
  475. package/dist/services/codex-app-token-usage.js +190 -0
  476. package/dist/services/codex-app-token-usage.js.map +1 -0
  477. package/dist/services/codex-transcript.d.ts +6 -1
  478. package/dist/services/codex-transcript.d.ts.map +1 -1
  479. package/dist/services/codex-transcript.js +103 -32
  480. package/dist/services/codex-transcript.js.map +1 -1
  481. package/dist/services/federation-roster.d.ts +4 -0
  482. package/dist/services/federation-roster.d.ts.map +1 -1
  483. package/dist/services/federation-roster.js +3 -0
  484. package/dist/services/federation-roster.js.map +1 -1
  485. package/dist/services/federation-store.d.ts +6 -0
  486. package/dist/services/federation-store.d.ts.map +1 -1
  487. package/dist/services/federation-store.js.map +1 -1
  488. package/dist/services/grant-policy.d.ts +13 -0
  489. package/dist/services/grant-policy.d.ts.map +1 -0
  490. package/dist/services/grant-policy.js +40 -0
  491. package/dist/services/grant-policy.js.map +1 -0
  492. package/dist/services/grant-store.d.ts +24 -3
  493. package/dist/services/grant-store.d.ts.map +1 -1
  494. package/dist/services/grant-store.js +196 -12
  495. package/dist/services/grant-store.js.map +1 -1
  496. package/dist/services/groups-store.d.ts +36 -0
  497. package/dist/services/groups-store.d.ts.map +1 -1
  498. package/dist/services/groups-store.js +123 -1
  499. package/dist/services/groups-store.js.map +1 -1
  500. package/dist/services/jsonl-cursor.d.ts.map +1 -1
  501. package/dist/services/jsonl-cursor.js +8 -2
  502. package/dist/services/jsonl-cursor.js.map +1 -1
  503. package/dist/services/local-cli-opener.d.ts.map +1 -1
  504. package/dist/services/local-cli-opener.js +32 -0
  505. package/dist/services/local-cli-opener.js.map +1 -1
  506. package/dist/services/message-listener-run-preview-store.d.ts +43 -0
  507. package/dist/services/message-listener-run-preview-store.d.ts.map +1 -0
  508. package/dist/services/message-listener-run-preview-store.js +152 -0
  509. package/dist/services/message-listener-run-preview-store.js.map +1 -0
  510. package/dist/services/message-listener-store.d.ts +26 -0
  511. package/dist/services/message-listener-store.d.ts.map +1 -0
  512. package/dist/services/message-listener-store.js +138 -0
  513. package/dist/services/message-listener-store.js.map +1 -0
  514. package/dist/services/message-listener.d.ts +112 -0
  515. package/dist/services/message-listener.d.ts.map +1 -0
  516. package/dist/services/message-listener.js +391 -0
  517. package/dist/services/message-listener.js.map +1 -0
  518. package/dist/services/overload-alert-migration.d.ts +15 -0
  519. package/dist/services/overload-alert-migration.d.ts.map +1 -0
  520. package/dist/services/overload-alert-migration.js +115 -0
  521. package/dist/services/overload-alert-migration.js.map +1 -0
  522. package/dist/services/send-policy.d.ts +38 -0
  523. package/dist/services/send-policy.d.ts.map +1 -1
  524. package/dist/services/send-policy.js +34 -0
  525. package/dist/services/send-policy.js.map +1 -1
  526. package/dist/services/session-store.d.ts +12 -1
  527. package/dist/services/session-store.d.ts.map +1 -1
  528. package/dist/services/session-store.js +44 -2
  529. package/dist/services/session-store.js.map +1 -1
  530. package/dist/services/skill-registry-store.d.ts.map +1 -1
  531. package/dist/services/skill-registry-store.js +87 -12
  532. package/dist/services/skill-registry-store.js.map +1 -1
  533. package/dist/services/substitute-mode-normalize.d.ts +3 -0
  534. package/dist/services/substitute-mode-normalize.d.ts.map +1 -1
  535. package/dist/services/substitute-mode-normalize.js +8 -0
  536. package/dist/services/substitute-mode-normalize.js.map +1 -1
  537. package/dist/services/substitute-mode-store.d.ts.map +1 -1
  538. package/dist/services/substitute-mode-store.js +8 -1
  539. package/dist/services/substitute-mode-store.js.map +1 -1
  540. package/dist/services/transcript-resolver.d.ts +6 -1
  541. package/dist/services/transcript-resolver.d.ts.map +1 -1
  542. package/dist/services/transcript-resolver.js +145 -29
  543. package/dist/services/transcript-resolver.js.map +1 -1
  544. package/dist/services/trigger-types.d.ts +16 -0
  545. package/dist/services/trigger-types.d.ts.map +1 -1
  546. package/dist/services/trigger-types.js +6 -0
  547. package/dist/services/trigger-types.js.map +1 -1
  548. package/dist/setup/bot-config-editor.js +2 -2
  549. package/dist/setup/bot-config-editor.js.map +1 -1
  550. package/dist/setup/ensure-zmx.d.ts +31 -0
  551. package/dist/setup/ensure-zmx.d.ts.map +1 -0
  552. package/dist/setup/ensure-zmx.js +161 -0
  553. package/dist/setup/ensure-zmx.js.map +1 -0
  554. package/dist/setup/index.d.ts +1 -1
  555. package/dist/setup/index.js +1 -1
  556. package/dist/setup/open-platform-automation.d.ts +9 -0
  557. package/dist/setup/open-platform-automation.d.ts.map +1 -1
  558. package/dist/setup/open-platform-automation.js +15 -2
  559. package/dist/setup/open-platform-automation.js.map +1 -1
  560. package/dist/setup/setup-args.d.ts +1 -1
  561. package/dist/setup/setup-args.d.ts.map +1 -1
  562. package/dist/setup/setup-args.js +1 -1
  563. package/dist/skills/definitions.d.ts.map +1 -1
  564. package/dist/skills/definitions.js +32 -0
  565. package/dist/skills/definitions.js.map +1 -1
  566. package/dist/skills/injection-mode.d.ts +6 -2
  567. package/dist/skills/injection-mode.d.ts.map +1 -1
  568. package/dist/skills/injection-mode.js +10 -5
  569. package/dist/skills/injection-mode.js.map +1 -1
  570. package/dist/types.d.ts +79 -3
  571. package/dist/types.d.ts.map +1 -1
  572. package/dist/utils/child-env.d.ts +1 -1
  573. package/dist/utils/child-env.d.ts.map +1 -1
  574. package/dist/utils/child-env.js +4 -0
  575. package/dist/utils/child-env.js.map +1 -1
  576. package/dist/utils/effort-confirm-dialog.d.ts +40 -0
  577. package/dist/utils/effort-confirm-dialog.d.ts.map +1 -0
  578. package/dist/utils/effort-confirm-dialog.js +74 -0
  579. package/dist/utils/effort-confirm-dialog.js.map +1 -0
  580. package/dist/utils/pending-input-queue.d.ts +56 -0
  581. package/dist/utils/pending-input-queue.d.ts.map +1 -1
  582. package/dist/utils/pending-input-queue.js +80 -0
  583. package/dist/utils/pending-input-queue.js.map +1 -1
  584. package/dist/utils/render-dimensions.d.ts +8 -0
  585. package/dist/utils/render-dimensions.d.ts.map +1 -1
  586. package/dist/utils/render-dimensions.js +10 -0
  587. package/dist/utils/render-dimensions.js.map +1 -1
  588. package/dist/utils/stuck-key-guard.d.ts +3 -2
  589. package/dist/utils/stuck-key-guard.d.ts.map +1 -1
  590. package/dist/utils/stuck-key-guard.js +3 -3
  591. package/dist/utils/stuck-key-guard.js.map +1 -1
  592. package/dist/utils/terminal-renderer.d.ts +6 -0
  593. package/dist/utils/terminal-renderer.d.ts.map +1 -1
  594. package/dist/utils/terminal-renderer.js +15 -0
  595. package/dist/utils/terminal-renderer.js.map +1 -1
  596. package/dist/utils/tui-input-delivery.d.ts +32 -0
  597. package/dist/utils/tui-input-delivery.d.ts.map +1 -0
  598. package/dist/utils/tui-input-delivery.js +53 -0
  599. package/dist/utils/tui-input-delivery.js.map +1 -0
  600. package/dist/utils/xml.d.ts +19 -0
  601. package/dist/utils/xml.d.ts.map +1 -0
  602. package/dist/utils/xml.js +26 -0
  603. package/dist/utils/xml.js.map +1 -0
  604. package/dist/worker.js +1852 -374
  605. package/dist/worker.js.map +1 -1
  606. package/dist/workflows/definition.d.ts +8 -8
  607. package/dist/workflows/events/payloads.d.ts +8 -8
  608. package/dist/workflows/events/schema.d.ts +160 -160
  609. package/dist/workflows/v3/session-relay-client.js +1 -1
  610. package/dist/workflows/v3/session-relay-client.js.map +1 -1
  611. package/dist/workflows/v3/worker-fence.d.ts.map +1 -1
  612. package/dist/workflows/v3/worker-fence.js +15 -10
  613. package/dist/workflows/v3/worker-fence.js.map +1 -1
  614. package/package.json +1 -1
  615. package/dist/dashboard-web/chunks/bot-defaults-page-UKA7K4JV.js +0 -10
  616. package/dist/dashboard-web/chunks/chunk-DFLNXQNT.js +0 -1
  617. package/dist/dashboard-web/chunks/connectors-page-A77YAB5Y.js +0 -1
  618. package/dist/dashboard-web/chunks/groups-page-RUZNVXQB.js +0 -8
  619. package/dist/dashboard-web/chunks/insights-page-RF7U7RTA.js +0 -22
  620. package/dist/dashboard-web/chunks/roles-page-BUMKD7Y4.js +0 -1
  621. package/dist/dashboard-web/chunks/sessions-page-6HLSEAB7.js +0 -2
  622. package/dist/dashboard-web/chunks/settings-page-MJDZHQ4X.js +0 -2
@@ -10,13 +10,13 @@ import { atomicWriteFileSync } from '../../utils/atomic-write.js';
10
10
  import { join } from 'node:path';
11
11
  import { getBot, getAllBots, findOncallChat, getOwnerOpenId } from '../../bot-registry.js';
12
12
  import { config, isVcMeetingAgentGloballyEnabled, vcMeetingAgentGlobalListenerBotAppId } from '../../config.js';
13
- import { getChatInfo, getChatMode, getCachedChatMode, resolveSiblingBotBySenderOpenId, replyMessage, sendMessage, sendUserMessage, isHumanOpenId, updateMessage } from './client.js';
13
+ import { getChatInfo, getChatMode, getCachedChatMode, getUserProfile, listChatMessagesUntil, resolveSiblingBotBySenderOpenId, replyMessage, sendMessage, sendUserMessage, isHumanOpenId, updateMessage } from './client.js';
14
14
  import { logger } from '../../utils/logger.js';
15
15
  import { BoundedMap } from '../../utils/bounded-map.js';
16
16
  import { serializeByAnchor } from '../../utils/anchor-serializer.js';
17
17
  import { parseForceTopicInvocation } from '../../core/command-handler.js';
18
18
  import { shouldAutoStartOnNewTopic } from '../../core/auto-start.js';
19
- import { stripLeadingMentions, mentionOpenId, extractMentionIdentities } from './message-parser.js';
19
+ import { stripLeadingMentions, mentionOpenId, mentionAppId, extractMentionIdentities, messageMentionsBot } from './message-parser.js';
20
20
  import { recordObservedBots, listObservedBots } from '../../services/observed-bots-store.js';
21
21
  import { isTeamBot, recordTeamBot } from '../../services/team-bots-store.js';
22
22
  import { isTeamGroupChat } from '../../services/team-groups-store.js';
@@ -28,12 +28,13 @@ import { BOTMUX_REQUIRED_SCOPES, DOC_FEATURE_SCOPES, DOC_WATCH_SCOPES, DOC_COMME
28
28
  import { automateOpenPlatformSetup } from '../../setup/open-platform-automation.js';
29
29
  import { larkHosts, normalizeBrand, sdkDomain } from './lark-hosts.js';
30
30
  import { tryHandleGrantCommand } from './grant-command.js';
31
+ import { tryHandleInviteCommand } from './invite-command.js';
31
32
  import { tryHandleReplyModeCommand } from './reply-mode-command.js';
32
33
  import { tryHandleSubstituteCommand } from './substitute-command.js';
33
34
  import { buildGrantCard } from './card-builder.js';
34
35
  import { openPending, isThrottled, clearPending } from './grant-pending.js';
35
36
  import { localeForBot, t } from '../../i18n/index.js';
36
- import { chatQuotaKey, globalQuotaKey } from '../../services/grant-store.js';
37
+ import { chatQuotaKey, getGrantExpiresAt, globalQuotaKey, removeExpiredGrant, } from '../../services/grant-store.js';
37
38
  import { ForwardFollowupBuffer } from './forward-followup-buffer.js';
38
39
  import { listForwardFollowups, putForwardFollowup, removeForwardFollowup } from './forward-followup-store.js';
39
40
  import { claimMessageOnce, _resetCacheForTest as _resetSeenMessagesForTest } from '../../services/seen-message-store.js';
@@ -42,7 +43,9 @@ import { resolveRegularGroupMode, resolveGroupMentionMode } from '../../services
42
43
  import { buildSummaryCommandPrompt } from './summary-command.js';
43
44
  import { DEFAULT_SUMMARY_PROMPT, summaryRangeFromBotConfig } from '../../services/summary-range-store.js';
44
45
  import { isSubstituteEnabledForChat } from '../../services/substitute-chat-toggle-store.js';
46
+ import { evaluateMessageListener, resolveListenerSenderIdentity, buildListenerBotAppIdToOpenId, collectListenerBotAppIds } from '../../services/message-listener.js';
45
47
  import { parseVcMeetingPushEvent, VC_BOT_MEETING_ACTIVITY_EVENT, VC_BOT_MEETING_ENDED_EVENT, VC_BOT_MEETING_INVITED_EVENT, VC_PARTICIPANT_MEETING_JOINED_EVENT, } from '../../vc-agent/push-source.js';
48
+ import { DEFAULT_GRANT_DURATION_MS, DEFAULT_GRANT_QUOTA } from '../../services/grant-policy.js';
46
49
  // 大厅回执互教的防环闸:每进程对同一打卡者只回一次(见 hall swallow 分支)。
47
50
  const hallEchoReplied = new Set();
48
51
  function vcMeetingEventPayloadForLog(data) {
@@ -674,6 +677,24 @@ export async function getGroupStats(larkAppId, chatId) {
674
677
  return { userCount: 999, botCount: 999 };
675
678
  }
676
679
  }
680
+ /**
681
+ * Drop the cached group stats for one chat — called when a membership-change
682
+ * event that is VISIBLE to this app arrives (user add/delete broadcast, or our
683
+ * own bot being added/removed), so the 1v1 relax gate sees fresh counts on the
684
+ * very next message instead of coasting on stale numbers for up to
685
+ * CHAT_CACHE_TTL. TTL 继续保留,作为事件未订阅(老应用)时的兜底。
686
+ * 注意跨进程边界:「别的 bot 进群/离群」事件只推给当事 bot 自己的 app,
687
+ * 一 bot 一 daemon 部署下此函数够不到兄弟进程的缓存(见 handler 处注释)。
688
+ */
689
+ export function invalidateChatStats(larkAppId, chatId) {
690
+ if (chatStatsCache.delete(`${larkAppId}:${chatId}`)) {
691
+ logger.debug(`[group-stats] invalidated cached stats for ${chatId} (${larkAppId}): membership changed`);
692
+ }
693
+ }
694
+ /** Test-only: clear the group-stats cache between cases. */
695
+ export function __resetChatStatsForTest() {
696
+ chatStatsCache.clear();
697
+ }
677
698
  // ─── Cross-bot open_id mapping ──────────────────────────────────────────
678
699
  //
679
700
  // Lark open_id is per-app scoped: Bot A sees a different open_id for Bot B
@@ -730,6 +751,10 @@ export function isKnownPeerBot(dataDir, larkAppId, senderOpenId) {
730
751
  * union_id roster)与 isTeamBot(团队群里学来的 union_id)都是 union_id 锚定的
731
752
  * bot 专属信任;平台拉的团队群则经 platform: 前缀镜像进 team-groups,走同一个
732
753
  * isTeamGroupChat。两条路都算团队模式,都不需要 /grant。
754
+ *
755
+ * 调用方只剩 {@link evaluateBotTalk} 一个(bot 闸门的唯一入口);(1) 的两条腿与
756
+ * evaluateTalk 的 teamBot 腿重合,真正额外放行的是 (2)。保留本谓词是因为它是
757
+ * 「bot 团队信任」的定义所在,且有专属单测(team-peer-trust / platform-team-trust)。
733
758
  */
734
759
  export function isTrustedTeamBotSender(dataDir, chatId, senderUnionId) {
735
760
  return isTeamBot(dataDir, senderUnionId)
@@ -889,40 +914,17 @@ export function isBotMentioned(larkAppId, message, _senderOpenId) {
889
914
  logger.debug(`[${larkAppId}] Bot open_id not yet known, skipping @mention check`);
890
915
  return false;
891
916
  }
892
- // 1. Check message.mentions array (populated for user-sent text messages).
893
- // mentionOpenId() tolerates both the WS event object shape and the REST
894
- // string shape, so a Lark mention-shape change can't silently break
895
- // @-detection (which would make the bot ignore every @ with no error).
896
- const mentions = message.mentions ?? [];
897
917
  // Early-warning: today's WS events carry mention.id as an object; the REST
898
918
  // API carries a bare string. A string-form id on the event path means Lark
899
919
  // has converged the event onto the REST shape — surface it (once) so a silent
900
- // @-routing regression announces itself even though mentionOpenId absorbs it.
920
+ // @-routing regression announces itself even though the shared gate absorbs it.
921
+ const mentions = message?.mentions ?? [];
901
922
  if (!warnedStringMentionShape && mentions.some((m) => typeof m?.id === 'string')) {
902
923
  warnedStringMentionShape = true;
903
924
  logger.warn(`[${larkAppId}] mention.id arrived in string form on the event path — Lark may have converged the WS event onto the REST shape. mentionOpenId() absorbs it, but verify group @-routing. (logged once per process)`);
904
925
  }
905
- if (mentions.some((m) => mentionMatchesBot(m, larkAppId, botOpenId))) {
906
- return true;
907
- }
908
- // 2. Check post content for inline at tags (bot-sent post messages may not
909
- // populate message.mentions — the @mention is embedded in the content structure)
910
- try {
911
- const content = JSON.parse(message.content ?? '{}');
912
- const inner = content.zh_cn ?? content.en_us ?? content;
913
- if (Array.isArray(inner?.content)) {
914
- for (const paragraph of inner.content) {
915
- if (!Array.isArray(paragraph))
916
- continue;
917
- for (const node of paragraph) {
918
- if (node.tag === 'at' && node.user_id === botOpenId)
919
- return true;
920
- }
921
- }
922
- }
923
- }
924
- catch { /* ignore parse errors */ }
925
- return false;
926
+ // Single source of truth shared with the poll + dashboard-preview legs.
927
+ return messageMentionsBot(message, larkAppId, botOpenId);
926
928
  }
927
929
  /** Does this message @mention a *specific other member* (a person or bot that
928
930
  * is NOT this bot)? Used by the 'ambient' mention policy to decide whether to
@@ -1011,7 +1013,15 @@ export function resolveSubstituteTrigger(larkAppId, message) {
1011
1013
  }
1012
1014
  return undefined;
1013
1015
  }
1016
+ function isSubstituteExcludedChat(cfg, chatId) {
1017
+ return cfg?.excludedChats?.includes(chatId) ?? false;
1018
+ }
1014
1019
  function isSubstituteAllowedChat(cfg, chatId) {
1020
+ // 黑名单先判且 deny-wins:命中即整段替身触发块短路(连带跳过其后的运行态
1021
+ // 开关 isSubstituteEnabledForChat),因此配置黑名单 = 硬关闭,/substitute on
1022
+ // 也翻不回来。对普通群与话题群一视同仁,与白名单同层。
1023
+ if (isSubstituteExcludedChat(cfg, chatId))
1024
+ return false;
1015
1025
  if (!cfg?.chats?.length)
1016
1026
  return true;
1017
1027
  return cfg.chats.includes(chatId);
@@ -1027,29 +1037,29 @@ function mentionMatchesBot(m, larkAppId, botOpenId) {
1027
1037
  const appId = mentionAppId(m);
1028
1038
  return Boolean(larkAppId && appId === larkAppId);
1029
1039
  }
1030
- function mentionIdType(m) {
1031
- if (!m || typeof m !== 'object')
1032
- return undefined;
1033
- if (typeof m.id_type === 'string')
1034
- return m.id_type;
1035
- if (typeof m.idType === 'string')
1036
- return m.idType;
1037
- return undefined;
1038
- }
1039
- function mentionAppId(m) {
1040
- if (!m || typeof m !== 'object')
1041
- return undefined;
1042
- if (typeof m.appId === 'string')
1043
- return m.appId;
1044
- if (typeof m.app_id === 'string')
1045
- return m.app_id;
1046
- const idType = mentionIdType(m);
1047
- if (idType === 'app_id' && typeof m.id === 'string')
1048
- return m.id;
1049
- if (m.id && typeof m.id === 'object' && typeof m.id.app_id === 'string')
1050
- return m.id.app_id;
1051
- return undefined;
1052
- }
1040
+ /**
1041
+ * 全部 talk 放行源的**运行时**枚举(TalkReason 由它派生)。
1042
+ *
1043
+ * 之所以是数组而不是纯 type:`test/bot-talk-parity.test.ts` 按它逐条核对
1044
+ * 「这条腿对人和对 bot 分别是什么行为」,少一格就红。tsconfig 的 include 只有
1045
+ * `src/**`,测试不过 tsc——纯类型层的穷尽约束在这里执行不了,必须落到运行时。
1046
+ *
1047
+ * 新增一条 talk 源时:改 evaluateTalk(人/bot 同时生效)+ 在那张表补一格说明,
1048
+ * **不要**去 bot 路由闸里加 OR 腿(那正是 allowedChatGroups 漏掉的老路)。
1049
+ */
1050
+ export const TALK_REASONS = [
1051
+ 'allowedUser',
1052
+ 'oncall',
1053
+ 'peer',
1054
+ 'teamBot',
1055
+ 'teamMember',
1056
+ 'allowedChatGroup',
1057
+ 'open',
1058
+ 'chatGrant',
1059
+ 'globalGrant',
1060
+ 'p2pOpen',
1061
+ 'none',
1062
+ ];
1053
1063
  export function grantCommandRestriction(larkAppId, chatId, senderOpenId) {
1054
1064
  const bot = getBot(larkAppId);
1055
1065
  if (bot.config.restrictGrantCommands !== true)
@@ -1062,11 +1072,42 @@ export function grantCommandRestriction(larkAppId, chatId, senderOpenId) {
1062
1072
  }
1063
1073
  /** per-chat per-user 授权命中判断(仅用于 canTalk —— 不给管理命令权)。 */
1064
1074
  function hasChatGrant(larkAppId, chatId, openId) {
1065
- return !!chatId && !!openId && !!getBot(larkAppId).config.chatGrants?.[chatId]?.includes(openId);
1075
+ if (!chatId || !openId || !getBot(larkAppId).config.chatGrants?.[chatId]?.includes(openId))
1076
+ return false;
1077
+ return grantNotExpired(larkAppId, 'chat', chatId, openId, chatQuotaKey(chatId, openId));
1066
1078
  }
1067
1079
  /** 全局对话授权命中判断(人/bot 通用,仅用于 canTalk / bot 路由闸 —— 不给管理命令权)。 */
1068
1080
  function hasGlobalGrant(larkAppId, openId) {
1069
- return !!openId && !!getBot(larkAppId).config.globalGrants?.includes(openId);
1081
+ if (!openId || !getBot(larkAppId).config.globalGrants?.includes(openId))
1082
+ return false;
1083
+ return grantNotExpired(larkAppId, 'global', undefined, openId, globalQuotaKey(openId));
1084
+ }
1085
+ const expiryCleanupInFlight = new Set();
1086
+ /**
1087
+ * 到期判断是同步安全边界:过期后本条消息立即拒绝;持久化清理由带 observedExpiresAt
1088
+ * 条件的原子 RMW 异步完成,避免误删刚续期的授权。
1089
+ */
1090
+ function grantNotExpired(larkAppId, scope, chatId, openId, grantKey) {
1091
+ const expiresAt = getGrantExpiresAt(larkAppId, grantKey);
1092
+ if (expiresAt === undefined || Date.now() < expiresAt)
1093
+ return true;
1094
+ const cleanupKey = `${larkAppId}:${grantKey}:${expiresAt}`;
1095
+ if (!expiryCleanupInFlight.has(cleanupKey)) {
1096
+ expiryCleanupInFlight.add(cleanupKey);
1097
+ void removeExpiredGrant(larkAppId, scope, chatId, openId, expiresAt)
1098
+ .then(result => {
1099
+ if (!result.ok)
1100
+ logger.warn(`[grant:${larkAppId}] expiry cleanup failed key=${grantKey} reason=${result.reason}`);
1101
+ })
1102
+ .catch(err => logger.warn(`[grant:${larkAppId}] expiry cleanup failed key=${grantKey}: ${err}`))
1103
+ .finally(() => expiryCleanupInFlight.delete(cleanupKey));
1104
+ }
1105
+ return false;
1106
+ }
1107
+ /** 整群 talk 授权命中判断(裸 `/grant` 写入的 allowedChatGroups)。chat 维度、sender 无关,
1108
+ * 与 oncall 同一个安全模型:只放行 canTalk / bot 路由闸,canOperate 绝不读它。 */
1109
+ function hasAllowedChatGroup(larkAppId, chatId) {
1110
+ return !!chatId && !!getBot(larkAppId).config.allowedChatGroups?.includes(chatId);
1070
1111
  }
1071
1112
  /**
1072
1113
  * 是否配置了任何白名单(限制态)。判定用 **原始** config.allowedUsers, 不用
@@ -1110,7 +1151,20 @@ export function evaluateTalk(larkAppId, chatId, senderOpenId, senderUnionId, mem
1110
1151
  if (chatId && findOncallChat(larkAppId, chatId)) {
1111
1152
  const def = bot.config.messageQuota?.defaultLimit;
1112
1153
  if (typeof def === 'number' && Number.isInteger(def) && def > 0 && senderOpenId) {
1113
- return { allowed: true, reason: 'oncall', quotaKey: chatQuotaKey(chatId, senderOpenId) };
1154
+ // 交集处理:同群若还持有显式 chatGrant,额度由授权决定而非 oncall default。
1155
+ // 三态:live 显式授权 → explicitGrantOverride(不兜 default);已过期 → expiredGrantCleanup
1156
+ //(透传给 consumeQuota,锁内以当前 expiry 为准原子清成员+quota+expiry 并回落 default);
1157
+ // 非成员 → 无覆盖,正常走 oncall default 懒初始化。
1158
+ const gk = chatQuotaKey(chatId, senderOpenId);
1159
+ const isMember = !!bot.config.chatGrants?.[chatId]?.includes(senderOpenId);
1160
+ const exp = isMember ? getGrantExpiresAt(larkAppId, gk) : undefined;
1161
+ if (isMember && exp !== undefined && Date.now() >= exp) {
1162
+ return {
1163
+ allowed: true, reason: 'oncall', quotaKey: gk,
1164
+ expiredGrantCleanup: { scope: 'chat', chatId, openId: senderOpenId },
1165
+ };
1166
+ }
1167
+ return { allowed: true, reason: 'oncall', quotaKey: gk, explicitGrantOverride: isMember };
1114
1168
  }
1115
1169
  return { allowed: true, reason: 'oncall' };
1116
1170
  }
@@ -1130,7 +1184,7 @@ export function evaluateTalk(larkAppId, chatId, senderOpenId, senderUnionId, mem
1130
1184
  if (memberUnionId && isPlatformTeamMemberChat(config.session.dataDir, chatId, memberUnionId)) {
1131
1185
  return { allowed: true, reason: 'teamMember' };
1132
1186
  }
1133
- if (chatId && bot.config.allowedChatGroups?.includes(chatId))
1187
+ if (hasAllowedChatGroup(larkAppId, chatId))
1134
1188
  return { allowed: true, reason: 'allowedChatGroup' };
1135
1189
  // p2pOpen:私聊维度的 talk-open,与 oncall(群维度)同一个安全模型——放行 canTalk,
1136
1190
  // canOperate 一行不读它(管理仍限 allowedUsers)。谁能私聊由飞书应用「可用范围」控制。
@@ -1151,6 +1205,63 @@ export function evaluateTalk(larkAppId, chatId, senderOpenId, senderUnionId, mem
1151
1205
  }
1152
1206
  return { allowed: false, reason: 'none' };
1153
1207
  }
1208
+ /**
1209
+ * BOT 发送方的 talk 判定 —— bot 路由闸(外部 bot @ 本 bot)的**唯一**入口。
1210
+ *
1211
+ * = 人侧 evaluateTalk 的完整模型 + 一条 bot 专属腿。历史上 bot 侧手抄了一份
1212
+ * talk 源子集(oncall / peer / teamBot / chatGrant / globalGrant 的 OR 链),
1213
+ * 于是每加一条新 talk 源就漏一次、复发一次同类 bug:oncall 漏过一次、开放模式
1214
+ * 漏过一次、allowedChatGroups 又漏一次(owner 整群 `/grant` 后真人能说话、外部 bot
1215
+ * 一 @ 仍弹授权卡)。收敛成单一谓词后,**新增 talk 源只需改 evaluateTalk**;
1216
+ * test/bot-talk-parity.test.ts 按 TALK_REASONS 逐条盯着这条约束。
1217
+ *
1218
+ * 与人侧仅有的两条差异,都是刻意的,改动前先读:
1219
+ *
1220
+ * 1. `isTrustedTeamBotSender`(+,bot 才有):其中真正额外放行的是**团队拉群**那条腿
1221
+ * ——本群是团队拉群(团队掌控成员)→ 说话的 botmux bot 即视为背书队友,首次接触
1222
+ * 也放行,与 oncall 群豁免同构。chat 维度、不看 union,覆盖 sender 事件没带
1223
+ * `union_id` 的情况(此时 gate 前那次 `recordTeamBot` 被 `senderUnionId &&` 短路,
1224
+ * evaluateTalk 的 teamBot 腿必然落空)。**这条不能下沉进 evaluateTalk**:那是人/bot
1225
+ * 共用的,加进去等于团队群里任何真人都自动过 canTalk,绕开 teamMember 腿的成员校验
1226
+ * (isPlatformTeamMemberChat 要求 union 在该团队成员名单里)。它另外两条 union 锚定
1227
+ * 的腿与 evaluateTalk 的 teamBot 腿重合,留着是为了让「bot 团队信任」只有一处定义。
1228
+ *
1229
+ * 2. `p2pOpen`(−):不传 chatType → 该腿 fail-closed 不生效。飞书里 bot 之间不存在
1230
+ * 私聊,开着只是白扩边界。
1231
+ *
1232
+ * @param senderUnionId 必须是飞书盖章的 bot 发送方 union(`sender_type ∈ app|bot`),
1233
+ * 与 evaluateTalk 的 bot-trust 腿同一契约。真人 union 走 memberUnionId,本函数恒不传。
1234
+ */
1235
+ export function evaluateBotTalk(larkAppId, chatId, senderOpenId, senderUnionId) {
1236
+ const ev = evaluateTalk(larkAppId, chatId, senderOpenId, senderUnionId);
1237
+ if (ev.allowed)
1238
+ return ev;
1239
+ return isTrustedTeamBotSender(config.session.dataDir, chatId, senderUnionId)
1240
+ ? { allowed: true, reason: 'teamBot' }
1241
+ : ev;
1242
+ }
1243
+ /**
1244
+ * `botmux ask` 答复者是否可作答(= 该 chat 的 canTalk 门)——ask-broker 注入的
1245
+ * canTalkChecker 的**唯一生产实现**,daemon bootstrap 一行转调它。
1246
+ *
1247
+ * 抽成独立导出函数(而非把分派内联进 daemon setter 闭包)的意义:让**分派谓词本身**
1248
+ * 能被回归测试直接咬住——测试对本函数喂真实 team store 状态断言三组行为,改坏这里的
1249
+ * 分派逻辑(如退回旧 evaluateTalk / 不认 actor.botSender)会立刻红。注意测试只覆盖到
1250
+ * 本函数;daemon setter 的一行转调与 submitCustomReply 的 actor 构造仍靠人工核对
1251
+ * (setter 只有一行,不经测试机械覆盖)。
1252
+ *
1253
+ * - bot 发送方(actor.botSender)→ evaluateBotTalk(含团队拉群那条 chat 维度腿,
1254
+ * 覆盖 sender 事件没带 union_id 的场景),与 dispatcher 外层闸 / quota 复查同源。
1255
+ * - 人 / 无 actor(卡片点击路径)→ evaluateTalk 全模型:actor.senderUnionId 走
1256
+ * teamBot 腿(人路径恒 undefined)、actor.memberUnionId 走 teamMember 腿。
1257
+ * actor 省略时两者都是 undefined → 退化为纯 evaluateTalk(openId, chatType),
1258
+ * 与本次改动前的卡片点击语义完全一致。
1259
+ */
1260
+ export function evaluateAskAnswerTalk(larkAppId, chatId, senderOpenId, chatType, actor) {
1261
+ return actor?.botSender
1262
+ ? evaluateBotTalk(larkAppId, chatId, senderOpenId, actor.senderUnionId).allowed
1263
+ : evaluateTalk(larkAppId, chatId, senderOpenId, actor?.senderUnionId, actor?.memberUnionId, chatType).allowed;
1264
+ }
1154
1265
  export function canOperate(larkAppId, _chatId, senderOpenId, senderUnionId) {
1155
1266
  const bot = getBot(larkAppId);
1156
1267
  // 同部署 cross-ref / isKnownPeerBot 只证明「这是一个可路由的 bot 身份」,仅供
@@ -1181,19 +1292,27 @@ export function canOperate(larkAppId, _chatId, senderOpenId, senderUnionId) {
1181
1292
  * 对话放行腿命中即可)。名单外或未配置 → 与 canOperate 完全等价(现状不变)。
1182
1293
  *
1183
1294
  * 只作用于 daemon.ts 两条路由的 DAEMON_COMMANDS 统一闸;在统一闸之前特判的命令
1184
- * (/vc-auth /term)与 handler 内部自带 owner 闸的命令(/card /insight /land
1295
+ * (/vc-auth /term)与 handler 内部自带 owner 闸的命令(/card /insight)
1185
1296
  * 不受影响——内部闸仍是最终权威(fail-closed)。
1186
1297
  *
1187
1298
  * chatType 省略时 p2pOpen 腿不生效(fail-closed),与 canTalk 语义一致——
1188
1299
  * 私聊路径的调用点必须把 chatType 传进来。
1300
+ *
1301
+ * botSender:降权到 talk 判定这一段,必须与 dispatcher 外层闸 / quota 复查用**同一个**
1302
+ * 谓词。bot 发送方走 evaluateBotTalk(含团队拉群那条 chat 维度腿,覆盖 sender 事件没带
1303
+ * union_id 的情况);否则「团队拉群里外部 bot 能 talk 却执行不了降权到 canTalk 的命令
1304
+ * (如 /status)」——单一 talk 谓词在这道 daemon 命令闸继续分叉。不传(人的路径)→
1305
+ * 原样走 canTalk / evaluateTalk,语义不变。canOperate 那段人/bot 通用,不受影响。
1189
1306
  */
1190
- export function canRunDaemonCommand(larkAppId, chatId, senderOpenId, senderUnionId, cmd, memberUnionId, chatType) {
1307
+ export function canRunDaemonCommand(larkAppId, chatId, senderOpenId, senderUnionId, cmd, memberUnionId, chatType, botSender) {
1191
1308
  if (canOperate(larkAppId, chatId, senderOpenId, senderUnionId))
1192
1309
  return true;
1193
1310
  const list = getBot(larkAppId).config.canTalkDaemonCommands;
1194
1311
  if (!list?.includes(cmd))
1195
1312
  return false;
1196
- return canTalk(larkAppId, chatId, senderOpenId, senderUnionId, memberUnionId, chatType);
1313
+ return botSender
1314
+ ? evaluateBotTalk(larkAppId, chatId, senderOpenId, senderUnionId).allowed
1315
+ : canTalk(larkAppId, chatId, senderOpenId, senderUnionId, memberUnionId, chatType);
1197
1316
  }
1198
1317
  /**
1199
1318
  * 入口 A:无权限者 @bot 时弹授权申请卡(正文 @owner,由 owner 处置)。
@@ -1214,10 +1333,23 @@ async function maybeSendGrantRequestCard(larkAppId, message, chatId, requesterOp
1214
1333
  const observedName = mentionName
1215
1334
  ? undefined
1216
1335
  : listObservedBots(config.session.dataDir, larkAppId, chatId).find(b => b.openId === requesterOpenId)?.name;
1217
- const name = mentionName ?? observedName ?? requesterOpenId;
1336
+ const profileName = mentionName || observedName
1337
+ ? undefined
1338
+ : (await getUserProfile(larkAppId, requesterOpenId).catch(() => null))?.name;
1339
+ const shortRequester = `${requesterOpenId.slice(0, 10)}…${requesterOpenId.slice(-4)}`;
1340
+ const name = mentionName ?? observedName ?? profileName ?? shortRequester;
1218
1341
  // 把原始消息事件挂在 pending 上:授权成功后可重放,用户无需再 @ 一遍。
1219
- const nonce = openPending(larkAppId, chatId, requesterOpenId, getBot(larkAppId).config.messageQuota?.defaultLimit, messageData);
1220
- const card = buildGrantCard({ ownerOpenId: owner, targets: [{ openId: requesterOpenId, name: String(name) }], chatId, nonce, mode: 'request' }, localeForBot(larkAppId));
1342
+ const quota = getBot(larkAppId).config.messageQuota?.defaultLimit ?? DEFAULT_GRANT_QUOTA;
1343
+ const nonce = openPending(larkAppId, chatId, requesterOpenId, quota, messageData, DEFAULT_GRANT_DURATION_MS);
1344
+ const card = buildGrantCard({
1345
+ ownerOpenId: owner,
1346
+ targets: [{ openId: requesterOpenId, name: String(name) }],
1347
+ chatId,
1348
+ nonce,
1349
+ mode: 'request',
1350
+ quota,
1351
+ durationMs: DEFAULT_GRANT_DURATION_MS,
1352
+ }, localeForBot(larkAppId));
1221
1353
  await replyMessage(larkAppId, message.message_id, card, 'interactive')
1222
1354
  .catch(err => {
1223
1355
  // 发卡失败必须撤掉刚开的 pending,否则该发送方被节流压死、owner 永远看不到卡片,
@@ -1244,7 +1376,13 @@ export async function checkGroupMessageAccess(larkAppId, message, chatId, sender
1244
1376
  // No @mention — only allow if sender is the sole human in the group
1245
1377
  // AND this is the only bot in the chat. With multiple bots, require @mention
1246
1378
  // to disambiguate.
1247
- if (isAllowed) {
1379
+ //
1380
+ // 若消息 @ 了别的具体成员(mentionsAnotherMember),群必然不是 1人1bot——
1381
+ // 只有群成员能被 @,多出的那个 @ 本身就是人数变化的证据。上游可能还抱着
1382
+ // 陈旧缓存 {1,1}(刚拉了新 bot 的 TTL 窗口),这会直接跳过人数查询落到
1383
+ // 'ignore',挡住「用户 @ 新 bot、老 bot 跟着回复」。群聊 @ 策略 never/ambient
1384
+ // 在调用方(relax 条款)已先行结算,这里不受影响。
1385
+ if (isAllowed && !mentionsAnotherMember(larkAppId, message)) {
1248
1386
  const { userCount, botCount } = await getGroupStats(larkAppId, chatId);
1249
1387
  logger.debug(`Group user count: ${userCount}, bot count: ${botCount}`);
1250
1388
  if (userCount <= 1 && botCount <= 1) {
@@ -1253,6 +1391,154 @@ export async function checkGroupMessageAccess(larkAppId, message, chatId, sender
1253
1391
  }
1254
1392
  return 'ignore';
1255
1393
  }
1394
+ function listenerRoutingContext(input) {
1395
+ return {
1396
+ data: input.data,
1397
+ ctx: {
1398
+ chatId: input.chatId,
1399
+ messageId: input.messageId,
1400
+ chatType: input.chatType,
1401
+ larkAppId: input.larkAppId,
1402
+ scope: 'thread',
1403
+ anchor: input.messageId,
1404
+ messageListener: input.match,
1405
+ },
1406
+ ownsSession: false,
1407
+ };
1408
+ }
1409
+ const MESSAGE_LISTENER_POLL_INTERVAL_MS = Math.max(5_000, Number(process.env.BOTMUX_MESSAGE_LISTENER_POLL_INTERVAL_MS) || 30_000);
1410
+ const MESSAGE_LISTENER_BACKFILL_WINDOW_MS = Math.max(60_000, Number(process.env.BOTMUX_MESSAGE_LISTENER_BACKFILL_WINDOW_MS) || 30 * 60_000);
1411
+ const MESSAGE_LISTENER_BACKFILL_SCAN_LIMIT = Math.max(1, Number(process.env.BOTMUX_MESSAGE_LISTENER_BACKFILL_SCAN_LIMIT) || 100);
1412
+ const MESSAGE_LISTENER_BACKFILL_PAGE_SIZE = Math.min(50, Math.max(1, Number(process.env.BOTMUX_MESSAGE_LISTENER_BACKFILL_PAGE_SIZE) || 50));
1413
+ function enabledMessageListenerChatIds(bot) {
1414
+ return Object.entries(bot.config.messageListeners ?? {})
1415
+ .filter(([, listener]) => listener?.enabled === true && !!listener.prompt?.trim())
1416
+ .map(([chatId]) => chatId);
1417
+ }
1418
+ function messageCreateTimeMs(message) {
1419
+ const raw = message?.create_time ?? message?.createTime;
1420
+ const value = Number(raw);
1421
+ return Number.isFinite(value) ? value : undefined;
1422
+ }
1423
+ function historyMessageSender(message) {
1424
+ const sender = message?.sender ?? {};
1425
+ const senderId = sender.id ?? sender.open_id ?? sender.user_id ?? sender.app_id
1426
+ ?? message?.sender_id?.open_id ?? message?.sender_id?.user_id ?? message?.sender_id?.app_id;
1427
+ const senderIdType = sender.id_type ?? sender.sender_id_type;
1428
+ const inferredType = sender.sender_type
1429
+ ?? message?.sender_type
1430
+ ?? (senderIdType === 'app_id' ? 'app' : undefined);
1431
+ return {
1432
+ senderOpenId: typeof senderId === 'string' ? senderId : undefined,
1433
+ senderTypeRaw: typeof inferredType === 'string' ? inferredType : undefined,
1434
+ senderIdType: typeof senderIdType === 'string' ? senderIdType : undefined,
1435
+ };
1436
+ }
1437
+ function larkReceiveEventFromHistoryMessage(message, chatId) {
1438
+ const { senderOpenId, senderTypeRaw, senderIdType } = historyMessageSender(message);
1439
+ const isAppId = senderIdType === 'app_id' || senderTypeRaw === 'app' || senderTypeRaw === 'bot';
1440
+ return {
1441
+ message: {
1442
+ ...message,
1443
+ message_type: message?.message_type ?? message?.msg_type,
1444
+ chat_id: message?.chat_id ?? chatId,
1445
+ chat_type: message?.chat_type ?? 'group',
1446
+ },
1447
+ sender: {
1448
+ sender_type: senderTypeRaw ?? (isAppId ? 'app' : 'user'),
1449
+ sender_id: isAppId
1450
+ ? { app_id: senderOpenId }
1451
+ : { open_id: senderOpenId },
1452
+ },
1453
+ };
1454
+ }
1455
+ async function dispatchHumanMessageViaHandlers(larkAppId, handlers, payload) {
1456
+ await serializeByAnchor(payload.ctx.anchor, () => {
1457
+ const ownsSession = handlers.isSessionOwner?.(payload.ctx.anchor, larkAppId) ?? payload.ownsSession;
1458
+ return ownsSession
1459
+ ? handlers.handleThreadReply(payload.data, payload.ctx)
1460
+ : handlers.handleNewTopic(payload.data, payload.ctx);
1461
+ });
1462
+ }
1463
+ async function dispatchPolledMessageListenerMatch(input) {
1464
+ if (!claimMessageOnce(input.larkAppId, input.messageId)) {
1465
+ logger.debug(`[message-listener:${input.larkAppId}] polled duplicate ignored msg=${input.messageId.substring(0, 12)}`);
1466
+ return;
1467
+ }
1468
+ logger.info(`[message-listener:${input.larkAppId}] matched polled chat=${input.chatId.substring(0, 12)} ` +
1469
+ `msg=${input.messageId.substring(0, 12)} sender=${input.match.senderOpenId?.substring(0, 12) ?? '-'}`);
1470
+ await dispatchHumanMessageViaHandlers(input.larkAppId, input.handlers, listenerRoutingContext({
1471
+ data: input.data,
1472
+ match: input.match,
1473
+ chatId: input.chatId,
1474
+ messageId: input.messageId,
1475
+ chatType: 'group',
1476
+ larkAppId: input.larkAppId,
1477
+ }));
1478
+ }
1479
+ async function pollMessageListenersOnce(larkAppId, handlers, now = Date.now()) {
1480
+ const bot = getBot(larkAppId);
1481
+ const chatIds = enabledMessageListenerChatIds(bot);
1482
+ if (chatIds.length === 0)
1483
+ return;
1484
+ const cutoff = now - MESSAGE_LISTENER_BACKFILL_WINDOW_MS;
1485
+ await ensureBotOpenId(larkAppId).catch(() => { });
1486
+ for (const chatId of chatIds) {
1487
+ let messages;
1488
+ try {
1489
+ messages = await listChatMessagesUntil(larkAppId, chatId, {
1490
+ pageSize: MESSAGE_LISTENER_BACKFILL_PAGE_SIZE,
1491
+ stopAfter: (message, seenCount) => {
1492
+ const createdAt = messageCreateTimeMs(message);
1493
+ return seenCount >= MESSAGE_LISTENER_BACKFILL_SCAN_LIMIT ||
1494
+ (Number.isFinite(createdAt) && createdAt < cutoff);
1495
+ },
1496
+ });
1497
+ }
1498
+ catch (err) {
1499
+ logger.warn(`[message-listener:${larkAppId}] failed to poll chat=${chatId.substring(0, 12)}: ${err instanceof Error ? err.message : String(err)}`);
1500
+ continue;
1501
+ }
1502
+ // Build the app_id→open_id map once per chat (shared with the dashboard
1503
+ // preview leg) so bot senders (reported by app_id in history) match sender
1504
+ // filters keyed on open_id, and unprovable bots fail closed on exclusion.
1505
+ const candidateBotAppIds = collectListenerBotAppIds(messages, historyMessageSender);
1506
+ const appIdToOpenId = await buildListenerBotAppIdToOpenId(larkAppId, chatId, candidateBotAppIds);
1507
+ for (const message of messages) {
1508
+ const messageId = String(message?.message_id ?? '');
1509
+ if (!messageId)
1510
+ continue;
1511
+ const createdAt = messageCreateTimeMs(message);
1512
+ if (Number.isFinite(createdAt) && createdAt < cutoff)
1513
+ continue;
1514
+ const data = larkReceiveEventFromHistoryMessage(message, chatId);
1515
+ const rawSender = historyMessageSender(message);
1516
+ const resolved = resolveListenerSenderIdentity(rawSender, appIdToOpenId);
1517
+ const match = evaluateMessageListener({
1518
+ bot,
1519
+ chatId,
1520
+ message: data.message,
1521
+ senderOpenId: resolved.senderOpenId,
1522
+ senderTypeRaw: rawSender.senderTypeRaw,
1523
+ senderIdentityUnverified: resolved.identityUnverified,
1524
+ explicitlyMentionedThisBot: isBotMentioned(larkAppId, data.message, resolved.senderOpenId),
1525
+ });
1526
+ if (!match)
1527
+ continue;
1528
+ await dispatchPolledMessageListenerMatch({
1529
+ larkAppId,
1530
+ handlers,
1531
+ data,
1532
+ match,
1533
+ chatId,
1534
+ messageId,
1535
+ }).catch(err => logger.error(`Error handling polled message listener event: ${err}`));
1536
+ }
1537
+ }
1538
+ }
1539
+ export async function __pollMessageListenersOnceForTest(larkAppId, handlers, now = Date.now()) {
1540
+ await pollMessageListenersOnce(larkAppId, handlers, now);
1541
+ }
1256
1542
  function usesForwardFollowupDelay(mentionMode) {
1257
1543
  return mentionMode === 'never' || mentionMode === 'ambient';
1258
1544
  }
@@ -1756,12 +2042,7 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
1756
2042
  resolveSessionsReady();
1757
2043
  sessionsReadyBarriers.set(larkAppId, { promise: sessionsReady, resolve: resolveSessionsReady });
1758
2044
  const dispatchHumanMessage = async (payload) => {
1759
- await serializeByAnchor(payload.ctx.anchor, () => {
1760
- const ownsSession = handlers.isSessionOwner?.(payload.ctx.anchor, larkAppId) ?? payload.ownsSession;
1761
- return ownsSession
1762
- ? handlers.handleThreadReply(payload.data, payload.ctx)
1763
- : handlers.handleNewTopic(payload.data, payload.ctx);
1764
- });
2045
+ await dispatchHumanMessageViaHandlers(larkAppId, handlers, payload);
1765
2046
  };
1766
2047
  const dispatchPersistedForwardFollowup = async (seedMessageId, payload) => {
1767
2048
  // Wait for activeSessions to be rehydrated before checking ownership, so a
@@ -1860,7 +2141,12 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
1860
2141
  const senderType = sender?.sender_type;
1861
2142
  const isBotSenderType = senderType === 'app' || senderType === 'bot';
1862
2143
  if (isBotSenderType) {
1863
- const senderOpenId = sender.sender_id?.open_id;
2144
+ const senderOpenId = sender.sender_id?.open_id ?? sender.sender_id?.app_id;
2145
+ // When Feishu gave us only an app_id (no open_id), this bot sender's
2146
+ // identity is not in the open_id domain the listener filters use, so
2147
+ // the matcher must fail closed on open_id-based exclusion. Realtime
2148
+ // events normally carry open_id; the app_id fallback is the rare case.
2149
+ const senderIdentityUnverified = !sender.sender_id?.open_id && !!sender.sender_id?.app_id;
1864
2150
  const isSelfMessage = senderOpenId === getBot(larkAppId).botOpenId;
1865
2151
  // Self messages: learn our OWN union_id from the echo first (the only
1866
2152
  // reliable source — see bot-union-ids-store; reported to the platform
@@ -1919,6 +2205,30 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
1919
2205
  logger.debug(`[${larkAppId}] hall bot message swallowed after learning (chat=${chatId.substring(0, 12)})`);
1920
2206
  return;
1921
2207
  }
2208
+ const botMessageListener = chatType === 'group'
2209
+ ? evaluateMessageListener({
2210
+ bot: getBot(larkAppId),
2211
+ chatId,
2212
+ message,
2213
+ senderOpenId,
2214
+ senderTypeRaw: sender?.sender_type,
2215
+ senderIdentityUnverified,
2216
+ explicitlyMentionedThisBot: isBotMentioned(larkAppId, message, senderOpenId),
2217
+ })
2218
+ : undefined;
2219
+ if (botMessageListener) {
2220
+ logger.info(`[message-listener:${larkAppId}] matched bot-sender chat=${chatId.substring(0, 12)} ` +
2221
+ `msg=${messageId.substring(0, 12)} sender=${senderOpenId?.substring(0, 12) ?? '-'}`);
2222
+ await dispatchHumanMessage(listenerRoutingContext({
2223
+ data,
2224
+ match: botMessageListener,
2225
+ chatId,
2226
+ messageId,
2227
+ chatType,
2228
+ larkAppId,
2229
+ })).catch(err => logger.error(`Error handling bot message listener event: ${err}`));
2230
+ return;
2231
+ }
1922
2232
  // Foreign bot: only route on @mention of us.
1923
2233
  if (!isBotMentioned(larkAppId, message, undefined))
1924
2234
  return;
@@ -1938,13 +2248,13 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
1938
2248
  const ownsThreadSession = ctx.scope === 'thread'
1939
2249
  ? (handlers.isSessionOwner?.(ctx.anchor, larkAppId) ?? false)
1940
2250
  : false;
1941
- const canFoldForeignBotThread = findOncallChat(larkAppId, chatId)
1942
- || isKnownPeerBot(config.session.dataDir, larkAppId, senderOpenId)
1943
- || isTrustedTeamBotSender(config.session.dataDir, chatId, senderUnionId)
1944
- || hasChatGrant(larkAppId, chatId, senderOpenId)
1945
- || hasGlobalGrant(larkAppId, senderOpenId);
2251
+ // 这个 bot 能不能在本群跟我们说话 —— 判定一次,fold 与下面的 gate 共用同一个
2252
+ // 结论(二者之间只有 fold / shared-seed 的路由计算,不改任何授权状态)。
2253
+ // 曾经这里是两条手抄的 OR 链,靠人肉保持同步;漏一处就是「能路由但不能 fold」
2254
+ // 或「fold 了却弹卡」的二次分裂。
2255
+ const botTalk = evaluateBotTalk(larkAppId, chatId, senderOpenId, senderUnionId);
1946
2256
  let replyRootId = await maybeFoldMentionedRegularGroupThreadToChat({
1947
- larkAppId, chatId, chatType, message, routing: ctx, forceTopicApplied: forcedTopic, mentionedThisBot: !!canFoldForeignBotThread, ownsThreadSession,
2257
+ larkAppId, chatId, chatType, message, routing: ctx, forceTopicApplied: forcedTopic, mentionedThisBot: botTalk.allowed, ownsThreadSession,
1948
2258
  });
1949
2259
  if (!replyRootId) {
1950
2260
  replyRootId = await maybeApplySharedTopicSeed({
@@ -1956,71 +2266,44 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
1956
2266
  // threads. Previously native topic replies skipped this outer gate and
1957
2267
  // were silently dropped by daemon's second evaluateTalk check, so the
1958
2268
  // owner never saw a grant card. Owning an existing session is not an
1959
- // authorization source; a revoked/unknown bot must still pass one of
1960
- // the explicit trust or grant legs below.
2269
+ // authorization source; a revoked/unknown bot must still pass
2270
+ // evaluateBotTalk (the single talk predicate see its doc for the two
2271
+ // deliberate human/bot deltas) or prove itself a sibling below.
1961
2272
  //
1962
- // 注意 isKnownPeerBot 查的是 cross-ref(bot-openids-<appId>.json),它只
1963
- // 收录 bots-info.json 里有名字的 bot,即本机 daemon 自己配置的 bot
1964
- // (getAllBots)。"别人的 bot" 永远进不了这个 cross-ref,所以 isKnownPeerBot
1965
- // 对外部 bot 恒为 false——这跟 /introduce 是两套独立存储:/introduce 写的是
1966
- // observed-bots-store,只负责让发送方"发现并能 @ 到"对方,过不了这道接收闸。
2273
+ // 这里**只判 evaluateBotTalk**。以前是手抄的 OR 链,每加一条 talk 源就漏一次:
2274
+ // oncall 漏过、开放模式漏过、allowedChatGroups 又漏过(整群 /grant 后人通 bot 卡)。
2275
+ // 要加新的 talk 放行源,改 evaluateTalk(人/bot 同时生效),不要在这里加腿;
2276
+ // test/bot-talk-parity.test.ts 会盯着这条约束。
1967
2277
  //
1968
- // Oncall 群是当前接收 bot 显式部署的协作工作区,canTalk 已对任何成员
1969
- // (含真人)放行;这里对 bot 同等放行,跳过 cross-ref vetting。否则本
1970
- // bot 已绑定的 oncall 群里外部 bot 互相 @ 会被静默丢弃、只有真人能拉起会话。
1971
- // 注意 oncall talk access bot-scoped:一个 bot 的 /oncall bind 不会放开
1972
- // sibling bot talk 权限;如果 sibling bot 也要开放,需要自己绑定同一个 chat。
1973
- //
1974
- // owner 还可用 `/grant @bot` 把外部 bot 加进本群 chatGrants(与真人 /grant
1975
- // 同一存储、同一 per-chat 语义)。命中 chatGrants bot 即便不在 cross-ref,
1976
- // 也与已注册 peer 同等放行——这是「授权外部 bot 在本群协作」的入口。
1977
- // 全局授权(globalGrants)同理:命中即在任意群放行,是上面的全局版。
1978
- //
1979
- // 团队 bot /grant:isTrustedTeamBotSender 命中(sender 的租户稳定
1980
- // union_id 已学进 team-bots,或本群就是团队拉群组建的协作群)即放行——
1981
- // 与「同部署兄弟 bot」(isKnownPeerBot) 对等,但覆盖跨部署的团队 peer。
1982
- // 这正是「加入团队即免 introduce/grant」的接收闸:身份只认 union_id /
1983
- // 团队群成员,绝不认自报名字(防同名 bot 冒名蹭权)。
1984
- //
1985
- // 开放模式(未配任何 allowlist:allowedUsers / allowedChatGroups /
1986
- // globalGrants 全空)与人侧 evaluateTalk `reason:'open'`(1011 行)对齐:
1987
- // 「谁都能触发」本应人、bot 同权。过去这道 gate 漏了这一腿,导致开放模式下
1988
- // 外部 bot @ 仍被丢弃,必须真人先 @ 一次建 session 才救活。
1989
- // 补上 hasConfiguredAllowlist 短路后两条路径统一:一旦配了任一 allowlist,
1990
- // 立刻恢复「限制态」设闸,安全边界不变。
1991
- if (!findOncallChat(larkAppId, chatId)
1992
- && hasConfiguredAllowlist(getBot(larkAppId))) {
1993
- if (!isKnownPeerBot(config.session.dataDir, larkAppId, senderOpenId)
1994
- && !isTrustedTeamBotSender(config.session.dataDir, chatId, senderUnionId)
1995
- && !hasChatGrant(larkAppId, chatId, senderOpenId)
1996
- && !hasGlobalGrant(larkAppId, senderOpenId)) {
1997
- // Cold-start self-heal: the cross-ref (bot-openids-<appId>.json) is
1998
- // learned lazily from observed mentions[], so the FIRST bot→bot
1999
- // direct @ from a same-deployment sibling can arrive before the
2000
- // receiver has learned that sibling's receiver-scoped open_id
2001
- // (Lark open_id is per-app). That raced a real sibling into this
2002
- // "unknown external bot" branch and mis-fired a /grant card
2003
- // (regression from ec146a49). Before deciding unknown, confirm the
2004
- // sender against the group's LIVE `/members/bots` roster: accept
2005
- // only when it binds to exactly one locally-configured sibling of
2006
- // the same unique name that independently confirms is_in_chat.
2007
- // Fails closed to the grant card on any API error / ambiguity /
2008
- // genuine external bot — never a name-only shortcut.
2009
- const sibling = await resolveSiblingBotBySenderOpenId(larkAppId, chatId, senderOpenId)
2010
- .catch((err) => ({ ok: false, reason: `resolve_threw: ${err?.message ?? String(err)}` }));
2011
- if (sibling.ok) {
2012
- // Persist the newly-proven mapping so subsequent @s from this
2013
- // sibling hit isKnownPeerBot directly (no live API roundtrip).
2014
- updateBotOpenIdCrossRef(config.session.dataDir, larkAppId, [
2015
- { name: sibling.botName, id: { open_id: sibling.senderOpenId } },
2016
- ]);
2017
- logger.info(`Lazy sibling cross-ref backfill: ${sibling.botName} → ${senderOpenId?.substring(0, 12)} (was cold; skipping /grant)`);
2018
- }
2019
- else {
2020
- logger.info(`Foreign bot @mention not a known sibling (${sibling.reason}); sending grant request card`);
2021
- await maybeSendGrantRequestCard(larkAppId, message, chatId, senderOpenId, data);
2022
- return;
2023
- }
2278
+ // 唯一留在闸门里的 bot 专属逻辑是下面的 cross-ref 冷启动自愈:它不是一条
2279
+ // 授权来源,而是「同部署兄弟 bot 的身份还没学到」这个**识别**问题的补救。
2280
+ if (!botTalk.allowed) {
2281
+ // Cold-start self-heal: the cross-ref (bot-openids-<appId>.json) is
2282
+ // learned lazily from observed mentions[], so the FIRST bot→bot
2283
+ // direct @ from a same-deployment sibling can arrive before the
2284
+ // receiver has learned that sibling's receiver-scoped open_id
2285
+ // (Lark open_id is per-app). That raced a real sibling into this
2286
+ // "unknown external bot" branch and mis-fired a /grant card
2287
+ // (regression from ec146a49). Before deciding unknown, confirm the
2288
+ // sender against the group's LIVE `/members/bots` roster: accept
2289
+ // only when it binds to exactly one locally-configured sibling of
2290
+ // the same unique name that independently confirms is_in_chat.
2291
+ // Fails closed to the grant card on any API error / ambiguity /
2292
+ // genuine external bot — never a name-only shortcut.
2293
+ const sibling = await resolveSiblingBotBySenderOpenId(larkAppId, chatId, senderOpenId)
2294
+ .catch((err) => ({ ok: false, reason: `resolve_threw: ${err?.message ?? String(err)}` }));
2295
+ if (sibling.ok) {
2296
+ // Persist the newly-proven mapping so subsequent @s from this
2297
+ // sibling hit isKnownPeerBot directly (no live API roundtrip).
2298
+ updateBotOpenIdCrossRef(config.session.dataDir, larkAppId, [
2299
+ { name: sibling.botName, id: { open_id: sibling.senderOpenId } },
2300
+ ]);
2301
+ logger.info(`Lazy sibling cross-ref backfill: ${sibling.botName} → ${senderOpenId?.substring(0, 12)} (was cold; skipping /grant)`);
2302
+ }
2303
+ else {
2304
+ logger.info(`Foreign bot @mention not a known sibling (${sibling.reason}); sending grant request card`);
2305
+ await maybeSendGrantRequestCard(larkAppId, message, chatId, senderOpenId, data);
2306
+ return;
2024
2307
  }
2025
2308
  }
2026
2309
  logger.info(`Bot-to-bot @mention detected (scope=${ctx.scope}): routing to handleThreadReply`);
@@ -2058,6 +2341,11 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
2058
2341
  if (await tryHandleGrantCommand(larkAppId, message, senderOpenId)) {
2059
2342
  return;
2060
2343
  }
2344
+ // /invite — 把群外 bot 拉进本群的元命令。与 /grant 同款拦截模型(仅 owner,
2345
+ // 需明确 @ 本 bot),防止进路由/spawn,也防多 bot 重复执行拉人。
2346
+ if (await tryHandleInviteCommand(larkAppId, message, senderOpenId)) {
2347
+ return;
2348
+ }
2061
2349
  logger.debug('Received message:', message);
2062
2350
  // Diagnostic: record the Lark quote-bubble UI quirk where root_id
2063
2351
  // appears without thread_id. decideRouting now treats this as
@@ -2087,6 +2375,24 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
2087
2375
  replyRootId = message.root_id;
2088
2376
  }
2089
2377
  const explicitlyMentionedThisBot = isBotMentioned(larkAppId, message, senderOpenId);
2378
+ const messageListener = chatType === 'group'
2379
+ ? evaluateMessageListener({
2380
+ bot: getBot(larkAppId),
2381
+ chatId,
2382
+ message,
2383
+ senderOpenId,
2384
+ senderTypeRaw: sender?.sender_type,
2385
+ explicitlyMentionedThisBot,
2386
+ })
2387
+ : undefined;
2388
+ if (messageListener) {
2389
+ routing.scope = 'thread';
2390
+ routing.anchor = messageId;
2391
+ routingSource = 'topic-chat';
2392
+ replyRootId = undefined;
2393
+ logger.info(`[message-listener:${larkAppId}] matched chat=${chatId.substring(0, 12)} ` +
2394
+ `msg=${messageId.substring(0, 12)} sender=${senderOpenId?.substring(0, 12) ?? '-'}`);
2395
+ }
2090
2396
  // Cheap in-memory gate FIRST: skip the getChatMode roundtrip and the
2091
2397
  // per-chat toggle disk read entirely for bots that never configured a
2092
2398
  // substitute target (the overwhelming majority on the hot path).
@@ -2103,6 +2409,21 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
2103
2409
  substituteChatMode = chatMode;
2104
2410
  }
2105
2411
  }
2412
+ // 黑名单硬静默:命中黑名单的群里,一条「本该触发替身」的消息(@ 到了配置的
2413
+ // 替身对象、但没有直接 @ 本 bot)必须当作没读到——直接 return,不只是清 trigger。
2414
+ // 只清 trigger 不够:消息会继续 fall-through 到通用群消息门,若 bot 在该群有活跃
2415
+ // 会话 / 是 solo 群 / mentionMode 放开,仍会被喂进去并弹卡片(用户实测现象)。
2416
+ // 直接 @ 本 bot(explicitlyMentionedThisBot)不受影响:黑名单只静音替身代答,
2417
+ // 不静音「直接找 bot 问问题」。/substitute 命令已在上方 command 处理器拦截。
2418
+ if (substituteCfg?.enabled === true
2419
+ && chatType === 'group'
2420
+ && !explicitlyMentionedThisBot
2421
+ && isSubstituteExcludedChat(substituteCfg, chatId)
2422
+ && resolveSubstituteTrigger(larkAppId, message)) {
2423
+ logger.info(`[substitute:${larkAppId}] excluded chat — dropping @target message ` +
2424
+ `msg=${messageId.substring(0, 12)} chat=${chatId.substring(0, 12)}`);
2425
+ return;
2426
+ }
2106
2427
  let substituteTrigger = substituteChatMode
2107
2428
  ? resolveSubstituteTrigger(larkAppId, message)
2108
2429
  : undefined;
@@ -2187,7 +2508,8 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
2187
2508
  // can't be cache-stale) or when chatType is p2p (DMs always thread).
2188
2509
  // Runs BEFORE /t override so a `@bot /t …` in a now-flat 普通群 still
2189
2510
  // gets the explicit topic seed it asked for.
2190
- if (routing.scope === 'thread' &&
2511
+ if (!messageListener &&
2512
+ routing.scope === 'thread' &&
2191
2513
  routing.anchor === messageId &&
2192
2514
  !message.thread_id &&
2193
2515
  chatType === 'group') {
@@ -2360,9 +2682,16 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
2360
2682
  // !ownsSession (group) → require @mention + allowlist
2361
2683
  // p2p → allowlist only
2362
2684
  if (chatType === 'group') {
2685
+ const mentionMode = resolveGroupMentionMode(larkAppId);
2686
+ // 消息里 @ 了别的具体成员,就已经证明群不是 1人1bot(只有群成员能被 @)——
2687
+ // 此刻末条 solo 放行必然不成立,而 stats 只被末条消费,直接跳过这次(可能
2688
+ // 昂贵的)人数查询。这在「刚拉了新 bot、用户 @ 新 bot」窗口里尤为重要:
2689
+ // 拦截老 bot 的同时不再为它反复刷新陈旧缓存。
2690
+ const mentionsOther = mentionsAnotherMember(larkAppId, message);
2363
2691
  let stats = null;
2364
- if (ownsSession && !replyRootId)
2692
+ if (ownsSession && !replyRootId && !mentionsOther && mentionMode !== 'never') {
2365
2693
  stats = await getGroupStats(larkAppId, chatId);
2694
+ }
2366
2695
  // replyRootId means this turn has already been explicitly addressed
2367
2696
  // to the bot by shared-topic logic (possibly from inside an existing
2368
2697
  // Lark thread). Do not re-run the generic group @ gate, which would
@@ -2387,8 +2716,8 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
2387
2716
  // treat it as a hand-off and stay quiet.
2388
2717
  // Both gated on isAllowed so restricted groups still only react to
2389
2718
  // permitted senders. (The shared fold-back's replyRootId is already
2390
- // handled by the first clause.)
2391
- const mentionMode = resolveGroupMentionMode(larkAppId);
2719
+ // handled by the first clause. `mentionMode` 已在块首解析,用于 stats
2720
+ // 惰性获取。)
2392
2721
  // 话题群 owned-topic 免@续话不再无条件放行(#336 引入的默认行为回归:
2393
2722
  // 多人群里旁人不 @ 也会触发 bot)。现在与普通群共用同一套「群聊 @ 策略」:
2394
2723
  // 默认 'always' 在多人群里必须 @,想要话题内免@续话就把 mentionMode 配成
@@ -2397,12 +2726,17 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
2397
2726
  // 注:pairedForwardSeed 仅在 never/ambient 模式下产生,且 ambient redirect
2398
2727
  // 已在配对前排除,故 isAllowed=true 时下方 never/ambient 条款必然放行;
2399
2728
  // 不在此单独加 clause,以免 isAllowed=false 时绕过权限检查。
2729
+ // 末条 solo 放行另有 `!mentionsOther` 守卫:消息 @ 了别的具体成员时,
2730
+ // 群必然不是 1人1bot(只有群成员能被 @),且是指给别人的——拦住「刚拉新 bot、
2731
+ // 缓存仍是陈旧 {1,1} 时老 bot 跟着回复」的误放行。never 条款在前已短路,
2732
+ // 故该守卫不影响「群聊 @ 策略」配置的 never 语义。
2400
2733
  const relax = (!!replyRootId && isAllowed)
2401
2734
  || (!!substituteTrigger && isAllowed)
2735
+ || !!messageListener
2402
2736
  || (isAllowed && mentionMode === 'never')
2403
- || (isAllowed && mentionMode === 'ambient' && !mentionsAnotherMember(larkAppId, message))
2404
- || (isAllowed && mentionMode === 'topic' && ownsSession && !!message.thread_id && !mentionsAnotherMember(larkAppId, message))
2405
- || (ownsSession && isAllowed && !!stats && stats.userCount <= 1 && stats.botCount <= 1);
2737
+ || (isAllowed && mentionMode === 'ambient' && !mentionsOther)
2738
+ || (isAllowed && mentionMode === 'topic' && ownsSession && !!message.thread_id && !mentionsOther)
2739
+ || (ownsSession && isAllowed && !!stats && !mentionsOther && stats.userCount <= 1 && stats.botCount <= 1);
2406
2740
  if (!relax) {
2407
2741
  const access = await checkGroupMessageAccess(larkAppId, message, chatId, senderOpenId, humanSenderUnionId);
2408
2742
  if (access === 'not_allowed') {
@@ -2459,6 +2793,7 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
2459
2793
  ? { name: 'summary-command', chatKind: summaryCommandMatch.chatKind }
2460
2794
  : undefined,
2461
2795
  substituteTrigger,
2796
+ messageListener,
2462
2797
  forwardSeedData: pairedForwardSeed?.payload.data,
2463
2798
  };
2464
2799
  if (explicitlyMentionedThisBot) {
@@ -2552,6 +2887,15 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
2552
2887
  const chatIdForKey = data?.chat_id;
2553
2888
  const operatorForKey = data?.operator_id?.open_id;
2554
2889
  const eventKey = `im.chat.member.bot.added_v1:${larkAppId}:${eventIdForKey(data) ?? `${chatIdForKey ?? 'unknown'}:${operatorForKey ?? 'unknown'}`}`;
2890
+ // 飞书只把 bot.added 推给「进群的那个 bot 自己的 app」(官方文档语義,
2891
+ // codex 复审证实),且生产是 PM2「一 bot 一 daemon 进程」(daemon.ts
2892
+ // "Load the assigned bot (one daemon per bot)")——同群其他 bot 的缓存在
2893
+ // 别的进程,这里够不到,只能清自己的 key。覆盖增量其实很小:新添进群
2894
+ // 时本 bot 此前若恰有该群条目(曾被移出又拉回),避免带着上轮陈旧数放行。
2895
+ // 「拉了别的 bot 进群→存量 bot 陈旧」方向无事件信号,靠 relax 末条款的
2896
+ // mentionsAnotherMember 守卫 + TTL 兜底。
2897
+ if (chatIdForKey)
2898
+ invalidateChatStats(larkAppId, chatIdForKey);
2555
2899
  scheduleAckSafeEvent(eventKey, async () => {
2556
2900
  try {
2557
2901
  const chatId = data?.chat_id;
@@ -2566,6 +2910,26 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
2566
2910
  }
2567
2911
  }, 'bot-added event');
2568
2912
  },
2913
+ // bot.deleted 同样只推给被移出的 bot 自己的 app——清自己的 key 即可(语义
2914
+ // 同 bot.added:进程边界上够不到兄弟 daemon;「别的 bot 离群→存量 bot
2915
+ // 陈旧」靠①守卫+TTL)。user.added/deleted_v1 推给群内已订阅的所有 bot
2916
+ // app,每个 bot 自己的 WS 都收到,各自清自己那条。纯本地 map 删除,同步
2917
+ // 处理、即时 ACK,不进 work 队列;去重也不必要——失效是幂等的。
2918
+ 'im.chat.member.bot.deleted_v1': (data) => {
2919
+ const chatId = data?.chat_id;
2920
+ if (chatId)
2921
+ invalidateChatStats(larkAppId, chatId);
2922
+ },
2923
+ 'im.chat.member.user.added_v1': (data) => {
2924
+ const chatId = data?.chat_id;
2925
+ if (chatId)
2926
+ invalidateChatStats(larkAppId, chatId);
2927
+ },
2928
+ 'im.chat.member.user.deleted_v1': (data) => {
2929
+ const chatId = data?.chat_id;
2930
+ if (chatId)
2931
+ invalidateChatStats(larkAppId, chatId);
2932
+ },
2569
2933
  // 文档评论入口(/watch-comment / /subscribe-lark-doc)。notice 事件主要覆盖 @Bot
2570
2934
  // 通知;普通评论由 daemon 应用身份轮询补齐,不依赖逐文件 subscribe API。
2571
2935
  'drive.notice.comment_add_v1': (data) => handleCommentEventAckSafe(data, larkAppId, handlers),
@@ -2677,6 +3041,29 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
2677
3041
  });
2678
3042
  wsClient.start({ eventDispatcher });
2679
3043
  logger.info('Daemon WSClient started');
3044
+ let listenerPollInFlight = false;
3045
+ const listenerPollTimer = setInterval(() => {
3046
+ if (listenerPollInFlight)
3047
+ return;
3048
+ listenerPollInFlight = true;
3049
+ void pollMessageListenersOnce(larkAppId, handlers)
3050
+ .catch(err => logger.error(`[message-listener:${larkAppId}] poll failed: ${err instanceof Error ? err.message : String(err)}`))
3051
+ .finally(() => { listenerPollInFlight = false; });
3052
+ }, MESSAGE_LISTENER_POLL_INTERVAL_MS);
3053
+ listenerPollTimer.unref();
3054
+ const hasListenerBackfill = enabledMessageListenerChatIds(getBot(larkAppId)).length > 0;
3055
+ if (hasListenerBackfill) {
3056
+ setTimeout(() => {
3057
+ if (listenerPollInFlight)
3058
+ return;
3059
+ listenerPollInFlight = true;
3060
+ void pollMessageListenersOnce(larkAppId, handlers)
3061
+ .catch(err => logger.error(`[message-listener:${larkAppId}] initial poll failed: ${err instanceof Error ? err.message : String(err)}`))
3062
+ .finally(() => { listenerPollInFlight = false; });
3063
+ }, 2_000).unref();
3064
+ logger.info(`[message-listener:${larkAppId}] polling backfill enabled interval=${MESSAGE_LISTENER_POLL_INTERVAL_MS}ms ` +
3065
+ `window=${MESSAGE_LISTENER_BACKFILL_WINDOW_MS}ms chats=${enabledMessageListenerChatIds(getBot(larkAppId)).length}`);
3066
+ }
2680
3067
  // ② SDK 重连耗尽后停在 terminalError(getConnectionStatus().state === 'failed')并
2681
3068
  // 永久放弃。每分钟探测一次,发现已放弃就 start() 重新发起一轮全新握手 —— start()
2682
3069
  // 会清掉 terminalError 并重新 pullConnectConfig + connect,无需手动重启 daemon。