openclacky 1.5.16 → 1.5.18

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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -0
  3. data/README.md +1 -1
  4. data/README_CN.md +1 -1
  5. data/README_JA.md +1 -1
  6. data/lib/clacky/agent/history_navigation.rb +1 -1
  7. data/lib/clacky/agent/llm_caller.rb +3 -51
  8. data/lib/clacky/agent/session_serializer.rb +12 -2
  9. data/lib/clacky/agent.rb +1 -1
  10. data/lib/clacky/agent_config.rb +21 -73
  11. data/lib/clacky/client.rb +18 -11
  12. data/lib/clacky/default_extensions/advisor/panels/advisor/view.js +1 -1
  13. data/lib/clacky/default_extensions/computer-use/skills/computer-use/SKILL.md +2 -0
  14. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +13 -19
  15. data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +2 -0
  16. data/lib/clacky/default_extensions/meeting/ext.yml +1 -0
  17. data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +2 -0
  18. data/lib/clacky/default_skills/browser-setup/SKILL.md +2 -0
  19. data/lib/clacky/default_skills/channel-manager/SKILL.md +2 -0
  20. data/lib/clacky/default_skills/channel-manager/install_feishu_skills.rb +1 -2
  21. data/lib/clacky/default_skills/code-explorer/SKILL.md +2 -0
  22. data/lib/clacky/default_skills/cron-task-creator/SKILL.md +2 -0
  23. data/lib/clacky/default_skills/deploy/SKILL.md +3 -1
  24. data/lib/clacky/default_skills/mcp-manager/SKILL.md +2 -0
  25. data/lib/clacky/default_skills/media-gen/SKILL.md +2 -0
  26. data/lib/clacky/default_skills/new/SKILL.md +2 -0
  27. data/lib/clacky/default_skills/onboard/SKILL.md +2 -0
  28. data/lib/clacky/default_skills/onboard/scripts/install_builtin_skills.rb +1 -2
  29. data/lib/clacky/default_skills/persist-memory/SKILL.md +2 -0
  30. data/lib/clacky/default_skills/personal-website/SKILL.md +2 -0
  31. data/lib/clacky/default_skills/personal-website/publish.rb +6 -10
  32. data/lib/clacky/default_skills/product-help/SKILL.md +2 -0
  33. data/lib/clacky/default_skills/recall-memory/SKILL.md +2 -0
  34. data/lib/clacky/default_skills/search-skills/SKILL.md +2 -0
  35. data/lib/clacky/default_skills/skill-add/SKILL.md +2 -0
  36. data/lib/clacky/default_skills/skill-creator/SKILL.md +2 -0
  37. data/lib/clacky/extension/packager.rb +3 -20
  38. data/lib/clacky/locales/en.rb +36 -4
  39. data/lib/clacky/locales/i18n.rb +16 -2
  40. data/lib/clacky/locales/zh.rb +37 -5
  41. data/lib/clacky/media/base.rb +2 -2
  42. data/lib/clacky/platform_http_client.rb +52 -117
  43. data/lib/clacky/providers.rb +62 -38
  44. data/lib/clacky/rich_ui/view_renderer.rb +3 -2
  45. data/lib/clacky/server/channel/adapters/feishu/adapter.rb +53 -1
  46. data/lib/clacky/server/channel/adapters/feishu/bot.rb +354 -75
  47. data/lib/clacky/server/channel/adapters/feishu/ws_client.rb +48 -13
  48. data/lib/clacky/server/channel/channel_config.rb +21 -4
  49. data/lib/clacky/server/channel/channel_manager.rb +5 -27
  50. data/lib/clacky/server/channel/channel_ui_controller.rb +65 -34
  51. data/lib/clacky/server/http_server.rb +59 -19
  52. data/lib/clacky/server/scheduler.rb +117 -2
  53. data/lib/clacky/server/session_registry.rb +0 -4
  54. data/lib/clacky/tools/ask_user.rb +2 -10
  55. data/lib/clacky/ui2/ui_controller.rb +5 -5
  56. data/lib/clacky/utils/channel_prompt.rb +33 -0
  57. data/lib/clacky/version.rb +1 -1
  58. data/lib/clacky/web/app.css +1277 -1344
  59. data/lib/clacky/web/app.js +10 -2
  60. data/lib/clacky/web/components/composer.js +130 -6
  61. data/lib/clacky/web/components/datepicker.js +11 -4
  62. data/lib/clacky/web/components/model-picker.js +10 -4
  63. data/lib/clacky/web/components/onboard.js +14 -22
  64. data/lib/clacky/web/core/ext.js +39 -1
  65. data/lib/clacky/web/features/billing/view.js +6 -5
  66. data/lib/clacky/web/features/channels/store.js +16 -1
  67. data/lib/clacky/web/features/channels/view.js +158 -148
  68. data/lib/clacky/web/features/extensions/view.js +10 -18
  69. data/lib/clacky/web/features/mcp/store.js +1 -1
  70. data/lib/clacky/web/features/mcp/view.js +112 -119
  71. data/lib/clacky/web/features/new-session/view.js +14 -12
  72. data/lib/clacky/web/features/profile/view.js +9 -9
  73. data/lib/clacky/web/features/skills/store.js +16 -8
  74. data/lib/clacky/web/features/skills/view.js +568 -215
  75. data/lib/clacky/web/features/tasks/store.js +28 -6
  76. data/lib/clacky/web/features/tasks/view.js +314 -102
  77. data/lib/clacky/web/features/trash/view.js +4 -6
  78. data/lib/clacky/web/features/workspace/view.js +24 -0
  79. data/lib/clacky/web/i18n.js +94 -32
  80. data/lib/clacky/web/index.html +157 -95
  81. data/lib/clacky/web/projects.js +10 -3
  82. data/lib/clacky/web/sessions.js +150 -44
  83. data/lib/clacky/web/utils.js +20 -0
  84. data/lib/clacky/web/ws-dispatcher.js +8 -4
  85. data/lib/clacky.rb +3 -0
  86. metadata +2 -1
