openclacky 1.4.1 → 1.5.0
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 +26 -0
- data/lib/clacky/agent/llm_caller.rb +3 -3
- data/lib/clacky/agent/session_serializer.rb +39 -5
- data/lib/clacky/brand_config.rb +49 -7
- data/lib/clacky/client.rb +22 -16
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +135 -84
- data/lib/clacky/default_extensions/ext-studio/ext.yml +2 -4
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +522 -0
- data/lib/clacky/extension/api_extension.rb +5 -6
- data/lib/clacky/extension/loader.rb +16 -2
- data/lib/clacky/extension/scaffold/templates/full/api/handler.rb.erb +16 -0
- data/lib/clacky/mcp/http_transport.rb +1 -1
- data/lib/clacky/providers.rb +109 -1
- data/lib/clacky/server/http_server.rb +112 -20
- data/lib/clacky/server/session_registry.rb +9 -0
- data/lib/clacky/utils/model_pricing.rb +36 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +321 -32
- data/lib/clacky/web/app.js +29 -6
- data/lib/clacky/web/components/onboard.js +7 -3
- data/lib/clacky/web/features/extensions/store.js +54 -2
- data/lib/clacky/web/features/extensions/view.js +40 -5
- data/lib/clacky/web/features/new-session/store.js +8 -4
- data/lib/clacky/web/features/version/view.js +5 -1
- data/lib/clacky/web/i18n.js +52 -18
- data/lib/clacky/web/index.html +51 -5
- data/lib/clacky/web/sessions.js +40 -11
- data/lib/clacky/web/settings.js +22 -3
- data/lib/clacky/web/theme.js +27 -58
- metadata +2 -5
- data/lib/clacky/default_extensions/ext-studio/skills/ext-debug/SKILL.md +0 -71
- data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +0 -73
- data/lib/clacky/default_extensions/ext-studio/skills/ext-scaffold/SKILL.md +0 -65
- data/lib/clacky/default_skills/extend-openclacky/SKILL.md +0 -106
data/lib/clacky/providers.rb
CHANGED
|
@@ -376,6 +376,70 @@ module Clacky
|
|
|
376
376
|
"website_url" => "https://open.bigmodel.cn/usercenter/apikeys"
|
|
377
377
|
}.freeze,
|
|
378
378
|
|
|
379
|
+
# Volcengine Ark (Doubao) — ByteDance's model platform, OpenAI-compatible.
|
|
380
|
+
# Exposes three functionally-equivalent endpoints (Pay-as-you-go / Coding
|
|
381
|
+
# Plan / Agent Plan) that share the same model lineup and capability
|
|
382
|
+
# profile, so a single preset with endpoint_variants is the right shape.
|
|
383
|
+
# Without a preset match, base_urls like /api/coding/v3 fell through to the
|
|
384
|
+
# conservative "assume vision=true" default and inlined images into
|
|
385
|
+
# text-only models (e.g. glm-5.2), which Ark rejects. The vision matrix
|
|
386
|
+
# below is the source of truth so text-only models route through the OCR
|
|
387
|
+
# sidecar instead.
|
|
388
|
+
"volcengine-ark" => {
|
|
389
|
+
"name" => "Volcengine Ark (Doubao)",
|
|
390
|
+
"name_key" => "provider.name.volcengine_ark",
|
|
391
|
+
"base_url" => "https://ark.cn-beijing.volces.com/api/v3",
|
|
392
|
+
"api" => "openai-completions",
|
|
393
|
+
"default_model" => "doubao-seed-2.0-pro",
|
|
394
|
+
"models" => [
|
|
395
|
+
"doubao-seed-evolving",
|
|
396
|
+
"doubao-seed-2.1-pro",
|
|
397
|
+
"doubao-seed-2.1-turbo",
|
|
398
|
+
"doubao-seed-2.0-code",
|
|
399
|
+
"doubao-seed-2.0-pro",
|
|
400
|
+
"doubao-seed-2.0-lite",
|
|
401
|
+
"minimax-m3",
|
|
402
|
+
"minimax-m2.7",
|
|
403
|
+
"kimi-k2.7-code",
|
|
404
|
+
"kimi-k2.6",
|
|
405
|
+
"glm-5.2",
|
|
406
|
+
"deepseek-v4-pro",
|
|
407
|
+
"deepseek-v4-flash"
|
|
408
|
+
],
|
|
409
|
+
"endpoint_variants" => [
|
|
410
|
+
{ "label" => "Pay-as-you-go", "label_key" => "settings.models.baseurl.variant.ark_payg", "base_url" => "https://ark.cn-beijing.volces.com/api/v3", "alias_group" => "payg" }.freeze,
|
|
411
|
+
{ "label" => "Coding Plan", "label_key" => "settings.models.baseurl.variant.ark_coding", "base_url" => "https://ark.cn-beijing.volces.com/api/coding/v3" }.freeze,
|
|
412
|
+
{ "label" => "Agent Plan", "label_key" => "settings.models.baseurl.variant.ark_agent", "base_url" => "https://ark.cn-beijing.volces.com/api/plan/v3" }.freeze
|
|
413
|
+
].freeze,
|
|
414
|
+
# Pay-as-you-go billing endpoint requires versioned model ids, while the
|
|
415
|
+
# Coding/Agent Plan endpoints accept the short display names. Users pick
|
|
416
|
+
# the intuitive short name; on the payg endpoint we transparently swap
|
|
417
|
+
# it for the versioned id the billing API expects. Only models whose ids
|
|
418
|
+
# actually differ are listed here.
|
|
419
|
+
"api_model_aliases" => {
|
|
420
|
+
"payg" => {
|
|
421
|
+
"glm-5.2" => "glm-5-2-260617",
|
|
422
|
+
"deepseek-v4-pro" => "deepseek-v4-pro-260425",
|
|
423
|
+
"deepseek-v4-flash" => "deepseek-v4-flash-260425",
|
|
424
|
+
"doubao-seed-2.1-pro" => "doubao-seed-2-1-pro-260628",
|
|
425
|
+
"doubao-seed-2.1-turbo" => "doubao-seed-2-1-turbo-260628",
|
|
426
|
+
"doubao-seed-2.0-pro" => "doubao-seed-2-0-pro-260215",
|
|
427
|
+
"doubao-seed-2.0-lite" => "doubao-seed-2-0-lite-260428",
|
|
428
|
+
"doubao-seed-2.0-code" => "doubao-seed-2-0-code-preview-260215"
|
|
429
|
+
}.freeze
|
|
430
|
+
}.freeze,
|
|
431
|
+
# Most Doubao/multimodal models accept image input; GLM-5.2 and
|
|
432
|
+
# DeepSeek-V4 on Ark are text-only.
|
|
433
|
+
"capabilities" => { "vision" => true }.freeze,
|
|
434
|
+
"model_capabilities" => {
|
|
435
|
+
"glm-5.2" => { "vision" => false }.freeze,
|
|
436
|
+
"deepseek-v4-pro" => { "vision" => false }.freeze,
|
|
437
|
+
"deepseek-v4-flash" => { "vision" => false }.freeze
|
|
438
|
+
}.freeze,
|
|
439
|
+
"default_ocr_model" => "doubao-seed-2.0-lite",
|
|
440
|
+
"website_url" => "https://console.volcengine.com/ark/region:cn-beijing/overview"
|
|
441
|
+
}.freeze,
|
|
442
|
+
|
|
379
443
|
"openai" => {
|
|
380
444
|
"name" => "OpenAI (GPT)",
|
|
381
445
|
"base_url" => "https://api.openai.com/v1",
|
|
@@ -788,7 +852,51 @@ module Clacky
|
|
|
788
852
|
nil
|
|
789
853
|
end
|
|
790
854
|
|
|
791
|
-
#
|
|
855
|
+
# Translate a display model name into the real model id the target
|
|
856
|
+
# endpoint expects. Some providers expose the same model under different
|
|
857
|
+
# ids per billing endpoint (e.g. Volcengine Ark's pay-as-you-go API needs
|
|
858
|
+
# versioned ids like "doubao-seed-2-0-pro-260215" while the Coding/Agent
|
|
859
|
+
# Plan endpoints accept the short "doubao-seed-2.0-pro"). Users always see
|
|
860
|
+
# and pick the short name; this swaps it just before the request goes out.
|
|
861
|
+
#
|
|
862
|
+
# Returns the original model unchanged when no alias applies (unknown
|
|
863
|
+
# provider, endpoint without an alias group, or model not in the map).
|
|
864
|
+
#
|
|
865
|
+
# @param base_url [String, nil] the configured base_url (identifies endpoint)
|
|
866
|
+
# @param api_key [String, nil] the configured api_key (provider fallback)
|
|
867
|
+
# @param model [String, nil] the display model name
|
|
868
|
+
# @return [String, nil] the real model id to send, or the input unchanged
|
|
869
|
+
def resolve_api_model(base_url:, api_key: nil, model:)
|
|
870
|
+
return model if model.nil?
|
|
871
|
+
|
|
872
|
+
provider_id = resolve_provider(base_url: base_url, api_key: api_key)
|
|
873
|
+
return model unless provider_id
|
|
874
|
+
|
|
875
|
+
preset = PRESETS[provider_id]
|
|
876
|
+
aliases = preset && preset["api_model_aliases"]
|
|
877
|
+
return model unless aliases.is_a?(Hash)
|
|
878
|
+
|
|
879
|
+
group = alias_group_for_base_url(preset, base_url)
|
|
880
|
+
return model unless group
|
|
881
|
+
|
|
882
|
+
aliases.dig(group, model.to_s) || model
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
# Find which alias group the given base_url belongs to by matching it
|
|
886
|
+
# against the preset's endpoint_variants. Uses exact base_url equality
|
|
887
|
+
# (after normalising trailing slash) so "/api/v3" never leaks into the
|
|
888
|
+
# "/api/coding/v3" match.
|
|
889
|
+
def alias_group_for_base_url(preset, base_url)
|
|
890
|
+
return nil if base_url.nil?
|
|
891
|
+
variants = preset["endpoint_variants"]
|
|
892
|
+
return nil unless variants.is_a?(Array)
|
|
893
|
+
|
|
894
|
+
normalized = base_url.to_s.chomp("/")
|
|
895
|
+
variant = variants.find do |v|
|
|
896
|
+
v.is_a?(Hash) && v["base_url"].to_s.chomp("/") == normalized
|
|
897
|
+
end
|
|
898
|
+
variant && variant["alias_group"]
|
|
899
|
+
end
|
|
792
900
|
#
|
|
793
901
|
# Resolution order (most specific wins):
|
|
794
902
|
# 1. PRESETS[provider_id]["model_capabilities"][model_name] — per-model
|
|
@@ -546,6 +546,7 @@ module Clacky
|
|
|
546
546
|
when ["POST", "/api/onboard/skip-soul"] then api_onboard_skip_soul(req, res)
|
|
547
547
|
when ["GET", "/api/store/skills"] then api_store_skills(res)
|
|
548
548
|
when ["GET", "/api/store/extensions"] then api_store_extensions(req, res)
|
|
549
|
+
when ["GET", "/api/store/extensions/brand"] then api_store_extensions_brand(res)
|
|
549
550
|
when ["GET", "/api/store/extensions/installed"] then api_store_extensions_installed(res)
|
|
550
551
|
when ["GET", "/api/store/extension"] then api_store_extension_detail(req, res)
|
|
551
552
|
when ["POST", "/api/store/extension/install"] then api_store_extension_install(req, res)
|
|
@@ -2312,6 +2313,9 @@ module Clacky
|
|
|
2312
2313
|
# Refresh skill_loader with the now-activated brand config so brand
|
|
2313
2314
|
# skills are loadable from this point forward (e.g. after sync).
|
|
2314
2315
|
@skill_loader = Clacky::SkillLoader.new(working_dir: nil, brand_config: brand)
|
|
2316
|
+
# Install all brand skills in the background on first activation so
|
|
2317
|
+
# they are available immediately without manual user action.
|
|
2318
|
+
brand.sync_brand_skills_async!(install_new: true)
|
|
2315
2319
|
json_response(res, 200, {
|
|
2316
2320
|
ok: true,
|
|
2317
2321
|
product_name: result[:product_name] || brand.product_name,
|
|
@@ -2362,10 +2366,9 @@ module Clacky
|
|
|
2362
2366
|
|
|
2363
2367
|
# GET /api/store/extensions?q=&sort=
|
|
2364
2368
|
#
|
|
2365
|
-
# Public extension marketplace catalog
|
|
2366
|
-
#
|
|
2367
|
-
#
|
|
2368
|
-
# — they ship inside license-gated brand packages (path B distribution).
|
|
2369
|
+
# Public extension marketplace catalog — always returns the full public
|
|
2370
|
+
# catalog regardless of brand status. Both brand and non-brand users see
|
|
2371
|
+
# the same public marketplace here.
|
|
2369
2372
|
def api_store_extensions(req, res)
|
|
2370
2373
|
brand = Clacky::BrandConfig.load
|
|
2371
2374
|
result = brand.search_extensions!(query: req.query["q"], sort: req.query["sort"])
|
|
@@ -2373,7 +2376,7 @@ module Clacky
|
|
|
2373
2376
|
if result[:success]
|
|
2374
2377
|
installed = installed_extension_containers
|
|
2375
2378
|
extensions = Array(result[:extensions]).map do |ext|
|
|
2376
|
-
slug
|
|
2379
|
+
slug = ext["name"] || ext[:name] || ext["slug"] || ext[:slug]
|
|
2377
2380
|
container = installed[slug]
|
|
2378
2381
|
ext.merge(
|
|
2379
2382
|
"installed" => !container.nil?,
|
|
@@ -2390,6 +2393,49 @@ module Clacky
|
|
|
2390
2393
|
end
|
|
2391
2394
|
end
|
|
2392
2395
|
|
|
2396
|
+
# GET /api/store/extensions/brand
|
|
2397
|
+
#
|
|
2398
|
+
# Brand-private extension catalog — only available to users with an
|
|
2399
|
+
# activated brand license. Returns extensions belonging to this brand via
|
|
2400
|
+
# BrandConfig#fetch_brand_extensions!.
|
|
2401
|
+
# Returns 403 when the license is not activated.
|
|
2402
|
+
def api_store_extensions_brand(res)
|
|
2403
|
+
brand = Clacky::BrandConfig.load
|
|
2404
|
+
|
|
2405
|
+
unless brand.activated?
|
|
2406
|
+
json_response(res, 403, { ok: false, error: "Brand license not activated." })
|
|
2407
|
+
return
|
|
2408
|
+
end
|
|
2409
|
+
|
|
2410
|
+
result = brand.fetch_brand_extensions!
|
|
2411
|
+
|
|
2412
|
+
if result[:success]
|
|
2413
|
+
installed = installed_extension_containers
|
|
2414
|
+
extensions = Array(result[:extensions]).map do |ext|
|
|
2415
|
+
slug = ext["name"] || ext[:name] || ext["slug"] || ext[:slug]
|
|
2416
|
+
container = installed[slug]
|
|
2417
|
+
# Merge local container data (e.g. emoji) for self-authored extensions.
|
|
2418
|
+
local_overrides = {}
|
|
2419
|
+
if container && ext["emoji"].to_s.empty?
|
|
2420
|
+
local_emoji = container.dig(:raw, "emoji") || container[:emoji]
|
|
2421
|
+
local_overrides["emoji"] = local_emoji if local_emoji.to_s.strip != ""
|
|
2422
|
+
end
|
|
2423
|
+
ext.merge(
|
|
2424
|
+
"installed" => !ext["installed_version"].nil?,
|
|
2425
|
+
"installed_version" => ext["installed_version"],
|
|
2426
|
+
**local_overrides
|
|
2427
|
+
)
|
|
2428
|
+
end
|
|
2429
|
+
json_response(res, 200, { ok: true, extensions: extensions })
|
|
2430
|
+
else
|
|
2431
|
+
json_response(res, 200, {
|
|
2432
|
+
ok: true,
|
|
2433
|
+
extensions: [],
|
|
2434
|
+
warning: result[:error] || "Could not reach the extension store."
|
|
2435
|
+
})
|
|
2436
|
+
end
|
|
2437
|
+
end
|
|
2438
|
+
|
|
2393
2439
|
# GET /api/store/extensions/installed
|
|
2394
2440
|
#
|
|
2395
2441
|
# Returns all locally installed extensions (all layers: builtin, installed,
|
|
@@ -2408,9 +2454,15 @@ module Clacky
|
|
|
2408
2454
|
|
|
2409
2455
|
extensions = local_entries.map do |ext_id, container|
|
|
2410
2456
|
market = market_by_slug[ext_id]
|
|
2457
|
+
# For self-authored (origin: self) extensions market is nil.
|
|
2458
|
+
# Fall back to local ext.yml data so name/description/author are populated.
|
|
2459
|
+
local_name = container[:name].to_s.then { |n| n.empty? ? ext_id : n }
|
|
2460
|
+
local_desc = container.dig(:raw, "description").to_s
|
|
2461
|
+
local_author = container[:author].to_s
|
|
2462
|
+
local_units = units_from_container(container)
|
|
2411
2463
|
{
|
|
2412
2464
|
"id" => ext_id,
|
|
2413
|
-
"name" => market ? (market["name"] || ext_id) :
|
|
2465
|
+
"name" => market ? (market["name"] || ext_id) : local_name,
|
|
2414
2466
|
"display_name" => market&.dig("display_name"),
|
|
2415
2467
|
"display_name_zh" => market&.dig("display_name_zh"),
|
|
2416
2468
|
"name_zh" => market&.dig("name_zh"),
|
|
@@ -2418,15 +2470,19 @@ module Clacky
|
|
|
2418
2470
|
"slug" => ext_id,
|
|
2419
2471
|
"version" => market ? (market["version"] || container[:version]) : container[:version],
|
|
2420
2472
|
"installed_version" => container[:version],
|
|
2421
|
-
"description" => market
|
|
2422
|
-
"author" => market
|
|
2473
|
+
"description" => market ? market["description"] : local_desc,
|
|
2474
|
+
"author" => market ? market["author"] : local_author,
|
|
2423
2475
|
"icon_url" => market&.dig("icon_url"),
|
|
2424
|
-
"units" => market
|
|
2425
|
-
"homepage" => market ? (market["homepage"] || "") :
|
|
2476
|
+
"units" => market ? market["units"] : local_units,
|
|
2477
|
+
"homepage" => market ? (market["homepage"] || "") : container[:homepage].to_s,
|
|
2426
2478
|
"origin" => market ? (market["origin"] || container[:origin]) : container[:origin],
|
|
2427
2479
|
"hub_active" => market&.dig("hub_active"),
|
|
2428
2480
|
"download_count" => market&.dig("download_count").to_i,
|
|
2429
|
-
|
|
2481
|
+
# Only mark as unlisted when the extension was published to the
|
|
2482
|
+
# marketplace (origin != "self") but is no longer listed there.
|
|
2483
|
+
# Self-authored extensions (origin: self) have never been published
|
|
2484
|
+
# and should never show the "unlisted" badge.
|
|
2485
|
+
"unlisted" => market.nil? && container[:origin].to_s != "self",
|
|
2430
2486
|
"layer" => container[:layer].to_s,
|
|
2431
2487
|
"installed" => true,
|
|
2432
2488
|
"removable" => true,
|
|
@@ -2470,6 +2526,21 @@ module Clacky
|
|
|
2470
2526
|
{}
|
|
2471
2527
|
end
|
|
2472
2528
|
|
|
2529
|
+
# Build a units summary hash from local ext.yml contributes data,
|
|
2530
|
+
# mirroring the format platform's unit_summary returns: { "api" => n, ... }
|
|
2531
|
+
private def units_from_container(container)
|
|
2532
|
+
contributes = container[:contributes] || {}
|
|
2533
|
+
units = {}
|
|
2534
|
+
units["api"] = 1 unless (contributes["api"] || contributes[:api]).to_s.empty?
|
|
2535
|
+
panels = Array(contributes["panels"] || contributes[:panels])
|
|
2536
|
+
units["panel"] = panels.size if panels.any?
|
|
2537
|
+
agents = Array(contributes["agents"] || contributes[:agents])
|
|
2538
|
+
units["agent"] = agents.size if agents.any?
|
|
2539
|
+
skills = Array(contributes["skills"] || contributes[:skills])
|
|
2540
|
+
units["skill"] = skills.size if skills.any?
|
|
2541
|
+
units
|
|
2542
|
+
end
|
|
2543
|
+
|
|
2473
2544
|
# GET /api/store/extension?id=<slug-or-id>
|
|
2474
2545
|
#
|
|
2475
2546
|
# Public detail for a single marketplace extension (contributes + version
|
|
@@ -2482,7 +2553,10 @@ module Clacky
|
|
|
2482
2553
|
end
|
|
2483
2554
|
|
|
2484
2555
|
brand = Clacky::BrandConfig.load
|
|
2485
|
-
|
|
2556
|
+
# Brand-private extensions (origin=self) are not visible via the public
|
|
2557
|
+
# /api/v1/extensions/:id endpoint. Activated brand users must use the
|
|
2558
|
+
# license-gated POST /api/v1/licenses/extension instead.
|
|
2559
|
+
result = brand.activated? ? brand.brand_extension_detail!(id) : brand.extension_detail!(id)
|
|
2486
2560
|
|
|
2487
2561
|
if result[:success] && result[:extension]
|
|
2488
2562
|
ext = result[:extension]
|
|
@@ -2499,22 +2573,29 @@ module Clacky
|
|
|
2499
2573
|
container = extension_container(id)
|
|
2500
2574
|
if container && container[:layer] == :installed
|
|
2501
2575
|
market = fetch_batch_market_data([id])[id]
|
|
2576
|
+
# Fall back to local ext.yml for self-authored extensions.
|
|
2577
|
+
local_name = container[:name].to_s.then { |n| n.empty? ? id : n }
|
|
2578
|
+
local_desc = container.dig(:raw, "description").to_s
|
|
2579
|
+
local_author = container[:author].to_s
|
|
2580
|
+
local_units = units_from_container(container)
|
|
2502
2581
|
ext = {
|
|
2503
2582
|
"id" => id,
|
|
2504
|
-
"name" => market ? (market["name"] || id) :
|
|
2583
|
+
"name" => market ? (market["name"] || id) : local_name,
|
|
2505
2584
|
"name_zh" => market&.dig("name_zh"),
|
|
2506
2585
|
"name_en" => market&.dig("name_en"),
|
|
2507
2586
|
"slug" => id,
|
|
2508
2587
|
"version" => market ? (market["version"] || container[:version]) : container[:version],
|
|
2509
2588
|
"installed_version" => container[:version],
|
|
2510
|
-
"description" => market
|
|
2511
|
-
"author" => market
|
|
2589
|
+
"description" => market ? market["description"] : local_desc,
|
|
2590
|
+
"author" => market ? market["author"] : local_author,
|
|
2512
2591
|
"icon_url" => market&.dig("icon_url"),
|
|
2513
|
-
"units" => market
|
|
2514
|
-
"homepage" => market ? (market["homepage"] || "") :
|
|
2592
|
+
"units" => market ? market["units"] : local_units,
|
|
2593
|
+
"homepage" => market ? (market["homepage"] || "") : container[:homepage].to_s,
|
|
2515
2594
|
"origin" => market ? (market["origin"] || container[:origin]) : container[:origin],
|
|
2516
2595
|
"hub_active" => market&.dig("hub_active"),
|
|
2517
|
-
|
|
2596
|
+
# Only mark as unlisted when the extension was published to the
|
|
2597
|
+
# marketplace (origin != "self") but is no longer listed there.
|
|
2598
|
+
"unlisted" => market.nil? && container[:origin].to_s != "self",
|
|
2518
2599
|
"installed" => true,
|
|
2519
2600
|
"removable" => true,
|
|
2520
2601
|
"disabled" => container[:disabled] == true,
|
|
@@ -2582,7 +2663,9 @@ module Clacky
|
|
|
2582
2663
|
end
|
|
2583
2664
|
|
|
2584
2665
|
def api_store_extension_uninstall(req, res)
|
|
2585
|
-
|
|
2666
|
+
body = parse_json_body(req)
|
|
2667
|
+
id = body["id"].to_s
|
|
2668
|
+
purge_data = body["purge_data"] == true
|
|
2586
2669
|
container = extension_container(id)
|
|
2587
2670
|
if container.nil?
|
|
2588
2671
|
json_response(res, 404, { ok: false, error: "Not installed." })
|
|
@@ -2593,7 +2676,7 @@ module Clacky
|
|
|
2593
2676
|
return
|
|
2594
2677
|
end
|
|
2595
2678
|
|
|
2596
|
-
if Clacky::ExtensionLoader.uninstall!(id)
|
|
2679
|
+
if Clacky::ExtensionLoader.uninstall!(id, purge_data: purge_data)
|
|
2597
2680
|
json_response(res, 200, { ok: true, id: id })
|
|
2598
2681
|
else
|
|
2599
2682
|
json_response(res, 404, { ok: false, error: "Not installed." })
|
|
@@ -5725,6 +5808,9 @@ module Clacky
|
|
|
5725
5808
|
{
|
|
5726
5809
|
id: id,
|
|
5727
5810
|
name: preset["name"],
|
|
5811
|
+
# Optional i18n key for the display name (localised per UI language);
|
|
5812
|
+
# frontend falls back to `name` when absent or untranslated.
|
|
5813
|
+
name_key: preset["name_key"],
|
|
5728
5814
|
base_url: preset["base_url"],
|
|
5729
5815
|
default_model: preset["default_model"],
|
|
5730
5816
|
models: preset["models"] || [],
|
|
@@ -6424,6 +6510,12 @@ module Clacky
|
|
|
6424
6510
|
private def interrupt_all_agents
|
|
6425
6511
|
return unless @registry && @session_manager
|
|
6426
6512
|
|
|
6513
|
+
# Stop idle compression first: an in-flight compression must fully roll
|
|
6514
|
+
# back (or complete) before the worker is killed, otherwise a hot
|
|
6515
|
+
# restart's SIGKILL can leave a chunk file on disk whose chunk_path
|
|
6516
|
+
# never made it into session.json (history vanishes on replay).
|
|
6517
|
+
@registry.shutdown_all_idle_timers
|
|
6518
|
+
|
|
6427
6519
|
@registry.each_live_agent do |id, agent, thread|
|
|
6428
6520
|
next unless thread&.alive?
|
|
6429
6521
|
begin
|
|
@@ -496,6 +496,15 @@ module Clacky
|
|
|
496
496
|
snapshot.each { |id, agent, thread| yield id, agent, thread }
|
|
497
497
|
end
|
|
498
498
|
|
|
499
|
+
# Shut down every session's idle-compression timer, waiting for any
|
|
500
|
+
# in-flight compression to roll back cleanly. Called on worker shutdown
|
|
501
|
+
# so a hot restart's SIGKILL cannot tear a compression apart between
|
|
502
|
+
# writing the chunk file and persisting its chunk_path into session.json.
|
|
503
|
+
def shutdown_all_idle_timers
|
|
504
|
+
timers = @mutex.synchronize { @sessions.values.filter_map { |s| s[:idle_timer] } }
|
|
505
|
+
timers.each { |t| t.shutdown rescue nil }
|
|
506
|
+
end
|
|
507
|
+
|
|
499
508
|
private def persist_and_release(id, session)
|
|
500
509
|
agent = session[:agent]
|
|
501
510
|
@session_manager&.save(agent.to_session_data(status: :success, preserve_updated_at: true)) if agent
|
|
@@ -286,6 +286,42 @@ module Clacky
|
|
|
286
286
|
}
|
|
287
287
|
},
|
|
288
288
|
|
|
289
|
+
# Volcengine Ark (Doubao) — priced in CNY, converted at 1 USD = 6.7730 CNY
|
|
290
|
+
# (2026-07-23) and rounded up to the cent. Ark tiers by input length in K
|
|
291
|
+
# tokens; we map (32,128] → default and (128,256] → over_200k, matching
|
|
292
|
+
# the coarse two-tier structure used elsewhere. Single-tier [0,256] models
|
|
293
|
+
# use the same value for both. cache.write bills at the miss (input) rate.
|
|
294
|
+
"doubao-seed-evolving" => {
|
|
295
|
+
input: { default: 0.89, over_200k: 0.89 },
|
|
296
|
+
output: { default: 4.43, over_200k: 4.43 },
|
|
297
|
+
cache: { write: 0.89, read: 0.18 }
|
|
298
|
+
},
|
|
299
|
+
"doubao-seed-2.1-pro" => {
|
|
300
|
+
input: { default: 0.89, over_200k: 0.89 },
|
|
301
|
+
output: { default: 4.43, over_200k: 4.43 },
|
|
302
|
+
cache: { write: 0.89, read: 0.18 }
|
|
303
|
+
},
|
|
304
|
+
"doubao-seed-2.1-turbo" => {
|
|
305
|
+
input: { default: 0.45, over_200k: 0.45 },
|
|
306
|
+
output: { default: 2.22, over_200k: 2.22 },
|
|
307
|
+
cache: { write: 0.45, read: 0.09 }
|
|
308
|
+
},
|
|
309
|
+
"doubao-seed-2.0-pro" => {
|
|
310
|
+
input: { default: 0.71, over_200k: 1.42 },
|
|
311
|
+
output: { default: 3.55, over_200k: 7.09 },
|
|
312
|
+
cache: { write: 0.71, read: 0.15 }
|
|
313
|
+
},
|
|
314
|
+
"doubao-seed-2.0-code" => {
|
|
315
|
+
input: { default: 0.71, over_200k: 1.42 },
|
|
316
|
+
output: { default: 3.55, over_200k: 7.09 },
|
|
317
|
+
cache: { write: 0.71, read: 0.15 }
|
|
318
|
+
},
|
|
319
|
+
"doubao-seed-2.0-lite" => {
|
|
320
|
+
input: { default: 0.14, over_200k: 0.27 },
|
|
321
|
+
output: { default: 0.80, over_200k: 1.60 },
|
|
322
|
+
cache: { write: 0.14, read: 0.03 }
|
|
323
|
+
},
|
|
324
|
+
|
|
289
325
|
# Google Gemini 3 series (via Vertex AI). Tiered at 200K input tokens
|
|
290
326
|
# for Pro; Flash has flat pricing.
|
|
291
327
|
"gemini-3.1-pro" => {
|
data/lib/clacky/version.rb
CHANGED