@fased/fased 0.1.36 → 0.1.38

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 (240) hide show
  1. package/config/capability-catalog.json +241 -0
  2. package/config/dependency-ownership.json +130 -0
  3. package/config/official-channel-catalog.json +39 -0
  4. package/dist/agents/tools/web-fetch-utils.js +2 -1
  5. package/dist/agents/tools/web-fetch-visibility.js +3 -1
  6. package/dist/brand.js +1 -1
  7. package/dist/browser/pw-session.js +2 -1
  8. package/dist/browser/pw-tools-core.state.js +3 -2
  9. package/dist/browser/runtime-dependency.js +27 -0
  10. package/dist/build-info.json +3 -3
  11. package/dist/canvas-host/a2ui/.bundle.hash +1 -1
  12. package/dist/capabilities/catalog.js +228 -0
  13. package/dist/cli/components-cli.js +105 -0
  14. package/dist/cli/hooks-cli.js +1 -1
  15. package/dist/cli/plugins-cli.js +3 -3
  16. package/dist/cli/program/register.subclis.js +9 -0
  17. package/dist/cli/update-cli/update-command.js +1 -1
  18. package/dist/commands/doctor-capabilities.js +26 -0
  19. package/dist/commands/doctor.js +2 -0
  20. package/dist/control-ui/assets/{app-DXYMrRJ9.js → app-JBrTSsNu.js} +1480 -1465
  21. package/dist/control-ui/assets/{index-iGQkJE0l.js → index-H-2DWAnH.js} +3 -3
  22. package/dist/control-ui/index.html +1 -1
  23. package/dist/gateway/method-scopes.js +1 -0
  24. package/dist/gateway/server-methods/plugins-marketplace.js +1 -1
  25. package/dist/gateway/server-methods/services.js +8 -0
  26. package/dist/gateway/server-methods-list.js +1 -0
  27. package/dist/infra/optional-runtime-dependency.js +46 -0
  28. package/dist/media/image-ops.js +6 -1
  29. package/dist/media/input-files.js +11 -2
  30. package/dist/media/mime.js +6 -1
  31. package/dist/memory/sqlite-vec.js +7 -1
  32. package/dist/plugin-sdk/{accounts-VqAX3HJ0.js → accounts-BwkH5bTD.js} +1 -1
  33. package/dist/plugin-sdk/{accounts-Zgt0NP54.js → accounts-RT6y7n3H.js} +1 -1
  34. package/dist/plugin-sdk/{actions-lB3PCwsz.js → actions-BrU_JMqM.js} +2 -2
  35. package/dist/plugin-sdk/{actions-BI5QViLk.js → actions-CUdVHzeJ.js} +2 -2
  36. package/dist/plugin-sdk/{active-listener-DKZiSaRA.js → active-listener-CE8t5g6l.js} +1 -1
  37. package/dist/plugin-sdk/{active-listener-D6_pdTMC.js → active-listener-Dx1SEGDn.js} +1 -1
  38. package/dist/plugin-sdk/{audio-preflight-Dx8J7XhV.js → audio-preflight-BPK8t0tI.js} +22 -22
  39. package/dist/plugin-sdk/{audio-preflight-CeRstduc.js → audio-preflight-kCa_KGRH.js} +22 -22
  40. package/dist/plugin-sdk/{audit-BImNOQvk.js → audit-DxB2Kml5.js} +11 -11
  41. package/dist/plugin-sdk/{audit-CKWkq0Un.js → audit-sSeRDwJb.js} +11 -11
  42. package/dist/plugin-sdk/channel-plugin-common.js +3 -2
  43. package/dist/plugin-sdk/{chunk-skAJZWCT.js → chunk-KCxbJLsV.js} +1 -1
  44. package/dist/plugin-sdk/{chunk-Cjw4712g.js → chunk-M2d7r-Zt.js} +1 -1
  45. package/dist/plugin-sdk/{chunk-items-Df5811D0.js → chunk-items-DGHUeFuI.js} +1 -1
  46. package/dist/plugin-sdk/command-status.js +22 -22
  47. package/dist/plugin-sdk/{commands-registry-5J619bQ2.js → commands-registry-CdT-nl7v.js} +2 -2
  48. package/dist/plugin-sdk/{commands-registry-CoLAT03J.js → commands-registry-DoWdYAJd.js} +2 -2
  49. package/dist/plugin-sdk/{common-CSTxewqY.js → common-Cpw-vuuM.js} +1 -1
  50. package/dist/plugin-sdk/{common-BiF0sTnY.js → common-DJWrErdE.js} +1 -1
  51. package/dist/plugin-sdk/{config-BxVjLTIv.js → config-C0qggNKQ.js} +7 -7
  52. package/dist/plugin-sdk/{config-TFokWwvO.js → config-CgwTaper.js} +45 -2
  53. package/dist/plugin-sdk/{config-DXlISPlx.js → config-DtqAN2L3.js} +45 -2
  54. package/dist/plugin-sdk/discord.js +2 -1
  55. package/dist/plugin-sdk/{fetch-guard-DPMynms8.js → fetch-guard-BLdujDnt.js} +1 -1
  56. package/dist/plugin-sdk/{fetch-guard-COLmW2TM.js → fetch-guard-DTKgMN9f.js} +1 -1
  57. package/dist/plugin-sdk/{handle-action-C-UndB8E.js → handle-action-B61D85yo.js} +12 -12
  58. package/dist/plugin-sdk/{handle-action-hhufKxmU.js → handle-action-CjcxGzQD.js} +12 -12
  59. package/dist/plugin-sdk/index.js +22 -22
  60. package/dist/plugin-sdk/{login-DL1oBBfL.js → login-CB_nCjX3.js} +3 -3
  61. package/dist/plugin-sdk/{login-Dnxas1dI.js → login-CzUuDd1v.js} +3 -3
  62. package/dist/plugin-sdk/{login-qr-CVQsGWKq.js → login-qr-D7MEpFza.js} +3 -3
  63. package/dist/plugin-sdk/{login-qr-B343JNXJ.js → login-qr-DgxfBFVN.js} +3 -3
  64. package/dist/plugin-sdk/{manager-BytwRIfK.js → manager-9see0IX5.js} +8 -3
  65. package/dist/plugin-sdk/{manager-BZjd_PCt.js → manager-D2kZ0Cps.js} +8 -3
  66. package/dist/plugin-sdk/{markdown-tables-krKiU4cT.js → markdown-tables-CCSD52rA.js} +1 -1
  67. package/dist/plugin-sdk/{markdown-tables-9rJDQ1X-.js → markdown-tables-DWUNvLrc.js} +1 -1
  68. package/dist/plugin-sdk/{media-D7ZkWU-A.js → media-C7YT4DeU.js} +13 -4
  69. package/dist/plugin-sdk/{media-C6C2c9a4.js → media-Cxw-xLGU.js} +13 -4
  70. package/dist/plugin-sdk/{monitor-BsLMfn3U.js → monitor-6lkdOqhQ.js} +26 -26
  71. package/dist/plugin-sdk/{monitor-BCxkQBoh.js → monitor-C6Ohw9BA.js} +26 -26
  72. package/dist/plugin-sdk/{monitor-BzDW50H6.js → monitor-o20KTHIi.js} +29 -29
  73. package/dist/plugin-sdk/{monitor-CVQNDfhs.js → monitor-rOs3mUol.js} +27 -27
  74. package/dist/plugin-sdk/{openresponses-http-BB67Z4QM.js → openresponses-http-BJONnUdy.js} +22 -22
  75. package/dist/plugin-sdk/{openresponses-http-NE76GGhj.js → openresponses-http-DkWqYznu.js} +22 -22
  76. package/dist/plugin-sdk/{ssrf-u0Ul7okY.js → optional-runtime-dependency-szS49voX.js} +46 -2
  77. package/dist/plugin-sdk/{outbound-PGNWU6T-.js → outbound-BCxOxdi-.js} +6 -6
  78. package/dist/plugin-sdk/{outbound-BHWJD9q0.js → outbound-JIwQbpRY.js} +6 -6
  79. package/dist/plugin-sdk/{outbound-attachment-BVzfhqnk.js → outbound-attachment-C-jSFaEB.js} +1 -1
  80. package/dist/plugin-sdk/{outbound-attachment-ClMvNW17.js → outbound-attachment-D6tj5Y5H.js} +1 -1
  81. package/dist/plugin-sdk/{pi-embedded-runner-DQ6UPx6-.js → pi-embedded-runner-DqDy0MWN.js} +72 -60
  82. package/dist/plugin-sdk/{pi-model-discovery-D51L5E9I.js → pi-model-discovery-DadlvsZW.js} +1 -1
  83. package/dist/plugin-sdk/{pi-model-discovery-runtime-DxoFm4Vd.js → pi-model-discovery-runtime-C5ssE6EW.js} +1 -1
  84. package/dist/plugin-sdk/{pi-model-discovery-runtime-DRuh4PNV.js → pi-model-discovery-runtime-DAaOPt64.js} +2 -2
  85. package/dist/plugin-sdk/{pi-model-discovery-runtime-BTWgkx4x.js → pi-model-discovery-runtime-gd77b0UF.js} +2 -2
  86. package/dist/plugin-sdk/{pi-model-discovery-CkH5l9X5.js → pi-model-discovery-yifTIDIn.js} +1 -1
  87. package/dist/plugin-sdk/{pw-ai-DZs6uYRd.js → pw-ai-Boor5vYw.js} +6 -5
  88. package/dist/plugin-sdk/{pw-ai-Cn-EUPZK.js → pw-ai-sH1Pi-Fh.js} +6 -5
  89. package/dist/plugin-sdk/{registry-B4HvWzob.js → registry-DNkJsNsz.js} +4 -3
  90. package/dist/plugin-sdk/{replies-jDMUBsNO.js → replies-fvzN5wnU.js} +2 -2
  91. package/dist/plugin-sdk/{replies-B_LW9c9f.js → replies-zebgZCHX.js} +2 -2
  92. package/dist/plugin-sdk/{resolve-route-_YZHbS-z.js → resolve-route-CNhTJG_2.js} +1 -1
  93. package/dist/plugin-sdk/{resolve-route-DlHzggnG.js → resolve-route-DO0WR4nB.js} +1 -1
  94. package/dist/plugin-sdk/{resolve-utils-zBhVX3Vx.js → resolve-utils-bixHYDBj.js} +1 -1
  95. package/dist/plugin-sdk/{retry-policy-DGLs5ebP.js → retry-policy-BLYEgIwP.js} +1 -1
  96. package/dist/plugin-sdk/{retry-policy-DaodShGa.js → retry-policy-sM01QZT2.js} +1 -1
  97. package/dist/plugin-sdk/{runtime-Dd0sswuW.js → runtime-BPyuAmka.js} +4 -3
  98. package/dist/plugin-sdk/{paths-CBCpJ5bu.js → runtime-dependency-Csswyv5r.js} +27 -3
  99. package/dist/plugin-sdk/{paths-DW-U2r7Y.js → runtime-dependency-Deg_sw2k.js} +36 -12
  100. package/dist/plugin-sdk/sat-runtime.js +1 -0
  101. package/dist/plugin-sdk/{send-8remZhsH.js → send-B09HRfHN.js} +6 -6
  102. package/dist/plugin-sdk/{send-DaaAscWj.js → send-BBYcgXA4.js} +5 -5
  103. package/dist/plugin-sdk/{send-DFTB0cnx.js → send-BQXQA81e.js} +6 -6
  104. package/dist/plugin-sdk/{send-CtMVvMJs.js → send-BzFdvD_i.js} +6 -6
  105. package/dist/plugin-sdk/{send-Di9irjS-.js → send-C3u-F_8s.js} +6 -6
  106. package/dist/plugin-sdk/{send-Dl88ZKq2.js → send-D3836_UN.js} +6 -6
  107. package/dist/plugin-sdk/{send-ifgZCHVB.js → send-D7tr-EsH.js} +5 -5
  108. package/dist/plugin-sdk/{send-DvUpYh0d.js → send-DRGzRdI3.js} +6 -6
  109. package/dist/plugin-sdk/{send-D2jlPeKH.js → send-DfkPKz_0.js} +4 -4
  110. package/dist/plugin-sdk/{send-QSt6nbny.js → send-DsxFXrCO.js} +4 -4
  111. package/dist/plugin-sdk/{session-CknrceVU.js → session-DG_bYisw.js} +2 -2
  112. package/dist/plugin-sdk/{session-CyRTcJ8N.js → session-EIStzK0E.js} +2 -2
  113. package/dist/plugin-sdk/{skill-commands-DHxkTA0s.js → skill-commands-CkgGmWBS.js} +2 -2
  114. package/dist/plugin-sdk/{skill-commands-CLXAG5sU.js → skill-commands-hyZA0LE3.js} +2 -2
  115. package/dist/plugin-sdk/{slack-Br9c6Yzs.js → slack-BErIh0uj.js} +34 -34
  116. package/dist/plugin-sdk/{slack-BpJXkwxF.js → slack-CGTAN4UD.js} +34 -34
  117. package/dist/plugin-sdk/{slack-actions-8tLGt1Ht.js → slack-actions-BQawWGTJ.js} +11 -11
  118. package/dist/plugin-sdk/{slack-actions-CDSSRo8Y.js → slack-actions-DDMszx56.js} +11 -11
  119. package/dist/plugin-sdk/slack.js +2 -1
  120. package/dist/plugin-sdk/src/brand.d.ts +2 -2
  121. package/dist/plugin-sdk/src/browser/runtime-dependency.d.ts +3 -0
  122. package/dist/plugin-sdk/src/infra/optional-runtime-dependency.d.ts +6 -0
  123. package/dist/plugin-sdk/{ssrf-fG4UHKBw.js → ssrf-Cce6LubA.js} +41 -7
  124. package/dist/plugin-sdk/{status-DoxnzyMC.js → status-CCGFb0f2.js} +76 -64
  125. package/dist/plugin-sdk/{sticker-cache-zmJ39ByC.js → sticker-cache-CKAkkCoU.js} +3 -3
  126. package/dist/plugin-sdk/{sticker-cache-D7WcMYsF.js → sticker-cache-wxl1Me2i.js} +3 -3
  127. package/dist/plugin-sdk/{tables-CkxblF0u.js → tables-BD7H5ug-.js} +1 -1
  128. package/dist/plugin-sdk/{tables-RBtf8G4P.js → tables-CJE8kB35.js} +1 -1
  129. package/dist/plugin-sdk/{telegram-actions-DAeYXSLk.js → telegram-actions-BRUNEXMr.js} +25 -25
  130. package/dist/plugin-sdk/{telegram-actions-BDeS3ocd.js → telegram-actions-r86ntIKR.js} +25 -25
  131. package/dist/plugin-sdk/telegram.js +2 -1
  132. package/dist/plugin-sdk/{thread-bindings.lifecycle-DsB94WbE.js → thread-bindings.lifecycle-Djem8FWF.js} +2 -2
  133. package/dist/plugin-sdk/{thread-bindings.lifecycle-DMhV3TsR.js → thread-bindings.lifecycle-gmHv3hc7.js} +2 -2
  134. package/dist/plugin-sdk/{thread-bindings.state-DYWpy7hJ.js → thread-bindings.state-CNT2vxm3.js} +2 -2
  135. package/dist/plugin-sdk/{thread-bindings.state-DG5cBaRn.js → thread-bindings.state-D8kN2X6H.js} +2 -2
  136. package/dist/plugin-sdk/{web-DJwacWQo.js → web-02I0vmdu.js} +26 -26
  137. package/dist/plugin-sdk/{web-IJ4NoGPJ.js → web-CXz56wyu.js} +26 -26
  138. package/dist/plugin-sdk/{whatsapp-actions-DAY8hWpD.js → whatsapp-actions-68Mfs2dJ.js} +12 -12
  139. package/dist/plugin-sdk/{whatsapp-actions-6opfo-oB.js → whatsapp-actions-DRqtzvTO.js} +12 -12
  140. package/dist/plugin-sdk/whatsapp.js +2 -1
  141. package/dist/plugin-sdk/{wsl-B2euLEc2.js → wsl-DcM2PgSS.js} +3 -3
  142. package/dist/plugin-sdk/{wsl-B8eyviuv.js → wsl-PXrdWitE.js} +3 -3
  143. package/dist/plugins/update.js +1 -1
  144. package/dist/tts/tts-core.js +6 -1
  145. package/dist/wizard/onboarding.finalize.js +9 -0
  146. package/extensions/acpx/package.json +1 -1
  147. package/extensions/bluebubbles/package.json +1 -1
  148. package/extensions/copilot-proxy/package.json +1 -1
  149. package/extensions/diagnostics-otel/package.json +1 -1
  150. package/extensions/fased-federation/package.json +1 -1
  151. package/extensions/google-gemini-cli-auth/package.json +1 -1
  152. package/extensions/imessage/package.json +1 -1
  153. package/extensions/irc/package.json +1 -1
  154. package/extensions/line/package.json +1 -1
  155. package/extensions/llm-task/package.json +1 -1
  156. package/extensions/lobster/package.json +1 -1
  157. package/extensions/matrix/CHANGELOG.md +12 -0
  158. package/extensions/matrix/package.json +1 -1
  159. package/extensions/mattermost/package.json +1 -1
  160. package/extensions/memory-core/package.json +2 -2
  161. package/extensions/memory-lancedb/package.json +1 -1
  162. package/extensions/minimax-portal-auth/package.json +1 -1
  163. package/extensions/msteams/CHANGELOG.md +12 -0
  164. package/extensions/msteams/package.json +1 -1
  165. package/extensions/nextcloud-talk/package.json +1 -1
  166. package/extensions/nostr/CHANGELOG.md +12 -0
  167. package/extensions/nostr/package.json +1 -1
  168. package/extensions/open-prose/package.json +1 -1
  169. package/extensions/sat-mining/package.json +1 -1
  170. package/extensions/signal/package.json +1 -1
  171. package/extensions/synology-chat/package.json +1 -1
  172. package/extensions/tlon/package.json +1 -1
  173. package/extensions/twitch/CHANGELOG.md +12 -0
  174. package/extensions/twitch/package.json +1 -1
  175. package/extensions/voice-call/CHANGELOG.md +12 -0
  176. package/extensions/voice-call/package.json +1 -1
  177. package/extensions/zalo/CHANGELOG.md +12 -0
  178. package/extensions/zalo/package.json +1 -1
  179. package/extensions/zalouser/CHANGELOG.md +12 -0
  180. package/extensions/zalouser/package.json +1 -1
  181. package/package.json +22 -15
  182. package/extensions/feishu/fased.plugin.json +0 -10
  183. package/extensions/feishu/index.ts +0 -63
  184. package/extensions/feishu/package.json +0 -33
  185. package/extensions/feishu/skills/feishu-doc/SKILL.md +0 -211
  186. package/extensions/feishu/skills/feishu-doc/references/block-types.md +0 -103
  187. package/extensions/feishu/skills/feishu-drive/SKILL.md +0 -97
  188. package/extensions/feishu/skills/feishu-perm/SKILL.md +0 -119
  189. package/extensions/feishu/skills/feishu-wiki/SKILL.md +0 -111
  190. package/extensions/feishu/src/accounts.ts +0 -159
  191. package/extensions/feishu/src/bitable.ts +0 -713
  192. package/extensions/feishu/src/bot.ts +0 -979
  193. package/extensions/feishu/src/channel.ts +0 -359
  194. package/extensions/feishu/src/client.ts +0 -118
  195. package/extensions/feishu/src/config-schema.ts +0 -226
  196. package/extensions/feishu/src/dedup.ts +0 -54
  197. package/extensions/feishu/src/directory.ts +0 -177
  198. package/extensions/feishu/src/doc-schema.ts +0 -122
  199. package/extensions/feishu/src/docx.ts +0 -998
  200. package/extensions/feishu/src/drive-schema.ts +0 -46
  201. package/extensions/feishu/src/drive.ts +0 -232
  202. package/extensions/feishu/src/dynamic-agent.ts +0 -131
  203. package/extensions/feishu/src/external-keys.ts +0 -19
  204. package/extensions/feishu/src/media.ts +0 -447
  205. package/extensions/feishu/src/mention.ts +0 -133
  206. package/extensions/feishu/src/monitor.ts +0 -434
  207. package/extensions/feishu/src/onboarding.ts +0 -430
  208. package/extensions/feishu/src/outbound.ts +0 -56
  209. package/extensions/feishu/src/perm-schema.ts +0 -52
  210. package/extensions/feishu/src/perm.ts +0 -180
  211. package/extensions/feishu/src/policy.ts +0 -116
  212. package/extensions/feishu/src/probe.ts +0 -44
  213. package/extensions/feishu/src/reactions.ts +0 -160
  214. package/extensions/feishu/src/reply-dispatcher.ts +0 -239
  215. package/extensions/feishu/src/runtime.ts +0 -14
  216. package/extensions/feishu/src/send-result.ts +0 -29
  217. package/extensions/feishu/src/send-target.ts +0 -25
  218. package/extensions/feishu/src/send.ts +0 -313
  219. package/extensions/feishu/src/streaming-card.ts +0 -218
  220. package/extensions/feishu/src/targets.ts +0 -78
  221. package/extensions/feishu/src/tool-account.ts +0 -58
  222. package/extensions/feishu/src/tools-config.ts +0 -21
  223. package/extensions/feishu/src/types.ts +0 -81
  224. package/extensions/feishu/src/typing.ts +0 -80
  225. package/extensions/feishu/src/wiki-schema.ts +0 -55
  226. package/extensions/feishu/src/wiki.ts +0 -237
  227. package/extensions/googlechat/fased.plugin.json +0 -9
  228. package/extensions/googlechat/index.ts +0 -19
  229. package/extensions/googlechat/package.json +0 -37
  230. package/extensions/googlechat/src/accounts.ts +0 -147
  231. package/extensions/googlechat/src/actions.ts +0 -176
  232. package/extensions/googlechat/src/api.ts +0 -282
  233. package/extensions/googlechat/src/auth.ts +0 -137
  234. package/extensions/googlechat/src/channel.ts +0 -591
  235. package/extensions/googlechat/src/monitor.ts +0 -996
  236. package/extensions/googlechat/src/onboarding.ts +0 -344
  237. package/extensions/googlechat/src/runtime.ts +0 -14
  238. package/extensions/googlechat/src/targets.ts +0 -65
  239. package/extensions/googlechat/src/types.config.ts +0 -3
  240. package/extensions/googlechat/src/types.ts +0 -73
