openclacky 1.3.9 → 1.3.11
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 +40 -0
- data/lib/clacky/brand_config.rb +32 -6
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +47 -1
- data/lib/clacky/default_extensions/ext-studio/ext.yml +2 -2
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +167 -80
- data/lib/clacky/extension/packager.rb +2 -3
- data/lib/clacky/locales/en.rb +1 -1
- data/lib/clacky/locales/zh.rb +1 -1
- data/lib/clacky/prompts/base.md +1 -1
- data/lib/clacky/server/http_server.rb +119 -13
- data/lib/clacky/telemetry.rb +9 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +99 -14
- data/lib/clacky/web/features/extensions/store.js +34 -9
- data/lib/clacky/web/features/extensions/view.js +47 -11
- data/lib/clacky/web/features/new-session/view.js +1 -1
- data/lib/clacky/web/i18n.js +18 -6
- data/lib/clacky/web/sessions.js +32 -2
- metadata +1 -1
|
@@ -66,9 +66,8 @@ module Clacky
|
|
|
66
66
|
raise Error, "extension #{ext_id.inspect} already installed at #{target} (pass force: true to overwrite)"
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
-
FileUtils.mkdir_p(
|
|
70
|
-
FileUtils.
|
|
71
|
-
FileUtils.cp_r(container_src, target)
|
|
69
|
+
FileUtils.mkdir_p(target)
|
|
70
|
+
FileUtils.cp_r(Dir.glob("#{container_src}/*"), target)
|
|
72
71
|
|
|
73
72
|
Clacky::ExtensionLoader.invalidate_cache!
|
|
74
73
|
units = verify_installed(ext_id, installed_dir)
|
data/lib/clacky/locales/en.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Clacky
|
|
|
5
5
|
EN = {
|
|
6
6
|
"llm.error.insufficient_credit" => "Insufficient credit, please top up your account to continue",
|
|
7
7
|
"llm.error.rate_limit_400" => "Rate limit or service issue, retrying...",
|
|
8
|
-
"llm.error.invalid_api_key" => "
|
|
8
|
+
"llm.error.invalid_api_key" => "API key is invalid or expired, please update it in Settings",
|
|
9
9
|
"llm.error.403.model_not_allowed" => "This model is not available on your current plan",
|
|
10
10
|
"llm.error.403.api_key_revoked" => "API key has been revoked, please generate a new one",
|
|
11
11
|
"llm.error.403.api_key_expired" => "API key has expired, please generate a new one",
|
data/lib/clacky/locales/zh.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Clacky
|
|
|
5
5
|
ZH = {
|
|
6
6
|
"llm.error.insufficient_credit" => "账户余额不足,请前往控制台充值后继续使用",
|
|
7
7
|
"llm.error.rate_limit_400" => "请求频率过高或服务暂时不可用,正在重试...",
|
|
8
|
-
"llm.error.invalid_api_key" => "API
|
|
8
|
+
"llm.error.invalid_api_key" => "API Key 无效或已过期,请到设置中重新配置",
|
|
9
9
|
"llm.error.403.model_not_allowed" => "当前模型不支持免费试用,请升级套餐或切换其他模型",
|
|
10
10
|
"llm.error.403.api_key_revoked" => "API 密钥已被撤销,请前往控制台重新生成",
|
|
11
11
|
"llm.error.403.api_key_expired" => "API 密钥已过期,请前往控制台重新生成",
|
data/lib/clacky/prompts/base.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
- Ask clarifying questions if requirements are unclear.
|
|
4
4
|
- Break down complex tasks into manageable steps.
|
|
5
5
|
- **USE TOOLS to create/modify files** — don't just return content.
|
|
6
|
-
- When the user asks to send/download a file or you generate one for them, append `[filename](file://~/path/to/file)` at the end of your reply.
|
|
6
|
+
- When the user asks to send/download a file or you generate one for them, append `[filename](file://~/path/to/file)` at the end of your reply. Use the raw file path as-is (keep non-ASCII characters and spaces literal); never URL-encode it.
|
|
7
7
|
|
|
8
8
|
## Tool Usage Rules
|
|
9
9
|
|
|
@@ -117,6 +117,7 @@ module Clacky
|
|
|
117
117
|
WEB_ROOT = File.expand_path("../web", __dir__)
|
|
118
118
|
EXCHANGE_RATE_PRIMARY_BASE_URL = "https://open.er-api.com/v6/latest"
|
|
119
119
|
EXCHANGE_RATE_FALLBACK_URL = "https://api.frankfurter.app/latest"
|
|
120
|
+
OSS_CDN_BASE = "https://oss.1024code.com/openclacky"
|
|
120
121
|
|
|
121
122
|
# Default SOUL.md written when the user skips the onboard conversation.
|
|
122
123
|
# A richer version is created by the Agent during the soul_setup phase.
|
|
@@ -476,8 +477,10 @@ module Clacky
|
|
|
476
477
|
elsif path.start_with?("/api/backup/download") || path == "/api/backup/run" || path == "/api/backup/restore"
|
|
477
478
|
# Building/extracting a tar.gz of ~/.clacky can take a while.
|
|
478
479
|
120
|
|
480
|
+
elsif path == "/api/store/extension/install"
|
|
481
|
+
300
|
|
479
482
|
else
|
|
480
|
-
|
|
483
|
+
30
|
|
481
484
|
end
|
|
482
485
|
Timeout.timeout(timeout_sec) do
|
|
483
486
|
_dispatch_rest(req, res)
|
|
@@ -538,7 +541,8 @@ module Clacky
|
|
|
538
541
|
when ["POST", "/api/onboard/complete"] then api_onboard_complete(req, res)
|
|
539
542
|
when ["POST", "/api/onboard/skip-soul"] then api_onboard_skip_soul(req, res)
|
|
540
543
|
when ["GET", "/api/store/skills"] then api_store_skills(res)
|
|
541
|
-
when ["GET", "/api/store/extensions"]
|
|
544
|
+
when ["GET", "/api/store/extensions"] then api_store_extensions(req, res)
|
|
545
|
+
when ["GET", "/api/store/extensions/installed"] then api_store_extensions_installed(res)
|
|
542
546
|
when ["GET", "/api/store/extension"] then api_store_extension_detail(req, res)
|
|
543
547
|
when ["POST", "/api/store/extension/install"] then api_store_extension_install(req, res)
|
|
544
548
|
when ["POST", "/api/store/extension/disable"] then api_store_extension_disable(req, res)
|
|
@@ -2332,10 +2336,14 @@ module Clacky
|
|
|
2332
2336
|
result = brand.search_extensions!(query: req.query["q"], sort: req.query["sort"])
|
|
2333
2337
|
|
|
2334
2338
|
if result[:success]
|
|
2335
|
-
installed =
|
|
2339
|
+
installed = installed_extension_containers
|
|
2336
2340
|
extensions = Array(result[:extensions]).map do |ext|
|
|
2337
2341
|
slug = ext["name"] || ext[:name] || ext["slug"] || ext[:slug]
|
|
2338
|
-
|
|
2342
|
+
container = installed[slug]
|
|
2343
|
+
ext.merge(
|
|
2344
|
+
"installed" => !container.nil?,
|
|
2345
|
+
"installed_version" => container&.dig(:version)
|
|
2346
|
+
)
|
|
2339
2347
|
end
|
|
2340
2348
|
json_response(res, 200, { ok: true, extensions: extensions })
|
|
2341
2349
|
else
|
|
@@ -2347,6 +2355,68 @@ module Clacky
|
|
|
2347
2355
|
end
|
|
2348
2356
|
end
|
|
2349
2357
|
|
|
2358
|
+
# GET /api/store/extensions/installed
|
|
2359
|
+
#
|
|
2360
|
+
# Returns all locally installed extensions (all layers: builtin, installed,
|
|
2361
|
+
# local) regardless of whether they are still listed on the marketplace.
|
|
2362
|
+
def api_store_extensions_installed(res)
|
|
2363
|
+
result = Clacky::ExtensionLoader.load_all
|
|
2364
|
+
disabled = Clacky::ExtensionLoader.disabled_ids
|
|
2365
|
+
|
|
2366
|
+
local_entries = Array(result&.containers).filter_map do |ext_id, container|
|
|
2367
|
+
next unless container[:layer] == :installed
|
|
2368
|
+
|
|
2369
|
+
[ext_id, container]
|
|
2370
|
+
end.to_h
|
|
2371
|
+
|
|
2372
|
+
market_by_slug = fetch_batch_market_data(local_entries.keys)
|
|
2373
|
+
|
|
2374
|
+
extensions = local_entries.map do |ext_id, container|
|
|
2375
|
+
market = market_by_slug[ext_id]
|
|
2376
|
+
{
|
|
2377
|
+
"id" => ext_id,
|
|
2378
|
+
"name" => market ? (market["name"] || ext_id) : ext_id,
|
|
2379
|
+
"name_zh" => market&.dig("name_zh"),
|
|
2380
|
+
"name_en" => market&.dig("name_en"),
|
|
2381
|
+
"slug" => ext_id,
|
|
2382
|
+
"version" => market ? (market["version"] || container[:version]) : container[:version],
|
|
2383
|
+
"installed_version" => container[:version],
|
|
2384
|
+
"description" => market&.dig("description"),
|
|
2385
|
+
"author" => market&.dig("author"),
|
|
2386
|
+
"icon_url" => market&.dig("icon_url"),
|
|
2387
|
+
"units" => market&.dig("units"),
|
|
2388
|
+
"homepage" => market ? (market["homepage"] || "") : "",
|
|
2389
|
+
"origin" => market ? (market["origin"] || container[:origin]) : container[:origin],
|
|
2390
|
+
"hub_active" => market&.dig("hub_active"),
|
|
2391
|
+
"download_count" => market&.dig("download_count").to_i,
|
|
2392
|
+
"unlisted" => market.nil?,
|
|
2393
|
+
"layer" => container[:layer].to_s,
|
|
2394
|
+
"installed" => true,
|
|
2395
|
+
"removable" => true,
|
|
2396
|
+
"disabled" => disabled.include?(ext_id),
|
|
2397
|
+
}
|
|
2398
|
+
end
|
|
2399
|
+
|
|
2400
|
+
json_response(res, 200, { ok: true, extensions: extensions })
|
|
2401
|
+
rescue StandardError => e
|
|
2402
|
+
json_response(res, 500, { ok: false, error: e.message })
|
|
2403
|
+
end
|
|
2404
|
+
|
|
2405
|
+
private def fetch_batch_market_data(slugs)
|
|
2406
|
+
return {} if slugs.empty?
|
|
2407
|
+
|
|
2408
|
+
client = Clacky::PlatformHttpClient.new
|
|
2409
|
+
slugs.each_slice(50).each_with_object({}) do |batch, result|
|
|
2410
|
+
ids_param = batch.join(",")
|
|
2411
|
+
response = client.get("/api/v1/extensions/batch?ids=#{ids_param}")
|
|
2412
|
+
next unless response[:success]
|
|
2413
|
+
|
|
2414
|
+
Array(response.dig(:data, "extensions")).each { |ext| result[ext["name"]] = ext }
|
|
2415
|
+
end
|
|
2416
|
+
rescue StandardError
|
|
2417
|
+
{}
|
|
2418
|
+
end
|
|
2419
|
+
|
|
2350
2420
|
# Slugs of every extension container currently loaded (any layer), used to
|
|
2351
2421
|
# flag "installed" on the public marketplace catalog.
|
|
2352
2422
|
def installed_extension_slugs
|
|
@@ -2356,6 +2426,13 @@ module Clacky
|
|
|
2356
2426
|
Set.new
|
|
2357
2427
|
end
|
|
2358
2428
|
|
|
2429
|
+
def installed_extension_containers
|
|
2430
|
+
result = Clacky::ExtensionLoader.load_all
|
|
2431
|
+
Array(result&.containers).filter_map { |id, c| [id, c] if c[:layer] == :installed }.to_h
|
|
2432
|
+
rescue StandardError
|
|
2433
|
+
{}
|
|
2434
|
+
end
|
|
2435
|
+
|
|
2359
2436
|
# GET /api/store/extension?id=<slug-or-id>
|
|
2360
2437
|
#
|
|
2361
2438
|
# Public detail for a single marketplace extension (contributes + version
|
|
@@ -2375,13 +2452,40 @@ module Clacky
|
|
|
2375
2452
|
slug = ext["name"] || ext[:name] || ext["slug"] || ext[:slug]
|
|
2376
2453
|
container = extension_container(slug)
|
|
2377
2454
|
ext = ext.merge(
|
|
2378
|
-
"installed"
|
|
2379
|
-
"
|
|
2380
|
-
"
|
|
2455
|
+
"installed" => !container.nil?,
|
|
2456
|
+
"installed_version" => container&.dig(:version),
|
|
2457
|
+
"removable" => container && container[:layer] == :installed,
|
|
2458
|
+
"disabled" => container ? container[:disabled] == true : false
|
|
2381
2459
|
)
|
|
2382
2460
|
json_response(res, 200, { ok: true, extension: ext })
|
|
2383
2461
|
else
|
|
2384
|
-
|
|
2462
|
+
container = extension_container(id)
|
|
2463
|
+
if container && container[:layer] == :installed
|
|
2464
|
+
market = fetch_batch_market_data([id])[id]
|
|
2465
|
+
ext = {
|
|
2466
|
+
"id" => id,
|
|
2467
|
+
"name" => market ? (market["name"] || id) : id,
|
|
2468
|
+
"name_zh" => market&.dig("name_zh"),
|
|
2469
|
+
"name_en" => market&.dig("name_en"),
|
|
2470
|
+
"slug" => id,
|
|
2471
|
+
"version" => market ? (market["version"] || container[:version]) : container[:version],
|
|
2472
|
+
"installed_version" => container[:version],
|
|
2473
|
+
"description" => market&.dig("description"),
|
|
2474
|
+
"author" => market&.dig("author"),
|
|
2475
|
+
"icon_url" => market&.dig("icon_url"),
|
|
2476
|
+
"units" => market&.dig("units"),
|
|
2477
|
+
"homepage" => market ? (market["homepage"] || "") : "",
|
|
2478
|
+
"origin" => market ? (market["origin"] || container[:origin]) : container[:origin],
|
|
2479
|
+
"hub_active" => market&.dig("hub_active"),
|
|
2480
|
+
"unlisted" => market.nil?,
|
|
2481
|
+
"installed" => true,
|
|
2482
|
+
"removable" => true,
|
|
2483
|
+
"disabled" => container[:disabled] == true,
|
|
2484
|
+
}
|
|
2485
|
+
json_response(res, 200, { ok: true, extension: ext })
|
|
2486
|
+
else
|
|
2487
|
+
json_response(res, 404, { ok: false, error: result[:error] || "Not found" })
|
|
2488
|
+
end
|
|
2385
2489
|
end
|
|
2386
2490
|
end
|
|
2387
2491
|
|
|
@@ -2432,6 +2536,7 @@ module Clacky
|
|
|
2432
2536
|
|
|
2433
2537
|
Clacky::ExtensionPackager.install(download_url, force: true)
|
|
2434
2538
|
Clacky::ExtensionLoader.invalidate_cache!
|
|
2539
|
+
Clacky::Telemetry.extension_install!(name) unless name.empty?
|
|
2435
2540
|
json_response(res, 200, { ok: true, name: name })
|
|
2436
2541
|
rescue Clacky::ExtensionPackager::Error => e
|
|
2437
2542
|
json_response(res, 422, { ok: false, error: e.message })
|
|
@@ -2899,7 +3004,7 @@ module Clacky
|
|
|
2899
3004
|
require "net/http"
|
|
2900
3005
|
require "uri"
|
|
2901
3006
|
|
|
2902
|
-
oss_base =
|
|
3007
|
+
oss_base = OSS_CDN_BASE
|
|
2903
3008
|
latest_url = "#{oss_base}/latest.txt"
|
|
2904
3009
|
|
|
2905
3010
|
Clacky::Logger.info("[Upgrade] Non-official source — fetching latest version from OSS CDN")
|
|
@@ -3071,11 +3176,12 @@ module Clacky
|
|
|
3071
3176
|
end
|
|
3072
3177
|
|
|
3073
3178
|
# Query the latest openclacky version.
|
|
3074
|
-
# Strategy:
|
|
3075
|
-
# then fall back to `gem list -r`
|
|
3076
|
-
# Uses Terminal (PTY + login shell) so rbenv/mise shims and gem mirrors work correctly.
|
|
3179
|
+
# Strategy: OSS CDN latest.txt first (fast, CDN-accelerated), then RubyGems API,
|
|
3180
|
+
# then fall back to `gem list -r` as a last resort.
|
|
3077
3181
|
private def fetch_latest_version_from_gem
|
|
3078
|
-
|
|
3182
|
+
fetch_oss_latest_version("#{OSS_CDN_BASE}/latest.txt") ||
|
|
3183
|
+
fetch_latest_version_from_rubygems_api ||
|
|
3184
|
+
fetch_latest_version_from_gem_command
|
|
3079
3185
|
end
|
|
3080
3186
|
|
|
3081
3187
|
# Try RubyGems official REST API — fast and always up-to-date.
|
data/lib/clacky/telemetry.rb
CHANGED
|
@@ -91,6 +91,15 @@ module Clacky
|
|
|
91
91
|
fire_and_forget("/api/v1/telemetry/share", payload.compact)
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
+
# Called after a marketplace extension is successfully installed.
|
|
95
|
+
# Increments the extension's download_count on the platform.
|
|
96
|
+
def extension_install!(slug)
|
|
97
|
+
return unless enabled?
|
|
98
|
+
return if slug.to_s.strip.empty?
|
|
99
|
+
|
|
100
|
+
fire_and_forget("/api/v1/telemetry/extension_install", { slug: slug })
|
|
101
|
+
end
|
|
102
|
+
|
|
94
103
|
# ── private helpers ────────────────────────────────────────────────
|
|
95
104
|
|
|
96
105
|
private def enabled?
|
data/lib/clacky/version.rb
CHANGED
data/lib/clacky/web/app.css
CHANGED
|
@@ -1874,9 +1874,7 @@ body {
|
|
|
1874
1874
|
flex-direction: column;
|
|
1875
1875
|
gap: 1.25rem;
|
|
1876
1876
|
width: 100%;
|
|
1877
|
-
max-
|
|
1878
|
-
margin: 0 auto;
|
|
1879
|
-
padding: 5.5rem 1.5rem 1rem;
|
|
1877
|
+
padding: 6rem max(1.5rem, calc(50% - 30rem)) 1rem;
|
|
1880
1878
|
}
|
|
1881
1879
|
.new-session-composer {
|
|
1882
1880
|
position: relative;
|
|
@@ -2007,6 +2005,11 @@ body {
|
|
|
2007
2005
|
font-size: 0.78125rem;
|
|
2008
2006
|
color: var(--color-text-tertiary);
|
|
2009
2007
|
line-height: 1.4;
|
|
2008
|
+
flex: 1;
|
|
2009
|
+
overflow: hidden;
|
|
2010
|
+
display: -webkit-box;
|
|
2011
|
+
-webkit-line-clamp: 2;
|
|
2012
|
+
-webkit-box-orient: vertical;
|
|
2010
2013
|
}
|
|
2011
2014
|
.agent-card-badge {
|
|
2012
2015
|
position: absolute;
|
|
@@ -2026,7 +2029,6 @@ body {
|
|
|
2026
2029
|
color: var(--color-accent-primary);
|
|
2027
2030
|
}
|
|
2028
2031
|
.agent-card-author {
|
|
2029
|
-
margin-top: 0.25rem;
|
|
2030
2032
|
font-size: 0.72rem;
|
|
2031
2033
|
color: var(--color-text-tertiary);
|
|
2032
2034
|
opacity: 0.8;
|
|
@@ -7070,6 +7072,12 @@ body {
|
|
|
7070
7072
|
}
|
|
7071
7073
|
|
|
7072
7074
|
/* ── Extension Market ─────────────────────────────────────────────────── */
|
|
7075
|
+
#extensions-panel {
|
|
7076
|
+
flex: 1;
|
|
7077
|
+
display: flex;
|
|
7078
|
+
flex-direction: column;
|
|
7079
|
+
overflow: hidden;
|
|
7080
|
+
}
|
|
7073
7081
|
#extensions-body {
|
|
7074
7082
|
flex: 1;
|
|
7075
7083
|
overflow-y: auto;
|
|
@@ -7169,7 +7177,7 @@ body {
|
|
|
7169
7177
|
.extension-card:hover { border-color: var(--color-text-muted); }
|
|
7170
7178
|
.extension-card-main {
|
|
7171
7179
|
display: flex;
|
|
7172
|
-
align-items:
|
|
7180
|
+
align-items: center;
|
|
7173
7181
|
gap: 0.75rem;
|
|
7174
7182
|
}
|
|
7175
7183
|
.extension-emoji {
|
|
@@ -7181,7 +7189,7 @@ body {
|
|
|
7181
7189
|
.extension-title {
|
|
7182
7190
|
display: flex;
|
|
7183
7191
|
align-items: center;
|
|
7184
|
-
gap: 0.
|
|
7192
|
+
gap: 0.5rem;
|
|
7185
7193
|
margin-bottom: 0.25rem;
|
|
7186
7194
|
flex-wrap: wrap;
|
|
7187
7195
|
}
|
|
@@ -7195,17 +7203,31 @@ body {
|
|
|
7195
7203
|
color: var(--color-text-secondary);
|
|
7196
7204
|
background: var(--color-bg-tertiary, rgba(255,255,255,0.05));
|
|
7197
7205
|
border-radius: 4px;
|
|
7198
|
-
padding: 0.0625rem
|
|
7206
|
+
padding: 0.0625rem 0.375rem;
|
|
7199
7207
|
}
|
|
7200
7208
|
.extension-units {
|
|
7201
7209
|
font-size: 0.6875rem;
|
|
7202
7210
|
color: var(--color-text-muted);
|
|
7203
7211
|
}
|
|
7204
7212
|
.extension-installed {
|
|
7205
|
-
font-size: 0.
|
|
7213
|
+
font-size: 0.625rem;
|
|
7206
7214
|
color: var(--color-success, #16a34a);
|
|
7207
7215
|
background: var(--color-success-bg, rgba(22,163,74,0.12));
|
|
7208
7216
|
border-radius: 4px;
|
|
7217
|
+
padding: 0.2rem 0.375rem;
|
|
7218
|
+
}
|
|
7219
|
+
.extension-updatable {
|
|
7220
|
+
font-size: 0.625rem;
|
|
7221
|
+
color: var(--color-button-primary);
|
|
7222
|
+
background: color-mix(in srgb, var(--color-button-primary) 12%, transparent);
|
|
7223
|
+
border-radius: 4px;
|
|
7224
|
+
padding: 0.2rem 0.375rem;
|
|
7225
|
+
}
|
|
7226
|
+
.extension-unlisted {
|
|
7227
|
+
font-size: 0.6875rem;
|
|
7228
|
+
color: var(--color-text-secondary);
|
|
7229
|
+
background: var(--color-surface-2, rgba(0,0,0,0.06));
|
|
7230
|
+
border-radius: 4px;
|
|
7209
7231
|
padding: 0.0625rem 0.375rem;
|
|
7210
7232
|
}
|
|
7211
7233
|
.extension-desc {
|
|
@@ -7222,6 +7244,16 @@ body {
|
|
|
7222
7244
|
font-size: 0.6875rem;
|
|
7223
7245
|
color: var(--color-text-muted);
|
|
7224
7246
|
}
|
|
7247
|
+
.extension-author {
|
|
7248
|
+
font-size: 0.75rem;
|
|
7249
|
+
color: var(--color-text-muted);
|
|
7250
|
+
font-weight: 400;
|
|
7251
|
+
}
|
|
7252
|
+
.extension-installs {
|
|
7253
|
+
font-size: 0.75rem;
|
|
7254
|
+
color: var(--color-text-muted);
|
|
7255
|
+
font-weight: 400;
|
|
7256
|
+
}
|
|
7225
7257
|
.extension-meta {
|
|
7226
7258
|
display: flex;
|
|
7227
7259
|
align-items: center;
|
|
@@ -7324,26 +7356,67 @@ body {
|
|
|
7324
7356
|
margin-top: 0.125rem;
|
|
7325
7357
|
}
|
|
7326
7358
|
.extension-version-item {
|
|
7327
|
-
padding: 0.
|
|
7359
|
+
padding: 0.75rem 1rem;
|
|
7328
7360
|
border-bottom: 1px solid var(--color-border-primary);
|
|
7329
7361
|
}
|
|
7330
7362
|
.extension-version-item:last-child { border-bottom: none; }
|
|
7331
7363
|
.extension-version-row {
|
|
7332
7364
|
display: flex;
|
|
7333
|
-
align-items:
|
|
7365
|
+
align-items: baseline;
|
|
7334
7366
|
gap: 0.5rem;
|
|
7335
7367
|
}
|
|
7368
|
+
.extension-version-item:has(.extension-version-notes) .extension-version-row {
|
|
7369
|
+
padding-bottom: 0.5rem;
|
|
7370
|
+
margin-bottom: 0.5rem;
|
|
7371
|
+
border-bottom: 1px solid var(--color-border-primary);
|
|
7372
|
+
}
|
|
7373
|
+
.extension-version-row .extension-version {
|
|
7374
|
+
font-size: 0.9375rem;
|
|
7375
|
+
font-weight: 700;
|
|
7376
|
+
color: var(--color-text-primary);
|
|
7377
|
+
background: none;
|
|
7378
|
+
padding: 0;
|
|
7379
|
+
border-radius: 0;
|
|
7380
|
+
}
|
|
7336
7381
|
.extension-version-date {
|
|
7337
7382
|
font-size: 0.6875rem;
|
|
7338
7383
|
color: var(--color-text-muted);
|
|
7339
7384
|
}
|
|
7340
7385
|
.extension-version-notes {
|
|
7341
|
-
font-size: 0.
|
|
7386
|
+
font-size: 0.8125rem;
|
|
7342
7387
|
color: var(--color-text-secondary);
|
|
7343
|
-
line-height: 1.
|
|
7344
|
-
margin-top: 0.
|
|
7345
|
-
white-space: pre-wrap;
|
|
7388
|
+
line-height: 1.6;
|
|
7389
|
+
margin-top: 0.5rem;
|
|
7346
7390
|
}
|
|
7391
|
+
.extension-version-notes h1,
|
|
7392
|
+
.extension-version-notes h2,
|
|
7393
|
+
.extension-version-notes h3 {
|
|
7394
|
+
font-size: 0.8125rem;
|
|
7395
|
+
font-weight: 600;
|
|
7396
|
+
color: var(--color-text-primary);
|
|
7397
|
+
margin: 0.75rem 0 0.25rem;
|
|
7398
|
+
}
|
|
7399
|
+
.extension-version-notes h1 {
|
|
7400
|
+
font-size: 0.9375rem;
|
|
7401
|
+
}
|
|
7402
|
+
.extension-version-notes h1:first-child,
|
|
7403
|
+
.extension-version-notes h2:first-child,
|
|
7404
|
+
.extension-version-notes h3:first-child {
|
|
7405
|
+
margin-top: 0;
|
|
7406
|
+
}
|
|
7407
|
+
.extension-version-notes ul,
|
|
7408
|
+
.extension-version-notes ol {
|
|
7409
|
+
margin: 0.25rem 0;
|
|
7410
|
+
padding-left: 1.25rem;
|
|
7411
|
+
}
|
|
7412
|
+
.extension-version-notes li {
|
|
7413
|
+
margin: 0.1rem 0;
|
|
7414
|
+
}
|
|
7415
|
+
.extension-version-notes p {
|
|
7416
|
+
margin: 0.25rem 0;
|
|
7417
|
+
}
|
|
7418
|
+
.extension-version-notes p:first-child { margin-top: 0; }
|
|
7419
|
+
.extension-version-notes p:last-child { margin-bottom: 0; }
|
|
7347
7420
|
.extension-detail-loading { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
7348
7421
|
|
|
7349
7422
|
.extension-detail-actions {
|
|
@@ -7385,6 +7458,18 @@ body {
|
|
|
7385
7458
|
opacity: 0.5;
|
|
7386
7459
|
cursor: not-allowed;
|
|
7387
7460
|
}
|
|
7461
|
+
.extension-action-update {
|
|
7462
|
+
background: var(--color-button-primary);
|
|
7463
|
+
color: var(--color-button-primary-text);
|
|
7464
|
+
border-color: transparent;
|
|
7465
|
+
}
|
|
7466
|
+
.extension-action-update:hover {
|
|
7467
|
+
background: var(--color-button-primary-hover);
|
|
7468
|
+
}
|
|
7469
|
+
.extension-action-update:disabled {
|
|
7470
|
+
opacity: 0.5;
|
|
7471
|
+
cursor: not-allowed;
|
|
7472
|
+
}
|
|
7388
7473
|
.extension-action-disable,
|
|
7389
7474
|
.extension-action-enable {
|
|
7390
7475
|
color: var(--color-text-tertiary);
|
|
@@ -66,6 +66,7 @@ const ExtensionsStore = (() => {
|
|
|
66
66
|
|
|
67
67
|
/** Fetch the catalog from the server for the current query + sort. */
|
|
68
68
|
async load() {
|
|
69
|
+
if (_filterInstalled) return;
|
|
69
70
|
_loading = true;
|
|
70
71
|
_error = null;
|
|
71
72
|
_emit("extensions:loading");
|
|
@@ -77,9 +78,9 @@ const ExtensionsStore = (() => {
|
|
|
77
78
|
const res = await fetch("/api/store/extensions" + (qs ? "?" + qs : ""));
|
|
78
79
|
const data = await res.json();
|
|
79
80
|
_allExtensions = data.extensions || [];
|
|
80
|
-
_extensions = _filterInstalled ? _allExtensions.filter(e => e.installed) : _allExtensions;
|
|
81
81
|
_error = data.warning || null;
|
|
82
82
|
_loading = false;
|
|
83
|
+
_extensions = _allExtensions;
|
|
83
84
|
_emit("extensions:changed", { extensions: _extensions, warning: _error });
|
|
84
85
|
} catch (e) {
|
|
85
86
|
console.error("[Extensions] load failed", e);
|
|
@@ -95,6 +96,7 @@ const ExtensionsStore = (() => {
|
|
|
95
96
|
/** Set the search text and reload. */
|
|
96
97
|
setQuery(query) {
|
|
97
98
|
_query = (query || "").trim();
|
|
99
|
+
if (_filterInstalled) return Extensions.setFilterInstalled(true);
|
|
98
100
|
return Extensions.load();
|
|
99
101
|
},
|
|
100
102
|
|
|
@@ -104,11 +106,30 @@ const ExtensionsStore = (() => {
|
|
|
104
106
|
return Extensions.load();
|
|
105
107
|
},
|
|
106
108
|
|
|
107
|
-
/** Toggle the "installed only" filter
|
|
108
|
-
setFilterInstalled(onlyInstalled) {
|
|
109
|
+
/** Toggle the "installed only" filter — fetches from local store when enabled. */
|
|
110
|
+
async setFilterInstalled(onlyInstalled) {
|
|
109
111
|
_filterInstalled = !!onlyInstalled;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
+
if (_filterInstalled) {
|
|
113
|
+
_loading = true;
|
|
114
|
+
_emit("extensions:loading");
|
|
115
|
+
try {
|
|
116
|
+
const res = await fetch("/api/store/extensions/installed");
|
|
117
|
+
const data = await res.json();
|
|
118
|
+
const all = data.extensions || [];
|
|
119
|
+
_extensions = _query
|
|
120
|
+
? all.filter(e => [e.name, e.name_zh, e.description].some(
|
|
121
|
+
f => f && f.toLowerCase().includes(_query.toLowerCase())))
|
|
122
|
+
: all;
|
|
123
|
+
} catch (e) {
|
|
124
|
+
console.error("[Extensions] load installed failed", e);
|
|
125
|
+
_extensions = [];
|
|
126
|
+
} finally {
|
|
127
|
+
_loading = false;
|
|
128
|
+
}
|
|
129
|
+
_emit("extensions:changed", { extensions: _extensions, warning: _error });
|
|
130
|
+
} else {
|
|
131
|
+
return Extensions.load();
|
|
132
|
+
}
|
|
112
133
|
},
|
|
113
134
|
|
|
114
135
|
/** Open the detail view for one extension (fetches contributes + versions). */
|
|
@@ -158,8 +179,7 @@ const ExtensionsStore = (() => {
|
|
|
158
179
|
});
|
|
159
180
|
const data = await res.json();
|
|
160
181
|
if (!res.ok || !data.ok) throw new Error(data.error || "toggle failed");
|
|
161
|
-
|
|
162
|
-
Extensions.load();
|
|
182
|
+
location.reload();
|
|
163
183
|
} catch (e) {
|
|
164
184
|
console.error("[Extensions] setEnabled failed", e);
|
|
165
185
|
_detailError = e.message;
|
|
@@ -184,7 +204,7 @@ const ExtensionsStore = (() => {
|
|
|
184
204
|
});
|
|
185
205
|
const data = await res.json();
|
|
186
206
|
if (!res.ok || !data.ok) throw new Error(data.error || "install failed");
|
|
187
|
-
|
|
207
|
+
location.reload();
|
|
188
208
|
} catch (e) {
|
|
189
209
|
console.error("[Extensions] install failed", e);
|
|
190
210
|
_detailError = e.message;
|
|
@@ -192,6 +212,11 @@ const ExtensionsStore = (() => {
|
|
|
192
212
|
}
|
|
193
213
|
},
|
|
194
214
|
|
|
215
|
+
/** Update an installed extension to the latest marketplace version. */
|
|
216
|
+
async update(id) {
|
|
217
|
+
return Extensions.install(id);
|
|
218
|
+
},
|
|
219
|
+
|
|
195
220
|
/** Remove an installed extension, then return to the list. */
|
|
196
221
|
async uninstall(id) {
|
|
197
222
|
if (!id) return;
|
|
@@ -203,7 +228,7 @@ const ExtensionsStore = (() => {
|
|
|
203
228
|
});
|
|
204
229
|
const data = await res.json();
|
|
205
230
|
if (!res.ok || !data.ok) throw new Error(data.error || "uninstall failed");
|
|
206
|
-
|
|
231
|
+
location.reload();
|
|
207
232
|
} catch (e) {
|
|
208
233
|
console.error("[Extensions] uninstall failed", e);
|
|
209
234
|
_detailError = e.message;
|