@@ -9,8 +9,7 @@ require_relative '../../skill-add/scripts/install_from_zip'
9
9
 
10
10
  class BuiltinSkillsInstaller
11
11
  PRIMARY_HOST = ENV.fetch('CLACKY_LICENSE_SERVER', 'https://www.openclacky.com')
12
- FALLBACK_HOST = 'https://openclacky.up.railway.app'
13
- API_HOSTS = ENV['CLACKY_LICENSE_SERVER'] ? [PRIMARY_HOST] : [PRIMARY_HOST, FALLBACK_HOST]
12
+ API_HOSTS = [PRIMARY_HOST]
14
13
  API_PATH = '/api/v1/skills/builtin'
15
14
  API_OPEN_TIMEOUT = 5
16
15
  API_READ_TIMEOUT = 10
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  name: persist-memory
3
+ name_zh: 记住信息
4
+ description_zh: 把需要记住的内容写入长期记忆,自动归类合并。
3
5
  description: Persist information to long-term memory at ~/.clacky/memories/. Use when the user asks you to remember/note something, or when reviewing a finished conversation for facts worth keeping. Handles file naming, topic merging, frontmatter, and size limits.
4
6
  fork_agent: true
5
7
  user-invocable: false
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  name: personal-website
3
+ name_zh: 个人主页
4
+ description_zh: 根据你的资料生成一张精美的个人主页并发布上线,得到可分享的链接。
3
5
  description: |
4
6
  Generate a beautiful personal homepage (linktree-style) and publish it online for the user.
5
7
  Reads user info from ~/.clacky/agents/USER.md and AI info from ~/.clacky/agents/SOUL.md.
@@ -22,12 +22,9 @@ require "fileutils"
22
22
 
23
23
  # ── Config ───────────────────────────────────────────────────────────────────
24
24
 
25
- # Primary CDN-accelerated endpoint.
26
- # Fallback bypasses EdgeOne and is used when the primary times out or errors.
27
- PRIMARY_HOST = ENV.fetch("CLACKY_LICENSE_SERVER", "https://www.openclacky.com")
28
- FALLBACK_HOST = "https://openclacky.up.railway.app"
29
- # When the env override is set we use only that host (dev/test mode).
30
- API_HOSTS = ENV["CLACKY_LICENSE_SERVER"] ? [PRIMARY_HOST] : [PRIMARY_HOST, FALLBACK_HOST]
25
+ # Platform endpoint; CLACKY_LICENSE_SERVER overrides it for dev/test.
26
+ PRIMARY_HOST = ENV.fetch("CLACKY_LICENSE_SERVER", "https://www.openclacky.com")
27
+ API_HOSTS = [PRIMARY_HOST]
31
28
 
32
29
  HMAC_SECRET = ENV.fetch("CARD_HMAC_SECRET", "openclacky-card-v1-default-secret-change-me")
33
30
  TOKEN_FILE = File.expand_path("~/clacky_workspace/personal_website/token.json")
@@ -64,15 +61,14 @@ end
64
61
 
65
62
  # ── HTTP helpers ──────────────────────────────────────────────────────────────
66
63
 
67
- # Resilient HTTP request: retries on transient errors, then fails over to the
68
- # fallback host before giving up.
64
+ # Resilient HTTP request: retries on transient errors before giving up.
69
65
  #
70
66
  # Returns [http_code_int, parsed_body_hash].
71
- # Calls exit(1) on network failure (all hosts/attempts exhausted).
67
+ # Calls exit(1) on network failure (all attempts exhausted).
72
68
  def http_request(method, path, body: nil, extra_headers: {})
73
69
  last_error = nil
74
70
 
75
- API_HOSTS.each_with_index do |base, host_index|
71
+ API_HOSTS.each do |base|
76
72
  ATTEMPTS_PER_HOST.times do |attempt|
77
73
  begin
78
74
  result = do_http_request(method, base, path, body: body, extra_headers: extra_headers)
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  name: product-help
3
+ name_zh: 产品帮助
4
+ description_zh: 解答关于产品本身的问题:安装、技能、配置、记忆、会话、品牌定制、故障排查等。
3
5
  description: 'Use this skill when the user asks about my own features, configuration, or usage — installation, skills, Web UI, CLI, API config, memory, sessions, encryption, white-label, publishing, pricing, troubleshooting, or restarting the server. Do NOT trigger for general coding tasks unrelated to me.'
4
6
  fork_agent: true
