openclacky 1.5.7 → 1.5.8
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 +33 -0
- data/README.md +2 -2
- data/README_CN.md +1 -1
- data/README_JA.md +1 -1
- data/lib/clacky/agent.rb +2 -3
- data/lib/clacky/agent_config.rb +40 -18
- data/lib/clacky/cli.rb +0 -1
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +23 -10
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +3 -2
- 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/verifier.rb +1 -1
- data/lib/clacky/media/generator.rb +3 -3
- data/lib/clacky/message_history.rb +1 -1
- data/lib/clacky/providers.rb +2 -1
- data/lib/clacky/server/http_server.rb +60 -76
- data/lib/clacky/session_manager.rb +7 -0
- 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/version.rb +1 -1
- data/lib/clacky/web/app.css +134 -210
- 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 +1 -1
- data/lib/clacky/web/features/extensions/view.js +28 -5
- data/lib/clacky/web/features/new-session/view.js +24 -0
- data/lib/clacky/web/i18n.js +20 -18
- data/lib/clacky/web/index.html +12 -13
- data/lib/clacky/web/projects.js +73 -282
- data/lib/clacky/web/settings.js +6 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: efbdc6d887e5beea867023b733d023f236b0e0a0a813201cce3767deebe43fd2
|
|
4
|
+
data.tar.gz: 1abf4430324ddd28b1d1d6a6b82390eaa8c76dc026f31cc1c10ff7fdac6c7207
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6df9bcdd91aa229039baad046a06d471ae11367e78a594f655abffd808c3f9b14507f9fca32d99c4f3fbd13a3ab2743451826238bc8e55e9ddc0161546276bae
|
|
7
|
+
data.tar.gz: 53b8fd83915c5ee898237dc113099a7ea0e339961dac83b11d4b16285a1f2fb3b4581d1876f70c2a78608556b22760da3ba8fff7146af0b66c701d12e963005b
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,39 @@ 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.8] - 2026-08-12
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Inline SVG icons (edit, clear sessions, delete) in project action menu (#474)
|
|
11
|
+
- New project button in project selector popover
|
|
12
|
+
- Verified badge for officially reviewed extensions in marketplace
|
|
13
|
+
|
|
14
|
+
### Improved
|
|
15
|
+
- Skip full session/file scan when count is under cap (#470 - @shipinliang)
|
|
16
|
+
- Token estimation uses count() instead of scan().length (#471 - @shipinliang)
|
|
17
|
+
- Faster server startup
|
|
18
|
+
- Brand user extension UI (non-admin publish button grayed out, unverified banner hidden)
|
|
19
|
+
- Sidebar my-data section icons refreshed
|
|
20
|
+
- Section chevron icon size increased from 11px to 13px
|
|
21
|
+
- Project sidebar hover effects, folder default hints, project icon sizing
|
|
22
|
+
- Billing UI cleanup and global theme color adjustment
|
|
23
|
+
- Section header font and project group spacing tweaks
|
|
24
|
+
- Project menu labels updated
|
|
25
|
+
- Billing disclaimer about provider billing
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Extension pack max zip size increased from 50 MB to 80 MB
|
|
29
|
+
- New session button in project area now correctly preselects the project
|
|
30
|
+
- Chat navigation popup no longer overlaps sidebar collapse button
|
|
31
|
+
- Sub-model config preserved when switching modes (auto/off/custom)
|
|
32
|
+
- minimax-m2.7 vision detection fixed
|
|
33
|
+
- Project icon no longer reverts to default after page refresh
|
|
34
|
+
- Load constant warning fixed
|
|
35
|
+
|
|
36
|
+
### More
|
|
37
|
+
- README cost table formatting normalized
|
|
38
|
+
- Ambiguous cost phrasing clarified across READMEs (#475 - @chengoak)
|
|
39
|
+
|
|
7
40
|
## [1.5.7] - 2026-08-10
|
|
8
41
|
|
|
9
42
|
### 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× |
|
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
|
|
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 倍 |
|
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/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": "创建新扩展",
|
|
@@ -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" }));
|
|
@@ -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
|
}
|
|
@@ -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);
|
|
@@ -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
|
|
|
@@ -423,8 +423,9 @@ smallest change, re-run verify, repeat until clean — fix ONE issue at a time.
|
|
|
423
423
|
- **`schema.unknown_contributes`** → a top-level key under `contributes:` is misspelled.
|
|
424
424
|
**Do:** fix the spelling to one of `panels api skills agents channels patches hooks`.
|
|
425
425
|
- **`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
|
|
426
|
+
spelling. Allowed top-level keys: `id name name_zh display_name display_name_zh title
|
|
427
|
+
description description_zh emoji version origin author homepage license public
|
|
428
|
+
license_required keywords contributes`.
|
|
428
429
|
- **`schema.unknown_field`** → a unit has a field not allowed for its type. **Do:** delete
|
|
429
430
|
or rename that field. Allowed fields per type (this is the authoritative list — do not
|
|
430
431
|
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
|
|
@@ -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)
|
|
@@ -286,7 +286,7 @@ module Clacky
|
|
|
286
286
|
private def estimate_content_tokens(content)
|
|
287
287
|
case content
|
|
288
288
|
when String
|
|
289
|
-
ascii_chars = content.
|
|
289
|
+
ascii_chars = content.count(" -~")
|
|
290
290
|
multibyte_chars = content.length - ascii_chars
|
|
291
291
|
((ascii_chars / 4.0) + (multibyte_chars / 1.5)).ceil
|
|
292
292
|
when Array
|
data/lib/clacky/providers.rb
CHANGED
|
@@ -472,7 +472,8 @@ module Clacky
|
|
|
472
472
|
"model_capabilities" => {
|
|
473
473
|
"glm-5.2" => { "vision" => false }.freeze,
|
|
474
474
|
"deepseek-v4-pro" => { "vision" => false }.freeze,
|
|
475
|
-
"deepseek-v4-flash" => { "vision" => false }.freeze
|
|
475
|
+
"deepseek-v4-flash" => { "vision" => false }.freeze,
|
|
476
|
+
"minimax-m2.7" => { "vision" => false }.freeze
|
|
476
477
|
}.freeze,
|
|
477
478
|
"default_ocr_model" => "doubao-seed-2.0-lite",
|
|
478
479
|
"website_url" => "https://console.volcengine.com/ark/region:cn-beijing/overview"
|
|
@@ -184,7 +184,7 @@ module Clacky
|
|
|
184
184
|
- 将大目标拆解为可执行的小步骤
|
|
185
185
|
MD
|
|
186
186
|
|
|
187
|
-
def initialize(host: "127.0.0.1", port: 7070, agent_config:, client_factory:, brand_test: false, sessions_dir: nil, socket: nil, master_pid: nil)
|
|
187
|
+
def initialize(host: "127.0.0.1", port: 7070, agent_config:, client_factory:, brand_test: false, sessions_dir: nil, projects_file: nil, socket: nil, master_pid: nil)
|
|
188
188
|
@host = host
|
|
189
189
|
@port = port
|
|
190
190
|
@agent_config = agent_config
|
|
@@ -197,7 +197,7 @@ module Clacky
|
|
|
197
197
|
@restart_script = File.expand_path($0)
|
|
198
198
|
@restart_argv = ARGV.dup
|
|
199
199
|
@session_manager = Clacky::SessionManager.new(sessions_dir: sessions_dir)
|
|
200
|
-
@project_manager = Clacky::Server::ProjectManager.new
|
|
200
|
+
@project_manager = Clacky::Server::ProjectManager.new(projects_file: projects_file)
|
|
201
201
|
@registry = SessionRegistry.new(
|
|
202
202
|
session_manager: @session_manager,
|
|
203
203
|
session_restorer: method(:build_session_from_data),
|
|
@@ -1840,6 +1840,9 @@ module Clacky
|
|
|
1840
1840
|
requested_model: state["requested_model"],
|
|
1841
1841
|
configured: state["configured"]
|
|
1842
1842
|
}
|
|
1843
|
+
if entry && entry["base_url"] && state["source"] != "custom"
|
|
1844
|
+
out[t][:saved_custom] = { model: entry["model"], base_url: entry["base_url"] }
|
|
1845
|
+
end
|
|
1843
1846
|
end
|
|
1844
1847
|
|
|
1845
1848
|
# Surface what the current default model can offer, even when the
|
|
@@ -1949,50 +1952,78 @@ module Clacky
|
|
|
1949
1952
|
end
|
|
1950
1953
|
|
|
1951
1954
|
def api_update_media_config(kind, req, res)
|
|
1952
|
-
|
|
1955
|
+
update_sidecar_config(res, kind, parse_json_body(req) || {})
|
|
1956
|
+
end
|
|
1957
|
+
|
|
1958
|
+
private def update_sidecar_config(res, kind, body)
|
|
1953
1959
|
source = body["source"].to_s
|
|
1954
1960
|
unless %w[off auto custom].include?(source)
|
|
1955
1961
|
return json_response(res, 422, { error: "invalid source" })
|
|
1956
1962
|
end
|
|
1957
1963
|
|
|
1958
|
-
@agent_config.models.
|
|
1964
|
+
existing = @agent_config.models.find { |m| m["type"] == kind }
|
|
1959
1965
|
|
|
1960
1966
|
case source
|
|
1961
1967
|
when "off"
|
|
1962
|
-
|
|
1963
|
-
"
|
|
1964
|
-
|
|
1965
|
-
"disabled" => true
|
|
1966
|
-
}
|
|
1967
|
-
when "auto"
|
|
1968
|
-
override = body["model"].to_s.strip
|
|
1969
|
-
unless override.empty?
|
|
1968
|
+
if existing
|
|
1969
|
+
existing["mode"] = "off"
|
|
1970
|
+
else
|
|
1970
1971
|
@agent_config.models << {
|
|
1971
|
-
"id"
|
|
1972
|
-
"type"
|
|
1973
|
-
"
|
|
1972
|
+
"id" => SecureRandom.uuid,
|
|
1973
|
+
"type" => kind,
|
|
1974
|
+
"mode" => "off"
|
|
1974
1975
|
}
|
|
1975
1976
|
end
|
|
1977
|
+
when "auto"
|
|
1978
|
+
override = body["model"].to_s.strip
|
|
1979
|
+
if existing
|
|
1980
|
+
existing.delete("disabled")
|
|
1981
|
+
existing["mode"] = "auto"
|
|
1982
|
+
existing["model"] = override unless override.empty?
|
|
1983
|
+
else
|
|
1984
|
+
unless override.empty?
|
|
1985
|
+
@agent_config.models << {
|
|
1986
|
+
"id" => SecureRandom.uuid,
|
|
1987
|
+
"type" => kind,
|
|
1988
|
+
"mode" => "auto",
|
|
1989
|
+
"model" => override
|
|
1990
|
+
}
|
|
1991
|
+
end
|
|
1992
|
+
end
|
|
1976
1993
|
when "custom"
|
|
1977
1994
|
model = body["model"].to_s.strip
|
|
1978
1995
|
base_url = body["base_url"].to_s.strip
|
|
1979
1996
|
api_key = body["api_key"].to_s
|
|
1980
|
-
if
|
|
1997
|
+
if api_key.empty? || api_key.include?("****")
|
|
1998
|
+
api_key = existing ? existing["api_key"].to_s : ""
|
|
1999
|
+
end
|
|
2000
|
+
if model.empty? || base_url.empty? || api_key.empty?
|
|
1981
2001
|
return json_response(res, 422, { error: "model, base_url, api_key are required" })
|
|
1982
2002
|
end
|
|
1983
2003
|
|
|
1984
|
-
|
|
1985
|
-
"
|
|
1986
|
-
"
|
|
1987
|
-
"
|
|
1988
|
-
"
|
|
1989
|
-
"
|
|
1990
|
-
"
|
|
1991
|
-
|
|
2004
|
+
if existing
|
|
2005
|
+
existing["model"] = model
|
|
2006
|
+
existing["base_url"] = base_url
|
|
2007
|
+
existing["api_key"] = api_key
|
|
2008
|
+
existing["anthropic_format"] = body["anthropic_format"] || false
|
|
2009
|
+
existing["mode"] = "custom"
|
|
2010
|
+
existing.delete("disabled")
|
|
2011
|
+
else
|
|
2012
|
+
@agent_config.models << {
|
|
2013
|
+
"id" => SecureRandom.uuid,
|
|
2014
|
+
"model" => model,
|
|
2015
|
+
"base_url" => base_url,
|
|
2016
|
+
"api_key" => api_key,
|
|
2017
|
+
"anthropic_format" => body["anthropic_format"] || false,
|
|
2018
|
+
"type" => kind,
|
|
2019
|
+
"mode" => "custom"
|
|
2020
|
+
}
|
|
2021
|
+
end
|
|
1992
2022
|
end
|
|
1993
2023
|
|
|
1994
2024
|
@agent_config.save
|
|
1995
|
-
|
|
2025
|
+
state = kind == "ocr" ? @agent_config.ocr_state : @agent_config.media_state(kind)
|
|
2026
|
+
json_response(res, 200, { ok: true, state: state })
|
|
1996
2027
|
rescue => e
|
|
1997
2028
|
json_response(res, 422, { error: e.message })
|
|
1998
2029
|
end
|
|
@@ -2016,6 +2047,9 @@ module Clacky
|
|
|
2016
2047
|
configured: state["configured"],
|
|
2017
2048
|
primary: state["primary"] || false
|
|
2018
2049
|
}
|
|
2050
|
+
if entry && entry["base_url"] && state["source"] != "custom"
|
|
2051
|
+
out[:saved_custom] = { model: entry["model"], base_url: entry["base_url"] }
|
|
2052
|
+
end
|
|
2019
2053
|
|
|
2020
2054
|
# Auto-mode preview: surface what the OCR sidecar *would* be if the
|
|
2021
2055
|
# user flipped to "auto" — derived from the same provider as the
|
|
@@ -2037,58 +2071,8 @@ module Clacky
|
|
|
2037
2071
|
# PATCH /api/config/ocr
|
|
2038
2072
|
# Body: { source: "off"|"auto"|"custom", model?, base_url?, api_key?,
|
|
2039
2073
|
# anthropic_format? }
|
|
2040
|
-
# Mirrors api_update_media_config but for the single "ocr" type.
|
|
2041
2074
|
def api_update_ocr_config(req, res)
|
|
2042
|
-
|
|
2043
|
-
source = body["source"].to_s
|
|
2044
|
-
unless %w[off auto custom].include?(source)
|
|
2045
|
-
return json_response(res, 422, { error: "invalid source" })
|
|
2046
|
-
end
|
|
2047
|
-
|
|
2048
|
-
@agent_config.models.reject! { |m| m["type"] == "ocr" }
|
|
2049
|
-
|
|
2050
|
-
case source
|
|
2051
|
-
when "off"
|
|
2052
|
-
@agent_config.models << {
|
|
2053
|
-
"id" => SecureRandom.uuid,
|
|
2054
|
-
"type" => "ocr",
|
|
2055
|
-
"disabled" => true
|
|
2056
|
-
}
|
|
2057
|
-
when "auto"
|
|
2058
|
-
override = body["model"].to_s.strip
|
|
2059
|
-
unless override.empty?
|
|
2060
|
-
@agent_config.models << {
|
|
2061
|
-
"id" => SecureRandom.uuid,
|
|
2062
|
-
"type" => "ocr",
|
|
2063
|
-
"model" => override
|
|
2064
|
-
}
|
|
2065
|
-
end
|
|
2066
|
-
when "custom"
|
|
2067
|
-
model = body["model"].to_s.strip
|
|
2068
|
-
base_url = body["base_url"].to_s.strip
|
|
2069
|
-
api_key = body["api_key"].to_s
|
|
2070
|
-
if api_key.include?("****")
|
|
2071
|
-
existing = @agent_config.models.find { |m| m["type"] == "ocr" && m["api_key"] }
|
|
2072
|
-
api_key = existing ? existing["api_key"].to_s : ""
|
|
2073
|
-
end
|
|
2074
|
-
if model.empty? || base_url.empty? || api_key.empty?
|
|
2075
|
-
return json_response(res, 422, { error: "model, base_url, api_key are required" })
|
|
2076
|
-
end
|
|
2077
|
-
|
|
2078
|
-
@agent_config.models << {
|
|
2079
|
-
"id" => SecureRandom.uuid,
|
|
2080
|
-
"model" => model,
|
|
2081
|
-
"base_url" => base_url,
|
|
2082
|
-
"api_key" => api_key,
|
|
2083
|
-
"anthropic_format" => body["anthropic_format"] || false,
|
|
2084
|
-
"type" => "ocr"
|
|
2085
|
-
}
|
|
2086
|
-
end
|
|
2087
|
-
|
|
2088
|
-
@agent_config.save
|
|
2089
|
-
json_response(res, 200, { ok: true, state: @agent_config.ocr_state })
|
|
2090
|
-
rescue => e
|
|
2091
|
-
json_response(res, 422, { error: e.message })
|
|
2075
|
+
update_sidecar_config(res, "ocr", parse_json_body(req) || {})
|
|
2092
2076
|
end
|
|
2093
2077
|
|
|
2094
2078
|
# POST /api/config/ocr/test
|