openclacky 0.9.11 → 0.9.13
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 +23 -0
- data/docs/install-script-simplification.md +89 -0
- data/lib/clacky/aes_gcm.rb +205 -0
- data/lib/clacky/agent/message_compressor.rb +7 -2
- data/lib/clacky/agent/message_compressor_helper.rb +16 -0
- data/lib/clacky/agent.rb +3 -1
- data/lib/clacky/brand_config.rb +42 -10
- data/lib/clacky/cli.rb +20 -8
- data/lib/clacky/client.rb +20 -9
- data/lib/clacky/default_skills/channel-setup/weixin_setup.rb +2 -2
- data/lib/clacky/server/browser_manager.rb +1 -1
- data/lib/clacky/server/channel/adapters/feishu/ws_client.rb +43 -33
- data/lib/clacky/server/channel/adapters/wecom/ws_client.rb +41 -34
- data/lib/clacky/server/channel/adapters/weixin/api_client.rb +4 -1
- data/lib/clacky/server/channel/channel_config.rb +1 -1
- data/lib/clacky/server/http_server.rb +75 -47
- data/lib/clacky/server/scheduler.rb +1 -1
- data/lib/clacky/tools/browser.rb +1 -1
- data/lib/clacky/ui2/ui_controller.rb +3 -0
- data/lib/clacky/utils/parser_manager.rb +4 -1
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky.rb +51 -0
- data/scripts/install.sh +343 -198
- data/scripts/install_simple.sh +582 -0
- metadata +50 -15
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "websocket
|
|
3
|
+
require "websocket"
|
|
4
4
|
require "json"
|
|
5
5
|
require "net/http"
|
|
6
6
|
require "uri"
|
|
@@ -47,7 +47,8 @@ module Clacky
|
|
|
47
47
|
def stop
|
|
48
48
|
@running = false
|
|
49
49
|
@ping_thread&.kill
|
|
50
|
-
|
|
50
|
+
send_raw_frame(:close, "") rescue nil
|
|
51
|
+
@ws_socket&.close rescue nil
|
|
51
52
|
end
|
|
52
53
|
|
|
53
54
|
private
|
|
@@ -73,37 +74,48 @@ module Clacky
|
|
|
73
74
|
tcp
|
|
74
75
|
end
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
# WebSocket handshake
|
|
78
|
+
handshake = WebSocket::Handshake::Client.new(url: endpoint)
|
|
79
|
+
socket.write(handshake.to_s)
|
|
78
80
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
# Read until handshake complete
|
|
82
|
+
until handshake.finished?
|
|
83
|
+
handshake << socket.readpartial(4096)
|
|
81
84
|
end
|
|
85
|
+
raise "WebSocket handshake failed" unless handshake.valid?
|
|
82
86
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
@ws.on :error do |event|
|
|
89
|
-
Clacky::Logger.warn("[feishu-ws] WebSocket error: #{event.message}")
|
|
90
|
-
end
|
|
87
|
+
Clacky::Logger.info("[feishu-ws] WebSocket connected")
|
|
88
|
+
@ws_version = handshake.version
|
|
89
|
+
@ws_socket = socket
|
|
90
|
+
@ws_open = true
|
|
91
|
+
@incoming = WebSocket::Frame::Incoming::Client.new(version: @ws_version)
|
|
91
92
|
|
|
92
|
-
@ws.on :close do |event|
|
|
93
|
-
Clacky::Logger.info("[feishu-ws] WebSocket closed (code=#{event.code}), will reconnect")
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
@ws.start
|
|
97
93
|
start_ping_thread
|
|
98
94
|
|
|
99
95
|
loop do
|
|
100
96
|
break unless @running
|
|
101
97
|
data = socket.readpartial(4096)
|
|
102
|
-
@
|
|
98
|
+
@incoming << data
|
|
99
|
+
while (frame = @incoming.next)
|
|
100
|
+
case frame.type
|
|
101
|
+
when :binary
|
|
102
|
+
raw = frame.data
|
|
103
|
+
handle_frame(raw.respond_to?(:b) ? raw.b : raw)
|
|
104
|
+
when :text
|
|
105
|
+
handle_frame(frame.data)
|
|
106
|
+
when :ping
|
|
107
|
+
send_raw_frame(:pong, frame.data)
|
|
108
|
+
when :close
|
|
109
|
+
Clacky::Logger.info("[feishu-ws] WebSocket closed, will reconnect")
|
|
110
|
+
return
|
|
111
|
+
end
|
|
112
|
+
end
|
|
103
113
|
end
|
|
104
114
|
rescue EOFError, Errno::ECONNRESET
|
|
105
115
|
Clacky::Logger.warn("[feishu-ws] Connection lost, reconnecting in #{RECONNECT_DELAY}s...")
|
|
106
116
|
ensure
|
|
117
|
+
@ws_open = false
|
|
118
|
+
@ws_socket = nil
|
|
107
119
|
socket&.close rescue nil
|
|
108
120
|
@ping_thread&.kill
|
|
109
121
|
end
|
|
@@ -211,11 +223,21 @@ module Clacky
|
|
|
211
223
|
payload: payload
|
|
212
224
|
}
|
|
213
225
|
encoded = ProtoFrame.encode(frame)
|
|
214
|
-
|
|
226
|
+
send_raw_frame(:binary, encoded)
|
|
215
227
|
rescue => e
|
|
216
228
|
warn "[feishu-ws] failed to send frame: #{e.message}"
|
|
217
229
|
end
|
|
218
230
|
|
|
231
|
+
def send_raw_frame(type, data)
|
|
232
|
+
return unless @ws_socket && @ws_open
|
|
233
|
+
outgoing = WebSocket::Frame::Outgoing::Client.new(
|
|
234
|
+
version: @ws_version || 13,
|
|
235
|
+
data: data,
|
|
236
|
+
type: type
|
|
237
|
+
)
|
|
238
|
+
@ws_socket.write(outgoing.to_s)
|
|
239
|
+
end
|
|
240
|
+
|
|
219
241
|
def start_ping_thread
|
|
220
242
|
@ping_thread&.kill
|
|
221
243
|
@ping_thread = Thread.new do
|
|
@@ -365,19 +387,7 @@ module Clacky
|
|
|
365
387
|
end
|
|
366
388
|
end
|
|
367
389
|
|
|
368
|
-
# Wraps a raw socket for websocket-driver client mode.
|
|
369
|
-
class SocketWrapper
|
|
370
|
-
attr_reader :url
|
|
371
390
|
|
|
372
|
-
def initialize(socket, url)
|
|
373
|
-
@socket = socket
|
|
374
|
-
@url = url
|
|
375
|
-
end
|
|
376
|
-
|
|
377
|
-
def write(data)
|
|
378
|
-
@socket.write(data)
|
|
379
|
-
end
|
|
380
|
-
end
|
|
381
391
|
end
|
|
382
392
|
end
|
|
383
393
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "websocket
|
|
3
|
+
require "websocket"
|
|
4
4
|
require "json"
|
|
5
5
|
require "uri"
|
|
6
6
|
require "securerandom"
|
|
@@ -58,7 +58,8 @@ module Clacky
|
|
|
58
58
|
def stop
|
|
59
59
|
@running = false
|
|
60
60
|
@ping_thread&.kill
|
|
61
|
-
|
|
61
|
+
send_raw_frame(:close, "") rescue nil
|
|
62
|
+
@ws_socket&.close rescue nil
|
|
62
63
|
end
|
|
63
64
|
|
|
64
65
|
# Proactively send a text message
|
|
@@ -130,37 +131,45 @@ module Clacky
|
|
|
130
131
|
ssl.sync_close = true
|
|
131
132
|
ssl.connect
|
|
132
133
|
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
# WebSocket handshake
|
|
135
|
+
handshake = WebSocket::Handshake::Client.new(url: @ws_url)
|
|
136
|
+
ssl.write(handshake.to_s)
|
|
135
137
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
authenticate
|
|
139
|
-
start_ping_thread
|
|
138
|
+
until handshake.finished?
|
|
139
|
+
handshake << ssl.readpartial(4096)
|
|
140
140
|
end
|
|
141
|
+
raise "WebSocket handshake failed" unless handshake.valid?
|
|
141
142
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
@
|
|
147
|
-
Clacky::Logger.error("[WecomWSClient] WS error: #{event.message}")
|
|
148
|
-
end
|
|
143
|
+
Clacky::Logger.info("[WecomWSClient] connected, authenticating")
|
|
144
|
+
@ws_version = handshake.version
|
|
145
|
+
@ws_socket = ssl
|
|
146
|
+
@ws_open = true
|
|
147
|
+
@incoming = WebSocket::Frame::Incoming::Client.new(version: @ws_version)
|
|
149
148
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
@ws.start
|
|
149
|
+
authenticate
|
|
150
|
+
start_ping_thread
|
|
155
151
|
|
|
156
152
|
loop do
|
|
157
153
|
break unless @running
|
|
158
154
|
data = ssl.readpartial(4096)
|
|
159
|
-
@
|
|
155
|
+
@incoming << data
|
|
156
|
+
while (frame = @incoming.next)
|
|
157
|
+
case frame.type
|
|
158
|
+
when :text
|
|
159
|
+
handle_message(frame.data)
|
|
160
|
+
when :ping
|
|
161
|
+
send_raw_frame(:pong, frame.data)
|
|
162
|
+
when :close
|
|
163
|
+
Clacky::Logger.info("[WecomWSClient] connection closed")
|
|
164
|
+
return
|
|
165
|
+
end
|
|
166
|
+
end
|
|
160
167
|
end
|
|
161
168
|
rescue EOFError, Errno::ECONNRESET
|
|
162
169
|
Clacky::Logger.info("[WecomWSClient] connection lost, reconnecting...")
|
|
163
170
|
ensure
|
|
171
|
+
@ws_open = false
|
|
172
|
+
@ws_socket = nil
|
|
164
173
|
ssl&.close rescue nil
|
|
165
174
|
@ping_thread&.kill
|
|
166
175
|
end
|
|
@@ -227,11 +236,21 @@ module Clacky
|
|
|
227
236
|
else
|
|
228
237
|
Clacky::Logger.info("[WecomWSClient] >> cmd=#{cmd} req_id=#{req_id}")
|
|
229
238
|
end
|
|
230
|
-
|
|
239
|
+
send_raw_frame(:text, JSON.generate(frame))
|
|
231
240
|
rescue => e
|
|
232
241
|
Clacky::Logger.error("[WecomWSClient] failed to send frame cmd=#{cmd}: #{e.message}")
|
|
233
242
|
end
|
|
234
243
|
|
|
244
|
+
def send_raw_frame(type, data)
|
|
245
|
+
return unless @ws_socket && @ws_open
|
|
246
|
+
outgoing = WebSocket::Frame::Outgoing::Client.new(
|
|
247
|
+
version: @ws_version || 13,
|
|
248
|
+
data: data,
|
|
249
|
+
type: type
|
|
250
|
+
)
|
|
251
|
+
@ws_socket.write(outgoing.to_s)
|
|
252
|
+
end
|
|
253
|
+
|
|
235
254
|
def start_ping_thread
|
|
236
255
|
@ping_thread&.kill
|
|
237
256
|
@ping_thread = Thread.new do
|
|
@@ -341,19 +360,7 @@ module Clacky
|
|
|
341
360
|
end
|
|
342
361
|
end
|
|
343
362
|
|
|
344
|
-
# Wraps a raw socket for websocket-driver client mode.
|
|
345
|
-
class SocketWrapper
|
|
346
|
-
attr_reader :url
|
|
347
363
|
|
|
348
|
-
def initialize(socket, url)
|
|
349
|
-
@socket = socket
|
|
350
|
-
@url = url
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
def write(data)
|
|
354
|
-
@socket.write(data)
|
|
355
|
-
end
|
|
356
|
-
end
|
|
357
364
|
end
|
|
358
365
|
end
|
|
359
366
|
end
|
|
@@ -37,7 +37,10 @@ module Clacky
|
|
|
37
37
|
# Raised for non-zero API return codes or HTTP errors.
|
|
38
38
|
class ApiError < StandardError
|
|
39
39
|
attr_reader :code
|
|
40
|
-
def initialize(code, msg)
|
|
40
|
+
def initialize(code, msg)
|
|
41
|
+
@code = code
|
|
42
|
+
super("WeixinApiError(#{code}): #{msg.to_s.slice(0, 200)}")
|
|
43
|
+
end
|
|
41
44
|
end
|
|
42
45
|
|
|
43
46
|
# Raised on network/read timeouts.
|
|
@@ -38,7 +38,7 @@ module Clacky
|
|
|
38
38
|
# @return [ChannelConfig]
|
|
39
39
|
def self.load(config_file = CONFIG_FILE)
|
|
40
40
|
if File.exist?(config_file)
|
|
41
|
-
data =
|
|
41
|
+
data = YAMLCompat.safe_load(File.read(config_file), permitted_classes: [Symbol]) || {}
|
|
42
42
|
else
|
|
43
43
|
data = {}
|
|
44
44
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "webrick"
|
|
4
|
-
require "websocket
|
|
4
|
+
require "websocket"
|
|
5
5
|
require "json"
|
|
6
6
|
require "thread"
|
|
7
7
|
require "fileutils"
|
|
@@ -75,7 +75,7 @@ module Clacky
|
|
|
75
75
|
|
|
76
76
|
# Ignore all other UI methods (progress, errors, etc.) during history replay
|
|
77
77
|
def method_missing(name, *args, **kwargs); end
|
|
78
|
-
def respond_to_missing?(name, include_private = false)
|
|
78
|
+
def respond_to_missing?(name, include_private = false); true; end
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
# HttpServer runs an embedded WEBrick HTTP server with WebSocket support.
|
|
@@ -559,6 +559,23 @@ module Clacky
|
|
|
559
559
|
return
|
|
560
560
|
end
|
|
561
561
|
|
|
562
|
+
# Send heartbeat if interval has elapsed (once per day)
|
|
563
|
+
if brand.heartbeat_due?
|
|
564
|
+
Clacky::Logger.info("[Brand] api_brand_status: heartbeat due, sending...")
|
|
565
|
+
result = brand.heartbeat!
|
|
566
|
+
if result[:success]
|
|
567
|
+
Clacky::Logger.info("[Brand] api_brand_status: heartbeat OK")
|
|
568
|
+
else
|
|
569
|
+
Clacky::Logger.warn("[Brand] api_brand_status: heartbeat failed — #{result[:message]}")
|
|
570
|
+
end
|
|
571
|
+
# Reload after heartbeat to pick up updated expires_at / last_heartbeat
|
|
572
|
+
brand = Clacky::BrandConfig.load
|
|
573
|
+
else
|
|
574
|
+
Clacky::Logger.debug("[Brand] api_brand_status: heartbeat not due yet")
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
Clacky::Logger.debug("[Brand] api_brand_status: expired=#{brand.expired?} grace_exceeded=#{brand.grace_period_exceeded?} expires_at=#{brand.license_expires_at&.iso8601 || "nil"}")
|
|
578
|
+
|
|
562
579
|
warning = nil
|
|
563
580
|
if brand.expired?
|
|
564
581
|
warning = "Your #{brand.product_name} license has expired. Please renew to continue."
|
|
@@ -571,6 +588,8 @@ module Clacky
|
|
|
571
588
|
end
|
|
572
589
|
end
|
|
573
590
|
|
|
591
|
+
Clacky::Logger.debug("[Brand] api_brand_status: warning=#{warning.inspect}")
|
|
592
|
+
|
|
574
593
|
json_response(res, 200, {
|
|
575
594
|
branded: true,
|
|
576
595
|
needs_activation: false,
|
|
@@ -1579,26 +1598,27 @@ module Clacky
|
|
|
1579
1598
|
req["Upgrade"]&.downcase == "websocket"
|
|
1580
1599
|
end
|
|
1581
1600
|
|
|
1582
|
-
# Hijacks the TCP socket from WEBrick and
|
|
1601
|
+
# Hijacks the TCP socket from WEBrick and upgrades it to WebSocket.
|
|
1583
1602
|
def handle_websocket(req, res)
|
|
1584
|
-
# Prevent WEBrick from closing the socket after this handler returns
|
|
1585
1603
|
socket = req.instance_variable_get(:@socket)
|
|
1586
1604
|
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1605
|
+
# Server handshake — parse the upgrade request
|
|
1606
|
+
handshake = WebSocket::Handshake::Server.new
|
|
1607
|
+
handshake << build_handshake_request(req)
|
|
1608
|
+
unless handshake.finished? && handshake.valid?
|
|
1609
|
+
$stderr.puts "WebSocket handshake invalid"
|
|
1610
|
+
return
|
|
1611
|
+
end
|
|
1591
1612
|
|
|
1592
|
-
|
|
1613
|
+
# Send the 101 Switching Protocols response
|
|
1614
|
+
socket.write(handshake.to_s)
|
|
1593
1615
|
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
driver.on(:error) { |event| $stderr.puts "WS error: #{event.message}" }
|
|
1616
|
+
version = handshake.version
|
|
1617
|
+
incoming = WebSocket::Frame::Incoming::Server.new(version: version)
|
|
1618
|
+
conn = WebSocketConnection.new(socket, version)
|
|
1598
1619
|
|
|
1599
|
-
|
|
1620
|
+
on_ws_open(conn)
|
|
1600
1621
|
|
|
1601
|
-
# Read loop — blocks this thread until the socket closes
|
|
1602
1622
|
begin
|
|
1603
1623
|
buf = String.new("", encoding: "BINARY")
|
|
1604
1624
|
loop do
|
|
@@ -1609,14 +1629,27 @@ module Clacky
|
|
|
1609
1629
|
when nil
|
|
1610
1630
|
break # EOF
|
|
1611
1631
|
else
|
|
1612
|
-
|
|
1632
|
+
incoming << chunk.dup
|
|
1633
|
+
while (frame = incoming.next)
|
|
1634
|
+
case frame.type
|
|
1635
|
+
when :text
|
|
1636
|
+
on_ws_message(conn, frame.data)
|
|
1637
|
+
when :binary
|
|
1638
|
+
on_ws_message(conn, frame.data)
|
|
1639
|
+
when :ping
|
|
1640
|
+
conn.send_raw(:pong, frame.data)
|
|
1641
|
+
when :close
|
|
1642
|
+
conn.send_raw(:close, "")
|
|
1643
|
+
break
|
|
1644
|
+
end
|
|
1645
|
+
end
|
|
1613
1646
|
end
|
|
1614
1647
|
end
|
|
1615
1648
|
rescue IOError, Errno::ECONNRESET, Errno::EPIPE
|
|
1616
1649
|
# Client disconnected
|
|
1617
1650
|
ensure
|
|
1618
1651
|
on_ws_close(conn)
|
|
1619
|
-
|
|
1652
|
+
socket.close rescue nil
|
|
1620
1653
|
end
|
|
1621
1654
|
|
|
1622
1655
|
# Tell WEBrick not to send any response (we handled everything)
|
|
@@ -1626,6 +1659,14 @@ module Clacky
|
|
|
1626
1659
|
$stderr.puts "WebSocket handler error: #{e.class}: #{e.message}"
|
|
1627
1660
|
end
|
|
1628
1661
|
|
|
1662
|
+
# Build a raw HTTP request string from WEBrick request for WebSocket::Handshake::Server
|
|
1663
|
+
private def build_handshake_request(req)
|
|
1664
|
+
lines = ["#{req.request_method} #{req.request_uri.request_uri} HTTP/1.1\r\n"]
|
|
1665
|
+
req.each { |k, v| lines << "#{k}: #{v}\r\n" }
|
|
1666
|
+
lines << "\r\n"
|
|
1667
|
+
lines.join
|
|
1668
|
+
end
|
|
1669
|
+
|
|
1629
1670
|
def on_ws_open(conn)
|
|
1630
1671
|
@ws_mutex.synchronize { @all_ws_conns << conn }
|
|
1631
1672
|
# Client will send a "subscribe" message to bind to a session
|
|
@@ -2040,47 +2081,34 @@ module Clacky
|
|
|
2040
2081
|
|
|
2041
2082
|
# ── Inner classes ─────────────────────────────────────────────────────────
|
|
2042
2083
|
|
|
2043
|
-
#
|
|
2044
|
-
class RackEnvAdapter
|
|
2045
|
-
def initialize(req, socket)
|
|
2046
|
-
@req = req
|
|
2047
|
-
@socket = socket
|
|
2048
|
-
end
|
|
2049
|
-
|
|
2050
|
-
def env
|
|
2051
|
-
{
|
|
2052
|
-
"REQUEST_METHOD" => @req.request_method,
|
|
2053
|
-
"HTTP_HOST" => @req["Host"],
|
|
2054
|
-
"REQUEST_URI" => @req.request_uri.to_s,
|
|
2055
|
-
"HTTP_UPGRADE" => @req["Upgrade"],
|
|
2056
|
-
"HTTP_CONNECTION" => @req["Connection"],
|
|
2057
|
-
"HTTP_SEC_WEBSOCKET_KEY" => @req["Sec-WebSocket-Key"],
|
|
2058
|
-
"HTTP_SEC_WEBSOCKET_VERSION" => @req["Sec-WebSocket-Version"],
|
|
2059
|
-
"rack.hijack" => proc {},
|
|
2060
|
-
"rack.input" => StringIO.new
|
|
2061
|
-
}
|
|
2062
|
-
end
|
|
2063
|
-
|
|
2064
|
-
def write(data)
|
|
2065
|
-
@socket.write(data)
|
|
2066
|
-
end
|
|
2067
|
-
end
|
|
2068
|
-
|
|
2069
|
-
# Wraps a raw TCP socket + WebSocket driver, providing a thread-safe send method.
|
|
2084
|
+
# Wraps a raw TCP socket, providing thread-safe WebSocket frame sending.
|
|
2070
2085
|
class WebSocketConnection
|
|
2071
2086
|
attr_accessor :session_id
|
|
2072
2087
|
|
|
2073
|
-
def initialize(socket,
|
|
2088
|
+
def initialize(socket, version)
|
|
2074
2089
|
@socket = socket
|
|
2075
|
-
@
|
|
2090
|
+
@version = version
|
|
2076
2091
|
@send_mutex = Mutex.new
|
|
2077
2092
|
end
|
|
2078
2093
|
|
|
2079
2094
|
def send_json(data)
|
|
2080
|
-
|
|
2095
|
+
send_raw(:text, JSON.generate(data))
|
|
2081
2096
|
rescue => e
|
|
2082
2097
|
$stderr.puts "WS send error: #{e.message}"
|
|
2083
2098
|
end
|
|
2099
|
+
|
|
2100
|
+
def send_raw(type, data)
|
|
2101
|
+
@send_mutex.synchronize do
|
|
2102
|
+
outgoing = WebSocket::Frame::Outgoing::Server.new(
|
|
2103
|
+
version: @version,
|
|
2104
|
+
data: data,
|
|
2105
|
+
type: type
|
|
2106
|
+
)
|
|
2107
|
+
@socket.write(outgoing.to_s)
|
|
2108
|
+
end
|
|
2109
|
+
rescue => e
|
|
2110
|
+
$stderr.puts "WS send_raw error: #{e.message}"
|
|
2111
|
+
end
|
|
2084
2112
|
end
|
|
2085
2113
|
end
|
|
2086
2114
|
end
|
|
@@ -244,7 +244,7 @@ module Clacky
|
|
|
244
244
|
private def load_schedules
|
|
245
245
|
return [] unless File.exist?(SCHEDULES_FILE)
|
|
246
246
|
|
|
247
|
-
data =
|
|
247
|
+
data = YAMLCompat.load_file(SCHEDULES_FILE, permitted_classes: [Symbol])
|
|
248
248
|
Array(data)
|
|
249
249
|
rescue => e
|
|
250
250
|
Clacky::Logger.error("scheduler_load_schedules_error", error: e)
|
data/lib/clacky/tools/browser.rb
CHANGED
|
@@ -234,7 +234,7 @@ module Clacky
|
|
|
234
234
|
end
|
|
235
235
|
|
|
236
236
|
private def browser_enabled?
|
|
237
|
-
config =
|
|
237
|
+
config = YAMLCompat.safe_load(File.read(BROWSER_CONFIG_PATH), permitted_classes: [Date, Time, Symbol])
|
|
238
238
|
config.is_a?(Hash) && config["enabled"] == true
|
|
239
239
|
end
|
|
240
240
|
|
|
@@ -72,11 +72,14 @@ module Clacky
|
|
|
72
72
|
|
|
73
73
|
stdout, stderr, status = Open3.capture3(RbConfig.ruby, parser_path, file_path)
|
|
74
74
|
|
|
75
|
+
# Filter out Ruby/Bundler version warnings that pollute stderr
|
|
76
|
+
clean_stderr = stderr.lines.reject { |l| l.match?(/warning:|already initialized constant/) }.join.strip
|
|
77
|
+
|
|
75
78
|
if status.success? && stdout.strip.length > 0
|
|
76
79
|
{ success: true, text: stdout.strip, error: nil, parser_path: parser_path }
|
|
77
80
|
else
|
|
78
81
|
{ success: false, text: nil,
|
|
79
|
-
error:
|
|
82
|
+
error: clean_stderr.empty? ? "Parser exited with code #{status.exitstatus}" : clean_stderr,
|
|
80
83
|
parser_path: parser_path }
|
|
81
84
|
end
|
|
82
85
|
end
|
data/lib/clacky/version.rb
CHANGED
data/lib/clacky.rb
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# ── Ruby < 2.7 polyfills ──────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
# Enumerable#filter_map was added in Ruby 2.7.
|
|
6
|
+
if RUBY_VERSION < "2.7"
|
|
7
|
+
module Enumerable
|
|
8
|
+
def filter_map(&block)
|
|
9
|
+
return to_enum(:filter_map) unless block
|
|
10
|
+
|
|
11
|
+
each_with_object([]) do |item, result|
|
|
12
|
+
mapped = block.call(item)
|
|
13
|
+
result << mapped if mapped
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# File.absolute_path? was added in Ruby 2.7.
|
|
20
|
+
# Polyfill: a path is absolute if it starts with "/" (Unix) or a drive letter (Windows).
|
|
21
|
+
unless File.respond_to?(:absolute_path?)
|
|
22
|
+
def File.absolute_path?(path)
|
|
23
|
+
File.expand_path(path) == path.to_s
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# URI.encode_uri_component was added in Ruby 3.2.
|
|
28
|
+
# CGI.escape encodes spaces as '+'; replace with '%20' to match URI encoding.
|
|
29
|
+
require "uri"
|
|
30
|
+
require "cgi"
|
|
31
|
+
unless URI.respond_to?(:encode_uri_component)
|
|
32
|
+
def URI.encode_uri_component(str)
|
|
33
|
+
CGI.escape(str.to_s).gsub("+", "%20")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# YAML.safe_load with permitted_classes: keyword was added in Psych 4 (Ruby 3.1).
|
|
38
|
+
# On older Ruby, the second positional argument serves the same purpose.
|
|
39
|
+
# This helper provides a unified interface across Ruby versions.
|
|
40
|
+
module YAMLCompat
|
|
41
|
+
def self.safe_load(yaml_string, permitted_classes: [])
|
|
42
|
+
if Psych::VERSION >= "4.0"
|
|
43
|
+
YAML.safe_load(yaml_string, permitted_classes: permitted_classes)
|
|
44
|
+
else
|
|
45
|
+
YAML.safe_load(yaml_string, permitted_classes)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.load_file(path, permitted_classes: [])
|
|
50
|
+
safe_load(File.read(path), permitted_classes: permitted_classes)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
3
54
|
require_relative "clacky/version"
|
|
4
55
|
require_relative "clacky/message_format/anthropic"
|
|
5
56
|
require_relative "clacky/message_format/open_ai"
|