5
7
  user-invocable: false
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  name: recall-memory
3
+ name_zh: 回忆记忆
4
+ description_zh: 按主题从长期记忆中找出相关内容并整理成摘要。
3
5
  description: Recall relevant long-term memories on demand. Given a topic or question, judges relevance from pre-loaded metadata, loads only relevant files, and returns a concise summary to the main agent.
4
6
  fork_agent: true
5
7
  user-invocable: false
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  name: search-skills
3
+ name_zh: 搜索技能
4
+ description_zh: 按关键词在所有已安装的技能中查找合适的技能。
3
5
  description: 'Search ALL installed skills (including ones not shown in AVAILABLE SKILLS) by keyword. Use this whenever you suspect a fitting skill might exist but is not listed in your system prompt — for example before building a new skill, when the user mentions a domain not covered by visible skills, or after seeing the (N more skills installed) hint. Triggers on phrases like search skills, find a skill for, is there a skill that, 查找skill, 有没有skill做.'
4
6
  disable-model-invocation: false
5
7
  user-invocable: true
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  name: skill-add
3
+ name_zh: 安装技能
4
+ description_zh: 通过 zip 链接或本地 zip 文件安装技能。
3
5
  description: 'Install skills from a zip URL or local zip file path. Use this skill whenever the user wants to install a skill from a zip link or a local file, or uses commands like /skill-add with a URL or file path. Trigger on phrases like: install skill, install from zip, skill from zip, skill from url, add skill from zip, 安装skill, 从zip安装skill, 从本地安装skill.'
4
6
  disable-model-invocation: false
5
7
  user-invocable: true
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  name: skill-creator
3
+ name_zh: 技能创建
4
+ description_zh: 从零创建新技能,或修改、优化已有技能,并评估技能效果。
3
5
  description: Create new skills, modify and improve existing skills, and measure skill performance. Use when users want to create a skill from scratch, edit, or optimize an existing skill, run evals to test a skill, benchmark skill performance with variance analysis, or optimize a skill's description for better triggering accuracy.
4
6
  always-show: true
5
7
  ---
@@ -240,7 +240,7 @@ module Clacky
240
240
  src = source.to_s
