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,191 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# dingtalk_setup.rb — DingTalk channel setup via Device Flow (QR scan).
|
|
5
|
+
#
|
|
6
|
+
# Modes:
|
|
7
|
+
# --print-qr Phase 1+2: call init/begin, print QR URL as JSON, exit immediately.
|
|
8
|
+
# --poll <device_code> Phase 3+4+5: poll until SUCCESS, save credentials, wait for WS.
|
|
9
|
+
#
|
|
10
|
+
# Environment:
|
|
11
|
+
# OCTO_SERVER_PORT, OCTO_SERVER_HOST — octo server coordinates
|
|
12
|
+
|
|
13
|
+
require "json"
|
|
14
|
+
require "net/http"
|
|
15
|
+
require "net/https"
|
|
16
|
+
require "uri"
|
|
17
|
+
|
|
18
|
+
DINGTALK_REG_BASE = "https://oapi.dingtalk.com"
|
|
19
|
+
# Registration source ID assigned by DingTalk (not a brand string — do not rebrand).
|
|
20
|
+
DINGTALK_REG_SOURCE = "DING_DWS_CLAW"
|
|
21
|
+
POLL_INTERVAL = 3
|
|
22
|
+
POLL_TIMEOUT = 300
|
|
23
|
+
|
|
24
|
+
OCTO_SERVER_URL = begin
|
|
25
|
+
url = "http://#{ENV.fetch("OCTO_SERVER_HOST")}:#{ENV.fetch("OCTO_SERVER_PORT")}"
|
|
26
|
+
uri = URI.parse(url)
|
|
27
|
+
raise "Invalid OCTO_SERVER_URL: #{url}" unless uri.is_a?(URI::HTTP) && uri.host && uri.port
|
|
28
|
+
url
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def step(msg); puts("[dingtalk-setup] #{msg}"); end
|
|
32
|
+
def ok(msg); puts("[dingtalk-setup] ✅ #{msg}"); end
|
|
33
|
+
def warn(msg); puts("[dingtalk-setup] ⚠️ #{msg}"); end
|
|
34
|
+
def fail!(msg)
|
|
35
|
+
puts("[dingtalk-setup] ❌ #{msg}")
|
|
36
|
+
exit 1
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def post_json(url, payload)
|
|
40
|
+
uri = URI.parse(url)
|
|
41
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
42
|
+
http.use_ssl = uri.scheme == "https"
|
|
43
|
+
req = Net::HTTP::Post.new(uri.path, "Content-Type" => "application/json")
|
|
44
|
+
req.body = JSON.generate(payload)
|
|
45
|
+
resp = http.request(req)
|
|
46
|
+
data = JSON.parse(resp.body)
|
|
47
|
+
fail! "API error (#{resp.code}): #{data["errmsg"] || resp.body}" if data["errcode"] && data["errcode"] != 0
|
|
48
|
+
data
|
|
49
|
+
rescue JSON::ParserError => e
|
|
50
|
+
fail! "JSON parse error from #{url}: #{e.message}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def server_post(path, body)
|
|
54
|
+
uri = URI(OCTO_SERVER_URL)
|
|
55
|
+
Net::HTTP.start(uri.host, uri.port, open_timeout: 3, read_timeout: 10) do |h|
|
|
56
|
+
req = Net::HTTP::Post.new(path, "Content-Type" => "application/json")
|
|
57
|
+
req.body = JSON.generate(body)
|
|
58
|
+
h.request(req)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def server_get(path)
|
|
63
|
+
uri = URI(OCTO_SERVER_URL)
|
|
64
|
+
Net::HTTP.start(uri.host, uri.port, open_timeout: 3, read_timeout: 10) do |h|
|
|
65
|
+
h.request(Net::HTTP::Get.new(path))
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# ── Mode: --print-qr ─────────────────────────────────────────────────────────
|
|
70
|
+
# Call init + begin, print JSON with qr_url / device_code / expires_in, exit 0.
|
|
71
|
+
def mode_print_qr
|
|
72
|
+
step "Phase 1 — Starting DingTalk Device Flow registration..."
|
|
73
|
+
|
|
74
|
+
init_data = post_json("#{DINGTALK_REG_BASE}/app/registration/init",
|
|
75
|
+
{ source: DINGTALK_REG_SOURCE })
|
|
76
|
+
nonce = init_data["nonce"].to_s.strip
|
|
77
|
+
fail! "Missing nonce in init response" if nonce.empty?
|
|
78
|
+
|
|
79
|
+
begin_data = post_json("#{DINGTALK_REG_BASE}/app/registration/begin", { nonce: nonce })
|
|
80
|
+
device_code = begin_data["device_code"].to_s.strip
|
|
81
|
+
qr_url = begin_data["verification_uri_complete"].to_s.strip
|
|
82
|
+
expires_in = (begin_data["expires_in"] || POLL_TIMEOUT).to_i
|
|
83
|
+
|
|
84
|
+
fail! "Missing device_code in begin response" if device_code.empty?
|
|
85
|
+
fail! "Missing verification_uri_complete" if qr_url.empty?
|
|
86
|
+
|
|
87
|
+
ok "Device Flow started. QR expires in #{expires_in}s."
|
|
88
|
+
puts JSON.generate({ qr_url: qr_url, device_code: device_code, expires_in: expires_in })
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# ── Mode: --poll <device_code> ────────────────────────────────────────────────
|
|
92
|
+
# Poll until SUCCESS or a terminal state. Exits with:
|
|
93
|
+
# 0 — SUCCESS: credentials saved and adapter started
|
|
94
|
+
# 2 — WAITING: user hasn't scanned yet (Agent should ask user to scan and retry)
|
|
95
|
+
# 1 — terminal failure (expired, fail, or server error)
|
|
96
|
+
def mode_poll(device_code, expires_in: POLL_TIMEOUT, interval: POLL_INTERVAL)
|
|
97
|
+
step "Phase 3 — Checking DingTalk authorization..."
|
|
98
|
+
|
|
99
|
+
client_id = nil
|
|
100
|
+
client_secret = nil
|
|
101
|
+
deadline = Time.now + expires_in
|
|
102
|
+
|
|
103
|
+
loop do
|
|
104
|
+
if Time.now > deadline
|
|
105
|
+
puts "[dingtalk-setup] WAITING_TIMEOUT"
|
|
106
|
+
exit 2
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
poll_data = post_json("#{DINGTALK_REG_BASE}/app/registration/poll",
|
|
110
|
+
{ device_code: device_code })
|
|
111
|
+
status = poll_data["status"].to_s.upcase
|
|
112
|
+
|
|
113
|
+
case status
|
|
114
|
+
when "WAITING"
|
|
115
|
+
puts "[dingtalk-setup] WAITING"
|
|
116
|
+
exit 2
|
|
117
|
+
when "SUCCESS"
|
|
118
|
+
client_id = poll_data["client_id"].to_s.strip
|
|
119
|
+
client_secret = poll_data["client_secret"].to_s.strip
|
|
120
|
+
fail! "Authorization succeeded but missing client credentials" if client_id.empty? || client_secret.empty?
|
|
121
|
+
ok "Authorization complete! client_id=#{client_id}"
|
|
122
|
+
break
|
|
123
|
+
when "EXPIRED"
|
|
124
|
+
fail! "Authorization QR code expired. Please re-run."
|
|
125
|
+
when "FAIL"
|
|
126
|
+
fail! "Authorization failed: #{poll_data["fail_reason"] || "unknown reason"}"
|
|
127
|
+
else
|
|
128
|
+
warn "Unknown status=#{status}, retrying..."
|
|
129
|
+
sleep interval
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# ── Phase 4: Save credentials to octo server ─────────────────────────────
|
|
134
|
+
step "Phase 4 — Saving credentials to octo server..."
|
|
135
|
+
|
|
136
|
+
begin
|
|
137
|
+
res = server_post("/api/channels/dingtalk",
|
|
138
|
+
{ client_id: client_id, client_secret: client_secret, enabled: true })
|
|
139
|
+
if res.code.to_i == 200
|
|
140
|
+
ok "Credentials saved, DingTalk Stream adapter starting..."
|
|
141
|
+
else
|
|
142
|
+
body = JSON.parse(res.body) rescue { "error" => res.body }
|
|
143
|
+
fail! "Server rejected credentials: #{body["error"] || res.body}"
|
|
144
|
+
end
|
|
145
|
+
rescue StandardError => e
|
|
146
|
+
fail! "Could not reach octo server: #{e.message}"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# ── Phase 5: Wait for Stream Mode WebSocket to connect ─────────────────────
|
|
150
|
+
step "Phase 5 — Waiting for DingTalk Stream connection..."
|
|
151
|
+
|
|
152
|
+
ws_ready = false
|
|
153
|
+
ws_deadline = Time.now + 30
|
|
154
|
+
|
|
155
|
+
loop do
|
|
156
|
+
break if Time.now > ws_deadline
|
|
157
|
+
begin
|
|
158
|
+
res = server_get("/api/channels")
|
|
159
|
+
channels = JSON.parse(res.body)["channels"] || []
|
|
160
|
+
dingtalk = channels.find { |c| c["platform"] == "dingtalk" }
|
|
161
|
+
if dingtalk&.fetch("running", false)
|
|
162
|
+
ws_ready = true
|
|
163
|
+
break
|
|
164
|
+
end
|
|
165
|
+
rescue StandardError => e
|
|
166
|
+
warn "Channel status check failed: #{e.message}"
|
|
167
|
+
end
|
|
168
|
+
sleep 2
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
if ws_ready
|
|
172
|
+
ok "DingTalk Stream WebSocket connected."
|
|
173
|
+
else
|
|
174
|
+
warn "Stream connection not confirmed within 30s — it may still be starting."
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
ok "🎉 DingTalk channel setup complete! Search for your robot in DingTalk to start chatting."
|
|
178
|
+
ok " client_id: #{client_id}"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# ── Entry point ───────────────────────────────────────────────────────────────
|
|
182
|
+
case ARGV[0]
|
|
183
|
+
when "--print-qr"
|
|
184
|
+
mode_print_qr
|
|
185
|
+
when "--poll"
|
|
186
|
+
device_code = ARGV[1].to_s.strip
|
|
187
|
+
fail! "Usage: dingtalk_setup.rb --poll <device_code>" if device_code.empty?
|
|
188
|
+
mode_poll(device_code)
|
|
189
|
+
else
|
|
190
|
+
fail! "Usage: dingtalk_setup.rb --print-qr | --poll <device_code>"
|
|
191
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# discord_setup.rb — Discord channel setup helper.
|
|
5
|
+
#
|
|
6
|
+
# Discord's developer portal requires manual interaction (hCaptcha + private API), so the
|
|
7
|
+
# Agent uses the browser only as a container — it navigates to the portal and the user
|
|
8
|
+
# creates the App manually, then pastes back the bot token and application id. This
|
|
9
|
+
# script handles everything a shell can do: emit the portal URL, validate the token
|
|
10
|
+
# against /users/@me, save to the octo server, generate the OAuth2 invite URL, and
|
|
11
|
+
# poll until the bot is in at least one guild.
|
|
12
|
+
#
|
|
13
|
+
# Modes:
|
|
14
|
+
# --portal-url Print the Discord developer portal URL (stdout, single line)
|
|
15
|
+
# --validate <token> Validate bot_token via /users/@me, then POST to server
|
|
16
|
+
# --invite-url <client_id> Print the OAuth2 invite URL (stdout, single line)
|
|
17
|
+
# --watch-guild Long-poll /users/@me/guilds via the saved token
|
|
18
|
+
# until at least one guild appears (or timeout)
|
|
19
|
+
# --bot-info <token> Print {id, username} JSON for an unsaved token
|
|
20
|
+
#
|
|
21
|
+
# Environment:
|
|
22
|
+
# OCTO_SERVER_HOST default 127.0.0.1
|
|
23
|
+
# OCTO_SERVER_PORT default 8888
|
|
24
|
+
|
|
25
|
+
require "json"
|
|
26
|
+
require "net/http"
|
|
27
|
+
require "net/https"
|
|
28
|
+
require "uri"
|
|
29
|
+
require "openssl"
|
|
30
|
+
require "cgi"
|
|
31
|
+
require "yaml"
|
|
32
|
+
|
|
33
|
+
DISCORD_API_BASE = "https://discord.com/api/v10"
|
|
34
|
+
DISCORD_OAUTH_BASE = "https://discord.com/oauth2/authorize"
|
|
35
|
+
DISCORD_PORTAL_URL = "https://discord.com/developers/applications"
|
|
36
|
+
DEFAULT_BOT_PERMS = "274877990912"
|
|
37
|
+
DEFAULT_BOT_SCOPES = "bot applications.commands"
|
|
38
|
+
WATCH_GUILD_DEADLINE = 10 * 60
|
|
39
|
+
WATCH_GUILD_INTERVAL = 3
|
|
40
|
+
USER_AGENT = "DiscordBot (https://github.com/octoai/octo, 1.0)"
|
|
41
|
+
|
|
42
|
+
OCTO_SERVER_URL = begin
|
|
43
|
+
host = ENV.fetch("OCTO_SERVER_HOST", "127.0.0.1")
|
|
44
|
+
port = ENV.fetch("OCTO_SERVER_PORT", "8888")
|
|
45
|
+
"http://#{host}:#{port}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def step(msg); $stderr.puts("[discord-setup] #{msg}"); end
|
|
49
|
+
def ok(msg); $stderr.puts("[discord-setup] #{msg}"); end
|
|
50
|
+
def warn!(msg); $stderr.puts("[discord-setup] #{msg}"); end
|
|
51
|
+
|
|
52
|
+
def fail!(msg, json: false)
|
|
53
|
+
if json
|
|
54
|
+
$stdout.puts(JSON.generate({ error: msg }))
|
|
55
|
+
else
|
|
56
|
+
$stderr.puts("[discord-setup] #{msg}")
|
|
57
|
+
end
|
|
58
|
+
exit 1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def discord_get(path, bot_token:, timeout: 15)
|
|
62
|
+
uri = URI("#{DISCORD_API_BASE}#{path}")
|
|
63
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
64
|
+
http.use_ssl = true
|
|
65
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
66
|
+
http.read_timeout = timeout
|
|
67
|
+
http.open_timeout = 10
|
|
68
|
+
|
|
69
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
|
70
|
+
req["Authorization"] = "Bot #{bot_token}"
|
|
71
|
+
req["User-Agent"] = USER_AGENT
|
|
72
|
+
req["Accept"] = "application/json"
|
|
73
|
+
|
|
74
|
+
res = http.request(req)
|
|
75
|
+
body = res.body.to_s
|
|
76
|
+
parsed = (JSON.parse(body) rescue nil)
|
|
77
|
+
|
|
78
|
+
unless res.is_a?(Net::HTTPSuccess)
|
|
79
|
+
msg = parsed.is_a?(Hash) ? (parsed["message"] || body.slice(0, 200)) : body.slice(0, 200)
|
|
80
|
+
raise "Discord HTTP #{res.code} #{path}: #{msg}"
|
|
81
|
+
end
|
|
82
|
+
parsed
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def saved_bot_token
|
|
86
|
+
yml_path = File.expand_path("~/.octo/channels.yml")
|
|
87
|
+
return nil unless File.exist?(yml_path)
|
|
88
|
+
data = YAML.safe_load_file(yml_path, permitted_classes: [Symbol], aliases: true) rescue nil
|
|
89
|
+
data&.dig("channels", "discord", "bot_token") || data&.dig(:channels, :discord, :bot_token)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def save_to_server(bot_token:)
|
|
93
|
+
uri = URI("#{OCTO_SERVER_URL}/api/channels/discord")
|
|
94
|
+
body = JSON.generate({ bot_token: bot_token })
|
|
95
|
+
|
|
96
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
97
|
+
http.read_timeout = 30
|
|
98
|
+
http.open_timeout = 5
|
|
99
|
+
|
|
100
|
+
req = Net::HTTP::Post.new(uri.path, "Content-Type" => "application/json")
|
|
101
|
+
req.body = body
|
|
102
|
+
|
|
103
|
+
res = http.request(req)
|
|
104
|
+
data = JSON.parse(res.body) rescue {}
|
|
105
|
+
|
|
106
|
+
unless res.is_a?(Net::HTTPSuccess) && data["ok"]
|
|
107
|
+
fail!("Failed to save Discord config: #{data["error"] || res.body.slice(0, 200)}")
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
mode_idx = ARGV.index { |a| a.start_with?("--") }
|
|
112
|
+
mode = mode_idx ? ARGV[mode_idx] : nil
|
|
113
|
+
arg = mode_idx ? ARGV[mode_idx + 1] : nil
|
|
114
|
+
|
|
115
|
+
case mode
|
|
116
|
+
when "--portal-url"
|
|
117
|
+
$stdout.puts(DISCORD_PORTAL_URL)
|
|
118
|
+
exit 0
|
|
119
|
+
|
|
120
|
+
when "--validate"
|
|
121
|
+
fail!("--validate requires <bot_token>") if arg.to_s.strip.empty?
|
|
122
|
+
bot_token = arg.strip
|
|
123
|
+
step("Validating bot token against Discord API...")
|
|
124
|
+
begin
|
|
125
|
+
me = discord_get("/users/@me", bot_token: bot_token)
|
|
126
|
+
rescue => e
|
|
127
|
+
fail!("Token validation failed: #{e.message}")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
bot_id = me["id"].to_s
|
|
131
|
+
username = me["username"].to_s
|
|
132
|
+
fail!("Empty bot id from /users/@me") if bot_id.empty?
|
|
133
|
+
|
|
134
|
+
ok("Authenticated as #{username} (id=#{bot_id})")
|
|
135
|
+
step("Saving credentials via octo server...")
|
|
136
|
+
save_to_server(bot_token: bot_token)
|
|
137
|
+
ok("Discord channel configured")
|
|
138
|
+
|
|
139
|
+
$stdout.puts(JSON.generate({ bot_id: bot_id, username: username }))
|
|
140
|
+
exit 0
|
|
141
|
+
|
|
142
|
+
when "--bot-info"
|
|
143
|
+
fail!("--bot-info requires <bot_token>", json: true) if arg.to_s.strip.empty?
|
|
144
|
+
begin
|
|
145
|
+
me = discord_get("/users/@me", bot_token: arg.strip)
|
|
146
|
+
rescue => e
|
|
147
|
+
fail!(e.message, json: true)
|
|
148
|
+
end
|
|
149
|
+
$stdout.puts(JSON.generate({ bot_id: me["id"], username: me["username"] }))
|
|
150
|
+
exit 0
|
|
151
|
+
|
|
152
|
+
when "--invite-url"
|
|
153
|
+
fail!("--invite-url requires <client_id>") if arg.to_s.strip.empty?
|
|
154
|
+
client_id = arg.strip
|
|
155
|
+
url = "#{DISCORD_OAUTH_BASE}?client_id=#{CGI.escape(client_id)}" \
|
|
156
|
+
"&permissions=#{DEFAULT_BOT_PERMS}" \
|
|
157
|
+
"&scope=#{CGI.escape(DEFAULT_BOT_SCOPES)}"
|
|
158
|
+
$stdout.puts(url)
|
|
159
|
+
exit 0
|
|
160
|
+
|
|
161
|
+
when "--watch-guild"
|
|
162
|
+
bot_token = saved_bot_token
|
|
163
|
+
fail!("No saved bot_token in ~/.octo/channels.yml — run --validate first") if bot_token.to_s.empty?
|
|
164
|
+
|
|
165
|
+
step("Waiting for the bot to be added to a guild (timeout: #{WATCH_GUILD_DEADLINE / 60} min)...")
|
|
166
|
+
deadline = Time.now + WATCH_GUILD_DEADLINE
|
|
167
|
+
|
|
168
|
+
loop do
|
|
169
|
+
fail!("Timed out waiting for the bot to join a guild. Open the invite URL again to retry.") if Time.now > deadline
|
|
170
|
+
|
|
171
|
+
begin
|
|
172
|
+
guilds = discord_get("/users/@me/guilds", bot_token: bot_token)
|
|
173
|
+
rescue => e
|
|
174
|
+
warn!("Poll error (will retry): #{e.message}")
|
|
175
|
+
sleep WATCH_GUILD_INTERVAL
|
|
176
|
+
next
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
if guilds.is_a?(Array) && !guilds.empty?
|
|
180
|
+
g = guilds.first
|
|
181
|
+
ok("Bot added to guild: #{g["name"]} (id=#{g["id"]})")
|
|
182
|
+
$stdout.puts(JSON.generate({ guild_id: g["id"], guild_name: g["name"], total: guilds.length }))
|
|
183
|
+
exit 0
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
sleep WATCH_GUILD_INTERVAL
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
else
|
|
190
|
+
$stderr.puts(<<~USAGE)
|
|
191
|
+
Usage:
|
|
192
|
+
ruby discord_setup.rb --portal-url
|
|
193
|
+
ruby discord_setup.rb --validate <bot_token>
|
|
194
|
+
ruby discord_setup.rb --bot-info <bot_token>
|
|
195
|
+
ruby discord_setup.rb --invite-url <client_id>
|
|
196
|
+
ruby discord_setup.rb --watch-guild
|
|
197
|
+
USAGE
|
|
198
|
+
exit 1
|
|
199
|
+
end
|