openclacky 1.2.18 → 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 +21 -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 +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 +65 -50
- data/lib/clacky/server/http_server.rb +345 -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 +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 +1 -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 +7 -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
|
@@ -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
|
}
|
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
|
|
@@ -432,6 +432,7 @@ files:
|
|
|
432
432
|
- lib/clacky/providers.rb
|
|
433
433
|
- lib/clacky/proxy_config.rb
|
|
434
434
|
- lib/clacky/rich_ui_controller.rb
|
|
435
|
+
- lib/clacky/server/backup_manager.rb
|
|
435
436
|
- lib/clacky/server/browser_manager.rb
|
|
436
437
|
- lib/clacky/server/channel.rb
|
|
437
438
|
- lib/clacky/server/channel/adapters/base.rb
|
|
@@ -456,7 +457,6 @@ files:
|
|
|
456
457
|
- lib/clacky/server/channel/channel_config.rb
|
|
457
458
|
- lib/clacky/server/channel/channel_manager.rb
|
|
458
459
|
- lib/clacky/server/channel/channel_ui_controller.rb
|
|
459
|
-
- lib/clacky/server/channel/group_message_buffer.rb
|
|
460
460
|
- lib/clacky/server/channel/user_adapter_loader.rb
|
|
461
461
|
- lib/clacky/server/discover.rb
|
|
462
462
|
- lib/clacky/server/epipe_safe_io.rb
|
|
@@ -540,12 +540,16 @@ files:
|
|
|
540
540
|
- lib/clacky/web/app.js
|
|
541
541
|
- lib/clacky/web/apple-touch-icon-180.png
|
|
542
542
|
- lib/clacky/web/auth.js
|
|
543
|
+
- lib/clacky/web/backup.js
|
|
543
544
|
- lib/clacky/web/billing.js
|
|
544
545
|
- lib/clacky/web/brand.js
|
|
545
546
|
- lib/clacky/web/channels.js
|
|
546
547
|
- lib/clacky/web/creator.js
|
|
547
548
|
- lib/clacky/web/datepicker.js
|
|
549
|
+
- lib/clacky/web/design-sample.css
|
|
550
|
+
- lib/clacky/web/design-sample.html
|
|
548
551
|
- lib/clacky/web/favicon.ico
|
|
552
|
+
- lib/clacky/web/favicon.svg
|
|
549
553
|
- lib/clacky/web/i18n.js
|
|
550
554
|
- lib/clacky/web/icon-dark.svg
|
|
551
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
|