openclacky 1.5.16 → 1.5.17

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 +39 -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 +35 -3
  39. data/lib/clacky/locales/i18n.rb +16 -2
  40. data/lib/clacky/locales/zh.rb +36 -4
  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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9939ab09ac4e473dc7b096dac6a14c67f6d6060fe7678d7bd0b2635c7cc924ed
4
- data.tar.gz: 9e0e566a77267f73388a9761d101672c86fd35513dc7248355a3c3df7d8017f6
3
+ metadata.gz: 23761f95ba9b1fa52c2acc37020fd4e903a92d8f49c0c97668d5cfbb6e572329
4
+ data.tar.gz: 16838f20ba7d650e15952c738c35b93ca418a8df81baaeb6f9fd0263c5754e65
5
5
  SHA512:
6
- metadata.gz: 206657c6e08b40482a5029abf25fb8bd8579728ea469e2b2f7fba5b0f95dbbbcbfb8ca75778f73090bbbb34d7145cf58dab64c7d7462325d981a4e5106ced23e
7
- data.tar.gz: 0b30e40beb4d544076f2063160d04d9fcc2099b43ce46c78e1694d35ec631859361ffc15b2521a8a53e0c0cd84fb52e157cebf4469557991f273770842808568
6
+ metadata.gz: d0a29e5ab8a0e6b21e1f2a2f36657bcb8043cfc9d904671acdead8157f082ac032c2d1f7f0a0cee16c0f72a7fdbb939020b3abfe52145a9eea722f4f4856bac2
7
+ data.tar.gz: '050389221f4f71ab6e54a6038606f0d1f48f1a6571116c5b052493362158a850fe0f1a90a4d520b35fff875315fd1292610bab104366251626d5d9fdcb5b7f54'
data/CHANGELOG.md CHANGED
@@ -4,6 +4,45 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
 