241
241
  if src.match?(%r{\Ahttps?://})
242
242
  dest = File.join(tmp, "download.zip")
243
- download(src, dest, on_progress: on_progress)
243
+ download_once(src, dest, on_progress: on_progress)
244
244
  dest
245
245
  else
246
246
  path = File.expand_path(src)
@@ -249,23 +249,6 @@ module Clacky
249
249
  end
250
250
  end
251
251
 
252
- # Download url → dest file, retrying once on the secondary CDN host when
253
- # the primary fails (same path/query). Both hosts serve the same backend,
254
- # so ActiveStorage signed_ids resolve identically on either.
255
- private def download(url, dest, on_progress: nil)
256
- download_once(url, dest, on_progress: on_progress)
257
- rescue Error
258
- fallback = secondary_url(url)
259
- raise unless fallback
260
- download_once(fallback, dest, on_progress: on_progress)
261
- end
262
-
263
- private def secondary_url(url)
264
- return nil unless url.start_with?(PlatformHttpClient::PRIMARY_HOST)
265
-
266
- url.sub(PlatformHttpClient::PRIMARY_HOST, PlatformHttpClient::SECONDARY_HOST)
267
- end
268
-
269
252
  # OpenURI's block form (`URI.open(url) { |io| ... }`) buffers the entire
270
253
  # response before yielding `io`, so reading `io` afterwards reports
271
254
  # progress only after the download has finished. We hook OpenURI's own
@@ -302,8 +285,8 @@ module Clacky
302
285
  end
303
286
  rescue Net::OpenTimeout, Net::ReadTimeout => e
304
287
  raise Error, "download timed out: #{e.message}"
305
- # Catch-all so any transport-layer failure triggers download()'s
306
- # secondary-host fallback instead of surfacing a raw error.
288
+ # Catch-all so any transport-layer failure surfaces as a packaged Error
289
+ # instead of a raw exception.
307
290
  rescue StandardError => e
308
291
  raise Error, "failed to download #{url}: #{e.message}"
309
292
  end
@@ -8,7 +8,7 @@ module Clacky
8
8
  "llm.error.insufficient_credit" => "Insufficient credit, please top up your account to continue",
9
9
  "llm.error.rate_limit_400" => "Rate limit or service issue, retrying...",
10
10
  "llm.error.invalid_api_key" => "API key is invalid or expired, please update it in Settings",
11
- "llm.error.403.model_not_allowed" => "This model is not available on your current plan",
11
+ "llm.error.403.model_not_allowed" => "Free trial only supports the or-gemini-3-8-flash model. Switch to that model, or top up to use other models",
12
12
  "llm.error.403.api_key_revoked" => "API key has been revoked, please generate a new one",
13
13
  "llm.error.403.api_key_expired" => "API key has expired, please generate a new one",
14
14
  "llm.error.403.quota_exceeded" => "API key quota exceeded. Please update the quota or disable the limit in the admin console",
@@ -20,11 +20,10 @@ module Clacky
20
20
  "llm.error.server_error" => "Service temporarily unavailable (%<status>d), retrying...",
21
21
  "llm.error.unexpected" => "Unexpected error (%<status>d)",
22
22
  "llm.error.html_response" => "Service temporarily unavailable (received HTML error page), retrying...",
23
- "llm.error.bad_request" => "Bad request: invalid parameters. Please check your model configuration",
23
+ "llm.error.bad_request" => "Model request failed. Please switch to another model and retry",
24
24
  "llm.error.request_timeout" => "Request timed out after %<retries>d retries",
25
25
  "llm.error.network_failed" => "Network connection failed after %<retries>d retries",
26
26
  "llm.error.service_unavailable" => "Service unavailable after %<retries>d retries",
27
- "llm.warn.switching_to_fallback_url" => "Primary endpoint unreachable. Switching to fallback gateway: %<url>s",
28
27
  "platform.error.invalid_proof" => "Invalid license key — please check and try again.",
29
28
  "platform.error.invalid_signature" => "Invalid request signature.",
30
29
  "platform.error.nonce_replayed" => "Duplicate request detected. Please try again.",
@@ -42,7 +41,40 @@ module Clacky
42
41
  "platform.error.device_token_expired" => "This device's authorization has expired. Please re-authorize before publishing.",
43
42
  "platform.error.owner_user_not_found" => "No account found for this device. Please re-authorize it.",
44
43
  "platform.error.generic" => "Request failed (HTTP %<code>s). Please contact support.",
45
- "platform.error.generic_with_code" => "Request failed (HTTP %<code>s, code: %<error_code>s). Please contact support."
44
+ "platform.error.generic_with_code" => "Request failed (HTTP %<code>s, code: %<error_code>s). Please contact support.",
45
+ "channel.questions.context" => "Context",
46
+ "channel.questions.single" => "Question",
47
+ "channel.questions.numbered" => "Question %<index>d",
48
+ "channel.questions.free_text_hint" => "Or reply with your own answer.",
49
+ "channel.questions.recorded" => "Answer recorded",
50
+ "channel.questions.expired" => "This question is no longer active.",
51
+ "channel.questions.multi_hint" => "Select all that apply, then submit.",
52
+ "channel.questions.submit" => "Submit",
53
+ "channel.questions.incomplete" => "Please answer every question first.",
54
+ "channel.progress.thinking" => "Thinking...",
55
+ "channel.progress.working" => "Working...",
56
+ "channel.progress.tool.browser" => "Using the browser...",
57
+ "channel.progress.tool.edit" => "Editing a file...",
58
+ "channel.progress.tool.file_reader" => "Reading a file...",
59
+ "channel.progress.tool.glob" => "Finding files...",
60
+ "channel.progress.tool.grep" => "Searching files...",
61
+ "channel.progress.tool.invoke_skill" => "Using a skill...",
62
+ "channel.progress.tool.terminal" => "Running a command...",
63
+ "channel.progress.tool.todo_manager" => "Updating the task plan...",
64
+ "channel.progress.tool.web_fetch" => "Reading a web page...",
65
+ "channel.progress.tool.web_search" => "Searching the web...",
66
+ "channel.progress.tool.write" => "Writing a file...",
67
+ "channel.progress.task_interrupted" => "Task interrupted.",
68
+ "channel.progress.waiting_response" => "Waiting for your response.",
69
+ "channel.progress.step" => "%<count>d step",
70
+ "channel.progress.steps" => "%<count>d steps",
71
+ "channel.progress.error" => "Error: %<message>s",
72
+ "channel.progress.view_process" => "View process",
73
+ "channel.progress.generating" => "[Generating...]",
74
+ "channel.progress.status.waiting" => "Waiting for input",
75
+ "channel.progress.status.success" => "Done",
76
+ "channel.progress.status.failed" => "Failed",
77
+ "channel.progress.status.interrupted" => "Stopped"
46
78
  }.freeze
47
79
  end
48
80
  end
@@ -9,13 +9,27 @@ module Clacky
9
9
  "zh" => Clacky::Locales::ZH,
10
10
  "en" => Clacky::Locales::EN
11
11
  }.freeze
12
+ DEFAULT_LOCALE = "en"
12
13
 
13
14
  def self.t(key, **vars)
14
- table = LOCALES[locale] || LOCALES["en"]
15
- msg = table[key] || LOCALES["en"][key] || key
15
+ translate(locale, key, **vars)
16
+ end
17
+
18
+ def self.translate(code, key, **vars)
19
+ table = LOCALES[code] || LOCALES[DEFAULT_LOCALE]
20
+ msg = table[key] || LOCALES[DEFAULT_LOCALE][key] || key
16
21
  vars.empty? ? msg : format(msg, **vars)
17
22
  end
18
23
 
24
+ # Builds { locale_code => value } for every supported locale.
25
+ def self.localized
26
+ LOCALES.keys.map { |code| [code, yield(code)] }.to_h
27
+ end
28
+
29
+ def self.translations(key, **vars)
30
+ localized { |code| translate(code, key, **vars) }
31
+ end
32
+
19
33
  def self.locale
20
34
  return Thread.current[:lang] if Thread.current[:lang]
21
35
 
