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,449 @@
|
|
|
1
|
+
// ── Version — version check and upgrade flow ───────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Badge states:
|
|
4
|
+
// (none) → up-to-date, muted version text
|
|
5
|
+
// has-update → amber pulsing dot: new version available
|
|
6
|
+
// is-upgrading → spinning ring: gem install in progress
|
|
7
|
+
// needs-restart → orange bouncing dot: upgrade done, waiting for restart
|
|
8
|
+
// upgrade-done → green check: restarted & reconnected successfully
|
|
9
|
+
//
|
|
10
|
+
// Flow:
|
|
11
|
+
// 1. Page load → checkVersion() → badge shows version number
|
|
12
|
+
// 2. needs_update: badge shows amber pulsing dot
|
|
13
|
+
// 3. Click badge → fixed popover (confirm state)
|
|
14
|
+
// 4. Click "Upgrade" → popover → progress state (live log)
|
|
15
|
+
// 5. upgrade_complete (success) → badge: needs-restart; popover: restart button
|
|
16
|
+
// 6. Click "Restart" → /api/restart → popover: reconnecting spinner
|
|
17
|
+
// → poll /api/version until server back → badge: upgrade-done (green ✓) → reload
|
|
18
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const Version = (() => {
|
|
21
|
+
// ── State ──────────────────────────────────────────────────────────────
|
|
22
|
+
let _current = null;
|
|
23
|
+
let _latest = null;
|
|
24
|
+
let _needsUpdate = false;
|
|
25
|
+
let _upgrading = false;
|
|
26
|
+
let _needsRestart = false; // upgrade done, waiting for restart
|
|
27
|
+
let _reconnecting = false; // restart sent, polling for server to come back
|
|
28
|
+
let _upgradeDone = false; // restarted and reconnected successfully
|
|
29
|
+
let _restartFailed = false; // 30s passed, server still not responding
|
|
30
|
+
let _popoverOpen = false;
|
|
31
|
+
let _reconnectTimer = null;
|
|
32
|
+
let _reconnectDeadline = 0;
|
|
33
|
+
let _logLines = [];
|
|
34
|
+
let _cliCommand = "octo";
|
|
35
|
+
|
|
36
|
+
const RECONNECT_TIMEOUT_MS = 30_000;
|
|
37
|
+
|
|
38
|
+
// ── DOM helpers ────────────────────────────────────────────────────────
|
|
39
|
+
const $ = id => document.getElementById(id);
|
|
40
|
+
const el = (tag, attrs = {}, ...children) => {
|
|
41
|
+
const e = document.createElement(tag);
|
|
42
|
+
Object.entries(attrs).forEach(([k, v]) => {
|
|
43
|
+
if (k === "className") e.className = v;
|
|
44
|
+
else if (k === "innerHTML") e.innerHTML = v;
|
|
45
|
+
else e.setAttribute(k, v);
|
|
46
|
+
});
|
|
47
|
+
children.forEach(c => c && e.appendChild(typeof c === "string" ? document.createTextNode(c) : c));
|
|
48
|
+
return e;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// ── Version check ──────────────────────────────────────────────────────
|
|
52
|
+
async function checkVersion() {
|
|
53
|
+
try {
|
|
54
|
+
const res = await fetch("/api/version");
|
|
55
|
+
if (!res.ok) return;
|
|
56
|
+
const data = await res.json();
|
|
57
|
+
_current = data.current;
|
|
58
|
+
_latest = data.latest;
|
|
59
|
+
_needsUpdate = !!data.needs_update;
|
|
60
|
+
if (data.cli_command) _cliCommand = data.cli_command;
|
|
61
|
+
_renderBadge();
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.warn("[Version] check failed:", e);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Badge render ───────────────────────────────────────────────────────
|
|
68
|
+
function _renderBadge() {
|
|
69
|
+
const badge = $("version-badge");
|
|
70
|
+
const text = $("version-text");
|
|
71
|
+
const dot = $("version-update-dot");
|
|
72
|
+
const restartDot = $("version-restart-dot");
|
|
73
|
+
const check = $("version-done-check");
|
|
74
|
+
const spinner = $("version-spinner");
|
|
75
|
+
if (!badge || !text) return;
|
|
76
|
+
|
|
77
|
+
text.textContent = _current ? `v${_current}` : "";
|
|
78
|
+
|
|
79
|
+
// Reset all indicators
|
|
80
|
+
if (dot) dot.style.display = "none";
|
|
81
|
+
if (restartDot) restartDot.style.display = "none";
|
|
82
|
+
if (check) check.style.display = "none";
|
|
83
|
+
if (spinner) spinner.style.display = "none";
|
|
84
|
+
badge.className = "version-badge";
|
|
85
|
+
|
|
86
|
+
if (_upgrading) {
|
|
87
|
+
// Spinning ring: gem install running
|
|
88
|
+
badge.classList.add("is-upgrading");
|
|
89
|
+
badge.title = I18n.t("upgrade.tooltip.upgrading");
|
|
90
|
+
if (spinner) spinner.style.display = "inline-block";
|
|
91
|
+
} else if (_needsRestart) {
|
|
92
|
+
// Orange bouncing dot: upgrade done, please restart
|
|
93
|
+
badge.classList.add("needs-restart");
|
|
94
|
+
badge.title = I18n.t("upgrade.tooltip.needs_restart");
|
|
95
|
+
if (restartDot) restartDot.style.display = "inline-block";
|
|
96
|
+
} else if (_upgradeDone) {
|
|
97
|
+
// Green check: restarted successfully
|
|
98
|
+
badge.classList.add("upgrade-done");
|
|
99
|
+
badge.title = I18n.t("upgrade.tooltip.done");
|
|
100
|
+
if (check) check.style.display = "inline-block";
|
|
101
|
+
} else if (_needsUpdate) {
|
|
102
|
+
// Amber pulsing dot: new version available
|
|
103
|
+
badge.classList.add("has-update");
|
|
104
|
+
badge.title = I18n.t("upgrade.tooltip.new", { latest: _latest });
|
|
105
|
+
if (dot) dot.style.display = "inline-block";
|
|
106
|
+
} else {
|
|
107
|
+
badge.title = I18n.t("upgrade.tooltip.ok", { current: _current });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
badge.style.display = "flex";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Popover (fixed, positioned above badge) ────────────────────────────
|
|
114
|
+
function _getOrCreatePopover() {
|
|
115
|
+
let pop = $("version-upgrade-popover");
|
|
116
|
+
if (pop) return pop;
|
|
117
|
+
|
|
118
|
+
pop = el("div", { id: "version-upgrade-popover", className: "vup" });
|
|
119
|
+
document.body.appendChild(pop);
|
|
120
|
+
return pop;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function _positionPopover() {
|
|
124
|
+
const badge = $("version-badge");
|
|
125
|
+
const pop = $("version-upgrade-popover");
|
|
126
|
+
if (!badge || !pop) return;
|
|
127
|
+
|
|
128
|
+
const rect = badge.getBoundingClientRect();
|
|
129
|
+
// Appear above the badge, right-aligned to sidebar edge
|
|
130
|
+
pop.style.left = rect.left + "px";
|
|
131
|
+
pop.style.bottom = (window.innerHeight - rect.top + 8) + "px";
|
|
132
|
+
pop.style.top = "auto";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function _openPopover() {
|
|
136
|
+
if (_popoverOpen) { _positionPopover(); return; }
|
|
137
|
+
_popoverOpen = true;
|
|
138
|
+
|
|
139
|
+
const pop = _getOrCreatePopover();
|
|
140
|
+
pop.innerHTML = "";
|
|
141
|
+
|
|
142
|
+
if (_restartFailed) {
|
|
143
|
+
_renderRestartFailedState(pop);
|
|
144
|
+
} else if (_reconnecting) {
|
|
145
|
+
_renderReconnectState(pop);
|
|
146
|
+
} else if (_upgrading) {
|
|
147
|
+
_renderProgressState(pop);
|
|
148
|
+
} else if (_needsRestart) {
|
|
149
|
+
_renderDoneState(pop);
|
|
150
|
+
} else if (_upgradeDone) {
|
|
151
|
+
_renderDoneState(pop);
|
|
152
|
+
} else if (_needsUpdate) {
|
|
153
|
+
_renderConfirmState(pop);
|
|
154
|
+
} else {
|
|
155
|
+
_renderUpToDateState(pop);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
pop.style.display = "block";
|
|
159
|
+
_positionPopover();
|
|
160
|
+
|
|
161
|
+
// Animate in
|
|
162
|
+
requestAnimationFrame(() => pop.classList.add("vup--visible"));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function _closePopover() {
|
|
166
|
+
// Don't allow closing while upgrading or waiting for server to come back
|
|
167
|
+
if (_upgrading || _reconnecting) return;
|
|
168
|
+
const pop = $("version-upgrade-popover");
|
|
169
|
+
if (!pop) return;
|
|
170
|
+
pop.classList.remove("vup--visible");
|
|
171
|
+
setTimeout(() => {
|
|
172
|
+
pop.style.display = "none";
|
|
173
|
+
_popoverOpen = false;
|
|
174
|
+
}, 180);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── Popover states ─────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
/** State 0: already up to date */
|
|
180
|
+
function _renderUpToDateState(pop) {
|
|
181
|
+
pop.innerHTML = `
|
|
182
|
+
<p class="vup-up-to-date">
|
|
183
|
+
<span class="vup-check-icon">✓</span>
|
|
184
|
+
${I18n.t("upgrade.tooltip.ok", { current: _current })}
|
|
185
|
+
</p>
|
|
186
|
+
`;
|
|
187
|
+
// Auto-close after 2 s so user doesn't need to click away
|
|
188
|
+
setTimeout(() => { if (_popoverOpen) _closePopover(); }, 2000);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** State 1: confirm upgrade */
|
|
192
|
+
function _renderConfirmState(pop) {
|
|
193
|
+
pop.innerHTML = `
|
|
194
|
+
<p class="vup-desc">${I18n.t("upgrade.desc")}</p>
|
|
195
|
+
<p class="vup-versions">v${_current} <span class="vup-arrow">→</span> v${_latest}</p>
|
|
196
|
+
<div class="vup-actions">
|
|
197
|
+
<button id="vup-btn-upgrade" class="vup-btn-primary">${I18n.t("upgrade.btn.upgrade")}</button>
|
|
198
|
+
<button id="vup-btn-cancel" class="vup-btn-cancel">${I18n.t("upgrade.btn.cancel")}</button>
|
|
199
|
+
</div>
|
|
200
|
+
`;
|
|
201
|
+
$("vup-btn-upgrade").addEventListener("click", () => _startUpgrade(pop));
|
|
202
|
+
$("vup-btn-cancel").addEventListener("click", _closePopover);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** State 2: upgrading — show live log */
|
|
206
|
+
function _renderProgressState(pop) {
|
|
207
|
+
pop.innerHTML = `
|
|
208
|
+
<div class="vup-progress-header">
|
|
209
|
+
<span class="vup-installing-dot"></span>
|
|
210
|
+
<span class="vup-installing-label">${I18n.t("upgrade.installing")}</span>
|
|
211
|
+
</div>
|
|
212
|
+
<pre id="vup-log" class="vup-log"></pre>
|
|
213
|
+
`;
|
|
214
|
+
// Replay any logs already received
|
|
215
|
+
const logEl = $("vup-log");
|
|
216
|
+
if (logEl && _logLines.length) {
|
|
217
|
+
logEl.textContent = _logLines.join("\n");
|
|
218
|
+
logEl.scrollTop = logEl.scrollHeight;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** State 3: done — show restart button */
|
|
223
|
+
function _renderDoneState(pop) {
|
|
224
|
+
pop.innerHTML = `
|
|
225
|
+
<div class="vup-done-header">
|
|
226
|
+
<span class="vup-done-icon">✓</span>
|
|
227
|
+
<span>${I18n.t("upgrade.done")}</span>
|
|
228
|
+
</div>
|
|
229
|
+
<button id="vup-btn-restart" class="vup-btn-restart">${I18n.t("upgrade.btn.restart")}</button>
|
|
230
|
+
`;
|
|
231
|
+
$("vup-btn-restart").addEventListener("click", _startRestart);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** State 4: reconnecting after restart */
|
|
235
|
+
function _renderReconnectState(pop) {
|
|
236
|
+
pop.innerHTML = `
|
|
237
|
+
<div class="vup-reconnect">
|
|
238
|
+
<div class="vup-reconnect-spinner"></div>
|
|
239
|
+
<p class="vup-reconnect-msg">${I18n.t("upgrade.reconnecting")}</p>
|
|
240
|
+
</div>
|
|
241
|
+
`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** State 5: restart timed out — show both recovery paths (tray + CLI) */
|
|
245
|
+
function _renderRestartFailedState(pop) {
|
|
246
|
+
const safeCmd = String(_cliCommand).replace(/[&<>"']/g, c => (
|
|
247
|
+
{ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]
|
|
248
|
+
));
|
|
249
|
+
const cmd = `<code class="vup-cmd">${safeCmd} server</code>`;
|
|
250
|
+
pop.innerHTML = `
|
|
251
|
+
<div class="vup-restart-failed">
|
|
252
|
+
<p class="vup-restart-failed-title">⚠ ${I18n.t("upgrade.restart.timeout.title")}</p>
|
|
253
|
+
<p class="vup-restart-failed-desc">${I18n.t("upgrade.restart.timeout.desc")}</p>
|
|
254
|
+
<ul class="vup-restart-failed-options">
|
|
255
|
+
<li>${I18n.t("upgrade.restart.timeout.tray")}</li>
|
|
256
|
+
<li>${I18n.t("upgrade.restart.timeout.cli", { cmd })}</li>
|
|
257
|
+
</ul>
|
|
258
|
+
<div class="vup-actions">
|
|
259
|
+
<button id="vup-btn-retry" class="vup-btn-primary">${I18n.t("upgrade.restart.timeout.retry")}</button>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
`;
|
|
263
|
+
const retry = $("vup-btn-retry");
|
|
264
|
+
if (retry) retry.addEventListener("click", _retryReconnect);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Upgrade ────────────────────────────────────────────────────────────
|
|
268
|
+
async function _startUpgrade(pop) {
|
|
269
|
+
if (_upgrading || _upgradeDone) return;
|
|
270
|
+
_upgrading = true;
|
|
271
|
+
_logLines = [];
|
|
272
|
+
_renderBadge();
|
|
273
|
+
_renderProgressState(pop);
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
await fetch("/api/version/upgrade", { method: "POST" });
|
|
277
|
+
} catch (e) {
|
|
278
|
+
console.warn("[Version] upgrade request failed:", e);
|
|
279
|
+
_upgrading = false;
|
|
280
|
+
_renderBadge();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── Restart ────────────────────────────────────────────────────────────
|
|
285
|
+
async function _startRestart() {
|
|
286
|
+
_reconnecting = true;
|
|
287
|
+
|
|
288
|
+
// Ensure popover is open and showing the reconnect spinner
|
|
289
|
+
const pop = _getOrCreatePopover();
|
|
290
|
+
_renderReconnectState(pop);
|
|
291
|
+
if (!_popoverOpen) {
|
|
292
|
+
_popoverOpen = true;
|
|
293
|
+
pop.style.display = "block";
|
|
294
|
+
_positionPopover();
|
|
295
|
+
requestAnimationFrame(() => pop.classList.add("vup--visible"));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
fetch("/api/restart", { method: "POST" }).catch(() => {});
|
|
300
|
+
} catch (_) {}
|
|
301
|
+
|
|
302
|
+
_waitForReconnect();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function _waitForReconnect() {
|
|
306
|
+
if (_reconnectTimer) clearInterval(_reconnectTimer);
|
|
307
|
+
_reconnectDeadline = Date.now() + RECONNECT_TIMEOUT_MS;
|
|
308
|
+
setTimeout(() => {
|
|
309
|
+
_reconnectTimer = setInterval(async () => {
|
|
310
|
+
if (Date.now() > _reconnectDeadline) {
|
|
311
|
+
clearInterval(_reconnectTimer);
|
|
312
|
+
_reconnectTimer = null;
|
|
313
|
+
_reconnecting = false;
|
|
314
|
+
_restartFailed = true;
|
|
315
|
+
_renderBadge();
|
|
316
|
+
const pop = $("version-upgrade-popover");
|
|
317
|
+
if (pop && _popoverOpen) _renderRestartFailedState(pop);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
const res = await fetch("/api/version", { cache: "no-store" });
|
|
322
|
+
if (res.ok) {
|
|
323
|
+
clearInterval(_reconnectTimer);
|
|
324
|
+
_reconnectTimer = null;
|
|
325
|
+
// Server is back — close popover, badge → green check, then reload
|
|
326
|
+
_reconnecting = false;
|
|
327
|
+
_needsRestart = false;
|
|
328
|
+
_upgradeDone = true;
|
|
329
|
+
_renderBadge();
|
|
330
|
+
_closePopover();
|
|
331
|
+
setTimeout(() => window.location.reload(), 800);
|
|
332
|
+
}
|
|
333
|
+
} catch (_) { /* server not yet up */ }
|
|
334
|
+
}, 2000);
|
|
335
|
+
}, 2500);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function _retryReconnect() {
|
|
339
|
+
_restartFailed = false;
|
|
340
|
+
_reconnecting = true;
|
|
341
|
+
const pop = $("version-upgrade-popover");
|
|
342
|
+
if (pop && _popoverOpen) _renderReconnectState(pop);
|
|
343
|
+
_renderBadge();
|
|
344
|
+
_waitForReconnect();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ── WebSocket events ───────────────────────────────────────────────────
|
|
348
|
+
function _handleWsEvent(event) {
|
|
349
|
+
if (event.type === "upgrade_log") {
|
|
350
|
+
const line = event.line || "";
|
|
351
|
+
_logLines.push(line);
|
|
352
|
+
// Append to live log if popover is open
|
|
353
|
+
const logEl = $("vup-log");
|
|
354
|
+
if (logEl) {
|
|
355
|
+
logEl.textContent += (logEl.textContent ? "\n" : "") + line;
|
|
356
|
+
logEl.scrollTop = logEl.scrollHeight;
|
|
357
|
+
}
|
|
358
|
+
} else if (event.type === "upgrade_complete") {
|
|
359
|
+
_upgrading = false;
|
|
360
|
+
if (event.success) {
|
|
361
|
+
_needsUpdate = false;
|
|
362
|
+
_needsRestart = true; // badge: orange bouncing dot
|
|
363
|
+
_upgradeDone = false;
|
|
364
|
+
}
|
|
365
|
+
// On failure, _needsUpdate stays true so badge stays amber
|
|
366
|
+
_renderBadge();
|
|
367
|
+
// Morph popover to done/error state
|
|
368
|
+
const pop = $("version-upgrade-popover");
|
|
369
|
+
if (pop && _popoverOpen) {
|
|
370
|
+
if (event.success) {
|
|
371
|
+
_renderDoneState(pop);
|
|
372
|
+
} else {
|
|
373
|
+
pop.innerHTML = `<p class="vup-error">${I18n.t("upgrade.failed")}</p>`;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ── Init ───────────────────────────────────────────────────────────────
|
|
380
|
+
let _hoverTimer = null;
|
|
381
|
+
|
|
382
|
+
function init() {
|
|
383
|
+
const badge = $("version-badge");
|
|
384
|
+
if (badge) {
|
|
385
|
+
// Click still works (e.g. during reconnect to keep popover visible)
|
|
386
|
+
badge.addEventListener("click", e => {
|
|
387
|
+
e.stopPropagation();
|
|
388
|
+
if (_reconnecting) { if (!_popoverOpen) _openPopover(); return; }
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Hover to open
|
|
392
|
+
badge.addEventListener("mouseenter", () => {
|
|
393
|
+
if (!_current) return;
|
|
394
|
+
clearTimeout(_hoverTimer);
|
|
395
|
+
_openPopover();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Leave badge or popover → close (with small delay so mouse can move to popover)
|
|
399
|
+
badge.addEventListener("mouseleave", () => {
|
|
400
|
+
_hoverTimer = setTimeout(() => {
|
|
401
|
+
const pop = $("version-upgrade-popover");
|
|
402
|
+
if (pop && pop.matches(":hover")) return;
|
|
403
|
+
_closePopover();
|
|
404
|
+
}, 200);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Keep popover open while hovering it; close when leaving
|
|
409
|
+
document.addEventListener("mouseover", e => {
|
|
410
|
+
const pop = $("version-upgrade-popover");
|
|
411
|
+
if (pop && e.target.closest("#version-upgrade-popover")) {
|
|
412
|
+
clearTimeout(_hoverTimer);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
document.addEventListener("mouseout", e => {
|
|
416
|
+
const pop = $("version-upgrade-popover");
|
|
417
|
+
if (!pop) return;
|
|
418
|
+
if (e.target.closest("#version-upgrade-popover") && !e.relatedTarget?.closest("#version-upgrade-popover") && !e.relatedTarget?.closest("#version-badge")) {
|
|
419
|
+
_hoverTimer = setTimeout(() => _closePopover(), 200);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// Click outside still closes (e.g. keyboard users, edge cases)
|
|
424
|
+
document.addEventListener("click", e => {
|
|
425
|
+
if (!e.target.closest("#version-badge") && !e.target.closest("#version-upgrade-popover")) {
|
|
426
|
+
if (_popoverOpen && !_upgrading && !_reconnecting) _closePopover();
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// Reposition on window resize
|
|
431
|
+
window.addEventListener("resize", () => {
|
|
432
|
+
if (_popoverOpen) _positionPopover();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
if (typeof WS !== "undefined") {
|
|
436
|
+
WS.onEvent(_handleWsEvent);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
checkVersion();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (document.readyState === "loading") {
|
|
443
|
+
document.addEventListener("DOMContentLoaded", init);
|
|
444
|
+
} else {
|
|
445
|
+
init();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return { checkVersion };
|
|
449
|
+
})();
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>微信扫码登录</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
11
|
+
background: #f5f5f5;
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
justify-content: center;
|
|
15
|
+
min-height: 100vh;
|
|
16
|
+
}
|
|
17
|
+
.card {
|
|
18
|
+
background: #fff;
|
|
19
|
+
border-radius: 16px;
|
|
20
|
+
padding: 40px 48px;
|
|
21
|
+
text-align: center;
|
|
22
|
+
box-shadow: 0 4px 24px rgba(0,0,0,.08);
|
|
23
|
+
max-width: 360px;
|
|
24
|
+
width: 100%;
|
|
25
|
+
}
|
|
26
|
+
.logo {
|
|
27
|
+
width: 48px;
|
|
28
|
+
height: 48px;
|
|
29
|
+
background: linear-gradient(135deg, #2dc100, #1aad19);
|
|
30
|
+
border-radius: 12px;
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
justify-content: center;
|
|
34
|
+
font-size: 24px;
|
|
35
|
+
margin: 0 auto 16px;
|
|
36
|
+
}
|
|
37
|
+
h1 {
|
|
38
|
+
font-size: 18px;
|
|
39
|
+
font-weight: 600;
|
|
40
|
+
color: #111;
|
|
41
|
+
margin-bottom: 6px;
|
|
42
|
+
}
|
|
43
|
+
p {
|
|
44
|
+
font-size: 13px;
|
|
45
|
+
color: #888;
|
|
46
|
+
margin-bottom: 24px;
|
|
47
|
+
line-height: 1.5;
|
|
48
|
+
}
|
|
49
|
+
#qrcode {
|
|
50
|
+
display: flex;
|
|
51
|
+
justify-content: center;
|
|
52
|
+
margin-bottom: 20px;
|
|
53
|
+
}
|
|
54
|
+
#qrcode canvas, #qrcode img {
|
|
55
|
+
border-radius: 8px;
|
|
56
|
+
border: 1px solid #eee;
|
|
57
|
+
}
|
|
58
|
+
.hint {
|
|
59
|
+
font-size: 12px;
|
|
60
|
+
color: #bbb;
|
|
61
|
+
}
|
|
62
|
+
.error {
|
|
63
|
+
color: #e53e3e;
|
|
64
|
+
font-size: 13px;
|
|
65
|
+
padding: 12px;
|
|
66
|
+
background: #fff5f5;
|
|
67
|
+
border-radius: 8px;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Success overlay */
|
|
71
|
+
#success-overlay {
|
|
72
|
+
display: none;
|
|
73
|
+
position: fixed;
|
|
74
|
+
inset: 0;
|
|
75
|
+
background: rgba(255,255,255,0.92);
|
|
76
|
+
align-items: center;
|
|
77
|
+
justify-content: center;
|
|
78
|
+
flex-direction: column;
|
|
79
|
+
gap: 16px;
|
|
80
|
+
z-index: 100;
|
|
81
|
+
}
|
|
82
|
+
#success-overlay.show {
|
|
83
|
+
display: flex;
|
|
84
|
+
}
|
|
85
|
+
.success-icon {
|
|
86
|
+
width: 72px;
|
|
87
|
+
height: 72px;
|
|
88
|
+
background: linear-gradient(135deg, #2dc100, #1aad19);
|
|
89
|
+
border-radius: 50%;
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: center;
|
|
93
|
+
font-size: 36px;
|
|
94
|
+
animation: pop 0.3s ease-out;
|
|
95
|
+
}
|
|
96
|
+
.success-title {
|
|
97
|
+
font-size: 20px;
|
|
98
|
+
font-weight: 700;
|
|
99
|
+
color: #111;
|
|
100
|
+
}
|
|
101
|
+
.success-sub {
|
|
102
|
+
font-size: 14px;
|
|
103
|
+
color: #888;
|
|
104
|
+
}
|
|
105
|
+
@keyframes pop {
|
|
106
|
+
0% { transform: scale(0.5); opacity: 0; }
|
|
107
|
+
80% { transform: scale(1.1); }
|
|
108
|
+
100% { transform: scale(1); opacity: 1; }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* Scanned state */
|
|
112
|
+
.scanned-hint {
|
|
113
|
+
display: none;
|
|
114
|
+
font-size: 13px;
|
|
115
|
+
color: #1aad19;
|
|
116
|
+
font-weight: 500;
|
|
117
|
+
margin-top: 8px;
|
|
118
|
+
}
|
|
119
|
+
.scanned-hint.show { display: block; }
|
|
120
|
+
</style>
|
|
121
|
+
</head>
|
|
122
|
+
<body>
|
|
123
|
+
<div class="card">
|
|
124
|
+
<div class="logo">微</div>
|
|
125
|
+
<h1>微信扫码登录</h1>
|
|
126
|
+
<p>使用微信扫描下方二维码<br>在手机上点击「确认登录」</p>
|
|
127
|
+
<div id="qrcode"></div>
|
|
128
|
+
<div class="hint">二维码有效期 5 分钟</div>
|
|
129
|
+
<div class="scanned-hint" id="scanned-hint">✅ 已扫码,请在手机上确认…</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<!-- Success overlay shown after polling detects token -->
|
|
133
|
+
<div id="success-overlay">
|
|
134
|
+
<div class="success-icon">✓</div>
|
|
135
|
+
<div class="success-title">登录成功</div>
|
|
136
|
+
<div class="success-sub">微信已连接,可以开始聊天了</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
|
|
140
|
+
<script>
|
|
141
|
+
const params = new URLSearchParams(location.search);
|
|
142
|
+
const url = params.get("url");
|
|
143
|
+
// since: Unix timestamp (seconds) passed by the setup skill when opening this page.
|
|
144
|
+
// We only show success if token_updated_at > since, preventing false positives
|
|
145
|
+
// when the user already had a token from a previous login.
|
|
146
|
+
const since = parseInt(params.get("since") || "0", 10);
|
|
147
|
+
const el = document.getElementById("qrcode");
|
|
148
|
+
|
|
149
|
+
if (!url) {
|
|
150
|
+
el.innerHTML = '<div class="error">缺少 url 参数</div>';
|
|
151
|
+
} else {
|
|
152
|
+
try {
|
|
153
|
+
new QRCode(el, {
|
|
154
|
+
text: url,
|
|
155
|
+
width: 220,
|
|
156
|
+
height: 220,
|
|
157
|
+
colorDark: "#111111",
|
|
158
|
+
colorLight: "#ffffff",
|
|
159
|
+
correctLevel: QRCode.CorrectLevel.M
|
|
160
|
+
});
|
|
161
|
+
} catch (e) {
|
|
162
|
+
el.innerHTML = '<div class="error">二维码生成失败: ' + e.message + '</div>';
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Poll GET /api/channels every 2s; show success overlay once weixin has_token = true
|
|
167
|
+
const POLL_INTERVAL_MS = 2000;
|
|
168
|
+
const POLL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes — matches QR expiry
|
|
169
|
+
const startedAt = Date.now();
|
|
170
|
+
let pollTimer = null;
|
|
171
|
+
let prevHasToken = false;
|
|
172
|
+
|
|
173
|
+
function showSuccess() {
|
|
174
|
+
clearTimeout(pollTimer);
|
|
175
|
+
document.getElementById("success-overlay").classList.add("show");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function pollChannelStatus() {
|
|
179
|
+
if (Date.now() - startedAt > POLL_TIMEOUT_MS) return; // QR expired, stop quietly
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const res = await fetch("/api/channels", { cache: "no-store" });
|
|
183
|
+
const data = await res.json();
|
|
184
|
+
const weixin = (data.channels || []).find(c => c.platform === "weixin");
|
|
185
|
+
|
|
186
|
+
if (weixin && weixin.has_token) {
|
|
187
|
+
const updatedAt = weixin.token_updated_at || 0;
|
|
188
|
+
if (updatedAt > since) {
|
|
189
|
+
showSuccess();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Show "scanned, waiting for confirm" hint once token appears in-progress
|
|
195
|
+
// (iLink doesn't expose a "scanned" state via this API, so we just keep polling)
|
|
196
|
+
} catch (_) {
|
|
197
|
+
// Server temporarily unreachable — keep polling
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
pollTimer = setTimeout(pollChannelStatus, POLL_INTERVAL_MS);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Start polling only when QR code is actually shown
|
|
204
|
+
if (url) {
|
|
205
|
+
pollTimer = setTimeout(pollChannelStatus, POLL_INTERVAL_MS);
|
|
206
|
+
}
|
|
207
|
+
</script>
|
|
208
|
+
</body>
|
|
209
|
+
</html>
|