openclacky 1.2.18 → 1.3.1
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 +35 -0
- data/lib/clacky/agent/time_machine.rb +256 -74
- data/lib/clacky/agent/tool_executor.rb +12 -0
- data/lib/clacky/agent.rb +15 -20
- data/lib/clacky/agent_config.rb +18 -0
- data/lib/clacky/cli.rb +55 -3
- data/lib/clacky/default_skills/media-gen/SKILL.md +172 -5
- data/lib/clacky/media/base.rb +93 -0
- data/lib/clacky/media/gemini.rb +10 -0
- data/lib/clacky/media/generator.rb +57 -0
- data/lib/clacky/media/openai_compat.rb +160 -0
- data/lib/clacky/message_history.rb +12 -7
- data/lib/clacky/providers.rb +29 -1
- data/lib/clacky/rich_ui_controller.rb +3 -1
- data/lib/clacky/server/backup_manager.rb +200 -0
- data/lib/clacky/server/channel/adapters/feishu/adapter.rb +10 -2
- data/lib/clacky/server/channel/adapters/feishu/bot.rb +68 -15
- data/lib/clacky/server/channel/channel_manager.rb +65 -50
- data/lib/clacky/server/http_server.rb +356 -14
- data/lib/clacky/server/scheduler.rb +19 -0
- data/lib/clacky/server/session_registry.rb +8 -4
- data/lib/clacky/session_manager.rb +40 -2
- data/lib/clacky/tools/trash_manager.rb +14 -0
- data/lib/clacky/ui2/components/command_suggestions.rb +1 -0
- data/lib/clacky/ui2/components/modal_component.rb +34 -7
- data/lib/clacky/ui2/ui_controller.rb +150 -19
- data/lib/clacky/utils/file_processor.rb +75 -4
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +2283 -1277
- data/lib/clacky/web/app.js +73 -1
- data/lib/clacky/web/backup.js +119 -0
- data/lib/clacky/web/billing.js +224 -11
- data/lib/clacky/web/channels.js +81 -11
- data/lib/clacky/web/design-sample.css +247 -0
- data/lib/clacky/web/design-sample.html +127 -0
- data/lib/clacky/web/favicon.svg +16 -0
- data/lib/clacky/web/i18n.js +167 -31
- data/lib/clacky/web/index.html +176 -55
- data/lib/clacky/web/logo_nav_dark.png +0 -0
- data/lib/clacky/web/onboard.js +121 -28
- data/lib/clacky/web/sessions.js +447 -192
- data/lib/clacky/web/settings.js +21 -1
- data/lib/clacky/web/skills.js +34 -1
- data/lib/clacky/web/tasks.js +129 -61
- data/lib/clacky/web/utils.js +72 -0
- data/lib/clacky/web/ws-dispatcher.js +6 -0
- data/lib/clacky.rb +1 -0
- metadata +9 -8
- data/lib/clacky/server/channel/group_message_buffer.rb +0 -53
data/lib/clacky/web/settings.js
CHANGED
|
@@ -20,6 +20,7 @@ const Settings = (() => {
|
|
|
20
20
|
_loadBrand();
|
|
21
21
|
_loadBrowserStatus();
|
|
22
22
|
_initNetworkSettings();
|
|
23
|
+
if (window.Backup) Backup.load();
|
|
23
24
|
_applyAboutTabVisibility();
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -1636,10 +1637,11 @@ const Settings = (() => {
|
|
|
1636
1637
|
const def = (_mediaDefaults && _mediaDefaults[kind]) || { available: [] };
|
|
1637
1638
|
const autoAvailable = !!(def && def.model);
|
|
1638
1639
|
const isCustomEditing = state.source === "custom" && (!state.configured || _mediaCustomDraft[kind]);
|
|
1640
|
+
const isVisionPrimary = kind === "ocr" && state.source === "auto" && state.primary;
|
|
1639
1641
|
|
|
1640
1642
|
const row = document.createElement("div");
|
|
1641
1643
|
row.className = "media-row";
|
|
1642
|
-
if (isCustomEditing || (state.source === "auto" && state.configured) || (state.source === "custom" && state.configured)) {
|
|
1644
|
+
if (!isVisionPrimary && (isCustomEditing || (state.source === "auto" && state.configured) || (state.source === "custom" && state.configured))) {
|
|
1643
1645
|
row.classList.add("is-expanded");
|
|
1644
1646
|
}
|
|
1645
1647
|
row.dataset.kind = kind;
|
|
@@ -1653,6 +1655,24 @@ const Settings = (() => {
|
|
|
1653
1655
|
title.textContent = I18n.t(`settings.media.kind.${kind}`);
|
|
1654
1656
|
head.appendChild(title);
|
|
1655
1657
|
|
|
1658
|
+
// When the default chat model already supports vision, the OCR sidecar
|
|
1659
|
+
// reuses it automatically — there's nothing to choose, so show a single
|
|
1660
|
+
// read-only note instead of the off/auto/custom switcher.
|
|
1661
|
+
if (isVisionPrimary) {
|
|
1662
|
+
const note = document.createElement("span");
|
|
1663
|
+
note.className = "media-row-status media-vision-primary-note";
|
|
1664
|
+
note.textContent = I18n.t("settings.media.vision.primary");
|
|
1665
|
+
head.appendChild(note);
|
|
1666
|
+
|
|
1667
|
+
const model = document.createElement("span");
|
|
1668
|
+
model.className = "media-vision-primary-model";
|
|
1669
|
+
model.textContent = state.model || "";
|
|
1670
|
+
head.appendChild(model);
|
|
1671
|
+
|
|
1672
|
+
row.appendChild(head);
|
|
1673
|
+
return row;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1656
1676
|
const seg = document.createElement("div");
|
|
1657
1677
|
seg.className = "media-row-segmented";
|
|
1658
1678
|
["off", "auto", "custom"].forEach(src => {
|
data/lib/clacky/web/skills.js
CHANGED
|
@@ -336,6 +336,15 @@ const Skills = (() => {
|
|
|
336
336
|
? ""
|
|
337
337
|
: `<button class="btn-skill-use" data-name="${escapeHtml(skill.name)}">${I18n.t("skills.btn.use")}</button>`;
|
|
338
338
|
|
|
339
|
+
// Delete button only for custom (non-system, non-brand) skills
|
|
340
|
+
const deleteButtonHtml = isSystem
|
|
341
|
+
? ""
|
|
342
|
+
: `<button class="btn-skill-delete" data-name="${escapeHtml(skill.name)}" title="${I18n.t("skills.btn.delete")}">
|
|
343
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
344
|
+
<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
|
|
345
|
+
</svg>
|
|
346
|
+
</button>`;
|
|
347
|
+
|
|
339
348
|
card.innerHTML = `
|
|
340
349
|
<div class="skill-card-main">
|
|
341
350
|
<div class="skill-card-info">
|
|
@@ -353,6 +362,7 @@ const Skills = (() => {
|
|
|
353
362
|
<span class="skill-toggle-track"></span>
|
|
354
363
|
</label>
|
|
355
364
|
${useButtonHtml}
|
|
365
|
+
${deleteButtonHtml}
|
|
356
366
|
</div>
|
|
357
367
|
</div>
|
|
358
368
|
${errorNoticeHtml}`;
|
|
@@ -374,7 +384,7 @@ const Skills = (() => {
|
|
|
374
384
|
const toggleTop = toggleLabel.getBoundingClientRect().top;
|
|
375
385
|
const scrollerTop = scroller.getBoundingClientRect().top;
|
|
376
386
|
if (toggleTop - scrollerTop < 80) {
|
|
377
|
-
toggleLabel.
|
|
387
|
+
toggleLabel.setAttribute("data-tooltip-pos", "bottom");
|
|
378
388
|
}
|
|
379
389
|
});
|
|
380
390
|
}
|
|
@@ -385,6 +395,15 @@ const Skills = (() => {
|
|
|
385
395
|
useBtn.addEventListener("click", () => _useInstalledSkill(skill.name));
|
|
386
396
|
}
|
|
387
397
|
|
|
398
|
+
// Bind delete button event
|
|
399
|
+
const deleteBtn = card.querySelector(".btn-skill-delete");
|
|
400
|
+
if (deleteBtn) {
|
|
401
|
+
deleteBtn.addEventListener("click", (e) => {
|
|
402
|
+
e.stopPropagation();
|
|
403
|
+
Skills.delete(skill.name);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
388
407
|
return card;
|
|
389
408
|
}
|
|
390
409
|
|
|
@@ -579,6 +598,20 @@ const Skills = (() => {
|
|
|
579
598
|
}
|
|
580
599
|
},
|
|
581
600
|
|
|
601
|
+
/** Delete a skill by name. Prompts confirmation, then DELETE to server. */
|
|
602
|
+
async delete(name) {
|
|
603
|
+
if (!confirm(I18n.t("skills.deleteConfirm", { name }))) return;
|
|
604
|
+
try {
|
|
605
|
+
const res = await fetch(`/api/skills/${encodeURIComponent(name)}`, { method: "DELETE" });
|
|
606
|
+
const data = await res.json();
|
|
607
|
+
if (!res.ok) { alert(data.error || I18n.t("skills.deleteError")); return; }
|
|
608
|
+
Modal.toast(I18n.t("skills.deleted", { name }), "success");
|
|
609
|
+
await Skills.load();
|
|
610
|
+
} catch (e) {
|
|
611
|
+
console.error("[Skills] delete failed", e);
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
|
|
582
615
|
/** Switch the Skills panel to the brand-skills tab.
|
|
583
616
|
* Called externally (e.g. from settings.js after license activation) to
|
|
584
617
|
* guide the user directly to the Brand Skills download page.
|
data/lib/clacky/web/tasks.js
CHANGED
|
@@ -18,81 +18,158 @@ const Tasks = (() => {
|
|
|
18
18
|
|
|
19
19
|
// ── Private helpers ────────────────────────────────────────────────────
|
|
20
20
|
|
|
21
|
+
function _humanCron(cron) {
|
|
22
|
+
if (!cron) return cron;
|
|
23
|
+
const parts = cron.trim().split(/\s+/);
|
|
24
|
+
if (parts.length !== 5) return cron;
|
|
25
|
+
const [min, hour, dom, month, dow] = parts;
|
|
26
|
+
|
|
27
|
+
const isAny = v => v === "*";
|
|
28
|
+
const isNum = v => /^\d+$/.test(v);
|
|
29
|
+
const pad = n => String(n).padStart(2, "0");
|
|
30
|
+
|
|
31
|
+
const lang = (typeof I18n !== "undefined" && I18n.lang()) || "zh";
|
|
32
|
+
const isZh = lang === "zh";
|
|
33
|
+
|
|
34
|
+
const dowNames = isZh
|
|
35
|
+
? ["周日","周一","周二","周三","周四","周五","周六"]
|
|
36
|
+
: ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
|
|
37
|
+
|
|
38
|
+
const monthNames = isZh
|
|
39
|
+
? ["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"]
|
|
40
|
+
: ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
|
|
41
|
+
|
|
42
|
+
const timeStr = (isNum(hour) && isNum(min))
|
|
43
|
+
? `${pad(hour)}:${pad(min)}`
|
|
44
|
+
: null;
|
|
45
|
+
|
|
46
|
+
// Every N minutes
|
|
47
|
+
if (min.startsWith("*/") && isAny(hour) && isAny(dom) && isAny(month) && isAny(dow)) {
|
|
48
|
+
const n = min.slice(2);
|
|
49
|
+
return isZh ? `每 ${n} 分钟` : `Every ${n} min`;
|
|
50
|
+
}
|
|
51
|
+
// Every N hours
|
|
52
|
+
if (isAny(min) && hour.startsWith("*/") && isAny(dom) && isAny(month) && isAny(dow)) {
|
|
53
|
+
const n = hour.slice(2);
|
|
54
|
+
return isZh ? `每 ${n} 小时` : `Every ${n} hr`;
|
|
55
|
+
}
|
|
56
|
+
// Every minute
|
|
57
|
+
if (isAny(min) && isAny(hour) && isAny(dom) && isAny(month) && isAny(dow)) {
|
|
58
|
+
return isZh ? "每分钟" : "Every minute";
|
|
59
|
+
}
|
|
60
|
+
// Every hour at :MM
|
|
61
|
+
if (isNum(min) && isAny(hour) && isAny(dom) && isAny(month) && isAny(dow)) {
|
|
62
|
+
return isZh ? `每小时 :${pad(min)}` : `Hourly at :${pad(min)}`;
|
|
63
|
+
}
|
|
64
|
+
// Specific day-of-week
|
|
65
|
+
if (timeStr && isAny(dom) && isAny(month) && isNum(dow)) {
|
|
66
|
+
const d = dowNames[parseInt(dow, 10)] || dow;
|
|
67
|
+
return isZh ? `每${d} ${timeStr}` : `${d} ${timeStr}`;
|
|
68
|
+
}
|
|
69
|
+
// Weekdays (1-5)
|
|
70
|
+
if (timeStr && isAny(dom) && isAny(month) && dow === "1-5") {
|
|
71
|
+
return isZh ? `工作日 ${timeStr}` : `Weekdays ${timeStr}`;
|
|
72
|
+
}
|
|
73
|
+
// Weekends (0,6 or 6,0)
|
|
74
|
+
if (timeStr && isAny(dom) && isAny(month) && (dow === "0,6" || dow === "6,0")) {
|
|
75
|
+
return isZh ? `周末 ${timeStr}` : `Weekends ${timeStr}`;
|
|
76
|
+
}
|
|
77
|
+
// Every day at HH:MM
|
|
78
|
+
if (timeStr && isAny(dom) && isAny(month) && isAny(dow)) {
|
|
79
|
+
return isZh ? `每天 ${timeStr}` : `Daily ${timeStr}`;
|
|
80
|
+
}
|
|
81
|
+
// Specific day of month
|
|
82
|
+
if (timeStr && isNum(dom) && isAny(month) && isAny(dow)) {
|
|
83
|
+
return isZh ? `每月 ${dom} 日 ${timeStr}` : `Monthly day ${dom} ${timeStr}`;
|
|
84
|
+
}
|
|
85
|
+
// Specific month + day
|
|
86
|
+
if (timeStr && isNum(dom) && isNum(month) && isAny(dow)) {
|
|
87
|
+
const m = monthNames[parseInt(month, 10) - 1] || month;
|
|
88
|
+
return isZh ? `${m}${dom}日 ${timeStr}` : `${m} ${dom} ${timeStr}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return cron;
|
|
92
|
+
}
|
|
93
|
+
|
|
21
94
|
/** Render a single task row in the main panel table. */
|
|
22
95
|
function _renderTaskRow(t) {
|
|
23
96
|
const row = document.createElement("div");
|
|
24
|
-
row.className = "task-
|
|
97
|
+
row.className = "task-card";
|
|
25
98
|
row.dataset.name = t.name;
|
|
26
99
|
|
|
100
|
+
const isPaused = t.scheduled && t.enabled === false;
|
|
101
|
+
row.classList.toggle("task-card-paused", isPaused);
|
|
102
|
+
|
|
27
103
|
const schedLabel = t.scheduled
|
|
28
|
-
? escapeHtml(t.cron)
|
|
29
|
-
: `<span class="
|
|
104
|
+
? `<span class="task-card-cron" title="${escapeHtml(t.cron)}">${escapeHtml(_humanCron(t.cron))}</span>`
|
|
105
|
+
: `<span class="task-card-cron task-card-cron-manual">${I18n.t("tasks.manual")}</span>`;
|
|
30
106
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
? `<span class="sched-paused-badge">${I18n.t("tasks.paused")}</span>
|
|
35
|
-
<span class="sched-paused-cron">${schedLabel}</span>`
|
|
36
|
-
: schedLabel;
|
|
107
|
+
const pausedBadge = isPaused
|
|
108
|
+
? `<span class="task-card-badge task-card-badge-paused">${I18n.t("tasks.paused")}</span>`
|
|
109
|
+
: "";
|
|
37
110
|
|
|
38
|
-
|
|
111
|
+
const preview = (t.content || "")
|
|
39
112
|
.split("\n")
|
|
40
113
|
.map(l => l.trim())
|
|
41
114
|
.find(l => l.length > 0) || I18n.t("tasks.empty");
|
|
42
|
-
const previewText = preview.length >
|
|
43
|
-
? escapeHtml(preview.slice(0,
|
|
115
|
+
const previewText = preview.length > 120
|
|
116
|
+
? escapeHtml(preview.slice(0, 120)) + "…"
|
|
44
117
|
: escapeHtml(preview);
|
|
45
118
|
|
|
46
|
-
// Build the pause/resume button only for *scheduled* tasks. Manual tasks
|
|
47
|
-
// have no cron to disable.
|
|
48
119
|
const toggleBtnHtml = t.scheduled ? (isPaused
|
|
49
|
-
? `<button class="task-btn task-btn-toggle task-btn-resume"
|
|
50
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="
|
|
120
|
+
? `<button class="task-action-btn task-btn-toggle task-btn-resume">
|
|
121
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
51
122
|
<polygon points="6 3 20 12 6 21 6 3"/>
|
|
52
|
-
</svg
|
|
123
|
+
</svg>
|
|
124
|
+
<span>${I18n.t("tasks.btn.resume")}</span>
|
|
53
125
|
</button>`
|
|
54
|
-
: `<button class="task-btn task-btn-toggle task-btn-pause"
|
|
55
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="
|
|
126
|
+
: `<button class="task-action-btn task-btn-toggle task-btn-pause">
|
|
127
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
56
128
|
<rect x="6" y="4" width="4" height="16" rx="1"/>
|
|
57
129
|
<rect x="14" y="4" width="4" height="16" rx="1"/>
|
|
58
|
-
</svg
|
|
130
|
+
</svg>
|
|
131
|
+
<span>${I18n.t("tasks.btn.pause")}</span>
|
|
59
132
|
</button>`
|
|
60
133
|
) : "";
|
|
61
134
|
|
|
62
|
-
row.classList.toggle("task-row-paused", isPaused);
|
|
63
|
-
|
|
64
135
|
row.innerHTML = `
|
|
65
|
-
<div class="task-
|
|
66
|
-
<
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
<div class="task-name-info">
|
|
71
|
-
<span class="task-name-text">${escapeHtml(t.name)}</span>
|
|
72
|
-
<span class="task-name-sched">${schedCell}</span>
|
|
73
|
-
</div>
|
|
74
|
-
</div>
|
|
75
|
-
<div class="task-col task-col-schedule">${schedCell}</div>
|
|
76
|
-
<div class="task-col task-col-content">${previewText}</div>
|
|
77
|
-
<div class="task-col task-col-actions">
|
|
78
|
-
<button class="task-btn task-btn-run" title="${I18n.t("tasks.btn.run")}">
|
|
79
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
|
|
80
|
-
<polygon points="6 3 20 12 6 21 6 3"/>
|
|
81
|
-
</svg><span class="btn-label"> ${I18n.t("tasks.btn.run")}</span>
|
|
82
|
-
</button>
|
|
83
|
-
${toggleBtnHtml}
|
|
84
|
-
<button class="task-btn task-btn-edit" title="${I18n.t("tasks.btn.edit")}">
|
|
85
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
|
|
86
|
-
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/>
|
|
87
|
-
<path d="m15 5 4 4"/>
|
|
88
|
-
</svg><span class="btn-label"> ${I18n.t("tasks.btn.edit")}</span>
|
|
89
|
-
</button>
|
|
90
|
-
<button class="task-btn task-btn-del" title="Delete">
|
|
91
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
|
|
92
|
-
<path d="M18 6 6 18"/>
|
|
93
|
-
<path d="m6 6 12 12"/>
|
|
136
|
+
<div class="task-card-main">
|
|
137
|
+
<div class="task-card-icon">
|
|
138
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
|
139
|
+
<circle cx="12" cy="12" r="10"/>
|
|
140
|
+
<polyline points="12 6 12 12 16 14"/>
|
|
94
141
|
</svg>
|
|
95
|
-
</
|
|
142
|
+
</div>
|
|
143
|
+
<div class="task-card-info">
|
|
144
|
+
<div class="task-card-title-row">
|
|
145
|
+
<span class="task-card-name">${escapeHtml(t.name)}</span>
|
|
146
|
+
${pausedBadge}
|
|
147
|
+
${schedLabel}
|
|
148
|
+
</div>
|
|
149
|
+
<div class="task-card-preview">${previewText}</div>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="task-card-actions">
|
|
152
|
+
<button class="task-run-btn task-btn-run" title="${I18n.t("tasks.btn.run")}">
|
|
153
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
154
|
+
<polygon points="6 3 20 12 6 21 6 3"/>
|
|
155
|
+
</svg>
|
|
156
|
+
<span>${I18n.t("tasks.btn.run")}</span>
|
|
157
|
+
</button>
|
|
158
|
+
${toggleBtnHtml}
|
|
159
|
+
<button class="task-action-btn task-btn-edit">
|
|
160
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
161
|
+
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/>
|
|
162
|
+
<path d="m15 5 4 4"/>
|
|
163
|
+
</svg>
|
|
164
|
+
<span>${I18n.t("tasks.btn.edit")}</span>
|
|
165
|
+
</button>
|
|
166
|
+
<button class="task-action-btn task-btn-del">
|
|
167
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
168
|
+
<path d="M3 6h18"/><path d="M19 6l-1 14H6L5 6"/><path d="M8 6V4h8v2"/>
|
|
169
|
+
</svg>
|
|
170
|
+
<span>${I18n.t("tasks.btn.delete")}</span>
|
|
171
|
+
</button>
|
|
172
|
+
</div>
|
|
96
173
|
</div>`;
|
|
97
174
|
|
|
98
175
|
row.querySelector(".task-btn-run").addEventListener("click", e => {
|
|
@@ -178,15 +255,6 @@ const Tasks = (() => {
|
|
|
178
255
|
return;
|
|
179
256
|
}
|
|
180
257
|
|
|
181
|
-
const header = document.createElement("div");
|
|
182
|
-
header.className = "task-table-header";
|
|
183
|
-
header.innerHTML = `
|
|
184
|
-
<div class="task-col task-col-name">${I18n.t("tasks.col.name")}</div>
|
|
185
|
-
<div class="task-col task-col-schedule">${I18n.t("tasks.col.schedule")}</div>
|
|
186
|
-
<div class="task-col task-col-content">${I18n.t("tasks.col.task")}</div>
|
|
187
|
-
<div class="task-col task-col-actions"></div>`;
|
|
188
|
-
table.appendChild(header);
|
|
189
|
-
|
|
190
258
|
_tasks.forEach(t => table.appendChild(_renderTaskRow(t)));
|
|
191
259
|
},
|
|
192
260
|
|
data/lib/clacky/web/utils.js
CHANGED
|
@@ -55,3 +55,75 @@ const IME = (() => {
|
|
|
55
55
|
|
|
56
56
|
return { track, bindEnter };
|
|
57
57
|
})();
|
|
58
|
+
|
|
59
|
+
const Tooltip = (() => {
|
|
60
|
+
const GAP = 8;
|
|
61
|
+
let el = null;
|
|
62
|
+
let _hideTimer = null;
|
|
63
|
+
|
|
64
|
+
function _el() {
|
|
65
|
+
if (!el) el = document.getElementById("tooltip");
|
|
66
|
+
return el;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function show(anchor) {
|
|
70
|
+
const tip = _el();
|
|
71
|
+
if (!tip) return;
|
|
72
|
+
clearTimeout(_hideTimer);
|
|
73
|
+
|
|
74
|
+
const text = anchor.getAttribute("data-tooltip");
|
|
75
|
+
if (!text) return;
|
|
76
|
+
|
|
77
|
+
const pos = anchor.getAttribute("data-tooltip-pos") || "top";
|
|
78
|
+
tip.textContent = text;
|
|
79
|
+
tip.setAttribute("data-pos", pos);
|
|
80
|
+
tip.style.display = "block";
|
|
81
|
+
|
|
82
|
+
const r = anchor.getBoundingClientRect();
|
|
83
|
+
const tw = tip.offsetWidth;
|
|
84
|
+
const th = tip.offsetHeight;
|
|
85
|
+
|
|
86
|
+
let top, left;
|
|
87
|
+
if (pos === "bottom") {
|
|
88
|
+
top = r.bottom + GAP;
|
|
89
|
+
left = r.left + r.width / 2 - tw / 2;
|
|
90
|
+
} else if (pos === "left") {
|
|
91
|
+
top = r.top + r.height / 2 - th / 2;
|
|
92
|
+
left = r.left - tw - GAP;
|
|
93
|
+
} else if (pos === "right") {
|
|
94
|
+
top = r.top + r.height / 2 - th / 2;
|
|
95
|
+
left = r.right + GAP;
|
|
96
|
+
} else {
|
|
97
|
+
top = r.top - th - GAP;
|
|
98
|
+
left = r.left + r.width / 2 - tw / 2;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
left = Math.max(6, Math.min(left, window.innerWidth - tw - 6));
|
|
102
|
+
top = Math.max(6, Math.min(top, window.innerHeight - th - 6));
|
|
103
|
+
|
|
104
|
+
tip.style.left = `${left}px`;
|
|
105
|
+
tip.style.top = `${top}px`;
|
|
106
|
+
requestAnimationFrame(() => tip.classList.add("visible"));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function hide() {
|
|
110
|
+
const tip = _el();
|
|
111
|
+
if (!tip) return;
|
|
112
|
+
tip.classList.remove("visible");
|
|
113
|
+
_hideTimer = setTimeout(() => { tip.style.display = "none"; }, 120);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function init() {
|
|
117
|
+
document.addEventListener("mouseover", (e) => {
|
|
118
|
+
const anchor = e.target.closest("[data-tooltip]");
|
|
119
|
+
if (anchor) show(anchor);
|
|
120
|
+
});
|
|
121
|
+
document.addEventListener("mouseout", (e) => {
|
|
122
|
+
const anchor = e.target.closest("[data-tooltip]");
|
|
123
|
+
if (!anchor) return;
|
|
124
|
+
if (!anchor.contains(e.relatedTarget)) hide();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { init, show, hide };
|
|
129
|
+
})();
|
|
@@ -424,6 +424,12 @@ WS.onEvent(ev => {
|
|
|
424
424
|
}
|
|
425
425
|
});
|
|
426
426
|
|
|
427
|
+
// ── Warning transformation ─────────────────────────────────────────────────
|
|
428
|
+
|
|
429
|
+
function _transformRetryWarning(message) {
|
|
430
|
+
return message;
|
|
431
|
+
}
|
|
432
|
+
|
|
427
433
|
// ── Error rendering ────────────────────────────────────────────────────────
|
|
428
434
|
|
|
429
435
|
function renderErrorEvent(ev) {
|
data/lib/clacky.rb
CHANGED
|
@@ -136,6 +136,7 @@ require_relative "clacky/agent"
|
|
|
136
136
|
require_relative "clacky/server/session_registry"
|
|
137
137
|
require_relative "clacky/server/web_ui_controller"
|
|
138
138
|
require_relative "clacky/server/browser_manager"
|
|
139
|
+
require_relative "clacky/server/backup_manager"
|
|
139
140
|
require_relative "clacky/cli"
|
|
140
141
|
|
|
141
142
|
# Runtime patch layer: load user/AI patches from ~/.clacky/patches/ after all
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: openclacky
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- windy
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: faraday
|
|
@@ -270,8 +269,8 @@ email:
|
|
|
270
269
|
- yafei@dao42.com
|
|
271
270
|
executables:
|
|
272
271
|
- clacky
|
|
273
|
-
- openclacky
|
|
274
272
|
- clarky
|
|
273
|
+
- openclacky
|
|
275
274
|
extensions: []
|
|
276
275
|
extra_rdoc_files: []
|
|
277
276
|
files:
|
|
@@ -432,6 +431,7 @@ files:
|
|
|
432
431
|
- lib/clacky/providers.rb
|
|
433
432
|
- lib/clacky/proxy_config.rb
|
|
434
433
|
- lib/clacky/rich_ui_controller.rb
|
|
434
|
+
- lib/clacky/server/backup_manager.rb
|
|
435
435
|
- lib/clacky/server/browser_manager.rb
|
|
436
436
|
- lib/clacky/server/channel.rb
|
|
437
437
|
- lib/clacky/server/channel/adapters/base.rb
|
|
@@ -456,7 +456,6 @@ files:
|
|
|
456
456
|
- lib/clacky/server/channel/channel_config.rb
|
|
457
457
|
- lib/clacky/server/channel/channel_manager.rb
|
|
458
458
|
- lib/clacky/server/channel/channel_ui_controller.rb
|
|
459
|
-
- lib/clacky/server/channel/group_message_buffer.rb
|
|
460
459
|
- lib/clacky/server/channel/user_adapter_loader.rb
|
|
461
460
|
- lib/clacky/server/discover.rb
|
|
462
461
|
- lib/clacky/server/epipe_safe_io.rb
|
|
@@ -540,12 +539,16 @@ files:
|
|
|
540
539
|
- lib/clacky/web/app.js
|
|
541
540
|
- lib/clacky/web/apple-touch-icon-180.png
|
|
542
541
|
- lib/clacky/web/auth.js
|
|
542
|
+
- lib/clacky/web/backup.js
|
|
543
543
|
- lib/clacky/web/billing.js
|
|
544
544
|
- lib/clacky/web/brand.js
|
|
545
545
|
- lib/clacky/web/channels.js
|
|
546
546
|
- lib/clacky/web/creator.js
|
|
547
547
|
- lib/clacky/web/datepicker.js
|
|
548
|
+
- lib/clacky/web/design-sample.css
|
|
549
|
+
- lib/clacky/web/design-sample.html
|
|
548
550
|
- lib/clacky/web/favicon.ico
|
|
551
|
+
- lib/clacky/web/favicon.svg
|
|
549
552
|
- lib/clacky/web/i18n.js
|
|
550
553
|
- lib/clacky/web/icon-dark.svg
|
|
551
554
|
- lib/clacky/web/icon.svg
|
|
@@ -628,7 +631,6 @@ metadata:
|
|
|
628
631
|
homepage_uri: https://github.com/clacky-ai/openclacky
|
|
629
632
|
source_code_uri: https://github.com/clacky-ai/openclacky
|
|
630
633
|
changelog_uri: https://github.com/clacky-ai/openclacky/blob/main/CHANGELOG.md
|
|
631
|
-
post_install_message:
|
|
632
634
|
rdoc_options: []
|
|
633
635
|
require_paths:
|
|
634
636
|
- lib
|
|
@@ -646,8 +648,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
646
648
|
- !ruby/object:Gem::Version
|
|
647
649
|
version: '0'
|
|
648
650
|
requirements: []
|
|
649
|
-
rubygems_version: 3.
|
|
650
|
-
signing_key:
|
|
651
|
+
rubygems_version: 3.6.9
|
|
651
652
|
specification_version: 4
|
|
652
653
|
summary: The most Token-efficient open-source AI Agent — BYOK, Skill-driven, IM-integrated.
|
|
653
654
|
test_files: []
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Clacky
|
|
4
|
-
module Channel
|
|
5
|
-
# Stores recent group chat messages per chat_id so that when the bot is
|
|
6
|
-
# @-mentioned it can inject prior conversation context into the agent prompt.
|
|
7
|
-
# Thread-safe; bounded to MAX_MESSAGES per chat to limit memory growth.
|
|
8
|
-
class GroupMessageBuffer
|
|
9
|
-
MAX_MESSAGES = 15
|
|
10
|
-
PROMPT_LIMIT = 5
|
|
11
|
-
|
|
12
|
-
Entry = Struct.new(:user_id, :user_name, :text, keyword_init: true)
|
|
13
|
-
|
|
14
|
-
def initialize
|
|
15
|
-
@buffers = {}
|
|
16
|
-
@mutex = Mutex.new
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# @param chat_id [String]
|
|
20
|
-
# @param user_id [String]
|
|
21
|
-
# @param user_name [String, nil]
|
|
22
|
-
# @param text [String]
|
|
23
|
-
def push(chat_id, user_id:, text:, user_name: nil)
|
|
24
|
-
return if text.nil? || text.strip.empty?
|
|
25
|
-
|
|
26
|
-
@mutex.synchronize do
|
|
27
|
-
buf = (@buffers[chat_id] ||= [])
|
|
28
|
-
buf << Entry.new(user_id: user_id, user_name: user_name, text: text.strip)
|
|
29
|
-
buf.shift if buf.size > MAX_MESSAGES
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Return the most recent `limit` entries without clearing the buffer.
|
|
34
|
-
# @param chat_id [String]
|
|
35
|
-
# @param limit [Integer, nil] max entries to return; nil = all
|
|
36
|
-
# @return [Array<Entry>]
|
|
37
|
-
def peek(chat_id, limit: nil)
|
|
38
|
-
@mutex.synchronize do
|
|
39
|
-
buf = @buffers[chat_id] || []
|
|
40
|
-
limit ? buf.last(limit) : buf.dup
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# Return buffered entries for a chat and clear them atomically.
|
|
45
|
-
# Returns an empty array when there is no history.
|
|
46
|
-
# @param chat_id [String]
|
|
47
|
-
# @return [Array<Entry>]
|
|
48
|
-
def take(chat_id)
|
|
49
|
-
@mutex.synchronize { @buffers.delete(chat_id) || [] }
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|