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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -0
- data/README.md +1 -1
- data/README_CN.md +1 -1
- data/README_JA.md +1 -1
- data/lib/clacky/agent/history_navigation.rb +1 -1
- data/lib/clacky/agent/llm_caller.rb +3 -51
- data/lib/clacky/agent/session_serializer.rb +12 -2
- data/lib/clacky/agent.rb +1 -1
- data/lib/clacky/agent_config.rb +21 -73
- data/lib/clacky/client.rb +18 -11
- data/lib/clacky/default_extensions/advisor/panels/advisor/view.js +1 -1
- data/lib/clacky/default_extensions/computer-use/skills/computer-use/SKILL.md +2 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +13 -19
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +2 -0
- data/lib/clacky/default_extensions/meeting/ext.yml +1 -0
- data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +2 -0
- data/lib/clacky/default_skills/browser-setup/SKILL.md +2 -0
- data/lib/clacky/default_skills/channel-manager/SKILL.md +2 -0
- data/lib/clacky/default_skills/channel-manager/install_feishu_skills.rb +1 -2
- data/lib/clacky/default_skills/code-explorer/SKILL.md +2 -0
- data/lib/clacky/default_skills/cron-task-creator/SKILL.md +2 -0
- data/lib/clacky/default_skills/deploy/SKILL.md +3 -1
- data/lib/clacky/default_skills/mcp-manager/SKILL.md +2 -0
- data/lib/clacky/default_skills/media-gen/SKILL.md +2 -0
- data/lib/clacky/default_skills/new/SKILL.md +2 -0
- data/lib/clacky/default_skills/onboard/SKILL.md +2 -0
- data/lib/clacky/default_skills/onboard/scripts/install_builtin_skills.rb +1 -2
- data/lib/clacky/default_skills/persist-memory/SKILL.md +2 -0
- data/lib/clacky/default_skills/personal-website/SKILL.md +2 -0
- data/lib/clacky/default_skills/personal-website/publish.rb +6 -10
- data/lib/clacky/default_skills/product-help/SKILL.md +2 -0
- data/lib/clacky/default_skills/recall-memory/SKILL.md +2 -0
- data/lib/clacky/default_skills/search-skills/SKILL.md +2 -0
- data/lib/clacky/default_skills/skill-add/SKILL.md +2 -0
- data/lib/clacky/default_skills/skill-creator/SKILL.md +2 -0
- data/lib/clacky/extension/packager.rb +3 -20
- data/lib/clacky/locales/en.rb +36 -4
- data/lib/clacky/locales/i18n.rb +16 -2
- data/lib/clacky/locales/zh.rb +37 -5
- data/lib/clacky/media/base.rb +2 -2
- data/lib/clacky/platform_http_client.rb +52 -117
- data/lib/clacky/providers.rb +62 -38
- data/lib/clacky/rich_ui/view_renderer.rb +3 -2
- data/lib/clacky/server/channel/adapters/feishu/adapter.rb +53 -1
- data/lib/clacky/server/channel/adapters/feishu/bot.rb +354 -75
- data/lib/clacky/server/channel/adapters/feishu/ws_client.rb +48 -13
- data/lib/clacky/server/channel/channel_config.rb +21 -4
- data/lib/clacky/server/channel/channel_manager.rb +5 -27
- data/lib/clacky/server/channel/channel_ui_controller.rb +65 -34
- data/lib/clacky/server/http_server.rb +59 -19
- data/lib/clacky/server/scheduler.rb +117 -2
- data/lib/clacky/server/session_registry.rb +0 -4
- data/lib/clacky/tools/ask_user.rb +2 -10
- data/lib/clacky/ui2/ui_controller.rb +5 -5
- data/lib/clacky/utils/channel_prompt.rb +33 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +1277 -1344
- data/lib/clacky/web/app.js +10 -2
- data/lib/clacky/web/components/composer.js +130 -6
- data/lib/clacky/web/components/datepicker.js +11 -4
- data/lib/clacky/web/components/model-picker.js +10 -4
- data/lib/clacky/web/components/onboard.js +14 -22
- data/lib/clacky/web/core/ext.js +39 -1
- data/lib/clacky/web/features/billing/view.js +6 -5
- data/lib/clacky/web/features/channels/store.js +16 -1
- data/lib/clacky/web/features/channels/view.js +158 -148
- data/lib/clacky/web/features/extensions/view.js +10 -18
- data/lib/clacky/web/features/mcp/store.js +1 -1
- data/lib/clacky/web/features/mcp/view.js +112 -119
- data/lib/clacky/web/features/new-session/view.js +14 -12
- data/lib/clacky/web/features/profile/view.js +9 -9
- data/lib/clacky/web/features/skills/store.js +16 -8
- data/lib/clacky/web/features/skills/view.js +568 -215
- data/lib/clacky/web/features/tasks/store.js +28 -6
- data/lib/clacky/web/features/tasks/view.js +314 -102
- data/lib/clacky/web/features/trash/view.js +4 -6
- data/lib/clacky/web/features/workspace/view.js +24 -0
- data/lib/clacky/web/i18n.js +94 -32
- data/lib/clacky/web/index.html +157 -95
- data/lib/clacky/web/projects.js +10 -3
- data/lib/clacky/web/sessions.js +150 -44
- data/lib/clacky/web/utils.js +20 -0
- data/lib/clacky/web/ws-dispatcher.js +8 -4
- data/lib/clacky.rb +3 -0
- 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
|
-
|
|
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
|
-
#
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
306
|
-
#
|
|
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
|
data/lib/clacky/locales/en.rb
CHANGED
|
@@ -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" => "
|
|
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" => "
|
|
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
|
data/lib/clacky/locales/i18n.rb
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
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
|
|
data/lib/clacky/locales/zh.rb
CHANGED
|
@@ -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
|
data/lib/clacky/media/base.rb
CHANGED
|
@@ -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-
|
|
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-
|
|
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
|
|
11
|
+
# OpenClacky platform API (www.openclacky.com).
|
|
12
12
|
#
|
|
13
13
|
# Features:
|
|
14
14
|
# - Automatic retry with exponential back-off on transient failures
|
|
15
|
-
# -
|
|
16
|
-
#
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
57
|
-
# - When CLACKY_LICENSE_SERVER is set →
|
|
58
|
-
# - Otherwise
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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,
|
|
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 —
|
|
112
|
+
# Net::HTTP loops — retry, redirects, and timeouts are handled here.
|
|
123
113
|
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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]
|
|
174
|
-
"#{
|
|
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
|
|
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
|
|
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
|
|
218
|
+
private def request_with_retry(method, path, payload, extra_headers, read_timeout_override: nil)
|
|
276
219
|
last_error = nil
|
|
277
220
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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]
|
|
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
|
|
236
|
+
# All attempts exhausted
|
|
302
237
|
{ success: false, error: "Network error: #{last_error&.message || "unknown"}", data: {} }
|
|
303
238
|
end
|
|
304
239
|
|