octo-agent 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. checksums.yaml +7 -0
  2. data/.clacky/skills/commit/SKILL.md +423 -0
  3. data/.clacky/skills/gem-release/SKILL.md +199 -0
  4. data/.clacky/skills/gem-release/scripts/release.sh +304 -0
  5. data/.clacky/skills/oss-upload/SKILL.md +47 -0
  6. data/.octorules +106 -0
  7. data/.rspec +3 -0
  8. data/.rubocop.yml +8 -0
  9. data/CHANGELOG.md +76 -0
  10. data/CODE_OF_CONDUCT.md +132 -0
  11. data/CONTRIBUTING.md +92 -0
  12. data/Dockerfile +28 -0
  13. data/LICENSE.txt +22 -0
  14. data/POSITIONING.md +46 -0
  15. data/README.md +134 -0
  16. data/README_CN.md +134 -0
  17. data/Rakefile +34 -0
  18. data/benchmark/fixtures/sample_project/Gemfile +3 -0
  19. data/benchmark/fixtures/sample_project/lib/api_handler.rb +32 -0
  20. data/benchmark/fixtures/sample_project/lib/order_calculator.rb +23 -0
  21. data/benchmark/fixtures/sample_project/lib/user_renderer.rb +20 -0
  22. data/benchmark/fixtures/sample_project/spec/order_calculator_spec.rb +20 -0
  23. data/benchmark/results/EVALUATION_REPORT.md +165 -0
  24. data/benchmark/results/baseline_20260511_174424.json +128 -0
  25. data/benchmark/results/report_20260511_175256.json +271 -0
  26. data/benchmark/results/report_20260511_175444.json +271 -0
  27. data/benchmark/results/treatment_20260511_175103.json +130 -0
  28. data/benchmark/runner.rb +441 -0
  29. data/bin/octo +7 -0
  30. data/docs/agent-first-ui-design.md +77 -0
  31. data/docs/billing-system.md +318 -0
  32. data/docs/channel-architecture.md +235 -0
  33. data/docs/engineering-article.md +343 -0
  34. data/docs/session-skill-invocation.md +69 -0
  35. data/docs/time_machine_design.md +247 -0
  36. data/docs/ui2-architecture.md +124 -0
  37. data/homebrew/README.md +96 -0
  38. data/homebrew/openocto.rb +24 -0
  39. data/lib/octo/agent/hook_manager.rb +61 -0
  40. data/lib/octo/agent/llm_caller.rb +800 -0
  41. data/lib/octo/agent/memory_updater.rb +246 -0
  42. data/lib/octo/agent/message_compressor.rb +225 -0
  43. data/lib/octo/agent/message_compressor_helper.rb +869 -0
  44. data/lib/octo/agent/next_message_suggester.rb +215 -0
  45. data/lib/octo/agent/session_serializer.rb +685 -0
  46. data/lib/octo/agent/skill_auto_creator.rb +114 -0
  47. data/lib/octo/agent/skill_evolution.rb +61 -0
  48. data/lib/octo/agent/skill_manager.rb +466 -0
  49. data/lib/octo/agent/skill_reflector.rb +89 -0
  50. data/lib/octo/agent/system_prompt_builder.rb +101 -0
  51. data/lib/octo/agent/time_machine.rb +214 -0
  52. data/lib/octo/agent/tool_executor.rb +454 -0
  53. data/lib/octo/agent/tool_registry.rb +150 -0
  54. data/lib/octo/agent.rb +2180 -0
  55. data/lib/octo/agent_config.rb +989 -0
  56. data/lib/octo/agent_profile.rb +112 -0
  57. data/lib/octo/anthropic_stream_aggregator.rb +137 -0
  58. data/lib/octo/background_task_registry.rb +324 -0
  59. data/lib/octo/banner.rb +34 -0
  60. data/lib/octo/bedrock_stream_aggregator.rb +137 -0
  61. data/lib/octo/block_font.rb +331 -0
  62. data/lib/octo/cli.rb +968 -0
  63. data/lib/octo/client.rb +623 -0
  64. data/lib/octo/default_agents/SOUL.md +3 -0
  65. data/lib/octo/default_agents/USER.md +1 -0
  66. data/lib/octo/default_agents/base_prompt.md +66 -0
  67. data/lib/octo/default_agents/coding/profile.yml +2 -0
  68. data/lib/octo/default_agents/coding/system_prompt.md +67 -0
  69. data/lib/octo/default_agents/general/profile.yml +2 -0
  70. data/lib/octo/default_agents/general/system_prompt.md +16 -0
  71. data/lib/octo/default_parsers/doc_parser.rb +69 -0
  72. data/lib/octo/default_parsers/docx_parser.rb +188 -0
  73. data/lib/octo/default_parsers/pdf_parser.rb +120 -0
  74. data/lib/octo/default_parsers/pdf_parser_ocr.py +103 -0
  75. data/lib/octo/default_parsers/pdf_parser_plumber.py +62 -0
  76. data/lib/octo/default_parsers/pptx_parser.rb +140 -0
  77. data/lib/octo/default_parsers/xlsx_parser.rb +121 -0
  78. data/lib/octo/default_skills/browser-setup/SKILL.md +426 -0
  79. data/lib/octo/default_skills/channel-manager/SKILL.md +623 -0
  80. data/lib/octo/default_skills/channel-manager/dingtalk_setup.rb +191 -0
  81. data/lib/octo/default_skills/channel-manager/discord_setup.rb +199 -0
  82. data/lib/octo/default_skills/channel-manager/feishu_setup.rb +574 -0
  83. data/lib/octo/default_skills/channel-manager/import_lark_skills.rb +97 -0
  84. data/lib/octo/default_skills/channel-manager/install_feishu_skills.rb +105 -0
  85. data/lib/octo/default_skills/channel-manager/weixin_setup.rb +274 -0
  86. data/lib/octo/default_skills/code-explorer/SKILL.md +36 -0
  87. data/lib/octo/default_skills/cron-task-creator/SKILL.md +257 -0
  88. data/lib/octo/default_skills/cron-task-creator/evals/evals.json +38 -0
  89. data/lib/octo/default_skills/onboard/SKILL.md +578 -0
  90. data/lib/octo/default_skills/onboard/scripts/import_external_skills.rb +413 -0
  91. data/lib/octo/default_skills/onboard/scripts/install_builtin_skills.rb +97 -0
  92. data/lib/octo/default_skills/persist-memory/SKILL.md +59 -0
  93. data/lib/octo/default_skills/personal-website/SKILL.md +113 -0
  94. data/lib/octo/default_skills/personal-website/publish.rb +235 -0
  95. data/lib/octo/default_skills/product-help/SKILL.md +123 -0
  96. data/lib/octo/default_skills/product-help/docs/agent-config.md +74 -0
  97. data/lib/octo/default_skills/product-help/docs/best-practices.md +49 -0
  98. data/lib/octo/default_skills/product-help/docs/browser-tool.md +53 -0
  99. data/lib/octo/default_skills/product-help/docs/built-in-skills.md +43 -0
  100. data/lib/octo/default_skills/product-help/docs/cli-reference.md +82 -0
  101. data/lib/octo/default_skills/product-help/docs/create-your-first-skill.md +47 -0
  102. data/lib/octo/default_skills/product-help/docs/faq.md +98 -0
  103. data/lib/octo/default_skills/product-help/docs/how-to-use-a-skill.md +58 -0
  104. data/lib/octo/default_skills/product-help/docs/installation.md +59 -0
  105. data/lib/octo/default_skills/product-help/docs/memory-system.md +61 -0
  106. data/lib/octo/default_skills/product-help/docs/octorules.md +62 -0
  107. data/lib/octo/default_skills/product-help/docs/session-management.md +63 -0
  108. data/lib/octo/default_skills/product-help/docs/skill-basics.md +55 -0
  109. data/lib/octo/default_skills/product-help/docs/skill-frontmatter.md +61 -0
  110. data/lib/octo/default_skills/product-help/docs/web-server.md +49 -0
  111. data/lib/octo/default_skills/product-help/docs/what-is-octo.md +37 -0
  112. data/lib/octo/default_skills/product-help/docs/windows-installation.md +36 -0
  113. data/lib/octo/default_skills/product-help/docs/writing-tips.md +53 -0
  114. data/lib/octo/default_skills/recall-memory/SKILL.md +65 -0
  115. data/lib/octo/default_skills/skill-add/SKILL.md +59 -0
  116. data/lib/octo/default_skills/skill-add/scripts/install_from_zip.rb +295 -0
  117. data/lib/octo/default_skills/skill-creator/SKILL.md +602 -0
  118. data/lib/octo/default_skills/skill-creator/agents/analyzer.md +274 -0
  119. data/lib/octo/default_skills/skill-creator/agents/comparator.md +202 -0
  120. data/lib/octo/default_skills/skill-creator/agents/grader.md +223 -0
  121. data/lib/octo/default_skills/skill-creator/eval-viewer/generate_review.py +471 -0
  122. data/lib/octo/default_skills/skill-creator/eval-viewer/viewer.html +1325 -0
  123. data/lib/octo/default_skills/skill-creator/references/schemas.md +430 -0
  124. data/lib/octo/default_skills/skill-creator/scripts/__init__.py +0 -0
  125. data/lib/octo/default_skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  126. data/lib/octo/default_skills/skill-creator/scripts/generate_report.py +326 -0
  127. data/lib/octo/default_skills/skill-creator/scripts/improve_description.py +310 -0
  128. data/lib/octo/default_skills/skill-creator/scripts/quick_validate.py +103 -0
  129. data/lib/octo/default_skills/skill-creator/scripts/run_eval.py +317 -0
  130. data/lib/octo/default_skills/skill-creator/scripts/run_loop.py +331 -0
  131. data/lib/octo/default_skills/skill-creator/scripts/utils.py +47 -0
  132. data/lib/octo/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb +143 -0
  133. data/lib/octo/idle_compression_timer.rb +115 -0
  134. data/lib/octo/json_ui_controller.rb +204 -0
  135. data/lib/octo/message_format/anthropic.rb +409 -0
  136. data/lib/octo/message_format/bedrock.rb +361 -0
  137. data/lib/octo/message_format/open_ai.rb +222 -0
  138. data/lib/octo/message_history.rb +373 -0
  139. data/lib/octo/openai_stream_aggregator.rb +130 -0
  140. data/lib/octo/plain_ui_controller.rb +166 -0
  141. data/lib/octo/providers.rb +534 -0
  142. data/lib/octo/server/browser_manager.rb +397 -0
  143. data/lib/octo/server/channel/adapters/base.rb +82 -0
  144. data/lib/octo/server/channel/adapters/dingtalk/adapter.rb +314 -0
  145. data/lib/octo/server/channel/adapters/dingtalk/api_client.rb +391 -0
  146. data/lib/octo/server/channel/adapters/dingtalk/stream_client.rb +203 -0
  147. data/lib/octo/server/channel/adapters/discord/adapter.rb +229 -0
  148. data/lib/octo/server/channel/adapters/discord/api_client.rb +107 -0
  149. data/lib/octo/server/channel/adapters/discord/gateway_client.rb +270 -0
  150. data/lib/octo/server/channel/adapters/feishu/adapter.rb +320 -0
  151. data/lib/octo/server/channel/adapters/feishu/bot.rb +478 -0
  152. data/lib/octo/server/channel/adapters/feishu/file_processor.rb +36 -0
  153. data/lib/octo/server/channel/adapters/feishu/message_parser.rb +129 -0
  154. data/lib/octo/server/channel/adapters/feishu/ws_client.rb +423 -0
  155. data/lib/octo/server/channel/adapters/telegram/adapter.rb +375 -0
  156. data/lib/octo/server/channel/adapters/telegram/api_client.rb +205 -0
  157. data/lib/octo/server/channel/adapters/wecom/adapter.rb +148 -0
  158. data/lib/octo/server/channel/adapters/wecom/media_downloader.rb +115 -0
  159. data/lib/octo/server/channel/adapters/wecom/ws_client.rb +395 -0
  160. data/lib/octo/server/channel/adapters/weixin/adapter.rb +692 -0
  161. data/lib/octo/server/channel/adapters/weixin/api_client.rb +402 -0
  162. data/lib/octo/server/channel/channel_config.rb +178 -0
  163. data/lib/octo/server/channel/channel_manager.rb +468 -0
  164. data/lib/octo/server/channel/channel_ui_controller.rb +224 -0
  165. data/lib/octo/server/channel.rb +33 -0
  166. data/lib/octo/server/discover.rb +77 -0
  167. data/lib/octo/server/epipe_safe_io.rb +105 -0
  168. data/lib/octo/server/http_server.rb +3554 -0
  169. data/lib/octo/server/scheduler.rb +317 -0
  170. data/lib/octo/server/server_master.rb +325 -0
  171. data/lib/octo/server/session_registry.rb +431 -0
  172. data/lib/octo/server/web_ui_controller.rb +487 -0
  173. data/lib/octo/session_manager.rb +385 -0
  174. data/lib/octo/skill.rb +466 -0
  175. data/lib/octo/skill_loader.rb +328 -0
  176. data/lib/octo/tools/base.rb +118 -0
  177. data/lib/octo/tools/browser.rb +625 -0
  178. data/lib/octo/tools/edit.rb +165 -0
  179. data/lib/octo/tools/file_reader.rb +549 -0
  180. data/lib/octo/tools/glob.rb +162 -0
  181. data/lib/octo/tools/grep.rb +356 -0
  182. data/lib/octo/tools/invoke_skill.rb +96 -0
  183. data/lib/octo/tools/list_tasks.rb +54 -0
  184. data/lib/octo/tools/redo_task.rb +41 -0
  185. data/lib/octo/tools/request_user_feedback.rb +84 -0
  186. data/lib/octo/tools/security.rb +333 -0
  187. data/lib/octo/tools/terminal/output_cleaner.rb +63 -0
  188. data/lib/octo/tools/terminal/persistent_session.rb +268 -0
  189. data/lib/octo/tools/terminal/safe_rm.sh +106 -0
  190. data/lib/octo/tools/terminal/session_manager.rb +213 -0
  191. data/lib/octo/tools/terminal.rb +1828 -0
  192. data/lib/octo/tools/todo_manager.rb +374 -0
  193. data/lib/octo/tools/trash_manager.rb +388 -0
  194. data/lib/octo/tools/undo_task.rb +35 -0
  195. data/lib/octo/tools/web_fetch.rb +242 -0
  196. data/lib/octo/tools/web_search.rb +260 -0
  197. data/lib/octo/tools/write.rb +77 -0
  198. data/lib/octo/ui2/block_font.rb +10 -0
  199. data/lib/octo/ui2/components/base_component.rb +163 -0
  200. data/lib/octo/ui2/components/command_suggestions.rb +290 -0
  201. data/lib/octo/ui2/components/common_component.rb +96 -0
  202. data/lib/octo/ui2/components/inline_input.rb +226 -0
  203. data/lib/octo/ui2/components/input_area.rb +1338 -0
  204. data/lib/octo/ui2/components/message_component.rb +99 -0
  205. data/lib/octo/ui2/components/modal_component.rb +419 -0
  206. data/lib/octo/ui2/components/todo_area.rb +149 -0
  207. data/lib/octo/ui2/components/tool_component.rb +107 -0
  208. data/lib/octo/ui2/components/welcome_banner.rb +139 -0
  209. data/lib/octo/ui2/layout_manager.rb +807 -0
  210. data/lib/octo/ui2/line_editor.rb +363 -0
  211. data/lib/octo/ui2/markdown_renderer.rb +100 -0
  212. data/lib/octo/ui2/output_buffer.rb +370 -0
  213. data/lib/octo/ui2/progress_handle.rb +362 -0
  214. data/lib/octo/ui2/progress_indicator.rb +55 -0
  215. data/lib/octo/ui2/screen_buffer.rb +273 -0
  216. data/lib/octo/ui2/terminal_detector.rb +119 -0
  217. data/lib/octo/ui2/theme_manager.rb +85 -0
  218. data/lib/octo/ui2/themes/base_theme.rb +105 -0
  219. data/lib/octo/ui2/themes/hacker_theme.rb +62 -0
  220. data/lib/octo/ui2/themes/minimal_theme.rb +56 -0
  221. data/lib/octo/ui2/thinking_verbs.rb +26 -0
  222. data/lib/octo/ui2/ui_controller.rb +1625 -0
  223. data/lib/octo/ui2/view_renderer.rb +177 -0
  224. data/lib/octo/ui2.rb +40 -0
  225. data/lib/octo/ui_interface.rb +154 -0
  226. data/lib/octo/utils/arguments_parser.rb +191 -0
  227. data/lib/octo/utils/browser_detector.rb +195 -0
  228. data/lib/octo/utils/encoding.rb +92 -0
  229. data/lib/octo/utils/environment_detector.rb +140 -0
  230. data/lib/octo/utils/file_ignore_helper.rb +170 -0
  231. data/lib/octo/utils/file_processor.rb +601 -0
  232. data/lib/octo/utils/gitignore_parser.rb +154 -0
  233. data/lib/octo/utils/limit_stack.rb +152 -0
  234. data/lib/octo/utils/logger.rb +124 -0
  235. data/lib/octo/utils/login_shell.rb +72 -0
  236. data/lib/octo/utils/model_pricing.rb +646 -0
  237. data/lib/octo/utils/parser_manager.rb +165 -0
  238. data/lib/octo/utils/path_helper.rb +15 -0
  239. data/lib/octo/utils/scripts_manager.rb +59 -0
  240. data/lib/octo/utils/string_matcher.rb +158 -0
  241. data/lib/octo/utils/trash_directory.rb +112 -0
  242. data/lib/octo/utils/workspace_rules.rb +46 -0
  243. data/lib/octo/version.rb +5 -0
  244. data/lib/octo/web/app.css +7141 -0
  245. data/lib/octo/web/app.js +543 -0
  246. data/lib/octo/web/apple-touch-icon.png +0 -0
  247. data/lib/octo/web/auth.js +150 -0
  248. data/lib/octo/web/channels.js +276 -0
  249. data/lib/octo/web/datepicker.js +205 -0
  250. data/lib/octo/web/favicon.png +0 -0
  251. data/lib/octo/web/i18n.js +1073 -0
  252. data/lib/octo/web/icon-512.png +0 -0
  253. data/lib/octo/web/icon-dark.svg +25 -0
  254. data/lib/octo/web/icon.svg +29 -0
  255. data/lib/octo/web/index.html +871 -0
  256. data/lib/octo/web/marked.min.js +69 -0
  257. data/lib/octo/web/onboard.js +491 -0
  258. data/lib/octo/web/profile.js +442 -0
  259. data/lib/octo/web/sessions.js +4421 -0
  260. data/lib/octo/web/settings.js +913 -0
  261. data/lib/octo/web/sidebar.js +32 -0
  262. data/lib/octo/web/skills.js +885 -0
  263. data/lib/octo/web/tasks.js +297 -0
  264. data/lib/octo/web/theme.js +105 -0
  265. data/lib/octo/web/trash.js +343 -0
  266. data/lib/octo/web/vendor/hljs/highlight.min.js +1244 -0
  267. data/lib/octo/web/vendor/hljs/hljs-theme.css +95 -0
  268. data/lib/octo/web/vendor/katex/auto-render.min.js +1 -0
  269. data/lib/octo/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  270. data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  271. data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  272. data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  273. data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  274. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  275. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  276. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  277. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  278. data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  279. data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  280. data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  281. data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  282. data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  283. data/lib/octo/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  284. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  285. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  286. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  287. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  288. data/lib/octo/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  289. data/lib/octo/web/vendor/katex/katex.min.css +1 -0
  290. data/lib/octo/web/vendor/katex/katex.min.js +1 -0
  291. data/lib/octo/web/version.js +449 -0
  292. data/lib/octo/web/weixin-qr.html +209 -0
  293. data/lib/octo/web/ws-dispatcher.js +357 -0
  294. data/lib/octo/web/ws.js +128 -0
  295. data/lib/octo.rb +145 -0
  296. data/scripts/build/build.sh +329 -0
  297. data/scripts/build/lib/apt.sh +56 -0
  298. data/scripts/build/lib/brew.sh +89 -0
  299. data/scripts/build/lib/colors.sh +17 -0
  300. data/scripts/build/lib/gem.sh +95 -0
  301. data/scripts/build/lib/mise.sh +125 -0
  302. data/scripts/build/lib/network.sh +157 -0
  303. data/scripts/build/lib/os.sh +57 -0
  304. data/scripts/build/lib/shell.sh +37 -0
  305. data/scripts/build/src/install.sh.cc +174 -0
  306. data/scripts/build/src/install_browser.sh.cc +101 -0
  307. data/scripts/build/src/install_full.sh.cc +290 -0
  308. data/scripts/build/src/install_rails_deps.sh.cc +145 -0
  309. data/scripts/build/src/install_system_deps.sh.cc +123 -0
  310. data/scripts/build/src/uninstall.sh.cc +101 -0
  311. data/scripts/install.ps1 +532 -0
  312. data/scripts/install.sh +567 -0
  313. data/scripts/install_browser.sh +479 -0
  314. data/scripts/install_full.sh +838 -0
  315. data/scripts/install_rails_deps.sh +746 -0
  316. data/scripts/install_system_deps.sh +518 -0
  317. data/scripts/uninstall.sh +287 -0
  318. data/sig/octo.rbs +4 -0
  319. metadata +614 -0
@@ -0,0 +1,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
+ { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[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>