openclacky 1.2.17 → 1.3.0
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 +34 -0
- data/lib/clacky/agent/skill_manager.rb +1 -1
- data/lib/clacky/agent/time_machine.rb +256 -74
- data/lib/clacky/agent/tool_executor.rb +12 -0
- data/lib/clacky/agent.rb +21 -31
- data/lib/clacky/agent_config.rb +18 -0
- data/lib/clacky/cli.rb +55 -3
- data/lib/clacky/default_skills/media-gen/SKILL.md +173 -5
- data/lib/clacky/default_skills/skill-creator/SKILL.md +1 -0
- data/lib/clacky/media/base.rb +125 -0
- data/lib/clacky/media/dashscope.rb +243 -0
- data/lib/clacky/media/gemini.rb +10 -0
- data/lib/clacky/media/generator.rb +75 -0
- data/lib/clacky/media/openai_compat.rb +160 -0
- data/lib/clacky/message_history.rb +12 -7
- data/lib/clacky/providers.rb +28 -0
- 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 +180 -81
- data/lib/clacky/server/http_server.rb +348 -15
- 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/skill.rb +3 -1
- 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 +2038 -1147
- data/lib/clacky/web/app.js +22 -1
- data/lib/clacky/web/backup.js +119 -0
- data/lib/clacky/web/billing.js +94 -7
- 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 +159 -31
- data/lib/clacky/web/index.html +175 -55
- data/lib/clacky/web/logo_nav_dark.png +0 -0
- data/lib/clacky/web/onboard.js +114 -28
- data/lib/clacky/web/sessions.js +436 -192
- data/lib/clacky/web/settings.js +21 -1
- data/lib/clacky/web/skills.js +6 -6
- 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 +8 -3
- 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
|
@@ -41,8 +41,8 @@ const Skills = (() => {
|
|
|
41
41
|
if (showSystemLabel) showSystemLabel.style.display = tab === "my-skills" ? "" : "none";
|
|
42
42
|
if (refreshBtn) refreshBtn.style.display = tab === "brand-skills" ? "" : "none";
|
|
43
43
|
|
|
44
|
-
//
|
|
45
|
-
if (tab === "brand-skills"
|
|
44
|
+
// Refresh brand skills every time the tab is opened
|
|
45
|
+
if (tab === "brand-skills") {
|
|
46
46
|
_loadBrandSkills();
|
|
47
47
|
}
|
|
48
48
|
}
|
|
@@ -374,7 +374,7 @@ const Skills = (() => {
|
|
|
374
374
|
const toggleTop = toggleLabel.getBoundingClientRect().top;
|
|
375
375
|
const scrollerTop = scroller.getBoundingClientRect().top;
|
|
376
376
|
if (toggleTop - scrollerTop < 80) {
|
|
377
|
-
toggleLabel.
|
|
377
|
+
toggleLabel.setAttribute("data-tooltip-pos", "bottom");
|
|
378
378
|
}
|
|
379
379
|
});
|
|
380
380
|
}
|
|
@@ -395,10 +395,10 @@ const Skills = (() => {
|
|
|
395
395
|
if (!container) { console.error("[Skills] skills-list not found!"); return; }
|
|
396
396
|
container.innerHTML = "";
|
|
397
397
|
|
|
398
|
-
// Optionally hide system (source=default) skills
|
|
398
|
+
// Optionally hide system (source=default) skills that aren't marked always_show
|
|
399
399
|
const visible = _showSystemSkills
|
|
400
400
|
? _skills
|
|
401
|
-
: _skills.filter(s => s.source !== "default");
|
|
401
|
+
: _skills.filter(s => s.always_show || s.source !== "default");
|
|
402
402
|
|
|
403
403
|
if (visible.length === 0) {
|
|
404
404
|
const emptyText = I18n.t("skills.empty");
|
|
@@ -907,7 +907,7 @@ const SkillAC = (() => {
|
|
|
907
907
|
.filter(({ score }) => score > 0);
|
|
908
908
|
|
|
909
909
|
if (!_showSystemSkills) {
|
|
910
|
-
scored = scored.filter(({ skill }) => skill.source_type !== "default");
|
|
910
|
+
scored = scored.filter(({ skill }) => skill.always_show || skill.source_type !== "default");
|
|
911
911
|
}
|
|
912
912
|
|
|
913
913
|
// Sort by score descending, stable secondary sort by name
|
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,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: openclacky
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- windy
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -417,6 +417,7 @@ files:
|
|
|
417
417
|
- lib/clacky/mcp/transport.rb
|
|
418
418
|
- lib/clacky/mcp/virtual_skill.rb
|
|
419
419
|
- lib/clacky/media/base.rb
|
|
420
|
+
- lib/clacky/media/dashscope.rb
|
|
420
421
|
- lib/clacky/media/gemini.rb
|
|
421
422
|
- lib/clacky/media/generator.rb
|
|
422
423
|
- lib/clacky/media/openai_compat.rb
|
|
@@ -431,6 +432,7 @@ files:
|
|
|
431
432
|
- lib/clacky/providers.rb
|
|
432
433
|
- lib/clacky/proxy_config.rb
|
|
433
434
|
- lib/clacky/rich_ui_controller.rb
|
|
435
|
+
- lib/clacky/server/backup_manager.rb
|
|
434
436
|
- lib/clacky/server/browser_manager.rb
|
|
435
437
|
- lib/clacky/server/channel.rb
|
|
436
438
|
- lib/clacky/server/channel/adapters/base.rb
|
|
@@ -455,7 +457,6 @@ files:
|
|
|
455
457
|
- lib/clacky/server/channel/channel_config.rb
|
|
456
458
|
- lib/clacky/server/channel/channel_manager.rb
|
|
457
459
|
- lib/clacky/server/channel/channel_ui_controller.rb
|
|
458
|
-
- lib/clacky/server/channel/group_message_buffer.rb
|
|
459
460
|
- lib/clacky/server/channel/user_adapter_loader.rb
|
|
460
461
|
- lib/clacky/server/discover.rb
|
|
461
462
|
- lib/clacky/server/epipe_safe_io.rb
|
|
@@ -539,12 +540,16 @@ files:
|
|
|
539
540
|
- lib/clacky/web/app.js
|
|
540
541
|
- lib/clacky/web/apple-touch-icon-180.png
|
|
541
542
|
- lib/clacky/web/auth.js
|
|
543
|
+
- lib/clacky/web/backup.js
|
|
542
544
|
- lib/clacky/web/billing.js
|
|
543
545
|
- lib/clacky/web/brand.js
|
|
544
546
|
- lib/clacky/web/channels.js
|
|
545
547
|
- lib/clacky/web/creator.js
|
|
546
548
|
- lib/clacky/web/datepicker.js
|
|
549
|
+
- lib/clacky/web/design-sample.css
|
|
550
|
+
- lib/clacky/web/design-sample.html
|
|
547
551
|
- lib/clacky/web/favicon.ico
|
|
552
|
+
- lib/clacky/web/favicon.svg
|
|
548
553
|
- lib/clacky/web/i18n.js
|
|
549
554
|
- lib/clacky/web/icon-dark.svg
|
|
550
555
|
- lib/clacky/web/icon.svg
|
|
@@ -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
|