@@ -5,10 +5,10 @@ module Clacky
5
5
  ZH = {
6
6
  "phase.skill_evolution" => "正在复盘本次任务",
7
7
  "phase.memory_update" => "正在更新长期记忆",
8
- "llm.error.insufficient_credit" => "账户余额不足,请前往控制台充值后继续使用",
8
+ "llm.error.insufficient_credit" => "API Key 账户余额不足,请前往控制台充值后继续使用",
9
9
  "llm.error.rate_limit_400" => "请求频率过高或服务暂时不可用,正在重试...",
10
10
  "llm.error.invalid_api_key" => "API Key 无效或已过期,请到设置中重新配置",
11
- "llm.error.403.model_not_allowed" => "当前模型不支持免费试用,请升级套餐或切换其他模型",
11
+ "llm.error.403.model_not_allowed" => "免费试用仅支持 or-gemini-3-8-flash 模型,请切换到该模型,或充值后使用其他模型",
12
12
  "llm.error.403.api_key_revoked" => "API 密钥已被撤销,请前往控制台重新生成",
13
13
  "llm.error.403.api_key_expired" => "API 密钥已过期,请前往控制台重新生成",
14
14
  "llm.error.403.quota_exceeded" => "API Key 配额已超限,请在管理后台调整配额或关闭限制",
@@ -20,11 +20,10 @@ module Clacky
20
20
  "llm.error.server_error" => "服务暂时不可用(%<status>d),正在重试...",
21
21
  "llm.error.unexpected" => "请求失败(%<status>d)",
22
22
  "llm.error.html_response" => "服务暂时不可用(收到 HTML 错误页),正在重试...",
23
- "llm.error.bad_request" => "请求参数有误,请检查模型配置或重试",
23
+ "llm.error.bad_request" => "请求模型出错,请暂时切换其他模型重试",
24
24
  "llm.error.request_timeout" => "请求超时(已重试 %<retries>d 次)",
25
25
  "llm.error.network_failed" => "网络连接失败(已重试 %<retries>d 次)",
26
26
  "llm.error.service_unavailable" => "服务暂时不可用(已重试 %<retries>d 次)",
27
- "llm.warn.switching_to_fallback_url" => "主节点无法连接,正在切换到备用节点:%<url>s",
28
27
  "platform.error.invalid_proof" => "许可证密钥无效,请检查后重试。",
29
28
  "platform.error.invalid_signature" => "请求签名无效。",
30
29
  "platform.error.nonce_replayed" => "检测到重复请求,请重试。",
@@ -42,7 +41,40 @@ module Clacky
42
41
  "platform.error.device_token_expired" => "此设备的授权已过期,请重新授权后再发布。",
43
42
  "platform.error.owner_user_not_found" => "未找到该设备对应的账户,请重新授权此设备。",
44
43
  "platform.error.generic" => "请求失败(HTTP %<code>s),请联系客服。",
45
- "platform.error.generic_with_code" => "请求失败(HTTP %<code>s,错误码:%<error_code>s),请联系客服。"
44
+ "platform.error.generic_with_code" => "请求失败(HTTP %<code>s,错误码:%<error_code>s),请联系客服。",
45
+ "channel.questions.context" => "背景",
46
+ "channel.questions.single" => "问题",
47
+ "channel.questions.numbered" => "问题 %<index>d",
48
+ "channel.questions.free_text_hint" => "也可以直接回复你自己的答案。",
49
+ "channel.questions.recorded" => "已记录你的选择",
50
+ "channel.questions.expired" => "该问题已失效。",
51
+ "channel.questions.multi_hint" => "可多选,选好后点提交。",
52
+ "channel.questions.submit" => "提交",
53
+ "channel.questions.incomplete" => "请先完成所有问题。",
54
+ "channel.progress.thinking" => "思考中...",
55
+ "channel.progress.working" => "处理中...",
56
+ "channel.progress.tool.browser" => "正在使用浏览器...",
57
+ "channel.progress.tool.edit" => "正在编辑文件...",
58
+ "channel.progress.tool.file_reader" => "正在读取文件...",
59
+ "channel.progress.tool.glob" => "正在查找文件...",
60
+ "channel.progress.tool.grep" => "正在搜索文件内容...",
61
+ "channel.progress.tool.invoke_skill" => "正在使用技能...",
62
+ "channel.progress.tool.terminal" => "正在执行命令...",
63
+ "channel.progress.tool.todo_manager" => "正在更新任务计划...",
64
+ "channel.progress.tool.web_fetch" => "正在读取网页...",
65
+ "channel.progress.tool.web_search" => "正在联网搜索...",
66
+ "channel.progress.tool.write" => "正在写入文件...",
67
+ "channel.progress.task_interrupted" => "任务已中断。",
68
+ "channel.progress.waiting_response" => "等待你的回复。",
69
+ "channel.progress.step" => "%<count>d 步",
70
+ "channel.progress.steps" => "%<count>d 步",
71
+ "channel.progress.error" => "错误:%<message>s",
72
+ "channel.progress.view_process" => "查看过程",
73
+ "channel.progress.generating" => "[生成中...]",
74
+ "channel.progress.status.waiting" => "等待输入",
75
+ "channel.progress.status.success" => "已完成",
76
+ "channel.progress.status.failed" => "失败",
77
+ "channel.progress.status.interrupted" => "已停止"
46
78
  }.freeze
