botmux 3.4.3 → 3.5.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 (350) hide show
  1. package/README.en.md +10 -7
  2. package/README.md +11 -6
  3. package/dist/adapters/backend/herdr-backend.d.ts +43 -6
  4. package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
  5. package/dist/adapters/backend/herdr-backend.js +362 -28
  6. package/dist/adapters/backend/herdr-backend.js.map +1 -1
  7. package/dist/adapters/backend/reproduce-command.d.ts +39 -0
  8. package/dist/adapters/backend/reproduce-command.d.ts.map +1 -0
  9. package/dist/adapters/backend/reproduce-command.js +63 -0
  10. package/dist/adapters/backend/reproduce-command.js.map +1 -0
  11. package/dist/adapters/backend/session-backend-selector.d.ts +10 -1
  12. package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -1
  13. package/dist/adapters/backend/session-backend-selector.js +73 -4
  14. package/dist/adapters/backend/session-backend-selector.js.map +1 -1
  15. package/dist/adapters/backend/types.d.ts +16 -0
  16. package/dist/adapters/backend/types.d.ts.map +1 -1
  17. package/dist/adapters/backend/types.js.map +1 -1
  18. package/dist/adapters/cli/pi-initial-prompt.d.ts.map +1 -1
  19. package/dist/adapters/cli/pi-initial-prompt.js +5 -1
  20. package/dist/adapters/cli/pi-initial-prompt.js.map +1 -1
  21. package/dist/adapters/cli/traex.d.ts.map +1 -1
  22. package/dist/adapters/cli/traex.js +5 -0
  23. package/dist/adapters/cli/traex.js.map +1 -1
  24. package/dist/adapters/cli/types.d.ts +3 -3
  25. package/dist/adapters/cli/types.d.ts.map +1 -1
  26. package/dist/adapters/hook-installer.d.ts +7 -0
  27. package/dist/adapters/hook-installer.d.ts.map +1 -1
  28. package/dist/adapters/hook-installer.js +56 -0
  29. package/dist/adapters/hook-installer.js.map +1 -1
  30. package/dist/bot-registry.d.ts +13 -0
  31. package/dist/bot-registry.d.ts.map +1 -1
  32. package/dist/bot-registry.js +118 -2
  33. package/dist/bot-registry.js.map +1 -1
  34. package/dist/cli/capabilities.d.ts +24 -0
  35. package/dist/cli/capabilities.d.ts.map +1 -0
  36. package/dist/cli/capabilities.js +25 -0
  37. package/dist/cli/capabilities.js.map +1 -0
  38. package/dist/cli/create-group-resolver.d.ts +24 -0
  39. package/dist/cli/create-group-resolver.d.ts.map +1 -1
  40. package/dist/cli/create-group-resolver.js +23 -0
  41. package/dist/cli/create-group-resolver.js.map +1 -1
  42. package/dist/cli/daemon-lifecycle-env.d.ts +5 -7
  43. package/dist/cli/daemon-lifecycle-env.d.ts.map +1 -1
  44. package/dist/cli/daemon-lifecycle-env.js +13 -7
  45. package/dist/cli/daemon-lifecycle-env.js.map +1 -1
  46. package/dist/cli/exact-chat-grant-client.d.ts +48 -0
  47. package/dist/cli/exact-chat-grant-client.d.ts.map +1 -0
  48. package/dist/cli/exact-chat-grant-client.js +108 -0
  49. package/dist/cli/exact-chat-grant-client.js.map +1 -0
  50. package/dist/cli/exact-chat-grant.d.ts +18 -0
  51. package/dist/cli/exact-chat-grant.d.ts.map +1 -0
  52. package/dist/cli/exact-chat-grant.js +97 -0
  53. package/dist/cli/exact-chat-grant.js.map +1 -0
  54. package/dist/cli.d.ts +10 -1
  55. package/dist/cli.d.ts.map +1 -1
  56. package/dist/cli.js +770 -126
  57. package/dist/cli.js.map +1 -1
  58. package/dist/codex-rpc-engine.d.ts +26 -0
  59. package/dist/codex-rpc-engine.d.ts.map +1 -1
  60. package/dist/codex-rpc-engine.js +113 -2
  61. package/dist/codex-rpc-engine.js.map +1 -1
  62. package/dist/config.d.ts +16 -0
  63. package/dist/config.d.ts.map +1 -1
  64. package/dist/config.js +16 -0
  65. package/dist/config.js.map +1 -1
  66. package/dist/core/bot-live-control.d.ts +41 -0
  67. package/dist/core/bot-live-control.d.ts.map +1 -0
  68. package/dist/core/bot-live-control.js +135 -0
  69. package/dist/core/bot-live-control.js.map +1 -0
  70. package/dist/core/command-handler.d.ts.map +1 -1
  71. package/dist/core/command-handler.js +28 -8
  72. package/dist/core/command-handler.js.map +1 -1
  73. package/dist/core/daemon-config-fence.d.ts +8 -0
  74. package/dist/core/daemon-config-fence.d.ts.map +1 -0
  75. package/dist/core/daemon-config-fence.js +13 -0
  76. package/dist/core/daemon-config-fence.js.map +1 -0
  77. package/dist/core/dashboard-events.d.ts +5 -0
  78. package/dist/core/dashboard-events.d.ts.map +1 -1
  79. package/dist/core/dashboard-events.js.map +1 -1
  80. package/dist/core/dashboard-ipc-server.d.ts +21 -2
  81. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  82. package/dist/core/dashboard-ipc-server.js +232 -5
  83. package/dist/core/dashboard-ipc-server.js.map +1 -1
  84. package/dist/core/dispatch-registry.d.ts +12 -0
  85. package/dist/core/dispatch-registry.d.ts.map +1 -0
  86. package/dist/core/dispatch-registry.js +33 -0
  87. package/dist/core/dispatch-registry.js.map +1 -0
  88. package/dist/core/dispatch.d.ts +99 -0
  89. package/dist/core/dispatch.d.ts.map +1 -1
  90. package/dist/core/dispatch.js +136 -0
  91. package/dist/core/dispatch.js.map +1 -1
  92. package/dist/core/maintenance.d.ts.map +1 -1
  93. package/dist/core/maintenance.js +10 -3
  94. package/dist/core/maintenance.js.map +1 -1
  95. package/dist/core/persistent-backend.d.ts +16 -2
  96. package/dist/core/persistent-backend.d.ts.map +1 -1
  97. package/dist/core/persistent-backend.js +40 -0
  98. package/dist/core/persistent-backend.js.map +1 -1
  99. package/dist/core/session-discovery.d.ts +8 -1
  100. package/dist/core/session-discovery.d.ts.map +1 -1
  101. package/dist/core/session-discovery.js +140 -8
  102. package/dist/core/session-discovery.js.map +1 -1
  103. package/dist/core/session-manager.d.ts.map +1 -1
  104. package/dist/core/session-manager.js +12 -8
  105. package/dist/core/session-manager.js.map +1 -1
  106. package/dist/core/session-title.d.ts +8 -0
  107. package/dist/core/session-title.d.ts.map +1 -1
  108. package/dist/core/session-title.js +54 -0
  109. package/dist/core/session-title.js.map +1 -1
  110. package/dist/core/terminal-write-auth.d.ts +15 -0
  111. package/dist/core/terminal-write-auth.d.ts.map +1 -1
  112. package/dist/core/terminal-write-auth.js +20 -0
  113. package/dist/core/terminal-write-auth.js.map +1 -1
  114. package/dist/core/trigger-session.d.ts.map +1 -1
  115. package/dist/core/trigger-session.js +3 -2
  116. package/dist/core/trigger-session.js.map +1 -1
  117. package/dist/core/types.d.ts +33 -0
  118. package/dist/core/types.d.ts.map +1 -1
  119. package/dist/core/types.js.map +1 -1
  120. package/dist/core/worker-pool.d.ts +27 -8
  121. package/dist/core/worker-pool.d.ts.map +1 -1
  122. package/dist/core/worker-pool.js +389 -19
  123. package/dist/core/worker-pool.js.map +1 -1
  124. package/dist/daemon.d.ts +4 -2
  125. package/dist/daemon.d.ts.map +1 -1
  126. package/dist/daemon.js +397 -26
  127. package/dist/daemon.js.map +1 -1
  128. package/dist/dashboard/bot-onboarding.d.ts +127 -1
  129. package/dist/dashboard/bot-onboarding.d.ts.map +1 -1
  130. package/dist/dashboard/bot-onboarding.js +1104 -25
  131. package/dist/dashboard/bot-onboarding.js.map +1 -1
  132. package/dist/dashboard/debug-terminal.d.ts +18 -0
  133. package/dist/dashboard/debug-terminal.d.ts.map +1 -0
  134. package/dist/dashboard/debug-terminal.js +338 -0
  135. package/dist/dashboard/debug-terminal.js.map +1 -0
  136. package/dist/dashboard/public-redact.d.ts +2 -0
  137. package/dist/dashboard/public-redact.d.ts.map +1 -1
  138. package/dist/dashboard/public-redact.js +12 -3
  139. package/dist/dashboard/public-redact.js.map +1 -1
  140. package/dist/dashboard/registry.d.ts +8 -0
  141. package/dist/dashboard/registry.d.ts.map +1 -1
  142. package/dist/dashboard/registry.js +13 -0
  143. package/dist/dashboard/registry.js.map +1 -1
  144. package/dist/dashboard/settings-write-applier.d.ts +47 -1
  145. package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
  146. package/dist/dashboard/settings-write-applier.js +102 -1
  147. package/dist/dashboard/settings-write-applier.js.map +1 -1
  148. package/dist/dashboard/web/app.d.ts.map +1 -1
  149. package/dist/dashboard/web/app.js +2 -0
  150. package/dist/dashboard/web/app.js.map +1 -1
  151. package/dist/dashboard/web/bot-defaults-page.d.ts +1 -0
  152. package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
  153. package/dist/dashboard/web/bot-defaults-page.js +24 -5
  154. package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
  155. package/dist/dashboard/web/bot-defaults.d.ts +15 -0
  156. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  157. package/dist/dashboard/web/bot-defaults.js +19 -0
  158. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  159. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  160. package/dist/dashboard/web/i18n.js +76 -6
  161. package/dist/dashboard/web/i18n.js.map +1 -1
  162. package/dist/dashboard/web/icon-tooltip.d.ts +3 -0
  163. package/dist/dashboard/web/icon-tooltip.d.ts.map +1 -0
  164. package/dist/dashboard/web/icon-tooltip.js +108 -0
  165. package/dist/dashboard/web/icon-tooltip.js.map +1 -0
  166. package/dist/dashboard/web/sessions-kanban.js +2 -2
  167. package/dist/dashboard/web/sessions-kanban.js.map +1 -1
  168. package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
  169. package/dist/dashboard/web/sessions-page.js +48 -9
  170. package/dist/dashboard/web/sessions-page.js.map +1 -1
  171. package/dist/dashboard/web/sessions.d.ts +2 -0
  172. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  173. package/dist/dashboard/web/sessions.js +46 -2
  174. package/dist/dashboard/web/sessions.js.map +1 -1
  175. package/dist/dashboard/web/settings-page.d.ts +78 -0
  176. package/dist/dashboard/web/settings-page.d.ts.map +1 -1
  177. package/dist/dashboard/web/settings-page.js +88 -1
  178. package/dist/dashboard/web/settings-page.js.map +1 -1
  179. package/dist/dashboard/web/store.d.ts +2 -0
  180. package/dist/dashboard/web/store.d.ts.map +1 -1
  181. package/dist/dashboard/web/store.js +13 -1
  182. package/dist/dashboard/web/store.js.map +1 -1
  183. package/dist/dashboard-web/app.js +1 -1
  184. package/dist/dashboard-web/chunks/bot-defaults-page-O376WSER.js +5 -0
  185. package/dist/dashboard-web/chunks/{chunk-JNRSPKLS.js → chunk-EJDYUINZ.js} +1 -1
  186. package/dist/dashboard-web/chunks/{chunk-CUV65BIQ.js → chunk-H7P5UV4W.js} +6 -6
  187. package/dist/dashboard-web/chunks/{chunk-P6GO5KT4.js → chunk-QCPPKCX3.js} +1 -1
  188. package/dist/dashboard-web/chunks/{chunk-UBOLHQCK.js → chunk-WXPMNEYV.js} +1 -1
  189. package/dist/dashboard-web/chunks/{connectors-page-KTC2QWNW.js → connectors-page-IR54AOAI.js} +1 -1
  190. package/dist/dashboard-web/chunks/{groups-page-B3EKGOLY.js → groups-page-3P4TLUMF.js} +1 -1
  191. package/dist/dashboard-web/chunks/{insights-page-X6RXNCWX.js → insights-page-TDCZV56A.js} +1 -1
  192. package/dist/dashboard-web/chunks/{monitor-room-LLRA5LQW.js → monitor-room-QWCWL3SX.js} +1 -1
  193. package/dist/dashboard-web/chunks/{monitoring-page-7MT5FEOS.js → monitoring-page-IOWFSNVC.js} +1 -1
  194. package/dist/dashboard-web/chunks/{office-page-KD46D5TD.js → office-page-5AUPEHZP.js} +1 -1
  195. package/dist/dashboard-web/chunks/{overview-page-JUWNH32O.js → overview-page-QJMI6NLW.js} +1 -1
  196. package/dist/dashboard-web/chunks/{roles-page-P6F7NLOI.js → roles-page-WJVB52ET.js} +1 -1
  197. package/dist/dashboard-web/chunks/{schedules-page-XIHWZST4.js → schedules-page-RVRWHP4V.js} +1 -1
  198. package/dist/dashboard-web/chunks/sessions-page-KWLGBX6G.js +3 -0
  199. package/dist/dashboard-web/chunks/settings-page-TNZEPC7V.js +2 -0
  200. package/dist/dashboard-web/chunks/{skills-page-A46KVWBL.js → skills-page-7YT5TIX5.js} +1 -1
  201. package/dist/dashboard-web/chunks/{team-federation-page-SDUC56DF.js → team-federation-page-NJZKTU7A.js} +1 -1
  202. package/dist/dashboard-web/chunks/{v3-page-J5JOTUSQ.js → v3-page-NQ73UA2Q.js} +1 -1
  203. package/dist/dashboard-web/chunks/{whiteboards-page-U7LSOAWI.js → whiteboards-page-DSM3MDMN.js} +1 -1
  204. package/dist/dashboard-web/style.css +198 -7
  205. package/dist/dashboard.js +294 -7
  206. package/dist/dashboard.js.map +1 -1
  207. package/dist/features/codex-notifier/adoption.d.ts +11 -0
  208. package/dist/features/codex-notifier/adoption.d.ts.map +1 -0
  209. package/dist/features/codex-notifier/adoption.js +11 -0
  210. package/dist/features/codex-notifier/adoption.js.map +1 -0
  211. package/dist/features/codex-notifier/app-opener.d.ts +21 -0
  212. package/dist/features/codex-notifier/app-opener.d.ts.map +1 -0
  213. package/dist/features/codex-notifier/app-opener.js +33 -0
  214. package/dist/features/codex-notifier/app-opener.js.map +1 -0
  215. package/dist/features/codex-notifier/card-action.d.ts +27 -0
  216. package/dist/features/codex-notifier/card-action.d.ts.map +1 -0
  217. package/dist/features/codex-notifier/card-action.js +84 -0
  218. package/dist/features/codex-notifier/card-action.js.map +1 -0
  219. package/dist/features/codex-notifier/card.d.ts +12 -0
  220. package/dist/features/codex-notifier/card.d.ts.map +1 -0
  221. package/dist/features/codex-notifier/card.js +150 -0
  222. package/dist/features/codex-notifier/card.js.map +1 -0
  223. package/dist/features/codex-notifier/codex-context.d.ts +12 -0
  224. package/dist/features/codex-notifier/codex-context.d.ts.map +1 -0
  225. package/dist/features/codex-notifier/codex-context.js +161 -0
  226. package/dist/features/codex-notifier/codex-context.js.map +1 -0
  227. package/dist/features/codex-notifier/config.d.ts +9 -0
  228. package/dist/features/codex-notifier/config.d.ts.map +1 -0
  229. package/dist/features/codex-notifier/config.js +11 -0
  230. package/dist/features/codex-notifier/config.js.map +1 -0
  231. package/dist/features/codex-notifier/confirmed-turn.d.ts +20 -0
  232. package/dist/features/codex-notifier/confirmed-turn.d.ts.map +1 -0
  233. package/dist/features/codex-notifier/confirmed-turn.js +130 -0
  234. package/dist/features/codex-notifier/confirmed-turn.js.map +1 -0
  235. package/dist/features/codex-notifier/emitter.d.ts +13 -0
  236. package/dist/features/codex-notifier/emitter.d.ts.map +1 -0
  237. package/dist/features/codex-notifier/emitter.js +33 -0
  238. package/dist/features/codex-notifier/emitter.js.map +1 -0
  239. package/dist/features/codex-notifier/event-store.d.ts +29 -0
  240. package/dist/features/codex-notifier/event-store.d.ts.map +1 -0
  241. package/dist/features/codex-notifier/event-store.js +217 -0
  242. package/dist/features/codex-notifier/event-store.js.map +1 -0
  243. package/dist/features/codex-notifier/event.d.ts +46 -0
  244. package/dist/features/codex-notifier/event.d.ts.map +1 -0
  245. package/dist/features/codex-notifier/event.js +188 -0
  246. package/dist/features/codex-notifier/event.js.map +1 -0
  247. package/dist/features/codex-notifier/hook-cli.d.ts +23 -0
  248. package/dist/features/codex-notifier/hook-cli.d.ts.map +1 -0
  249. package/dist/features/codex-notifier/hook-cli.js +130 -0
  250. package/dist/features/codex-notifier/hook-cli.js.map +1 -0
  251. package/dist/features/codex-notifier/hook-installer.d.ts +19 -0
  252. package/dist/features/codex-notifier/hook-installer.d.ts.map +1 -0
  253. package/dist/features/codex-notifier/hook-installer.js +158 -0
  254. package/dist/features/codex-notifier/hook-installer.js.map +1 -0
  255. package/dist/features/codex-notifier/index.d.ts +21 -0
  256. package/dist/features/codex-notifier/index.d.ts.map +1 -0
  257. package/dist/features/codex-notifier/index.js +19 -0
  258. package/dist/features/codex-notifier/index.js.map +1 -0
  259. package/dist/features/codex-notifier/internal-turn.d.ts +5 -0
  260. package/dist/features/codex-notifier/internal-turn.d.ts.map +1 -0
  261. package/dist/features/codex-notifier/internal-turn.js +43 -0
  262. package/dist/features/codex-notifier/internal-turn.js.map +1 -0
  263. package/dist/features/codex-notifier/outbox-worker.d.ts +71 -0
  264. package/dist/features/codex-notifier/outbox-worker.d.ts.map +1 -0
  265. package/dist/features/codex-notifier/outbox-worker.js +290 -0
  266. package/dist/features/codex-notifier/outbox-worker.js.map +1 -0
  267. package/dist/features/codex-notifier/outbox.d.ts +25 -0
  268. package/dist/features/codex-notifier/outbox.d.ts.map +1 -0
  269. package/dist/features/codex-notifier/outbox.js +225 -0
  270. package/dist/features/codex-notifier/outbox.js.map +1 -0
  271. package/dist/features/codex-notifier/paths.d.ts +7 -0
  272. package/dist/features/codex-notifier/paths.d.ts.map +1 -0
  273. package/dist/features/codex-notifier/paths.js +20 -0
  274. package/dist/features/codex-notifier/paths.js.map +1 -0
  275. package/dist/features/codex-notifier/screen-lock.d.ts +12 -0
  276. package/dist/features/codex-notifier/screen-lock.d.ts.map +1 -0
  277. package/dist/features/codex-notifier/screen-lock.js +47 -0
  278. package/dist/features/codex-notifier/screen-lock.js.map +1 -0
  279. package/dist/features/codex-notifier/side-conversation-monitor.d.ts +102 -0
  280. package/dist/features/codex-notifier/side-conversation-monitor.d.ts.map +1 -0
  281. package/dist/features/codex-notifier/side-conversation-monitor.js +774 -0
  282. package/dist/features/codex-notifier/side-conversation-monitor.js.map +1 -0
  283. package/dist/features/codex-notifier/types.d.ts +42 -0
  284. package/dist/features/codex-notifier/types.d.ts.map +1 -0
  285. package/dist/features/codex-notifier/types.js +2 -0
  286. package/dist/features/codex-notifier/types.js.map +1 -0
  287. package/dist/features/codex-notifier/worker-lock.d.ts +8 -0
  288. package/dist/features/codex-notifier/worker-lock.d.ts.map +1 -0
  289. package/dist/features/codex-notifier/worker-lock.js +108 -0
  290. package/dist/features/codex-notifier/worker-lock.js.map +1 -0
  291. package/dist/global-config.d.ts +13 -0
  292. package/dist/global-config.d.ts.map +1 -1
  293. package/dist/global-config.js +30 -0
  294. package/dist/global-config.js.map +1 -1
  295. package/dist/im/lark/card-builder.js +2 -2
  296. package/dist/im/lark/card-builder.js.map +1 -1
  297. package/dist/im/lark/card-handler.d.ts +2 -0
  298. package/dist/im/lark/card-handler.d.ts.map +1 -1
  299. package/dist/im/lark/card-handler.js +165 -34
  300. package/dist/im/lark/card-handler.js.map +1 -1
  301. package/dist/im/lark/client.d.ts +62 -4
  302. package/dist/im/lark/client.d.ts.map +1 -1
  303. package/dist/im/lark/client.js +237 -29
  304. package/dist/im/lark/client.js.map +1 -1
  305. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  306. package/dist/im/lark/event-dispatcher.js +20 -30
  307. package/dist/im/lark/event-dispatcher.js.map +1 -1
  308. package/dist/services/codex-app-threads.d.ts +47 -0
  309. package/dist/services/codex-app-threads.d.ts.map +1 -1
  310. package/dist/services/codex-app-threads.js +502 -10
  311. package/dist/services/codex-app-threads.js.map +1 -1
  312. package/dist/services/exact-chat-grant.d.ts +83 -0
  313. package/dist/services/exact-chat-grant.d.ts.map +1 -0
  314. package/dist/services/exact-chat-grant.js +249 -0
  315. package/dist/services/exact-chat-grant.js.map +1 -0
  316. package/dist/services/group-creator.d.ts +11 -0
  317. package/dist/services/group-creator.d.ts.map +1 -1
  318. package/dist/services/group-creator.js +18 -9
  319. package/dist/services/group-creator.js.map +1 -1
  320. package/dist/services/local-cli-opener.d.ts.map +1 -1
  321. package/dist/services/local-cli-opener.js +10 -5
  322. package/dist/services/local-cli-opener.js.map +1 -1
  323. package/dist/services/traex-user-input.d.ts +19 -0
  324. package/dist/services/traex-user-input.d.ts.map +1 -0
  325. package/dist/services/traex-user-input.js +57 -0
  326. package/dist/services/traex-user-input.js.map +1 -0
  327. package/dist/setup/open-platform-automation.d.ts +24 -3
  328. package/dist/setup/open-platform-automation.d.ts.map +1 -1
  329. package/dist/setup/open-platform-automation.js +45 -8
  330. package/dist/setup/open-platform-automation.js.map +1 -1
  331. package/dist/setup/verify-permissions.d.ts +18 -0
  332. package/dist/setup/verify-permissions.d.ts.map +1 -1
  333. package/dist/setup/verify-permissions.js +60 -0
  334. package/dist/setup/verify-permissions.js.map +1 -1
  335. package/dist/types.d.ts +70 -4
  336. package/dist/types.d.ts.map +1 -1
  337. package/dist/utils/stuck-detector.d.ts +45 -0
  338. package/dist/utils/stuck-detector.d.ts.map +1 -0
  339. package/dist/utils/stuck-detector.js +188 -0
  340. package/dist/utils/stuck-detector.js.map +1 -0
  341. package/dist/utils/stuck-key-guard.d.ts +75 -0
  342. package/dist/utils/stuck-key-guard.d.ts.map +1 -0
  343. package/dist/utils/stuck-key-guard.js +77 -0
  344. package/dist/utils/stuck-key-guard.js.map +1 -0
  345. package/dist/worker.js +705 -93
  346. package/dist/worker.js.map +1 -1
  347. package/package.json +1 -1
  348. package/dist/dashboard-web/chunks/bot-defaults-page-B5AY4B2J.js +0 -5
  349. package/dist/dashboard-web/chunks/sessions-page-2VECQ27T.js +0 -3
  350. package/dist/dashboard-web/chunks/settings-page-HMIUE2JA.js +0 -2