@@ -1,979 +0,0 @@
1
- import type { FasedAgentConfig, RuntimeEnv } from "fased/plugin-sdk";
2
- import {
3
- buildAgentMediaPayload,
4
- buildPendingHistoryContextFromMap,
5
- clearHistoryEntriesIfEnabled,
6
- createScopedPairingAccess,
7
- DEFAULT_GROUP_HISTORY_LIMIT,
8
- type HistoryEntry,
9
- recordPendingHistoryEntryIfEnabled,
10
- resolveOpenProviderRuntimeGroupPolicy,
11
- resolveDefaultGroupPolicy,
12
- warnMissingProviderGroupPolicyFallbackOnce,
13
- } from "fased/plugin-sdk";
14
- import { resolveFeishuAccount } from "./accounts.js";
15
- import { createFeishuClient } from "./client.js";
16
- import { tryRecordMessagePersistent } from "./dedup.js";
17
- import { maybeCreateDynamicAgent } from "./dynamic-agent.js";
18
- import { normalizeFeishuExternalKey } from "./external-keys.js";
19
- import { downloadMessageResourceFeishu } from "./media.js";
20
- import {
21
- escapeRegExp,
22
- extractMentionTargets,
23
- extractMessageBody,
24
- isMentionForwardRequest,
25
- } from "./mention.js";
26
- import {
27
- resolveFeishuGroupConfig,
28
- resolveFeishuReplyPolicy,
29
- resolveFeishuAllowlistMatch,
30
- isFeishuGroupAllowed,
31
- } from "./policy.js";
32
- import { createFeishuReplyDispatcher } from "./reply-dispatcher.js";
33
- import { getFeishuRuntime } from "./runtime.js";
34
- import { getMessageFeishu, sendMessageFeishu } from "./send.js";
35
- import type { FeishuMessageContext, FeishuMediaInfo, ResolvedFeishuAccount } from "./types.js";
36
- import type { DynamicAgentCreationConfig } from "./types.js";
37
-
38
- // --- Permission error extraction ---
39
- // Extract permission grant URL from Feishu API error response.
40
- type PermissionError = {
41
- code: number;
42
- message: string;
43
- grantUrl?: string;
44
- };
45
-
46
- function extractPermissionError(err: unknown): PermissionError | null {
47
- if (!err || typeof err !== "object") return null;
48
-
49
- // Axios error structure: err.response.data contains the Feishu error
50
- const axiosErr = err as { response?: { data?: unknown } };
51
- const data = axiosErr.response?.data;
52
- if (!data || typeof data !== "object") return null;
53
-
54
- const feishuErr = data as {
55
- code?: number;
56
- msg?: string;
57
- error?: { permission_violations?: Array<{ uri?: string }> };
58
- };
59
-
60
- // Feishu permission error code: 99991672
61
- if (feishuErr.code !== 99991672) return null;
62
-
63
- // Extract the grant URL from the error message (contains the direct link)
64
- const msg = feishuErr.msg ?? "";
65
- const urlMatch = msg.match(/https:\/\/[^\s,]+\/app\/[^\s,]+/);
66
- const grantUrl = urlMatch?.[0];
67
-
68
- return {
69
- code: feishuErr.code,
70
- message: msg,
71
- grantUrl,
72
- };
73
- }
74
-
75
- // --- Sender name resolution (so the agent can distinguish who is speaking in group chats) ---
76
- // Cache display names by open_id to avoid an API call on every message.
77
- const SENDER_NAME_TTL_MS = 10 * 60 * 1000;
78
- const senderNameCache = new Map<string, { name: string; expireAt: number }>();
79
-
80
- // Cache permission errors to avoid spamming the user with repeated notifications.
81
- // Key: appId or "default", Value: timestamp of last notification
82
- const permissionErrorNotifiedAt = new Map<string, number>();
83
- const PERMISSION_ERROR_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
84
-
85
- type SenderNameResult = {
86
- name?: string;
87
- permissionError?: PermissionError;
88
- };
89
-
90
- async function resolveFeishuSenderName(params: {
91
- account: ResolvedFeishuAccount;
92
- senderOpenId: string;
93
- log: (...args: any[]) => void;
94
- }): Promise<SenderNameResult> {
95
- const { account, senderOpenId, log } = params;
96
- if (!account.configured) return {};
97
- if (!senderOpenId) return {};
98
-
99
- const cached = senderNameCache.get(senderOpenId);
100
- const now = Date.now();
101
- if (cached && cached.expireAt > now) return { name: cached.name };
102
-
103
- try {
104
- const client = createFeishuClient(account);
105
-
106
- // contact/v3/users/:user_id?user_id_type=open_id
107
- const res: any = await client.contact.user.get({
108
- path: { user_id: senderOpenId },
109
- params: { user_id_type: "open_id" },
110
- });
111
-
112
- const name: string | undefined =
113
- res?.data?.user?.name ||
114
- res?.data?.user?.display_name ||
115
- res?.data?.user?.nickname ||
116
- res?.data?.user?.en_name;
117
-
118
- if (name && typeof name === "string") {
119
- senderNameCache.set(senderOpenId, { name, expireAt: now + SENDER_NAME_TTL_MS });
120
- return { name };
121
- }
122
-
123
- return {};
124
- } catch (err) {
125
- // Check if this is a permission error
126
- const permErr = extractPermissionError(err);
127
- if (permErr) {
128
- log(`feishu: permission error resolving sender name: code=${permErr.code}`);
129
- return { permissionError: permErr };
130
- }
131
-
132
- // Best-effort. Don't fail message handling if name lookup fails.
133
- log(`feishu: failed to resolve sender name for ${senderOpenId}: ${String(err)}`);
134
- return {};
135
- }
136
- }
137
-
138
- export type FeishuMessageEvent = {
139
- sender: {
140
- sender_id: {
141
- open_id?: string;
142
- user_id?: string;
143
- union_id?: string;
144
- };
145
- sender_type?: string;
146
- tenant_key?: string;
147
- };
148
- message: {
149
- message_id: string;
150
- root_id?: string;
151
- parent_id?: string;
152
- chat_id: string;
153
- chat_type: "p2p" | "group";
154
- message_type: string;
155
- content: string;
156
- mentions?: Array<{
157
- key: string;
158
- id: {
159
- open_id?: string;
160
- user_id?: string;
161
- union_id?: string;
162
- };
163
- name: string;
164
- tenant_key?: string;
165
- }>;
166
- };
167
- };
168
-
169
- export type FeishuBotAddedEvent = {
170
- chat_id: string;
171
- operator_id: {
172
- open_id?: string;
173
- user_id?: string;
174
- union_id?: string;
175
- };
176
- external: boolean;
177
- operator_tenant_key?: string;
178
- };
179
-
180
- function parseMessageContent(content: string, messageType: string): string {
181
- try {
182
- const parsed = JSON.parse(content);
183
- if (messageType === "text") {
184
- return parsed.text || "";
185
- }
186
- if (messageType === "post") {
187
- // Extract text content from rich text post
188
- const { textContent } = parsePostContent(content);
189
- return textContent;
190
- }
191
- return content;
192
- } catch {
193
- return content;
194
- }
195
- }
196
-
197
- function checkBotMentioned(event: FeishuMessageEvent, botOpenId?: string): boolean {
198
- if (!botOpenId) return false;
199
- const mentions = event.message.mentions ?? [];
200
- if (mentions.length > 0) {
201
- return mentions.some((m) => m.id.open_id === botOpenId);
202
- }
203
- // Post (rich text) messages may have empty message.mentions when they contain docs/paste
204
- if (event.message.message_type === "post") {
205
- const { mentionedOpenIds } = parsePostContent(event.message.content);
206
- return mentionedOpenIds.some((id) => id === botOpenId);
207
- }
208
- return false;
209
- }
210
-
211
- export function stripBotMention(
212
- text: string,
213
- mentions?: FeishuMessageEvent["message"]["mentions"],
214
- ): string {
215
- if (!mentions || mentions.length === 0) return text;
216
- let result = text;
217
- for (const mention of mentions) {
218
- result = result.replace(new RegExp(`@${escapeRegExp(mention.name)}\\s*`, "g"), "");
219
- result = result.replace(new RegExp(escapeRegExp(mention.key), "g"), "");
220
- }
221
- return result.trim();
222
- }
223
-
224
- /**
225
- * Parse media keys from message content based on message type.
226
- */
227
- function parseMediaKeys(
228
- content: string,
229
- messageType: string,
230
- ): {
231
- imageKey?: string;
232
- fileKey?: string;
233
- fileName?: string;
234
- } {
235
- try {
236
- const parsed = JSON.parse(content);
237
- const imageKey = normalizeFeishuExternalKey(parsed.image_key);
238
- const fileKey = normalizeFeishuExternalKey(parsed.file_key);
239
- switch (messageType) {
240
- case "image":
241
- return { imageKey };
242
- case "file":
243
- return { fileKey, fileName: parsed.file_name };
244
- case "audio":
245
- return { fileKey };
246
- case "video":
247
- // Video has both file_key (video) and image_key (thumbnail)
248
- return { fileKey, imageKey };
249
- case "sticker":
250
- return { fileKey };
251
- default:
252
- return {};
253
- }
254
- } catch {
255
- return {};
256
- }
257
- }
258
-
259
- /**
260
- * Parse post (rich text) content and extract embedded image keys.
261
- * Post structure: { title?: string, content: [[{ tag, text?, image_key?, ... }]] }
262
- */
263
- function parsePostContent(content: string): {
264
- textContent: string;
265
- imageKeys: string[];
266
- mentionedOpenIds: string[];
267
- } {
268
- try {
269
- const parsed = JSON.parse(content);
270
- const title = parsed.title || "";
271
- const contentBlocks = parsed.content || [];
272
- let textContent = title ? `${title}\n\n` : "";
273
- const imageKeys: string[] = [];
274
- const mentionedOpenIds: string[] = [];
275
-
276
- for (const paragraph of contentBlocks) {
277
- if (Array.isArray(paragraph)) {
278
- for (const element of paragraph) {
279
- if (element.tag === "text") {
280
- textContent += element.text || "";
281
- } else if (element.tag === "a") {
282
- // Link: show text or href
283
- textContent += element.text || element.href || "";
284
- } else if (element.tag === "at") {
285
- // Mention: @username
286
- textContent += `@${element.user_name || element.user_id || ""}`;
287
- if (element.user_id) {
288
- mentionedOpenIds.push(element.user_id);
289
- }
290
- } else if (element.tag === "img" && element.image_key) {
291
- // Embedded image
292
- const imageKey = normalizeFeishuExternalKey(element.image_key);
293
- if (imageKey) {
294
- imageKeys.push(imageKey);
295
- }
296
- }
297
- }
298
- textContent += "\n";
299
- }
300
- }
301
-
302
- return {
303
- textContent: textContent.trim() || "[Rich text message]",
304
- imageKeys,
305
- mentionedOpenIds,
306
- };
307
- } catch {
308
- return { textContent: "[Rich text message]", imageKeys: [], mentionedOpenIds: [] };
309
- }
310
- }
311
-
312
- /**
313
- * Infer placeholder text based on message type.
314
- */
315
- function inferPlaceholder(messageType: string): string {
316
- switch (messageType) {
317
- case "image":
318
- return "<media:image>";
319
- case "file":
320
- return "<media:document>";
321
- case "audio":
322
- return "<media:audio>";
323
- case "video":
324
- return "<media:video>";
325
- case "sticker":
326
- return "<media:sticker>";
327
- default:
328
- return "<media:document>";
329
- }
330
- }
331
-
332
- /**
333
- * Resolve media from a Feishu message, downloading and saving to disk.
334
- * Similar to Discord's resolveMediaList().
335
- */
336
- async function resolveFeishuMediaList(params: {
337
- cfg: FasedAgentConfig;
338
- messageId: string;
339
- messageType: string;
340
- content: string;
341
- maxBytes: number;
342
- log?: (msg: string) => void;
343
- accountId?: string;
344
- }): Promise<FeishuMediaInfo[]> {
345
- const { cfg, messageId, messageType, content, maxBytes, log, accountId } = params;
346
-
347
- // Only process media message types (including post for embedded images)
348
- const mediaTypes = ["image", "file", "audio", "video", "sticker", "post"];
349
- if (!mediaTypes.includes(messageType)) {
350
- return [];
351
- }
352
-
353
- const out: FeishuMediaInfo[] = [];
354
- const core = getFeishuRuntime();
355
-
356
- // Handle post (rich text) messages with embedded images
357
- if (messageType === "post") {
358
- const { imageKeys } = parsePostContent(content);
359
- if (imageKeys.length === 0) {
360
- return [];
361
- }
362
-
363
- log?.(`feishu: post message contains ${imageKeys.length} embedded image(s)`);
364
-
365
- for (const imageKey of imageKeys) {
366
- try {
367
- // Embedded images in post use messageResource API with image_key as file_key
368
- const result = await downloadMessageResourceFeishu({
369
- cfg,
370
- messageId,
371
- fileKey: imageKey,
372
- type: "image",
373
- accountId,
374
- });
375
-
376
- let contentType = result.contentType;
377
- if (!contentType) {
378
- contentType = await core.media.detectMime({ buffer: result.buffer });
379
- }
380
-
381
- const saved = await core.channel.media.saveMediaBuffer(
382
- result.buffer,
383
- contentType,
384
- "inbound",
385
- maxBytes,
386
- );
387
-
388
- out.push({
389
- path: saved.path,
390
- contentType: saved.contentType,
391
- placeholder: "<media:image>",
392
- });
393
-
394
- log?.(`feishu: downloaded embedded image ${imageKey}, saved to ${saved.path}`);
395
- } catch (err) {
396
- log?.(`feishu: failed to download embedded image ${imageKey}: ${String(err)}`);
397
- }
398
- }
399
-
400
- return out;
401
- }
402
-
403
- // Handle other media types
404
- const mediaKeys = parseMediaKeys(content, messageType);
405
- if (!mediaKeys.imageKey && !mediaKeys.fileKey) {
406
- return [];
407
- }
408
-
409
- try {
410
- let buffer: Buffer;
411
- let contentType: string | undefined;
412
- let fileName: string | undefined;
413
-
414
- // For message media, always use messageResource API
415
- // The image.get API is only for images uploaded via im/v1/images, not for message attachments
416
- const fileKey = mediaKeys.fileKey || mediaKeys.imageKey;
417
- if (!fileKey) {
418
- return [];
419
- }
420
-
421
- const resourceType = messageType === "image" ? "image" : "file";
422
- const result = await downloadMessageResourceFeishu({
423
- cfg,
424
- messageId,
425
- fileKey,
426
- type: resourceType,
427
- accountId,
428
- });
429
- buffer = result.buffer;
430
- contentType = result.contentType;
431
- fileName = result.fileName || mediaKeys.fileName;
432
-
433
- // Detect mime type if not provided
434
- if (!contentType) {
435
- contentType = await core.media.detectMime({ buffer });
436
- }
437
-
438
- // Save to disk using core's saveMediaBuffer
439
- const saved = await core.channel.media.saveMediaBuffer(
440
- buffer,
441
- contentType,
442
- "inbound",
443
- maxBytes,
444
- fileName,
445
- );
446
-
447
- out.push({
448
- path: saved.path,
449
- contentType: saved.contentType,
450
- placeholder: inferPlaceholder(messageType),
451
- });
452
-
453
- log?.(`feishu: downloaded ${messageType} media, saved to ${saved.path}`);
454
- } catch (err) {
455
- log?.(`feishu: failed to download ${messageType} media: ${String(err)}`);
456
- }
457
-
458
- return out;
459
- }
460
-
461
- /**
462
- * Build media payload for inbound context.
463
- * Similar to Discord's buildDiscordMediaPayload().
464
- */
465
- export function parseFeishuMessageEvent(
466
- event: FeishuMessageEvent,
467
- botOpenId?: string,
468
- ): FeishuMessageContext {
469
- const rawContent = parseMessageContent(event.message.content, event.message.message_type);
470
- const mentionedBot = checkBotMentioned(event, botOpenId);
471
- const content = stripBotMention(rawContent, event.message.mentions);
472
-
473
- const ctx: FeishuMessageContext = {
474
- chatId: event.message.chat_id,
475
- messageId: event.message.message_id,
476
- senderId: event.sender.sender_id.user_id || event.sender.sender_id.open_id || "",
477
- senderOpenId: event.sender.sender_id.open_id || "",
478
- chatType: event.message.chat_type,
479
- mentionedBot,
480
- rootId: event.message.root_id || undefined,
481
- parentId: event.message.parent_id || undefined,
482
- content,
483
- contentType: event.message.message_type,
484
- };
485
-
486
- // Detect mention forward request: message mentions bot + at least one other user
487
- if (isMentionForwardRequest(event, botOpenId)) {
488
- const mentionTargets = extractMentionTargets(event, botOpenId);
489
- if (mentionTargets.length > 0) {
490
- ctx.mentionTargets = mentionTargets;
491
- // Extract message body (remove all @ placeholders)
492
- const allMentionKeys = (event.message.mentions ?? []).map((m) => m.key);
493
- ctx.mentionMessageBody = extractMessageBody(content, allMentionKeys);
494
- }
495
- }
496
-
497
- return ctx;
498
- }
499
-
500
- export function buildFeishuAgentBody(params: {
501
- ctx: Pick<
502
- FeishuMessageContext,
503
- "content" | "senderName" | "senderOpenId" | "mentionTargets" | "messageId"
504
- >;
505
- quotedContent?: string;
506
- permissionErrorForAgent?: PermissionError;
507
- }): string {
508
- const { ctx, quotedContent, permissionErrorForAgent } = params;
509
- let messageBody = ctx.content;
510
- if (quotedContent) {
511
- messageBody = `[Replying to: "${quotedContent}"]\n\n${ctx.content}`;
512
- }
513
-
514
- // DMs already have per-sender sessions, but this label still improves attribution.
515
- const speaker = ctx.senderName ?? ctx.senderOpenId;
516
- messageBody = `${speaker}: ${messageBody}`;
517
-
518
- if (ctx.mentionTargets && ctx.mentionTargets.length > 0) {
519
- const targetNames = ctx.mentionTargets.map((t) => t.name).join(", ");
520
- messageBody += `\n\n[System: Your reply will automatically @mention: ${targetNames}. Do not write @xxx yourself.]`;
521
- }
522
-
523
- // Keep message_id on its own line so shared message-id hint stripping can parse it reliably.
524
- messageBody = `[message_id: ${ctx.messageId}]\n${messageBody}`;
525
-
526
- if (permissionErrorForAgent) {
527
- const grantUrl = permissionErrorForAgent.grantUrl ?? "";
528
- messageBody += `\n\n[System: The bot encountered a Feishu API permission error. Please inform the user about this issue and provide the permission grant URL for the admin to authorize. Permission grant URL: ${grantUrl}]`;
529
- }
530
-
531
- return messageBody;
532
- }
533
-
534
- export async function handleFeishuMessage(params: {
535
- cfg: FasedAgentConfig;
536
- event: FeishuMessageEvent;
537
- botOpenId?: string;
538
- runtime?: RuntimeEnv;
539
- chatHistories?: Map<string, HistoryEntry[]>;
540
- accountId?: string;
541
- }): Promise<void> {
542
- const { cfg, event, botOpenId, runtime, chatHistories, accountId } = params;
543
-
544
- // Resolve account with merged config
545
- const account = resolveFeishuAccount({ cfg, accountId });
546
- const feishuCfg = account.config;
547
-
548
- const log = runtime?.log ?? console.log;
549
- const error = runtime?.error ?? console.error;
550
-
551
- // Dedup check: skip if this message was already processed (memory + disk).
552
- const messageId = event.message.message_id;
553
- if (!(await tryRecordMessagePersistent(messageId, account.accountId, log))) {
554
- log(`feishu: skipping duplicate message ${messageId}`);
555
- return;
556
- }
557
-
558
- let ctx = parseFeishuMessageEvent(event, botOpenId);
559
- const isGroup = ctx.chatType === "group";
560
- const senderUserId = event.sender.sender_id.user_id?.trim() || undefined;
561
-
562
- // Resolve sender display name (best-effort) so the agent can attribute messages correctly.
563
- const senderResult = await resolveFeishuSenderName({
564
- account,
565
- senderOpenId: ctx.senderOpenId,
566
- log,
567
- });
568
- if (senderResult.name) ctx = { ...ctx, senderName: senderResult.name };
569
-
570
- // Track permission error to inform agent later (with cooldown to avoid repetition)
571
- let permissionErrorForAgent: PermissionError | undefined;
572
- if (senderResult.permissionError) {
573
- const appKey = account.appId ?? "default";
574
- const now = Date.now();
575
- const lastNotified = permissionErrorNotifiedAt.get(appKey) ?? 0;
576
-
577
- if (now - lastNotified > PERMISSION_ERROR_COOLDOWN_MS) {
578
- permissionErrorNotifiedAt.set(appKey, now);
579
- permissionErrorForAgent = senderResult.permissionError;
580
- }
581
- }
582
-
583
- log(
584
- `feishu[${account.accountId}]: received message from ${ctx.senderOpenId} in ${ctx.chatId} (${ctx.chatType})`,
585
- );
586
-
587
- // Log mention targets if detected
588
- if (ctx.mentionTargets && ctx.mentionTargets.length > 0) {
589
- const names = ctx.mentionTargets.map((t) => t.name).join(", ");
590
- log(`feishu[${account.accountId}]: detected @ forward request, targets: [${names}]`);
591
- }
592
-
593
- const historyLimit = Math.max(
594
- 0,
595
- feishuCfg?.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT,
596
- );
597
- const groupConfig = isGroup
598
- ? resolveFeishuGroupConfig({ cfg: feishuCfg, groupId: ctx.chatId })
599
- : undefined;
600
- const dmPolicy = feishuCfg?.dmPolicy ?? "pairing";
601
- const configAllowFrom = feishuCfg?.allowFrom ?? [];
602
- const useAccessGroups = cfg.commands?.useAccessGroups !== false;
603
-
604
- if (isGroup) {
605
- const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
606
- const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({
607
- providerConfigPresent: cfg.channels?.feishu !== undefined,
608
- groupPolicy: feishuCfg?.groupPolicy,
609
- defaultGroupPolicy,
610
- });
611
- warnMissingProviderGroupPolicyFallbackOnce({
612
- providerMissingFallbackApplied,
613
- providerKey: "feishu",
614
- accountId: account.accountId,
615
- log,
616
- });
617
- const groupAllowFrom = feishuCfg?.groupAllowFrom ?? [];
618
- // DEBUG: log(`feishu[${account.accountId}]: groupPolicy=${groupPolicy}`);
619
-
620
- // Check if this GROUP is allowed (groupAllowFrom contains group IDs like oc_xxx, not user IDs)
621
- const groupAllowed = isFeishuGroupAllowed({
622
- groupPolicy,
623
- allowFrom: groupAllowFrom,
624
- senderId: ctx.chatId, // Check group ID, not sender ID
625
- senderName: undefined,
626
- });
627
-
628
- if (!groupAllowed) {
629
- log(`feishu[${account.accountId}]: sender ${ctx.senderOpenId} not in group allowlist`);
630
- return;
631
- }
632
-
633
- // Additional sender-level allowlist check if group has specific allowFrom config
634
- const senderAllowFrom = groupConfig?.allowFrom ?? [];
635
- if (senderAllowFrom.length > 0) {
636
- const senderAllowed = isFeishuGroupAllowed({
637
- groupPolicy: "allowlist",
638
- allowFrom: senderAllowFrom,
639
- senderId: ctx.senderOpenId,
640
- senderIds: [senderUserId],
641
- senderName: ctx.senderName,
642
- });
643
- if (!senderAllowed) {
644
- log(`feishu: sender ${ctx.senderOpenId} not in group ${ctx.chatId} sender allowlist`);
645
- return;
646
- }
647
- }
648
-
649
- const { requireMention } = resolveFeishuReplyPolicy({
650
- isDirectMessage: false,
651
- globalConfig: feishuCfg,
652
- groupConfig,
653
- });
654
-
655
- if (requireMention && !ctx.mentionedBot) {
656
- log(
657
- `feishu[${account.accountId}]: message in group ${ctx.chatId} did not mention bot, recording to history`,
658
- );
659
- if (chatHistories) {
660
- recordPendingHistoryEntryIfEnabled({
661
- historyMap: chatHistories,
662
- historyKey: ctx.chatId,
663
- limit: historyLimit,
664
- entry: {
665
- sender: ctx.senderOpenId,
666
- body: `${ctx.senderName ?? ctx.senderOpenId}: ${ctx.content}`,
667
- timestamp: Date.now(),
668
- messageId: ctx.messageId,
669
- },
670
- });
671
- }
672
- return;
673
- }
674
- } else {
675
- }
676
-
677
- try {
678
- const core = getFeishuRuntime();
679
- const pairing = createScopedPairingAccess({
680
- core,
681
- channel: "feishu",
682
- accountId: account.accountId,
683
- });
684
- const shouldComputeCommandAuthorized = core.channel.commands.shouldComputeCommandAuthorized(
685
- ctx.content,
686
- cfg,
687
- );
688
- const storeAllowFrom =
689
- !isGroup &&
690
- dmPolicy !== "allowlist" &&
691
- (dmPolicy !== "open" || shouldComputeCommandAuthorized)
692
- ? await pairing.readAllowFromStore().catch(() => [])
693
- : [];
694
- const effectiveDmAllowFrom = [...configAllowFrom, ...storeAllowFrom];
695
- const dmAllowed = resolveFeishuAllowlistMatch({
696
- allowFrom: effectiveDmAllowFrom,
697
- senderId: ctx.senderOpenId,
698
- senderIds: [senderUserId],
699
- senderName: ctx.senderName,
700
- }).allowed;
701
-
702
- if (!isGroup && dmPolicy !== "open" && !dmAllowed) {
703
- if (dmPolicy === "pairing") {
704
- const { code, created } = await pairing.upsertPairingRequest({
705
- id: ctx.senderOpenId,
706
- meta: { name: ctx.senderName },
707
- });
708
- if (created) {
709
- log(`feishu[${account.accountId}]: pairing request sender=${ctx.senderOpenId}`);
710
- try {
711
- await sendMessageFeishu({
712
- cfg,
713
- to: `user:${ctx.senderOpenId}`,
714
- text: core.channel.pairing.buildPairingReply({
715
- channel: "feishu",
716
- idLine: `Your Feishu user id: ${ctx.senderOpenId}`,
717
- code,
718
- }),
719
- accountId: account.accountId,
720
- });
721
- } catch (err) {
722
- log(
723
- `feishu[${account.accountId}]: pairing reply failed for ${ctx.senderOpenId}: ${String(err)}`,
724
- );
725
- }
726
- }
727
- } else {
728
- log(
729
- `feishu[${account.accountId}]: blocked unauthorized sender ${ctx.senderOpenId} (dmPolicy=${dmPolicy})`,
730
- );
731
- }
732
- return;
733
- }
734
-
735
- const commandAllowFrom = isGroup
736
- ? (groupConfig?.allowFrom ?? configAllowFrom)
737
- : effectiveDmAllowFrom;
738
- const senderAllowedForCommands = resolveFeishuAllowlistMatch({
739
- allowFrom: commandAllowFrom,
740
- senderId: ctx.senderOpenId,
741
- senderIds: [senderUserId],
742
- senderName: ctx.senderName,
743
- }).allowed;
744
- const commandAuthorized = shouldComputeCommandAuthorized
745
- ? core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
746
- useAccessGroups,
747
- authorizers: [
748
- { configured: commandAllowFrom.length > 0, allowed: senderAllowedForCommands },
749
- ],
750
- })
751
- : undefined;
752
-
753
- // In group chats, the session is scoped to the group, but the *speaker* is the sender.
754
- // Using a group-scoped From causes the agent to treat different users as the same person.
755
- const feishuFrom = `feishu:${ctx.senderOpenId}`;
756
- const feishuTo = isGroup ? `chat:${ctx.chatId}` : `user:${ctx.senderOpenId}`;
757
-
758
- // Resolve peer ID for session routing
759
- // When topicSessionMode is enabled, messages within a topic (identified by root_id)
760
- // get a separate session from the main group chat.
761
- let peerId = isGroup ? ctx.chatId : ctx.senderOpenId;
762
- let topicSessionMode: "enabled" | "disabled" = "disabled";
763
- if (isGroup && ctx.rootId) {
764
- const groupConfig = resolveFeishuGroupConfig({ cfg: feishuCfg, groupId: ctx.chatId });
765
- topicSessionMode = groupConfig?.topicSessionMode ?? feishuCfg?.topicSessionMode ?? "disabled";
766
- if (topicSessionMode === "enabled") {
767
- // Use chatId:topic:rootId as peer ID for topic-scoped sessions
768
- peerId = `${ctx.chatId}:topic:${ctx.rootId}`;
769
- log(`feishu[${account.accountId}]: topic session isolation enabled, peer=${peerId}`);
770
- }
771
- }
772
-
773
- let route = core.channel.routing.resolveAgentRoute({
774
- cfg,
775
- channel: "feishu",
776
- accountId: account.accountId,
777
- peer: {
778
- kind: isGroup ? "group" : "direct",
779
- id: peerId,
780
- },
781
- // Add parentPeer for binding inheritance in topic mode
782
- parentPeer:
783
- isGroup && ctx.rootId && topicSessionMode === "enabled"
784
- ? {
785
- kind: "group",
786
- id: ctx.chatId,
787
- }
788
- : null,
789
- });
790
-
791
- // Dynamic agent creation for DM users
792
- // When enabled, creates a unique agent instance with its own workspace for each DM user.
793
- let effectiveCfg = cfg;
794
- if (!isGroup && route.matchedBy === "default") {
795
- const dynamicCfg = feishuCfg?.dynamicAgentCreation as DynamicAgentCreationConfig | undefined;
796
- if (dynamicCfg?.enabled) {
797
- const runtime = getFeishuRuntime();
798
- const result = await maybeCreateDynamicAgent({
799
- cfg,
800
- runtime,
801
- senderOpenId: ctx.senderOpenId,
802
- dynamicCfg,
803
- log: (msg) => log(msg),
804
- });
805
- if (result.created) {
806
- effectiveCfg = result.updatedCfg;
807
- // Re-resolve route with updated config
808
- route = core.channel.routing.resolveAgentRoute({
809
- cfg: result.updatedCfg,
810
- channel: "feishu",
811
- accountId: account.accountId,
812
- peer: { kind: "direct", id: ctx.senderOpenId },
813
- });
814
- log(
815
- `feishu[${account.accountId}]: dynamic agent created, new route: ${route.sessionKey}`,
816
- );
817
- }
818
- }
819
- }
820
-
821
- const preview = ctx.content.replace(/\s+/g, " ").slice(0, 160);
822
- const inboundLabel = isGroup
823
- ? `Feishu[${account.accountId}] message in group ${ctx.chatId}`
824
- : `Feishu[${account.accountId}] DM from ${ctx.senderOpenId}`;
825
-
826
- core.system.enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
827
- sessionKey: route.sessionKey,
828
- contextKey: `feishu:message:${ctx.chatId}:${ctx.messageId}`,
829
- });
830
-
831
- // Resolve media from message
832
- const mediaMaxBytes = (feishuCfg?.mediaMaxMb ?? 30) * 1024 * 1024; // 30MB default
833
- const mediaList = await resolveFeishuMediaList({
834
- cfg,
835
- messageId: ctx.messageId,
836
- messageType: event.message.message_type,
837
- content: event.message.content,
838
- maxBytes: mediaMaxBytes,
839
- log,
840
- accountId: account.accountId,
841
- });
842
- const mediaPayload = buildAgentMediaPayload(mediaList);
843
-
844
- // Fetch quoted/replied message content if parentId exists
845
- let quotedContent: string | undefined;
846
- if (ctx.parentId) {
847
- try {
848
- const quotedMsg = await getMessageFeishu({
849
- cfg,
850
- messageId: ctx.parentId,
851
- accountId: account.accountId,
852
- });
853
- if (quotedMsg) {
854
- quotedContent = quotedMsg.content;
855
- log(
856
- `feishu[${account.accountId}]: fetched quoted message: ${quotedContent?.slice(0, 100)}`,
857
- );
858
- }
859
- } catch (err) {
860
- log(`feishu[${account.accountId}]: failed to fetch quoted message: ${String(err)}`);
861
- }
862
- }
863
-
864
- const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
865
- const messageBody = buildFeishuAgentBody({
866
- ctx,
867
- quotedContent,
868
- permissionErrorForAgent,
869
- });
870
- const envelopeFrom = isGroup ? `${ctx.chatId}:${ctx.senderOpenId}` : ctx.senderOpenId;
871
- if (permissionErrorForAgent) {
872
- // Keep the notice in a single dispatch to avoid duplicate replies (#27372).
873
- log(`feishu[${account.accountId}]: appending permission error notice to message body`);
874
- }
875
-
876
- const body = core.channel.reply.formatAgentEnvelope({
877
- channel: "Feishu",
878
- from: envelopeFrom,
879
- timestamp: new Date(),
880
- envelope: envelopeOptions,
881
- body: messageBody,
882
- });
883
-
884
- let combinedBody = body;
885
- const historyKey = isGroup ? ctx.chatId : undefined;
886
-
887
- if (isGroup && historyKey && chatHistories) {
888
- combinedBody = buildPendingHistoryContextFromMap({
889
- historyMap: chatHistories,
890
- historyKey,
891
- limit: historyLimit,
892
- currentMessage: combinedBody,
893
- formatEntry: (entry) =>
894
- core.channel.reply.formatAgentEnvelope({
895
- channel: "Feishu",
896
- // Preserve speaker identity in group history as well.
897
- from: `${ctx.chatId}:${entry.sender}`,
898
- timestamp: entry.timestamp,
899
- body: entry.body,
900
- envelope: envelopeOptions,
901
- }),
902
- });
903
- }
904
-
905
- const inboundHistory =
906
- isGroup && historyKey && historyLimit > 0 && chatHistories
907
- ? (chatHistories.get(historyKey) ?? []).map((entry) => ({
908
- sender: entry.sender,
909
- body: entry.body,
910
- timestamp: entry.timestamp,
911
- }))
912
- : undefined;
913
-
914
- const ctxPayload = core.channel.reply.finalizeInboundContext({
915
- Body: combinedBody,
916
- BodyForAgent: messageBody,
917
- InboundHistory: inboundHistory,
918
- RawBody: ctx.content,
919
- CommandBody: ctx.content,
920
- From: feishuFrom,
921
- To: feishuTo,
922
- SessionKey: route.sessionKey,
923
- AccountId: route.accountId,
924
- ChatType: isGroup ? "group" : "direct",
925
- GroupSubject: isGroup ? ctx.chatId : undefined,
926
- SenderName: ctx.senderName ?? ctx.senderOpenId,
927
- SenderId: ctx.senderOpenId,
928
- Provider: "feishu" as const,
929
- Surface: "feishu" as const,
930
- MessageSid: ctx.messageId,
931
- ReplyToBody: quotedContent ?? undefined,
932
- Timestamp: Date.now(),
933
- WasMentioned: ctx.mentionedBot,
934
- CommandAuthorized: commandAuthorized,
935
- OriginatingChannel: "feishu" as const,
936
- OriginatingTo: feishuTo,
937
- ...mediaPayload,
938
- });
939
-
940
- const { dispatcher, replyOptions, markDispatchIdle } = createFeishuReplyDispatcher({
941
- cfg,
942
- agentId: route.agentId,
943
- runtime: runtime as RuntimeEnv,
944
- chatId: ctx.chatId,
945
- replyToMessageId: ctx.messageId,
946
- mentionTargets: ctx.mentionTargets,
947
- accountId: account.accountId,
948
- });
949
-
950
- log(`feishu[${account.accountId}]: dispatching to agent (session=${route.sessionKey})`);
951
- const { queuedFinal, counts } = await core.channel.reply.withReplyDispatcher({
952
- dispatcher,
953
- onSettled: () => {
954
- markDispatchIdle();
955
- },
956
- run: () =>
957
- core.channel.reply.dispatchReplyFromConfig({
958
- ctx: ctxPayload,
959
- cfg,
960
- dispatcher,
961
- replyOptions,
962
- }),
963
- });
964
-
965
- if (isGroup && historyKey && chatHistories) {
966
- clearHistoryEntriesIfEnabled({
967
- historyMap: chatHistories,
968
- historyKey,
969
- limit: historyLimit,
970
- });
971
- }
972
-
973
- log(
974
- `feishu[${account.accountId}]: dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final})`,
975
- );
976
- } catch (err) {
977
- error(`feishu[${account.accountId}]: failed to dispatch message: ${String(err)}`);
978
- }
979
- }