47
79
  end
48
80
  end
@@ -53,7 +53,7 @@ module Clacky
53
53
 
54
54
  def generate_transcription(audio_base64:, mime_type:, **_kwargs)
55
55
  transcription_error_response(
56
- error: "Speech-to-text is not supported by #{self.class.name.split("::").last}. Use the openclacky gateway with an STT model such as or-stt-gemini-3-5-flash.",
56
+ error: "Speech-to-text is not supported by #{self.class.name.split("::").last}. Use the openclacky gateway with an STT model such as or-stt-gemini-3-8-flash.",
57
57
  error_type: "not_implemented",
58
58
  provider: ""
59
59
  )
@@ -63,7 +63,7 @@ module Clacky
63
63
  # video_understanding_error_response(...)
64
64
  def understand_video(video_base64:, mime_type:, prompt: nil, **_kwargs)
65
65
  video_understanding_error_response(
66
- error: "Video understanding is not supported by #{self.class.name.split("::").last}. Use the openclacky gateway with a video understanding model such as or-gemini-3-5-flash.",
66
+ error: "Video understanding is not supported by #{self.class.name.split("::").last}. Use the openclacky gateway with a video understanding model such as or-gemini-3-8-flash.",
67
67
  error_type: "not_implemented",
68
68
  provider: ""
69
69
  )
@@ -8,14 +8,12 @@ require_relative "locales/i18n"
8
8
 
9
9
  module Clacky
10
10
  # PlatformHttpClient provides a resilient HTTP client for all calls to the
11
- # OpenClacky platform API (www.openclacky.com and its fallback domain).
11
+ # OpenClacky platform API (www.openclacky.com).
12
12
  #
13
13
  # Features:
14
14
  # - Automatic retry with exponential back-off on transient failures
15
- # - Transparent domain failover: if the primary domain times out or returns a
16
- # 5xx error, the request is automatically retried against the fallback domain
17
- # - Unified large-file download entry point (#download_file) that reuses the
18
- # same primary → fallback failover policy as API calls
15
+ # - Unified large-file download entry point (#download_file) sharing the
16
+ # same retry policy as API calls
19
17
  # - Override via CLACKY_LICENSE_SERVER env var (auto-detected, used in development)
20
18
  #
21
19
  # Usage:
@@ -25,16 +23,12 @@ module Clacky
25
23
  # # or { success: false, error: "...", data: {} }
26
24
  class PlatformHttpClient
27
25
  # Primary endpoint
28
- PRIMARY_HOST = "https://www.openclacky.com"
29
- # Secondary CDN-accelerated endpoint (China mainland)
30
- SECONDARY_HOST = "https://api.1024code.com"
31
- # Direct fallback — bypasses EdgeOne, used when the primary times out
32
- FALLBACK_HOST = "https://openclacky.up.railway.app"
33
-
34
- # Number of attempts per domain (1 = no retry within the same domain)
35
- ATTEMPTS_PER_HOST = 1
36
- # Initial back-off between retries within the same domain (seconds)
37
- INITIAL_BACKOFF = 0.5
26
+ PRIMARY_HOST = "https://www.openclacky.com"
27
+
28
+ # Attempts per request: the initial try plus one retry
29
+ MAX_ATTEMPTS = 2
30
+ # Back-off before a retry (seconds), doubled on each further attempt
31
+ INITIAL_BACKOFF = 0.5
38
32
  # Connection / read timeouts (seconds) for API calls
39
33
  OPEN_TIMEOUT = 5
40
34
  READ_TIMEOUT = 15
@@ -53,15 +47,12 @@ module Clacky
53
47
  invalid_device_token device_token_revoked device_token_expired owner_user_not_found
54
48
  ].freeze
55
49
 
56
- # Auto-detects the target host(s):
57
- # - When CLACKY_LICENSE_SERVER is set → single host (dev override, no failover)
58
- # - Otherwise → [PRIMARY_HOST, SECONDARY_HOST, FALLBACK_HOST]
50
+ # Auto-detects the platform endpoint:
51
+ # - When CLACKY_LICENSE_SERVER is set → that URL (dev override)
52
+ # - Otherwise → PRIMARY_HOST
59
53
  def initialize
60
- if (override = ENV["CLACKY_LICENSE_SERVER"]) && !override.empty?
61
- @hosts = [override]
62
- else
63
- @hosts = [PRIMARY_HOST, SECONDARY_HOST, FALLBACK_HOST]
64
- end
54
+ override = ENV["CLACKY_LICENSE_SERVER"]
55
+ @base_url = override && !override.empty? ? override : PRIMARY_HOST
65
56
  end
66
57
 
67
58
  # Send a POST request with a JSON body and return a normalised result hash.
@@ -71,7 +62,7 @@ module Clacky
71
62
  # @param headers [Hash] Additional HTTP headers (optional)
72
63
  # @return [Hash] { success: Boolean, data: Hash, error: String }
73
64
  def post(path, payload, headers: {})
74
- request_with_failover(:post, path, payload, headers)
65
+ request_with_retry(:post, path, payload, headers)
75
66
  end
76
67
 
77
68
  # Send a GET request and return a normalised result hash.
@@ -81,17 +72,17 @@ module Clacky
81
72
  # @param headers [Hash] Additional HTTP headers (optional)
