octo-agent 0.11.2

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 (319) hide show
  1. checksums.yaml +7 -0
  2. data/.clacky/skills/commit/SKILL.md +423 -0
  3. data/.clacky/skills/gem-release/SKILL.md +199 -0
  4. data/.clacky/skills/gem-release/scripts/release.sh +304 -0
  5. data/.clacky/skills/oss-upload/SKILL.md +47 -0
  6. data/.octorules +106 -0
  7. data/.rspec +3 -0
  8. data/.rubocop.yml +8 -0
  9. data/CHANGELOG.md +76 -0
  10. data/CODE_OF_CONDUCT.md +132 -0
  11. data/CONTRIBUTING.md +92 -0
  12. data/Dockerfile +28 -0
  13. data/LICENSE.txt +22 -0
  14. data/POSITIONING.md +46 -0
  15. data/README.md +134 -0
  16. data/README_CN.md +134 -0
  17. data/Rakefile +34 -0
  18. data/benchmark/fixtures/sample_project/Gemfile +3 -0
  19. data/benchmark/fixtures/sample_project/lib/api_handler.rb +32 -0
  20. data/benchmark/fixtures/sample_project/lib/order_calculator.rb +23 -0
  21. data/benchmark/fixtures/sample_project/lib/user_renderer.rb +20 -0
  22. data/benchmark/fixtures/sample_project/spec/order_calculator_spec.rb +20 -0
  23. data/benchmark/results/EVALUATION_REPORT.md +165 -0
  24. data/benchmark/results/baseline_20260511_174424.json +128 -0
  25. data/benchmark/results/report_20260511_175256.json +271 -0
  26. data/benchmark/results/report_20260511_175444.json +271 -0
  27. data/benchmark/results/treatment_20260511_175103.json +130 -0
  28. data/benchmark/runner.rb +441 -0
  29. data/bin/octo +7 -0
  30. data/docs/agent-first-ui-design.md +77 -0
  31. data/docs/billing-system.md +318 -0
  32. data/docs/channel-architecture.md +235 -0
  33. data/docs/engineering-article.md +343 -0
  34. data/docs/session-skill-invocation.md +69 -0
  35. data/docs/time_machine_design.md +247 -0
  36. data/docs/ui2-architecture.md +124 -0
  37. data/homebrew/README.md +96 -0
  38. data/homebrew/openocto.rb +24 -0
  39. data/lib/octo/agent/hook_manager.rb +61 -0
  40. data/lib/octo/agent/llm_caller.rb +800 -0
  41. data/lib/octo/agent/memory_updater.rb +246 -0
  42. data/lib/octo/agent/message_compressor.rb +225 -0
  43. data/lib/octo/agent/message_compressor_helper.rb +869 -0
  44. data/lib/octo/agent/next_message_suggester.rb +215 -0
  45. data/lib/octo/agent/session_serializer.rb +685 -0
  46. data/lib/octo/agent/skill_auto_creator.rb +114 -0
  47. data/lib/octo/agent/skill_evolution.rb +61 -0
  48. data/lib/octo/agent/skill_manager.rb +466 -0
  49. data/lib/octo/agent/skill_reflector.rb +89 -0
  50. data/lib/octo/agent/system_prompt_builder.rb +101 -0
  51. data/lib/octo/agent/time_machine.rb +214 -0
  52. data/lib/octo/agent/tool_executor.rb +454 -0
  53. data/lib/octo/agent/tool_registry.rb +150 -0
  54. data/lib/octo/agent.rb +2180 -0
  55. data/lib/octo/agent_config.rb +989 -0
  56. data/lib/octo/agent_profile.rb +112 -0
  57. data/lib/octo/anthropic_stream_aggregator.rb +137 -0
  58. data/lib/octo/background_task_registry.rb +324 -0
  59. data/lib/octo/banner.rb +34 -0
  60. data/lib/octo/bedrock_stream_aggregator.rb +137 -0
  61. data/lib/octo/block_font.rb +331 -0
  62. data/lib/octo/cli.rb +968 -0
  63. data/lib/octo/client.rb +623 -0
  64. data/lib/octo/default_agents/SOUL.md +3 -0
  65. data/lib/octo/default_agents/USER.md +1 -0
  66. data/lib/octo/default_agents/base_prompt.md +66 -0
  67. data/lib/octo/default_agents/coding/profile.yml +2 -0
  68. data/lib/octo/default_agents/coding/system_prompt.md +67 -0
  69. data/lib/octo/default_agents/general/profile.yml +2 -0
  70. data/lib/octo/default_agents/general/system_prompt.md +16 -0
  71. data/lib/octo/default_parsers/doc_parser.rb +69 -0
  72. data/lib/octo/default_parsers/docx_parser.rb +188 -0
  73. data/lib/octo/default_parsers/pdf_parser.rb +120 -0
  74. data/lib/octo/default_parsers/pdf_parser_ocr.py +103 -0
  75. data/lib/octo/default_parsers/pdf_parser_plumber.py +62 -0
  76. data/lib/octo/default_parsers/pptx_parser.rb +140 -0
  77. data/lib/octo/default_parsers/xlsx_parser.rb +121 -0
  78. data/lib/octo/default_skills/browser-setup/SKILL.md +426 -0
  79. data/lib/octo/default_skills/channel-manager/SKILL.md +623 -0
  80. data/lib/octo/default_skills/channel-manager/dingtalk_setup.rb +191 -0
  81. data/lib/octo/default_skills/channel-manager/discord_setup.rb +199 -0
  82. data/lib/octo/default_skills/channel-manager/feishu_setup.rb +574 -0
  83. data/lib/octo/default_skills/channel-manager/import_lark_skills.rb +97 -0
  84. data/lib/octo/default_skills/channel-manager/install_feishu_skills.rb +105 -0
  85. data/lib/octo/default_skills/channel-manager/weixin_setup.rb +274 -0
  86. data/lib/octo/default_skills/code-explorer/SKILL.md +36 -0
  87. data/lib/octo/default_skills/cron-task-creator/SKILL.md +257 -0
  88. data/lib/octo/default_skills/cron-task-creator/evals/evals.json +38 -0
  89. data/lib/octo/default_skills/onboard/SKILL.md +578 -0
  90. data/lib/octo/default_skills/onboard/scripts/import_external_skills.rb +413 -0
  91. data/lib/octo/default_skills/onboard/scripts/install_builtin_skills.rb +97 -0
  92. data/lib/octo/default_skills/persist-memory/SKILL.md +59 -0
  93. data/lib/octo/default_skills/personal-website/SKILL.md +113 -0
  94. data/lib/octo/default_skills/personal-website/publish.rb +235 -0
  95. data/lib/octo/default_skills/product-help/SKILL.md +123 -0
  96. data/lib/octo/default_skills/product-help/docs/agent-config.md +74 -0
  97. data/lib/octo/default_skills/product-help/docs/best-practices.md +49 -0
  98. data/lib/octo/default_skills/product-help/docs/browser-tool.md +53 -0
  99. data/lib/octo/default_skills/product-help/docs/built-in-skills.md +43 -0
  100. data/lib/octo/default_skills/product-help/docs/cli-reference.md +82 -0
  101. data/lib/octo/default_skills/product-help/docs/create-your-first-skill.md +47 -0
  102. data/lib/octo/default_skills/product-help/docs/faq.md +98 -0
  103. data/lib/octo/default_skills/product-help/docs/how-to-use-a-skill.md +58 -0
  104. data/lib/octo/default_skills/product-help/docs/installation.md +59 -0
  105. data/lib/octo/default_skills/product-help/docs/memory-system.md +61 -0
  106. data/lib/octo/default_skills/product-help/docs/octorules.md +62 -0
  107. data/lib/octo/default_skills/product-help/docs/session-management.md +63 -0
  108. data/lib/octo/default_skills/product-help/docs/skill-basics.md +55 -0
  109. data/lib/octo/default_skills/product-help/docs/skill-frontmatter.md +61 -0
  110. data/lib/octo/default_skills/product-help/docs/web-server.md +49 -0
  111. data/lib/octo/default_skills/product-help/docs/what-is-octo.md +37 -0
  112. data/lib/octo/default_skills/product-help/docs/windows-installation.md +36 -0
  113. data/lib/octo/default_skills/product-help/docs/writing-tips.md +53 -0
  114. data/lib/octo/default_skills/recall-memory/SKILL.md +65 -0
  115. data/lib/octo/default_skills/skill-add/SKILL.md +59 -0
  116. data/lib/octo/default_skills/skill-add/scripts/install_from_zip.rb +295 -0
  117. data/lib/octo/default_skills/skill-creator/SKILL.md +602 -0
  118. data/lib/octo/default_skills/skill-creator/agents/analyzer.md +274 -0
  119. data/lib/octo/default_skills/skill-creator/agents/comparator.md +202 -0
  120. data/lib/octo/default_skills/skill-creator/agents/grader.md +223 -0
  121. data/lib/octo/default_skills/skill-creator/eval-viewer/generate_review.py +471 -0
  122. data/lib/octo/default_skills/skill-creator/eval-viewer/viewer.html +1325 -0
  123. data/lib/octo/default_skills/skill-creator/references/schemas.md +430 -0
  124. data/lib/octo/default_skills/skill-creator/scripts/__init__.py +0 -0
  125. data/lib/octo/default_skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  126. data/lib/octo/default_skills/skill-creator/scripts/generate_report.py +326 -0
  127. data/lib/octo/default_skills/skill-creator/scripts/improve_description.py +310 -0
  128. data/lib/octo/default_skills/skill-creator/scripts/quick_validate.py +103 -0
  129. data/lib/octo/default_skills/skill-creator/scripts/run_eval.py +317 -0
  130. data/lib/octo/default_skills/skill-creator/scripts/run_loop.py +331 -0
  131. data/lib/octo/default_skills/skill-creator/scripts/utils.py +47 -0
  132. data/lib/octo/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb +143 -0
  133. data/lib/octo/idle_compression_timer.rb +115 -0
  134. data/lib/octo/json_ui_controller.rb +204 -0
  135. data/lib/octo/message_format/anthropic.rb +409 -0
  136. data/lib/octo/message_format/bedrock.rb +361 -0
  137. data/lib/octo/message_format/open_ai.rb +222 -0
  138. data/lib/octo/message_history.rb +373 -0
  139. data/lib/octo/openai_stream_aggregator.rb +130 -0
  140. data/lib/octo/plain_ui_controller.rb +166 -0
  141. data/lib/octo/providers.rb +534 -0
  142. data/lib/octo/server/browser_manager.rb +397 -0
  143. data/lib/octo/server/channel/adapters/base.rb +82 -0
  144. data/lib/octo/server/channel/adapters/dingtalk/adapter.rb +314 -0
  145. data/lib/octo/server/channel/adapters/dingtalk/api_client.rb +391 -0
  146. data/lib/octo/server/channel/adapters/dingtalk/stream_client.rb +203 -0
  147. data/lib/octo/server/channel/adapters/discord/adapter.rb +229 -0
  148. data/lib/octo/server/channel/adapters/discord/api_client.rb +107 -0
  149. data/lib/octo/server/channel/adapters/discord/gateway_client.rb +270 -0
  150. data/lib/octo/server/channel/adapters/feishu/adapter.rb +320 -0
  151. data/lib/octo/server/channel/adapters/feishu/bot.rb +478 -0
  152. data/lib/octo/server/channel/adapters/feishu/file_processor.rb +36 -0
  153. data/lib/octo/server/channel/adapters/feishu/message_parser.rb +129 -0
  154. data/lib/octo/server/channel/adapters/feishu/ws_client.rb +423 -0
  155. data/lib/octo/server/channel/adapters/telegram/adapter.rb +375 -0
  156. data/lib/octo/server/channel/adapters/telegram/api_client.rb +205 -0
  157. data/lib/octo/server/channel/adapters/wecom/adapter.rb +148 -0
  158. data/lib/octo/server/channel/adapters/wecom/media_downloader.rb +115 -0
  159. data/lib/octo/server/channel/adapters/wecom/ws_client.rb +395 -0
  160. data/lib/octo/server/channel/adapters/weixin/adapter.rb +692 -0
  161. data/lib/octo/server/channel/adapters/weixin/api_client.rb +402 -0
  162. data/lib/octo/server/channel/channel_config.rb +178 -0
  163. data/lib/octo/server/channel/channel_manager.rb +468 -0
  164. data/lib/octo/server/channel/channel_ui_controller.rb +224 -0
  165. data/lib/octo/server/channel.rb +33 -0
  166. data/lib/octo/server/discover.rb +77 -0
  167. data/lib/octo/server/epipe_safe_io.rb +105 -0
  168. data/lib/octo/server/http_server.rb +3554 -0
  169. data/lib/octo/server/scheduler.rb +317 -0
  170. data/lib/octo/server/server_master.rb +325 -0
  171. data/lib/octo/server/session_registry.rb +431 -0
  172. data/lib/octo/server/web_ui_controller.rb +487 -0
  173. data/lib/octo/session_manager.rb +385 -0
  174. data/lib/octo/skill.rb +466 -0
  175. data/lib/octo/skill_loader.rb +328 -0
  176. data/lib/octo/tools/base.rb +118 -0
  177. data/lib/octo/tools/browser.rb +625 -0
  178. data/lib/octo/tools/edit.rb +165 -0
  179. data/lib/octo/tools/file_reader.rb +549 -0
  180. data/lib/octo/tools/glob.rb +162 -0
  181. data/lib/octo/tools/grep.rb +356 -0
  182. data/lib/octo/tools/invoke_skill.rb +96 -0
  183. data/lib/octo/tools/list_tasks.rb +54 -0
  184. data/lib/octo/tools/redo_task.rb +41 -0
  185. data/lib/octo/tools/request_user_feedback.rb +84 -0
  186. data/lib/octo/tools/security.rb +333 -0
  187. data/lib/octo/tools/terminal/output_cleaner.rb +63 -0
  188. data/lib/octo/tools/terminal/persistent_session.rb +268 -0
  189. data/lib/octo/tools/terminal/safe_rm.sh +106 -0
  190. data/lib/octo/tools/terminal/session_manager.rb +213 -0
  191. data/lib/octo/tools/terminal.rb +1828 -0
  192. data/lib/octo/tools/todo_manager.rb +374 -0
  193. data/lib/octo/tools/trash_manager.rb +388 -0
  194. data/lib/octo/tools/undo_task.rb +35 -0
  195. data/lib/octo/tools/web_fetch.rb +242 -0
  196. data/lib/octo/tools/web_search.rb +260 -0
  197. data/lib/octo/tools/write.rb +77 -0
  198. data/lib/octo/ui2/block_font.rb +10 -0
  199. data/lib/octo/ui2/components/base_component.rb +163 -0
  200. data/lib/octo/ui2/components/command_suggestions.rb +290 -0
  201. data/lib/octo/ui2/components/common_component.rb +96 -0
  202. data/lib/octo/ui2/components/inline_input.rb +226 -0
  203. data/lib/octo/ui2/components/input_area.rb +1338 -0
  204. data/lib/octo/ui2/components/message_component.rb +99 -0
  205. data/lib/octo/ui2/components/modal_component.rb +419 -0
  206. data/lib/octo/ui2/components/todo_area.rb +149 -0
  207. data/lib/octo/ui2/components/tool_component.rb +107 -0
  208. data/lib/octo/ui2/components/welcome_banner.rb +139 -0
  209. data/lib/octo/ui2/layout_manager.rb +807 -0
  210. data/lib/octo/ui2/line_editor.rb +363 -0
  211. data/lib/octo/ui2/markdown_renderer.rb +100 -0
  212. data/lib/octo/ui2/output_buffer.rb +370 -0
  213. data/lib/octo/ui2/progress_handle.rb +362 -0
  214. data/lib/octo/ui2/progress_indicator.rb +55 -0
  215. data/lib/octo/ui2/screen_buffer.rb +273 -0
  216. data/lib/octo/ui2/terminal_detector.rb +119 -0
  217. data/lib/octo/ui2/theme_manager.rb +85 -0
  218. data/lib/octo/ui2/themes/base_theme.rb +105 -0
  219. data/lib/octo/ui2/themes/hacker_theme.rb +62 -0
  220. data/lib/octo/ui2/themes/minimal_theme.rb +56 -0
  221. data/lib/octo/ui2/thinking_verbs.rb +26 -0
  222. data/lib/octo/ui2/ui_controller.rb +1625 -0
  223. data/lib/octo/ui2/view_renderer.rb +177 -0
  224. data/lib/octo/ui2.rb +40 -0
  225. data/lib/octo/ui_interface.rb +154 -0
  226. data/lib/octo/utils/arguments_parser.rb +191 -0
  227. data/lib/octo/utils/browser_detector.rb +195 -0
  228. data/lib/octo/utils/encoding.rb +92 -0
  229. data/lib/octo/utils/environment_detector.rb +140 -0
  230. data/lib/octo/utils/file_ignore_helper.rb +170 -0
  231. data/lib/octo/utils/file_processor.rb +601 -0
  232. data/lib/octo/utils/gitignore_parser.rb +154 -0
  233. data/lib/octo/utils/limit_stack.rb +152 -0
  234. data/lib/octo/utils/logger.rb +124 -0
  235. data/lib/octo/utils/login_shell.rb +72 -0
  236. data/lib/octo/utils/model_pricing.rb +646 -0
  237. data/lib/octo/utils/parser_manager.rb +165 -0
  238. data/lib/octo/utils/path_helper.rb +15 -0
  239. data/lib/octo/utils/scripts_manager.rb +59 -0
  240. data/lib/octo/utils/string_matcher.rb +158 -0
  241. data/lib/octo/utils/trash_directory.rb +112 -0
  242. data/lib/octo/utils/workspace_rules.rb +46 -0
  243. data/lib/octo/version.rb +5 -0
  244. data/lib/octo/web/app.css +7141 -0
  245. data/lib/octo/web/app.js +543 -0
  246. data/lib/octo/web/apple-touch-icon.png +0 -0
  247. data/lib/octo/web/auth.js +150 -0
  248. data/lib/octo/web/channels.js +276 -0
  249. data/lib/octo/web/datepicker.js +205 -0
  250. data/lib/octo/web/favicon.png +0 -0
  251. data/lib/octo/web/i18n.js +1073 -0
  252. data/lib/octo/web/icon-512.png +0 -0
  253. data/lib/octo/web/icon-dark.svg +25 -0
  254. data/lib/octo/web/icon.svg +29 -0
  255. data/lib/octo/web/index.html +871 -0
  256. data/lib/octo/web/marked.min.js +69 -0
  257. data/lib/octo/web/onboard.js +491 -0
  258. data/lib/octo/web/profile.js +442 -0
  259. data/lib/octo/web/sessions.js +4421 -0
  260. data/lib/octo/web/settings.js +913 -0
  261. data/lib/octo/web/sidebar.js +32 -0
  262. data/lib/octo/web/skills.js +885 -0
  263. data/lib/octo/web/tasks.js +297 -0
  264. data/lib/octo/web/theme.js +105 -0
  265. data/lib/octo/web/trash.js +343 -0
  266. data/lib/octo/web/vendor/hljs/highlight.min.js +1244 -0
  267. data/lib/octo/web/vendor/hljs/hljs-theme.css +95 -0
  268. data/lib/octo/web/vendor/katex/auto-render.min.js +1 -0
  269. data/lib/octo/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  270. data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  271. data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  272. data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  273. data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  274. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  275. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  276. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  277. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  278. data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  279. data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  280. data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  281. data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  282. data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  283. data/lib/octo/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  284. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  285. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  286. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  287. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  288. data/lib/octo/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  289. data/lib/octo/web/vendor/katex/katex.min.css +1 -0
  290. data/lib/octo/web/vendor/katex/katex.min.js +1 -0
  291. data/lib/octo/web/version.js +449 -0
  292. data/lib/octo/web/weixin-qr.html +209 -0
  293. data/lib/octo/web/ws-dispatcher.js +357 -0
  294. data/lib/octo/web/ws.js +128 -0
  295. data/lib/octo.rb +145 -0
  296. data/scripts/build/build.sh +329 -0
  297. data/scripts/build/lib/apt.sh +56 -0
  298. data/scripts/build/lib/brew.sh +89 -0
  299. data/scripts/build/lib/colors.sh +17 -0
  300. data/scripts/build/lib/gem.sh +95 -0
  301. data/scripts/build/lib/mise.sh +125 -0
  302. data/scripts/build/lib/network.sh +157 -0
  303. data/scripts/build/lib/os.sh +57 -0
  304. data/scripts/build/lib/shell.sh +37 -0
  305. data/scripts/build/src/install.sh.cc +174 -0
  306. data/scripts/build/src/install_browser.sh.cc +101 -0
  307. data/scripts/build/src/install_full.sh.cc +290 -0
  308. data/scripts/build/src/install_rails_deps.sh.cc +145 -0
  309. data/scripts/build/src/install_system_deps.sh.cc +123 -0
  310. data/scripts/build/src/uninstall.sh.cc +101 -0
  311. data/scripts/install.ps1 +532 -0
  312. data/scripts/install.sh +567 -0
  313. data/scripts/install_browser.sh +479 -0
  314. data/scripts/install_full.sh +838 -0
  315. data/scripts/install_rails_deps.sh +746 -0
  316. data/scripts/install_system_deps.sh +518 -0
  317. data/scripts/uninstall.sh +287 -0
  318. data/sig/octo.rbs +4 -0
  319. metadata +614 -0
