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.
- checksums.yaml +7 -0
- data/.clacky/skills/commit/SKILL.md +423 -0
- data/.clacky/skills/gem-release/SKILL.md +199 -0
- data/.clacky/skills/gem-release/scripts/release.sh +304 -0
- data/.clacky/skills/oss-upload/SKILL.md +47 -0
- data/.octorules +106 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +76 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CONTRIBUTING.md +92 -0
- data/Dockerfile +28 -0
- data/LICENSE.txt +22 -0
- data/POSITIONING.md +46 -0
- data/README.md +134 -0
- data/README_CN.md +134 -0
- data/Rakefile +34 -0
- data/benchmark/fixtures/sample_project/Gemfile +3 -0
- data/benchmark/fixtures/sample_project/lib/api_handler.rb +32 -0
- data/benchmark/fixtures/sample_project/lib/order_calculator.rb +23 -0
- data/benchmark/fixtures/sample_project/lib/user_renderer.rb +20 -0
- data/benchmark/fixtures/sample_project/spec/order_calculator_spec.rb +20 -0
- data/benchmark/results/EVALUATION_REPORT.md +165 -0
- data/benchmark/results/baseline_20260511_174424.json +128 -0
- data/benchmark/results/report_20260511_175256.json +271 -0
- data/benchmark/results/report_20260511_175444.json +271 -0
- data/benchmark/results/treatment_20260511_175103.json +130 -0
- data/benchmark/runner.rb +441 -0
- data/bin/octo +7 -0
- data/docs/agent-first-ui-design.md +77 -0
- data/docs/billing-system.md +318 -0
- data/docs/channel-architecture.md +235 -0
- data/docs/engineering-article.md +343 -0
- data/docs/session-skill-invocation.md +69 -0
- data/docs/time_machine_design.md +247 -0
- data/docs/ui2-architecture.md +124 -0
- data/homebrew/README.md +96 -0
- data/homebrew/openocto.rb +24 -0
- data/lib/octo/agent/hook_manager.rb +61 -0
- data/lib/octo/agent/llm_caller.rb +800 -0
- data/lib/octo/agent/memory_updater.rb +246 -0
- data/lib/octo/agent/message_compressor.rb +225 -0
- data/lib/octo/agent/message_compressor_helper.rb +869 -0
- data/lib/octo/agent/next_message_suggester.rb +215 -0
- data/lib/octo/agent/session_serializer.rb +685 -0
- data/lib/octo/agent/skill_auto_creator.rb +114 -0
- data/lib/octo/agent/skill_evolution.rb +61 -0
- data/lib/octo/agent/skill_manager.rb +466 -0
- data/lib/octo/agent/skill_reflector.rb +89 -0
- data/lib/octo/agent/system_prompt_builder.rb +101 -0
- data/lib/octo/agent/time_machine.rb +214 -0
- data/lib/octo/agent/tool_executor.rb +454 -0
- data/lib/octo/agent/tool_registry.rb +150 -0
- data/lib/octo/agent.rb +2180 -0
- data/lib/octo/agent_config.rb +989 -0
- data/lib/octo/agent_profile.rb +112 -0
- data/lib/octo/anthropic_stream_aggregator.rb +137 -0
- data/lib/octo/background_task_registry.rb +324 -0
- data/lib/octo/banner.rb +34 -0
- data/lib/octo/bedrock_stream_aggregator.rb +137 -0
- data/lib/octo/block_font.rb +331 -0
- data/lib/octo/cli.rb +968 -0
- data/lib/octo/client.rb +623 -0
- data/lib/octo/default_agents/SOUL.md +3 -0
- data/lib/octo/default_agents/USER.md +1 -0
- data/lib/octo/default_agents/base_prompt.md +66 -0
- data/lib/octo/default_agents/coding/profile.yml +2 -0
- data/lib/octo/default_agents/coding/system_prompt.md +67 -0
- data/lib/octo/default_agents/general/profile.yml +2 -0
- data/lib/octo/default_agents/general/system_prompt.md +16 -0
- data/lib/octo/default_parsers/doc_parser.rb +69 -0
- data/lib/octo/default_parsers/docx_parser.rb +188 -0
- data/lib/octo/default_parsers/pdf_parser.rb +120 -0
- data/lib/octo/default_parsers/pdf_parser_ocr.py +103 -0
- data/lib/octo/default_parsers/pdf_parser_plumber.py +62 -0
- data/lib/octo/default_parsers/pptx_parser.rb +140 -0
- data/lib/octo/default_parsers/xlsx_parser.rb +121 -0
- data/lib/octo/default_skills/browser-setup/SKILL.md +426 -0
- data/lib/octo/default_skills/channel-manager/SKILL.md +623 -0
- data/lib/octo/default_skills/channel-manager/dingtalk_setup.rb +191 -0
- data/lib/octo/default_skills/channel-manager/discord_setup.rb +199 -0
- data/lib/octo/default_skills/channel-manager/feishu_setup.rb +574 -0
- data/lib/octo/default_skills/channel-manager/import_lark_skills.rb +97 -0
- data/lib/octo/default_skills/channel-manager/install_feishu_skills.rb +105 -0
- data/lib/octo/default_skills/channel-manager/weixin_setup.rb +274 -0
- data/lib/octo/default_skills/code-explorer/SKILL.md +36 -0
- data/lib/octo/default_skills/cron-task-creator/SKILL.md +257 -0
- data/lib/octo/default_skills/cron-task-creator/evals/evals.json +38 -0
- data/lib/octo/default_skills/onboard/SKILL.md +578 -0
- data/lib/octo/default_skills/onboard/scripts/import_external_skills.rb +413 -0
- data/lib/octo/default_skills/onboard/scripts/install_builtin_skills.rb +97 -0
- data/lib/octo/default_skills/persist-memory/SKILL.md +59 -0
- data/lib/octo/default_skills/personal-website/SKILL.md +113 -0
- data/lib/octo/default_skills/personal-website/publish.rb +235 -0
- data/lib/octo/default_skills/product-help/SKILL.md +123 -0
- data/lib/octo/default_skills/product-help/docs/agent-config.md +74 -0
- data/lib/octo/default_skills/product-help/docs/best-practices.md +49 -0
- data/lib/octo/default_skills/product-help/docs/browser-tool.md +53 -0
- data/lib/octo/default_skills/product-help/docs/built-in-skills.md +43 -0
- data/lib/octo/default_skills/product-help/docs/cli-reference.md +82 -0
- data/lib/octo/default_skills/product-help/docs/create-your-first-skill.md +47 -0
- data/lib/octo/default_skills/product-help/docs/faq.md +98 -0
- data/lib/octo/default_skills/product-help/docs/how-to-use-a-skill.md +58 -0
- data/lib/octo/default_skills/product-help/docs/installation.md +59 -0
- data/lib/octo/default_skills/product-help/docs/memory-system.md +61 -0
- data/lib/octo/default_skills/product-help/docs/octorules.md +62 -0
- data/lib/octo/default_skills/product-help/docs/session-management.md +63 -0
- data/lib/octo/default_skills/product-help/docs/skill-basics.md +55 -0
- data/lib/octo/default_skills/product-help/docs/skill-frontmatter.md +61 -0
- data/lib/octo/default_skills/product-help/docs/web-server.md +49 -0
- data/lib/octo/default_skills/product-help/docs/what-is-octo.md +37 -0
- data/lib/octo/default_skills/product-help/docs/windows-installation.md +36 -0
- data/lib/octo/default_skills/product-help/docs/writing-tips.md +53 -0
- data/lib/octo/default_skills/recall-memory/SKILL.md +65 -0
- data/lib/octo/default_skills/skill-add/SKILL.md +59 -0
- data/lib/octo/default_skills/skill-add/scripts/install_from_zip.rb +295 -0
- data/lib/octo/default_skills/skill-creator/SKILL.md +602 -0
- data/lib/octo/default_skills/skill-creator/agents/analyzer.md +274 -0
- data/lib/octo/default_skills/skill-creator/agents/comparator.md +202 -0
- data/lib/octo/default_skills/skill-creator/agents/grader.md +223 -0
- data/lib/octo/default_skills/skill-creator/eval-viewer/generate_review.py +471 -0
- data/lib/octo/default_skills/skill-creator/eval-viewer/viewer.html +1325 -0
- data/lib/octo/default_skills/skill-creator/references/schemas.md +430 -0
- data/lib/octo/default_skills/skill-creator/scripts/__init__.py +0 -0
- data/lib/octo/default_skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- data/lib/octo/default_skills/skill-creator/scripts/generate_report.py +326 -0
- data/lib/octo/default_skills/skill-creator/scripts/improve_description.py +310 -0
- data/lib/octo/default_skills/skill-creator/scripts/quick_validate.py +103 -0
- data/lib/octo/default_skills/skill-creator/scripts/run_eval.py +317 -0
- data/lib/octo/default_skills/skill-creator/scripts/run_loop.py +331 -0
- data/lib/octo/default_skills/skill-creator/scripts/utils.py +47 -0
- data/lib/octo/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb +143 -0
- data/lib/octo/idle_compression_timer.rb +115 -0
- data/lib/octo/json_ui_controller.rb +204 -0
- data/lib/octo/message_format/anthropic.rb +409 -0
- data/lib/octo/message_format/bedrock.rb +361 -0
- data/lib/octo/message_format/open_ai.rb +222 -0
- data/lib/octo/message_history.rb +373 -0
- data/lib/octo/openai_stream_aggregator.rb +130 -0
- data/lib/octo/plain_ui_controller.rb +166 -0
- data/lib/octo/providers.rb +534 -0
- data/lib/octo/server/browser_manager.rb +397 -0
- data/lib/octo/server/channel/adapters/base.rb +82 -0
- data/lib/octo/server/channel/adapters/dingtalk/adapter.rb +314 -0
- data/lib/octo/server/channel/adapters/dingtalk/api_client.rb +391 -0
- data/lib/octo/server/channel/adapters/dingtalk/stream_client.rb +203 -0
- data/lib/octo/server/channel/adapters/discord/adapter.rb +229 -0
- data/lib/octo/server/channel/adapters/discord/api_client.rb +107 -0
- data/lib/octo/server/channel/adapters/discord/gateway_client.rb +270 -0
- data/lib/octo/server/channel/adapters/feishu/adapter.rb +320 -0
- data/lib/octo/server/channel/adapters/feishu/bot.rb +478 -0
- data/lib/octo/server/channel/adapters/feishu/file_processor.rb +36 -0
- data/lib/octo/server/channel/adapters/feishu/message_parser.rb +129 -0
- data/lib/octo/server/channel/adapters/feishu/ws_client.rb +423 -0
- data/lib/octo/server/channel/adapters/telegram/adapter.rb +375 -0
- data/lib/octo/server/channel/adapters/telegram/api_client.rb +205 -0
- data/lib/octo/server/channel/adapters/wecom/adapter.rb +148 -0
- data/lib/octo/server/channel/adapters/wecom/media_downloader.rb +115 -0
- data/lib/octo/server/channel/adapters/wecom/ws_client.rb +395 -0
- data/lib/octo/server/channel/adapters/weixin/adapter.rb +692 -0
- data/lib/octo/server/channel/adapters/weixin/api_client.rb +402 -0
- data/lib/octo/server/channel/channel_config.rb +178 -0
- data/lib/octo/server/channel/channel_manager.rb +468 -0
- data/lib/octo/server/channel/channel_ui_controller.rb +224 -0
- data/lib/octo/server/channel.rb +33 -0
- data/lib/octo/server/discover.rb +77 -0
- data/lib/octo/server/epipe_safe_io.rb +105 -0
- data/lib/octo/server/http_server.rb +3554 -0
- data/lib/octo/server/scheduler.rb +317 -0
- data/lib/octo/server/server_master.rb +325 -0
- data/lib/octo/server/session_registry.rb +431 -0
- data/lib/octo/server/web_ui_controller.rb +487 -0
- data/lib/octo/session_manager.rb +385 -0
- data/lib/octo/skill.rb +466 -0
- data/lib/octo/skill_loader.rb +328 -0
- data/lib/octo/tools/base.rb +118 -0
- data/lib/octo/tools/browser.rb +625 -0
- data/lib/octo/tools/edit.rb +165 -0
- data/lib/octo/tools/file_reader.rb +549 -0
- data/lib/octo/tools/glob.rb +162 -0
- data/lib/octo/tools/grep.rb +356 -0
- data/lib/octo/tools/invoke_skill.rb +96 -0
- data/lib/octo/tools/list_tasks.rb +54 -0
- data/lib/octo/tools/redo_task.rb +41 -0
- data/lib/octo/tools/request_user_feedback.rb +84 -0
- data/lib/octo/tools/security.rb +333 -0
- data/lib/octo/tools/terminal/output_cleaner.rb +63 -0
- data/lib/octo/tools/terminal/persistent_session.rb +268 -0
- data/lib/octo/tools/terminal/safe_rm.sh +106 -0
- data/lib/octo/tools/terminal/session_manager.rb +213 -0
- data/lib/octo/tools/terminal.rb +1828 -0
- data/lib/octo/tools/todo_manager.rb +374 -0
- data/lib/octo/tools/trash_manager.rb +388 -0
- data/lib/octo/tools/undo_task.rb +35 -0
- data/lib/octo/tools/web_fetch.rb +242 -0
- data/lib/octo/tools/web_search.rb +260 -0
- data/lib/octo/tools/write.rb +77 -0
- data/lib/octo/ui2/block_font.rb +10 -0
- data/lib/octo/ui2/components/base_component.rb +163 -0
- data/lib/octo/ui2/components/command_suggestions.rb +290 -0
- data/lib/octo/ui2/components/common_component.rb +96 -0
- data/lib/octo/ui2/components/inline_input.rb +226 -0
- data/lib/octo/ui2/components/input_area.rb +1338 -0
- data/lib/octo/ui2/components/message_component.rb +99 -0
- data/lib/octo/ui2/components/modal_component.rb +419 -0
- data/lib/octo/ui2/components/todo_area.rb +149 -0
- data/lib/octo/ui2/components/tool_component.rb +107 -0
- data/lib/octo/ui2/components/welcome_banner.rb +139 -0
- data/lib/octo/ui2/layout_manager.rb +807 -0
- data/lib/octo/ui2/line_editor.rb +363 -0
- data/lib/octo/ui2/markdown_renderer.rb +100 -0
- data/lib/octo/ui2/output_buffer.rb +370 -0
- data/lib/octo/ui2/progress_handle.rb +362 -0
- data/lib/octo/ui2/progress_indicator.rb +55 -0
- data/lib/octo/ui2/screen_buffer.rb +273 -0
- data/lib/octo/ui2/terminal_detector.rb +119 -0
- data/lib/octo/ui2/theme_manager.rb +85 -0
- data/lib/octo/ui2/themes/base_theme.rb +105 -0
- data/lib/octo/ui2/themes/hacker_theme.rb +62 -0
- data/lib/octo/ui2/themes/minimal_theme.rb +56 -0
- data/lib/octo/ui2/thinking_verbs.rb +26 -0
- data/lib/octo/ui2/ui_controller.rb +1625 -0
- data/lib/octo/ui2/view_renderer.rb +177 -0
- data/lib/octo/ui2.rb +40 -0
- data/lib/octo/ui_interface.rb +154 -0
- data/lib/octo/utils/arguments_parser.rb +191 -0
- data/lib/octo/utils/browser_detector.rb +195 -0
- data/lib/octo/utils/encoding.rb +92 -0
- data/lib/octo/utils/environment_detector.rb +140 -0
- data/lib/octo/utils/file_ignore_helper.rb +170 -0
- data/lib/octo/utils/file_processor.rb +601 -0
- data/lib/octo/utils/gitignore_parser.rb +154 -0
- data/lib/octo/utils/limit_stack.rb +152 -0
- data/lib/octo/utils/logger.rb +124 -0
- data/lib/octo/utils/login_shell.rb +72 -0
- data/lib/octo/utils/model_pricing.rb +646 -0
- data/lib/octo/utils/parser_manager.rb +165 -0
- data/lib/octo/utils/path_helper.rb +15 -0
- data/lib/octo/utils/scripts_manager.rb +59 -0
- data/lib/octo/utils/string_matcher.rb +158 -0
- data/lib/octo/utils/trash_directory.rb +112 -0
- data/lib/octo/utils/workspace_rules.rb +46 -0
- data/lib/octo/version.rb +5 -0
- data/lib/octo/web/app.css +7141 -0
- data/lib/octo/web/app.js +543 -0
- data/lib/octo/web/apple-touch-icon.png +0 -0
- data/lib/octo/web/auth.js +150 -0
- data/lib/octo/web/channels.js +276 -0
- data/lib/octo/web/datepicker.js +205 -0
- data/lib/octo/web/favicon.png +0 -0
- data/lib/octo/web/i18n.js +1073 -0
- data/lib/octo/web/icon-512.png +0 -0
- data/lib/octo/web/icon-dark.svg +25 -0
- data/lib/octo/web/icon.svg +29 -0
- data/lib/octo/web/index.html +871 -0
- data/lib/octo/web/marked.min.js +69 -0
- data/lib/octo/web/onboard.js +491 -0
- data/lib/octo/web/profile.js +442 -0
- data/lib/octo/web/sessions.js +4421 -0
- data/lib/octo/web/settings.js +913 -0
- data/lib/octo/web/sidebar.js +32 -0
- data/lib/octo/web/skills.js +885 -0
- data/lib/octo/web/tasks.js +297 -0
- data/lib/octo/web/theme.js +105 -0
- data/lib/octo/web/trash.js +343 -0
- data/lib/octo/web/vendor/hljs/highlight.min.js +1244 -0
- data/lib/octo/web/vendor/hljs/hljs-theme.css +95 -0
- data/lib/octo/web/vendor/katex/auto-render.min.js +1 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/katex.min.css +1 -0
- data/lib/octo/web/vendor/katex/katex.min.js +1 -0
- data/lib/octo/web/version.js +449 -0
- data/lib/octo/web/weixin-qr.html +209 -0
- data/lib/octo/web/ws-dispatcher.js +357 -0
- data/lib/octo/web/ws.js +128 -0
- data/lib/octo.rb +145 -0
- data/scripts/build/build.sh +329 -0
- data/scripts/build/lib/apt.sh +56 -0
- data/scripts/build/lib/brew.sh +89 -0
- data/scripts/build/lib/colors.sh +17 -0
- data/scripts/build/lib/gem.sh +95 -0
- data/scripts/build/lib/mise.sh +125 -0
- data/scripts/build/lib/network.sh +157 -0
- data/scripts/build/lib/os.sh +57 -0
- data/scripts/build/lib/shell.sh +37 -0
- data/scripts/build/src/install.sh.cc +174 -0
- data/scripts/build/src/install_browser.sh.cc +101 -0
- data/scripts/build/src/install_full.sh.cc +290 -0
- data/scripts/build/src/install_rails_deps.sh.cc +145 -0
- data/scripts/build/src/install_system_deps.sh.cc +123 -0
- data/scripts/build/src/uninstall.sh.cc +101 -0
- data/scripts/install.ps1 +532 -0
- data/scripts/install.sh +567 -0
- data/scripts/install_browser.sh +479 -0
- data/scripts/install_full.sh +838 -0
- data/scripts/install_rails_deps.sh +746 -0
- data/scripts/install_system_deps.sh +518 -0
- data/scripts/uninstall.sh +287 -0
- data/sig/octo.rbs +4 -0
- 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
|