82
73
  # @return [Hash] { success: Boolean, data: Hash, error: String }
83
74
  def get(path, headers: {})
84
- request_with_failover(:get, path, nil, headers)
75
+ request_with_retry(:get, path, nil, headers)
85
76
  end
86
77
 
87
78
  # Send a PATCH request. Same contract as #post.
88
79
  def patch(path, payload, headers: {})
89
- request_with_failover(:patch, path, payload, headers)
80
+ request_with_retry(:patch, path, payload, headers)
90
81
  end
91
82
 
92
83
  # Send a DELETE request (no body).
93
84
  def delete(path, headers: {})
94
- request_with_failover(:delete, path, nil, headers)
85
+ request_with_retry(:delete, path, nil, headers)
95
86
  end
96
87
 
97
88
  # Send a multipart/form-data POST.
@@ -103,34 +94,27 @@ module Clacky
103
94
  # @return [Hash] { success: Boolean, data: Hash, error: String }
104
95
  def multipart_post(path, body_bytes, boundary, read_timeout: READ_TIMEOUT)
105
96
  headers = { "Content-Type" => "multipart/form-data; boundary=#{boundary}" }
106
- request_with_failover(:multipart_post, path, body_bytes, headers,
107
- read_timeout_override: read_timeout)
97
+ request_with_retry(:multipart_post, path, body_bytes, headers,
98
+ read_timeout_override: read_timeout)
108
99
  end
109
100
 
110
101
  # Send a multipart/form-data PATCH. Same contract as #multipart_post.
111
102
  def multipart_patch(path, body_bytes, boundary, read_timeout: READ_TIMEOUT)
112
103
  headers = { "Content-Type" => "multipart/form-data; boundary=#{boundary}" }
113
- request_with_failover(:multipart_patch, path, body_bytes, headers,
114
- read_timeout_override: read_timeout)
104
+ request_with_retry(:multipart_patch, path, body_bytes, headers,
105
+ read_timeout_override: read_timeout)
115
106
  end
116
107
 
117
- # Stream a remote URL to a local file path, with automatic primary → secondary
118
- # host failover.
108
+ # Stream a remote URL to a local file path, retrying transient failures.
119
109
  #
120
110
  # This is the unified entry point for all large-file downloads (brand skill
121
111
  # ZIPs, platform-hosted assets, etc.). Callers should NOT build their own
122
- # Net::HTTP loops — failover, retry, redirects, and timeouts are handled here.
112
+ # Net::HTTP loops — retry, redirects, and timeouts are handled here.
123
113
  #
124
- # Host failover policy:
125
- # - If +url+'s host matches PRIMARY_HOST and the request fails with a
126
- # retryable error (timeout, connection reset, SSL, 5xx), the URL is
127
- # rewritten to SECONDARY_HOST (same path/query) and retried.
128
- # - Both hosts serve the same Rails backend and share +secret_key_base+,
129
- # so ActiveStorage signed_ids resolve identically on either.
130
- # - Third-party hosts (e.g. S3 presigned URLs reached via redirect) are
131
- # fetched as-is without host rewriting.
114
+ # The URL is always fetched as-is: third-party hosts (S3 presigned URLs
115
+ # reached via redirect, CDNs, user-provided URLs) are never rewritten.
132
116
  #
133
- # Each host gets ATTEMPTS_PER_HOST attempts with exponential back-off.
117
+ # The request gets MAX_ATTEMPTS attempts with exponential back-off.
134
118
  # Up to DOWNLOAD_MAX_REDIRECTS redirects are followed per attempt.
135
119
  #
136
120
  # @param url [String] Full URL to download
@@ -140,39 +124,23 @@ module Clacky
140
124
  # @param read_timeout [Integer] Override read timeout (seconds)
141
125
  # @return [Hash] { success: Boolean, bytes: Integer, error: String }
142
126
  def download_file(url, dest, read_timeout: DOWNLOAD_READ_TIMEOUT)
143
- candidate_urls = [url]
144
- # Only auto-add a secondary candidate when the URL is on our primary host.
145
- # External hosts (S3, CDNs, user-provided URLs) are fetched as-is.
146
- if primary_host_url?(url)
147
- candidate_urls << swap_to_secondary_host(url)
148
- end
149
-
150
127
  last_error = nil
151
128
  FileUtils.mkdir_p(File.dirname(dest))
152
129
  tmp_dest = "#{dest}.part"
153
130
 
