openclacky 0.9.16 → 0.9.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/lib/clacky/providers.rb +10 -5
- data/lib/clacky/server/channel/adapters/feishu/ws_client.rb +27 -5
- data/lib/clacky/server/channel/adapters/wecom/ws_client.rb +31 -5
- data/lib/clacky/server/http_server.rb +2 -1
- data/lib/clacky/tools/browser.rb +44 -5
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/i18n.js +4 -0
- data/lib/clacky/web/index.html +4 -1
- data/lib/clacky/web/onboard.js +11 -0
- data/lib/clacky/web/settings.js +14 -1
- data/scripts/install.ps1 +81 -34
- data/scripts/install_simple.sh +3 -2
- 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: 1dd996048847d73d0a71f8d4833f338e4c5ae2a58c6228122b817039f2d9fda4
|
|
4
|
+
data.tar.gz: 2003df86f0c4446d6f3d6b7d0c14eef19cc05246735acdd1f79910a0cfb0a324
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab9f7d2b510bdf5dc4736723dbfa602968a584f2d097d4747af4f5c74247f494e66d32a0420c7f7aa4b5e87662d5662671ab3c56e12bdb627bd7b4766504fda9
|
|
7
|
+
data.tar.gz: 4e5543ac5c01d46b5be821769c1505de6797746991fa9605788a1036ed334fa03d6a3c2d7546706dcd0bde5595d94812196acbd51df8e56008c7ba493c1058ec
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.17] - 2026-03-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Browser screenshots now saved to disk**: every screenshot action automatically saves both the original full-resolution PNG and the compressed (800px) version to disk — the agent reports both file paths so you can reference, open, or pass the screenshots to other tools
|
|
14
|
+
- **Provider "Get API Key" links in onboarding**: the setup wizard now shows a direct link to the provider's website when you select a provider that has a `website_url` — making it easier to sign up and get your API key without leaving the flow
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **WebSocket auto-reconnect for Feishu and WeCom channels**: the WebSocket clients for Feishu and WeCom now automatically retry the connection after failures — channels stay online without manual intervention after a network hiccup
|
|
18
|
+
- **Brand command in simple install script**: the `clacky` brand command was incorrectly invoked in `install_simple.sh` — now fixed so the post-install branding step runs correctly
|
|
19
|
+
- **Windows WSL2 and Hyper-V detection in PowerShell installer**: improved detection logic for WSL2 and Hyper-V environments in `install.ps1`, reducing false negatives on Windows machines with non-standard configurations
|
|
20
|
+
|
|
10
21
|
## [0.9.16] - 2026-03-27
|
|
11
22
|
|
|
12
23
|
### Fixed
|
data/lib/clacky/providers.rb
CHANGED
|
@@ -17,7 +17,8 @@ module Clacky
|
|
|
17
17
|
"base_url" => "https://openrouter.ai/api/v1",
|
|
18
18
|
"api" => "openai-responses",
|
|
19
19
|
"default_model" => "anthropic/claude-sonnet-4-6",
|
|
20
|
-
"models" => [] # Dynamic - fetched from API
|
|
20
|
+
"models" => [], # Dynamic - fetched from API
|
|
21
|
+
"website_url" => "https://openrouter.ai/keys"
|
|
21
22
|
}.freeze,
|
|
22
23
|
|
|
23
24
|
"minimax" => {
|
|
@@ -25,7 +26,8 @@ module Clacky
|
|
|
25
26
|
"base_url" => "https://api.minimaxi.com/v1",
|
|
26
27
|
"api" => "openai-completions",
|
|
27
28
|
"default_model" => "MiniMax-M2.7",
|
|
28
|
-
"models" => ["MiniMax-M2.5", "MiniMax-M2.7"]
|
|
29
|
+
"models" => ["MiniMax-M2.5", "MiniMax-M2.7"],
|
|
30
|
+
"website_url" => "https://www.minimaxi.com/user-center/basic-information/interface-key"
|
|
29
31
|
}.freeze,
|
|
30
32
|
|
|
31
33
|
"kimi" => {
|
|
@@ -33,7 +35,8 @@ module Clacky
|
|
|
33
35
|
"base_url" => "https://api.moonshot.cn/v1",
|
|
34
36
|
"api" => "openai-completions",
|
|
35
37
|
"default_model" => "kimi-k2.5",
|
|
36
|
-
"models" => ["kimi-k2.5"]
|
|
38
|
+
"models" => ["kimi-k2.5"],
|
|
39
|
+
"website_url" => "https://platform.moonshot.cn/console/api-keys"
|
|
37
40
|
}.freeze,
|
|
38
41
|
|
|
39
42
|
"anthropic" => {
|
|
@@ -41,7 +44,8 @@ module Clacky
|
|
|
41
44
|
"base_url" => "https://api.anthropic.com",
|
|
42
45
|
"api" => "anthropic-messages",
|
|
43
46
|
"default_model" => "claude-sonnet-4.6",
|
|
44
|
-
"models" => ["claude-opus-4-6", "claude-sonnet-4.6", "claude-haiku-4.5"]
|
|
47
|
+
"models" => ["claude-opus-4-6", "claude-sonnet-4.6", "claude-haiku-4.5"],
|
|
48
|
+
"website_url" => "https://console.anthropic.com/settings/keys"
|
|
45
49
|
}.freeze,
|
|
46
50
|
|
|
47
51
|
"bedrock-jp" => {
|
|
@@ -52,7 +56,8 @@ module Clacky
|
|
|
52
56
|
"models" => [
|
|
53
57
|
"jp.anthropic.claude-sonnet-4-6",
|
|
54
58
|
"jp.anthropic.claude-haiku-4-6"
|
|
55
|
-
]
|
|
59
|
+
],
|
|
60
|
+
"website_url" => "https://console.aws.amazon.com/iam/home#/security_credentials"
|
|
56
61
|
}.freeze
|
|
57
62
|
|
|
58
63
|
}.freeze
|
|
@@ -53,6 +53,11 @@ module Clacky
|
|
|
53
53
|
|
|
54
54
|
private
|
|
55
55
|
|
|
56
|
+
# Timeout for IO.select on the read loop. Feishu server sends pings every
|
|
57
|
+
# @ping_interval seconds (default 90s). Allow two missed pings before
|
|
58
|
+
# treating the connection as dead.
|
|
59
|
+
READ_TIMEOUT_MULTIPLIER = 2.5
|
|
60
|
+
|
|
56
61
|
def connect_and_listen
|
|
57
62
|
Clacky::Logger.info("[feishu-ws] Fetching WebSocket endpoint...")
|
|
58
63
|
endpoint = fetch_ws_endpoint
|
|
@@ -92,9 +97,22 @@ module Clacky
|
|
|
92
97
|
|
|
93
98
|
start_ping_thread
|
|
94
99
|
|
|
100
|
+
# read_timeout is based on the server-provided ping interval so it
|
|
101
|
+
# automatically adapts if Feishu changes the cadence.
|
|
102
|
+
read_timeout = (@ping_interval * READ_TIMEOUT_MULTIPLIER).ceil
|
|
103
|
+
|
|
95
104
|
loop do
|
|
96
105
|
break unless @running
|
|
97
|
-
|
|
106
|
+
|
|
107
|
+
# Use IO.select with a timeout to detect silent connection drops
|
|
108
|
+
# (NAT expiry, firewall idle-kill) that never send a TCP FIN/RST.
|
|
109
|
+
ready = IO.select([socket], nil, nil, read_timeout)
|
|
110
|
+
unless ready
|
|
111
|
+
Clacky::Logger.warn("[feishu-ws] read timeout (#{read_timeout}s), reconnecting...")
|
|
112
|
+
return
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
data = socket.read_nonblock(4096)
|
|
98
116
|
@incoming << data
|
|
99
117
|
while (frame = @incoming.next)
|
|
100
118
|
case frame.type
|
|
@@ -106,13 +124,14 @@ module Clacky
|
|
|
106
124
|
when :ping
|
|
107
125
|
send_raw_frame(:pong, frame.data)
|
|
108
126
|
when :close
|
|
109
|
-
Clacky::Logger.info("[feishu-ws] WebSocket closed, will reconnect")
|
|
127
|
+
Clacky::Logger.info("[feishu-ws] WebSocket closed by server, will reconnect")
|
|
110
128
|
return
|
|
111
129
|
end
|
|
112
130
|
end
|
|
113
131
|
end
|
|
114
|
-
rescue EOFError, Errno::ECONNRESET
|
|
115
|
-
|
|
132
|
+
rescue EOFError, IOError, Errno::ECONNRESET, Errno::EPIPE,
|
|
133
|
+
Errno::ETIMEDOUT, OpenSSL::SSL::SSLError => e
|
|
134
|
+
Clacky::Logger.warn("[feishu-ws] Connection lost (#{e.class}: #{e.message}), reconnecting in #{RECONNECT_DELAY}s...")
|
|
116
135
|
ensure
|
|
117
136
|
@ws_open = false
|
|
118
137
|
@ws_socket = nil
|
|
@@ -254,7 +273,10 @@ module Clacky
|
|
|
254
273
|
headers: { "type" => "ping" }
|
|
255
274
|
)
|
|
256
275
|
rescue => e
|
|
257
|
-
warn
|
|
276
|
+
Clacky::Logger.warn("[feishu-ws] ping failed (#{e.class}: #{e.message}), forcing reconnect")
|
|
277
|
+
# Close the socket so IO.select in the read loop immediately
|
|
278
|
+
# returns nil / read_nonblock raises IOError, triggering reconnect.
|
|
279
|
+
@ws_socket&.close rescue nil
|
|
258
280
|
break
|
|
259
281
|
end
|
|
260
282
|
end
|
|
@@ -117,6 +117,13 @@ module Clacky
|
|
|
117
117
|
|
|
118
118
|
private
|
|
119
119
|
|
|
120
|
+
# Timeout for IO.select on the read loop. If no data arrives within this
|
|
121
|
+
# window we treat the connection as dead and reconnect. This catches the
|
|
122
|
+
# silent-drop case where the TCP stack never delivers a FIN/RST (e.g.
|
|
123
|
+
# NAT timeout, firewall idle-kill). The WeCom server sends pings every
|
|
124
|
+
# ~30 s, so 75 s gives two missed pings before we give up.
|
|
125
|
+
READ_TIMEOUT_S = 75
|
|
126
|
+
|
|
120
127
|
def connect_and_listen
|
|
121
128
|
uri = URI.parse(@ws_url)
|
|
122
129
|
port = uri.port || 443
|
|
@@ -151,7 +158,17 @@ module Clacky
|
|
|
151
158
|
|
|
152
159
|
loop do
|
|
153
160
|
break unless @running
|
|
154
|
-
|
|
161
|
+
|
|
162
|
+
# Use IO.select with a timeout so we detect silent connection drops
|
|
163
|
+
# (e.g. NAT expiry) that never deliver a TCP FIN/RST. Without this,
|
|
164
|
+
# readpartial blocks forever and the thread hangs permanently.
|
|
165
|
+
ready = IO.select([ssl], nil, nil, READ_TIMEOUT_S)
|
|
166
|
+
unless ready
|
|
167
|
+
Clacky::Logger.warn("[WecomWSClient] read timeout (#{READ_TIMEOUT_S}s), reconnecting...")
|
|
168
|
+
return
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
data = ssl.read_nonblock(4096)
|
|
155
172
|
@incoming << data
|
|
156
173
|
while (frame = @incoming.next)
|
|
157
174
|
case frame.type
|
|
@@ -160,13 +177,14 @@ module Clacky
|
|
|
160
177
|
when :ping
|
|
161
178
|
send_raw_frame(:pong, frame.data)
|
|
162
179
|
when :close
|
|
163
|
-
Clacky::Logger.info("[WecomWSClient] connection closed")
|
|
180
|
+
Clacky::Logger.info("[WecomWSClient] connection closed by server")
|
|
164
181
|
return
|
|
165
182
|
end
|
|
166
183
|
end
|
|
167
184
|
end
|
|
168
|
-
rescue EOFError, Errno::ECONNRESET
|
|
169
|
-
|
|
185
|
+
rescue EOFError, IOError, Errno::ECONNRESET, Errno::EPIPE,
|
|
186
|
+
Errno::ETIMEDOUT, OpenSSL::SSL::SSLError => e
|
|
187
|
+
Clacky::Logger.info("[WecomWSClient] connection lost (#{e.class}: #{e.message}), reconnecting...")
|
|
170
188
|
ensure
|
|
171
189
|
@ws_open = false
|
|
172
190
|
@ws_socket = nil
|
|
@@ -257,7 +275,15 @@ module Clacky
|
|
|
257
275
|
loop do
|
|
258
276
|
sleep HEARTBEAT_INTERVAL
|
|
259
277
|
break unless @running
|
|
260
|
-
|
|
278
|
+
begin
|
|
279
|
+
send_frame(cmd: "ping", req_id: generate_req_id("ping"))
|
|
280
|
+
rescue => e
|
|
281
|
+
Clacky::Logger.warn("[WecomWSClient] ping failed (#{e.class}: #{e.message}), forcing reconnect")
|
|
282
|
+
# Close the socket so IO.select in the read loop immediately
|
|
283
|
+
# returns nil / read_nonblock raises IOError, triggering reconnect.
|
|
284
|
+
@ws_socket&.close rescue nil
|
|
285
|
+
break
|
|
286
|
+
end
|
|
261
287
|
end
|
|
262
288
|
end
|
|
263
289
|
end
|
|
@@ -1528,7 +1528,8 @@ module Clacky
|
|
|
1528
1528
|
name: preset["name"],
|
|
1529
1529
|
base_url: preset["base_url"],
|
|
1530
1530
|
default_model: preset["default_model"],
|
|
1531
|
-
models: preset["models"] || []
|
|
1531
|
+
models: preset["models"] || [],
|
|
1532
|
+
website_url: preset["website_url"]
|
|
1532
1533
|
}
|
|
1533
1534
|
end
|
|
1534
1535
|
json_response(res, 200, { providers: providers })
|
data/lib/clacky/tools/browser.rb
CHANGED
|
@@ -6,6 +6,9 @@ require "timeout"
|
|
|
6
6
|
require "tmpdir"
|
|
7
7
|
require "shellwords"
|
|
8
8
|
require "yaml"
|
|
9
|
+
require "base64"
|
|
10
|
+
require "fileutils"
|
|
11
|
+
require "securerandom"
|
|
9
12
|
require_relative "base"
|
|
10
13
|
|
|
11
14
|
module Clacky
|
|
@@ -159,11 +162,20 @@ module Clacky
|
|
|
159
162
|
action = result[:action].to_s
|
|
160
163
|
|
|
161
164
|
if action == "screenshot" && result[:image_data]
|
|
162
|
-
mime_type
|
|
163
|
-
image_data
|
|
164
|
-
data_url
|
|
165
|
+
mime_type = result[:mime_type] || "image/png"
|
|
166
|
+
image_data = result[:image_data]
|
|
167
|
+
data_url = "data:#{mime_type};base64,#{image_data}"
|
|
168
|
+
original_path = result[:original_path]
|
|
169
|
+
compressed_path = result[:compressed_path]
|
|
170
|
+
|
|
171
|
+
text = "Screenshot captured."
|
|
172
|
+
if original_path || compressed_path
|
|
173
|
+
text += "\n- Original (full resolution): #{original_path || 'unavailable'}" \
|
|
174
|
+
"\n- Compressed (800px, sent to AI): #{compressed_path || 'unavailable'}"
|
|
175
|
+
end
|
|
176
|
+
|
|
165
177
|
return [
|
|
166
|
-
{ type: "text", text:
|
|
178
|
+
{ type: "text", text: text },
|
|
167
179
|
{ type: "image_url", image_url: { url: data_url } }
|
|
168
180
|
]
|
|
169
181
|
end
|
|
@@ -416,6 +428,9 @@ module Clacky
|
|
|
416
428
|
output: text.empty? ? "Screenshot captured." : text }
|
|
417
429
|
end
|
|
418
430
|
|
|
431
|
+
# Save original (full-resolution) PNG to disk before any downscaling
|
|
432
|
+
original_path = save_screenshot_to_disk(image_block["data"], suffix: "original")
|
|
433
|
+
|
|
419
434
|
image_data = png_downscale_base64(image_block["data"], SCREENSHOT_MAX_WIDTH)
|
|
420
435
|
|
|
421
436
|
if image_data.bytesize > SCREENSHOT_MAX_BASE64_BYTES
|
|
@@ -424,8 +439,13 @@ module Clacky
|
|
|
424
439
|
output: "Screenshot too large after resize (#{size_kb}KB). Use action=snapshot instead." }
|
|
425
440
|
end
|
|
426
441
|
|
|
442
|
+
# Save compressed (800px) PNG for AI reference
|
|
443
|
+
compressed_path = save_screenshot_to_disk(image_data, suffix: "compressed")
|
|
444
|
+
|
|
427
445
|
{ action: "screenshot", success: true, profile: "user",
|
|
428
|
-
image_data: image_data, mime_type: "image/png",
|
|
446
|
+
image_data: image_data, mime_type: "image/png",
|
|
447
|
+
original_path: original_path, compressed_path: compressed_path,
|
|
448
|
+
output: "Screenshot captured." }
|
|
429
449
|
end
|
|
430
450
|
|
|
431
451
|
private def png_downscale_base64(b64, max_width)
|
|
@@ -443,6 +463,25 @@ module Clacky
|
|
|
443
463
|
result
|
|
444
464
|
end
|
|
445
465
|
|
|
466
|
+
# Save a base64-encoded PNG screenshot to disk and return the file path.
|
|
467
|
+
# suffix: "original" or "compressed" — embedded in filename for clarity.
|
|
468
|
+
# Uses the same upload directory as other image files so the agent can
|
|
469
|
+
# reference, read, or pass the path to other tools.
|
|
470
|
+
private def save_screenshot_to_disk(base64_data, suffix: nil)
|
|
471
|
+
upload_dir = File.join(Dir.tmpdir, "clacky-uploads")
|
|
472
|
+
FileUtils.mkdir_p(upload_dir)
|
|
473
|
+
ts = Time.now.strftime("%Y%m%d_%H%M%S")
|
|
474
|
+
hex = SecureRandom.hex(4)
|
|
475
|
+
label = suffix ? "_#{suffix}" : ""
|
|
476
|
+
filename = "screenshot_#{ts}_#{hex}#{label}.png"
|
|
477
|
+
path = File.join(upload_dir, filename)
|
|
478
|
+
File.binwrite(path, Base64.strict_decode64(base64_data))
|
|
479
|
+
path
|
|
480
|
+
rescue => e
|
|
481
|
+
Clacky::Logger.error("screenshot_save_failed", error: e.message)
|
|
482
|
+
nil
|
|
483
|
+
end
|
|
484
|
+
|
|
446
485
|
# -----------------------------------------------------------------------
|
|
447
486
|
# Chrome MCP
|
|
448
487
|
# -----------------------------------------------------------------------
|
data/lib/clacky/version.rb
CHANGED
data/lib/clacky/web/i18n.js
CHANGED
|
@@ -181,6 +181,7 @@ const I18n = (() => {
|
|
|
181
181
|
"settings.models.field.model": "Model",
|
|
182
182
|
"settings.models.field.baseurl": "Base URL",
|
|
183
183
|
"settings.models.field.apikey": "API Key",
|
|
184
|
+
"settings.models.field.getApiKey": "How to get →",
|
|
184
185
|
"settings.models.placeholder.provider": "— Choose provider —",
|
|
185
186
|
"settings.models.placeholder.model": "e.g. claude-sonnet-4-5",
|
|
186
187
|
"settings.models.placeholder.baseurl": "https://api.anthropic.com",
|
|
@@ -254,6 +255,7 @@ const I18n = (() => {
|
|
|
254
255
|
"onboard.key.model": "Model",
|
|
255
256
|
"onboard.key.baseurl": "Base URL",
|
|
256
257
|
"onboard.key.apikey": "API Key",
|
|
258
|
+
"onboard.key.getApiKey": "How to get →",
|
|
257
259
|
"onboard.key.btn.test": "Test & Continue →",
|
|
258
260
|
"onboard.key.btn.back": "← Back",
|
|
259
261
|
"onboard.provider.custom": "Custom",
|
|
@@ -454,6 +456,7 @@ const I18n = (() => {
|
|
|
454
456
|
"settings.models.field.model": "Model",
|
|
455
457
|
"settings.models.field.baseurl": "Base URL",
|
|
456
458
|
"settings.models.field.apikey": "API Key",
|
|
459
|
+
"settings.models.field.getApiKey": "如何获取 →",
|
|
457
460
|
"settings.models.placeholder.provider": "— 选择服务商 —",
|
|
458
461
|
"settings.models.placeholder.model": "如 claude-sonnet-4-5",
|
|
459
462
|
"settings.models.placeholder.baseurl": "https://api.anthropic.com",
|
|
@@ -527,6 +530,7 @@ const I18n = (() => {
|
|
|
527
530
|
"onboard.key.model": "模型",
|
|
528
531
|
"onboard.key.baseurl": "Base URL",
|
|
529
532
|
"onboard.key.apikey": "API Key",
|
|
533
|
+
"onboard.key.getApiKey": "如何获取 →",
|
|
530
534
|
"onboard.key.btn.test": "测试并继续 →",
|
|
531
535
|
"onboard.key.btn.back": "← 返回",
|
|
532
536
|
"onboard.provider.custom": "自定义",
|
data/lib/clacky/web/index.html
CHANGED
|
@@ -553,7 +553,10 @@
|
|
|
553
553
|
</div>
|
|
554
554
|
|
|
555
555
|
<div class="setup-field">
|
|
556
|
-
<label class="setup-label"
|
|
556
|
+
<label class="setup-label">
|
|
557
|
+
<span data-i18n="onboard.key.apikey">API Key</span>
|
|
558
|
+
<a id="setup-get-apikey-link" href="#" target="_blank" rel="noopener" style="display:none;margin-left:8px;font-size:12px;color:var(--accent,#6366f1);text-decoration:none;opacity:0.85;" data-i18n="onboard.key.getApiKey">How to get →</a>
|
|
559
|
+
</label>
|
|
557
560
|
<div class="setup-input-row">
|
|
558
561
|
<input id="setup-api-key" type="password" class="setup-input" data-i18n-placeholder="settings.models.placeholder.apikey" placeholder="sk-…">
|
|
559
562
|
<button id="setup-toggle-key" class="btn-toggle-key" title="Show/hide">
|
data/lib/clacky/web/onboard.js
CHANGED
|
@@ -180,16 +180,27 @@ const Onboard = (() => {
|
|
|
180
180
|
dropdown.classList.remove("open");
|
|
181
181
|
trigger.classList.remove("open");
|
|
182
182
|
|
|
183
|
+
const getApiKeyLink = $("setup-get-apikey-link");
|
|
183
184
|
if (value === "__custom__") {
|
|
184
185
|
// Custom: clear presets so the user can fill in their own values
|
|
185
186
|
$("setup-model").value = "";
|
|
186
187
|
$("setup-base-url").value = "";
|
|
188
|
+
if (getApiKeyLink) getApiKeyLink.style.display = "none";
|
|
187
189
|
} else if (value) {
|
|
188
190
|
const preset = _providers.find(p => p.id === value);
|
|
189
191
|
if (preset) {
|
|
190
192
|
$("setup-model").value = preset.default_model || "";
|
|
191
193
|
$("setup-base-url").value = preset.base_url || "";
|
|
194
|
+
// Show "how to get" link if provider has a website_url
|
|
195
|
+
if (getApiKeyLink && preset.website_url) {
|
|
196
|
+
getApiKeyLink.href = preset.website_url;
|
|
197
|
+
getApiKeyLink.style.display = "";
|
|
198
|
+
} else if (getApiKeyLink) {
|
|
199
|
+
getApiKeyLink.style.display = "none";
|
|
200
|
+
}
|
|
192
201
|
}
|
|
202
|
+
} else if (getApiKeyLink) {
|
|
203
|
+
getApiKeyLink.style.display = "none";
|
|
193
204
|
}
|
|
194
205
|
});
|
|
195
206
|
|
data/lib/clacky/web/settings.js
CHANGED
|
@@ -105,7 +105,10 @@ const Settings = (() => {
|
|
|
105
105
|
placeholder="${I18n.t("settings.models.placeholder.baseurl")}" value="${_esc(model.base_url)}">
|
|
106
106
|
</label>
|
|
107
107
|
<label class="model-field">
|
|
108
|
-
<span class="field-label"
|
|
108
|
+
<span class="field-label">
|
|
109
|
+
${I18n.t("settings.models.field.apikey")}
|
|
110
|
+
<a class="get-apikey-link" data-index="${index}" href="#" target="_blank" rel="noopener" style="display:none;margin-left:8px;font-size:12px;color:var(--accent,#6366f1);text-decoration:none;opacity:0.85;">${I18n.t("settings.models.field.getApiKey")}</a>
|
|
111
|
+
</span>
|
|
109
112
|
<div class="field-input-row">
|
|
110
113
|
<input type="password" class="field-input api-key-input" data-key="api_key" data-index="${index}"
|
|
111
114
|
placeholder="${I18n.t("settings.models.placeholder.apikey")}" value="${_esc(model.api_key_masked)}">
|
|
@@ -179,6 +182,7 @@ const Settings = (() => {
|
|
|
179
182
|
trigger.classList.remove("open");
|
|
180
183
|
|
|
181
184
|
// Auto-fill model & base_url if a provider preset was selected
|
|
185
|
+
const getApiKeyLink = card.querySelector(`.get-apikey-link[data-index="${index}"]`);
|
|
182
186
|
if (value && value !== "custom") {
|
|
183
187
|
const preset = _providers.find(p => p.id === value);
|
|
184
188
|
if (preset) {
|
|
@@ -186,7 +190,16 @@ const Settings = (() => {
|
|
|
186
190
|
const baseUrlInput = card.querySelector(`[data-key="base_url"]`);
|
|
187
191
|
if (modelInput) modelInput.value = preset.default_model || "";
|
|
188
192
|
if (baseUrlInput) baseUrlInput.value = preset.base_url || "";
|
|
193
|
+
// Show "how to get" link if provider has a website_url
|
|
194
|
+
if (getApiKeyLink && preset.website_url) {
|
|
195
|
+
getApiKeyLink.href = preset.website_url;
|
|
196
|
+
getApiKeyLink.style.display = "";
|
|
197
|
+
} else if (getApiKeyLink) {
|
|
198
|
+
getApiKeyLink.style.display = "none";
|
|
199
|
+
}
|
|
189
200
|
}
|
|
201
|
+
} else if (getApiKeyLink) {
|
|
202
|
+
getApiKeyLink.style.display = "none";
|
|
190
203
|
}
|
|
191
204
|
});
|
|
192
205
|
});
|
data/scripts/install.ps1
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#Requires -Version 5
|
|
2
2
|
# OpenClacky Windows Installation Script
|
|
3
|
-
# Usage: powershell -c "irm https://oss.1024code.com/install.ps1 | iex"
|
|
3
|
+
# Usage: powershell -c "irm https://oss.1024code.com/clacky-ai/openclacky/main/scripts/install.ps1 | iex"
|
|
4
4
|
#
|
|
5
5
|
# If WSL is not installed, this script will install it and ask you to reboot.
|
|
6
6
|
# After rebooting, run the same command again to complete installation.
|
|
@@ -9,10 +9,11 @@ Set-StrictMode -Version Latest
|
|
|
9
9
|
$ErrorActionPreference = "Stop"
|
|
10
10
|
|
|
11
11
|
$CLACKY_CDN_BASE_URL = "https://oss.1024code.com"
|
|
12
|
-
$INSTALL_PS1_COMMAND = "powershell -c `"irm $CLACKY_CDN_BASE_URL/install.ps1 | iex`""
|
|
13
|
-
$INSTALL_SCRIPT_URL = "$CLACKY_CDN_BASE_URL/
|
|
12
|
+
$INSTALL_PS1_COMMAND = "powershell -c `"irm $CLACKY_CDN_BASE_URL/clacky-ai/openclacky/main/scripts/install.ps1 | iex`""
|
|
13
|
+
$INSTALL_SCRIPT_URL = "$CLACKY_CDN_BASE_URL/clacky-ai/openclacky/main/scripts/install_simple.sh"
|
|
14
14
|
$UBUNTU_WSL_URL = "$CLACKY_CDN_BASE_URL/ubuntu-jammy-wsl-amd64-ubuntu22.04lts.rootfs.tar.gz"
|
|
15
|
-
$WSL_UPDATE_URL = "$CLACKY_CDN_BASE_URL/wsl_update_x64.msi"
|
|
15
|
+
$WSL_UPDATE_URL = "$CLACKY_CDN_BASE_URL/wsl_update_x64.msi" # Windows 10
|
|
16
|
+
$WSL_UPDATE_URL_WIN11 = "$CLACKY_CDN_BASE_URL/wsl.2.6.3.0.x64.msi" # Windows 11
|
|
16
17
|
$UBUNTU_WSL_DIR = "C:\WSL\Ubuntu"
|
|
17
18
|
|
|
18
19
|
# ---------------------------------------------------------------------------
|
|
@@ -34,27 +35,28 @@ function Test-IsAdmin {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
# exit 1 = WSL feature not enabled (stub wsl.exe)
|
|
37
|
-
# exit -1 = feature enabled but kernel missing
|
|
38
38
|
# exit 0 = fully functional
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return ($LASTEXITCODE -eq 0)
|
|
39
|
+
#
|
|
40
|
+
# Use cmd.exe to run wsl and discard stderr: native wsl.exe writes localized
|
|
41
|
+
# install hints to stderr; in Windows PowerShell 5.x with $ErrorActionPreference
|
|
42
|
+
# Stop, stderr merged via 2>&1 becomes NativeCommandError and can terminate the
|
|
43
|
+
# script (and mojibake when console encoding mismatches).
|
|
44
|
+
function Invoke-WslListExitCode {
|
|
45
|
+
cmd.exe /c "wsl.exe --list 1>nul 2>nul"
|
|
46
|
+
return $LASTEXITCODE
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
# Returns $true if an Ubuntu distro is already registered
|
|
50
|
+
# wsl --list outputs UTF-16LE; temporarily switch OutputEncoding to decode correctly
|
|
51
51
|
function Test-UbuntuInstalled {
|
|
52
|
+
$prev = [Console]::OutputEncoding
|
|
53
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::Unicode
|
|
52
54
|
try {
|
|
53
|
-
$out =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return $false
|
|
55
|
+
$out = (wsl.exe --list --quiet 2>$null) -join "`n"
|
|
56
|
+
} finally {
|
|
57
|
+
[Console]::OutputEncoding = $prev
|
|
57
58
|
}
|
|
59
|
+
return ($out -match '(?im)^ubuntu')
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
function Prompt-Reboot {
|
|
@@ -63,28 +65,52 @@ function Prompt-Reboot {
|
|
|
63
65
|
Write-Warn "After restarting, run the same command again:"
|
|
64
66
|
Write-Host " $INSTALL_PS1_COMMAND" -ForegroundColor Yellow
|
|
65
67
|
Write-Host ""
|
|
68
|
+
Read-Host "Press Enter to exit"
|
|
66
69
|
exit 0
|
|
67
70
|
}
|
|
68
71
|
|
|
69
|
-
# Enable WSL via dism — works offline, no dependency on Microsoft download servers
|
|
72
|
+
# Enable WSL via dism — works offline, no dependency on Microsoft download servers.
|
|
73
|
+
# Also installs the WSL2 kernel MSI immediately so that after reboot wsl.exe is
|
|
74
|
+
# fully functional and won't show the "WSL must be updated" interactive prompt,
|
|
75
|
+
# which would cause this script to loop endlessly on re-run.
|
|
70
76
|
function Enable-WslFeatures {
|
|
71
77
|
Write-Step "Enabling WSL components (requires admin)..."
|
|
72
78
|
dism /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart | Out-Null
|
|
73
79
|
dism /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart | Out-Null
|
|
74
80
|
Write-Success "WSL components enabled."
|
|
81
|
+
|
|
82
|
+
# Install kernel MSI right away — without this the stub wsl.exe keeps
|
|
83
|
+
# returning exit code 1 ("must be updated") after every reboot, causing
|
|
84
|
+
# this script to re-enable features and prompt for reboot in a loop.
|
|
85
|
+
Install-WslKernel
|
|
86
|
+
|
|
75
87
|
Prompt-Reboot
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
function Install-WslKernel {
|
|
91
|
+
$build = [System.Environment]::OSVersion.Version.Build
|
|
92
|
+
|
|
93
|
+
# WSL2 requires Windows 10 build 19041 (version 2004) or later
|
|
94
|
+
if ($build -lt 19041) {
|
|
95
|
+
Write-Fail "Your Windows version is too old to run OpenClacky."
|
|
96
|
+
Write-Fail "Please upgrade to Windows 10 (2020 or later) or Windows 11."
|
|
97
|
+
exit 1
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Windows 11 (build >= 22000) requires the new full WSL2 package MSI;
|
|
101
|
+
# the legacy wsl_update_x64.msi (v5.x) fails with error 1603 on Windows 11.
|
|
102
|
+
$isWin11 = ($build -ge 22000)
|
|
103
|
+
$url = if ($isWin11) { $WSL_UPDATE_URL_WIN11 } else { $WSL_UPDATE_URL }
|
|
104
|
+
|
|
79
105
|
$msiPath = "$env:TEMP\wsl_update.msi"
|
|
80
106
|
Write-Step "Downloading WSL2 kernel update..."
|
|
81
107
|
$curlOk = $false
|
|
82
|
-
try { curl -L --progress-bar $
|
|
108
|
+
try { curl -L --progress-bar $url -o $msiPath; $curlOk = ($LASTEXITCODE -eq 0) } catch {}
|
|
83
109
|
if (-not $curlOk) {
|
|
84
|
-
Invoke-WebRequest -Uri $
|
|
110
|
+
Invoke-WebRequest -Uri $url -OutFile $msiPath -UseBasicParsing
|
|
85
111
|
}
|
|
86
112
|
Write-Info "Download complete. Installing WSL2 kernel..."
|
|
87
|
-
Start-Process msiexec -
|
|
113
|
+
Start-Process msiexec -Wait -ArgumentList "/i","$msiPath","/quiet","/norestart"
|
|
88
114
|
Write-Success "WSL2 kernel installed."
|
|
89
115
|
}
|
|
90
116
|
|
|
@@ -112,28 +138,39 @@ function Install-Ubuntu {
|
|
|
112
138
|
}
|
|
113
139
|
|
|
114
140
|
function Install-Wsl {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
Write-Host " Right-click PowerShell -> 'Run as administrator', then:" -ForegroundColor Yellow
|
|
119
|
-
Write-Host " $INSTALL_PS1_COMMAND" -ForegroundColor Yellow
|
|
120
|
-
exit 1
|
|
121
|
-
}
|
|
141
|
+
Write-Step "Checking WSL status..."
|
|
142
|
+
$wslCode = Invoke-WslListExitCode
|
|
143
|
+
Write-Info "WSL check result: exit code $wslCode"
|
|
122
144
|
|
|
123
|
-
if (-
|
|
145
|
+
if ($wslCode -eq 1) {
|
|
124
146
|
Enable-WslFeatures
|
|
125
147
|
# Enable-WslFeatures exits after prompting reboot
|
|
126
148
|
}
|
|
127
149
|
|
|
128
|
-
if (-not (Test-WslKernel)) {
|
|
129
|
-
Install-WslKernel
|
|
130
|
-
}
|
|
131
|
-
|
|
132
150
|
if (-not (Test-UbuntuInstalled)) {
|
|
133
151
|
Install-Ubuntu
|
|
134
152
|
}
|
|
135
153
|
}
|
|
136
154
|
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
# Ensure Hyper-V hypervisor is active (required for WSL2)
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
function Ensure-HyperV {
|
|
159
|
+
$hypervisorPresent = (Get-CimInstance -ClassName Win32_ComputerSystem).HypervisorPresent
|
|
160
|
+
if ($hypervisorPresent) { return }
|
|
161
|
+
|
|
162
|
+
Write-Step "Enabling Hyper-V hypervisor..."
|
|
163
|
+
bcdedit /set hypervisorlaunchtype auto | Out-Null
|
|
164
|
+
Write-Success "Hyper-V enabled."
|
|
165
|
+
Write-Host ""
|
|
166
|
+
Write-Warn "A restart is required to apply the changes."
|
|
167
|
+
Write-Warn "After restarting, run the same command again:"
|
|
168
|
+
Write-Host " $INSTALL_PS1_COMMAND" -ForegroundColor Yellow
|
|
169
|
+
Write-Host ""
|
|
170
|
+
Read-Host "Press Enter to restart now"
|
|
171
|
+
Restart-Computer -Force
|
|
172
|
+
}
|
|
173
|
+
|
|
137
174
|
# ---------------------------------------------------------------------------
|
|
138
175
|
# Install OpenClacky inside WSL
|
|
139
176
|
# ---------------------------------------------------------------------------
|
|
@@ -173,6 +210,16 @@ Write-Host ""
|
|
|
173
210
|
Write-Host "OpenClacky Installation Script (Windows)" -ForegroundColor Cyan
|
|
174
211
|
Write-Host ""
|
|
175
212
|
|
|
213
|
+
# All subsequent operations (bcdedit, dism, wsl) require admin privileges
|
|
214
|
+
if (-not (Test-IsAdmin)) {
|
|
215
|
+
Write-Fail "Please re-run this script as Administrator:"
|
|
216
|
+
Write-Host ""
|
|
217
|
+
Write-Host " Right-click PowerShell -> 'Run as administrator', then:" -ForegroundColor Yellow
|
|
218
|
+
Write-Host " $INSTALL_PS1_COMMAND" -ForegroundColor Yellow
|
|
219
|
+
exit 1
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
Ensure-HyperV
|
|
176
223
|
Install-Wsl
|
|
177
224
|
|
|
178
225
|
Write-Success "WSL is ready."
|
data/scripts/install_simple.sh
CHANGED
|
@@ -434,18 +434,19 @@ setup_gem_home() {
|
|
|
434
434
|
|
|
435
435
|
export GEM_HOME="$HOME/.gem/ruby/${ruby_api}"
|
|
436
436
|
export GEM_PATH="$HOME/.gem/ruby/${ruby_api}"
|
|
437
|
-
export PATH="$HOME/.gem/ruby/${ruby_api}/bin:$PATH"
|
|
437
|
+
export PATH="$HOME/.local/bin:$HOME/.gem/ruby/${ruby_api}/bin:$PATH"
|
|
438
438
|
|
|
439
439
|
print_info "System Ruby detected — gems will install to ~/.gem/ruby/${ruby_api}"
|
|
440
440
|
|
|
441
441
|
# Persist to shell rc (use $HOME so the line is portable)
|
|
442
|
+
# Also add ~/.local/bin so brand wrapper commands installed there are found
|
|
442
443
|
if [ -n "$SHELL_RC" ] && ! grep -q "GEM_HOME" "$SHELL_RC" 2>/dev/null; then
|
|
443
444
|
{
|
|
444
445
|
echo ""
|
|
445
446
|
echo "# Ruby user gem dir (added by openclacky installer)"
|
|
446
447
|
echo "export GEM_HOME=\"\$HOME/.gem/ruby/${ruby_api}\""
|
|
447
448
|
echo "export GEM_PATH=\"\$HOME/.gem/ruby/${ruby_api}\""
|
|
448
|
-
echo "export PATH=\"\$HOME/.gem/ruby/${ruby_api}/bin:\$PATH\""
|
|
449
|
+
echo "export PATH=\"\$HOME/.local/bin:\$HOME/.gem/ruby/${ruby_api}/bin:\$PATH\""
|
|
449
450
|
} >> "$SHELL_RC"
|
|
450
451
|
print_info "GEM_HOME written to $SHELL_RC"
|
|
451
452
|
fi
|