@@ -0,0 +1,320 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../adapters/base"
4
+ require_relative "bot"
5
+ require_relative "message_parser"
6
+ require_relative "file_processor"
7
+ require_relative "ws_client"
8
+
9
+ module Octo
10
+ module Channel
11
+ module Adapters
12
+ module Feishu
13
+ DEFAULT_DOMAIN = "https://open.feishu.cn"
14
+
15
+ # Feishu adapter implementation.
16
+ # Handles message receiving via WebSocket and sending via Bot API.
17
+ class Adapter < Base
18
+
19
+ def self.platform_id
20
+ :feishu
21
+ end
22
+
23
+ def self.env_keys
24
+ %w[IM_FEISHU_APP_ID IM_FEISHU_APP_SECRET IM_FEISHU_DOMAIN IM_FEISHU_ALLOWED_USERS]
25
+ end
26
+
27
+ def self.platform_config(data)
28
+ {
29
+ app_id: data["IM_FEISHU_APP_ID"],
30
+ app_secret: data["IM_FEISHU_APP_SECRET"],
31
+ domain: data["IM_FEISHU_DOMAIN"] || DEFAULT_DOMAIN,
32
+ allowed_users: data["IM_FEISHU_ALLOWED_USERS"]&.split(",")&.map(&:strip)&.reject(&:empty?)
33
+ }
34
+ end
35
+
36
+ def self.set_env_data(data, config)
37
+ data["IM_FEISHU_APP_ID"] = config[:app_id]
38
+ data["IM_FEISHU_APP_SECRET"] = config[:app_secret]
39
+ data["IM_FEISHU_DOMAIN"] = config[:domain] if config[:domain]
40
+ data["IM_FEISHU_ALLOWED_USERS"] = Array(config[:allowed_users]).join(",")
41
+ end
42
+
43
+ # Test connectivity with provided credentials (does not persist).
44
+ # @param fields [Hash] symbol-keyed credential fields
45
+ # @return [Hash] { ok: Boolean, message: String }
46
+ def self.test_connection(fields)
47
+ app_id = fields[:app_id].to_s.strip
48
+ app_secret = fields[:app_secret].to_s.strip
49
+ domain = fields[:domain].to_s.strip
50
+ domain = DEFAULT_DOMAIN if domain.empty?
51
+
52
+ return { ok: false, error: "app_id is required" } if app_id.empty?
53
+ return { ok: false, error: "app_secret is required" } if app_secret.empty?
54
+
55
+ bot = Bot.new(app_id: app_id, app_secret: app_secret, domain: domain)
56
+ # Attempt to fetch a tenant access token — success means credentials are valid.
57
+ token = bot.tenant_access_token
58
+ if token && !token.empty?
59
+ { ok: true, message: "Connected — tenant access token obtained" }
60
+ else
61
+ { ok: false, error: "Empty token returned — check app_id and app_secret" }
62
+ end
63
+ rescue StandardError => e
64
+ { ok: false, error: e.message }
65
+ end
66
+
67
+ def initialize(config)
68
+ @config = config
69
+ @bot = Bot.new(
70
+ app_id: config[:app_id],
71
+ app_secret: config[:app_secret],
72
+ domain: config[:domain] || DEFAULT_DOMAIN
73
+ )
74
+ @ws_client = nil
75
+ @running = false
76
+ @doc_retry_cache = {} # { chat_id => { doc_urls: [...], attempts: N } }
77
+ end
78
+
79
+ # Start listening for messages via WebSocket
80
+ # @yield [event] Yields standardized inbound messages
81
+ # @return [void]
82
+ def start(&on_message)
83
+ @running = true
84
+ @on_message = on_message
85
+
86
+ @ws_client = WSClient.new(
87
+ app_id: @config[:app_id],
88
+ app_secret: @config[:app_secret],
89
+ domain: @config[:domain] || DEFAULT_DOMAIN
90
+ )
91
+
92
+ @ws_client.start do |raw_event|
93
+ handle_event(raw_event)
94
+ end
95
+ end
96
+
97
+ # Stop the adapter
98
+ # @return [void]
99
+ def stop
100
+ @running = false
101
+ @ws_client&.stop
102
+ end
103
+
104
+ # Send plain text message
105
+ # @param chat_id [String] Chat ID
106
+ # @param text [String] Message text
107
+ # @param reply_to [String, nil] Message ID to reply to
108
+ # @return [Hash] Result with :message_id
109
+ def send_text(chat_id, text, reply_to: nil)
110
+ @bot.send_text(chat_id, text, reply_to: reply_to)
111
+ end
112
+
113
+ # Send a file (or image) to a chat.
114
+ # @param chat_id [String] Chat ID
115
+ # @param path [String] Local file path
116
+ # @param name [String, nil] Display filename
117
+ # @param reply_to [String, nil] Message ID to reply to
118
+ def send_file(chat_id, path, name: nil, reply_to: nil)
119
+ @bot.send_file(chat_id, path, name: name, reply_to: reply_to)
120
+ end
121
+
122
+ # Update existing message
123
+ # @param chat_id [String] Chat ID (unused for Feishu)
124
+ # @param message_id [String] Message ID to update
125
+ # @param text [String] New text
126
+ # @return [Boolean] Success status
127
+ def update_message(chat_id, message_id, text)
128
+ @bot.update_message(message_id, text)
129
+ end
130
+
131
+ # @return [Boolean]
132
+ def supports_message_updates?
133
+ true
134
+ end
135
+
136
+ # Validate configuration
137
+ # @param config [Hash] Configuration to validate
138
+ # @return [Array<String>] Error messages
139
+ def validate_config(config)
140
+ errors = []
141
+ errors << "app_id is required" if config[:app_id].nil? || config[:app_id].empty?
142
+ errors << "app_secret is required" if config[:app_secret].nil? || config[:app_secret].empty?
143
+ errors
144
+ end
145
+
146
+
147
+ # Handle incoming WebSocket event
148
+ # @param raw_event [Hash] Raw event data
149
+ # @return [void]
150
+ def handle_event(raw_event)
151
+ parsed = MessageParser.parse(raw_event)
152
+ return unless parsed
153
+
154
+ case parsed[:type]
155
+ when :message
156
+ handle_message_event(parsed)
157
+ when :challenge
158
+ # Challenge is handled by MessageParser
159
+ end
160
+ rescue => e
161
+ Octo::Logger.warn("[feishu] Error handling event: #{e.message}")
162
+ Octo::Logger.warn(e.backtrace.first(5).join("\n"))
163
+ end
164
+
165
+ # Handle message event
166
+ # @param event [Hash] Parsed message event
167
+ # @return [void]
168
+ def handle_message_event(event)
169
+ # In group chats, only respond when the bot is explicitly @-mentioned.
170
+ # Private chats always respond.
171
+ # Fail closed: if the bot's own open_id cannot be fetched (API error,
172
+ # bad credentials, etc.), drop group messages instead of responding to
173
+ # every message and spamming the group.
174
+ if event[:chat_type] == :group
175
+ bot_id = @bot.bot_open_id
176
+ if bot_id.nil?
177
+ Octo::Logger.warn("[feishu] bot_open_id unavailable; dropping group message to avoid spam")
178
+ return
179
+ end
180
+ return unless Array(event[:mentioned_open_ids]).include?(bot_id)
181
+ end
182
+
183
+ allowed_users = @config[:allowed_users]
184
+ if allowed_users && !allowed_users.empty?
185
+ return unless allowed_users.include?(event[:user_id])
186
+ end
187
+
188
+ # Download images and attach as file hashes
189
+ image_files = []
190
+ if event[:image_keys] && !event[:image_keys].empty?
191
+ image_files, errors = download_images(event[:image_keys], event[:message_id])
192
+ if image_files.empty? && !errors.empty?
193
+ @bot.send_text(event[:chat_id], "#{errors.first}", reply_to: event[:message_id])
194
+ return
195
+ end
196
+ end
197
+
198
+ # Download and process file attachments
199
+ disk_files = []
200
+ if event[:file_attachments] && !event[:file_attachments].empty?
201
+ disk_files = process_files(event[:file_attachments], event[:message_id])
202
+ end
203
+
204
+ all_files = image_files + disk_files
205
+ event = event.merge(files: all_files) unless all_files.empty?
206
+
207
+ # Merge cached doc_urls (from previous failed attempts) into current event
208
+ cached = @doc_retry_cache[event[:chat_id]]
209
+ if cached
210
+ merged_urls = ((event[:doc_urls] || []) + cached[:doc_urls]).uniq
211
+ event = event.merge(doc_urls: merged_urls)
212
+ end
213
+
214
+ # Fetch Feishu document content for any doc URLs in the message
215
+ if event[:doc_urls] && !event[:doc_urls].empty?
216
+ event = enrich_with_doc_content(event)
217
+ return if event.nil?
218
+ end
219
+
220
+ @on_message&.call(event)
221
+ end
222
+
223
+ # Fetch Feishu document content and append to event[:text].
224
+ # If the app lacks permission (91403), sends a guidance message and returns nil
225
+ # so the caller can skip forwarding the event to the agent.
226
+ # @param event [Hash]
227
+ # @return [Hash, nil] enriched event or nil if permission error
228
+ DOC_RETRY_MAX = 3
229
+
230
+ def enrich_with_doc_content(event)
231
+ doc_sections = []
232
+ failed_urls = []
233
+
234
+ event[:doc_urls].each do |url|
235
+ content = @bot.fetch_doc_content(url)
236
+ doc_sections << "📄 [Doc content from #{url}]\n#{content}" unless content.empty?
237
+ rescue Feishu::FeishuDocPermissionError
238
+ failed_urls << url
239
+ doc_sections << "#{url}\n[System Notice] Cannot read the above Feishu doc: the app has no access (error 91403). Tell user to: open the doc → top-right \"...\" → \"Add Document App\" → add this bot → just send any message to retry."
240
+ rescue Feishu::FeishuDocScopeError => e
241
+ failed_urls << url
242
+ scope_hint = e.auth_url ? "Admin can approve with one click: [点击授权](#{e.auth_url})" : "Admin needs to enable 'docx:document:readonly' scope in Feishu Open Platform."
243
+ doc_sections << "#{url}\n[System Notice] Cannot read the above Feishu doc: app is missing docx API scope (error 99991672). #{scope_hint} Tell user to just send any message to retry after approval."
244
+ rescue => e
245
+ failed_urls << url
246
+ Octo::Logger.warn("[feishu] Failed to fetch doc #{url}: #{e.message}")
247
+ doc_sections << "#{url}\n[System Notice] Cannot read the above Feishu doc: #{e.message}. Tell user to just send any message to retry."
248
+ end
249
+
250
+ # Update retry cache
251
+ chat_id = event[:chat_id]
252
+ if failed_urls.any?
253
+ existing = @doc_retry_cache[chat_id]
254
+ attempts = (existing&.dig(:attempts) || 0) + 1
255
+ if attempts >= DOC_RETRY_MAX
256
+ @doc_retry_cache.delete(chat_id)
257
+ else
258
+ @doc_retry_cache[chat_id] = { doc_urls: failed_urls, attempts: attempts }
259
+ end
260
+ else
261
+ # All docs fetched successfully, clear cache
262
+ @doc_retry_cache.delete(chat_id)
263
+ end
264
+
265
+ return event if doc_sections.empty?
266
+
267
+ enriched_text = [event[:text], *doc_sections].reject(&:empty?).join("\n\n")
268
+ event.merge(text: enriched_text)
269
+ end
270
+
271
+ MAX_IMAGE_BYTES = Octo::Utils::FileProcessor::MAX_IMAGE_BYTES
272
+
273
+ # Download images from Feishu and return as file hashes.
274
+ # Images within MAX_IMAGE_BYTES are returned with data_url for vision.
275
+ # Oversized images are rejected with an error message.
276
+ # @param image_keys [Array<String>]
277
+ # @param message_id [String]
278
+ # @return [Array<Hash>, Array<String>] [file_hashes, error_messages]
279
+ def download_images(image_keys, message_id)
280
+ require "base64"
281
+ file_hashes = []
282
+ errors = []
283
+ image_keys.each do |image_key|
284
+ result = @bot.download_message_resource(message_id, image_key, type: "image")
285
+ if result[:body].bytesize > MAX_IMAGE_BYTES
286
+ errors << "Image too large (#{(result[:body].bytesize / 1024.0).round(0).to_i}KB), max #{MAX_IMAGE_BYTES / 1024}KB"
287
+ next
288
+ end
289
+ mime = result[:content_type]
290
+ mime = "image/jpeg" if mime.nil? || mime.empty? || !mime.start_with?("image/")
291
+ data_url = "data:#{mime};base64,#{Base64.strict_encode64(result[:body])}"
292
+ file_hashes << { name: "image.jpg", mime_type: mime, data_url: data_url }
293
+ rescue => e
294
+ Octo::Logger.warn("[feishu] Failed to download image #{image_key}: #{e.message}")
295
+ errors << "Image download failed: #{e.message}"
296
+ end
297
+ [file_hashes, errors]
298
+ end
299
+
300
+ # Download and save file attachments, returning file hashes for agent.
301
+ # Parsing happens inside agent.run, not here.
302
+ # @param attachments [Array<Hash>] [{key:, name:}]
303
+ # @param message_id [String]
304
+ # @return [Array<Hash>] { name:, path: }
305
+ def process_files(attachments, message_id)
306
+ attachments.filter_map do |attachment|
307
+ result = @bot.download_message_resource(message_id, attachment[:key], type: "file")
308
+ Octo::Utils::FileProcessor.save(body: result[:body], filename: attachment[:name])
309
+ rescue => e
310
+ Octo::Logger.warn("[feishu] Failed to download file #{attachment[:name]}: #{e.message}")
311
+ nil
312
+ end.compact
313
+ end
314
+ end
315
+
316
+ Adapters.register(:feishu, Adapter)
317
+ end
318
+ end
319
+ end
320
+ end