openclacky 1.2.10 → 1.2.12
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 +33 -1
- data/lib/clacky/agent/session_serializer.rb +1 -1
- data/lib/clacky/agent/tool_registry.rb +10 -0
- data/lib/clacky/agent.rb +59 -22
- data/lib/clacky/anthropic_stream_aggregator.rb +17 -1
- data/lib/clacky/bedrock_stream_aggregator.rb +17 -1
- data/lib/clacky/client.rb +25 -3
- data/lib/clacky/default_skills/channel-manager/SKILL.md +47 -42
- data/lib/clacky/default_skills/channel-manager/feishu_setup.rb +134 -0
- data/lib/clacky/default_skills/media-gen/SKILL.md +5 -0
- data/lib/clacky/message_history.rb +57 -0
- data/lib/clacky/openai_stream_aggregator.rb +26 -2
- data/lib/clacky/server/channel/adapters/dingtalk/adapter.rb +10 -1
- data/lib/clacky/server/channel/adapters/discord/adapter.rb +8 -2
- data/lib/clacky/server/channel/adapters/feishu/adapter.rb +10 -1
- data/lib/clacky/server/channel/adapters/feishu/bot.rb +12 -0
- data/lib/clacky/server/channel/adapters/feishu/message_parser.rb +23 -3
- data/lib/clacky/server/channel/adapters/telegram/adapter.rb +12 -2
- data/lib/clacky/server/channel/adapters/wecom/adapter.rb +5 -1
- data/lib/clacky/server/channel/channel_manager.rb +65 -4
- data/lib/clacky/server/channel/group_message_buffer.rb +53 -0
- data/lib/clacky/server/http_server.rb +73 -7
- data/lib/clacky/server/session_registry.rb +4 -6
- data/lib/clacky/tools/trash_manager.rb +1 -1
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +21 -3
- data/lib/clacky/web/apple-touch-icon-180.png +0 -0
- data/lib/clacky/web/brand.js +22 -2
- data/lib/clacky/web/favicon.ico +0 -0
- data/lib/clacky/web/i18n.js +4 -0
- data/lib/clacky/web/index.html +4 -3
- data/lib/clacky/web/logo_nav_dark.png +0 -0
- data/lib/clacky/web/model-tester.js +8 -1
- data/lib/clacky/web/sessions.js +169 -41
- data/lib/clacky/web/theme.js +1 -0
- data/scripts/build/lib/gem.sh +9 -2
- data/scripts/build/src/install_full.sh.cc +2 -0
- data/scripts/build/src/uninstall.sh.cc +1 -1
- data/scripts/install.ps1 +19 -5
- data/scripts/install.sh +9 -2
- data/scripts/install_full.sh +11 -2
- data/scripts/install_rails_deps.sh +9 -2
- data/scripts/uninstall.sh +10 -3
- metadata +7 -2
|
@@ -99,16 +99,15 @@ module Clacky
|
|
|
99
99
|
@sessions.key?(session_id)
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
-
# Restore
|
|
103
|
-
#
|
|
104
|
-
def restore_from_disk(n:
|
|
102
|
+
# Restore at most n sessions per source as a hot cache at startup.
|
|
103
|
+
# Everything else is loaded on demand via ensure(id).
|
|
104
|
+
def restore_from_disk(n: 2)
|
|
105
105
|
return unless @session_manager && @session_restorer
|
|
106
106
|
|
|
107
107
|
all = @session_manager.all_sessions
|
|
108
108
|
.sort_by { |s| s[:created_at] || "" }
|
|
109
109
|
.reverse
|
|
110
110
|
|
|
111
|
-
# Take up to n per source type
|
|
112
111
|
counts = Hash.new(0)
|
|
113
112
|
all.each do |session_data|
|
|
114
113
|
src = (session_data[:source] || "manual").to_s
|
|
@@ -291,6 +290,7 @@ module Clacky
|
|
|
291
290
|
latest_latency: ls&.dig(:latest_latency),
|
|
292
291
|
reasoning_effort: ls&.dig(:reasoning_effort) || s.dig(:config, :reasoning_effort),
|
|
293
292
|
pinned: s[:pinned] || false,
|
|
293
|
+
channel_info: s[:channel_info],
|
|
294
294
|
}
|
|
295
295
|
end
|
|
296
296
|
|
|
@@ -375,8 +375,6 @@ module Clacky
|
|
|
375
375
|
count_by_status(:running) >= max_running_agents
|
|
376
376
|
end
|
|
377
377
|
|
|
378
|
-
# Evict oldest idle agents beyond MAX_IDLE_AGENTS.
|
|
379
|
-
# Persists session data to disk before releasing the agent from memory.
|
|
380
378
|
def evict_excess_idle!
|
|
381
379
|
to_evict = []
|
|
382
380
|
|
data/lib/clacky/version.rb
CHANGED
data/lib/clacky/web/app.css
CHANGED
|
@@ -256,8 +256,7 @@ body {
|
|
|
256
256
|
letter-spacing: 0;
|
|
257
257
|
}
|
|
258
258
|
.header-logo-img {
|
|
259
|
-
height:
|
|
260
|
-
max-width: 6.875rem;
|
|
259
|
+
height: 2.5rem;
|
|
261
260
|
object-fit: contain;
|
|
262
261
|
display: block;
|
|
263
262
|
flex-shrink: 0;
|
|
@@ -2172,7 +2171,26 @@ body {
|
|
|
2172
2171
|
gap: 0.375rem;
|
|
2173
2172
|
}
|
|
2174
2173
|
.tool-item-name { color: var(--color-warning); font-weight: 600; }
|
|
2175
|
-
.tool-item-arg { color: var(--color-text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
2174
|
+
.tool-item-arg { color: var(--color-text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 1 1 auto; }
|
|
2175
|
+
.tool-item-header.tool-item-expandable { cursor: pointer; user-select: none; }
|
|
2176
|
+
.tool-item-header.tool-item-expandable:hover .tool-item-name,
|
|
2177
|
+
.tool-item-header.tool-item-expandable:hover .tool-item-arg { color: var(--color-text-primary); }
|
|
2178
|
+
.tool-item.expanded .tool-item-arg { white-space: normal; overflow: visible; text-overflow: clip; word-break: break-all; }
|
|
2179
|
+
.tool-item-details {
|
|
2180
|
+
margin: 0.25rem 0 2px 0;
|
|
2181
|
+
padding: 0.5rem;
|
|
2182
|
+
background: var(--color-bg-secondary);
|
|
2183
|
+
border: 1px solid var(--color-border-secondary);
|
|
2184
|
+
border-radius: 4px;
|
|
2185
|
+
font-size: 0.6875rem;
|
|
2186
|
+
font-family: monospace;
|
|
2187
|
+
color: var(--color-text-primary);
|
|
2188
|
+
white-space: pre-wrap;
|
|
2189
|
+
word-break: break-all;
|
|
2190
|
+
line-height: 1.5;
|
|
2191
|
+
max-height: 18rem;
|
|
2192
|
+
overflow-y: auto;
|
|
2193
|
+
}
|
|
2176
2194
|
.tool-item-status { margin-left: auto; font-size: 0.6875rem; flex-shrink: 0; }
|
|
2177
2195
|
.tool-item-status.ok { color: var(--color-success); }
|
|
2178
2196
|
.tool-item-status.err { color: var(--color-error); }
|
|
Binary file
|
data/lib/clacky/web/brand.js
CHANGED
|
@@ -329,13 +329,16 @@ const Brand = (() => {
|
|
|
329
329
|
};
|
|
330
330
|
img.src = info.logo_url;
|
|
331
331
|
}
|
|
332
|
-
} else {
|
|
333
|
-
//
|
|
332
|
+
} else if (info.product_name) {
|
|
333
|
+
// Brand configured but no logo — hide the image, brand name text is enough
|
|
334
334
|
if (logoImg) {
|
|
335
335
|
logoImg.style.display = "none";
|
|
336
336
|
logoImg.src = "";
|
|
337
337
|
}
|
|
338
338
|
if (brandWrap) brandWrap.classList.remove("has-logo");
|
|
339
|
+
} else {
|
|
340
|
+
// No brand at all — show default OpenClacky logo
|
|
341
|
+
_applyDefaultLogo();
|
|
339
342
|
}
|
|
340
343
|
|
|
341
344
|
// Always show brand name text; hide it only when no brand name is set
|
|
@@ -355,6 +358,18 @@ const Brand = (() => {
|
|
|
355
358
|
});
|
|
356
359
|
}
|
|
357
360
|
|
|
361
|
+
// Apply the default OpenClacky logo based on current theme.
|
|
362
|
+
function _applyDefaultLogo() {
|
|
363
|
+
const logoImg = document.getElementById("header-logo-img");
|
|
364
|
+
const brandWrap = document.getElementById("header-brand");
|
|
365
|
+
if (!logoImg) return;
|
|
366
|
+
|
|
367
|
+
logoImg.src = "/logo_nav_dark.png";
|
|
368
|
+
logoImg.alt = "OpenClacky";
|
|
369
|
+
logoImg.style.display = "";
|
|
370
|
+
if (brandWrap) brandWrap.classList.add("has-logo");
|
|
371
|
+
}
|
|
372
|
+
|
|
358
373
|
// Replace the browser tab favicon with the given URL.
|
|
359
374
|
// Works for both image URLs and SVG data URIs.
|
|
360
375
|
function _applyFavicon(url) {
|
|
@@ -510,5 +525,10 @@ const Brand = (() => {
|
|
|
510
525
|
}
|
|
511
526
|
}
|
|
512
527
|
|
|
528
|
+
// Both themes use the same transparent A版 logo via CSS, no swap needed.
|
|
529
|
+
window.addEventListener("clacky-theme-change", e => {
|
|
530
|
+
// No-op
|
|
531
|
+
});
|
|
532
|
+
|
|
513
533
|
return { check, refresh, applyBrandName: _applyBrandName, applyHeaderLogo: _applyHeaderLogo, applyOwnerBadge: _applyOwnerBadge, applyGetSerialLink: _applyGetSerialLink, clearBrandCache: _clearBrandCache, goToLicenseInput: _goToLicenseInput, get userLicensed() { return _userLicensed; }, get branded() { return _branded; } };
|
|
514
534
|
})();
|
|
Binary file
|
data/lib/clacky/web/i18n.js
CHANGED
|
@@ -99,6 +99,8 @@ const I18n = (() => {
|
|
|
99
99
|
"workspace.downloadFailed": "Download failed",
|
|
100
100
|
"sib.model.tooltip": "Click to switch model",
|
|
101
101
|
"sib.model.tooltip.busy": "Model switching is disabled while the agent is responding",
|
|
102
|
+
"sib.variant.header": "Quick switch",
|
|
103
|
+
"sib.variant.default": "default",
|
|
102
104
|
"sib.signal.tooltip": "Recent LLM latency",
|
|
103
105
|
"sib.reasoning.tooltip": "Click to change reasoning effort",
|
|
104
106
|
"sib.reasoning.label": "Reasoning",
|
|
@@ -831,6 +833,8 @@ const I18n = (() => {
|
|
|
831
833
|
"workspace.downloadFailed": "下载失败",
|
|
832
834
|
"sib.model.tooltip": "点击切换模型",
|
|
833
835
|
"sib.model.tooltip.busy": "Agent 回复中,暂时无法切换模型",
|
|
836
|
+
"sib.variant.header": "快速切换",
|
|
837
|
+
"sib.variant.default": "默认",
|
|
834
838
|
"sib.signal.tooltip": "最近一次 LLM 响应延迟",
|
|
835
839
|
"sib.reasoning.tooltip": "点击调整思考等级",
|
|
836
840
|
"sib.reasoning.label": "思考",
|
data/lib/clacky/web/index.html
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title id="page-title">{{BRAND_NAME}}</title>
|
|
7
|
-
<link rel="icon"
|
|
7
|
+
<link rel="icon" href="/favicon.ico">
|
|
8
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon-180.png">
|
|
8
9
|
<link rel="stylesheet" href="/vendor/katex/katex.min.css">
|
|
9
10
|
<link rel="stylesheet" href="/vendor/hljs/hljs-theme.css">
|
|
10
11
|
<link rel="stylesheet" href="/app.css">
|
|
@@ -30,8 +31,8 @@
|
|
|
30
31
|
<path d="M9 3v18"/>
|
|
31
32
|
</svg>
|
|
32
33
|
</button>
|
|
33
|
-
<div id="header-brand" style="cursor:pointer" onclick="Router.navigate('chat')">
|
|
34
|
-
<img class="header-logo-img" id="header-logo-img" src="" alt=""
|
|
34
|
+
<div id="header-brand" class="has-logo" style="cursor:pointer" onclick="Router.navigate('chat')">
|
|
35
|
+
<img class="header-logo-img" id="header-logo-img" src="/logo_nav_dark.png" alt="OpenClacky">
|
|
35
36
|
<span class="header-logo-divider"></span>
|
|
36
37
|
<span class="header-logo" id="header-logo">{{BRAND_NAME}}</span>
|
|
37
38
|
<!-- Owner badge: visible only for creator-tier licenses (user_licensed=true).
|
|
Binary file
|
|
@@ -23,7 +23,14 @@ window.ModelTester = (function () {
|
|
|
23
23
|
return { ok: false, message: e.message };
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
if (!data.ok)
|
|
26
|
+
if (!data.ok) {
|
|
27
|
+
const msg = data.message || "";
|
|
28
|
+
const code = data.error_code || "";
|
|
29
|
+
if (code === "insufficient_credit") {
|
|
30
|
+
return { ok: false, message: I18n.t("error.insufficient_credit"), error_code: code };
|
|
31
|
+
}
|
|
32
|
+
return { ok: false, message: msg, error_code: code };
|
|
33
|
+
}
|
|
27
34
|
|
|
28
35
|
if (data.effective_base_url && data.effective_base_url !== base_url) {
|
|
29
36
|
return { ok: true, base_url: data.effective_base_url, message: data.message || "", rewrote: true };
|
data/lib/clacky/web/sessions.js
CHANGED
|
@@ -27,6 +27,16 @@ const Sessions = (() => {
|
|
|
27
27
|
let _searchOpen = false; // is the search panel visible?
|
|
28
28
|
let _cronView = false; // are we in the cron sub-view?
|
|
29
29
|
let _cronCount = 0; // total cron sessions from server
|
|
30
|
+
// ── Cron sub-view independent pagination (commit 2) ──────────────────────
|
|
31
|
+
// The folded cron sub-view paginates *independently* of the outer list so
|
|
32
|
+
// that "Load more" inside it never advances the outer list's cursor, and so
|
|
33
|
+
// all cron sessions can be loaded even when they're sparse across the mixed
|
|
34
|
+
// outer pages. Cron rows fetched here are pushed into the shared `_sessions`
|
|
35
|
+
// array (with dedup), so WS updates / patch / remove keep working unchanged;
|
|
36
|
+
// we only track a separate cursor + hasMore/loading flags for the sub-view.
|
|
37
|
+
let _cronBefore = null; // cursor: oldest cron created_at loaded into the sub-view
|
|
38
|
+
let _cronHasMore = false; // are there older cron sessions to load?
|
|
39
|
+
let _cronLoadingMore = false;
|
|
30
40
|
let _pendingRunTaskId = null; // session_id waiting to send "run_task" after subscribe
|
|
31
41
|
let _pendingMessage = null; // { session_id, content } — slash command to send after subscribe
|
|
32
42
|
// Buffer for tool_stdout lines that arrive before history has finished rendering.
|
|
@@ -933,25 +943,57 @@ const Sessions = (() => {
|
|
|
933
943
|
const item = document.createElement("div");
|
|
934
944
|
item.className = "tool-item";
|
|
935
945
|
|
|
936
|
-
|
|
946
|
+
const argsJson = _formatToolArgs(args);
|
|
947
|
+
if (argsJson) item.dataset.argsJson = argsJson;
|
|
948
|
+
if (name) item.dataset.toolName = String(name);
|
|
949
|
+
|
|
937
950
|
const argSummary = summary || _summariseArgs(name, args);
|
|
938
951
|
|
|
939
|
-
// When a structured summary is available, show it as the primary label (no redundant tool name).
|
|
940
|
-
// Otherwise show the raw tool name + arg summary as before.
|
|
941
952
|
const label = summary
|
|
942
953
|
? `<span class="tool-item-name">⚙ ${escapeHtml(summary)}</span>`
|
|
943
954
|
: `<span class="tool-item-name">⚙ ${escapeHtml(name)}</span>` +
|
|
944
955
|
(argSummary ? `<span class="tool-item-arg">${escapeHtml(argSummary)}</span>` : "");
|
|
945
956
|
|
|
957
|
+
const expandable = !!argsJson;
|
|
958
|
+
const headerCls = expandable ? "tool-item-header tool-item-expandable" : "tool-item-header";
|
|
959
|
+
|
|
946
960
|
item.innerHTML =
|
|
947
|
-
`<div class="
|
|
961
|
+
`<div class="${headerCls}">` +
|
|
948
962
|
label +
|
|
949
963
|
`<span class="tool-item-status running">…</span>` +
|
|
950
964
|
`</div>` +
|
|
965
|
+
`<div class="tool-item-details" style="display:none"></div>` +
|
|
951
966
|
`<pre class="tool-item-stdout" style="display:none"></pre>`;
|
|
967
|
+
_ensureCopyDelegation();
|
|
952
968
|
return item;
|
|
953
969
|
}
|
|
954
970
|
|
|
971
|
+
function _toggleToolItemDetails(item) {
|
|
972
|
+
if (!item) return;
|
|
973
|
+
const details = item.querySelector(".tool-item-details");
|
|
974
|
+
if (!details) return;
|
|
975
|
+
const isHidden = details.style.display === "none";
|
|
976
|
+
if (isHidden) {
|
|
977
|
+
if (!details.dataset.filled) {
|
|
978
|
+
const json = item.dataset.argsJson || "";
|
|
979
|
+
details.textContent = json;
|
|
980
|
+
details.dataset.filled = "1";
|
|
981
|
+
}
|
|
982
|
+
details.style.display = "";
|
|
983
|
+
item.classList.add("expanded");
|
|
984
|
+
} else {
|
|
985
|
+
details.style.display = "none";
|
|
986
|
+
item.classList.remove("expanded");
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Pretty-print tool args as a JSON string, or empty string if unavailable.
|
|
991
|
+
function _formatToolArgs(args) {
|
|
992
|
+
if (args == null) return "";
|
|
993
|
+
if (typeof args === "string") return args;
|
|
994
|
+
try { return JSON.stringify(args, null, 2); } catch (_) { return ""; }
|
|
995
|
+
}
|
|
996
|
+
|
|
955
997
|
// Convert ANSI escape codes to HTML spans with color classes.
|
|
956
998
|
// Handles the common SGR codes used by shell scripts (colors + reset).
|
|
957
999
|
function _ansiToHtml(text) {
|
|
@@ -1503,6 +1545,14 @@ const Sessions = (() => {
|
|
|
1503
1545
|
const messages = $("messages");
|
|
1504
1546
|
if (!messages) return;
|
|
1505
1547
|
messages.addEventListener("click", (e) => {
|
|
1548
|
+
// ── Tool item: click header to expand/collapse args details ──
|
|
1549
|
+
const toolHeader = e.target.closest(".tool-item-header.tool-item-expandable");
|
|
1550
|
+
if (toolHeader) {
|
|
1551
|
+
e.preventDefault();
|
|
1552
|
+
e.stopPropagation();
|
|
1553
|
+
_toggleToolItemDetails(toolHeader.closest(".tool-item"));
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1506
1556
|
// ── Code block copy button ──
|
|
1507
1557
|
const codeBtn = e.target.closest(".code-block-copy");
|
|
1508
1558
|
if (codeBtn) {
|
|
@@ -1584,12 +1634,13 @@ const Sessions = (() => {
|
|
|
1584
1634
|
}
|
|
1585
1635
|
|
|
1586
1636
|
// Build the unified load-more button.
|
|
1587
|
-
function _makeLoadMoreBtn() {
|
|
1637
|
+
function _makeLoadMoreBtn(cron = false) {
|
|
1588
1638
|
const btn = document.createElement("button");
|
|
1589
1639
|
btn.className = "btn-load-more-sessions";
|
|
1590
|
-
|
|
1591
|
-
btn.
|
|
1592
|
-
btn.
|
|
1640
|
+
const loading = cron ? _cronLoadingMore : _loadingMore;
|
|
1641
|
+
btn.disabled = loading;
|
|
1642
|
+
btn.textContent = loading ? I18n.t("sessions.loadingMore") : I18n.t("sessions.loadMore");
|
|
1643
|
+
btn.onclick = () => cron ? Sessions.loadMoreCron() : Sessions.loadMore();
|
|
1593
1644
|
return btn;
|
|
1594
1645
|
}
|
|
1595
1646
|
|
|
@@ -1679,13 +1730,17 @@ const Sessions = (() => {
|
|
|
1679
1730
|
}
|
|
1680
1731
|
|
|
1681
1732
|
// ── Cron group entry (renders the folded "Scheduled Tasks" entry) ─────
|
|
1682
|
-
|
|
1733
|
+
// `hasRunning` mirrors a normal session's status dot: show the green
|
|
1734
|
+
// (running) dot only when any folded cron session is currently running,
|
|
1735
|
+
// and render no dot at all when the group is idle.
|
|
1736
|
+
function _renderCronGroupItem(container, count, hasRunning = false) {
|
|
1683
1737
|
const el = document.createElement("div");
|
|
1684
1738
|
el.className = "session-item cron-group-item";
|
|
1739
|
+
const dotHtml = hasRunning ? `<span class="session-dot dot-running"></span>` : "";
|
|
1685
1740
|
el.innerHTML = `
|
|
1686
1741
|
<div class="session-body">
|
|
1687
1742
|
<div class="session-name">
|
|
1688
|
-
|
|
1743
|
+
${dotHtml}
|
|
1689
1744
|
<span class="session-name__text">📋 ${I18n.t("sessions.cronGroup")} (${count})</span>
|
|
1690
1745
|
</div>
|
|
1691
1746
|
<div class="session-meta">${I18n.t("sessions.cronGroupMeta", { n: count })}</div>
|
|
@@ -1694,6 +1749,10 @@ const Sessions = (() => {
|
|
|
1694
1749
|
el.onclick = () => {
|
|
1695
1750
|
_cronView = true;
|
|
1696
1751
|
Sessions.renderList();
|
|
1752
|
+
// Method A: always (re-)load the freshest first page of cron sessions
|
|
1753
|
+
// on entering the sub-view. Independent cursor — never touches the
|
|
1754
|
+
// outer list's pagination.
|
|
1755
|
+
Sessions.loadMoreCron({ reset: true });
|
|
1697
1756
|
};
|
|
1698
1757
|
container.appendChild(el);
|
|
1699
1758
|
}
|
|
@@ -1881,6 +1940,53 @@ const Sessions = (() => {
|
|
|
1881
1940
|
}
|
|
1882
1941
|
},
|
|
1883
1942
|
|
|
1943
|
+
/** Cron sub-view pagination — independent cursor, does NOT touch the outer
|
|
1944
|
+
* list's `_hasMore` / `loadMore` cursor. Fetches the next page of cron
|
|
1945
|
+
* sessions (type=cron) and pushes them into the shared `_sessions` array
|
|
1946
|
+
* (deduped), so WS patch/remove/add keep working unchanged. Only the
|
|
1947
|
+
* sub-view's own cursor + flags advance. Pass `reset:true` to start over
|
|
1948
|
+
* from the newest cron page (used on entering the sub-view). */
|
|
1949
|
+
async loadMoreCron({ reset = false } = {}) {
|
|
1950
|
+
if (_cronLoadingMore) return;
|
|
1951
|
+
if (!reset && !_cronHasMore) return;
|
|
1952
|
+
_cronLoadingMore = true;
|
|
1953
|
+
if (reset) { _cronBefore = null; _cronHasMore = false; }
|
|
1954
|
+
|
|
1955
|
+
const sidebarList = document.getElementById("sidebar-list");
|
|
1956
|
+
const savedScrollTop = sidebarList ? sidebarList.scrollTop : 0;
|
|
1957
|
+
Sessions.renderList({ skipScrollToActive: true });
|
|
1958
|
+
|
|
1959
|
+
try {
|
|
1960
|
+
const params = new URLSearchParams({ limit: "20", type: "cron" });
|
|
1961
|
+
if (_cronBefore) params.set("before", _cronBefore);
|
|
1962
|
+
|
|
1963
|
+
const res = await fetch(`/api/sessions?${params}`);
|
|
1964
|
+
if (!res.ok) return;
|
|
1965
|
+
const data = await res.json();
|
|
1966
|
+
|
|
1967
|
+
const rows = data.sessions || [];
|
|
1968
|
+
rows.forEach(s => {
|
|
1969
|
+
if (!_sessions.find(x => x.id === s.id)) _sessions.push(s);
|
|
1970
|
+
});
|
|
1971
|
+
// Advance cursor to the oldest cron created_at in THIS batch (exclude
|
|
1972
|
+
// pinned — backend returns all pinned on the first page, bypassing
|
|
1973
|
+
// pagination, so their created_at must not drag the cursor back).
|
|
1974
|
+
const oldest = rows.reduce((min, s) => {
|
|
1975
|
+
if (s.pinned || !s.created_at) return min;
|
|
1976
|
+
return (!min || s.created_at < min) ? s.created_at : min;
|
|
1977
|
+
}, null);
|
|
1978
|
+
if (oldest) _cronBefore = oldest;
|
|
1979
|
+
_cronHasMore = !!data.has_more;
|
|
1980
|
+
if (data.cron_count != null) _cronCount = data.cron_count;
|
|
1981
|
+
} catch (e) {
|
|
1982
|
+
console.error("loadMoreCron error:", e);
|
|
1983
|
+
} finally {
|
|
1984
|
+
_cronLoadingMore = false;
|
|
1985
|
+
Sessions.renderList({ skipScrollToActive: true });
|
|
1986
|
+
if (sidebarList) sidebarList.scrollTop = savedScrollTop;
|
|
1987
|
+
}
|
|
1988
|
+
},
|
|
1989
|
+
|
|
1884
1990
|
/** Commit current filter values and re-fetch from server. Called by Enter / Go button. */
|
|
1885
1991
|
async commitSearch() {
|
|
1886
1992
|
// Read live input values into _filter
|
|
@@ -2097,7 +2203,6 @@ const Sessions = (() => {
|
|
|
2097
2203
|
const hasActiveFilter = !!(_filter.q || _filter.type || _filter.date);
|
|
2098
2204
|
const isCronView = _cronView && !hasActiveFilter;
|
|
2099
2205
|
const cronSessions = visible.filter(s => s.source === "cron");
|
|
2100
|
-
const nonCronSessions = visible.filter(s => s.source !== "cron");
|
|
2101
2206
|
|
|
2102
2207
|
// Update chat-section header based on view mode
|
|
2103
2208
|
_updateChatHeader(isCronView);
|
|
@@ -2109,27 +2214,46 @@ const Sessions = (() => {
|
|
|
2109
2214
|
// Filter active: show all matching results flat, no group entry
|
|
2110
2215
|
visible.forEach(s => _renderSessionItem(list, s));
|
|
2111
2216
|
} else if (isCronView) {
|
|
2112
|
-
// Cron sub-view: show only cron sessions
|
|
2113
|
-
//
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
if (_loadingMore) {
|
|
2121
|
-
// A loadMore() call is already in flight (its own renderList call
|
|
2122
|
-
// reached us). Keep the loading indicator so the user never sees
|
|
2123
|
-
// the "no sessions" empty state during the gap.
|
|
2124
|
-
list.innerHTML = `<div class="session-empty">${I18n.t("sessions.cronLoading")}</div>`;
|
|
2125
|
-
return;
|
|
2126
|
-
}
|
|
2217
|
+
// Cron sub-view: show only cron sessions, paginated independently via
|
|
2218
|
+
// loadMoreCron() (the first page is fetched on entering the view).
|
|
2219
|
+
// We never call the outer loadMore() here, so the outer list's cursor
|
|
2220
|
+
// is left untouched. While the first page is in flight and nothing is
|
|
2221
|
+
// loaded yet, show a loading placeholder instead of the empty state.
|
|
2222
|
+
if (cronSessions.length === 0 && _cronLoadingMore) {
|
|
2223
|
+
list.innerHTML = `<div class="session-empty">${I18n.t("sessions.cronLoading")}</div>`;
|
|
2224
|
+
return;
|
|
2127
2225
|
}
|
|
2128
2226
|
cronSessions.forEach(s => _renderSessionItem(list, s));
|
|
2129
2227
|
} else if (_cronCount > 0) {
|
|
2130
|
-
// Normal list view: group entry
|
|
2131
|
-
|
|
2132
|
-
|
|
2228
|
+
// Normal list view: the cron group entry is a *virtual* row that
|
|
2229
|
+
// participates in the time-ordering instead of being pinned to the
|
|
2230
|
+
// top. We walk the already-sorted `visible` list and drop the group
|
|
2231
|
+
// entry at the position of the newest (first-encountered) cron
|
|
2232
|
+
// session — so it sits exactly where the latest folded cron task
|
|
2233
|
+
// would sort by created_at. Pinning is intentionally ignored for the
|
|
2234
|
+
// entry itself: a pinned cron session only takes effect *inside* the
|
|
2235
|
+
// folded sub-view, not on the outer entry.
|
|
2236
|
+
const cronHasRunning = cronSessions.some(s => s.status === "running");
|
|
2237
|
+
let cronEntryRendered = false;
|
|
2238
|
+
visible.forEach(s => {
|
|
2239
|
+
if (s.source === "cron") {
|
|
2240
|
+
// Render the group entry once, at the first (newest) cron slot;
|
|
2241
|
+
// skip every individual cron session in the flat list.
|
|
2242
|
+
if (!cronEntryRendered) {
|
|
2243
|
+
_renderCronGroupItem(list, _cronCount, cronHasRunning);
|
|
2244
|
+
cronEntryRendered = true;
|
|
2245
|
+
}
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
_renderSessionItem(list, s);
|
|
2249
|
+
});
|
|
2250
|
+
// NOTE: we intentionally do NOT append a fallback entry when no cron
|
|
2251
|
+
// session is loaded yet. The group entry is a virtual row that must
|
|
2252
|
+
// ride along with pagination: it only appears at the sort slot of the
|
|
2253
|
+
// first loaded cron session. If the newest cron lives on a later page,
|
|
2254
|
+
// the entry simply shows up once that page is loaded — rather than
|
|
2255
|
+
// being forced onto the bottom of page 1 (which would mislead the
|
|
2256
|
+
// sort position and miss the running state of an unpaged cron).
|
|
2133
2257
|
} else {
|
|
2134
2258
|
// Normal list view, no cron sessions
|
|
2135
2259
|
visible.forEach(s => _renderSessionItem(list, s));
|
|
@@ -2140,7 +2264,11 @@ const Sessions = (() => {
|
|
|
2140
2264
|
list.innerHTML = `<div class="session-empty">${I18n.t("sessions.empty")}</div>`;
|
|
2141
2265
|
}
|
|
2142
2266
|
|
|
2143
|
-
if (
|
|
2267
|
+
if (isCronView) {
|
|
2268
|
+
if (_cronHasMore) list.appendChild(_makeLoadMoreBtn(true));
|
|
2269
|
+
} else if (_hasMore) {
|
|
2270
|
+
list.appendChild(_makeLoadMoreBtn());
|
|
2271
|
+
}
|
|
2144
2272
|
|
|
2145
2273
|
// Scroll active session into view so the sidebar always shows the current session.
|
|
2146
2274
|
if (!skipScrollToActive) {
|
|
@@ -3452,16 +3580,16 @@ const Sessions = (() => {
|
|
|
3452
3580
|
|
|
3453
3581
|
const nameLine = document.createElement("span");
|
|
3454
3582
|
nameLine.className = "sib-model-name-main";
|
|
3455
|
-
|
|
3583
|
+
// When a non-default quick-switch model is active for the current row,
|
|
3584
|
+
// show only that model's name (avoid the long "main → quick-switch"
|
|
3585
|
+
// string that gets truncated).
|
|
3586
|
+
const hasActiveOverride =
|
|
3587
|
+
m.id === currentModelId &&
|
|
3588
|
+
subInfo.current &&
|
|
3589
|
+
subInfo.current !== subInfo.cardModel;
|
|
3590
|
+
nameLine.textContent = hasActiveOverride ? subInfo.current : m.model;
|
|
3456
3591
|
left.appendChild(nameLine);
|
|
3457
3592
|
|
|
3458
|
-
if (m.id === currentModelId && subInfo.current && subInfo.current !== subInfo.cardModel) {
|
|
3459
|
-
const overrideLine = document.createElement("span");
|
|
3460
|
-
overrideLine.className = "sib-model-name-override";
|
|
3461
|
-
overrideLine.textContent = `→ ${subInfo.current}`;
|
|
3462
|
-
left.appendChild(overrideLine);
|
|
3463
|
-
}
|
|
3464
|
-
|
|
3465
3593
|
if (_nameCounts[m.model] > 1) {
|
|
3466
3594
|
left.classList.add("has-sub");
|
|
3467
3595
|
const host = (() => {
|
|
@@ -3503,7 +3631,7 @@ const Sessions = (() => {
|
|
|
3503
3631
|
const toggleBtn = document.createElement("button");
|
|
3504
3632
|
toggleBtn.type = "button";
|
|
3505
3633
|
toggleBtn.className = "sib-submodel-toggle";
|
|
3506
|
-
toggleBtn.title = "
|
|
3634
|
+
toggleBtn.title = I18n.t("sib.variant.header");
|
|
3507
3635
|
toggleBtn.setAttribute("aria-expanded", "false");
|
|
3508
3636
|
toggleBtn.innerHTML =
|
|
3509
3637
|
'<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true">' +
|
|
@@ -3766,7 +3894,7 @@ const Sessions = (() => {
|
|
|
3766
3894
|
|
|
3767
3895
|
const header = document.createElement("div");
|
|
3768
3896
|
header.className = "sib-submodel-panel-header";
|
|
3769
|
-
header.textContent = "
|
|
3897
|
+
header.textContent = I18n.t("sib.variant.header");
|
|
3770
3898
|
panel.appendChild(header);
|
|
3771
3899
|
|
|
3772
3900
|
const cardDefault = subInfo.cardModel;
|
|
@@ -3788,7 +3916,7 @@ const Sessions = (() => {
|
|
|
3788
3916
|
if (name === cardDefault) {
|
|
3789
3917
|
const tag = document.createElement("span");
|
|
3790
3918
|
tag.className = "sib-submodel-default-tag";
|
|
3791
|
-
tag.textContent = "default";
|
|
3919
|
+
tag.textContent = I18n.t("sib.variant.default");
|
|
3792
3920
|
row.appendChild(tag);
|
|
3793
3921
|
}
|
|
3794
3922
|
|
data/lib/clacky/web/theme.js
CHANGED
|
@@ -21,6 +21,7 @@ const Theme = (() => {
|
|
|
21
21
|
function _applyAttr(theme) {
|
|
22
22
|
document.documentElement.setAttribute(ATTR_NAME, theme);
|
|
23
23
|
_updateToggleIcon(theme);
|
|
24
|
+
window.dispatchEvent(new CustomEvent("clacky-theme-change", { detail: { theme } }));
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
function _updateToggleIcon(theme) {
|
data/scripts/build/lib/gem.sh
CHANGED
|
@@ -59,7 +59,12 @@ restore_gemrc() {
|
|
|
59
59
|
setup_gem_home() {
|
|
60
60
|
local gem_dir
|
|
61
61
|
gem_dir=$(gem environment gemdir 2>/dev/null || true)
|
|
62
|
-
|
|
62
|
+
|
|
63
|
+
# gemdir writable → no workaround needed, clean up old RC entries
|
|
64
|
+
if [ -w "$gem_dir" ]; then
|
|
65
|
+
restore_gem_home
|
|
66
|
+
return 0
|
|
67
|
+
fi
|
|
63
68
|
|
|
64
69
|
local ruby_api
|
|
65
70
|
ruby_api=$(ruby -e 'puts RbConfig::CONFIG["ruby_version"]' 2>/dev/null)
|
|
@@ -68,7 +73,9 @@ setup_gem_home() {
|
|
|
68
73
|
export PATH="$HOME/.gem/ruby/${ruby_api}/bin:$PATH"
|
|
69
74
|
print_info "System Ruby detected — gems will install to ~/.gem/ruby/${ruby_api}"
|
|
70
75
|
|
|
71
|
-
|
|
76
|
+
# Clean old RC entries (could be from a different Ruby version), then write new ones
|
|
77
|
+
restore_gem_home
|
|
78
|
+
if [ -n "$SHELL_RC" ]; then
|
|
72
79
|
{
|
|
73
80
|
echo ""
|
|
74
81
|
echo "# Ruby user gem dir (added by openclacky installer)"
|
|
@@ -153,6 +153,8 @@ install_via_gem() {
|
|
|
153
153
|
local ver; ver=$(ruby -e 'puts RUBY_VERSION' 2>/dev/null)
|
|
154
154
|
version_ge "$ver" "3.1.0" || { print_error "Ruby $ver too old (>= 3.1.0 required)"; return 1; }
|
|
155
155
|
|
|
156
|
+
setup_gem_home
|
|
157
|
+
|
|
156
158
|
print_info "Installing ${DISPLAY_NAME}..."
|
|
157
159
|
if gem install openclacky --no-document; then
|
|
158
160
|
print_success "${DISPLAY_NAME} installed!"
|
|
@@ -21,7 +21,7 @@ load_brand() {
|
|
|
21
21
|
[ -f "$brand_file" ] || return 0
|
|
22
22
|
BRAND_NAME=$(awk -F': ' '/^product_name:/{gsub(/^"|"$/, "", $2); gsub(/^ +| +$/, "", $2); print $2}' "$brand_file") || true
|
|
23
23
|
BRAND_COMMAND=$(awk -F': ' '/^package_name:/{gsub(/^"|"$/, "", $2); gsub(/^ +| +$/, "", $2); print $2}' "$brand_file") || true
|
|
24
|
-
[ -n "$BRAND_NAME" ]
|
|
24
|
+
if [ -n "$BRAND_NAME" ]; then DISPLAY_NAME="$BRAND_NAME"; fi
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
check_installation() {
|
data/scripts/install.ps1
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
# Parameters:
|
|
11
11
|
# -BrandName Display name shown in prompts (default: OpenClacky)
|
|
12
12
|
# -CommandName CLI command name after install (default: openclacky)
|
|
13
|
+
# -Region CDN region: china (default) or global (use global for non-China networks)
|
|
13
14
|
#
|
|
14
15
|
# WSL1 is preferred (shares Windows network stack — no mirrored networking needed).
|
|
15
16
|
# If WSL1 import fails, the script falls back to WSL2 with mirrored networking.
|
|
@@ -22,7 +23,9 @@
|
|
|
22
23
|
param(
|
|
23
24
|
[switch]$Local,
|
|
24
25
|
[string]$BrandName = "",
|
|
25
|
-
[string]$CommandName = ""
|
|
26
|
+
[string]$CommandName = "",
|
|
27
|
+
[ValidateSet("china", "global")]
|
|
28
|
+
[string]$Region = "china"
|
|
26
29
|
)
|
|
27
30
|
|
|
28
31
|
Set-StrictMode -Version Latest
|
|
@@ -32,11 +35,19 @@ $env:WSL_UTF8 = "1"
|
|
|
32
35
|
$global:DisplayName = if ($BrandName) { $BrandName } else { "OpenClacky" }
|
|
33
36
|
$global:DisplayCmd = if ($CommandName) { $CommandName } else { "openclacky" }
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
Write-Host "==> Region: $Region"
|
|
39
|
+
|
|
40
|
+
$CLACKY_CDN_BASE_URL = if ($Region -eq "global") { "https://sg.oss.1024code.com" } else { "https://oss.1024code.com" }
|
|
41
|
+
$CLACKY_INSTALL_SCRIPT_BASE_URL = if ($Region -eq "global") { "https://raw.githubusercontent.com" } else { "https://oss.1024code.com" }
|
|
42
|
+
$CLACKY_CDN_PRIMARY_HOST = if ($Region -eq "global") { "sg.oss.1024code.com" } else { "oss.1024code.com" }
|
|
43
|
+
|
|
44
|
+
Write-Host "==> CDN_BASE_URL: $CLACKY_CDN_BASE_URL"
|
|
45
|
+
Write-Host "==> INSTALL_SCRIPT_BASE_URL: $CLACKY_INSTALL_SCRIPT_BASE_URL"
|
|
46
|
+
Write-Host "==> CDN_PRIMARY_HOST: $CLACKY_CDN_PRIMARY_HOST"
|
|
47
|
+
|
|
37
48
|
$CLACKY_CDN_BACKUP_HOST = "clackyai-1258723534.cos.ap-guangzhou.myqcloud.com"
|
|
38
|
-
$INSTALL_PS1_COMMAND = "powershell -c `"irm $
|
|
39
|
-
$INSTALL_SCRIPT_URL = "$
|
|
49
|
+
$INSTALL_PS1_COMMAND = "powershell -c `"irm $CLACKY_INSTALL_SCRIPT_BASE_URL/clacky-ai/openclacky/main/scripts/install.ps1 | iex`""
|
|
50
|
+
$INSTALL_SCRIPT_URL = "$CLACKY_INSTALL_SCRIPT_BASE_URL/clacky-ai/openclacky/main/scripts/install.sh"
|
|
40
51
|
$UBUNTU_WSL_AMD64_URL = "$CLACKY_CDN_BASE_URL/ubuntu-jammy-wsl-amd64-ubuntu22.04lts.rootfs.tar.gz"
|
|
41
52
|
$UBUNTU_WSL_AMD64_SHA256_URL = "$CLACKY_CDN_BASE_URL/ubuntu-jammy-wsl-amd64-ubuntu22.04lts.rootfs.tar.gz.sha256"
|
|
42
53
|
$UBUNTU_WSL_ARM64_URL = "$CLACKY_CDN_BASE_URL/ubuntu-jammy-wsl-arm64-ubuntu22.04lts.rootfs.tar.gz"
|
|
@@ -303,6 +314,9 @@ function Run-InstallInWsl {
|
|
|
303
314
|
wsl.exe -d Ubuntu -u root -- bash -c "cd ~ && curl -fsSL $INSTALL_SCRIPT_URL | bash -s -- --brand-name=$BrandName --command=$CommandName"
|
|
304
315
|
}
|
|
305
316
|
|
|
317
|
+
if ($LASTEXITCODE -eq 2) {
|
|
318
|
+
exit 2
|
|
319
|
+
}
|
|
306
320
|
if ($LASTEXITCODE -ne 0) {
|
|
307
321
|
Write-Fail "Installation failed inside WSL (exit $LASTEXITCODE)."
|
|
308
322
|
Write-Fail "You can retry manually:"
|