openclacky 0.9.19 → 0.9.21
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 +27 -0
- data/docs/c-end-user-positioning.md +64 -0
- data/lib/clacky/agent/llm_caller.rb +1 -1
- data/lib/clacky/default_skills/channel-setup/feishu_setup.rb +4 -4
- data/lib/clacky/default_skills/cron-task-creator/SKILL.md +65 -58
- data/lib/clacky/default_skills/skill-creator/SKILL.md +6 -0
- data/lib/clacky/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb +143 -0
- data/lib/clacky/server/http_server.rb +60 -91
- data/lib/clacky/server/scheduler.rb +55 -0
- data/lib/clacky/tools/browser.rb +2 -2
- data/lib/clacky/utils/encoding.rb +21 -0
- data/lib/clacky/utils/environment_detector.rb +9 -6
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +1 -1
- data/lib/clacky/web/tasks.js +10 -26
- data/scripts/install.ps1 +4 -4
- data/scripts/install.sh +50 -31
- metadata +3 -6
- data/lib/clacky/default_skills/cron-task-creator/scripts/list_tasks.rb +0 -121
- data/lib/clacky/default_skills/cron-task-creator/scripts/manage_schedule.rb +0 -149
- data/lib/clacky/default_skills/cron-task-creator/scripts/manage_task.rb +0 -81
- data/lib/clacky/default_skills/cron-task-creator/scripts/task_history.rb +0 -137
- data/scripts/install_simple.sh +0 -630
|
@@ -315,11 +315,8 @@ module Clacky
|
|
|
315
315
|
case [method, path]
|
|
316
316
|
when ["GET", "/api/sessions"] then api_list_sessions(req, res)
|
|
317
317
|
when ["POST", "/api/sessions"] then api_create_session(req, res)
|
|
318
|
-
when ["GET", "/api/
|
|
319
|
-
when ["POST", "/api/
|
|
320
|
-
when ["GET", "/api/tasks"] then api_list_tasks(res)
|
|
321
|
-
when ["POST", "/api/tasks"] then api_create_task(req, res)
|
|
322
|
-
when ["POST", "/api/tasks/run"] then api_run_task(req, res)
|
|
318
|
+
when ["GET", "/api/cron-tasks"] then api_list_cron_tasks(res)
|
|
319
|
+
when ["POST", "/api/cron-tasks"] then api_create_cron_task(req, res)
|
|
323
320
|
when ["GET", "/api/skills"] then api_list_skills(res)
|
|
324
321
|
when ["GET", "/api/config"] then api_get_config(res)
|
|
325
322
|
when ["POST", "/api/config"] then api_save_config(req, res)
|
|
@@ -365,15 +362,15 @@ module Clacky
|
|
|
365
362
|
elsif method == "DELETE" && path.start_with?("/api/sessions/")
|
|
366
363
|
session_id = path.sub("/api/sessions/", "")
|
|
367
364
|
api_delete_session(session_id, res)
|
|
368
|
-
elsif method == "
|
|
369
|
-
name = URI.decode_www_form_component(path.sub("/api/
|
|
370
|
-
|
|
371
|
-
elsif method == "
|
|
372
|
-
name = URI.decode_www_form_component(path.sub("/api/tasks/", ""))
|
|
373
|
-
|
|
374
|
-
elsif method == "DELETE" && path.
|
|
375
|
-
name = URI.decode_www_form_component(path.sub("/api/tasks/", ""))
|
|
376
|
-
|
|
365
|
+
elsif method == "POST" && path.match?(%r{^/api/cron-tasks/[^/]+/run$})
|
|
366
|
+
name = URI.decode_www_form_component(path.sub("/api/cron-tasks/", "").sub("/run", ""))
|
|
367
|
+
api_run_cron_task(name, res)
|
|
368
|
+
elsif method == "PATCH" && path.match?(%r{^/api/cron-tasks/[^/]+$})
|
|
369
|
+
name = URI.decode_www_form_component(path.sub("/api/cron-tasks/", ""))
|
|
370
|
+
api_update_cron_task(name, req, res)
|
|
371
|
+
elsif method == "DELETE" && path.match?(%r{^/api/cron-tasks/[^/]+$})
|
|
372
|
+
name = URI.decode_www_form_component(path.sub("/api/cron-tasks/", ""))
|
|
373
|
+
api_delete_cron_task(name, res)
|
|
377
374
|
elsif method == "PATCH" && path.match?(%r{^/api/skills/[^/]+/toggle$})
|
|
378
375
|
name = URI.decode_www_form_component(path.sub("/api/skills/", "").sub("/toggle", ""))
|
|
379
376
|
api_toggle_skill(name, req, res)
|
|
@@ -1242,109 +1239,81 @@ module Clacky
|
|
|
1242
1239
|
}
|
|
1243
1240
|
end
|
|
1244
1241
|
|
|
1245
|
-
# ── Schedules API ─────────────────────────────────────────────────────────
|
|
1246
1242
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1243
|
+
# ── Cron-Tasks API ───────────────────────────────────────────────────────
|
|
1244
|
+
# Unified API that manages task file + schedule as a single resource.
|
|
1245
|
+
|
|
1246
|
+
# GET /api/cron-tasks
|
|
1247
|
+
def api_list_cron_tasks(res)
|
|
1248
|
+
json_response(res, 200, { cron_tasks: @scheduler.list_cron_tasks })
|
|
1249
1249
|
end
|
|
1250
1250
|
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1251
|
+
# POST /api/cron-tasks — create task file + schedule in one step
|
|
1252
|
+
# Body: { name, content, cron, enabled? }
|
|
1253
|
+
def api_create_cron_task(req, res)
|
|
1254
|
+
body = parse_json_body(req)
|
|
1255
|
+
name = body["name"].to_s.strip
|
|
1256
|
+
content = body["content"].to_s
|
|
1257
|
+
cron = body["cron"].to_s.strip
|
|
1258
|
+
enabled = body.key?("enabled") ? body["enabled"] : true
|
|
1256
1259
|
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
end
|
|
1260
|
+
return json_response(res, 422, { error: "name is required" }) if name.empty?
|
|
1261
|
+
return json_response(res, 422, { error: "content is required" }) if content.empty?
|
|
1262
|
+
return json_response(res, 422, { error: "cron is required" }) if cron.empty?
|
|
1261
1263
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
return
|
|
1264
|
+
fields = cron.strip.split(/\s+/)
|
|
1265
|
+
unless fields.size == 5
|
|
1266
|
+
return json_response(res, 422, { error: "cron must have 5 fields (min hour dom month dow)" })
|
|
1265
1267
|
end
|
|
1266
1268
|
|
|
1267
|
-
@scheduler.
|
|
1269
|
+
@scheduler.create_cron_task(name: name, content: content, cron: cron, enabled: enabled)
|
|
1268
1270
|
json_response(res, 201, { ok: true, name: name })
|
|
1269
1271
|
end
|
|
1270
1272
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
# ── Tasks API ─────────────────────────────────────────────────────────────
|
|
1273
|
+
# PATCH /api/cron-tasks/:name — update content and/or cron/enabled
|
|
1274
|
+
# Body: { content?, cron?, enabled? }
|
|
1275
|
+
def api_update_cron_task(name, req, res)
|
|
1276
|
+
body = parse_json_body(req)
|
|
1277
|
+
content = body["content"]
|
|
1278
|
+
cron = body["cron"]&.to_s&.strip
|
|
1279
|
+
enabled = body["enabled"]
|
|
1280
1280
|
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
content = begin
|
|
1284
|
-
@scheduler.read_task(name)
|
|
1285
|
-
rescue StandardError
|
|
1286
|
-
""
|
|
1287
|
-
end
|
|
1288
|
-
{ name: name, path: @scheduler.task_file_path(name), content: content }
|
|
1281
|
+
if cron && cron.split(/\s+/).size != 5
|
|
1282
|
+
return json_response(res, 422, { error: "cron must have 5 fields (min hour dom month dow)" })
|
|
1289
1283
|
end
|
|
1290
|
-
json_response(res, 200, { tasks: tasks })
|
|
1291
|
-
end
|
|
1292
1284
|
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
json_response(res, 200, { name: name, content: content })
|
|
1285
|
+
@scheduler.update_cron_task(name, content: content, cron: cron, enabled: enabled)
|
|
1286
|
+
json_response(res, 200, { ok: true, name: name })
|
|
1296
1287
|
rescue => e
|
|
1297
1288
|
json_response(res, 404, { error: e.message })
|
|
1298
1289
|
end
|
|
1299
1290
|
|
|
1300
|
-
|
|
1301
|
-
|
|
1291
|
+
# DELETE /api/cron-tasks/:name — remove task file + schedule
|
|
1292
|
+
def api_delete_cron_task(name, res)
|
|
1293
|
+
if @scheduler.delete_cron_task(name)
|
|
1302
1294
|
json_response(res, 200, { ok: true })
|
|
1303
1295
|
else
|
|
1304
|
-
json_response(res, 404, { error: "
|
|
1296
|
+
json_response(res, 404, { error: "Cron task not found: #{name}" })
|
|
1305
1297
|
end
|
|
1306
1298
|
end
|
|
1307
1299
|
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
if name.empty?
|
|
1314
|
-
json_response(res, 422, { error: "name is required" })
|
|
1315
|
-
return
|
|
1316
|
-
end
|
|
1317
|
-
|
|
1318
|
-
@scheduler.write_task(name, content)
|
|
1319
|
-
json_response(res, 201, { ok: true, name: name })
|
|
1320
|
-
end
|
|
1321
|
-
|
|
1322
|
-
def api_run_task(req, res)
|
|
1323
|
-
body = parse_json_body(req)
|
|
1324
|
-
name = body["name"].to_s.strip
|
|
1325
|
-
|
|
1326
|
-
if name.empty?
|
|
1327
|
-
json_response(res, 422, { error: "name is required" })
|
|
1328
|
-
return
|
|
1300
|
+
# POST /api/cron-tasks/:name/run — execute immediately
|
|
1301
|
+
def api_run_cron_task(name, res)
|
|
1302
|
+
unless @scheduler.list_tasks.include?(name)
|
|
1303
|
+
return json_response(res, 404, { error: "Cron task not found: #{name}" })
|
|
1329
1304
|
end
|
|
1330
1305
|
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
FileUtils.mkdir_p(working_dir)
|
|
1336
|
-
|
|
1337
|
-
# Tasks run unattended — use auto_approve so request_user_feedback doesn't block.
|
|
1338
|
-
session_id = build_session(name: session_name, working_dir: working_dir, permission_mode: :auto_approve)
|
|
1306
|
+
prompt = @scheduler.read_task(name)
|
|
1307
|
+
session_name = "▶ #{name} #{Time.now.strftime("%H:%M")}"
|
|
1308
|
+
working_dir = File.expand_path("~/clacky_workspace")
|
|
1309
|
+
FileUtils.mkdir_p(working_dir)
|
|
1339
1310
|
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
@registry.update(session_id, pending_task: prompt, pending_working_dir: working_dir)
|
|
1311
|
+
session_id = build_session(name: session_name, working_dir: working_dir, permission_mode: :auto_approve)
|
|
1312
|
+
@registry.update(session_id, pending_task: prompt, pending_working_dir: working_dir)
|
|
1343
1313
|
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
end
|
|
1314
|
+
json_response(res, 202, { ok: true, session: @registry.session_summary(session_id) })
|
|
1315
|
+
rescue => e
|
|
1316
|
+
json_response(res, 422, { error: e.message })
|
|
1348
1317
|
end
|
|
1349
1318
|
|
|
1350
1319
|
# ── Skills API ────────────────────────────────────────────────────────────
|
|
@@ -85,6 +85,61 @@ module Clacky
|
|
|
85
85
|
list.size < before_count
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
# Update an existing schedule entry (cron and/or enabled).
|
|
89
|
+
# Returns false if the schedule does not exist.
|
|
90
|
+
def update_schedule(name, cron: nil, enabled: nil)
|
|
91
|
+
list = load_schedules
|
|
92
|
+
entry = list.find { |s| s["name"] == name }
|
|
93
|
+
return false unless entry
|
|
94
|
+
|
|
95
|
+
entry["cron"] = cron unless cron.nil?
|
|
96
|
+
entry["enabled"] = enabled unless enabled.nil?
|
|
97
|
+
save_schedules(list)
|
|
98
|
+
true
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# ── Composite cron-task helpers ──────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
# Create a task file and its schedule in one step.
|
|
104
|
+
def create_cron_task(name:, content:, cron:, enabled: true)
|
|
105
|
+
write_task(name, content)
|
|
106
|
+
add_schedule(name: name, task: name, cron: cron, enabled: enabled)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Update a cron-task: optionally update content and/or schedule fields.
|
|
110
|
+
def update_cron_task(name, content: nil, cron: nil, enabled: nil)
|
|
111
|
+
raise "Cron task not found: #{name}" unless list_tasks.include?(name)
|
|
112
|
+
|
|
113
|
+
write_task(name, content) unless content.nil?
|
|
114
|
+
update_schedule(name, cron: cron, enabled: enabled) if cron || !enabled.nil?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Delete a cron-task: remove both the task file and its schedule.
|
|
118
|
+
def delete_cron_task(name)
|
|
119
|
+
removed_schedule = remove_schedule(name)
|
|
120
|
+
removed_task = delete_task(name)
|
|
121
|
+
removed_schedule || removed_task
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Return a merged list of cron-tasks (task content + schedule metadata).
|
|
125
|
+
def list_cron_tasks
|
|
126
|
+
schedule_map = load_schedules.each_with_object({}) do |s, h|
|
|
127
|
+
h[s["task"]] = s if s.is_a?(Hash)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
list_tasks.map do |task_name|
|
|
131
|
+
content = begin; read_task(task_name); rescue StandardError; ""; end
|
|
132
|
+
schedule = schedule_map[task_name] || {}
|
|
133
|
+
{
|
|
134
|
+
"name" => task_name,
|
|
135
|
+
"content" => content,
|
|
136
|
+
"cron" => schedule["cron"],
|
|
137
|
+
"enabled" => schedule.fetch("enabled", true),
|
|
138
|
+
"scheduled" => !schedule.empty?
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
88
143
|
# ── Task file helpers ────────────────────────────────────────────────────
|
|
89
144
|
|
|
90
145
|
# Read the prompt content of a named task.
|
data/lib/clacky/tools/browser.rb
CHANGED
|
@@ -486,7 +486,7 @@ module Clacky
|
|
|
486
486
|
# -----------------------------------------------------------------------
|
|
487
487
|
|
|
488
488
|
private def find_node_binary
|
|
489
|
-
path = `which node 2>/dev/null
|
|
489
|
+
path = Clacky::Utils::Encoding.cmd_to_utf8(`which node 2>/dev/null`, source_encoding: "UTF-8").strip
|
|
490
490
|
return nil if path.empty? || !File.executable?(path)
|
|
491
491
|
path
|
|
492
492
|
end
|
|
@@ -494,7 +494,7 @@ module Clacky
|
|
|
494
494
|
private def node_major_version
|
|
495
495
|
node = find_node_binary
|
|
496
496
|
return nil unless node
|
|
497
|
-
`#{node} --version 2>/dev/null
|
|
497
|
+
Clacky::Utils::Encoding.cmd_to_utf8(`#{node} --version 2>/dev/null`, source_encoding: "UTF-8").strip.gsub(/^v/, "").split(".").first.to_i
|
|
498
498
|
end
|
|
499
499
|
|
|
500
500
|
private def node_error
|
|
@@ -54,6 +54,27 @@ module Clacky
|
|
|
54
54
|
str.encode("UTF-8", "UTF-8", invalid: :replace, undef: :replace, replace: "")
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
# Convert raw shell command output to valid UTF-8.
|
|
58
|
+
# Handles two common cases:
|
|
59
|
+
# - Windows commands (e.g. powershell.exe) that output GBK/CP936 bytes
|
|
60
|
+
# - Unix commands that output UTF-8 or ASCII bytes with ASCII-8BIT encoding
|
|
61
|
+
#
|
|
62
|
+
# Strategy: try GBK decode first (superset of ASCII, covers Chinese Windows);
|
|
63
|
+
# if that fails fall back to UTF-8 scrub.
|
|
64
|
+
#
|
|
65
|
+
# @param data [String, nil] raw bytes from backtick / IO.popen
|
|
66
|
+
# @param source_encoding [String] hint for source encoding (default: "GBK")
|
|
67
|
+
# @return [String] valid UTF-8 string
|
|
68
|
+
def self.cmd_to_utf8(data, source_encoding: "GBK")
|
|
69
|
+
return "" if data.nil? || data.empty?
|
|
70
|
+
|
|
71
|
+
data.dup
|
|
72
|
+
.force_encoding(source_encoding)
|
|
73
|
+
.encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
|
|
74
|
+
rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
|
|
75
|
+
to_utf8(data)
|
|
76
|
+
end
|
|
77
|
+
|
|
57
78
|
# Return an ASCII-safe UTF-8 copy of *str* suitable for security regex
|
|
58
79
|
# pattern matching. Any byte that is not valid in the source encoding, or
|
|
59
80
|
# that cannot be represented in UTF-8, is replaced with '?'. The
|
|
@@ -55,29 +55,32 @@ module Clacky
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
private_class_method def self.wsl_desktop_path
|
|
58
|
-
if `which powershell.exe 2>/dev/null
|
|
58
|
+
if Utils::Encoding.cmd_to_utf8(`which powershell.exe 2>/dev/null`).strip.empty?
|
|
59
59
|
return fallback_desktop_path
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
# powershell.exe on Chinese Windows outputs GBK bytes; decode explicitly
|
|
63
|
+
win_path = Utils::Encoding.cmd_to_utf8(
|
|
64
|
+
`powershell.exe -NoProfile -Command '[Environment]::GetFolderPath("Desktop")' 2>/dev/null`
|
|
65
|
+
).strip.tr("\r\n", "")
|
|
64
66
|
return fallback_desktop_path if win_path.empty?
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
# wslpath output is UTF-8 (Linux side)
|
|
69
|
+
linux_path = Utils::Encoding.cmd_to_utf8(`wslpath '#{win_path}' 2>/dev/null`, source_encoding: "UTF-8").strip
|
|
67
70
|
return linux_path if !linux_path.empty? && Dir.exist?(linux_path)
|
|
68
71
|
|
|
69
72
|
fallback_desktop_path
|
|
70
73
|
end
|
|
71
74
|
|
|
72
75
|
private_class_method def self.linux_desktop_path
|
|
73
|
-
path = `xdg-user-dir DESKTOP 2>/dev/null
|
|
76
|
+
path = Utils::Encoding.cmd_to_utf8(`xdg-user-dir DESKTOP 2>/dev/null`, source_encoding: "UTF-8").strip
|
|
74
77
|
return path if !path.empty? && path != Dir.home && Dir.exist?(path)
|
|
75
78
|
|
|
76
79
|
fallback_desktop_path
|
|
77
80
|
end
|
|
78
81
|
|
|
79
82
|
private_class_method def self.macos_desktop_path
|
|
80
|
-
path = `osascript -e 'POSIX path of (path to desktop)' 2>/dev/null
|
|
83
|
+
path = Utils::Encoding.cmd_to_utf8(`osascript -e 'POSIX path of (path to desktop)' 2>/dev/null`, source_encoding: "UTF-8").strip.chomp("/")
|
|
81
84
|
return path if !path.empty? && Dir.exist?(path)
|
|
82
85
|
|
|
83
86
|
fallback_desktop_path
|
data/lib/clacky/version.rb
CHANGED
data/lib/clacky/web/app.css
CHANGED
|
@@ -938,7 +938,7 @@ body {
|
|
|
938
938
|
.task-btn-run { background: var(--color-accent-primary); color: #fff; }
|
|
939
939
|
.task-btn-run:hover { background: var(--color-accent-hover); }
|
|
940
940
|
.task-btn-edit { background: var(--color-border-primary); color: var(--color-text-primary); }
|
|
941
|
-
.task-btn-edit:hover { background:
|
|
941
|
+
.task-btn-edit:hover { background: var(--color-bg-hover); }
|
|
942
942
|
.task-btn-del { background: var(--color-error-bg); color: var(--color-error); }
|
|
943
943
|
.task-btn-del:hover { background: var(--color-error); color: #fff; }
|
|
944
944
|
|
data/lib/clacky/web/tasks.js
CHANGED
|
@@ -14,27 +14,18 @@
|
|
|
14
14
|
|
|
15
15
|
const Tasks = (() => {
|
|
16
16
|
// ── Private state ──────────────────────────────────────────────────────
|
|
17
|
-
let _tasks
|
|
18
|
-
let _schedules = []; // [{ name, task, cron, enabled }]
|
|
17
|
+
let _tasks = []; // [{ name, content, cron, enabled, scheduled }]
|
|
19
18
|
|
|
20
19
|
// ── Private helpers ────────────────────────────────────────────────────
|
|
21
20
|
|
|
22
|
-
/** Merge schedule info into task objects. Pure function. */
|
|
23
|
-
function _attachSchedules(tasks, schedules) {
|
|
24
|
-
return tasks.map(t => ({
|
|
25
|
-
...t,
|
|
26
|
-
schedules: schedules.filter(s => s.task === t.name)
|
|
27
|
-
}));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
21
|
/** Render a single task row in the main panel table. */
|
|
31
22
|
function _renderTaskRow(t) {
|
|
32
23
|
const row = document.createElement("div");
|
|
33
24
|
row.className = "task-table-row";
|
|
34
25
|
row.dataset.name = t.name;
|
|
35
26
|
|
|
36
|
-
const schedLabel = t.
|
|
37
|
-
? escapeHtml(t.
|
|
27
|
+
const schedLabel = t.scheduled
|
|
28
|
+
? escapeHtml(t.cron)
|
|
38
29
|
: `<span class="sched-manual">${I18n.t("tasks.manual")}</span>`;
|
|
39
30
|
|
|
40
31
|
const preview = (t.content || "")
|
|
@@ -99,17 +90,12 @@ const Tasks = (() => {
|
|
|
99
90
|
|
|
100
91
|
// ── Data ─────────────────────────────────────────────────────────────
|
|
101
92
|
|
|
102
|
-
/** Fetch tasks
|
|
93
|
+
/** Fetch cron tasks from server; re-render sidebar + panel if open. */
|
|
103
94
|
async load() {
|
|
104
95
|
try {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
]);
|
|
109
|
-
const td = await tr.json();
|
|
110
|
-
const sd = await sr.json();
|
|
111
|
-
_schedules = sd.schedules || [];
|
|
112
|
-
_tasks = _attachSchedules(td.tasks || [], _schedules);
|
|
96
|
+
const res = await fetch("/api/cron-tasks");
|
|
97
|
+
const data = await res.json();
|
|
98
|
+
_tasks = data.cron_tasks || [];
|
|
113
99
|
Tasks.renderSection();
|
|
114
100
|
if (Router.current === "tasks") Tasks.renderTable();
|
|
115
101
|
} catch (e) {
|
|
@@ -175,10 +161,8 @@ const Tasks = (() => {
|
|
|
175
161
|
// ── CRUD ─────────────────────────────────────────────────────────────
|
|
176
162
|
|
|
177
163
|
async run(name) {
|
|
178
|
-
const res = await fetch(
|
|
179
|
-
method:
|
|
180
|
-
headers: { "Content-Type": "application/json" },
|
|
181
|
-
body: JSON.stringify({ name })
|
|
164
|
+
const res = await fetch(`/api/cron-tasks/${encodeURIComponent(name)}/run`, {
|
|
165
|
+
method: "POST"
|
|
182
166
|
});
|
|
183
167
|
const data = await res.json();
|
|
184
168
|
if (!res.ok) { alert(I18n.t("tasks.runError") + (data.error || "unknown")); return; }
|
|
@@ -255,7 +239,7 @@ const Tasks = (() => {
|
|
|
255
239
|
|
|
256
240
|
async delete(name) {
|
|
257
241
|
if (!confirm(I18n.t("tasks.confirmDelete", { name }))) return;
|
|
258
|
-
const res = await fetch(`/api/tasks/${encodeURIComponent(name)}`, { method: "DELETE" });
|
|
242
|
+
const res = await fetch(`/api/cron-tasks/${encodeURIComponent(name)}`, { method: "DELETE" });
|
|
259
243
|
if (!res.ok) { alert(I18n.t("tasks.deleteError")); return; }
|
|
260
244
|
|
|
261
245
|
await Tasks.load();
|
data/scripts/install.ps1
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
# After rebooting, run the same command again to complete installation.
|
|
18
18
|
#
|
|
19
19
|
# Development: .\install.ps1 -Local
|
|
20
|
-
# Uses
|
|
20
|
+
# Uses install.sh from the same directory as this script instead of CDN.
|
|
21
21
|
|
|
22
22
|
param(
|
|
23
23
|
[switch]$Local,
|
|
@@ -33,7 +33,7 @@ $global:DisplayCmd = if ($CommandName) { $CommandName } else { "openclacky" }
|
|
|
33
33
|
|
|
34
34
|
$CLACKY_CDN_BASE_URL = "https://oss.1024code.com"
|
|
35
35
|
$INSTALL_PS1_COMMAND = "powershell -c `"irm $CLACKY_CDN_BASE_URL/clacky-ai/openclacky/main/scripts/install.ps1 | iex`""
|
|
36
|
-
$INSTALL_SCRIPT_URL = "$CLACKY_CDN_BASE_URL/clacky-ai/openclacky/main/scripts/
|
|
36
|
+
$INSTALL_SCRIPT_URL = "$CLACKY_CDN_BASE_URL/clacky-ai/openclacky/main/scripts/install.sh"
|
|
37
37
|
$UBUNTU_WSL_AMD64_URL = "$CLACKY_CDN_BASE_URL/ubuntu-jammy-wsl-amd64-ubuntu22.04lts.rootfs.tar.gz"
|
|
38
38
|
$UBUNTU_WSL_AMD64_SHA256_URL = "$CLACKY_CDN_BASE_URL/ubuntu-jammy-wsl-amd64-ubuntu22.04lts.rootfs.tar.gz.sha256"
|
|
39
39
|
$UBUNTU_WSL_ARM64_URL = "$CLACKY_CDN_BASE_URL/ubuntu-jammy-wsl-arm64-ubuntu22.04lts.rootfs.tar.gz"
|
|
@@ -245,9 +245,9 @@ function Run-InstallInWsl {
|
|
|
245
245
|
if ($Local) {
|
|
246
246
|
# Convert Windows path to WSL path (e.g. C:\foo\bar -> /mnt/c/foo/bar)
|
|
247
247
|
$scriptDir = Split-Path -Parent $MyInvocation.PSCommandPath
|
|
248
|
-
$localScript = Join-Path $scriptDir "
|
|
248
|
+
$localScript = Join-Path $scriptDir "install.sh"
|
|
249
249
|
if (-not (Test-Path $localScript)) {
|
|
250
|
-
Write-Fail "Local mode:
|
|
250
|
+
Write-Fail "Local mode: install.sh not found at $localScript"
|
|
251
251
|
exit 1
|
|
252
252
|
}
|
|
253
253
|
$wslPath = ($localScript -replace '\', '/') -replace '^([A-Za-z]):', { '/mnt/' + $args[0].Groups[1].Value.ToLower() }
|
data/scripts/install.sh
CHANGED
|
@@ -368,9 +368,19 @@ ensure_ruby() {
|
|
|
368
368
|
return 0
|
|
369
369
|
fi
|
|
370
370
|
|
|
371
|
-
#
|
|
371
|
+
# Linux: try apt first (fast, Ubuntu 22.04 ships Ruby 3.0)
|
|
372
|
+
if [ "$OS" = "Linux" ] && ([ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ]); then
|
|
373
|
+
print_info "Installing Ruby via apt..."
|
|
374
|
+
if sudo apt-get install -y ruby ruby-dev 2>/dev/null && check_ruby; then
|
|
375
|
+
return 0
|
|
376
|
+
fi
|
|
377
|
+
print_warning "apt Ruby install failed or version too old, falling back to mise..."
|
|
378
|
+
fi
|
|
379
|
+
|
|
380
|
+
# Fallback: install via mise (compiles from source)
|
|
372
381
|
print_step "Installing Ruby via mise..."
|
|
373
382
|
detect_shell
|
|
383
|
+
install_linux_build_deps
|
|
374
384
|
|
|
375
385
|
if ! install_mise; then
|
|
376
386
|
return 1
|
|
@@ -390,29 +400,37 @@ ensure_ruby() {
|
|
|
390
400
|
}
|
|
391
401
|
|
|
392
402
|
# --------------------------------------------------------------------------
|
|
393
|
-
# Linux:
|
|
403
|
+
# Linux: configure apt mirror + update (always runs before any apt install)
|
|
394
404
|
# --------------------------------------------------------------------------
|
|
395
|
-
|
|
396
|
-
if [ "$DISTRO"
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
sudo tee /etc/apt/sources.list > /dev/null <<EOF
|
|
405
|
+
setup_apt_mirror() {
|
|
406
|
+
if [ "$DISTRO" != "ubuntu" ] && [ "$DISTRO" != "debian" ]; then return 0; fi
|
|
407
|
+
|
|
408
|
+
if [ "$USE_CN_MIRRORS" = true ]; then
|
|
409
|
+
print_info "Configuring apt mirror (Aliyun)..."
|
|
410
|
+
local codename="${VERSION_CODENAME:-jammy}"
|
|
411
|
+
local mirror_base="https://mirrors.aliyun.com/ubuntu/"
|
|
412
|
+
local components="main restricted universe multiverse"
|
|
413
|
+
sudo tee /etc/apt/sources.list > /dev/null <<EOF
|
|
405
414
|
deb ${mirror_base} ${codename} ${components}
|
|
406
415
|
deb ${mirror_base} ${codename}-updates ${components}
|
|
407
416
|
deb ${mirror_base} ${codename}-backports ${components}
|
|
408
417
|
deb ${mirror_base} ${codename}-security ${components}
|
|
409
418
|
EOF
|
|
410
|
-
fi
|
|
411
|
-
|
|
412
|
-
sudo apt update
|
|
413
|
-
sudo apt install -y build-essential libssl-dev libyaml-dev zlib1g-dev libgmp-dev git
|
|
414
|
-
print_success "Build dependencies installed"
|
|
415
419
|
fi
|
|
420
|
+
|
|
421
|
+
sudo apt-get update -qq
|
|
422
|
+
print_success "apt updated"
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
# --------------------------------------------------------------------------
|
|
426
|
+
# Linux: install build deps (only needed when compiling Ruby via mise)
|
|
427
|
+
# --------------------------------------------------------------------------
|
|
428
|
+
install_linux_build_deps() {
|
|
429
|
+
if [ "$DISTRO" != "ubuntu" ] && [ "$DISTRO" != "debian" ]; then return 0; fi
|
|
430
|
+
|
|
431
|
+
print_step "Installing build dependencies..."
|
|
432
|
+
sudo apt-get install -y build-essential libssl-dev libyaml-dev zlib1g-dev libgmp-dev git
|
|
433
|
+
print_success "Build dependencies installed"
|
|
416
434
|
}
|
|
417
435
|
|
|
418
436
|
# --------------------------------------------------------------------------
|
|
@@ -434,19 +452,18 @@ setup_gem_home() {
|
|
|
434
452
|
|
|
435
453
|
export GEM_HOME="$HOME/.gem/ruby/${ruby_api}"
|
|
436
454
|
export GEM_PATH="$HOME/.gem/ruby/${ruby_api}"
|
|
437
|
-
export PATH="$HOME/.
|
|
455
|
+
export PATH="$HOME/.gem/ruby/${ruby_api}/bin:$PATH"
|
|
438
456
|
|
|
439
457
|
print_info "System Ruby detected — gems will install to ~/.gem/ruby/${ruby_api}"
|
|
440
458
|
|
|
441
459
|
# Persist to shell rc (use $HOME so the line is portable)
|
|
442
|
-
# Also add ~/.local/bin so brand wrapper commands installed there are found
|
|
443
460
|
if [ -n "$SHELL_RC" ] && ! grep -q "GEM_HOME" "$SHELL_RC" 2>/dev/null; then
|
|
444
461
|
{
|
|
445
462
|
echo ""
|
|
446
463
|
echo "# Ruby user gem dir (added by openclacky installer)"
|
|
447
464
|
echo "export GEM_HOME=\"\$HOME/.gem/ruby/${ruby_api}\""
|
|
448
465
|
echo "export GEM_PATH=\"\$HOME/.gem/ruby/${ruby_api}\""
|
|
449
|
-
echo "export PATH=\"\$HOME/.
|
|
466
|
+
echo "export PATH=\"\$HOME/.gem/ruby/${ruby_api}/bin:\$PATH\""
|
|
450
467
|
} >> "$SHELL_RC"
|
|
451
468
|
print_info "GEM_HOME written to $SHELL_RC"
|
|
452
469
|
fi
|
|
@@ -524,8 +541,17 @@ YAML
|
|
|
524
541
|
print_success "Brand config written to $brand_file"
|
|
525
542
|
|
|
526
543
|
if [ -n "$BRAND_COMMAND" ]; then
|
|
527
|
-
|
|
528
|
-
|
|
544
|
+
# Install wrapper in the same directory as the openclacky binary so it is
|
|
545
|
+
# always on PATH regardless of whether we are running as root or a normal user.
|
|
546
|
+
local clacky_bin bin_dir
|
|
547
|
+
clacky_bin=$(command -v openclacky 2>/dev/null || true)
|
|
548
|
+
if [ -n "$clacky_bin" ]; then
|
|
549
|
+
bin_dir=$(dirname "$clacky_bin")
|
|
550
|
+
else
|
|
551
|
+
print_warning "openclacky binary not found in PATH; skipping wrapper install"
|
|
552
|
+
return 0
|
|
553
|
+
fi
|
|
554
|
+
|
|
529
555
|
local wrapper="$bin_dir/$BRAND_COMMAND"
|
|
530
556
|
cat > "$wrapper" <<WRAPPER
|
|
531
557
|
#!/bin/sh
|
|
@@ -533,13 +559,6 @@ exec openclacky "\$@"
|
|
|
533
559
|
WRAPPER
|
|
534
560
|
chmod +x "$wrapper"
|
|
535
561
|
print_success "Wrapper installed: $wrapper"
|
|
536
|
-
|
|
537
|
-
case ":$PATH:" in
|
|
538
|
-
*":$bin_dir:"*) ;;
|
|
539
|
-
*)
|
|
540
|
-
print_warning "Add to your shell profile: export PATH=\"\$HOME/.local/bin:\$PATH\""
|
|
541
|
-
;;
|
|
542
|
-
esac
|
|
543
562
|
fi
|
|
544
563
|
}
|
|
545
564
|
|
|
@@ -579,10 +598,10 @@ main() {
|
|
|
579
598
|
detect_shell
|
|
580
599
|
detect_network_region
|
|
581
600
|
|
|
582
|
-
# Linux:
|
|
601
|
+
# Linux: configure apt mirror + update (always runs; build deps deferred to mise fallback)
|
|
583
602
|
if [ "$OS" = "Linux" ]; then
|
|
584
603
|
if [ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ]; then
|
|
585
|
-
|
|
604
|
+
setup_apt_mirror
|
|
586
605
|
else
|
|
587
606
|
print_error "Unsupported Linux distribution: $DISTRO"
|
|
588
607
|
print_info "Please install Ruby >= 2.6.0 manually and run: gem install openclacky"
|