7
+ ## [1.5.17] - 2026-09-30
8
+
9
+ ### Added
10
+ - Show a top-up action when a free-trial model rejects the request
11
+ - Add Requesty as a named provider preset (#587 - @Thibaultjaigu)
12
+ - Redesign the skills page around cards and a detail view
13
+ - Redesign the scheduled tasks page and keep a run history per task
14
+ - Redesign the channels page as a compact two-column list
15
+ - Rework the MCP page layout, its row menu and the remove confirmation
16
+ - Add "Add to chat" to the workspace file context menu
17
+ - Add a global switch for IM progress cards (#599)
18
+ - Show the vendor logo on the model chip of a new session
19
+ - Use Chinese names for the built-in skills
20
+
21
+ ### Improved
22
+ - Optimize the Feishu card styling (#597)
23
+ - Improve drag-and-drop for session references and files (#600)
24
+ - Restyle the channel status badge, cron rows, message settings and search boxes
25
+ - Polish the assistant memory page, onboarding page, billing header and trash toolbar
26
+ - Improve the discoverability of overflowing aside tabs (#601)
27
+ - Show skill, MCP and task errors as in-app toasts instead of browser alerts
28
+ - Localize the default extension icon letter
29
+ - Ship meeting as a default-off extension
30
+
31
+ ### Fixed
32
+ - Make an IM message interrupt the running task instead of queueing silently (#586)
33
+ - Hide the sidecar models from the model switchers (#598)
34
+ - Show the "View process" panel in Feishu only when there is process history (#596)
35
+ - Keep a subagent phase running when a message is queued (#594)
36
+ - Fix the skill install click and the extension installed state
37
+ - Strip channel-only prompt prefixes when replaying history (#585)
38
+ - Use the in-app dialog for delete confirmations instead of the browser prompt
39
+ - Toggle a sidebar ··· menu on a repeated click (#595)
40
+ - Hide the auto tag and routing tooltip once the alias is pinned to a model
41
+
42
+ ### More
43
+ - Retire Claude 4.5 / Opus 4.6 and Gemini 3.5 / 3.6 Flash from the catalogs
44
+ - Remove legacy fallback domains and dead failover paths
45
+
7
46
  ## [1.5.16] - 2026-09-24
8
47
 
9
48
  ### Added
data/README.md CHANGED
@@ -205,7 +205,7 @@ $ openclacky
205
205
 
206
206
  Set your **API Key**, **Model**, and **Base URL** (any OpenAI-compatible provider).
207
207
 
208
- Supported out of the box: **Claude (Anthropic) · GPT (OpenAI) · DeepSeek · Kimi (Moonshot) · MiniMax · OpenRouter · OrcaRouter** — or any custom endpoint.
208
+ Supported out of the box: **Claude (Anthropic) · GPT (OpenAI) · DeepSeek · Kimi (Moonshot) · MiniMax · OpenRouter · OrcaRouter · Requesty** — or any custom endpoint.
209
209
 
210
210
  ## Coding use case
211
211
 
data/README_CN.md CHANGED
@@ -189,7 +189,7 @@ $ openclacky
189
189
 
190
190
  设置你的 **API Key**、**模型**和 **Base URL**(任意 OpenAI 兼容提供商)。
191
191
 
192
- 开箱即支持:**Claude (Anthropic) · GPT (OpenAI) · DeepSeek · Kimi (Moonshot) · MiniMax · OpenRouter · OrcaRouter**,或任意自定义端点。
192
+ 开箱即支持:**Claude (Anthropic) · GPT (OpenAI) · DeepSeek · Kimi (Moonshot) · MiniMax · OpenRouter · OrcaRouter · Requesty**,或任意自定义端点。
193
193
 
194
194
  ## 代码开发场景
195
195
 
data/README_JA.md CHANGED
@@ -194,7 +194,7 @@ $ openclacky
194
194
 
195
195
  **API Key**、**Model**、**Base URL**(任意の OpenAI 互換プロバイダー)を設定します。
196
196
 
197
- 標準でサポート: **Claude (Anthropic) · GPT (OpenAI) · DeepSeek · Kimi (Moonshot) · MiniMax · OpenRouter · OrcaRouter** — または任意のカスタムエンドポイント。
197
+ 標準でサポート: **Claude (Anthropic) · GPT (OpenAI) · DeepSeek · Kimi (Moonshot) · MiniMax · OpenRouter · OrcaRouter · Requesty** — または任意のカスタムエンドポイント。
198
198
 
199
199
  ## コーディングのユースケース
200
200
 
@@ -208,7 +208,7 @@ module Clacky
208
208
 
209
209
  private def navigation_entry(round)
210
210
  msg = round[:user_msg]
211
- user = navigation_preview(msg[:display_text] || extract_text_from_content(msg[:content]))
211
+ user = navigation_preview(msg[:display_text] || strip_channel_prompt_prefix(extract_text_from_content(msg[:content])))
212
212
  if user.empty?
213
213
  names = Array(msg[:display_files]).map { |file| file[:name] || file["name"] }
214
214
  Array(msg[:content]).each do |block|
@@ -211,21 +211,7 @@ module Clacky
211
211
  Clacky::Shutdown.sleep(retry_delay)
212
212
  retry
213
213
  else
214
- # All retries exhausted for a timeout. Try the secondary gateway URL if available.
215
- if !@config.url_fallback_active?
216
- fallback_url = @config.fallback_base_url_for_current_provider
217
- if fallback_url
218
- @config.activate_url_fallback!(fallback_url)
219
- rebuild_client_for_current_model!
220
- retries = 0
221
- @ui&.show_warning(
222
- I18n.t("llm.warn.switching_to_fallback_url", url: fallback_url)
223
- )
224
- retry
225
- end
226
- end
227
-
228
- raise AgentError.new("[LLM] #{I18n.t("llm.error.request_timeout", retries: max_retries)}", raw_message: e.message)
214
+ raise AgentError.new("#{I18n.t("llm.error.request_timeout", retries: max_retries)}", raw_message: e.message)
229
215
  end
230
216
 
231
217
  rescue Faraday::ConnectionFailed, Faraday::SSLError, Errno::ECONNREFUSED, Errno::ETIMEDOUT, Errno::EPIPE => e
@@ -259,24 +245,9 @@ module Clacky
259
245
  Clacky::Shutdown.sleep(retry_delay)
260
246
  retry
261
247
  else
262
- # All retries on the primary endpoint exhausted for a network-level
263
- # failure (DNS/TCP/TLS). Try the secondary gateway URL if available.
264
- if !@config.url_fallback_active?
265
- fallback_url = @config.fallback_base_url_for_current_provider
266
- if fallback_url
267
- @config.activate_url_fallback!(fallback_url)
268
- rebuild_client_for_current_model!
269
- retries = 0
270
- @ui&.show_warning(
271
- I18n.t("llm.warn.switching_to_fallback_url", url: fallback_url)
272
- )
273
- retry
274
- end
275
- end
276
-
277
248
  # Don't show_error here — let the outer rescue block handle it to avoid duplicates.
278
249
  # Progress cleanup is the caller's responsibility (via its own ensure block).
279
- raise AgentError.new("[LLM] #{I18n.t("llm.error.network_failed", retries: max_retries)}", raw_message: e.message)
250
+ raise AgentError.new("#{I18n.t("llm.error.network_failed", retries: max_retries)}", raw_message: e.message)
280
251
  end
281
252
 
282
253
  rescue RetryableError => e
@@ -318,28 +289,9 @@ module Clacky
318
289
  Clacky::Shutdown.sleep(retry_delay)
319
290
  retry
320
291
  else
321
- # All retries on the current endpoint exhausted.
322
- # If a secondary gateway URL is configured and not yet tried, switch
323
- # to it now, rebuild the client, and reset the retry counter so the
324
- # same request gets a fresh MAX_RETRIES_ON_FALLBACK budget on the
325
- # new endpoint. This is a one-shot switch — url_fallback_active?
326
- # prevents a second switch if the fallback URL also fails.
327
- if !@config.url_fallback_active?
328
- fallback_url = @config.fallback_base_url_for_current_provider
329
- if fallback_url
330
- @config.activate_url_fallback!(fallback_url)
331
- rebuild_client_for_current_model!
332
- retries = 0
333
- @ui&.show_warning(
334
- I18n.t("llm.warn.switching_to_fallback_url", url: fallback_url)
335
- )
336
- retry
337
- end
338
- end
339
-
340
292
  # Don't show_error here — let the outer rescue block handle it to avoid duplicates.
341
293
  # Progress cleanup is the caller's responsibility (via its own ensure block).
342
- raise AgentError.new("[LLM] #{I18n.t("llm.error.service_unavailable", retries: current_max)}", raw_message: e.message)
294
+ raise AgentError.new("#{I18n.t("llm.error.service_unavailable", retries: current_max)}", raw_message: e.message)
343
295
  end
344
296
 
345
297
  rescue Clacky::BadRequestError => e
@@ -332,10 +332,20 @@ module Clacky
332
332
  { has_more: has_more }
333
333
  end
334
334
 
335
+ # ChannelManager prefixes channel prompts with agent-only context for
336
+ # the LLM; the clean original rides in display_text. Replay paths that
337
+ # fall back to raw content (pre-display_text sessions, chunk MD archives)
338
+ # must strip that block for UI parity with live rendering. Gated on
339
+ # channel_info so a normal user literally typing these lines keeps them.
340
+ private def strip_channel_prompt_prefix(text)
341
+ return text unless @channel_info
342
+ Clacky::Utils::ChannelPrompt.strip(text)
343
+ end
344
+
335
345
  private def replay_rounds(ui, page)
336
346
  page.each do |round|
337
347
  msg = round[:user_msg]
338
- raw_text = msg[:display_text] || extract_text_from_content(msg[:content])
348
+ raw_text = msg[:display_text] || strip_channel_prompt_prefix(extract_text_from_content(msg[:content]))
339
349
  # Images: recovered from inline image_url blocks in content (carry data_url for <img> rendering)
340
350
  image_files = extract_image_files_from_content(msg[:content])
341
351
  # Disk files (PDF, doc, etc.): stored in display_files on the user message at send time
@@ -507,7 +517,7 @@ module Clacky
507
517
  synthetic_ts = base_time - ((index ? index[:section_count] : sections.size) - round_offset - round_index) * 1.0
508
518
  user_msg = {
509
519
  role: "user",
510
- content: text,
520
+ content: strip_channel_prompt_prefix(text),
511
521
  created_at: synthetic_ts,
512
522
  ext_events: sec_ext_events,
513
523
  _from_chunk: true
data/lib/clacky/agent.rb CHANGED
@@ -238,7 +238,7 @@ module Clacky
238
238
  entry = @config.current_model
239
239
  @client = Clacky::Client.new(
240
240
  @config.api_key,
241
- base_url: @config.effective_base_url,
241
+ base_url: @config.base_url,
242
242
  model: @config.model_name,
243
243
  anthropic_format: @config.anthropic_format?,
244
244
  api_format: @config.api_format,
@@ -523,13 +523,7 @@ module Clacky
523
523
  @current_model_id = id
524
524
  @current_model_index = index
525
525
 
526
- if previous_id != id
527
- @session_model_overlay = nil
528
- # Reset URL fallback when switching models — the new model has its own
529
- # base_url and the old fallback endpoint is irrelevant (or even wrong).
530
- @url_fallback_active = false
531
- @url_fallback_base_url = nil
532
- end
526
+ @session_model_overlay = nil if previous_id != id
533
527
 
534
528
  true
535
529
  end
@@ -545,16 +539,9 @@ module Clacky
545
539
  index = @models.find_index { |m| m["model"].to_s.downcase == name_str }
546
540
  return false if index.nil?
547
541
 
548
- previous_id = @current_model_id
549
542
  @current_model_id = @models[index]["id"]
550
543
  @current_model_index = index
551
544
 
552
- if previous_id != @current_model_id
553
- # Reset URL fallback when switching models — same rationale as switch_model_by_id.
554
- @url_fallback_active = false
555
- @url_fallback_base_url = nil
556
- end
557
-
558
545
  true
559
546
  end
560
547
 
@@ -601,9 +588,27 @@ module Clacky
601
588
  true
602
589
  end
603
590
 
604
- # List all model names
591
+ # Model entries the user can actually chat with.
592
+ #
593
+ # `type` doubles as a record discriminator: `default`/`lite` tag a chat
594
+ # model's role, while the media/ocr kinds mark sidecar capability configs
595
+ # that only the dedicated media UI should surface. Every model switcher
596
+ # (CLI, IM, Web UI) must go through here so the rule lives in one place.
597
+ def chat_models
598
+ @models.select { |m| self.class.chat_model?(m) }
599
+ end
600
+
601
+ # @param entry [Hash, nil]
602
+ def self.chat_model?(entry)
603
+ return false unless entry
604
+ return false if entry["auto_injected"]
605
+
606
+ !Clacky::Providers::SIDECAR_KINDS.include?(entry["type"].to_s)
607
+ end
608
+
609
+ # List all chat model names
605
610
  def model_names
606
- @models.map { |m| m["model"] }
611
+ chat_models.map { |m| m["model"] }
607
612
  end
608
613
 
609
614
  # Get API key for current model
@@ -1138,63 +1143,6 @@ module Clacky
1138
1143
  end
1139
1144
  end
1140
1145
 
1141
- # ── URL-level fallback ─────────────────────────────────────────────────
1142
- # Independent from the model-name fallback above. When all max_retries
1143
- # on the primary endpoint are exhausted, the caller may switch to a
1144
- # secondary gateway URL (same model, different host) via these methods.
1145
- # The URL fallback is intentionally one-shot per session — we do not
1146
- # probe or reset it automatically, keeping the logic simple.
1147
-
1148
- # Look up the fallback base URL for the current model's provider.
1149
- # Returns nil if:
1150
- # - provider cannot be determined
1151
- # - provider is not openclacky (URL failover is only supported for the
1152
- # OpenClacky gateway; other providers have no secondary endpoint)
1153
- # - current base_url is already the fallback URL (user picked the
1154
- # Secondary/China node directly — switching to the same URL does nothing)
1155
- # @return [String, nil]
1156
- def fallback_base_url_for_current_provider
1157
- m = current_model
1158
- return nil unless m
1159
-
1160
- # Use strict URL matching to identify the provider — no inference.
1161
- # resolve_provider would incorrectly return "openclacky" for localhost
1162
- # addresses and clacky-* api keys, triggering unwanted failover.
1163
- # find_by_base_url only matches registered preset URLs, so localhost
1164
- # and other non-openclacky endpoints correctly return nil.
1165
- provider_id = Clacky::Providers.find_by_base_url(m["base_url"])
1166
- return nil unless provider_id == Clacky::Providers::OPENCLACKY_ID
1167
-
1168
- fallback_url = Clacky::Providers.fallback_base_url(provider_id)
1169
- return nil unless fallback_url
1170
-
1171
- # Don't offer a fallback when the current base_url is already the
1172
- # fallback endpoint — switching to the same URL accomplishes nothing.
1173
- current_base = m["base_url"].to_s.chomp("/")
1174
- return nil if current_base == fallback_url.to_s.chomp("/")
1175
-
1176
- fallback_url
1177
- end
1178
-
1179
- # Activate the URL fallback: override base_url with the secondary gateway.
1180
- # Idempotent — safe to call multiple times.
1181
- # @param fallback_url [String] the secondary gateway URL
1182
- def activate_url_fallback!(fallback_url)
1183
- @url_fallback_active = true
1184
- @url_fallback_base_url = fallback_url
1185
- end
1186
-
1187
- # Returns true when the secondary gateway URL is in use.
1188
- def url_fallback_active?
1189
- @url_fallback_active == true
1190
- end
1191
-
1192
- # The effective base URL for API calls.
1193
- # Returns the URL-fallback override when active, otherwise the model's configured base_url.
1194
- def effective_base_url
1195
- @url_fallback_active ? @url_fallback_base_url : base_url
1196
- end
1197
-
1198
1146
  # Get current model configuration.
1199
1147
  #
1200
1148
  # Resolution order:
data/lib/clacky/client.rb CHANGED
@@ -817,7 +817,7 @@ module Clacky
817
817
 
818
818
  if error_code == "insufficient_credit" || response.status == 402
819
819
  raise InsufficientCreditError.new(
820
- "[LLM] #{I18n.t("llm.error.insufficient_credit")}",
820
+ "#{I18n.t("llm.error.insufficient_credit")}",
821
821
  error_code: "insufficient_credit",
822
822
  provider_id: @provider_id,
823
823
  raw_message: error_message
@@ -827,30 +827,37 @@ module Clacky
827
827
  case response.status
828
828
  when 400
829
829
  if error_message.match?(/ThrottlingException|unavailable|quota/i)
830
- raise RetryableError.new("[LLM] #{I18n.t("llm.error.rate_limit_400")}", routed_tier: routed_tier)
830
+ raise RetryableError.new("#{I18n.t("llm.error.rate_limit_400")}", routed_tier: routed_tier)
831
831
  end
832
832
 
833
833
  raise BadRequestError.new(
834
834
  "[LLM] Client request error: #{error_message}",
835
- display_message: "[LLM] #{I18n.t("llm.error.bad_request")}",
835
+ display_message: "#{I18n.t("llm.error.bad_request")}",
836
836
  raw_message: error_message
837
837
  )
838
838
  when 401
839
- raise AgentError.new("[LLM] #{I18n.t("llm.error.invalid_api_key")}", raw_message: error_message)
839
+ raise AgentError.new("#{I18n.t("llm.error.invalid_api_key")}", raw_message: error_message)
840
840
  when 403
841
841
  i18n_key = "llm.error.403.#{error_code}"
842
842
  translated = I18n.t(i18n_key)
843
843
  translated = I18n.t("llm.error.403.default") if translated == i18n_key
844
- raise AgentError.new("[LLM] #{translated}", raw_message: error_message)
844
+ raise AgentError.new(translated, raw_message: error_message) unless error_code == "model_not_allowed"
845
+
846
+ raise ModelNotAllowedError.new(
847
+ translated,
848
+ error_code: error_code,
849
+ provider_id: @provider_id,
850
+ raw_message: error_message
851
+ )
845
852
  when 404
846
- raise AgentError.new("[LLM] #{I18n.t("llm.error.endpoint_not_found")}", raw_message: error_message)
853
+ raise AgentError.new("#{I18n.t("llm.error.endpoint_not_found")}", raw_message: error_message)
847
854
  when 429
848
855
  if error_code == "quota_exceeded"
849
- raise AgentError.new("[LLM] #{I18n.t("llm.error.quota_exhausted")}", raw_message: error_message)
856
+ raise AgentError.new("#{I18n.t("llm.error.quota_exhausted")}", raw_message: error_message)
850
857
  end
851
- raise RetryableError.new("[LLM] #{I18n.t("llm.error.rate_limit_429")}", routed_tier: routed_tier)
852
- when 500..599 then raise RetryableError.new("[LLM] #{I18n.t("llm.error.server_error", status: response.status)}", routed_tier: routed_tier)
853
- else raise AgentError.new("[LLM] #{I18n.t("llm.error.unexpected", status: response.status)}", raw_message: error_message)
858
+ raise RetryableError.new("#{I18n.t("llm.error.rate_limit_429")}", routed_tier: routed_tier)
859
+ when 500..599 then raise RetryableError.new("#{I18n.t("llm.error.server_error", status: response.status)}", routed_tier: routed_tier)
860
+ else raise AgentError.new("#{I18n.t("llm.error.unexpected", status: response.status)}", raw_message: error_message)
854
861
  end
855
862
  end
856
863
 
@@ -858,7 +865,7 @@ module Clacky
858
865
  def check_html_response(response)
859
866
  body = response.body.to_s.lstrip
860
867
  if body.start_with?("<!DOCTYPE", "<!doctype", "<html", "<HTML")
861
- raise RetryableError, "[LLM] #{I18n.t("llm.error.html_response")}"
868
+ raise RetryableError, "#{I18n.t("llm.error.html_response")}"
862
869
  end
863
870
  end
864
871
 
@@ -366,7 +366,7 @@
366
366
 
367
367
  function settingsTab() {
368
368
  const btn = document.createElement("button");
369
- btn.className = "settings-tab";
369
+ btn.className = "settings-tab page-tab";
370
370
  btn.dataset.tab = "advisor";
371
371
  btn.textContent = t("advisor.settingsTab", "工作建议");
372
372
  return btn;
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  name: computer-use
3
+ name_zh: 电脑操控
4
+ description_zh: 操控桌面上的原生应用、弹窗、菜单栏和文件选择器,处理终端和浏览器够不着的操作,支持 macOS 和 WSL 下的 Windows。
3
5
  description: Drive the desktop — macOS, or the Windows desktop through WSL — when a task needs a native app, dialog, menu bar, file picker or anything the terminal and browser tools cannot reach. Screenshot first, then click and type by coordinates read from that image.
4
6
  ---
5
7
 
@@ -1643,7 +1643,11 @@
1643
1643
  }
1644
1644
 
1645
1645
  function _skeletonHtml() {
1646
- return Array.from({ length: 3 }).map(() => `
1646
+ const bar = `
1647
+ <div class="studio-seg-bar" style="pointer-events:none">
1648
+ <span class="skel" style="display:inline-block;height:2rem;width:12rem;border-radius:8px"></span>
1649
+ </div>`;
1650
+ return bar + Array.from({ length: 3 }).map(() => `
1647
1651
  <div class="studio-skill-card" style="pointer-events:none">
1648
1652
  <div class="studio-skill-head">
1649
1653
  <span class="skel" style="display:inline-block;height:1rem;width:40%"></span>
@@ -2078,16 +2082,10 @@
2078
2082
  headerActions.appendChild(newBtn);
2079
2083
  }
2080
2084
 
2081
- if (locked) {
2082
- const lockHint = el("p", { class: "studio-empty", text: t("skills.locked") });
2083
- lockHint.style.marginBottom = "12px";
2084
- container.appendChild(lockHint);
2085
- }
2086
-
2087
2085
  const sections = [{
2088
2086
  id: "local",
2089
2087
  label: t("skills.section.local"),
2090
- hint: t("skills.section.localHint"),
2088
+ hint: locked ? t("skills.locked") : t("skills.section.localHint"),
2091
2089
  items: local,
2092
2090
  cardFn: localCard,
2093
2091
  emptyKey: "skills.local.empty",
@@ -2122,12 +2120,12 @@
2122
2120
  root.appendChild(page);
2123
2121
 
2124
2122
  const header = el("div", { class: "studio-page-head" });
2125
- header.appendChild(el("h2", { class: "studio-page-title", text: t("ws.title") }));
2123
+ header.appendChild(el("h2", { class: "studio-page-title page-title", text: t("ws.title") }));
2126
2124
  const headerActions = el("div", { class: "studio-page-head-actions" });
2127
2125
  header.appendChild(headerActions);
2128
2126
  page.appendChild(header);
2129
2127
 
2130
- const tabsBar = el("div", { class: "studio-tabs" });
2128
+ const tabsBar = el("div", { class: "studio-tabs page-tabs" });
2131
2129
  const body = el("div", { class: "studio-tab-body" });
2132
2130
  page.appendChild(tabsBar);
2133
2131
  page.appendChild(body);
@@ -2140,7 +2138,7 @@
2140
2138
 
2141
2139
  function select(id) {
2142
2140
  active = id;
2143
- Array.from(tabsBar.children).forEach((b) => b.classList.toggle("studio-tab-active", b.dataset.tab === id));
2141
+ Array.from(tabsBar.children).forEach((b) => b.classList.toggle("active", b.dataset.tab === id));
2144
2142
  body.innerHTML = "";
2145
2143
  headerActions.innerHTML = "";
2146
2144
  const tab = tabs.find((t2) => t2.id === id);
@@ -2148,7 +2146,7 @@
2148
2146
  }
2149
2147
 
2150
2148
  tabs.forEach((tab) => {
2151
- const btn = el("button", { class: "studio-tab", text: tab.label });
2149
+ const btn = el("button", { class: "studio-tab page-tab", text: tab.label });
2152
2150
  btn.dataset.tab = tab.id;
2153
2151
  btn.addEventListener("click", () => select(tab.id));
2154
2152
  tabsBar.appendChild(btn);
@@ -2282,16 +2280,12 @@
2282
2280
  .studio-published-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px 0; font-size: 12px; border-bottom: 1px solid var(--color-border-primary); }
2283
2281
  .studio-published-name { display: flex; align-items: center; gap: 6px; color: var(--color-text-primary); }
2284
2282
  .studio-page { width: 100%; }
2285
- .studio-page-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
2283
+ .studio-page-head { display: flex; align-items: center; justify-content: space-between; min-height: 36px; margin-bottom: 20px; }
2286
2284
  .studio-page-head-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
2287
- .studio-page-title { margin: 0; font-size: 22px; font-weight: 600; color: var(--color-text-primary); }
2288
2285
  .studio-btn-sm { padding: 5px 12px; font-size: 12px; }
2289
- .studio-seg-bar { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
2286
+ .studio-seg-bar { display: flex; align-items: center; gap: 12px; min-height: 36px; margin-bottom: 14px; }
2290
2287
  .studio-seg-hint { font-size: 11px; color: var(--color-text-muted); }
2291
- .studio-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--color-border-primary); margin-bottom: 24px; }
2292
- .studio-tab { padding: 8px 16px; border: none; background: transparent; color: var(--color-text-tertiary); cursor: pointer; font-size: 14px; font-weight: 500; border-bottom: 2px solid transparent; margin-bottom: -1px; }
2293
- .studio-tab:hover { color: var(--color-text-primary); }
2294
- .studio-tab-active { color: var(--color-text-primary); border-bottom-color: var(--color-accent-primary); }
2288
+ .studio-tabs { margin-bottom: 24px; }
2295
2289
  .studio-ext-block { margin-bottom: 20px; }
2296
2290
  .studio-block-title { font-size: 13px; font-weight: 600; color: var(--color-text-secondary); padding: 8px 0 0; }
2297
2291
  .studio-skill-card { border: 1px solid var(--color-border-primary); border-radius: var(--radius-md, 8px); padding: 12px; margin: 0 0 10px; background: var(--color-bg-secondary); }
@@ -3,6 +3,8 @@
3
3
  # After context compression, previously inspected host files may be
4
4
  # mistaken for extension edit targets.
5
5
  name: ext-develop
6
+ name_zh: 扩展开发
7
+ description_zh: 开发、调试和发布扩展:从想法搭出新扩展,修复加载失败或不显示的问题,或发布到扩展市场。
6
8
  description: Build, debug, or publish an OpenClacky extension — scaffold a new one from an idea, fix a broken/invisible panel/api/skill/agent, or ship it to the marketplace. Trigger on create/start extension, plugin, panel, ext verify error, "won't load", "not showing up", publish/ship/unpublish an extension.
7
9
  agent: ext-developer
8
10
  ---
@@ -8,6 +8,7 @@ version: "0.1.0"
8
8
  author: OpenClacky
9
9
  homepage: https://www.openclacky.com
10
10
  license: MIT
11
+ enabled_by_default: false
11
12
  contributes:
12
13
  api: api/handler.rb
13
14
  panels:
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  name: meeting-summarizer
3
+ name_zh: 会议纪要
4
+ description_zh: 根据会议转写内容生成结构化纪要,包含关键决策、待办事项和讨论要点,会议结束后自动触发。
3
5
  description: Summarize a completed meeting from its transcript. Produces a structured summary with key decisions, action items, and discussion highlights. Triggered automatically when a meeting ends.
4
6
  user-invocable: false
5
7
  auto_summarize: false
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  name: browser-setup
3
+ name_zh: 浏览器配置
4
+ description_zh: 配置 Chrome 或 Edge 浏览器工具并检测连接,支持 macOS、Linux 和 WSL。
3
5
  description: |
4
6
  Configure the browser tool for Clacky. Guides the user through Chrome or Edge setup,
5
7
  verifies the connection, and writes ~/.clacky/browser.yml.
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  name: channel-manager
3
+ name_zh: 渠道管理
4
+ description_zh: 接入飞书、企业微信、微信、钉钉、Discord、Telegram 等 IM 渠道,支持配置、启停、诊断和发消息。
3
5
  description: |
4
6
  Configure IM platform channels (Feishu, WeCom, Weixin, Discord, Telegram, DingTalk) for openclacky.
5
7
  Uses browser automation for navigation; guides the user to paste credentials and perform UI steps.
@@ -23,8 +23,7 @@ require_relative '../skill-add/scripts/install_from_zip'
23
23
 
24
24
  class FeishuSkillsInstaller
25
25
  PRIMARY_HOST = ENV.fetch('CLACKY_LICENSE_SERVER', 'https://www.openclacky.com')
26
- FALLBACK_HOST = 'https://openclacky.up.railway.app'
27
- API_HOSTS = ENV['CLACKY_LICENSE_SERVER'] ? [PRIMARY_HOST] : [PRIMARY_HOST, FALLBACK_HOST]
26
+ API_HOSTS = [PRIMARY_HOST]
28
27
  API_PATH = '/api/v1/skills/feishu'
29
28
  API_OPEN_TIMEOUT = 5
30
29
  API_READ_TIMEOUT = 10
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  name: code-explorer
3
+ name_zh: 代码探索
4
+ description_zh: 分析项目结构,梳理代码逻辑,回答「这个功能是怎么实现的」。
3
5
  description: Use this skill when exploring, analyzing, or understanding project/code structure. Required for tasks like "analyze project", "explore codebase", "understand how X works".
4
6
  agent: coding
5
7
  fork_agent: true
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  name: cron-task-creator
3
+ name_zh: 定时任务
4
+ description_zh: 创建和管理定时自动任务,支持按天、周、小时执行,可随时编辑、启停或删除。
3
5
  description: 'Create, manage, and run scheduled automated tasks (cron jobs) in Clacky. Use this skill whenever the user wants to create a new automated task or cron job, set up recurring automation, schedule something to run daily/weekly/hourly, view all scheduled tasks, edit an existing task prompt or cron schedule, enable or disable a task, delete a task, check task run history or logs, or run a task immediately via the WebUI. Trigger on phrases like cron, scheduled task, run every day, automate this; 定时任务, 每天自动, 定时执行.'
4
6
  disable-model-invocation: false
5
7
  user-invocable: true
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: deploy
3
- description: Deploy Rails applications to Railway. Handles first-time setup and re-deploys idempotently using Railway CLI. Trigger on: "deploy", "deploy to railway", "railway deploy", "发布", "部署", "上线".
3
+ name_zh: 一键部署
4
+ description_zh: 把 Rails 应用部署到 Railway,首次配置和后续更新都能自动完成。
5
+ description: 'Deploy Rails applications to Railway. Handles first-time setup and re-deploys idempotently using Railway CLI. Trigger on: "deploy", "deploy to railway", "railway deploy", "发布", "部署", "上线".'
4
6
  agent: coding
5
7
  disable-model-invocation: false
6
8
  ---
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  name: mcp-manager
3
+ name_zh: MCP 管理
4
+ description_zh: 添加、查看、检测、删除和重新配置 MCP 服务,无需手动编辑 JSON。
3
5
  description: |
4
6
  Manage MCP (Model Context Protocol) servers for openclacky: add, list, probe, remove,
5
7
  reconfigure. Edits ~/.clacky/mcp.json so the user never writes JSON by hand.
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  name: media-gen
3
+ name_zh: 媒体生成
4
+ description_zh: 生成或编辑图片、视频,以及文字转语音,适用于封面、海报、插画、配音等。
3
5
  description: 'Generate or edit images, videos, or audio (text-to-speech) in the current task. Use whenever the user asks to create/generate/produce or edit/modify a picture / image / illustration / cover / poster / icon / artwork, a video / clip / animation, or speech / voiceover / narration / TTS — e.g. generate image, draw, design a cover, edit this image, change the background, text-to-video, generate speech; 画一张, 配图, 编辑图片, 改图, 换背景, 做个视频, 配音, 文字转语音. Also use when a document (slides, poster, README hero) needs an inline image.'
4
6
  disable-model-invocation: false
5
7
  user-invocable: true
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  name: new
3
+ name_zh: 新建项目
4
+ description_zh: 快速创建一个新项目,马上开始开发。
3
5
  description: Create a new project to start development quickly
4
6
  agent: coding
5
7
  disable-model-invocation: false
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  name: onboard
3
+ name_zh: 初始设置
4
+ description_zh: 首次使用引导:设置助手名字、性格和你的个人档案,也可单独调整其中一项。
3
5
  description: Onboard a new user OR curate a single piece of the assistant's inner state. Without arguments, runs the full first-run ceremony (AI name, personality, user profile, SOUL.md + USER.md, optional browser + personal website). With `scope:soul` or `scope:user`, runs a quick chat to update just that one profile file. With `path:<abs>`, runs a quick chat to update / keep / delete one memory file under ~/.clacky/memories/.
4
6
  disable-model-invocation: true
5
7
  user-invocable: true