openclacky 0.9.7 → 0.9.9
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/.clacky/skills/gem-release/SKILL.md +16 -1
- data/CHANGELOG.md +51 -0
- data/docs/browser-cdp-native-design.md +195 -0
- data/docs/session-management-redesign.md +202 -0
- data/docs/system-skill-authoring-guide.md +47 -0
- data/lib/clacky/agent/cost_tracker.rb +2 -1
- data/lib/clacky/agent/message_compressor_helper.rb +2 -2
- data/lib/clacky/agent/session_serializer.rb +36 -3
- data/lib/clacky/agent/skill_manager.rb +68 -5
- data/lib/clacky/agent/system_prompt_builder.rb +30 -13
- data/lib/clacky/agent/tool_executor.rb +8 -12
- data/lib/clacky/agent.rb +172 -30
- data/lib/clacky/brand_config.rb +90 -0
- data/lib/clacky/cli.rb +37 -8
- data/lib/clacky/default_parsers/doc_parser.rb +69 -0
- data/lib/clacky/default_parsers/docx_parser.rb +172 -0
- data/lib/clacky/default_parsers/pdf_parser.rb +79 -0
- data/lib/clacky/default_parsers/pptx_parser.rb +140 -0
- data/lib/clacky/default_parsers/xlsx_parser.rb +121 -0
- data/lib/clacky/default_skills/browser-setup/SKILL.md +158 -0
- data/lib/clacky/default_skills/channel-setup/SKILL.md +139 -42
- data/lib/clacky/default_skills/channel-setup/feishu_setup.rb +582 -0
- data/lib/clacky/default_skills/channel-setup/weixin_setup.rb +274 -0
- data/lib/clacky/default_skills/onboard/SKILL.md +132 -6
- data/lib/clacky/default_skills/personal-website/SKILL.md +113 -0
- data/lib/clacky/default_skills/personal-website/publish.rb +214 -0
- data/lib/clacky/default_skills/skill-add/SKILL.md +6 -7
- data/lib/clacky/default_skills/skill-add/scripts/install_from_zip.rb +1 -1
- data/lib/clacky/default_skills/skill-creator/SKILL.md +3 -3
- data/lib/clacky/json_ui_controller.rb +5 -0
- data/lib/clacky/message_format/open_ai.rb +7 -1
- data/lib/clacky/plain_ui_controller.rb +5 -0
- data/lib/clacky/server/browser_manager.rb +308 -0
- data/lib/clacky/server/channel/adapters/feishu/adapter.rb +80 -15
- data/lib/clacky/server/channel/adapters/feishu/bot.rb +216 -7
- data/lib/clacky/server/channel/adapters/feishu/message_parser.rb +12 -0
- data/lib/clacky/server/channel/adapters/wecom/adapter.rb +2 -8
- data/lib/clacky/server/channel/adapters/weixin/adapter.rb +391 -0
- data/lib/clacky/server/channel/adapters/weixin/api_client.rb +374 -0
- data/lib/clacky/server/channel/channel_config.rb +6 -0
- data/lib/clacky/server/channel/channel_manager.rb +50 -15
- data/lib/clacky/server/channel/channel_ui_controller.rb +18 -0
- data/lib/clacky/server/channel.rb +1 -0
- data/lib/clacky/server/http_server.rb +210 -45
- data/lib/clacky/server/scheduler.rb +1 -1
- data/lib/clacky/server/session_registry.rb +131 -40
- data/lib/clacky/server/web_ui_controller.rb +21 -2
- data/lib/clacky/session_manager.rb +43 -73
- data/lib/clacky/skill.rb +64 -42
- data/lib/clacky/tools/browser.rb +728 -183
- data/lib/clacky/tools/invoke_skill.rb +18 -11
- data/lib/clacky/tools/shell.rb +16 -7
- data/lib/clacky/tools/web_fetch.rb +3 -1
- data/lib/clacky/ui2/README.md +1 -1
- data/lib/clacky/ui2/components/input_area.rb +5 -2
- data/lib/clacky/ui2/components/welcome_banner.rb +23 -0
- data/lib/clacky/ui2/ui_controller.rb +16 -2
- data/lib/clacky/ui_interface.rb +7 -0
- data/lib/clacky/utils/file_processor.rb +116 -139
- data/lib/clacky/utils/model_pricing.rb +5 -4
- data/lib/clacky/utils/parser_manager.rb +93 -0
- data/lib/clacky/utils/workspace_rules.rb +46 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +696 -62
- data/lib/clacky/web/app.js +181 -51
- data/lib/clacky/web/brand.js +14 -0
- data/lib/clacky/web/channels.js +10 -1
- data/lib/clacky/web/i18n.js +49 -7
- data/lib/clacky/web/index.html +67 -10
- data/lib/clacky/web/onboard.js +2 -1
- data/lib/clacky/web/sessions.js +255 -69
- data/lib/clacky/web/settings.js +113 -1
- data/lib/clacky/web/skills.js +16 -3
- data/lib/clacky/web/tasks.js +11 -6
- data/lib/clacky/web/weixin-qr.html +104 -0
- data/lib/clacky.rb +2 -0
- data/scripts/install.sh +41 -11
- metadata +48 -7
- data/lib/clacky/default_skills/pdf-reader/SKILL.md +0 -90
- data/lib/clacky/utils/file_parser/docx_parser.rb +0 -156
- data/lib/clacky/utils/file_parser/pptx_parser.rb +0 -116
- data/lib/clacky/utils/file_parser/xlsx_parser.rb +0 -95
- data/lib/clacky/utils/file_parser/zip_parser.rb +0 -60
- data/scripts/install_agent_browser.sh +0 -67
|
@@ -31,7 +31,12 @@ module Clacky
|
|
|
31
31
|
def show_user_message(content, created_at: nil, files: [])
|
|
32
32
|
ev = { type: "history_user_message", session_id: @session_id, content: content }
|
|
33
33
|
ev[:created_at] = created_at if created_at
|
|
34
|
-
|
|
34
|
+
rendered = Array(files).filter_map do |f|
|
|
35
|
+
url = f[:data_url] || f["data_url"]
|
|
36
|
+
name = f[:name] || f["name"]
|
|
37
|
+
url || (name ? "pdf:#{name}" : nil)
|
|
38
|
+
end
|
|
39
|
+
ev[:images] = rendered unless rendered.empty?
|
|
35
40
|
@events << ev
|
|
36
41
|
end
|
|
37
42
|
|
|
@@ -127,7 +132,7 @@ module Clacky
|
|
|
127
132
|
- 将大目标拆解为可执行的小步骤
|
|
128
133
|
MD
|
|
129
134
|
|
|
130
|
-
def initialize(host: "127.0.0.1", port: 7070, agent_config:, client_factory:, brand_test: false)
|
|
135
|
+
def initialize(host: "127.0.0.1", port: 7070, agent_config:, client_factory:, brand_test: false, sessions_dir: nil)
|
|
131
136
|
@host = host
|
|
132
137
|
@port = port
|
|
133
138
|
@agent_config = agent_config
|
|
@@ -137,16 +142,16 @@ module Clacky
|
|
|
137
142
|
# so api_restart can re-exec the correct binary even if cwd changes later.
|
|
138
143
|
@restart_script = File.expand_path($0)
|
|
139
144
|
@restart_argv = ARGV.dup
|
|
140
|
-
@
|
|
141
|
-
@
|
|
145
|
+
@session_manager = Clacky::SessionManager.new(sessions_dir: sessions_dir)
|
|
146
|
+
@registry = SessionRegistry.new(
|
|
147
|
+
session_manager: @session_manager,
|
|
148
|
+
session_restorer: method(:build_session_from_data)
|
|
149
|
+
)
|
|
142
150
|
@ws_clients = {} # session_id => [WebSocketConnection, ...]
|
|
143
151
|
@ws_mutex = Mutex.new
|
|
144
152
|
# Version cache: { latest: "x.y.z", checked_at: Time }
|
|
145
153
|
@version_cache = nil
|
|
146
154
|
@version_mutex = Mutex.new
|
|
147
|
-
# Version cache: { latest: "x.y.z", checked_at: Time }
|
|
148
|
-
@version_cache = nil
|
|
149
|
-
@version_mutex = Mutex.new
|
|
150
155
|
@scheduler = Scheduler.new(
|
|
151
156
|
session_registry: @registry,
|
|
152
157
|
session_builder: method(:build_session)
|
|
@@ -158,6 +163,7 @@ module Clacky
|
|
|
158
163
|
interrupt_session: method(:interrupt_session),
|
|
159
164
|
channel_config: Clacky::ChannelConfig.load
|
|
160
165
|
)
|
|
166
|
+
@browser_manager = Clacky::BrowserManager.instance
|
|
161
167
|
@skill_loader = Clacky::SkillLoader.new(working_dir: nil, brand_config: Clacky::BrandConfig.load)
|
|
162
168
|
end
|
|
163
169
|
|
|
@@ -171,6 +177,14 @@ module Clacky
|
|
|
171
177
|
File.write(pid_file, Process.pid.to_s)
|
|
172
178
|
at_exit { File.delete(pid_file) if File.exist?(pid_file) }
|
|
173
179
|
|
|
180
|
+
# Expose server address and brand name to all child processes (skill scripts, shell commands, etc.)
|
|
181
|
+
# so they can call back into the server without hardcoding the port,
|
|
182
|
+
# and use the correct product name without re-reading brand.yml.
|
|
183
|
+
ENV["CLACKY_SERVER_PORT"] = @port.to_s
|
|
184
|
+
ENV["CLACKY_SERVER_HOST"] = (@host == "0.0.0.0" ? "127.0.0.1" : @host)
|
|
185
|
+
product_name = Clacky::BrandConfig.load.product_name
|
|
186
|
+
ENV["CLACKY_PRODUCT_NAME"] = (product_name.nil? || product_name.strip.empty?) ? "OpenClacky" : product_name
|
|
187
|
+
|
|
174
188
|
# Override WEBrick's built-in signal traps via StartCallback,
|
|
175
189
|
# which fires after WEBrick sets its own INT/TERM handlers.
|
|
176
190
|
# This ensures Ctrl-C always exits immediately.
|
|
@@ -243,6 +257,9 @@ module Clacky
|
|
|
243
257
|
# Start IM channel adapters (non-blocking — each platform runs in its own thread)
|
|
244
258
|
@channel_manager.start
|
|
245
259
|
|
|
260
|
+
# Start browser MCP daemon if browser.yml is configured (non-blocking)
|
|
261
|
+
@browser_manager.start
|
|
262
|
+
|
|
246
263
|
server.start
|
|
247
264
|
end
|
|
248
265
|
|
|
@@ -261,7 +278,7 @@ module Clacky
|
|
|
261
278
|
end
|
|
262
279
|
|
|
263
280
|
case [method, path]
|
|
264
|
-
when ["GET", "/api/sessions"] then api_list_sessions(res)
|
|
281
|
+
when ["GET", "/api/sessions"] then api_list_sessions(req, res)
|
|
265
282
|
when ["POST", "/api/sessions"] then api_create_session(req, res)
|
|
266
283
|
when ["GET", "/api/schedules"] then api_list_schedules(res)
|
|
267
284
|
when ["POST", "/api/schedules"] then api_create_schedule(req, res)
|
|
@@ -274,6 +291,10 @@ module Clacky
|
|
|
274
291
|
when ["POST", "/api/config/test"] then api_test_config(req, res)
|
|
275
292
|
when ["GET", "/api/providers"] then api_list_providers(res)
|
|
276
293
|
when ["GET", "/api/onboard/status"] then api_onboard_status(res)
|
|
294
|
+
when ["GET", "/api/browser/status"] then api_browser_status(res)
|
|
295
|
+
when ["POST", "/api/browser/configure"] then api_browser_configure(req, res)
|
|
296
|
+
when ["POST", "/api/browser/reload"] then api_browser_reload(res)
|
|
297
|
+
when ["POST", "/api/browser/toggle"] then api_browser_toggle(res)
|
|
277
298
|
when ["POST", "/api/onboard/complete"] then api_onboard_complete(req, res)
|
|
278
299
|
when ["POST", "/api/onboard/skip-soul"] then api_onboard_skip_soul(req, res)
|
|
279
300
|
when ["GET", "/api/store/skills"] then api_store_skills(res)
|
|
@@ -282,6 +303,7 @@ module Clacky
|
|
|
282
303
|
when ["GET", "/api/brand/skills"] then api_brand_skills(res)
|
|
283
304
|
when ["GET", "/api/brand"] then api_brand_info(res)
|
|
284
305
|
when ["GET", "/api/channels"] then api_list_channels(res)
|
|
306
|
+
when ["POST", "/api/tool/browser"] then api_tool_browser(req, res)
|
|
285
307
|
when ["POST", "/api/upload"] then api_upload_file(req, res)
|
|
286
308
|
when ["GET", "/api/version"] then api_get_version(res)
|
|
287
309
|
when ["POST", "/api/version/upgrade"] then api_upgrade_version(req, res)
|
|
@@ -337,8 +359,17 @@ module Clacky
|
|
|
337
359
|
|
|
338
360
|
# ── REST API ──────────────────────────────────────────────────────────────
|
|
339
361
|
|
|
340
|
-
def api_list_sessions(res)
|
|
341
|
-
|
|
362
|
+
def api_list_sessions(req, res)
|
|
363
|
+
query = URI.decode_www_form(req.query_string.to_s).to_h
|
|
364
|
+
limit = [query["limit"].to_i.then { |n| n > 0 ? n : 10 }, 50].min
|
|
365
|
+
before = query["before"].to_s.strip.then { |v| v.empty? ? nil : v }
|
|
366
|
+
source = query["source"].to_s.strip.then { |v| v.empty? ? nil : v }
|
|
367
|
+
profile = query["profile"].to_s.strip.then { |v| v.empty? ? nil : v }
|
|
368
|
+
# Fetch one extra to detect has_more without a separate count query
|
|
369
|
+
sessions = @registry.list(limit: limit + 1, before: before, source: source, profile: profile)
|
|
370
|
+
has_more = sessions.size > limit
|
|
371
|
+
sessions = sessions.first(limit)
|
|
372
|
+
json_response(res, 200, { sessions: sessions, has_more: has_more })
|
|
342
373
|
end
|
|
343
374
|
|
|
344
375
|
def api_create_session(req, res)
|
|
@@ -346,11 +377,20 @@ module Clacky
|
|
|
346
377
|
name = body["name"]
|
|
347
378
|
return json_response(res, 400, { error: "name is required" }) if name.nil? || name.strip.empty?
|
|
348
379
|
|
|
380
|
+
# Optional agent_profile; defaults to "general" if omitted or invalid
|
|
381
|
+
profile = body["agent_profile"].to_s.strip
|
|
382
|
+
profile = "general" if profile.empty?
|
|
383
|
+
|
|
384
|
+
# Optional source; defaults to :manual. Accept "system" for skill-launched sessions
|
|
385
|
+
# (e.g. /onboard, /browser-setup, /channel-setup).
|
|
386
|
+
raw_source = body["source"].to_s.strip
|
|
387
|
+
source = %w[manual cron channel setup].include?(raw_source) ? raw_source.to_sym : :manual
|
|
388
|
+
|
|
349
389
|
working_dir = default_working_dir
|
|
350
390
|
FileUtils.mkdir_p(working_dir)
|
|
351
391
|
|
|
352
|
-
session_id = build_session(name: name, working_dir: working_dir)
|
|
353
|
-
json_response(res, 201, { session: @registry.
|
|
392
|
+
session_id = build_session(name: name, working_dir: working_dir, profile: profile, source: source)
|
|
393
|
+
json_response(res, 201, { session: @registry.session_summary(session_id) })
|
|
354
394
|
end
|
|
355
395
|
|
|
356
396
|
# Auto-restore persisted sessions (or create a fresh default) when the server starts.
|
|
@@ -363,15 +403,13 @@ module Clacky
|
|
|
363
403
|
def create_default_session
|
|
364
404
|
return unless @agent_config.models_configured?
|
|
365
405
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
# Restore the most recent 5 sessions for this working directory
|
|
370
|
-
sessions_data = @session_manager.latest_n_for_directory(working_dir, 5)
|
|
406
|
+
# Restore up to 5 sessions per source type from disk into the registry.
|
|
407
|
+
@registry.restore_from_disk(n: 5)
|
|
371
408
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
409
|
+
# If nothing was restored (no persisted sessions), create a fresh default.
|
|
410
|
+
unless @registry.list(limit: 1).any?
|
|
411
|
+
working_dir = default_working_dir
|
|
412
|
+
FileUtils.mkdir_p(working_dir) unless Dir.exist?(working_dir)
|
|
375
413
|
build_session(name: "Session 1", working_dir: working_dir)
|
|
376
414
|
end
|
|
377
415
|
end
|
|
@@ -390,12 +428,50 @@ module Clacky
|
|
|
390
428
|
end
|
|
391
429
|
end
|
|
392
430
|
|
|
431
|
+
# GET /api/browser/status
|
|
432
|
+
# Returns real daemon liveness from BrowserManager (not just yml read).
|
|
433
|
+
def api_browser_status(res)
|
|
434
|
+
json_response(res, 200, @browser_manager.status)
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# POST /api/browser/configure
|
|
438
|
+
# Called by browser-setup skill to write browser.yml and hot-reload the daemon.
|
|
439
|
+
# Body: { chrome_version: "146" }
|
|
440
|
+
def api_browser_configure(req, res)
|
|
441
|
+
body = JSON.parse(req.body.to_s) rescue {}
|
|
442
|
+
chrome_version = body["chrome_version"].to_s.strip
|
|
443
|
+
return json_response(res, 422, { ok: false, error: "chrome_version is required" }) if chrome_version.empty?
|
|
444
|
+
|
|
445
|
+
@browser_manager.configure(chrome_version: chrome_version)
|
|
446
|
+
json_response(res, 200, { ok: true })
|
|
447
|
+
rescue StandardError => e
|
|
448
|
+
json_response(res, 500, { ok: false, error: e.message })
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
# POST /api/browser/reload
|
|
452
|
+
# Called by browser-setup skill after writing browser.yml.
|
|
453
|
+
# Hot-reloads the MCP daemon with the new configuration.
|
|
454
|
+
def api_browser_reload(res)
|
|
455
|
+
@browser_manager.reload
|
|
456
|
+
json_response(res, 200, { ok: true })
|
|
457
|
+
rescue StandardError => e
|
|
458
|
+
json_response(res, 500, { ok: false, error: e.message })
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# POST /api/browser/toggle
|
|
462
|
+
def api_browser_toggle(res)
|
|
463
|
+
enabled = @browser_manager.toggle
|
|
464
|
+
json_response(res, 200, { ok: true, enabled: enabled })
|
|
465
|
+
rescue StandardError => e
|
|
466
|
+
json_response(res, 500, { ok: false, error: e.message })
|
|
467
|
+
end
|
|
468
|
+
|
|
393
469
|
# POST /api/onboard/complete
|
|
394
470
|
# Called after key setup is done (soul_setup is optional/skipped).
|
|
395
471
|
# Creates the default session if none exists yet, returns it.
|
|
396
472
|
def api_onboard_complete(req, res)
|
|
397
|
-
create_default_session if @registry.list.empty?
|
|
398
|
-
first_session = @registry.list.first
|
|
473
|
+
create_default_session if @registry.list(limit: 1).empty?
|
|
474
|
+
first_session = @registry.list(limit: 1).first
|
|
399
475
|
json_response(res, 200, { ok: true, session: first_session })
|
|
400
476
|
end
|
|
401
477
|
|
|
@@ -674,8 +750,17 @@ module Clacky
|
|
|
674
750
|
argv = @restart_argv
|
|
675
751
|
Thread.new do
|
|
676
752
|
sleep 0.5 # Let WEBrick flush the HTTP response
|
|
677
|
-
|
|
678
|
-
exec
|
|
753
|
+
|
|
754
|
+
# Use login shell to re-exec so rbenv/mise shims resolve the newly installed gem version.
|
|
755
|
+
# Direct `exec(RbConfig.ruby, script, *argv)` would reuse the old Ruby interpreter path
|
|
756
|
+
# and miss gem updates installed under a different Ruby version managed by rbenv/mise.
|
|
757
|
+
shell = ENV["SHELL"].to_s
|
|
758
|
+
shell = "/bin/bash" if shell.empty?
|
|
759
|
+
cmd_parts = [Shellwords.escape(script), *argv.map { |a| Shellwords.escape(a) }]
|
|
760
|
+
cmd_string = cmd_parts.join(" ")
|
|
761
|
+
|
|
762
|
+
Clacky::Logger.info("[Restart] exec: #{shell} -l -c #{cmd_string}")
|
|
763
|
+
exec(shell, "-l", "-c", cmd_string)
|
|
679
764
|
end
|
|
680
765
|
end
|
|
681
766
|
|
|
@@ -753,6 +838,28 @@ module Clacky
|
|
|
753
838
|
|
|
754
839
|
# GET /api/channels
|
|
755
840
|
# Returns current config and running status for all supported platforms.
|
|
841
|
+
# POST /api/tool/browser
|
|
842
|
+
# Executes a browser tool action via the shared BrowserManager daemon.
|
|
843
|
+
# Used by skill scripts (e.g. feishu_setup.rb) to reuse the server's
|
|
844
|
+
# existing Chrome connection without spawning a second MCP daemon.
|
|
845
|
+
#
|
|
846
|
+
# Request body: JSON with same params as the browser tool
|
|
847
|
+
# { "action": "snapshot", "interactive": true, ... }
|
|
848
|
+
#
|
|
849
|
+
# Response: JSON result from the browser tool
|
|
850
|
+
def api_tool_browser(req, res)
|
|
851
|
+
params = parse_json_body(req)
|
|
852
|
+
action = params["action"]
|
|
853
|
+
return json_response(res, 400, { error: "action is required" }) if action.nil? || action.empty?
|
|
854
|
+
|
|
855
|
+
tool = Clacky::Tools::Browser.new
|
|
856
|
+
result = tool.execute(**params.transform_keys(&:to_sym))
|
|
857
|
+
|
|
858
|
+
json_response(res, 200, result)
|
|
859
|
+
rescue StandardError => e
|
|
860
|
+
json_response(res, 500, { error: e.message })
|
|
861
|
+
end
|
|
862
|
+
|
|
756
863
|
def api_list_channels(res)
|
|
757
864
|
config = Clacky::ChannelConfig.load
|
|
758
865
|
running = @channel_manager.running_platforms
|
|
@@ -782,18 +889,12 @@ module Clacky
|
|
|
782
889
|
return
|
|
783
890
|
end
|
|
784
891
|
|
|
785
|
-
|
|
892
|
+
saved = Clacky::Utils::FileProcessor.save(
|
|
786
893
|
body: upload[:data],
|
|
787
894
|
filename: upload[:filename].to_s
|
|
788
895
|
)
|
|
789
896
|
|
|
790
|
-
json_response(res, 200, {
|
|
791
|
-
ok: true,
|
|
792
|
-
name: ref.name,
|
|
793
|
-
type: ref.type.to_s,
|
|
794
|
-
path: ref.original_path,
|
|
795
|
-
preview_path: ref.preview_path
|
|
796
|
-
})
|
|
897
|
+
json_response(res, 200, { ok: true, name: saved[:name], path: saved[:path] })
|
|
797
898
|
rescue => e
|
|
798
899
|
json_response(res, 500, { ok: false, error: e.message })
|
|
799
900
|
end
|
|
@@ -808,6 +909,20 @@ module Clacky
|
|
|
808
909
|
|
|
809
910
|
fields = body.transform_keys(&:to_sym).reject { |k, _| k == :platform }
|
|
810
911
|
fields = fields.transform_values { |v| v.is_a?(String) ? v.strip : v }
|
|
912
|
+
|
|
913
|
+
# Validate credentials against live API before persisting.
|
|
914
|
+
# Merge with existing config so partial updates (e.g. allowed_users only) still validate correctly.
|
|
915
|
+
klass = Clacky::Channel::Adapters.find(platform)
|
|
916
|
+
if klass && klass.respond_to?(:test_connection)
|
|
917
|
+
existing = config.platform_config(platform) || {}
|
|
918
|
+
merged = existing.merge(fields)
|
|
919
|
+
result = klass.test_connection(merged)
|
|
920
|
+
unless result[:ok]
|
|
921
|
+
json_response(res, 422, { ok: false, error: result[:error] || "Credential validation failed" })
|
|
922
|
+
return
|
|
923
|
+
end
|
|
924
|
+
end
|
|
925
|
+
|
|
811
926
|
config.set_platform(platform, **fields)
|
|
812
927
|
config.save
|
|
813
928
|
|
|
@@ -868,6 +983,13 @@ module Clacky
|
|
|
868
983
|
{
|
|
869
984
|
bot_id: raw["bot_id"] || ""
|
|
870
985
|
}
|
|
986
|
+
when :weixin
|
|
987
|
+
{
|
|
988
|
+
base_url: raw["base_url"] || Clacky::Channel::Adapters::Weixin::ApiClient::DEFAULT_BASE_URL,
|
|
989
|
+
allowed_users: raw["allowed_users"] || [],
|
|
990
|
+
# Indicate whether a token is present (never expose the token itself)
|
|
991
|
+
has_token: !raw["token"].to_s.strip.empty?
|
|
992
|
+
}
|
|
871
993
|
else
|
|
872
994
|
{}
|
|
873
995
|
end
|
|
@@ -1039,8 +1161,7 @@ module Clacky
|
|
|
1039
1161
|
# after the client has subscribed and is ready to receive broadcasts.
|
|
1040
1162
|
@registry.update(session_id, pending_task: prompt, pending_working_dir: working_dir)
|
|
1041
1163
|
|
|
1042
|
-
|
|
1043
|
-
json_response(res, 202, { ok: true, session: session })
|
|
1164
|
+
json_response(res, 202, { ok: true, session: @registry.session_summary(session_id) })
|
|
1044
1165
|
rescue => e
|
|
1045
1166
|
json_response(res, 422, { error: e.message })
|
|
1046
1167
|
end
|
|
@@ -1071,6 +1192,10 @@ module Clacky
|
|
|
1071
1192
|
# filtered by the session's agent profile. Used by the frontend slash-command
|
|
1072
1193
|
# autocomplete so only skills valid for the current profile are suggested.
|
|
1073
1194
|
def api_session_skills(session_id, res)
|
|
1195
|
+
unless @registry.ensure(session_id)
|
|
1196
|
+
json_response(res, 404, { error: "Session not found" })
|
|
1197
|
+
return
|
|
1198
|
+
end
|
|
1074
1199
|
session = @registry.get(session_id)
|
|
1075
1200
|
unless session
|
|
1076
1201
|
json_response(res, 404, { error: "Session not found" })
|
|
@@ -1089,10 +1214,16 @@ module Clacky
|
|
|
1089
1214
|
skills = agent.skill_loader.user_invocable_skills
|
|
1090
1215
|
skills = skills.select { |s| s.allowed_for_agent?(profile.name) } if profile
|
|
1091
1216
|
|
|
1217
|
+
loader = agent.skill_loader
|
|
1218
|
+
loaded_from = loader.loaded_from
|
|
1219
|
+
|
|
1092
1220
|
skill_data = skills.map do |skill|
|
|
1221
|
+
source_type = loaded_from[skill.identifier]
|
|
1093
1222
|
{
|
|
1094
1223
|
name: skill.identifier,
|
|
1095
|
-
description: skill.description || skill.context_description
|
|
1224
|
+
description: skill.description || skill.context_description,
|
|
1225
|
+
encrypted: skill.encrypted?,
|
|
1226
|
+
source_type: source_type
|
|
1096
1227
|
}
|
|
1097
1228
|
end
|
|
1098
1229
|
|
|
@@ -1326,7 +1457,7 @@ module Clacky
|
|
|
1326
1457
|
# Replays conversation history for a session via the agent's replay_history method.
|
|
1327
1458
|
# Returns a list of UI events (same format as WS events) for the frontend to render.
|
|
1328
1459
|
def api_session_messages(session_id, req, res)
|
|
1329
|
-
unless @registry.
|
|
1460
|
+
unless @registry.ensure(session_id)
|
|
1330
1461
|
return json_response(res, 404, { error: "Session not found" })
|
|
1331
1462
|
end
|
|
1332
1463
|
|
|
@@ -1355,7 +1486,7 @@ module Clacky
|
|
|
1355
1486
|
new_name = body["name"].to_s.strip
|
|
1356
1487
|
|
|
1357
1488
|
return json_response(res, 400, { error: "name is required" }) if new_name.empty?
|
|
1358
|
-
return json_response(res, 404, { error: "Session not found" }) unless @registry.
|
|
1489
|
+
return json_response(res, 404, { error: "Session not found" }) unless @registry.ensure(session_id)
|
|
1359
1490
|
|
|
1360
1491
|
agent = nil
|
|
1361
1492
|
@registry.with_session(session_id) { |s| agent = s[:agent] }
|
|
@@ -1369,6 +1500,8 @@ module Clacky
|
|
|
1369
1500
|
|
|
1370
1501
|
def api_delete_session(session_id, res)
|
|
1371
1502
|
if @registry.delete(session_id)
|
|
1503
|
+
# Also remove the persisted session file from disk
|
|
1504
|
+
@session_manager.delete(session_id)
|
|
1372
1505
|
# Notify connected clients the session is gone
|
|
1373
1506
|
broadcast(session_id, { type: "session_deleted", session_id: session_id })
|
|
1374
1507
|
unsubscribe_all(session_id)
|
|
@@ -1442,7 +1575,7 @@ module Clacky
|
|
|
1442
1575
|
case type
|
|
1443
1576
|
when "subscribe"
|
|
1444
1577
|
session_id = msg["session_id"]
|
|
1445
|
-
if @registry.
|
|
1578
|
+
if @registry.ensure(session_id)
|
|
1446
1579
|
conn.session_id = session_id
|
|
1447
1580
|
subscribe(session_id, conn)
|
|
1448
1581
|
conn.send_json(type: "subscribed", session_id: session_id)
|
|
@@ -1467,7 +1600,24 @@ module Clacky
|
|
|
1467
1600
|
interrupt_session(session_id)
|
|
1468
1601
|
|
|
1469
1602
|
when "list_sessions"
|
|
1470
|
-
|
|
1603
|
+
# Initial load: 5 per bucket so all tabs/sections get their first page.
|
|
1604
|
+
# General area tabs: manual / cron / channel / setup — filtered by source.
|
|
1605
|
+
# Coding section: profile=coding — source is irrelevant (no source filter).
|
|
1606
|
+
# has_more_by_source drives independent load-more buttons on the frontend.
|
|
1607
|
+
buckets = {
|
|
1608
|
+
"manual" => { source: "manual", profile: "general" },
|
|
1609
|
+
"cron" => { source: "cron", profile: "general" },
|
|
1610
|
+
"channel" => { source: "channel", profile: "general" },
|
|
1611
|
+
"setup" => { source: "setup", profile: "general" },
|
|
1612
|
+
"coding" => { profile: "coding" },
|
|
1613
|
+
}
|
|
1614
|
+
by_bucket = buckets.each_with_object({}) do |(key, params), h|
|
|
1615
|
+
page = @registry.list(limit: 6, **params) # +1 to detect has_more
|
|
1616
|
+
h[key] = { sessions: page.first(5), has_more: page.size > 5 }
|
|
1617
|
+
end
|
|
1618
|
+
all_sessions = by_bucket.values.flat_map { |v| v[:sessions] }.uniq { |s| s[:id] }
|
|
1619
|
+
has_more_map = by_bucket.transform_values { |v| v[:has_more] }
|
|
1620
|
+
conn.send_json(type: "session_list", sessions: all_sessions, has_more_by_source: has_more_map)
|
|
1471
1621
|
|
|
1472
1622
|
when "run_task"
|
|
1473
1623
|
# Client sends this after subscribing to guarantee it's ready to receive
|
|
@@ -1513,6 +1663,11 @@ module Clacky
|
|
|
1513
1663
|
broadcast(session_id, { type: "session_renamed", session_id: session_id, name: auto_name })
|
|
1514
1664
|
end
|
|
1515
1665
|
|
|
1666
|
+
# Broadcast user message through web_ui so channel subscribers (飞书/企微) receive it.
|
|
1667
|
+
web_ui = nil
|
|
1668
|
+
@registry.with_session(session_id) { |s| web_ui = s[:ui] }
|
|
1669
|
+
web_ui&.show_user_message(content, source: :web)
|
|
1670
|
+
|
|
1516
1671
|
# File references are now handled inside agent.run — injected as a system_injected
|
|
1517
1672
|
# message after the user message, so replay_history skips them automatically.
|
|
1518
1673
|
run_agent_task(session_id, agent) { agent.run(content, files: files) }
|
|
@@ -1580,7 +1735,10 @@ module Clacky
|
|
|
1580
1735
|
rescue => e
|
|
1581
1736
|
@registry.update(session_id, status: :error, error: e.message)
|
|
1582
1737
|
broadcast_session_update(session_id)
|
|
1583
|
-
|
|
1738
|
+
# Route error through web_ui so channel subscribers (飞书/企微) receive it too.
|
|
1739
|
+
web_ui = nil
|
|
1740
|
+
@registry.with_session(session_id) { |s| web_ui = s[:ui] }
|
|
1741
|
+
web_ui&.show_error(e.message)
|
|
1584
1742
|
@session_manager.save(agent.to_session_data(status: :error, error_message: e.message))
|
|
1585
1743
|
end
|
|
1586
1744
|
@registry.with_session(session_id) { |s| s[:thread] = thread }
|
|
@@ -1623,7 +1781,7 @@ module Clacky
|
|
|
1623
1781
|
# Broadcast a session_update event to all clients so they can patch their
|
|
1624
1782
|
# local session list without needing a full session_list refresh.
|
|
1625
1783
|
def broadcast_session_update(session_id)
|
|
1626
|
-
session = @registry.list.find { |s| s[:id] == session_id }
|
|
1784
|
+
session = @registry.list(limit: 200).find { |s| s[:id] == session_id }
|
|
1627
1785
|
return unless session
|
|
1628
1786
|
|
|
1629
1787
|
broadcast_all(type: "session_update", session: session)
|
|
@@ -1631,7 +1789,6 @@ module Clacky
|
|
|
1631
1789
|
|
|
1632
1790
|
# ── Helpers ───────────────────────────────────────────────────────────────
|
|
1633
1791
|
|
|
1634
|
-
# Default working directory for new sessions.
|
|
1635
1792
|
def default_working_dir
|
|
1636
1793
|
File.expand_path("~/clacky_workspace")
|
|
1637
1794
|
end
|
|
@@ -1643,7 +1800,7 @@ module Clacky
|
|
|
1643
1800
|
# @param working_dir [String] working directory for the agent
|
|
1644
1801
|
# @param permission_mode [Symbol] :confirm_all (default, human present) or
|
|
1645
1802
|
# :auto_approve (unattended — suppresses request_user_feedback waits)
|
|
1646
|
-
def build_session(name:, working_dir:, permission_mode: :confirm_all, profile: "general")
|
|
1803
|
+
def build_session(name:, working_dir:, permission_mode: :confirm_all, profile: "general", source: :manual)
|
|
1647
1804
|
session_id = Clacky::SessionManager.generate_id
|
|
1648
1805
|
@registry.create(session_id: session_id)
|
|
1649
1806
|
|
|
@@ -1653,7 +1810,7 @@ module Clacky
|
|
|
1653
1810
|
broadcaster = method(:broadcast)
|
|
1654
1811
|
ui = WebUIController.new(session_id, broadcaster)
|
|
1655
1812
|
agent = Clacky::Agent.new(client, config, working_dir: working_dir, ui: ui, profile: profile,
|
|
1656
|
-
session_id: session_id)
|
|
1813
|
+
session_id: session_id, source: source)
|
|
1657
1814
|
agent.rename(name) unless name.nil? || name.empty?
|
|
1658
1815
|
idle_timer = build_idle_timer(session_id, agent)
|
|
1659
1816
|
|
|
@@ -1663,6 +1820,10 @@ module Clacky
|
|
|
1663
1820
|
s[:idle_timer] = idle_timer
|
|
1664
1821
|
end
|
|
1665
1822
|
|
|
1823
|
+
# Persist an initial snapshot so the session is immediately visible in registry.list
|
|
1824
|
+
# (which reads from disk). Without this, new sessions only appear after their first task.
|
|
1825
|
+
@session_manager.save(agent.to_session_data)
|
|
1826
|
+
|
|
1666
1827
|
session_id
|
|
1667
1828
|
end
|
|
1668
1829
|
|
|
@@ -1683,7 +1844,11 @@ module Clacky
|
|
|
1683
1844
|
config.permission_mode = permission_mode
|
|
1684
1845
|
broadcaster = method(:broadcast)
|
|
1685
1846
|
ui = WebUIController.new(original_id, broadcaster)
|
|
1686
|
-
agent
|
|
1847
|
+
# Restore the agent profile from the persisted session; fall back to "general"
|
|
1848
|
+
# for sessions saved before the agent_profile field was introduced.
|
|
1849
|
+
profile = session_data[:agent_profile].to_s
|
|
1850
|
+
profile = "general" if profile.empty?
|
|
1851
|
+
agent = Clacky::Agent.from_session(client, config, session_data, ui: ui, profile: profile)
|
|
1687
1852
|
idle_timer = build_idle_timer(original_id, agent)
|
|
1688
1853
|
|
|
1689
1854
|
@registry.with_session(original_id) do |s|
|
|
@@ -168,7 +168,7 @@ module Clacky
|
|
|
168
168
|
FileUtils.mkdir_p(working_dir)
|
|
169
169
|
|
|
170
170
|
# Scheduled tasks run unattended — use auto_approve so request_user_feedback doesn't block.
|
|
171
|
-
session_id = @session_builder.call(name: name, working_dir: working_dir, permission_mode: :auto_approve)
|
|
171
|
+
session_id = @session_builder.call(name: name, working_dir: working_dir, permission_mode: :auto_approve, source: :cron)
|
|
172
172
|
|
|
173
173
|
Clacky::Logger.info("scheduler_task_fired", task: task_name, session: session_id)
|
|
174
174
|
|