154
- candidate_urls.each_with_index do |candidate, host_index|
155
- ATTEMPTS_PER_HOST.times do |attempt|
156
- begin
157
- bytes = stream_download(candidate, tmp_dest, read_timeout: read_timeout)
158
- File.rename(tmp_dest, dest)
159
- return { success: true, bytes: bytes, error: nil }
160
- rescue RetryableNetworkError => e
161
- last_error = e
162
- backoff = INITIAL_BACKOFF * (2**attempt)
163
- Clacky::Logger.debug(
164
- "[PlatformHTTP] DOWNLOAD #{candidate} attempt #{attempt + 1} failed: " \
165
- "#{e.message} — retrying in #{backoff}s"
166
- )
167
- sleep(backoff)
168
- end
169
- end
170
-
171
- if host_index + 1 < candidate_urls.size
131
+ MAX_ATTEMPTS.times do |attempt|
132
+ begin
133
+ bytes = stream_download(url, tmp_dest, read_timeout: read_timeout)
134
+ File.rename(tmp_dest, dest)
135
+ return { success: true, bytes: bytes, error: nil }
136
+ rescue RetryableNetworkError => e
137
+ last_error = e
138
+ backoff = INITIAL_BACKOFF * (2**attempt)
172
139
  Clacky::Logger.debug(
173
- "[PlatformHTTP] Primary host exhausted for download, switching to secondary: " \
174
- "#{candidate_urls[host_index + 1]}"
140
+ "[PlatformHTTP] DOWNLOAD #{url} attempt #{attempt + 1} failed: " \
141
+ "#{e.message} — retrying in #{backoff}s"
175
142
  )
143
+ sleep(backoff)
176
144
  end
177
145
  end
178
146
 
@@ -180,34 +148,9 @@ module Clacky
180
148
  { success: false, bytes: 0, error: "Download failed: #{last_error&.message || "unknown"}" }
181
149
  end
182
150
 
183
- # True when +url+ targets the primary platform host.
184
- # Used by #download_file to decide whether secondary-host rewriting is safe.
185
- private def primary_host_url?(url)
186
- return false if url.nil? || url.empty?
187
-
188
- uri = URI.parse(url)
189
- primary = URI.parse(PRIMARY_HOST)
190
- uri.host == primary.host
191
- rescue URI::InvalidURIError
192
- false
193
- end
194
-
195
- # Rewrite +url+ so its host is the secondary host (same path + query).
196
- # Callers must have already confirmed the URL's host is PRIMARY_HOST via
197
- # #primary_host_url? — this method does not validate that precondition.
198
- private def swap_to_secondary_host(url)
199
- uri = URI.parse(url)
200
- secondary = URI.parse(SECONDARY_HOST)
201
- uri.scheme = secondary.scheme
202
- uri.host = secondary.host
203
- # Only apply an explicit port when secondary declares a non-default one
204
- uri.port = secondary.port if secondary.port && secondary.port != secondary.default_port
205
- uri.to_s
206
- end
207
-
208
151
  # Execute a streaming GET with redirect following, writing the response body
209
152
  # to +dest+ as it arrives. Raises RetryableNetworkError on any transient
210
- # failure so the caller can decide whether to retry / failover.
153
+ # failure so the caller can decide whether to retry.
211
154
  #
212
155
  # @return [Integer] Number of bytes written
213
156
  private def stream_download(url, dest, read_timeout:)
@@ -247,7 +190,7 @@ module Clacky
247
190
  # 5xx is retryable, 4xx is terminal — but we don't have separate
248
191
  # handling in the existing API path and fallback is still useful
249
192
  # for e.g. upstream 502/503, so treat everything non-2xx/3xx as
250
- # retryable to match the spirit of request_with_failover.
193
+ # retryable to match the spirit of request_with_retry.
251
194
  raise RetryableNetworkError, "HTTP #{resp.code}"
252
195
  end
253
196
  end
@@ -272,33 +215,25 @@ module Clacky
272
215
  raise RetryableNetworkError, e.message
273
216
  end
274
217
 
275
- private def request_with_failover(method, path, payload, extra_headers, read_timeout_override: nil)
218
+ private def request_with_retry(method, path, payload, extra_headers, read_timeout_override: nil)
276
219
  last_error = nil
277
220
 
278
- @hosts.each_with_index do |base, host_index|
279
- ATTEMPTS_PER_HOST.times do |attempt|
280
- begin
281
- return execute_request(method, base, path, payload, extra_headers,
282
- read_timeout_override: read_timeout_override)
283
- rescue RetryableNetworkError => e
284
- last_error = e
285
- backoff = INITIAL_BACKOFF * (2**attempt)
286
- Clacky::Logger.debug(
287
- "[PlatformHTTP] #{method.upcase} #{base}#{path} attempt #{attempt + 1} failed: " \
288
- "#{e.message} — retrying in #{backoff}s"
289
- )
290
- sleep(backoff)
291
- end
292
- end
293
-
294
- if host_index + 1 < @hosts.size
221
+ MAX_ATTEMPTS.times do |attempt|
222
+ begin
223
+ return execute_request(method, @base_url, path, payload, extra_headers,
224
+ read_timeout_override: read_timeout_override)
225
+ rescue RetryableNetworkError => e
226
+ last_error = e
227
+ backoff = INITIAL_BACKOFF * (2**attempt)
295
228
  Clacky::Logger.debug(
296
- "[PlatformHTTP] Primary host exhausted, switching to fallback: #{@hosts[host_index + 1]}"
229
+ "[PlatformHTTP] #{method.upcase} #{@base_url}#{path} attempt #{attempt + 1} failed: " \
230
+ "#{e.message} — retrying in #{backoff}s"
297
231
  )
232
+ sleep(backoff)
298
233
  end
299
234
  end
300
235
 
301
- # All hosts / attempts exhausted
236
+ # All attempts exhausted
302
237
  { success: false, error: "Network error: #{last_error&.message || "unknown"}", data: {} }
303
238
  end
304
239