openclacky 1.5.7 → 1.5.9
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 +71 -0
- data/README.md +3 -3
- data/README_CN.md +2 -2
- data/README_JA.md +2 -2
- data/lib/clacky/agent.rb +2 -3
- data/lib/clacky/agent_config.rb +40 -18
- data/lib/clacky/brand_config.rb +12 -7
- data/lib/clacky/cli.rb +0 -1
- data/lib/clacky/default_extensions/ext-studio/ext.yml +1 -1
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +27 -14
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +22 -5
- data/lib/clacky/default_extensions/general/ext.yml +1 -1
- data/lib/clacky/extension/api_loader.rb +4 -11
- data/lib/clacky/extension/packager.rb +1 -1
- data/lib/clacky/extension/scaffold/templates/hello/test/handler_test.rb.erb +22 -0
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/media/generator.rb +3 -3
- data/lib/clacky/message_format/anthropic.rb +19 -2
- data/lib/clacky/message_format/bedrock.rb +8 -3
- data/lib/clacky/message_history.rb +1 -1
- data/lib/clacky/providers.rb +159 -105
- data/lib/clacky/server/channel/adapters/feishu/ws_client.rb +6 -0
- data/lib/clacky/server/http_server.rb +67 -79
- data/lib/clacky/session_manager.rb +7 -0
- data/lib/clacky/skill_loader.rb +4 -0
- data/lib/clacky/tools/base.rb +11 -1
- data/lib/clacky/tools/edit.rb +28 -0
- data/lib/clacky/tools/terminal.rb +5 -0
- data/lib/clacky/tools/trash_manager.rb +1 -1
- data/lib/clacky/ui2/components/inline_input.rb +1 -0
- data/lib/clacky/ui2/components/input_area.rb +1 -0
- data/lib/clacky/ui2/components/modal_component.rb +0 -1
- data/lib/clacky/ui2/components/todo_area.rb +1 -0
- data/lib/clacky/ui2/line_editor.rb +1 -0
- data/lib/clacky/ui2/markdown_renderer.rb +5 -1
- data/lib/clacky/ui2/ui_controller.rb +1 -0
- data/lib/clacky/utils/model_pricing.rb +62 -27
- data/lib/clacky/utils/string_matcher.rb +29 -2
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +278 -221
- data/lib/clacky/web/components/chat-navigator.js +1 -1
- data/lib/clacky/web/features/billing/store.js +3 -3
- data/lib/clacky/web/features/billing/view.js +5 -61
- data/lib/clacky/web/features/extensions/store.js +65 -41
- data/lib/clacky/web/features/extensions/view.js +74 -10
- data/lib/clacky/web/features/new-session/view.js +24 -0
- data/lib/clacky/web/features/version/view.js +2 -2
- data/lib/clacky/web/i18n.js +36 -22
- data/lib/clacky/web/index.html +17 -17
- data/lib/clacky/web/projects.js +83 -285
- data/lib/clacky/web/sessions.js +151 -17
- data/lib/clacky/web/settings.js +6 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 565adf42059330fb821c57cb3b1200035fb866a1f2190ca87e093bc34de9edc0
|
|
4
|
+
data.tar.gz: 89db4dd987f39d4b77acd52121aa31bef54858f81d614557ac7b9984a1066ba0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e3bbb8988d833e3c6e6ce4cd13da64111e2ee15b400c2eb34b78bca9f51ae80525336baf923ba18cdbf85b5320d542f8050567792e7d5a437b28e2d09d29729d
|
|
7
|
+
data.tar.gz: ae508f3bdb44a35b8edd0c89ebcc53a64d3f7340214ded6c8dd47d0c757c66f5f99700f466c86278e8ee0a38ab838b8509c80ba8af2f253653f967b91f52a814
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,77 @@ 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.9] - 2026-08-14
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- OrcaRouter as a named provider preset (#480 - @Marc-oss-hub)
|
|
11
|
+
- DeepSeek peak/off-peak billing support
|
|
12
|
+
- Unicode homoglyph-tolerant string matching in edit mode
|
|
13
|
+
- Load-more pagination in extension marketplace
|
|
14
|
+
- Remove-from-project action in session menu
|
|
15
|
+
- Move-to-project submenu in session actions menu
|
|
16
|
+
- Changelog link in version badge and upgrade popover
|
|
17
|
+
- ext-studio scaffolding generates a runnable test/handler_test.rb and promotes "write tests" to its own step
|
|
18
|
+
- Lucide SVG icons for cron and extension group icons
|
|
19
|
+
|
|
20
|
+
### Improved
|
|
21
|
+
- Skill "use" button themed with accent color
|
|
22
|
+
- Reordered provider presets list
|
|
23
|
+
- Replaced native confirm() with a Modal component and refreshed aside icons
|
|
24
|
+
- Billing sidebar icon switched to circle-dollar-sign
|
|
25
|
+
- Settings data tab renamed to backup management
|
|
26
|
+
- Refined project session group indentation and hover styling
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- Scroll to top when entering a session group sub-view
|
|
30
|
+
- Zhipu AI website_url now points to the console overview page
|
|
31
|
+
- WSL path translation and CRLF line-ending preservation (#454 - @shipinliang)
|
|
32
|
+
- Project sessions re-render on language switch
|
|
33
|
+
- Corrected arrow direction for move-in/move-out project icons
|
|
34
|
+
- Attachment drafts isolated per session (#478)
|
|
35
|
+
- Pinned sessions prioritized in project session lists
|
|
36
|
+
- Projects.all() returns a sorted list
|
|
37
|
+
- Builtin extension skills classified as system and hidden third-party ext skills from the panel
|
|
38
|
+
- Feishu WebSocket TLS SNI (#479 - @ayers-ltd)
|
|
39
|
+
- stop_reason mapping expanded from 3 to 8 cases (#469 - @shipinliang)
|
|
40
|
+
|
|
41
|
+
### More
|
|
42
|
+
- Shortened ext-developer title
|
|
43
|
+
- cmdbar hover and session group indent tweaks
|
|
44
|
+
|
|
45
|
+
## [1.5.8] - 2026-08-12
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
- Inline SVG icons (edit, clear sessions, delete) in project action menu (#474)
|
|
49
|
+
- New project button in project selector popover
|
|
50
|
+
- Verified badge for officially reviewed extensions in marketplace
|
|
51
|
+
|
|
52
|
+
### Improved
|
|
53
|
+
- Skip full session/file scan when count is under cap (#470 - @shipinliang)
|
|
54
|
+
- Token estimation uses count() instead of scan().length (#471 - @shipinliang)
|
|
55
|
+
- Faster server startup
|
|
56
|
+
- Brand user extension UI (non-admin publish button grayed out, unverified banner hidden)
|
|
57
|
+
- Sidebar my-data section icons refreshed
|
|
58
|
+
- Section chevron icon size increased from 11px to 13px
|
|
59
|
+
- Project sidebar hover effects, folder default hints, project icon sizing
|
|
60
|
+
- Billing UI cleanup and global theme color adjustment
|
|
61
|
+
- Section header font and project group spacing tweaks
|
|
62
|
+
- Project menu labels updated
|
|
63
|
+
- Billing disclaimer about provider billing
|
|
64
|
+
|
|
65
|
+
### Fixed
|
|
66
|
+
- Extension pack max zip size increased from 50 MB to 80 MB
|
|
67
|
+
- New session button in project area now correctly preselects the project
|
|
68
|
+
- Chat navigation popup no longer overlaps sidebar collapse button
|
|
69
|
+
- Sub-model config preserved when switching modes (auto/off/custom)
|
|
70
|
+
- minimax-m2.7 vision detection fixed
|
|
71
|
+
- Project icon no longer reverts to default after page refresh
|
|
72
|
+
- Load constant warning fixed
|
|
73
|
+
|
|
74
|
+
### More
|
|
75
|
+
- README cost table formatting normalized
|
|
76
|
+
- Ambiguous cost phrasing clarified across READMEs (#475 - @chengoak)
|
|
77
|
+
|
|
7
78
|
## [1.5.7] - 2026-08-10
|
|
8
79
|
|
|
9
80
|
### Added
|
data/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
**The most Token-efficient open-source AI Agent.**
|
|
16
16
|
|
|
17
|
-
OpenClacky matches Claude Code on capability at comparable cost, and saves significantly against other open-source agents (~50% vs OpenClaw, ~3
|
|
17
|
+
OpenClacky matches Claude Code on capability at comparable cost, and saves significantly against other open-source agents (~50% vs OpenClaw, costs only ~1/3 of Hermes). 100% open source (MIT), BYOK with any OpenAI-compatible model, built on two years of Agentic R&D and harness engineering.
|
|
18
18
|
|
|
19
19
|
> Website: https://www.openclacky.com/ · Backed by **MiraclePlus · ZhenFund · Sequoia China · Hillhouse Capital**
|
|
20
20
|
|
|
@@ -24,7 +24,7 @@ Same task, how much do you pay? Under comparable agent workloads, OpenClacky sav
|
|
|
24
24
|
|
|
25
25
|
| Agent | Relative cost | Notes |
|
|
26
26
|
|---|---|---|
|
|
27
|
-
| **OpenClacky** | **~0.8
|
|
27
|
+
| **OpenClacky** | **~0.8×** | 16 tools · ~100% cache hit · subagent routing |
|
|
28
28
|
| Claude Code | 1.0× (baseline) | World-class harness, closed-source subscription |
|
|
29
29
|
| OpenClaw | ~1.5× | Comparable harness agent |
|
|
30
30
|
| Hermes | ~3× | 52 built-in tools — schema bloat ~3–4× |
|
|
@@ -194,7 +194,7 @@ $ openclacky
|
|
|
194
194
|
|
|
195
195
|
Set your **API Key**, **Model**, and **Base URL** (any OpenAI-compatible provider).
|
|
196
196
|
|
|
197
|
-
Supported out of the box: **Claude (Anthropic) · GPT (OpenAI) · DeepSeek · Kimi (Moonshot) · MiniMax · OpenRouter** — or any custom endpoint.
|
|
197
|
+
Supported out of the box: **Claude (Anthropic) · GPT (OpenAI) · DeepSeek · Kimi (Moonshot) · MiniMax · OpenRouter · OrcaRouter** — or any custom endpoint.
|
|
198
198
|
|
|
199
199
|
## Coding use case
|
|
200
200
|
|
data/README_CN.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
**最省 Token 的开源 AI Agent。**
|
|
12
12
|
|
|
13
|
-
OpenClacky 在任务能力上对齐 Claude Code,成本相当,同时相比其他开源 Agent 有显著优势(约节省 50% vs OpenClaw
|
|
13
|
+
OpenClacky 在任务能力上对齐 Claude Code,成本相当,同时相比其他开源 Agent 有显著优势(约节省 50% vs OpenClaw,成本约为 Hermes 的 1/3)。100% 开源(MIT),支持 BYOK 接入任意 OpenAI 兼容模型,背后是两年 Agentic 研发与 Harness 工程积累。
|
|
14
14
|
|
|
15
15
|
> 官网:https://www.openclacky.com/ · 投资方:**奇绩创坛 · 真格基金 · 红杉中国 · 高瓴资本**
|
|
16
16
|
|
|
@@ -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**,或任意自定义端点。
|
|
192
|
+
开箱即支持:**Claude (Anthropic) · GPT (OpenAI) · DeepSeek · Kimi (Moonshot) · MiniMax · OpenRouter · OrcaRouter**,或任意自定义端点。
|
|
193
193
|
|
|
194
194
|
## 代码开发场景
|
|
195
195
|
|
data/README_JA.md
CHANGED
|
@@ -24,7 +24,7 @@ OpenClacky は Claude Code と同等の性能を同等のコストで実現し
|
|
|
24
24
|
|
|
25
25
|
| エージェント | 相対コスト | 備考 |
|
|
26
26
|
|---|---|---|
|
|
27
|
-
| **OpenClacky** | **約 0.8
|
|
27
|
+
| **OpenClacky** | **約 0.8×** | 16 ツール · キャッシュヒット率約 100% · サブエージェントルーティング |
|
|
28
28
|
| Claude Code | 1.0×(基準) | 世界クラスのハーネス、クローズドソースのサブスクリプション |
|
|
29
29
|
| OpenClaw | 約 1.5× | 同等のハーネスエージェント |
|
|
30
30
|
| Hermes | 約 3× | 52 個の組み込みツール — スキーマの肥大化が約 3〜4 倍 |
|
|
@@ -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** — または任意のカスタムエンドポイント。
|
|
197
|
+
標準でサポート: **Claude (Anthropic) · GPT (OpenAI) · DeepSeek · Kimi (Moonshot) · MiniMax · OpenRouter · OrcaRouter** — または任意のカスタムエンドポイント。
|
|
198
198
|
|
|
199
199
|
## コーディングのユースケース
|
|
200
200
|
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
require "json"
|
|
5
5
|
require "cgi"
|
|
6
|
-
require "tty-prompt"
|
|
7
6
|
require "set"
|
|
8
7
|
require_relative "null_ui_controller"
|
|
9
8
|
require_relative "utils/arguments_parser"
|
|
@@ -1392,7 +1391,7 @@ module Clacky
|
|
|
1392
1391
|
# plain text, inflating token counts by 20-40x. The tool result carries a
|
|
1393
1392
|
# plain-text description for the LLM; the actual image is delivered here.
|
|
1394
1393
|
vision_supported = @config.current_model_supports?(:vision)
|
|
1395
|
-
ocr_entry = vision_supported ? nil : @config.
|
|
1394
|
+
ocr_entry = vision_supported ? nil : @config.effective_ocr_entry
|
|
1396
1395
|
|
|
1397
1396
|
tool_results.each do |tr|
|
|
1398
1397
|
inject = tr[:image_inject]
|
|
@@ -1959,7 +1958,7 @@ module Clacky
|
|
|
1959
1958
|
# OCR sidecar — only consulted when the primary doesn't see images.
|
|
1960
1959
|
# When the sidecar entry has "primary"=>true, the primary itself can see,
|
|
1961
1960
|
# so vision_supported was already true and we never enter the OCR branch.
|
|
1962
|
-
ocr_entry = vision_supported ? nil : @config.
|
|
1961
|
+
ocr_entry = vision_supported ? nil : @config.effective_ocr_entry
|
|
1963
1962
|
|
|
1964
1963
|
vision_images = [] # Array of { url:, name:, size_bytes:, path: }
|
|
1965
1964
|
downgraded = []
|
data/lib/clacky/agent_config.rb
CHANGED
|
@@ -633,16 +633,10 @@ module Clacky
|
|
|
633
633
|
}.compact
|
|
634
634
|
end
|
|
635
635
|
|
|
636
|
-
#
|
|
637
|
-
#
|
|
638
|
-
#
|
|
639
|
-
#
|
|
640
|
-
# based on the default model's provider — mirroring how lite is
|
|
641
|
-
# virtually derived via #lite_model_config_for_current.
|
|
642
|
-
# For "ocr": same custom→auto→nil pattern. Auto path first checks
|
|
643
|
-
# whether the default model itself supports vision (zero-overhead path,
|
|
644
|
-
# no sidecar needed); if not, derives from the provider's
|
|
645
|
-
# default_ocr_model.
|
|
636
|
+
# Raw accessor - returns the stored model entry without mode-aware filtering.
|
|
637
|
+
# For media/ocr: does NOT check mode field, only legacy disabled flag.
|
|
638
|
+
# Mode-aware callers should use #effective_media_entry / #effective_ocr_entry.
|
|
639
|
+
# HTTP server uses this to read raw entries for saved_custom / api_key masking.
|
|
646
640
|
def find_model_by_type(type)
|
|
647
641
|
kind = type.to_s
|
|
648
642
|
if Clacky::Providers::MEDIA_KINDS.include?(kind)
|
|
@@ -701,6 +695,38 @@ module Clacky
|
|
|
701
695
|
@models.reject! { |m| m["auto_injected"] && Clacky::Providers::MEDIA_KINDS.include?(m["type"].to_s) }
|
|
702
696
|
end
|
|
703
697
|
|
|
698
|
+
private def sidecar_off?(entry)
|
|
699
|
+
entry && (entry["disabled"] || entry["mode"] == "off")
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
private def sidecar_custom?(entry)
|
|
703
|
+
entry &&
|
|
704
|
+
entry["base_url"].to_s.strip != "" &&
|
|
705
|
+
entry["api_key"].to_s.strip != "" &&
|
|
706
|
+
entry["mode"] != "auto"
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
def effective_media_entry(kind)
|
|
710
|
+
kind = kind.to_s
|
|
711
|
+
raw_entry = @models.find { |m| m["type"] == kind }
|
|
712
|
+
return nil if sidecar_off?(raw_entry)
|
|
713
|
+
|
|
714
|
+
is_custom = sidecar_custom?(raw_entry)
|
|
715
|
+
override_model = raw_entry && !is_custom ? raw_entry["model"] : nil
|
|
716
|
+
|
|
717
|
+
is_custom ? raw_entry : derive_media_model(kind, model_override: override_model)
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
def effective_ocr_entry
|
|
721
|
+
raw_entry = @models.find { |m| m["type"] == "ocr" }
|
|
722
|
+
return nil if sidecar_off?(raw_entry)
|
|
723
|
+
|
|
724
|
+
is_custom = sidecar_custom?(raw_entry)
|
|
725
|
+
override_model = raw_entry && !is_custom ? raw_entry["model"] : nil
|
|
726
|
+
|
|
727
|
+
is_custom ? raw_entry : derive_ocr_model(model_override: override_model)
|
|
728
|
+
end
|
|
729
|
+
|
|
704
730
|
# Derive an OCR sidecar model entry from the default model's provider.
|
|
705
731
|
# Resolution order:
|
|
706
732
|
# 1. If the default model itself supports vision → return the default
|
|
@@ -763,7 +789,7 @@ module Clacky
|
|
|
763
789
|
kind = kind.to_s
|
|
764
790
|
raw_entry = @models.find { |m| m["type"] == kind }
|
|
765
791
|
|
|
766
|
-
if raw_entry
|
|
792
|
+
if sidecar_off?(raw_entry)
|
|
767
793
|
default = find_model_by_type("default")
|
|
768
794
|
default_provider = default && Clacky::Providers.resolve_provider(
|
|
769
795
|
base_url: default["base_url"], api_key: default["api_key"]
|
|
@@ -782,9 +808,7 @@ module Clacky
|
|
|
782
808
|
}
|
|
783
809
|
end
|
|
784
810
|
|
|
785
|
-
is_custom = raw_entry
|
|
786
|
-
raw_entry["base_url"].to_s.strip != "" &&
|
|
787
|
-
raw_entry["api_key"].to_s.strip != ""
|
|
811
|
+
is_custom = sidecar_custom?(raw_entry)
|
|
788
812
|
override_model = raw_entry && !is_custom ? raw_entry["model"] : nil
|
|
789
813
|
|
|
790
814
|
entry = if is_custom
|
|
@@ -842,7 +866,7 @@ module Clacky
|
|
|
842
866
|
)
|
|
843
867
|
available = default_provider ? Clacky::Providers.ocr_models(default_provider) : []
|
|
844
868
|
|
|
845
|
-
if raw_entry
|
|
869
|
+
if sidecar_off?(raw_entry)
|
|
846
870
|
# A disabled OCR sidecar only means "no separate vision model"; it must
|
|
847
871
|
# not override the fact that the chat model may handle images itself.
|
|
848
872
|
anchor = current_model || default
|
|
@@ -872,9 +896,7 @@ module Clacky
|
|
|
872
896
|
}
|
|
873
897
|
end
|
|
874
898
|
|
|
875
|
-
is_custom = raw_entry
|
|
876
|
-
raw_entry["base_url"].to_s.strip != "" &&
|
|
877
|
-
raw_entry["api_key"].to_s.strip != ""
|
|
899
|
+
is_custom = sidecar_custom?(raw_entry)
|
|
878
900
|
override_model = raw_entry && !is_custom ? raw_entry["model"] : nil
|
|
879
901
|
|
|
880
902
|
entry = if is_custom
|
data/lib/clacky/brand_config.rb
CHANGED
|
@@ -777,21 +777,26 @@ module Clacky
|
|
|
777
777
|
end
|
|
778
778
|
|
|
779
779
|
# Search the public extension marketplace. Anonymous — no license required.
|
|
780
|
-
# Uses GET /api/v1/extensions. Returns { success:, extensions: [], error: }.
|
|
781
|
-
|
|
780
|
+
# Uses GET /api/v1/extensions. Returns { success:, extensions: [], meta:, error: }.
|
|
781
|
+
# `meta` carries the platform's pagination info ({ current_page, total_pages,
|
|
782
|
+
# total_count, per_page }) when available.
|
|
783
|
+
def search_extensions!(query: nil, sort: nil, page: nil, per_page: nil)
|
|
782
784
|
params = {}
|
|
783
|
-
params["q"]
|
|
784
|
-
params["sort"]
|
|
785
|
+
params["q"] = query if query && !query.to_s.strip.empty?
|
|
786
|
+
params["sort"] = sort if sort && !sort.to_s.strip.empty?
|
|
787
|
+
params["page"] = page if page && page.to_i > 1
|
|
788
|
+
params["per_page"] = per_page if per_page && per_page.to_i > 0
|
|
785
789
|
qs = params.empty? ? "" : "?#{URI.encode_www_form(params)}"
|
|
786
790
|
response = platform_client.get("/api/v1/extensions#{qs}")
|
|
787
791
|
|
|
788
792
|
if response[:success]
|
|
789
|
-
|
|
793
|
+
data = response[:data] || {}
|
|
794
|
+
{ success: true, extensions: data["extensions"] || [], meta: data["meta"] }
|
|
790
795
|
else
|
|
791
|
-
{ success: false, error: response[:error] || "Search failed", extensions: [] }
|
|
796
|
+
{ success: false, error: response[:error] || "Search failed", extensions: [], meta: nil }
|
|
792
797
|
end
|
|
793
798
|
rescue StandardError => e
|
|
794
|
-
{ success: false, error: "Network error: #{e.message}", extensions: [] }
|
|
799
|
+
{ success: false, error: "Network error: #{e.message}", extensions: [], meta: nil }
|
|
795
800
|
end
|
|
796
801
|
|
|
797
802
|
# Fetch a single public marketplace extension's detail (contributes +
|
data/lib/clacky/cli.rb
CHANGED
|
@@ -148,6 +148,7 @@
|
|
|
148
148
|
"extlist.unpublish.confirm": "Unpublish {{id}} from the marketplace?",
|
|
149
149
|
"extlist.publishing": "Publishing…",
|
|
150
150
|
"extlist.needLicense": "Publishing requires an activated user license.",
|
|
151
|
+
"extlist.brandLocked": "Publishing to the marketplace requires a creator license.",
|
|
151
152
|
"extlist.newExt.label": "Create a new extension",
|
|
152
153
|
"extlist.newExt.hint": "Opens an AI session that scaffolds and builds it for you.",
|
|
153
154
|
"extlist.newExt.btn": "Create New Extension",
|
|
@@ -338,6 +339,7 @@
|
|
|
338
339
|
"extlist.unpublish.confirm": "确定要从市场下架 {{id}} 吗?",
|
|
339
340
|
"extlist.publishing": "发布中…",
|
|
340
341
|
"extlist.needLicense": "发布需要已激活的用户授权。",
|
|
342
|
+
"extlist.brandLocked": "发布到市场需要创作者权限。",
|
|
341
343
|
"extlist.newExt.label": "创建新扩展",
|
|
342
344
|
"extlist.newExt.hint": "打开一个 AI 会话,帮你生成并开发它。",
|
|
343
345
|
"extlist.newExt.btn": "创建新扩展",
|
|
@@ -1370,7 +1372,7 @@
|
|
|
1370
1372
|
const unpubBtn = el("button", { class: "studio-btn studio-btn-danger studio-btn-sm", text: t("btn.unpublish") });
|
|
1371
1373
|
unpubBtn.style.marginLeft = "auto";
|
|
1372
1374
|
unpubBtn.addEventListener("click", async () => {
|
|
1373
|
-
if (!
|
|
1375
|
+
if (!await Clacky.Modal.confirm(t("published.confirm", { id: e.id }))) return;
|
|
1374
1376
|
unpubBtn.disabled = true;
|
|
1375
1377
|
try { await postJson("/unpublish", { ext_id: e.id }); loadPublished(); }
|
|
1376
1378
|
catch (err) { unpubBtn.disabled = false; feedback(t("err.generic", { msg: err.message }), "error"); }
|
|
@@ -1406,11 +1408,16 @@
|
|
|
1406
1408
|
));
|
|
1407
1409
|
|
|
1408
1410
|
const bar = el("div", { class: "studio-actions" });
|
|
1409
|
-
|
|
1411
|
+
const brandNonAdmin = typeof Brand !== "undefined" && Brand.branded && !Brand.userLicensed;
|
|
1412
|
+
const pubBtn = el("button", { class: "studio-btn studio-btn-primary studio-publish-btn", text: t("btn.publish"), onclick: doPublish });
|
|
1413
|
+
pubBtn.disabled = brandNonAdmin;
|
|
1414
|
+
if (brandNonAdmin) pubBtn.setAttribute("data-tooltip", t("extlist.brandLocked"));
|
|
1415
|
+
bar.appendChild(pubBtn);
|
|
1410
1416
|
if (typeof Brand !== "undefined" && Brand.userLicensed) {
|
|
1411
1417
|
bar.appendChild(el("button", { class: "studio-btn studio-publish-brand-btn", text: t("btn.publishBrand"), onclick: doPublishBrand }));
|
|
1412
1418
|
}
|
|
1413
|
-
|
|
1419
|
+
const packBtn = el("button", { class: "studio-btn studio-btn-ghost studio-pack-btn", text: t("btn.pack"), onclick: doPack });
|
|
1420
|
+
bar.appendChild(packBtn);
|
|
1414
1421
|
container.appendChild(bar);
|
|
1415
1422
|
|
|
1416
1423
|
container.appendChild(el("div", { class: "studio-feedback" }));
|
|
@@ -1518,7 +1525,7 @@
|
|
|
1518
1525
|
actions.appendChild(el("button", { class: "studio-btn studio-btn-primary", text: t("extlist.btn.iterate"), onclick: () => createExtension(t("extlist.iterate.seed", { id: ext.id })) }));
|
|
1519
1526
|
const un = el("button", { class: "studio-btn studio-btn-ghost", text: t("extlist.btn.unpublish") });
|
|
1520
1527
|
un.addEventListener("click", async () => {
|
|
1521
|
-
if (!
|
|
1528
|
+
if (!await Clacky.Modal.confirm(t("extlist.unpublish.confirm", { id: ext.id }))) return;
|
|
1522
1529
|
un.disabled = true;
|
|
1523
1530
|
try { await postJson("/unpublish", { ext_id: ext.id }); await reload(); rebuild(); }
|
|
1524
1531
|
catch (e) { un.disabled = false; alert(t("err.generic", { msg: e.message })); }
|
|
@@ -1548,9 +1555,11 @@
|
|
|
1548
1555
|
card.appendChild(meta);
|
|
1549
1556
|
|
|
1550
1557
|
const actions = el("div", { class: "studio-actions" });
|
|
1558
|
+
const brandNonAdmin = typeof Brand !== "undefined" && Brand.branded && !Brand.userLicensed;
|
|
1551
1559
|
const pub = el("button", { class: "studio-btn studio-btn-primary", text: published ? t("extlist.btn.update") : t("extlist.btn.publish") });
|
|
1552
|
-
pub.disabled = !!ext.error_count;
|
|
1553
|
-
|
|
1560
|
+
pub.disabled = !!ext.error_count || brandNonAdmin;
|
|
1561
|
+
const pubTip = ext.error_count ? t("extlist.verify.errors", { n: ext.error_count }) : (brandNonAdmin ? t("extlist.brandLocked") : "");
|
|
1562
|
+
if (pubTip) pub.setAttribute("data-tooltip", pubTip);
|
|
1554
1563
|
pub.addEventListener("click", () => doPublish(ext, cloudEntry ? cloudEntry.version : null));
|
|
1555
1564
|
actions.appendChild(pub);
|
|
1556
1565
|
|
|
@@ -1558,7 +1567,7 @@
|
|
|
1558
1567
|
const brandEntry = brandCloud.find((c) => c.id === ext.id);
|
|
1559
1568
|
const brandPub = el("button", { class: "studio-btn", text: t("extlist.btn.publishBrand") });
|
|
1560
1569
|
brandPub.disabled = !!ext.error_count;
|
|
1561
|
-
|
|
1570
|
+
if (ext.error_count) brandPub.setAttribute("data-tooltip", t("extlist.verify.errors", { n: ext.error_count }));
|
|
1562
1571
|
brandPub.addEventListener("click", () => doPublish(ext, brandEntry ? brandEntry.version : null, "self"));
|
|
1563
1572
|
actions.appendChild(brandPub);
|
|
1564
1573
|
}
|
|
@@ -1590,7 +1599,7 @@
|
|
|
1590
1599
|
}
|
|
1591
1600
|
|
|
1592
1601
|
async function doDelete(ext, btn) {
|
|
1593
|
-
if (!confirm(t("extlist.delete.confirm", { id: ext.id }))) return;
|
|
1602
|
+
if (!await Clacky.Modal.confirm(t("extlist.delete.confirm", { id: ext.id }))) return;
|
|
1594
1603
|
btn.disabled = true;
|
|
1595
1604
|
try {
|
|
1596
1605
|
const res = await fetch("/api/ext/ext-studio/local", {
|
|
@@ -1635,11 +1644,15 @@
|
|
|
1635
1644
|
|
|
1636
1645
|
if (headerActions) {
|
|
1637
1646
|
headerActions.innerHTML = "";
|
|
1638
|
-
|
|
1647
|
+
const brandNonAdmin = typeof Brand !== "undefined" && Brand.branded && !Brand.userLicensed;
|
|
1648
|
+
const newBtn = el("button", {
|
|
1639
1649
|
class: "studio-btn studio-btn-primary",
|
|
1640
1650
|
text: t("extlist.newExt.btn"),
|
|
1641
1651
|
onclick: () => createExtension(t("extlist.newExt.seed")),
|
|
1642
|
-
})
|
|
1652
|
+
});
|
|
1653
|
+
newBtn.disabled = brandNonAdmin;
|
|
1654
|
+
if (brandNonAdmin) newBtn.setAttribute("data-tooltip", t("extlist.brandLocked"));
|
|
1655
|
+
headerActions.appendChild(newBtn);
|
|
1643
1656
|
}
|
|
1644
1657
|
|
|
1645
1658
|
container.appendChild(section("extlist.section.cloud", "extlist.section.cloudHint", cloud, cloudCard, "extlist.cloud.empty"));
|
|
@@ -1711,7 +1724,7 @@
|
|
|
1711
1724
|
if (skill.local_present && skill.has_local_changes) {
|
|
1712
1725
|
const btn = el("button", { class: "studio-btn studio-btn-primary", text: t("skills.btn.update") });
|
|
1713
1726
|
btn.disabled = locked;
|
|
1714
|
-
|
|
1727
|
+
if (locked) btn.setAttribute("data-tooltip", t("skills.locked"));
|
|
1715
1728
|
btn.addEventListener("click", () => doPublish(skill.name, btn, true));
|
|
1716
1729
|
actions.appendChild(btn);
|
|
1717
1730
|
} else if (skill.local_present) {
|
|
@@ -1738,7 +1751,7 @@
|
|
|
1738
1751
|
const actions = el("div", { class: "studio-actions" });
|
|
1739
1752
|
const btn = el("button", { class: "studio-btn studio-btn-primary", text: t("skills.btn.publish") });
|
|
1740
1753
|
btn.disabled = locked;
|
|
1741
|
-
|
|
1754
|
+
if (locked) btn.setAttribute("data-tooltip", t("skills.locked"));
|
|
1742
1755
|
btn.addEventListener("click", () => doPublish(skill.name, btn, false));
|
|
1743
1756
|
actions.appendChild(btn);
|
|
1744
1757
|
card.appendChild(actions);
|
|
@@ -1753,7 +1766,7 @@
|
|
|
1753
1766
|
try {
|
|
1754
1767
|
let result = await publish(name, isUpdate);
|
|
1755
1768
|
if (!result.ok && result.already_exists && !isUpdate) {
|
|
1756
|
-
if (
|
|
1769
|
+
if (await Clacky.Modal.confirm(`"${name}" already exists on the platform. Overwrite?`)) {
|
|
1757
1770
|
result = await publish(name, true);
|
|
1758
1771
|
} else {
|
|
1759
1772
|
btn.disabled = false;
|
|
@@ -1798,7 +1811,7 @@
|
|
|
1798
1811
|
onclick: () => createSkill(null),
|
|
1799
1812
|
});
|
|
1800
1813
|
newBtn.disabled = locked;
|
|
1801
|
-
|
|
1814
|
+
if (locked) newBtn.setAttribute("data-tooltip", t("skills.locked"));
|
|
1802
1815
|
headerActions.appendChild(newBtn);
|
|
1803
1816
|
}
|
|
1804
1817
|
|
|
@@ -280,14 +280,15 @@ This creates `~/.clacky/ext/local/<id>/` with a working hello panel + handler:
|
|
|
280
280
|
- `ext.yml` — the manifest
|
|
281
281
|
- `panels/hello/view.js` — a panel that pings the backend
|
|
282
282
|
- `api/handler.rb` — a `Clacky::ApiExtension` subclass mounted at `/api/ext/<id>/`
|
|
283
|
+
- `test/handler_test.rb` — a runnable minitest example (`ruby test/handler_test.rb`)
|
|
283
284
|
|
|
284
285
|
Use `--full` only when the user needs the kitchen-sink reference exercising all seven
|
|
285
286
|
contributes types — it's a lot to read, so prefer the plain scaffold otherwise.
|
|
286
287
|
|
|
287
288
|
### 3 — Read what was generated
|
|
288
289
|
|
|
289
|
-
Always read the generated `ext.yml`, `view.js`,
|
|
290
|
-
is your starting point; you'll reshape it to match the idea.
|
|
290
|
+
Always read the generated `ext.yml`, `view.js`, `handler.rb`, and `test/handler_test.rb`
|
|
291
|
+
before editing. This is your starting point; you'll reshape it to match the idea.
|
|
291
292
|
|
|
292
293
|
### 4 — Reshape to the idea
|
|
293
294
|
|
|
@@ -366,7 +367,22 @@ Rules while reshaping:
|
|
|
366
367
|
reference `panels: [id]` and `skills: [id]`. Add those blocks to `ext.yml` only if the
|
|
367
368
|
idea needs them.
|
|
368
369
|
|
|
369
|
-
### 5 —
|
|
370
|
+
### 5 — Write tests
|
|
371
|
+
|
|
372
|
+
Test the backend before you call it done. Rewrite the scaffolded `test/handler_test.rb`
|
|
373
|
+
to exercise your real routes (parse the JSON response, assert its fields), then run it
|
|
374
|
+
green:
|
|
375
|
+
|
|
376
|
+
```
|
|
377
|
+
ruby test/handler_test.rb
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
- If `test/` is missing from the scaffold, create `test/handler_test.rb` yourself
|
|
381
|
+
(`mkdir -p test` then write it) — do NOT skip tests just because the file is absent.
|
|
382
|
+
- A handler that only returns a static string may keep the scaffold's hello test;
|
|
383
|
+
anything that fetches, transforms, caches, or computes MUST have a real test.
|
|
384
|
+
|
|
385
|
+
### 6 — Confirm it loads
|
|
370
386
|
|
|
371
387
|
Run `clacky ext verify` and confirm the new units resolve with no errors, then have the
|
|
372
388
|
user reload the WebUI page. If verify reports problems, go to **Debug & verify**.
|
|
@@ -423,8 +439,9 @@ smallest change, re-run verify, repeat until clean — fix ONE issue at a time.
|
|
|
423
439
|
- **`schema.unknown_contributes`** → a top-level key under `contributes:` is misspelled.
|
|
424
440
|
**Do:** fix the spelling to one of `panels api skills agents channels patches hooks`.
|
|
425
441
|
- **`schema.unknown_key`** → an unknown **top-level** key in `ext.yml`. **Do:** fix the
|
|
426
|
-
spelling. Allowed top-level keys: `id name
|
|
427
|
-
homepage license public
|
|
442
|
+
spelling. Allowed top-level keys: `id name name_zh display_name display_name_zh title
|
|
443
|
+
description description_zh emoji version origin author homepage license public
|
|
444
|
+
license_required keywords contributes`.
|
|
428
445
|
- **`schema.unknown_field`** → a unit has a field not allowed for its type. **Do:** delete
|
|
429
446
|
or rename that field. Allowed fields per type (this is the authoritative list — do not
|
|
430
447
|
invent others):
|
|
@@ -65,17 +65,10 @@ module Clacky
|
|
|
65
65
|
existing = reload ? Clacky::ApiExtension.registry[ext_id] : nil
|
|
66
66
|
existing&.reset_routes!
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
load(handler_path)
|
|
73
|
-
ensure
|
|
74
|
-
$VERBOSE = old_verbose
|
|
75
|
-
end
|
|
76
|
-
else
|
|
77
|
-
require(handler_path)
|
|
78
|
-
end
|
|
68
|
+
# Wrapped load: each handler gets a fresh anonymous module, so two exts
|
|
69
|
+
# defining the same top-level class/constant names cannot clobber each
|
|
70
|
+
# other (and the `inherited` hook fires even on repeat loads).
|
|
71
|
+
load(handler_path, true)
|
|
79
72
|
|
|
80
73
|
new_subclasses = Clacky::ApiExtension.pending_subclasses[before..] || []
|
|
81
74
|
klass = new_subclasses.last || existing
|
|
@@ -16,7 +16,7 @@ module Clacky
|
|
|
16
16
|
# optional URL fetch in `install`.
|
|
17
17
|
module ExtensionPackager
|
|
18
18
|
MANIFEST = "ext.yml"
|
|
19
|
-
MAX_ZIP_SIZE =
|
|
19
|
+
MAX_ZIP_SIZE = 80 * 1024 * 1024
|
|
20
20
|
|
|
21
21
|
# Network timeouts for the download step. `read_timeout` is an *idle*
|
|
22
22
|
# timeout (resets on every chunk received), so a stalled connection with
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Run from the container root:
|
|
4
|
+
# ruby test/handler_test.rb
|
|
5
|
+
require "minitest/autorun"
|
|
6
|
+
require "clacky"
|
|
7
|
+
require "json"
|
|
8
|
+
|
|
9
|
+
load File.expand_path("../api/handler.rb", __dir__)
|
|
10
|
+
|
|
11
|
+
class <%= const_prefix %>ExtTest < Minitest::Test
|
|
12
|
+
def test_root_route_returns_json
|
|
13
|
+
route = <%= const_prefix %>Ext.routes.find { |r| r.method == :get && r.pattern == "/" }
|
|
14
|
+
refute_nil route, "expected a GET / route"
|
|
15
|
+
|
|
16
|
+
handler = <%= const_prefix %>Ext.new(req: nil, res: nil, route: route, params: {}, http_server: nil)
|
|
17
|
+
halt = assert_raises(Clacky::ApiExtension::Halt) { handler.invoke }
|
|
18
|
+
|
|
19
|
+
assert_equal 200, halt.status
|
|
20
|
+
assert_equal "hello from <%= slug %>", JSON.parse(halt.payload)["message"]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -16,7 +16,7 @@ module Clacky
|
|
|
16
16
|
module ExtensionVerifier
|
|
17
17
|
Issue = Struct.new(:ext, :unit, :level, :code, :message, :file, :hint, keyword_init: true)
|
|
18
18
|
|
|
19
|
-
KNOWN_TOP_KEYS = %w[id name title description version origin author homepage license public license_required keywords contributes].freeze
|
|
19
|
+
KNOWN_TOP_KEYS = %w[id name name_zh display_name display_name_zh title description description_zh emoji version origin author homepage license public license_required keywords contributes].freeze
|
|
20
20
|
KNOWN_CONTRIBUTES = %w[panels api skills agents channels patches hooks].freeze
|
|
21
21
|
|
|
22
22
|
PANEL_KEYS = %w[id title title_zh description description_zh view order attach entry_points].freeze
|
|
@@ -49,17 +49,17 @@ module Clacky
|
|
|
49
49
|
|
|
50
50
|
# @return [Hash, nil] the type=image model entry, or nil if not configured
|
|
51
51
|
def image_model_entry
|
|
52
|
-
@agent_config.
|
|
52
|
+
@agent_config.effective_media_entry("image")
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
# @return [Hash, nil] the type=video model entry, or nil if not configured
|
|
56
56
|
def video_model_entry
|
|
57
|
-
@agent_config.
|
|
57
|
+
@agent_config.effective_media_entry("video")
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
# @return [Hash, nil] the type=audio model entry, or nil if not configured
|
|
61
61
|
def audio_model_entry
|
|
62
|
-
@agent_config.
|
|
62
|
+
@agent_config.effective_media_entry("audio")
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def generate_image(prompt:, aspect_ratio: "landscape", output_dir: nil, **kwargs)
|