openclacky 1.0.4 → 1.1.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/.clacky/skills/gem-release/SKILL.md +99 -356
- data/.clacky/skills/gem-release/scripts/release.sh +304 -0
- data/CHANGELOG.md +42 -0
- data/docs/system-skill-authoring-guide.md +1 -1
- data/lib/clacky/agent/tool_executor.rb +3 -1
- data/lib/clacky/agent.rb +12 -7
- data/lib/clacky/agent_config.rb +9 -3
- data/lib/clacky/brand_config.rb +19 -4
- data/lib/clacky/cli.rb +1 -1
- data/lib/clacky/default_skills/{channel-setup → channel-manager}/SKILL.md +180 -18
- data/lib/clacky/default_skills/channel-manager/dingtalk_setup.rb +191 -0
- data/lib/clacky/default_skills/channel-manager/discord_setup.rb +199 -0
- data/lib/clacky/default_skills/channel-manager/install_feishu_skills.rb +105 -0
- data/lib/clacky/default_skills/onboard/SKILL.md +2 -2
- data/lib/clacky/default_skills/onboard/scripts/import_external_skills.rb +2 -4
- data/lib/clacky/default_skills/onboard/scripts/install_builtin_skills.rb +18 -96
- data/lib/clacky/default_skills/product-help/SKILL.md +10 -2
- data/lib/clacky/message_history.rb +26 -1
- data/lib/clacky/providers.rb +29 -4
- data/lib/clacky/server/channel/adapters/dingtalk/adapter.rb +177 -0
- data/lib/clacky/server/channel/adapters/dingtalk/api_client.rb +82 -0
- data/lib/clacky/server/channel/adapters/dingtalk/stream_client.rb +205 -0
- data/lib/clacky/server/channel/adapters/discord/adapter.rb +229 -0
- data/lib/clacky/server/channel/adapters/discord/api_client.rb +108 -0
- data/lib/clacky/server/channel/adapters/discord/gateway_client.rb +272 -0
- data/lib/clacky/server/channel/adapters/telegram/adapter.rb +375 -0
- data/lib/clacky/server/channel/adapters/telegram/api_client.rb +205 -0
- data/lib/clacky/server/channel/channel_config.rb +26 -0
- data/lib/clacky/server/channel.rb +3 -0
- data/lib/clacky/server/http_server.rb +75 -4
- data/lib/clacky/server/server_master.rb +35 -13
- data/lib/clacky/server/session_registry.rb +54 -3
- data/lib/clacky/server/web_ui_controller.rb +7 -1
- data/lib/clacky/telemetry.rb +1 -16
- data/lib/clacky/tools/browser.rb +8 -5
- data/lib/clacky/tools/glob.rb +11 -38
- data/lib/clacky/tools/grep.rb +7 -16
- data/lib/clacky/ui2/markdown_renderer.rb +1 -1
- data/lib/clacky/ui2/ui_controller.rb +2 -1
- data/lib/clacky/utils/file_ignore_helper.rb +49 -0
- data/lib/clacky/utils/gitignore_parser.rb +27 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +248 -31
- data/lib/clacky/web/app.js +51 -1
- data/lib/clacky/web/channels.js +98 -28
- data/lib/clacky/web/datepicker.js +205 -0
- data/lib/clacky/web/i18n.js +48 -9
- data/lib/clacky/web/index.html +33 -6
- data/lib/clacky/web/onboard.js +46 -4
- data/lib/clacky/web/sessions.js +33 -72
- data/lib/clacky/web/settings.js +42 -4
- data/lib/clacky/web/version.js +52 -1
- metadata +21 -10
- data/docs/proposals/2026-05-11-system-prompt-alignment.md +0 -325
- data/docs/proposals/2026-05-12-memory-mechanism-optimization.md +0 -89
- /data/lib/clacky/default_skills/{channel-setup → channel-manager}/feishu_setup.rb +0 -0
- /data/lib/clacky/default_skills/{channel-setup → channel-manager}/import_lark_skills.rb +0 -0
- /data/lib/clacky/default_skills/{channel-setup → channel-manager}/weixin_setup.rb +0 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// datepicker.js — Lightweight custom calendar picker
|
|
2
|
+
//
|
|
3
|
+
// Usage:
|
|
4
|
+
// Any <button data-value=""> with class "datepicker-trigger" gets a calendar
|
|
5
|
+
// popup on click. When a date is selected the button's data-value is set to
|
|
6
|
+
// an ISO string (YYYY-MM-DD) and a "datepicker:change" CustomEvent bubbles up.
|
|
7
|
+
//
|
|
8
|
+
// DatePicker.clear(el) — clear value and label
|
|
9
|
+
// DatePicker.init(el) — programmatically attach (optional; click delegation
|
|
10
|
+
// handles all .datepicker-trigger elements automatically)
|
|
11
|
+
|
|
12
|
+
const DatePicker = (() => {
|
|
13
|
+
const LOCALE = {
|
|
14
|
+
en: {
|
|
15
|
+
days: ["Su","Mo","Tu","We","Th","Fr","Sa"],
|
|
16
|
+
months: ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],
|
|
17
|
+
title: (y, m, months) => `${months[m]} ${y}`,
|
|
18
|
+
clear: "Clear",
|
|
19
|
+
today: "Today",
|
|
20
|
+
label: (y, m, d) => `${y}/${String(m + 1).padStart(2,"0")}/${String(d).padStart(2,"0")}`,
|
|
21
|
+
},
|
|
22
|
+
zh: {
|
|
23
|
+
days: ["日","一","二","三","四","五","六"],
|
|
24
|
+
months: ["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],
|
|
25
|
+
title: (y, m, months) => `${y}年${months[m]}`,
|
|
26
|
+
clear: "清除",
|
|
27
|
+
today: "今天",
|
|
28
|
+
label: (y, m, d) => `${y}/${String(m + 1).padStart(2,"0")}/${String(d).padStart(2,"0")}`,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function _loc() {
|
|
33
|
+
const code = (typeof I18n !== "undefined") ? I18n.lang() : "en";
|
|
34
|
+
return LOCALE[code] || LOCALE.en;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function _pad(n) { return String(n).padStart(2, "0"); }
|
|
38
|
+
function _toISO(y, m, d) { return `${y}-${_pad(m + 1)}-${_pad(d)}`; }
|
|
39
|
+
|
|
40
|
+
let _popup = null;
|
|
41
|
+
let _anchor = null;
|
|
42
|
+
let _year = 0;
|
|
43
|
+
let _month = 0;
|
|
44
|
+
|
|
45
|
+
function _ensurePopup() {
|
|
46
|
+
if (_popup) return;
|
|
47
|
+
_popup = document.createElement("div");
|
|
48
|
+
_popup.className = "dp-popup";
|
|
49
|
+
_popup.hidden = true;
|
|
50
|
+
document.body.appendChild(_popup);
|
|
51
|
+
|
|
52
|
+
_popup.addEventListener("click", (e) => {
|
|
53
|
+
const nav = e.target.closest(".dp-nav");
|
|
54
|
+
if (nav) {
|
|
55
|
+
e.stopPropagation();
|
|
56
|
+
_month += parseInt(nav.dataset.dir);
|
|
57
|
+
if (_month < 0) { _month = 11; _year--; }
|
|
58
|
+
if (_month > 11) { _month = 0; _year++; }
|
|
59
|
+
_render();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const day = e.target.closest("[data-date]");
|
|
63
|
+
if (day) { _pick(day.dataset.date); return; }
|
|
64
|
+
if (e.target.closest(".dp-btn-clear")) {
|
|
65
|
+
clear(_anchor); _dispatch(); _close(); return;
|
|
66
|
+
}
|
|
67
|
+
if (e.target.closest(".dp-btn-today")) {
|
|
68
|
+
const t = new Date();
|
|
69
|
+
_pick(_toISO(t.getFullYear(), t.getMonth(), t.getDate()));
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _render() {
|
|
75
|
+
const loc = _loc();
|
|
76
|
+
const selVal = _anchor ? (_anchor.dataset.value || "") : "";
|
|
77
|
+
const today = new Date();
|
|
78
|
+
const tY = today.getFullYear(), tM = today.getMonth(), tD = today.getDate();
|
|
79
|
+
|
|
80
|
+
const firstWeekday = new Date(_year, _month, 1).getDay();
|
|
81
|
+
const daysInMonth = new Date(_year, _month + 1, 0).getDate();
|
|
82
|
+
|
|
83
|
+
let html = `<div class="dp-header">
|
|
84
|
+
<button class="dp-nav" data-dir="-12" type="button">≪</button>
|
|
85
|
+
<button class="dp-nav" data-dir="-1" type="button">‹</button>
|
|
86
|
+
<span class="dp-title">${loc.title(_year, _month, loc.months)}</span>
|
|
87
|
+
<button class="dp-nav" data-dir="1" type="button">›</button>
|
|
88
|
+
<button class="dp-nav" data-dir="12" type="button">≫</button>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="dp-grid">`;
|
|
91
|
+
|
|
92
|
+
loc.days.forEach(d => { html += `<div class="dp-dow">${d}</div>`; });
|
|
93
|
+
for (let i = 0; i < firstWeekday; i++) html += `<div></div>`;
|
|
94
|
+
|
|
95
|
+
for (let d = 1; d <= daysInMonth; d++) {
|
|
96
|
+
const iso = _toISO(_year, _month, d);
|
|
97
|
+
const cls = ["dp-day",
|
|
98
|
+
iso === selVal ? "dp-day--selected" : "",
|
|
99
|
+
(_year === tY && _month === tM && d === tD) ? "dp-day--today" : "",
|
|
100
|
+
].filter(Boolean).join(" ");
|
|
101
|
+
html += `<button class="${cls}" data-date="${iso}" type="button">${d}</button>`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
html += `</div>
|
|
105
|
+
<div class="dp-footer">
|
|
106
|
+
<button class="dp-btn-clear" type="button">${loc.clear}</button>
|
|
107
|
+
<button class="dp-btn-today" type="button">${loc.today}</button>
|
|
108
|
+
</div>`;
|
|
109
|
+
|
|
110
|
+
_popup.innerHTML = html;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function _pick(iso) {
|
|
114
|
+
if (!_anchor) return;
|
|
115
|
+
const [y, m, d] = iso.split("-").map(Number);
|
|
116
|
+
const loc = _loc();
|
|
117
|
+
_anchor.dataset.value = iso;
|
|
118
|
+
_anchor.textContent = loc.label(y, m - 1, d);
|
|
119
|
+
_anchor.dataset.active = "true";
|
|
120
|
+
_anchor.removeAttribute("data-i18n");
|
|
121
|
+
_dispatch();
|
|
122
|
+
_close();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function _dispatch() {
|
|
126
|
+
if (!_anchor) return;
|
|
127
|
+
_anchor.dispatchEvent(new CustomEvent("datepicker:change", { bubbles: true }));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function _position() {
|
|
131
|
+
if (!_anchor || !_popup || _popup.hidden) return;
|
|
132
|
+
const rect = _anchor.getBoundingClientRect();
|
|
133
|
+
_popup.style.left = rect.left + "px";
|
|
134
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
135
|
+
if (spaceBelow >= 290) {
|
|
136
|
+
_popup.style.top = (rect.bottom + 4) + "px";
|
|
137
|
+
_popup.style.bottom = "auto";
|
|
138
|
+
} else {
|
|
139
|
+
_popup.style.top = "auto";
|
|
140
|
+
_popup.style.bottom = (window.innerHeight - rect.top + 4) + "px";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function _open(anchorEl) {
|
|
145
|
+
_ensurePopup();
|
|
146
|
+
_anchor = anchorEl;
|
|
147
|
+
const val = anchorEl.dataset.value;
|
|
148
|
+
if (val) {
|
|
149
|
+
const d = new Date(val + "T00:00:00");
|
|
150
|
+
_year = d.getFullYear(); _month = d.getMonth();
|
|
151
|
+
} else {
|
|
152
|
+
const now = new Date();
|
|
153
|
+
_year = now.getFullYear(); _month = now.getMonth();
|
|
154
|
+
}
|
|
155
|
+
_render();
|
|
156
|
+
_popup.hidden = false;
|
|
157
|
+
_position();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function _close() {
|
|
161
|
+
if (_popup) _popup.hidden = true;
|
|
162
|
+
_anchor = null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Click delegation — open on trigger, close on outside click
|
|
166
|
+
document.addEventListener("click", (e) => {
|
|
167
|
+
const trigger = e.target.closest(".datepicker-trigger");
|
|
168
|
+
if (trigger) {
|
|
169
|
+
e.stopPropagation();
|
|
170
|
+
if (_anchor === trigger) { _close(); return; }
|
|
171
|
+
_open(trigger);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (_popup && !_popup.hidden && !e.target.closest(".dp-popup")) _close();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
window.addEventListener("scroll", _position, true);
|
|
178
|
+
window.addEventListener("resize", _position);
|
|
179
|
+
|
|
180
|
+
// Re-render popup label when language changes
|
|
181
|
+
document.addEventListener("langchange", () => {
|
|
182
|
+
if (_popup && !_popup.hidden && _anchor) _render();
|
|
183
|
+
_refreshTriggers();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
function _refreshTriggers() {
|
|
187
|
+
const loc = _loc();
|
|
188
|
+
document.querySelectorAll(".datepicker-trigger[data-value]").forEach(el => {
|
|
189
|
+
const val = el.dataset.value;
|
|
190
|
+
if (!val) return;
|
|
191
|
+
const [y, m, d] = val.split("-").map(Number);
|
|
192
|
+
el.textContent = loc.label(y, m - 1, d);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function clear(el) {
|
|
197
|
+
if (!el) return;
|
|
198
|
+
el.dataset.value = "";
|
|
199
|
+
el.dataset.active = "false";
|
|
200
|
+
el.setAttribute("data-i18n", "sessions.search.datePlaceholder");
|
|
201
|
+
if (typeof I18n !== "undefined") el.textContent = I18n.t("sessions.search.datePlaceholder");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { clear };
|
|
205
|
+
})();
|
data/lib/clacky/web/i18n.js
CHANGED
|
@@ -60,7 +60,7 @@ const I18n = (() => {
|
|
|
60
60
|
|
|
61
61
|
// ── Session list ──
|
|
62
62
|
"sessions.empty": "No sessions yet",
|
|
63
|
-
"sessions.confirmDelete": "Delete session
|
|
63
|
+
"sessions.confirmDelete": "Delete session {{name}}? This cannot be undone.",
|
|
64
64
|
"sessions.meta": "{{tasks}} tasks · ${{cost}}",
|
|
65
65
|
"sessions.metaTasks": "{{n}} tasks",
|
|
66
66
|
"sessions.deleteTitle": "Delete session",
|
|
@@ -92,6 +92,7 @@ const I18n = (() => {
|
|
|
92
92
|
"sessions.search.typeChannel": "Channel",
|
|
93
93
|
"sessions.search.typeCoding": "Coding",
|
|
94
94
|
"sessions.search.typeSetup": "Setup",
|
|
95
|
+
"sessions.search.datePlaceholder": "Date",
|
|
95
96
|
|
|
96
97
|
// ── Modal ──
|
|
97
98
|
"modal.yes": "Yes",
|
|
@@ -130,6 +131,11 @@ const I18n = (() => {
|
|
|
130
131
|
"upgrade.failed": "Upgrade failed. Try again or run manually: gem update openclacky",
|
|
131
132
|
"upgrade.reconnecting": "Restarting server…",
|
|
132
133
|
"upgrade.restart.success": "✓ Restarted successfully!",
|
|
134
|
+
"upgrade.restart.timeout.title": "Hot restart didn't complete",
|
|
135
|
+
"upgrade.restart.timeout.desc": "The server didn't come back within 30 s. Please recover using one of the methods below:",
|
|
136
|
+
"upgrade.restart.timeout.tray": "From the tray menu, choose “Quit” and start the app again.",
|
|
137
|
+
"upgrade.restart.timeout.cli": "If you launched from a terminal, run: {{cmd}}",
|
|
138
|
+
"upgrade.restart.timeout.retry": "Try Again",
|
|
133
139
|
"upgrade.tooltip.upgrading": "Upgrading — click to see progress",
|
|
134
140
|
"upgrade.tooltip.new": "v{{latest}} available — click to upgrade",
|
|
135
141
|
"upgrade.tooltip.ok": "v{{current}} (up to date)",
|
|
@@ -347,17 +353,20 @@ const I18n = (() => {
|
|
|
347
353
|
|
|
348
354
|
// ── Channels panel ──
|
|
349
355
|
"channels.title": "Channels",
|
|
350
|
-
"channels.subtitle": "Connect IM platforms so your users can chat with the assistant via Feishu, WeCom or
|
|
356
|
+
"channels.subtitle": "Connect IM platforms so your users can chat with the assistant via Feishu, WeCom, Weixin, Discord, Telegram or DingTalk",
|
|
351
357
|
"channels.loading": "Loading…",
|
|
352
358
|
"channels.badge.running": "Running",
|
|
353
359
|
"channels.badge.enabled": "Enabled",
|
|
354
|
-
"channels.badge.disabled": "
|
|
360
|
+
"channels.badge.disabled": "Disabled",
|
|
355
361
|
"channels.badge.notConfigured": "Not configured",
|
|
356
362
|
"channels.hint.running": "Adapter is running and accepting messages from users",
|
|
357
363
|
"channels.hint.enabled": "⚠ Enabled but not running — the adapter may have failed to connect. Run Diagnostics to investigate.",
|
|
358
364
|
"channels.hint.enabledNotRunning":"Enabled but not running — the adapter may have failed to connect. Run Diagnostics to investigate.",
|
|
359
365
|
"channels.hint.idle": "Not configured yet. Click \"Set Up with Agent\" to get started.",
|
|
360
366
|
"channels.hint.notConfigured": "Not configured yet. Click \"Set Up with Agent\" to get started.",
|
|
367
|
+
"channels.hint.disabled": "Configured but turned off. Use the toggle to enable.",
|
|
368
|
+
"channels.toggle.on": "Enabled — click to turn off",
|
|
369
|
+
"channels.toggle.off": "Disabled — click to turn on",
|
|
361
370
|
"channels.btn.test": "Diagnostics",
|
|
362
371
|
"channels.btn.setup": "Set Up with Agent",
|
|
363
372
|
"channels.btn.reconfig": "Reconfigure",
|
|
@@ -368,6 +377,9 @@ const I18n = (() => {
|
|
|
368
377
|
"channels.feishu.desc": "Connect via Feishu open platform WebSocket long connection",
|
|
369
378
|
"channels.wecom.desc": "Connect via WeCom intelligent robot WebSocket",
|
|
370
379
|
"channels.weixin.desc": "Connect via WeChat iLink bot (QR login, HTTP long-poll)",
|
|
380
|
+
"channels.discord.desc": "Connect via Discord bot, invite via OAuth2 authorization link",
|
|
381
|
+
"channels.telegram.desc": "Connect via Telegram Bot API (HTTPS long-poll, token from @BotFather)",
|
|
382
|
+
"channels.dingtalk.desc": "Connect via DingTalk Stream Mode WebSocket",
|
|
371
383
|
|
|
372
384
|
// ── Settings panel ──
|
|
373
385
|
"settings.title": "Settings",
|
|
@@ -383,6 +395,8 @@ const I18n = (() => {
|
|
|
383
395
|
"settings.models.field.baseurl": "Base URL",
|
|
384
396
|
"settings.models.field.apikey": "API Key",
|
|
385
397
|
"settings.models.field.getApiKey": "How to get →",
|
|
398
|
+
"settings.models.field.docsGuide.question": "New to AI keys?",
|
|
399
|
+
"settings.models.field.docsGuide.cta": "See the guide →",
|
|
386
400
|
"settings.models.placeholder.provider": "— Choose provider —",
|
|
387
401
|
"settings.models.placeholder.model": "e.g. claude-sonnet-4-5",
|
|
388
402
|
"settings.models.placeholder.baseurl": "https://api.anthropic.com",
|
|
@@ -471,9 +485,16 @@ const I18n = (() => {
|
|
|
471
485
|
"onboard.key.baseurl": "Base URL",
|
|
472
486
|
"onboard.key.apikey": "API Key",
|
|
473
487
|
"onboard.key.getApiKey": "How to get →",
|
|
488
|
+
"onboard.key.docsGuide.question": "New to AI keys?",
|
|
489
|
+
"onboard.key.docsGuide.cta": "See the guide →",
|
|
474
490
|
"onboard.key.btn.test": "Test & Continue →",
|
|
475
491
|
"onboard.key.btn.back": "← Back",
|
|
476
492
|
"onboard.provider.custom": "Custom",
|
|
493
|
+
"provider.recommended": "Recommended",
|
|
494
|
+
"provider.promo.openclacky.title": "Recommended: OpenClacky",
|
|
495
|
+
"provider.promo.openclacky.1": "One key for all frontier models worldwide.",
|
|
496
|
+
"provider.promo.openclacky.2": "Switch models instantly — no extra accounts or billing setup.",
|
|
497
|
+
"provider.promo.openclacky.3": "Same pricing as official APIs, with ultra-low latency.",
|
|
477
498
|
"onboard.key.testing": "Testing…",
|
|
478
499
|
"onboard.key.saving": "Saving…",
|
|
479
500
|
"onboard.soul.title": "Personalize your assistant",
|
|
@@ -558,7 +579,7 @@ const I18n = (() => {
|
|
|
558
579
|
|
|
559
580
|
// ── Session list ──
|
|
560
581
|
"sessions.empty": "暂无对话",
|
|
561
|
-
"sessions.confirmDelete": "
|
|
582
|
+
"sessions.confirmDelete": "删除对话 {{name}}?此操作不可撤销。",
|
|
562
583
|
"sessions.meta": "{{tasks}} 个任务 · ${{cost}}",
|
|
563
584
|
"sessions.metaTasks": "{{n}} 个任务",
|
|
564
585
|
"sessions.deleteTitle": "删除对话",
|
|
@@ -590,9 +611,7 @@ const I18n = (() => {
|
|
|
590
611
|
"sessions.search.typeChannel": "频道",
|
|
591
612
|
"sessions.search.typeCoding": "Coding",
|
|
592
613
|
"sessions.search.typeSetup": "配置",
|
|
593
|
-
|
|
594
|
-
// ── Modal ──
|
|
595
|
-
"modal.yes": "确认",
|
|
614
|
+
"sessions.search.datePlaceholder": "日期",
|
|
596
615
|
"modal.no": "取消",
|
|
597
616
|
"modal.ok": "确定",
|
|
598
617
|
"modal.cancel": "取消",
|
|
@@ -628,6 +647,11 @@ const I18n = (() => {
|
|
|
628
647
|
"upgrade.failed": "升级失败,请重试或手动执行:gem update openclacky",
|
|
629
648
|
"upgrade.reconnecting": "服务重启中…",
|
|
630
649
|
"upgrade.restart.success": "✓ 重启成功!",
|
|
650
|
+
"upgrade.restart.timeout.title": "热重启未完成",
|
|
651
|
+
"upgrade.restart.timeout.desc": "服务在 30 秒内未恢复。请通过以下任一方式重启:",
|
|
652
|
+
"upgrade.restart.timeout.tray": "在系统托盘菜单中选择「退出」,然后重新打开应用。",
|
|
653
|
+
"upgrade.restart.timeout.cli": "如果是终端启动,请执行命令: {{cmd}}",
|
|
654
|
+
"upgrade.restart.timeout.retry": "重试",
|
|
631
655
|
"upgrade.tooltip.upgrading": "升级中,点击查看进度",
|
|
632
656
|
"upgrade.tooltip.new": "v{{latest}} 可用,点击升级",
|
|
633
657
|
"upgrade.tooltip.ok": "v{{current}}(已是最新)",
|
|
@@ -844,18 +868,21 @@ const I18n = (() => {
|
|
|
844
868
|
|
|
845
869
|
// ── Channels panel ──
|
|
846
870
|
"channels.title": "频道",
|
|
847
|
-
"channels.subtitle": "
|
|
871
|
+
"channels.subtitle": "连接即时通讯平台,让用户通过飞书、企业微信、微信、Discord、Telegram 或钉钉与助手对话",
|
|
848
872
|
"channels.loading": "加载中…",
|
|
849
873
|
"channels.loadError": "加载频道失败:{{msg}}",
|
|
850
874
|
"channels.badge.running": "运行中",
|
|
851
875
|
"channels.badge.enabled": "已启用",
|
|
852
|
-
"channels.badge.disabled": "
|
|
876
|
+
"channels.badge.disabled": "已关闭",
|
|
853
877
|
"channels.badge.notConfigured": "未配置",
|
|
854
878
|
"channels.hint.running": "适配器运行中,正在接受用户消息",
|
|
855
879
|
"channels.hint.enabled": "⚠ 已启用但未运行 — 适配器可能连接失败,请点击「诊断问题」排查。",
|
|
856
880
|
"channels.hint.enabledNotRunning":"已启用但未运行 — 适配器可能连接失败,请点击「诊断问题」排查。",
|
|
857
881
|
"channels.hint.idle": "尚未配置。点击「用 Agent 配置」开始。",
|
|
858
882
|
"channels.hint.notConfigured": "尚未配置。点击「用 Agent 配置」开始。",
|
|
883
|
+
"channels.hint.disabled": "已配置但已关闭,使用开关重新启用。",
|
|
884
|
+
"channels.toggle.on": "已启用 — 点击关闭",
|
|
885
|
+
"channels.toggle.off": "已关闭 — 点击启用",
|
|
859
886
|
"channels.btn.test": "诊断问题",
|
|
860
887
|
"channels.btn.setup": "用 Agent 配置",
|
|
861
888
|
"channels.btn.reconfig": "重新配置",
|
|
@@ -865,6 +892,9 @@ const I18n = (() => {
|
|
|
865
892
|
"channels.feishu.desc": "通过飞书开放平台 WebSocket 长连接接入",
|
|
866
893
|
"channels.wecom.desc": "通过企业微信智能机器人 WebSocket 接入",
|
|
867
894
|
"channels.weixin.desc": "通过微信 iLink 机器人接入(扫码登录,HTTP 长轮询)",
|
|
895
|
+
"channels.discord.desc": "通过 Discord 机器人接入,授权链接邀请 Bot 加入服务器",
|
|
896
|
+
"channels.telegram.desc": "通过 Telegram Bot API 接入(HTTPS 长轮询,token 来自 @BotFather)",
|
|
897
|
+
"channels.dingtalk.desc": "通过钉钉 Stream 模式 WebSocket 接入",
|
|
868
898
|
|
|
869
899
|
// ── Settings panel ──
|
|
870
900
|
"settings.title": "设置",
|
|
@@ -880,6 +910,8 @@ const I18n = (() => {
|
|
|
880
910
|
"settings.models.field.baseurl": "Base URL",
|
|
881
911
|
"settings.models.field.apikey": "API Key",
|
|
882
912
|
"settings.models.field.getApiKey": "如何获取 →",
|
|
913
|
+
"settings.models.field.docsGuide.question": "不会配置 AI 模型?",
|
|
914
|
+
"settings.models.field.docsGuide.cta": "查看指南 →",
|
|
883
915
|
"settings.models.placeholder.provider": "— 选择服务商 —",
|
|
884
916
|
"settings.models.placeholder.model": "如 claude-sonnet-4-5",
|
|
885
917
|
"settings.models.placeholder.baseurl": "https://api.anthropic.com",
|
|
@@ -968,9 +1000,16 @@ const I18n = (() => {
|
|
|
968
1000
|
"onboard.key.baseurl": "Base URL",
|
|
969
1001
|
"onboard.key.apikey": "API Key",
|
|
970
1002
|
"onboard.key.getApiKey": "如何获取 →",
|
|
1003
|
+
"onboard.key.docsGuide.question": "不会配置 AI 模型?",
|
|
1004
|
+
"onboard.key.docsGuide.cta": "查看指南 →",
|
|
971
1005
|
"onboard.key.btn.test": "测试并继续 →",
|
|
972
1006
|
"onboard.key.btn.back": "← 返回",
|
|
973
1007
|
"onboard.provider.custom": "自定义",
|
|
1008
|
+
"provider.recommended": "推荐",
|
|
1009
|
+
"provider.promo.openclacky.title": "推荐选择 OpenClacky",
|
|
1010
|
+
"provider.promo.openclacky.1": "一个 Key 即可使用全球顶级模型。",
|
|
1011
|
+
"provider.promo.openclacky.2": "国内网络直连,无需额外配置。",
|
|
1012
|
+
"provider.promo.openclacky.3": "官方同价,秒级响应延迟。",
|
|
974
1013
|
"onboard.key.testing": "测试中…",
|
|
975
1014
|
"onboard.key.saving": "保存中…",
|
|
976
1015
|
"onboard.soul.title": "个性化助手",
|
data/lib/clacky/web/index.html
CHANGED
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
<option value="coding" data-i18n="sessions.search.typeCoding">Coding</option>
|
|
111
111
|
</select>
|
|
112
112
|
<div class="search-date-wrap">
|
|
113
|
-
<
|
|
113
|
+
<button id="session-search-date" class="search-date datepicker-trigger" type="button" data-value="" data-i18n="sessions.search.datePlaceholder"></button>
|
|
114
114
|
</div>
|
|
115
115
|
<button id="btn-search-clear-all" class="btn-search-clear-all" aria-label="Clear filters" hidden>✕</button>
|
|
116
116
|
</div>
|
|
@@ -834,15 +834,16 @@
|
|
|
834
834
|
<label class="setup-label" data-i18n="onboard.key.provider">Provider</label>
|
|
835
835
|
<div class="custom-select-wrapper" id="setup-provider-wrapper">
|
|
836
836
|
<div class="custom-select-trigger">
|
|
837
|
-
<span class="custom-select-value placeholder">— Choose provider —</span>
|
|
837
|
+
<span class="custom-select-value placeholder" data-i18n="onboard.key.provider.placeholder">— Choose provider —</span>
|
|
838
838
|
<svg class="custom-select-arrow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
839
839
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
840
840
|
</svg>
|
|
841
841
|
</div>
|
|
842
842
|
<div class="custom-select-dropdown" id="setup-provider-dropdown">
|
|
843
|
-
<div class="custom-select-option" data-value="">— Choose provider —</div>
|
|
843
|
+
<div class="custom-select-option" data-value="" data-i18n="onboard.key.provider.placeholder">— Choose provider —</div>
|
|
844
844
|
</div>
|
|
845
845
|
</div>
|
|
846
|
+
<div id="setup-provider-promo" class="provider-promo-hint"></div>
|
|
846
847
|
</div>
|
|
847
848
|
|
|
848
849
|
<div class="setup-field">
|
|
@@ -883,7 +884,12 @@
|
|
|
883
884
|
</div>
|
|
884
885
|
</div>
|
|
885
886
|
|
|
886
|
-
<div
|
|
887
|
+
<div class="setup-docs-hint" style="font-size:12px;margin-top:2px;text-align:left;">
|
|
888
|
+
<span style="color:var(--muted,#6b7280);" data-i18n="onboard.key.docsGuide.question">New to AI keys?</span>
|
|
889
|
+
<a id="setup-docs-link" href="https://www.openclacky.com/docs/ai-key-guide" target="_blank" rel="noopener" style="margin-left:4px;color:var(--accent,#6366f1);text-decoration:none;" data-i18n="onboard.key.docsGuide.cta">See the guide →</a>
|
|
890
|
+
</div>
|
|
891
|
+
|
|
892
|
+
<div id="setup-test-result" class="setup-test-result" style="min-height:0;"></div>
|
|
887
893
|
|
|
888
894
|
<div class="setup-key-actions">
|
|
889
895
|
<button id="setup-btn-back" class="setup-back-btn" data-i18n="onboard.key.btn.back">← Back</button>
|
|
@@ -948,10 +954,10 @@
|
|
|
948
954
|
<!-- ── CONFIRMATION MODAL ────────────────────────────────────────────────── -->
|
|
949
955
|
<div id="modal-overlay" class="modal-overlay" style="display:none">
|
|
950
956
|
<div class="modal-box sm">
|
|
951
|
-
<div id="modal-message"
|
|
957
|
+
<div id="modal-message" class="modal-confirm-message"></div>
|
|
952
958
|
<div class="modal-actions">
|
|
953
|
-
<button id="modal-yes" class="btn-primary" data-i18n="modal.yes">Yes</button>
|
|
954
959
|
<button id="modal-no" class="btn-secondary" data-i18n="modal.no">No</button>
|
|
960
|
+
<button id="modal-yes" class="btn-primary" data-i18n="modal.yes">Yes</button>
|
|
955
961
|
</div>
|
|
956
962
|
</div>
|
|
957
963
|
</div>
|
|
@@ -968,6 +974,26 @@
|
|
|
968
974
|
</div>
|
|
969
975
|
</div>
|
|
970
976
|
|
|
977
|
+
<!-- Rename Session Modal -->
|
|
978
|
+
<div id="rename-modal-overlay" class="modal-overlay" style="display:none">
|
|
979
|
+
<div class="modal-box sm">
|
|
980
|
+
<div class="modal-title" data-i18n="sessions.actions.rename">Rename</div>
|
|
981
|
+
<div class="modal-body">
|
|
982
|
+
<div class="modal-field">
|
|
983
|
+
<label class="modal-label" for="rename-modal-input">
|
|
984
|
+
<span data-i18n="sessions.modal.name">Name</span>
|
|
985
|
+
<span class="label-required">*</span>
|
|
986
|
+
</label>
|
|
987
|
+
<input type="text" id="rename-modal-input" class="modal-input" autocomplete="off" spellcheck="false">
|
|
988
|
+
</div>
|
|
989
|
+
</div>
|
|
990
|
+
<div class="modal-actions">
|
|
991
|
+
<button id="rename-modal-cancel" class="btn-secondary" data-i18n="modal.cancel">Cancel</button>
|
|
992
|
+
<button id="rename-modal-save" class="btn-primary" data-i18n="modal.ok">OK</button>
|
|
993
|
+
</div>
|
|
994
|
+
</div>
|
|
995
|
+
</div>
|
|
996
|
+
|
|
971
997
|
|
|
972
998
|
|
|
973
999
|
<script src="/marked.min.js"></script>
|
|
@@ -977,6 +1003,7 @@
|
|
|
977
1003
|
<script src="/ws.js"></script>
|
|
978
1004
|
<script src="/ws-dispatcher.js"></script>
|
|
979
1005
|
<script src="/sessions.js"></script>
|
|
1006
|
+
<script src="/datepicker.js"></script>
|
|
980
1007
|
<script src="/tasks.js"></script>
|
|
981
1008
|
<script src="/skills.js"></script>
|
|
982
1009
|
<script src="/channels.js"></script>
|
data/lib/clacky/web/onboard.js
CHANGED
|
@@ -126,6 +126,9 @@ const Onboard = (() => {
|
|
|
126
126
|
_renderProviderOptions();
|
|
127
127
|
// Bind event listeners only once (delegation-based, safe to skip on re-entry)
|
|
128
128
|
_bindCustomDropdown();
|
|
129
|
+
// Show promo hint by default (no provider selected on initial entry)
|
|
130
|
+
const promoHint = $("setup-provider-promo");
|
|
131
|
+
if (promoHint && !promoHint.classList.contains("visible")) _showPromoHint(promoHint);
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
function _renderProviderOptions() {
|
|
@@ -133,11 +136,26 @@ const Onboard = (() => {
|
|
|
133
136
|
// Clear any previously rendered options before re-rendering
|
|
134
137
|
dropdown.innerHTML = "";
|
|
135
138
|
|
|
139
|
+
// Insert placeholder option first
|
|
140
|
+
const placeholder = document.createElement("div");
|
|
141
|
+
placeholder.className = "custom-select-option";
|
|
142
|
+
placeholder.dataset.value = "";
|
|
143
|
+
placeholder.dataset.i18n = "onboard.key.provider.placeholder";
|
|
144
|
+
placeholder.textContent = I18n.t("onboard.key.provider.placeholder");
|
|
145
|
+
dropdown.appendChild(placeholder);
|
|
146
|
+
|
|
136
147
|
_providers.forEach(p => {
|
|
137
148
|
const opt = document.createElement("div");
|
|
138
149
|
opt.className = "custom-select-option";
|
|
139
150
|
opt.dataset.value = p.id;
|
|
140
|
-
opt.
|
|
151
|
+
opt.dataset.label = p.name;
|
|
152
|
+
if (p.id === "openclacky") {
|
|
153
|
+
const nameSpan = document.createElement("span");
|
|
154
|
+
nameSpan.textContent = p.name;
|
|
155
|
+
opt.innerHTML = nameSpan.outerHTML + ` <span class="provider-badge-recommended">${I18n.t("provider.recommended")}</span>`;
|
|
156
|
+
} else {
|
|
157
|
+
opt.textContent = p.name;
|
|
158
|
+
}
|
|
141
159
|
dropdown.appendChild(opt);
|
|
142
160
|
});
|
|
143
161
|
|
|
@@ -225,6 +243,18 @@ const Onboard = (() => {
|
|
|
225
243
|
});
|
|
226
244
|
}
|
|
227
245
|
|
|
246
|
+
function _showPromoHint(promoHint) {
|
|
247
|
+
const items = [
|
|
248
|
+
I18n.t("provider.promo.openclacky.1"),
|
|
249
|
+
I18n.t("provider.promo.openclacky.2"),
|
|
250
|
+
I18n.t("provider.promo.openclacky.3"),
|
|
251
|
+
];
|
|
252
|
+
const title = `<div class="promo-title">${I18n.t("provider.promo.openclacky.title")}</div>`;
|
|
253
|
+
const body = items.map(s => `<div class="promo-item"><span class="promo-icon">✦</span>${s}</div>`).join("");
|
|
254
|
+
promoHint.innerHTML = `<div class="promo-inner">${title}${body}</div>`;
|
|
255
|
+
promoHint.classList.add("visible");
|
|
256
|
+
}
|
|
257
|
+
|
|
228
258
|
function _bindCustomDropdown() {
|
|
229
259
|
if (_dropdownBound) return; // listeners already attached
|
|
230
260
|
_dropdownBound = true;
|
|
@@ -248,7 +278,7 @@ const Onboard = (() => {
|
|
|
248
278
|
if (!opt) return;
|
|
249
279
|
|
|
250
280
|
const value = opt.dataset.value;
|
|
251
|
-
valueSpan.textContent = opt.textContent;
|
|
281
|
+
valueSpan.textContent = opt.dataset.label || opt.textContent;
|
|
252
282
|
valueSpan.classList.toggle("placeholder", !value);
|
|
253
283
|
dropdown.querySelectorAll(".custom-select-option").forEach(o => o.classList.remove("selected"));
|
|
254
284
|
opt.classList.add("selected");
|
|
@@ -256,6 +286,7 @@ const Onboard = (() => {
|
|
|
256
286
|
trigger.classList.remove("open");
|
|
257
287
|
|
|
258
288
|
const getApiKeyLink = $("setup-get-apikey-link");
|
|
289
|
+
const promoHint = $("setup-provider-promo");
|
|
259
290
|
if (value === "__custom__") {
|
|
260
291
|
// Custom: clear presets so the user can fill in their own values
|
|
261
292
|
$("setup-model").value = "";
|
|
@@ -263,6 +294,7 @@ const Onboard = (() => {
|
|
|
263
294
|
_updateSetupModelDropdown([]);
|
|
264
295
|
_updateSetupBaseUrlDropdown(null);
|
|
265
296
|
if (getApiKeyLink) getApiKeyLink.style.display = "none";
|
|
297
|
+
if (promoHint) promoHint.classList.remove("visible");
|
|
266
298
|
} else if (value) {
|
|
267
299
|
const preset = _providers.find(p => p.id === value);
|
|
268
300
|
if (preset) {
|
|
@@ -278,8 +310,18 @@ const Onboard = (() => {
|
|
|
278
310
|
getApiKeyLink.style.display = "none";
|
|
279
311
|
}
|
|
280
312
|
}
|
|
281
|
-
|
|
282
|
-
|
|
313
|
+
// Show promo hint for openclacky, hide for others
|
|
314
|
+
if (promoHint) {
|
|
315
|
+
if (value === "openclacky") {
|
|
316
|
+
_showPromoHint(promoHint);
|
|
317
|
+
} else {
|
|
318
|
+
promoHint.classList.remove("visible");
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
if (getApiKeyLink) getApiKeyLink.style.display = "none";
|
|
323
|
+
// Show promo hint when no provider selected (default state)
|
|
324
|
+
if (promoHint) _showPromoHint(promoHint);
|
|
283
325
|
}
|
|
284
326
|
});
|
|
285
327
|
|