@@ -1,18 +1,96 @@
1
1
  import { createRequire } from 'node:module';
2
- import { existsSync, readFileSync, unlinkSync } from 'node:fs';
2
+ import { existsSync, readFileSync, rmSync, unlinkSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
3
4
  import { readBotsJsonOrEmpty, writeBotsJsonAtomic } from '../setup/bots-store.js';
4
5
  import { atomicWriteFileSync } from '../utils/atomic-write.js';
5
6
  import { logger } from '../utils/logger.js';
6
7
  import { normalizeBotConfig, findInvalidAllowedUserEntries, hasOwnerEntry } from '../setup/bot-config-editor.js';
7
8
  import { tryRegisterApp } from '../setup/register-app.js';
8
- import { validateCredentials, buildRemainingSteps } from '../setup/verify-permissions.js';
9
+ import { validateCredentials, readCriticalScopesFromApplicationInfo, buildRemainingSteps, } from '../setup/verify-permissions.js';
9
10
  import { resolveSetupAppName } from '../setup/app-name.js';
10
- import { automateOpenPlatformSetup, createFeishuOpenPlatformApp, inspectCachedFeishuOpenPlatformSession, } from '../setup/open-platform-automation.js';
11
+ import { automateOpenPlatformSetup, BOT_BASELINE_APP_EVENTS, BOT_BASELINE_CALLBACKS, createFeishuOpenPlatformApp, inspectCachedFeishuOpenPlatformSession, } from '../setup/open-platform-automation.js';
11
12
  import { sdkDomain } from '../im/lark/lark-hosts.js';
12
13
  import * as Lark from '@larksuiteoapi/node-sdk';
13
14
  const require = createRequire(import.meta.url);
14
15
  const QRCode = require('qrcode-terminal/vendor/QRCode');
15
16
  const QRErrorCorrectLevel = require('qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel');
17
+ const MANAGED_VERIFIED_EVENT_COUNT = BOT_BASELINE_APP_EVENTS.length + BOT_BASELINE_CALLBACKS.length;
18
+ function hasExactManagedAutomationAck(result) {
19
+ return result.ok
20
+ && result.eventMode === 4
21
+ && result.verifiedEventCount === MANAGED_VERIFIED_EVENT_COUNT
22
+ && typeof result.versionId === 'string'
23
+ && result.versionId.trim().length > 0;
24
+ }
25
+ function hasExactManagedPermissionAck(permission) {
26
+ return permission?.eventMode === 4
27
+ && permission.verifiedEventCount === MANAGED_VERIFIED_EVENT_COUNT
28
+ && typeof permission.versionId === 'string'
29
+ && permission.versionId.trim().length > 0;
30
+ }
31
+ function managedActivationStateForSnapshot(job) {
32
+ if (job.criticalScopeActivationRequired !== true
33
+ || !job.appId
34
+ || !job.workingDir
35
+ || job.platformQrScanConfirmedAt === undefined
36
+ || !hasExactManagedPermissionAck(job.permission)) {
37
+ return undefined;
38
+ }
39
+ if (job.status === 'failed'
40
+ && job.recoveryOfJobId
41
+ && job.error === 'permission_recovery_failed'
42
+ && job.permission?.ok === false
43
+ && job.permission.reason === 'scope_mapping_failed') {
44
+ return 'scope_propagation_pending';
45
+ }
46
+ if (job.status === 'completed'
47
+ && job.permission?.ok === true
48
+ && job.activationPending === true
49
+ && job.activationCommitting === true) {
50
+ return 'activation_committing';
51
+ }
52
+ if (job.status === 'completed'
53
+ && job.permission?.ok === true
54
+ && job.activationPending === true) {
55
+ return 'initial_scope_propagation_pending';
56
+ }
57
+ if (job.status === 'completed'
58
+ && job.permission?.ok === true
59
+ && job.activationPending !== true
60
+ && job.liveStarted === true) {
61
+ return 'completed';
62
+ }
63
+ return undefined;
64
+ }
65
+ function isExactManagedActivationMarker(value, appId, jobId) {
66
+ return !!value
67
+ && typeof value === 'object'
68
+ && !Array.isArray(value)
69
+ && value.appId === appId
70
+ && typeof value.jobId === 'string'
71
+ && (jobId === undefined || value.jobId === jobId);
72
+ }
73
+ function managedActivationMarker(bot) {
74
+ if (!bot || typeof bot !== 'object' || Array.isArray(bot))
75
+ return undefined;
76
+ const record = bot;
77
+ const appId = typeof record.larkAppId === 'string' ? record.larkAppId : '';
78
+ const markers = [
79
+ ['deactivating', record.activationDeactivating],
80
+ ['starting', record.activationStarting],
81
+ ['committed', record.activationCommitted],
82
+ ].filter(([, value]) => value !== undefined);
83
+ if (markers.length !== 1)
84
+ return undefined;
85
+ const [phase, value] = markers[0];
86
+ if (!isExactManagedActivationMarker(value, appId))
87
+ return undefined;
88
+ return {
89
+ phase,
90
+ appId,
91
+ jobId: value.jobId,
92
+ };
93
+ }
16
94
  function svgEscape(value) {
17
95
  return value.replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
18
96
  }
@@ -52,10 +130,19 @@ export class BotOnboardingManager {
52
130
  registerApp;
53
131
  validateCredentials;
54
132
  automateOpenPlatform;
133
+ verifyCriticalScopes;
134
+ criticalScopeStableReads;
135
+ criticalScopeMaxAttempts;
136
+ criticalScopePollIntervalMs;
55
137
  renderQrDataUrl;
56
138
  now;
57
139
  startBotLive;
58
140
  pendingStorePath;
141
+ stopBotLive;
142
+ permissionRecoveryStorePath;
143
+ scopePropagationFlights = new Map();
144
+ activationStartupReconciliation = Promise.resolve();
145
+ permissionRecoveryStateError;
59
146
  constructor(opts) {
60
147
  this.opts = opts;
61
148
  // 生产默认走单次 Web session;旧单测/外部注入若只给 registerApp,则明确
@@ -65,11 +152,33 @@ export class BotOnboardingManager {
65
152
  this.registerApp = opts.registerApp ?? tryRegisterApp;
66
153
  this.validateCredentials = opts.validateCredentials ?? validateCredentials;
67
154
  this.automateOpenPlatform = opts.automateOpenPlatform ?? automateOpenPlatformSetup;
155
+ this.verifyCriticalScopes = opts.verifyCriticalScopes ?? readCriticalScopesFromApplicationInfo;
156
+ this.criticalScopeStableReads = Number.isInteger(opts.criticalScopeStableReads)
157
+ && (opts.criticalScopeStableReads ?? 0) > 0
158
+ ? opts.criticalScopeStableReads
159
+ : 3;
160
+ const configuredMaxAttempts = Number.isInteger(opts.criticalScopeMaxAttempts)
161
+ && (opts.criticalScopeMaxAttempts ?? 0) > 0
162
+ ? opts.criticalScopeMaxAttempts
163
+ : 12;
164
+ this.criticalScopeMaxAttempts = Math.max(this.criticalScopeStableReads, configuredMaxAttempts);
165
+ this.criticalScopePollIntervalMs = Number.isInteger(opts.criticalScopePollIntervalMs)
166
+ && (opts.criticalScopePollIntervalMs ?? -1) >= 0
167
+ ? opts.criticalScopePollIntervalMs
168
+ : 5_000;
68
169
  this.renderQrDataUrl = opts.renderQrDataUrl ?? renderQrSvgDataUrl;
69
170
  this.now = opts.now ?? (() => Date.now());
70
171
  this.startBotLive = opts.startBotLive;
71
172
  this.pendingStorePath = opts.pendingStorePath ?? `${opts.botsJsonPath}.onboarding-pending.json`;
173
+ this.stopBotLive = opts.stopBotLive;
174
+ this.permissionRecoveryStorePath = opts.permissionRecoveryStorePath ?? `${opts.botsJsonPath}.permission-recoveries.json`;
72
175
  this.restorePendingJobs();
176
+ this.restorePermissionRecoveryJobs();
177
+ this.requireDurableManagedInitialJobs();
178
+ // A dashboard crash after PM2 has accepted the start but before the
179
+ // durable marker is cleared must converge to pending, never assume the
180
+ // daemon is safe to keep online.
181
+ this.activationStartupReconciliation = this.reconcileInterruptedManagedActivations();
73
182
  }
74
183
  /**
75
184
  * 恢复 owner 待确认任务。凭证只存在 0600 私有文件和内存中,公开 job snapshot
@@ -137,21 +246,640 @@ export class BotOnboardingManager {
137
246
  logger.warn(`[bot-onboarding] 无法持久化 owner 待确认任务: ${err?.message ?? String(err)}`);
138
247
  }
139
248
  }
249
+ restorePermissionRecoveryJobs() {
250
+ if (!existsSync(this.permissionRecoveryStorePath))
251
+ return;
252
+ let parsed;
253
+ try {
254
+ parsed = JSON.parse(readFileSync(this.permissionRecoveryStorePath, 'utf8'));
255
+ }
256
+ catch (err) {
257
+ this.permissionRecoveryStateError = `permission recovery ledger is unreadable: ${err instanceof Error ? err.message : String(err)}`;
258
+ return;
259
+ }
260
+ if (parsed?.version !== 1 || !Array.isArray(parsed.jobs) || parsed.jobs.length > 128) {
261
+ this.permissionRecoveryStateError = 'permission recovery ledger schema is invalid';
262
+ return;
263
+ }
264
+ const records = parsed.jobs;
265
+ const restored = [];
266
+ const ids = new Set();
267
+ for (const raw of records) {
268
+ const isRecovery = (typeof raw?.recoveryOfJobId === 'string'
269
+ && raw.recoveryOfJobId.length > 0);
270
+ const isManagedInitial = (!isRecovery
271
+ && ['initial_scope_propagation_pending', 'activation_committing', 'completed'].includes(raw?.managedActivationState));
272
+ if (!raw || typeof raw !== 'object'
273
+ || typeof raw.id !== 'string'
274
+ || (isRecovery
275
+ ? !/^botperm_[A-Za-z0-9_-]{1,160}$/.test(raw.id)
276
+ : !/^bot_[A-Za-z0-9_-]{1,160}$/.test(raw.id))
277
+ || (!isRecovery && !isManagedInitial)
278
+ || ids.has(raw.id)
279
+ || typeof raw.appId !== 'string' || !raw.appId
280
+ || typeof raw.workingDir !== 'string' || !raw.workingDir
281
+ || !Number.isFinite(raw.createdAt) || !Number.isFinite(raw.updatedAt)
282
+ || (isRecovery
283
+ ? (!Number.isInteger(raw.recoveryAttempt) || raw.recoveryAttempt < 1)
284
+ : raw.recoveryAttempt !== undefined)
285
+ || !['starting', 'configuring_permissions', 'waiting_for_platform_scan', 'completed', 'failed'].includes(raw.status)
286
+ || (isManagedInitial && raw.status !== 'completed')
287
+ || !['feishu', 'lark'].includes(raw.brand)
288
+ || (raw.previousRecoveryJobId !== undefined
289
+ && (!isRecovery || typeof raw.previousRecoveryJobId !== 'string'))
290
+ || (raw.platformQrScanConfirmedAt !== undefined
291
+ && (!Number.isInteger(raw.platformQrScanConfirmedAt) || raw.platformQrScanConfirmedAt <= 0))
292
+ || (raw.activationDeactivating !== undefined && raw.activationDeactivating !== true)
293
+ || (raw.activationCommitting !== undefined && raw.activationCommitting !== true)) {
294
+ this.permissionRecoveryStateError = 'permission recovery ledger contains an invalid record';
295
+ return;
296
+ }
297
+ ids.add(raw.id);
298
+ const status = ['completed', 'failed'].includes(raw.status)
299
+ ? raw.status
300
+ : 'failed';
301
+ const snapshot = {
302
+ id: raw.id,
303
+ status,
304
+ createdAt: raw.createdAt,
305
+ updatedAt: this.now(),
306
+ appId: raw.appId,
307
+ brand: raw.brand === 'lark' ? 'lark' : 'feishu',
308
+ cliId: 'traex',
309
+ workingDir: raw.workingDir,
310
+ registrationMode: 'compat',
311
+ ...(isRecovery
312
+ ? {
313
+ recoveryOfJobId: raw.recoveryOfJobId,
314
+ recoveryAttempt: raw.recoveryAttempt,
315
+ ...(typeof raw.previousRecoveryJobId === 'string'
316
+ ? { previousRecoveryJobId: raw.previousRecoveryJobId }
317
+ : {}),
318
+ }
319
+ : {}),
320
+ ...(raw.criticalScopeActivationRequired === true
321
+ ? { criticalScopeActivationRequired: true }
322
+ : {}),
323
+ ...(raw.activationPending === true
324
+ || raw.managedActivationState === 'initial_scope_propagation_pending'
325
+ ? { activationPending: true }
326
+ : {}),
327
+ ...(raw.activationDeactivating === true
328
+ ? { activationDeactivating: true, activationPending: true }
329
+ : {}),
330
+ ...(raw.activationCommitting === true
331
+ ? { activationCommitting: true, activationPending: true }
332
+ : {}),
333
+ ...(Number.isInteger(raw.platformQrScanConfirmedAt)
334
+ ? { platformQrScanConfirmedAt: raw.platformQrScanConfirmedAt }
335
+ : {}),
336
+ ...(raw.managedActivationState === 'initial_scope_propagation_pending'
337
+ ? {
338
+ permission: {
339
+ ok: true,
340
+ eventMode: raw.managedActivationAck?.eventMode,
341
+ verifiedEventCount: raw.managedActivationAck?.verifiedEventCount,
342
+ versionId: raw.managedActivationAck?.versionId,
343
+ },
344
+ }
345
+ : {}),
346
+ ...(raw.managedActivationState === 'scope_propagation_pending'
347
+ ? {
348
+ permission: {
349
+ ok: false,
350
+ reason: 'scope_mapping_failed',
351
+ message: '关键权限发布后仍在传播,等待 botmux 精确回读并启动',
352
+ eventMode: raw.managedActivationAck?.eventMode,
353
+ verifiedEventCount: raw.managedActivationAck?.verifiedEventCount,
354
+ versionId: raw.managedActivationAck?.versionId,
355
+ },
356
+ }
357
+ : {}),
358
+ ...(raw.managedActivationState === 'activation_committing'
359
+ ? {
360
+ activationPending: true,
361
+ activationCommitting: true,
362
+ permission: {
363
+ ok: true,
364
+ eventMode: raw.managedActivationAck?.eventMode,
365
+ verifiedEventCount: raw.managedActivationAck?.verifiedEventCount,
366
+ versionId: raw.managedActivationAck?.versionId,
367
+ },
368
+ }
369
+ : {}),
370
+ ...(raw.managedActivationState === 'completed'
371
+ ? {
372
+ liveStarted: true,
373
+ activationPending: false,
374
+ permission: {
375
+ ok: true,
376
+ eventMode: raw.managedActivationAck?.eventMode,
377
+ verifiedEventCount: raw.managedActivationAck?.verifiedEventCount,
378
+ versionId: raw.managedActivationAck?.versionId,
379
+ },
380
+ }
381
+ : {}),
382
+ ...(status === 'failed'
383
+ ? {
384
+ error: raw.status === 'failed' && typeof raw.error === 'string'
385
+ ? raw.error
386
+ : 'permission_recovery_interrupted',
387
+ message: raw.status === 'failed' && typeof raw.message === 'string'
388
+ ? raw.message
389
+ : 'Dashboard restarted before permission recovery reached a terminal result',
390
+ }
391
+ : {}),
392
+ };
393
+ if ((raw.managedActivationState !== undefined
394
+ && ![
395
+ 'initial_scope_propagation_pending',
396
+ 'scope_propagation_pending',
397
+ 'activation_committing',
398
+ 'completed',
399
+ ].includes(raw.managedActivationState))
400
+ || (raw.managedActivationState === 'initial_scope_propagation_pending'
401
+ && (raw.status !== 'completed'
402
+ || raw.activationPending !== true))
403
+ || (raw.managedActivationState === 'scope_propagation_pending'
404
+ && (!isRecovery
405
+ || raw.status !== 'failed'
406
+ || raw.error !== 'permission_recovery_failed'))
407
+ || (raw.managedActivationState === 'activation_committing'
408
+ && (raw.status !== 'completed'
409
+ || raw.activationPending !== true
410
+ || raw.activationDeactivating === true
411
+ || raw.activationCommitting !== true))
412
+ || (raw.managedActivationState === 'completed'
413
+ && raw.status !== 'completed')
414
+ || ((raw.managedActivationState !== undefined)
415
+ !== (raw.criticalScopeActivationRequired === true
416
+ && Number.isInteger(raw.platformQrScanConfirmedAt)
417
+ && raw.managedActivationAck?.eventMode === 4
418
+ && raw.managedActivationAck?.verifiedEventCount === MANAGED_VERIFIED_EVENT_COUNT
419
+ && typeof raw.managedActivationAck?.versionId === 'string'
420
+ && raw.managedActivationAck.versionId.trim().length > 0))) {
421
+ this.permissionRecoveryStateError = 'permission recovery ledger contains an invalid activation ACK';
422
+ return;
423
+ }
424
+ restored.push(snapshot);
425
+ }
426
+ const groups = new Map();
427
+ for (const snapshot of restored.filter(item => item.recoveryOfJobId)) {
428
+ const key = JSON.stringify([snapshot.recoveryOfJobId, snapshot.workingDir, snapshot.appId]);
429
+ const group = groups.get(key) ?? [];
430
+ group.push(snapshot);
431
+ groups.set(key, group);
432
+ }
433
+ for (const group of groups.values()) {
434
+ group.sort((a, b) => (a.recoveryAttempt ?? 0) - (b.recoveryAttempt ?? 0));
435
+ for (let index = 0; index < group.length; index += 1) {
436
+ if (group[index].recoveryAttempt !== index + 1
437
+ || (index === 0 && group[index].previousRecoveryJobId !== undefined)
438
+ || (index > 0 && group[index].previousRecoveryJobId !== group[index - 1].id)) {
439
+ this.permissionRecoveryStateError = 'permission recovery ledger lineage is ambiguous';
440
+ return;
441
+ }
442
+ }
443
+ }
444
+ try {
445
+ for (const snapshot of restored) {
446
+ if (snapshot.error === 'permission_recovery_interrupted') {
447
+ rmSync(join(dirname(this.opts.botsJsonPath), 'onboarding-sessions', `${snapshot.id}.json`), { force: true });
448
+ }
449
+ }
450
+ }
451
+ catch (err) {
452
+ this.permissionRecoveryStateError = `interrupted owner session could not be cleaned: ${err instanceof Error ? err.message : String(err)}`;
453
+ return;
454
+ }
455
+ for (const snapshot of restored) {
456
+ this.jobs.set(snapshot.id, snapshot);
457
+ }
458
+ try {
459
+ this.persistPermissionRecoveryJobs();
460
+ }
461
+ catch (err) {
462
+ this.permissionRecoveryStateError = `permission recovery ledger cannot be updated: ${err instanceof Error ? err.message : String(err)}`;
463
+ }
464
+ }
465
+ persistPermissionRecoveryJobs() {
466
+ const jobs = [...this.jobs.values()]
467
+ .filter(job => ((job.recoveryOfJobId && job.appId && job.workingDir && job.recoveryAttempt)
468
+ || managedActivationStateForSnapshot(job) !== undefined))
469
+ .sort((a, b) => a.updatedAt - b.updatedAt)
470
+ .slice(-128)
471
+ .map(job => {
472
+ const managedActivationState = managedActivationStateForSnapshot(job);
473
+ return {
474
+ id: job.id,
475
+ status: job.status,
476
+ createdAt: job.createdAt,
477
+ updatedAt: job.updatedAt,
478
+ appId: job.appId,
479
+ brand: job.brand,
480
+ workingDir: job.workingDir,
481
+ recoveryOfJobId: job.recoveryOfJobId,
482
+ recoveryAttempt: job.recoveryAttempt,
483
+ previousRecoveryJobId: job.previousRecoveryJobId,
484
+ criticalScopeActivationRequired: job.criticalScopeActivationRequired,
485
+ activationPending: job.activationPending,
486
+ ...(job.activationDeactivating === true
487
+ ? { activationDeactivating: true }
488
+ : {}),
489
+ ...(job.activationCommitting === true
490
+ ? { activationCommitting: true }
491
+ : {}),
492
+ platformQrScanConfirmedAt: job.platformQrScanConfirmedAt,
493
+ error: job.error,
494
+ message: job.message,
495
+ ...(managedActivationState && hasExactManagedPermissionAck(job.permission)
496
+ ? {
497
+ managedActivationState,
498
+ managedActivationAck: {
499
+ eventMode: job.permission.eventMode,
500
+ verifiedEventCount: job.permission.verifiedEventCount,
501
+ versionId: job.permission.versionId,
502
+ },
503
+ }
504
+ : {}),
505
+ };
506
+ });
507
+ if (jobs.length === 0)
508
+ return;
509
+ atomicWriteFileSync(this.permissionRecoveryStorePath, `${JSON.stringify({ version: 1, jobs }, null, 2)}\n`, { mode: 0o600 });
510
+ }
511
+ savePermissionRecoveryJobsBestEffort() {
512
+ try {
513
+ this.persistPermissionRecoveryJobs();
514
+ }
515
+ catch (err) {
516
+ logger.warn(`[bot-onboarding] 无法更新权限恢复 lineage: ${err?.message ?? String(err)}`);
517
+ }
518
+ }
519
+ /**
520
+ * A managed initial ACK is restart authority, not optional UI history.
521
+ * Refuse activation when its first durable write cannot be proven.
522
+ */
523
+ requireDurableManagedInitialJobs() {
524
+ if (this.permissionRecoveryStateError)
525
+ return;
526
+ const hasPendingInitial = [...this.jobs.values()].some(job => (!job.recoveryOfJobId
527
+ && managedActivationStateForSnapshot(job) === 'initial_scope_propagation_pending'));
528
+ if (!hasPendingInitial)
529
+ return;
530
+ try {
531
+ this.persistPermissionRecoveryJobs();
532
+ }
533
+ catch (err) {
534
+ this.permissionRecoveryStateError = `managed initial activation ledger could not be persisted: ${err instanceof Error ? err.message : String(err)}`;
535
+ }
536
+ }
140
537
  /**
141
538
  * Best-effort auto-start of the just-persisted bot's daemon (no fleet restart).
142
539
  * Records the outcome on the job snapshot so the frontend shows "已自动上线"
143
540
  * instead of the restart hint. Never throws.
144
541
  */
145
- async runLiveStart(id, appId) {
542
+ async runLiveStart(id, appId, recordOutcome = true) {
146
543
  if (!this.startBotLive)
147
- return;
544
+ return { attempted: false };
148
545
  try {
149
546
  const r = await this.startBotLive(appId);
150
- this.patch(id, { liveStarted: r.ok, liveStartMessage: r.message });
547
+ if (recordOutcome) {
548
+ this.patch(id, { liveStarted: r.ok, liveStartMessage: r.message });
549
+ }
550
+ return { attempted: true, ok: r.ok, message: r.message };
151
551
  }
152
552
  catch (err) {
153
- this.patch(id, { liveStarted: false, liveStartMessage: err instanceof Error ? err.message : String(err) });
553
+ const message = err instanceof Error ? err.message : String(err);
554
+ if (recordOutcome) {
555
+ this.patch(id, { liveStarted: false, liveStartMessage: message });
556
+ }
557
+ return { attempted: true, ok: false, message };
558
+ }
559
+ }
560
+ async runLiveStop(id, appId) {
561
+ if (!this.stopBotLive)
562
+ return { attempted: false };
563
+ try {
564
+ const r = await this.stopBotLive(appId);
565
+ this.patch(id, { liveStopped: r.ok, liveStopMessage: r.message });
566
+ return { attempted: true, ok: r.ok, message: r.message };
567
+ }
568
+ catch (err) {
569
+ const message = err instanceof Error ? err.message : String(err);
570
+ this.patch(id, { liveStopped: false, liveStopMessage: message });
571
+ return { attempted: true, ok: false, message };
572
+ }
573
+ }
574
+ async stableCriticalScopeReadback(id, appId, appSecret, brand) {
575
+ let consecutiveComplete = 0;
576
+ let readback = {
577
+ ok: false,
578
+ error: 'network',
579
+ message: '尚未读取权限状态',
580
+ };
581
+ for (let attempt = 1; attempt <= this.criticalScopeMaxAttempts; attempt += 1) {
582
+ try {
583
+ readback = await this.verifyCriticalScopes(appId, appSecret, brand);
584
+ }
585
+ catch (err) {
586
+ readback = {
587
+ ok: false,
588
+ error: 'network',
589
+ message: err instanceof Error ? err.message : String(err),
590
+ };
591
+ }
592
+ if (readback.ok && readback.missingCritical.length === 0) {
593
+ consecutiveComplete += 1;
594
+ }
595
+ else {
596
+ consecutiveComplete = 0;
597
+ }
598
+ this.patch(id, {
599
+ permissionStatusMsg: consecutiveComplete > 0
600
+ ? `正在确认 9 项核心权限稳定生效(${consecutiveComplete}/${this.criticalScopeStableReads})`
601
+ : `正在等待 9 项核心权限生效(${attempt}/${this.criticalScopeMaxAttempts})`,
602
+ });
603
+ if (consecutiveComplete >= this.criticalScopeStableReads) {
604
+ return { ready: true, readback };
605
+ }
606
+ if (attempt < this.criticalScopeMaxAttempts
607
+ && this.criticalScopePollIntervalMs > 0) {
608
+ await new Promise(resolve => {
609
+ setTimeout(resolve, this.criticalScopePollIntervalMs);
610
+ });
611
+ }
612
+ }
613
+ return { ready: false, readback };
614
+ }
615
+ async activateRecoveredBot(id, appId, addedBotIndex, ensureLiveStart = false) {
616
+ const bots = readBotsJsonOrEmpty(this.opts.botsJsonPath);
617
+ const current = bots[addedBotIndex];
618
+ if (!current
619
+ || typeof current !== 'object'
620
+ || current.larkAppId !== appId
621
+ || current.activationStarting !== undefined
622
+ || current.activationCommitted !== undefined
623
+ || current.activationCommitting !== undefined) {
624
+ throw new Error('permission_recovery_activation_target_drift');
625
+ }
626
+ // Existing pre-gate bots were already daemon-visible. Their exact App may
627
+ // recover permissions without performing a second activation transition.
628
+ if (current.activationPending !== true && !ensureLiveStart) {
629
+ return;
630
+ }
631
+ this.beginManagedActivation(id, appId, addedBotIndex);
632
+ const live = await this.runLiveStart(id, appId, false);
633
+ if (live.attempted && live.ok === true) {
634
+ try {
635
+ this.patch(id, {
636
+ activationPending: true,
637
+ activationCommitting: true,
638
+ liveStarted: false,
639
+ liveStartMessage: live.message,
640
+ });
641
+ this.persistPermissionRecoveryJobs();
642
+ this.commitManagedActivation(id, appId, addedBotIndex);
643
+ this.completeManagedActivation(id, appId, addedBotIndex);
644
+ return;
645
+ }
646
+ catch (err) {
647
+ const stopped = await this.runLiveStop(id, appId);
648
+ if (stopped.attempted && stopped.ok === true) {
649
+ this.restoreManagedActivationPending(id, appId, addedBotIndex);
650
+ }
651
+ throw err;
652
+ }
653
+ }
654
+ // An exit/timeout from start-bot is an unknown PM2 outcome. Stop and
655
+ // re-read the exact App before returning it to pending; otherwise a daemon
656
+ // could remain online while the durable config says it is blocked.
657
+ const stopped = await this.runLiveStop(id, appId);
658
+ if (!stopped.attempted || stopped.ok !== true) {
659
+ throw new Error(`permission_recovery_activation_stop_not_acknowledged${stopped.message ? `: ${stopped.message}` : ''}`);
660
+ }
661
+ this.restoreManagedActivationPending(id, appId, addedBotIndex);
662
+ throw new Error(`permission_recovery_activation_not_acknowledged${live.message ? `: ${live.message}` : ''}`);
663
+ }
664
+ beginManagedActivation(id, appId, addedBotIndex) {
665
+ const bots = readBotsJsonOrEmpty(this.opts.botsJsonPath);
666
+ const current = bots[addedBotIndex];
667
+ if (!current
668
+ || typeof current !== 'object'
669
+ || current.larkAppId !== appId
670
+ || current.activationPending !== true) {
671
+ throw new Error('permission_recovery_activation_target_drift');
672
+ }
673
+ const next = [...bots];
674
+ const activating = {
675
+ ...current,
676
+ activationStarting: { appId, jobId: id },
677
+ };
678
+ delete activating.activationPending;
679
+ delete activating.activationDeactivating;
680
+ delete activating.activationCommitting;
681
+ next[addedBotIndex] = activating;
682
+ writeBotsJsonAtomic(this.opts.botsJsonPath, next);
683
+ }
684
+ commitManagedActivation(id, appId, addedBotIndex) {
685
+ const bots = readBotsJsonOrEmpty(this.opts.botsJsonPath);
686
+ const current = bots[addedBotIndex];
687
+ if (!current
688
+ || typeof current !== 'object'
689
+ || current.larkAppId !== appId
690
+ || current.activationPending === true
691
+ || !isExactManagedActivationMarker(current.activationStarting, appId, id)) {
692
+ throw new Error('permission_recovery_activation_ack_target_drift');
693
+ }
694
+ const next = [...bots];
695
+ const ready = { ...current, activationCommitted: { appId, jobId: id } };
696
+ delete ready.activationStarting;
697
+ next[addedBotIndex] = ready;
698
+ writeBotsJsonAtomic(this.opts.botsJsonPath, next);
699
+ }
700
+ restoreManagedActivationPending(id, appId, addedBotIndex) {
701
+ const bots = readBotsJsonOrEmpty(this.opts.botsJsonPath);
702
+ const current = bots[addedBotIndex];
703
+ if (!current
704
+ || typeof current !== 'object'
705
+ || current.larkAppId !== appId
706
+ || (!isExactManagedActivationMarker(current.activationStarting, appId, id)
707
+ && !isExactManagedActivationMarker(current.activationDeactivating, appId, id)
708
+ && !isExactManagedActivationMarker(current.activationCommitted, appId, id))) {
709
+ throw new Error('permission_recovery_activation_rollback_target_drift');
710
+ }
711
+ const next = [...bots];
712
+ const pending = { ...current, activationPending: true };
713
+ delete pending.activationStarting;
714
+ delete pending.activationDeactivating;
715
+ delete pending.activationCommitted;
716
+ next[addedBotIndex] = pending;
717
+ writeBotsJsonAtomic(this.opts.botsJsonPath, next);
718
+ this.patch(id, {
719
+ activationPending: true,
720
+ activationDeactivating: false,
721
+ activationCommitting: false,
722
+ liveStarted: false,
723
+ });
724
+ }
725
+ completeManagedActivation(id, appId, addedBotIndex) {
726
+ const bots = readBotsJsonOrEmpty(this.opts.botsJsonPath);
727
+ const current = bots[addedBotIndex];
728
+ if (!current
729
+ || typeof current !== 'object'
730
+ || current.larkAppId !== appId
731
+ || current.activationPending === true
732
+ || current.activationStarting !== undefined
733
+ || current.activationDeactivating !== undefined
734
+ || !isExactManagedActivationMarker(current.activationCommitted, appId, id)) {
735
+ throw new Error('permission_recovery_activation_completion_target_drift');
736
+ }
737
+ this.patch(id, {
738
+ activationPending: false,
739
+ activationCommitting: false,
740
+ liveStarted: true,
741
+ });
742
+ this.persistPermissionRecoveryJobs();
743
+ const next = [...bots];
744
+ const ready = { ...current };
745
+ delete ready.activationCommitted;
746
+ next[addedBotIndex] = ready;
747
+ writeBotsJsonAtomic(this.opts.botsJsonPath, next);
748
+ }
749
+ clearManagedActivationCommitted(id, appId, addedBotIndex) {
750
+ const bots = readBotsJsonOrEmpty(this.opts.botsJsonPath);
751
+ const current = bots[addedBotIndex];
752
+ if (!current
753
+ || typeof current !== 'object'
754
+ || current.larkAppId !== appId
755
+ || !isExactManagedActivationMarker(current.activationCommitted, appId, id)) {
756
+ throw new Error('permission_recovery_activation_committed_target_drift');
757
+ }
758
+ const next = [...bots];
759
+ const ready = { ...current };
760
+ delete ready.activationCommitted;
761
+ next[addedBotIndex] = ready;
762
+ writeBotsJsonAtomic(this.opts.botsJsonPath, next);
763
+ }
764
+ async reconcileInterruptedManagedActivations() {
765
+ const targets = readBotsJsonOrEmpty(this.opts.botsJsonPath).flatMap((bot, index) => {
766
+ const marker = managedActivationMarker(bot);
767
+ if (!marker) {
768
+ if (bot
769
+ && typeof bot === 'object'
770
+ && (bot.activationDeactivating !== undefined
771
+ || bot.activationStarting !== undefined
772
+ || bot.activationCommitted !== undefined
773
+ || bot.activationCommitting !== undefined)) {
774
+ this.permissionRecoveryStateError = 'managed activation marker is invalid';
775
+ }
776
+ return [];
777
+ }
778
+ if (marker.phase === 'deactivating'
779
+ || marker.phase === 'starting'
780
+ || marker.phase === 'committed') {
781
+ return [{ index, ...marker }];
782
+ }
783
+ if (!marker.appId) {
784
+ this.permissionRecoveryStateError = 'managed activation marker is invalid';
785
+ return [];
786
+ }
787
+ return [];
788
+ });
789
+ if (this.permissionRecoveryStateError)
790
+ return;
791
+ for (const target of targets) {
792
+ try {
793
+ const job = this.jobs.get(target.jobId);
794
+ if (target.phase === 'committed'
795
+ && job?.status === 'completed'
796
+ && job.activationPending !== true
797
+ && job.liveStarted === true) {
798
+ this.clearManagedActivationCommitted(target.jobId, target.appId, target.index);
799
+ continue;
800
+ }
801
+ const stopped = await this.runLiveStop(target.jobId, target.appId);
802
+ if (!stopped.attempted || stopped.ok !== true) {
803
+ this.permissionRecoveryStateError = `managed activation stop could not be confirmed for ${target.appId}`;
804
+ return;
805
+ }
806
+ this.restoreManagedActivationPending(target.jobId, target.appId, target.index);
807
+ }
808
+ catch (err) {
809
+ this.permissionRecoveryStateError = `managed activation rollback failed: ${err instanceof Error ? err.message : String(err)}`;
810
+ return;
811
+ }
812
+ }
813
+ for (const job of this.jobs.values()) {
814
+ if (job.activationCommitting !== true
815
+ || job.status !== 'completed'
816
+ || !job.appId
817
+ || !job.workingDir) {
818
+ continue;
819
+ }
820
+ const matches = readBotsJsonOrEmpty(this.opts.botsJsonPath).flatMap((bot, index) => (bot
821
+ && typeof bot === 'object'
822
+ && bot.larkAppId === job.appId
823
+ && bot.cliId === 'traex'
824
+ && bot.defaultWorkingDir === job.workingDir
825
+ && bot.activationPending !== true
826
+ && bot.activationStarting === undefined
827
+ && bot.activationDeactivating === undefined
828
+ && isExactManagedActivationMarker(bot.activationCommitted, job.appId, job.id)
829
+ ? [index]
830
+ : []));
831
+ if (matches.length !== 1) {
832
+ this.permissionRecoveryStateError = `managed activation commit could not be reconciled for ${job.appId}`;
833
+ return;
834
+ }
835
+ try {
836
+ this.completeManagedActivation(job.id, job.appId, matches[0]);
837
+ }
838
+ catch (err) {
839
+ this.permissionRecoveryStateError = `managed activation commit reconciliation failed: ${err instanceof Error ? err.message : String(err)}`;
840
+ return;
841
+ }
842
+ }
843
+ }
844
+ holdRecoveryActivation(id, appId, addedBotIndex) {
845
+ const bots = readBotsJsonOrEmpty(this.opts.botsJsonPath);
846
+ const current = bots[addedBotIndex];
847
+ if (!current
848
+ || typeof current !== 'object'
849
+ || current.larkAppId !== appId) {
850
+ throw new Error('permission_recovery_activation_target_drift');
851
+ }
852
+ if (current.activationStarting !== undefined
853
+ || current.activationCommitted !== undefined
854
+ || (current.activationDeactivating !== undefined
855
+ && !isExactManagedActivationMarker(current.activationDeactivating, appId, id))) {
856
+ throw new Error('permission_recovery_activation_target_drift');
857
+ }
858
+ const next = [...bots];
859
+ next[addedBotIndex] = {
860
+ ...current,
861
+ activationPending: true,
862
+ activationDeactivating: { appId, jobId: id },
863
+ };
864
+ writeBotsJsonAtomic(this.opts.botsJsonPath, next);
865
+ this.patch(id, { activationPending: true, activationDeactivating: true });
866
+ this.persistPermissionRecoveryJobs();
867
+ }
868
+ markManagedDeactivated(id, appId, addedBotIndex) {
869
+ const bots = readBotsJsonOrEmpty(this.opts.botsJsonPath);
870
+ const current = bots[addedBotIndex];
871
+ if (!current
872
+ || typeof current !== 'object'
873
+ || current.larkAppId !== appId
874
+ || !isExactManagedActivationMarker(current.activationDeactivating, appId, id)) {
875
+ throw new Error('permission_recovery_deactivation_target_drift');
154
876
  }
877
+ const next = [...bots];
878
+ const pending = { ...current, activationPending: true };
879
+ delete pending.activationDeactivating;
880
+ next[addedBotIndex] = pending;
881
+ writeBotsJsonAtomic(this.opts.botsJsonPath, next);
882
+ this.patch(id, { activationPending: true, activationDeactivating: false });
155
883
  }
156
884
  start(input = {}) {
157
885
  const id = `bot_${Math.random().toString(36).slice(2)}_${this.now().toString(36)}`;
@@ -166,6 +894,221 @@ export class BotOnboardingManager {
166
894
  });
167
895
  return { id, done };
168
896
  }
897
+ /**
898
+ * Resume only Open Platform authorization for one already-persisted Space
899
+ * Agent. The exact neutral working directory is the durable target anchor:
900
+ * zero or multiple matching bots fail closed, and this path never creates an
901
+ * App or registers another Bot. A managed caller may place the exact existing
902
+ * row behind the critical-scope activation marker before the fresh QR.
903
+ */
904
+ startPermissionRecovery(input) {
905
+ if (this.permissionRecoveryStateError) {
906
+ return { ok: false, error: 'permission_recovery_state_unavailable' };
907
+ }
908
+ const configuredBots = readBotsJsonOrEmpty(this.opts.botsJsonPath);
909
+ if (configuredBots.some((bot) => (bot?.activationDeactivating !== undefined
910
+ || bot?.activationStarting !== undefined
911
+ || bot?.activationCommitted !== undefined))) {
912
+ return { ok: false, error: 'permission_recovery_state_unavailable' };
913
+ }
914
+ const candidates = configuredBots.flatMap((bot, index) => {
915
+ if (!bot
916
+ || typeof bot !== 'object'
917
+ || bot.cliId !== 'traex'
918
+ || bot.defaultWorkingDir !== input.workingDir) {
919
+ return [];
920
+ }
921
+ const appId = typeof bot.larkAppId === 'string' ? bot.larkAppId.trim() : '';
922
+ const appSecret = typeof bot.larkAppSecret === 'string' ? bot.larkAppSecret : '';
923
+ if (!appId || !appSecret || !Array.isArray(bot.allowedUsers) || !hasOwnerEntry(bot.allowedUsers))
924
+ return [];
925
+ return [{ appId, appSecret, brand: bot.brand === 'lark' ? 'lark' : 'feishu', index }];
926
+ });
927
+ if (candidates.length === 0)
928
+ return { ok: false, error: 'permission_recovery_target_missing' };
929
+ if (candidates.length !== 1)
930
+ return { ok: false, error: 'permission_recovery_target_ambiguous' };
931
+ if (!input.predecessorJobId || !input.workingDir || !input.expectedAppId || candidates[0].appId !== input.expectedAppId) {
932
+ return { ok: false, error: 'permission_recovery_target_invalid' };
933
+ }
934
+ const lineage = [...this.jobs.values()].filter(job => job.recoveryOfJobId === input.predecessorJobId
935
+ && job.workingDir === input.workingDir
936
+ && job.appId === input.expectedAppId).sort((a, b) => (a.recoveryAttempt ?? 0) - (b.recoveryAttempt ?? 0));
937
+ const latest = lineage.at(-1);
938
+ if (!input.priorRecoveryJobId) {
939
+ if (latest)
940
+ return { ok: true, job: { id: latest.id, done: Promise.resolve() } };
941
+ }
942
+ else {
943
+ if (!latest || latest.id !== input.priorRecoveryJobId) {
944
+ return { ok: false, error: 'permission_recovery_target_invalid' };
945
+ }
946
+ if (!['completed', 'failed'].includes(latest.status)) {
947
+ return { ok: true, job: { id: latest.id, done: Promise.resolve() } };
948
+ }
949
+ }
950
+ const target = candidates[0];
951
+ const id = `botperm_${Math.random().toString(36).slice(2)}_${this.now().toString(36)}`;
952
+ const createdAt = this.now();
953
+ this.jobs.set(id, {
954
+ id,
955
+ status: 'starting',
956
+ createdAt,
957
+ updatedAt: createdAt,
958
+ appId: target.appId,
959
+ brand: target.brand,
960
+ cliId: 'traex',
961
+ workingDir: input.workingDir,
962
+ registrationMode: 'compat',
963
+ recoveryOfJobId: input.predecessorJobId,
964
+ recoveryAttempt: (latest?.recoveryAttempt ?? 0) + 1,
965
+ ...(input.requireCriticalScopesBeforeActivation === true
966
+ ? { criticalScopeActivationRequired: true }
967
+ : {}),
968
+ ...(latest ? { previousRecoveryJobId: latest.id } : {}),
969
+ });
970
+ try {
971
+ this.persistPermissionRecoveryJobs();
972
+ }
973
+ catch {
974
+ this.jobs.delete(id);
975
+ this.permissionRecoveryStateError = 'permission recovery intent could not be persisted';
976
+ return { ok: false, error: 'permission_recovery_state_unavailable' };
977
+ }
978
+ const done = this.runPermissionRecovery(id, target.appId, target.appSecret, target.brand, target.index, input.requireCriticalScopesBeforeActivation === true).catch(err => {
979
+ this.patch(id, {
980
+ status: 'failed',
981
+ error: 'permission_recovery_failed',
982
+ message: err instanceof Error ? err.message : String(err),
983
+ });
984
+ });
985
+ return { ok: true, job: { id, done } };
986
+ }
987
+ /**
988
+ * Finish only the exact activation-pending tail of an already-scanned
989
+ * managed initial/recovery job. This never opens SSO, creates an App, or
990
+ * issues another owner QR. botmux itself re-reads stable scopes, removes
991
+ * activationPending, and requires an idempotent exact-daemon start ACK
992
+ * before returning success.
993
+ */
994
+ async completeScopePropagation(input) {
995
+ await this.activationStartupReconciliation;
996
+ if (this.permissionRecoveryStateError) {
997
+ return { ok: false, error: 'permission_recovery_state_unavailable' };
998
+ }
999
+ const job = this.jobs.get(input.jobId);
1000
+ if (!job)
1001
+ return { ok: false, error: 'permission_recovery_target_missing' };
1002
+ const exactTarget = (job.workingDir === input.workingDir
1003
+ && job.appId === input.expectedAppId
1004
+ && job.criticalScopeActivationRequired === true);
1005
+ if (!exactTarget) {
1006
+ return { ok: false, error: 'permission_recovery_target_invalid' };
1007
+ }
1008
+ if (job.status === 'completed'
1009
+ && job.activationPending !== true
1010
+ && job.liveStarted === true
1011
+ && job.permission?.ok === true
1012
+ && job.permission.eventMode === 4
1013
+ && job.permission.verifiedEventCount === MANAGED_VERIFIED_EVENT_COUNT
1014
+ && typeof job.permission.versionId === 'string'
1015
+ && job.permission.versionId.trim().length > 0) {
1016
+ return { ok: true };
1017
+ }
1018
+ const exactAck = (job.permission?.eventMode === 4
1019
+ && job.permission.verifiedEventCount === MANAGED_VERIFIED_EVENT_COUNT
1020
+ && typeof job.permission.versionId === 'string'
1021
+ && job.permission.versionId.trim().length > 0);
1022
+ const initialPropagationPending = (job.status === 'completed'
1023
+ && job.activationPending === true
1024
+ && job.liveStarted !== true
1025
+ && job.permission?.ok === true
1026
+ && job.activationCommitting !== true);
1027
+ const recoveryPropagationPending = (job.status === 'failed'
1028
+ && job.error === 'permission_recovery_failed'
1029
+ && job.permission?.ok === false
1030
+ && job.permission.reason === 'scope_mapping_failed');
1031
+ if (job.platformQrScanConfirmedAt === undefined
1032
+ || !exactAck
1033
+ || (!initialPropagationPending && !recoveryPropagationPending)) {
1034
+ return { ok: false, error: 'permission_recovery_target_invalid' };
1035
+ }
1036
+ const existingFlight = this.scopePropagationFlights.get(job.id);
1037
+ if (existingFlight)
1038
+ return existingFlight;
1039
+ const flight = this.finishScopePropagation(job, input);
1040
+ this.scopePropagationFlights.set(job.id, flight);
1041
+ try {
1042
+ return await flight;
1043
+ }
1044
+ finally {
1045
+ if (this.scopePropagationFlights.get(job.id) === flight) {
1046
+ this.scopePropagationFlights.delete(job.id);
1047
+ }
1048
+ }
1049
+ }
1050
+ async finishScopePropagation(job, input) {
1051
+ const managedPermission = job.permission;
1052
+ if (!managedPermission) {
1053
+ return { ok: false, error: 'permission_recovery_target_invalid' };
1054
+ }
1055
+ const candidates = readBotsJsonOrEmpty(this.opts.botsJsonPath).flatMap((bot, index) => {
1056
+ if (!bot
1057
+ || typeof bot !== 'object'
1058
+ || bot.cliId !== 'traex'
1059
+ || bot.defaultWorkingDir !== input.workingDir
1060
+ || bot.larkAppId !== input.expectedAppId
1061
+ || typeof bot.larkAppSecret !== 'string'
1062
+ || !bot.larkAppSecret
1063
+ || !Array.isArray(bot.allowedUsers)
1064
+ || !hasOwnerEntry(bot.allowedUsers)) {
1065
+ return [];
1066
+ }
1067
+ return [{
1068
+ index,
1069
+ appSecret: bot.larkAppSecret,
1070
+ brand: bot.brand === 'lark' ? 'lark' : 'feishu',
1071
+ }];
1072
+ });
1073
+ if (candidates.length === 0) {
1074
+ return { ok: false, error: 'permission_recovery_target_missing' };
1075
+ }
1076
+ if (candidates.length !== 1) {
1077
+ return { ok: false, error: 'permission_recovery_target_ambiguous' };
1078
+ }
1079
+ const target = candidates[0];
1080
+ const stable = await this.stableCriticalScopeReadback(job.id, input.expectedAppId, target.appSecret, target.brand);
1081
+ if (!stable.ready) {
1082
+ return { ok: false, error: 'permission_recovery_scopes_pending' };
1083
+ }
1084
+ const permissionAck = {
1085
+ ok: true,
1086
+ sessionFile: 'permission-recovery-ledger',
1087
+ sessionSource: 'qr_login',
1088
+ cookieCount: 0,
1089
+ scopeCount: 9,
1090
+ skippedScopeCount: 0,
1091
+ subscribedEventCount: MANAGED_VERIFIED_EVENT_COUNT,
1092
+ missingVcEvents: [],
1093
+ eventModeReady: true,
1094
+ eventMode: managedPermission.eventMode,
1095
+ verifiedEventCount: managedPermission.verifiedEventCount,
1096
+ versionId: managedPermission.versionId,
1097
+ };
1098
+ this.finalizePermissions(job.id, input.expectedAppId, target.brand, target.index, permissionAck, 'completed');
1099
+ try {
1100
+ await this.activateRecoveredBot(job.id, input.expectedAppId, target.index, true);
1101
+ }
1102
+ catch {
1103
+ return { ok: false, error: 'permission_recovery_activation_failed' };
1104
+ }
1105
+ this.patch(job.id, {
1106
+ error: undefined,
1107
+ message: undefined,
1108
+ remainingSteps: undefined,
1109
+ });
1110
+ return { ok: true };
1111
+ }
169
1112
  get(id) {
170
1113
  const job = this.jobs.get(id);
171
1114
  return job ? { ...job } : undefined;
@@ -183,7 +1126,23 @@ export class BotOnboardingManager {
183
1126
  const current = this.jobs.get(id);
184
1127
  if (!current)
185
1128
  return;
186
- this.jobs.set(id, { ...current, ...patch, updatedAt: this.now() });
1129
+ const next = { ...current, ...patch, updatedAt: this.now() };
1130
+ this.jobs.set(id, next);
1131
+ if (current.recoveryOfJobId
1132
+ || next.recoveryOfJobId
1133
+ || managedActivationStateForSnapshot(current) !== undefined
1134
+ || managedActivationStateForSnapshot(next) !== undefined) {
1135
+ this.savePermissionRecoveryJobsBestEffort();
1136
+ }
1137
+ }
1138
+ confirmPlatformQrScan(id, confirmedAt) {
1139
+ const current = this.jobs.get(id);
1140
+ if (!current || current.platformQrScanConfirmedAt !== undefined)
1141
+ return;
1142
+ if (!Number.isInteger(confirmedAt) || confirmedAt <= 0) {
1143
+ throw new Error('platform_qr_scan_confirmation_invalid');
1144
+ }
1145
+ this.patch(id, { platformQrScanConfirmedAt: confirmedAt });
187
1146
  }
188
1147
  async run(id, input = {}) {
189
1148
  // Freeze the resolved name before any asynchronous work. Later bot list
@@ -191,6 +1150,9 @@ export class BotOnboardingManager {
191
1150
  const appName = resolveSetupAppName(input.appName, readBotsJsonOrEmpty(this.opts.botsJsonPath).length);
192
1151
  this.patch(id, {
193
1152
  registrationMode: input.registrationMode ?? 'web',
1153
+ ...(input.requireCriticalScopesBeforeActivation
1154
+ ? { criticalScopeActivationRequired: true }
1155
+ : {}),
194
1156
  // SDK/Lark compatibility mode cannot apply a custom application name, so
195
1157
  // do not claim that the resolved Feishu name was used.
196
1158
  ...(input.registrationMode === 'compat' ? {} : { appName }),
@@ -291,7 +1253,18 @@ export class BotOnboardingManager {
291
1253
  cliId,
292
1254
  workingDir,
293
1255
  registrationMode: input.registrationMode ?? 'web',
1256
+ requireVerifiedEvents: input.requireCriticalScopesBeforeActivation === true,
294
1257
  });
1258
+ // Every managed initial App first reaches one durable activation-pending
1259
+ // snapshot. Even when scopes are immediately stable, starting here would
1260
+ // leave a crash window between the exact PM2 ACK and persistence of the
1261
+ // owner-session event/version ACK. The Web helper always completes this
1262
+ // same job through the singleflight scope-propagation endpoint.
1263
+ const activationPending = input.requireCriticalScopesBeforeActivation === true;
1264
+ if (activationPending) {
1265
+ bot.activationPending = true;
1266
+ this.patch(id, { activationPending: true });
1267
+ }
295
1268
  // 关键顺序:先确认 owner, 再决定是否落盘 + 终态。completed 必须意味着「bots.json
296
1269
  // 里这个 bot 带着至少一个 owner」, 绝不产出空 allowedUsers 的可启动 bot。
297
1270
  let ownerEntry;
@@ -310,7 +1283,9 @@ export class BotOnboardingManager {
310
1283
  }
311
1284
  if (ownerEntry) {
312
1285
  const addedBotIndex = this.persistBot({ ...bot, allowedUsers: [ownerEntry] });
313
- await this.runLiveStart(id, result.appId);
1286
+ if (!activationPending) {
1287
+ await this.runLiveStart(id, result.appId);
1288
+ }
314
1289
  this.finalizePermissions(id, result.appId, result.brand, addedBotIndex, auto, 'completed');
315
1290
  }
316
1291
  else {
@@ -392,8 +1367,14 @@ export class BotOnboardingManager {
392
1367
  // 校验通过才落盘:此刻 bot 第一次进入 bots.json, 且带着非空 allowedUsers。
393
1368
  const addedBotIndex = this.persistBot({ ...pending, allowedUsers: entries });
394
1369
  this.pendingBots.delete(id);
395
- await this.runLiveStart(id, appId);
1370
+ if (pending.activationPending === true) {
1371
+ this.patch(id, { activationPending: true });
1372
+ }
1373
+ else {
1374
+ await this.runLiveStart(id, appId);
1375
+ }
396
1376
  this.patch(id, { status: 'completed', addedBotIndex });
1377
+ this.requireDurableManagedInitialJobs();
397
1378
  this.savePendingJobs();
398
1379
  return { ok: true };
399
1380
  }
@@ -409,8 +1390,27 @@ export class BotOnboardingManager {
409
1390
  cliId: meta.cliId,
410
1391
  workingDir: meta.workingDir,
411
1392
  });
1393
+ const callbacks = {
1394
+ onQrCode: (info) => {
1395
+ this.patch(id, {
1396
+ status: 'waiting_for_platform_scan',
1397
+ platformQrDataUrl: this.renderQrDataUrl(info.qrPayload),
1398
+ });
1399
+ },
1400
+ onQrScanConfirmed: (info) => {
1401
+ this.confirmPlatformQrScan(id, info.confirmedAt);
1402
+ },
1403
+ onStatus: (msg) => {
1404
+ // onStatus follows onQrCode while polling; keep the QR visible.
1405
+ this.patch(id, { permissionStatusMsg: msg });
1406
+ },
1407
+ };
412
1408
  try {
413
- return await this.automateOpenPlatform({
1409
+ if (meta.registrationMode === 'compat') {
1410
+ const sessionFilePath = join(dirname(this.opts.botsJsonPath), 'onboarding-sessions', `${id}.json`);
1411
+ return await this.runOwnerPermissionAutomation(id, appId, brand, callbacks, sessionFilePath, meta.requireVerifiedEvents);
1412
+ }
1413
+ const first = await this.automateOpenPlatform({
414
1414
  appId,
415
1415
  brand,
416
1416
  // The Feishu primary path must never surprise the user with a second
@@ -418,19 +1418,10 @@ export class BotOnboardingManager {
418
1418
  // manual recovery. Only explicit compatibility mode may scan again.
419
1419
  disableQrLogin: meta.registrationMode === 'web',
420
1420
  disableBytedcliFallback: meta.registrationMode === 'web',
421
- onQrCode: info => {
422
- this.patch(id, {
423
- status: 'waiting_for_platform_scan',
424
- platformQrDataUrl: this.renderQrDataUrl(info.qrPayload),
425
- });
426
- },
427
- onStatus: msg => {
428
- // 只更新进度文案, 不动 status——onStatus 在 onQrCode 之后的轮询里就会
429
- // 触发 ('等待飞书扫码'), 若把 status 拨回 configuring_permissions 会瞬间
430
- // 把刚弹出的第二个二维码 (waiting_for_platform_scan) 盖掉.
431
- this.patch(id, { permissionStatusMsg: msg });
432
- },
1421
+ requireVerifiedEvents: meta.requireVerifiedEvents,
1422
+ ...callbacks,
433
1423
  });
1424
+ return first;
434
1425
  }
435
1426
  catch (err) {
436
1427
  // automation 不应抛 (内部已结构化返回), 兜底当作失败 → 手动步骤.
@@ -441,11 +1432,88 @@ export class BotOnboardingManager {
441
1432
  };
442
1433
  }
443
1434
  }
1435
+ async runPermissionRecovery(id, appId, appSecret, brand, addedBotIndex, requireCriticalScopesBeforeActivation) {
1436
+ if (requireCriticalScopesBeforeActivation) {
1437
+ this.holdRecoveryActivation(id, appId, addedBotIndex);
1438
+ const stopped = await this.runLiveStop(id, appId);
1439
+ if (!stopped.attempted || stopped.ok !== true) {
1440
+ throw new Error(`permission_recovery_deactivation_not_acknowledged${stopped.message ? `: ${stopped.message}` : ''}`);
1441
+ }
1442
+ this.markManagedDeactivated(id, appId, addedBotIndex);
1443
+ }
1444
+ this.patch(id, { status: 'configuring_permissions' });
1445
+ const sessionFilePath = join(dirname(this.opts.botsJsonPath), 'onboarding-sessions', `${id}.json`);
1446
+ const auto = await this.runOwnerPermissionAutomation(id, appId, brand, {
1447
+ onQrCode: (info) => {
1448
+ this.patch(id, {
1449
+ status: 'waiting_for_platform_scan',
1450
+ platformQrDataUrl: this.renderQrDataUrl(info.qrPayload),
1451
+ });
1452
+ },
1453
+ onQrScanConfirmed: (info) => {
1454
+ this.confirmPlatformQrScan(id, info.confirmedAt);
1455
+ },
1456
+ onStatus: (msg) => this.patch(id, { permissionStatusMsg: msg }),
1457
+ }, sessionFilePath, requireCriticalScopesBeforeActivation);
1458
+ if (!auto.ok) {
1459
+ this.finalizePermissions(id, appId, brand, addedBotIndex, auto, 'failed', 'permission_recovery_failed');
1460
+ return;
1461
+ }
1462
+ if (requireCriticalScopesBeforeActivation && !hasExactManagedAutomationAck(auto)) {
1463
+ this.finalizePermissions(id, appId, brand, addedBotIndex, {
1464
+ ok: false,
1465
+ reason: 'event_verification_failed',
1466
+ message: '受管权限恢复缺少同一 owner 会话的事件与版本精确 ACK',
1467
+ }, 'failed', 'permission_recovery_failed');
1468
+ return;
1469
+ }
1470
+ if (requireCriticalScopesBeforeActivation
1471
+ && this.jobs.get(id)?.platformQrScanConfirmedAt === undefined) {
1472
+ throw new Error('platform_qr_scan_not_confirmed');
1473
+ }
1474
+ const stableScopeReadback = await this.stableCriticalScopeReadback(id, appId, appSecret, brand);
1475
+ const scopeReadback = stableScopeReadback.readback;
1476
+ if (!stableScopeReadback.ready) {
1477
+ const failed = {
1478
+ ok: false,
1479
+ reason: scopeReadback.ok ? 'scope_mapping_failed' : 'api_error',
1480
+ message: scopeReadback.ok
1481
+ ? scopeReadback.missingCritical.length > 0
1482
+ ? `关键权限回读仍缺失: ${scopeReadback.missingCritical.map(scope => scope.name).join(', ')}`
1483
+ : `9 项核心权限尚未连续稳定生效(需要连续 ${this.criticalScopeStableReads} 次)`
1484
+ : `关键权限回读失败: ${scopeReadback.message}`,
1485
+ eventMode: auto.eventMode,
1486
+ verifiedEventCount: auto.verifiedEventCount,
1487
+ versionId: auto.versionId,
1488
+ };
1489
+ this.finalizePermissions(id, appId, brand, addedBotIndex, failed, 'failed', 'permission_recovery_failed');
1490
+ return;
1491
+ }
1492
+ this.finalizePermissions(id, appId, brand, addedBotIndex, auto, 'completed');
1493
+ await this.activateRecoveredBot(id, appId, addedBotIndex);
1494
+ }
1495
+ async runOwnerPermissionAutomation(id, appId, brand, callbacks, sessionFilePath, requireVerifiedEvents) {
1496
+ try {
1497
+ return await this.automateOpenPlatform({
1498
+ appId,
1499
+ brand,
1500
+ sessionFilePath,
1501
+ forceQrLogin: true,
1502
+ disableQrLogin: false,
1503
+ disableBytedcliFallback: true,
1504
+ requireVerifiedEvents,
1505
+ ...callbacks,
1506
+ });
1507
+ }
1508
+ finally {
1509
+ rmSync(sessionFilePath, { force: true });
1510
+ }
1511
+ }
444
1512
  /**
445
1513
  * 统一落终态:completed (已落盘 + 有 owner) 或 needs_owner (尚未落盘、待用户手动填)。
446
1514
  * needs_owner 时 addedBotIndex 为 undefined——bot 还没进 bots.json, 没有行号。
447
1515
  */
448
- finalizePermissions(id, appId, brand, addedBotIndex, auto, status) {
1516
+ finalizePermissions(id, appId, brand, addedBotIndex, auto, status, error) {
449
1517
  const permission = auto.ok
450
1518
  ? {
451
1519
  ok: true,
@@ -453,15 +1521,26 @@ export class BotOnboardingManager {
453
1521
  skippedScopeCount: auto.skippedScopeCount,
454
1522
  versionId: auto.versionId,
455
1523
  scopeWarning: auto.scopeWarning,
1524
+ eventMode: auto.eventMode,
1525
+ verifiedEventCount: auto.verifiedEventCount,
456
1526
  }
457
- : { ok: false, reason: auto.reason, message: auto.message };
1527
+ : {
1528
+ ok: false,
1529
+ reason: auto.reason,
1530
+ message: auto.message,
1531
+ eventMode: auto.eventMode,
1532
+ verifiedEventCount: auto.verifiedEventCount,
1533
+ versionId: auto.versionId,
1534
+ };
458
1535
  this.patch(id, {
459
1536
  status,
460
1537
  ...(addedBotIndex !== undefined ? { addedBotIndex } : {}),
461
1538
  platformQrDataUrl: undefined,
462
1539
  permission,
1540
+ ...(error ? { error } : {}),
463
1541
  ...(auto.ok ? {} : { remainingSteps: buildRemainingSteps(appId, brand) }),
464
1542
  });
1543
+ this.requireDurableManagedInitialJobs();
465
1544
  }
466
1545
  }
467
1546
  /**