openclacky 0.9.7 → 0.9.9
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 +16 -1
- data/CHANGELOG.md +51 -0
- data/docs/browser-cdp-native-design.md +195 -0
- data/docs/session-management-redesign.md +202 -0
- data/docs/system-skill-authoring-guide.md +47 -0
- data/lib/clacky/agent/cost_tracker.rb +2 -1
- data/lib/clacky/agent/message_compressor_helper.rb +2 -2
- data/lib/clacky/agent/session_serializer.rb +36 -3
- data/lib/clacky/agent/skill_manager.rb +68 -5
- data/lib/clacky/agent/system_prompt_builder.rb +30 -13
- data/lib/clacky/agent/tool_executor.rb +8 -12
- data/lib/clacky/agent.rb +172 -30
- data/lib/clacky/brand_config.rb +90 -0
- data/lib/clacky/cli.rb +37 -8
- data/lib/clacky/default_parsers/doc_parser.rb +69 -0
- data/lib/clacky/default_parsers/docx_parser.rb +172 -0
- data/lib/clacky/default_parsers/pdf_parser.rb +79 -0
- data/lib/clacky/default_parsers/pptx_parser.rb +140 -0
- data/lib/clacky/default_parsers/xlsx_parser.rb +121 -0
- data/lib/clacky/default_skills/browser-setup/SKILL.md +158 -0
- data/lib/clacky/default_skills/channel-setup/SKILL.md +139 -42
- data/lib/clacky/default_skills/channel-setup/feishu_setup.rb +582 -0
- data/lib/clacky/default_skills/channel-setup/weixin_setup.rb +274 -0
- data/lib/clacky/default_skills/onboard/SKILL.md +132 -6
- data/lib/clacky/default_skills/personal-website/SKILL.md +113 -0
- data/lib/clacky/default_skills/personal-website/publish.rb +214 -0
- data/lib/clacky/default_skills/skill-add/SKILL.md +6 -7
- data/lib/clacky/default_skills/skill-add/scripts/install_from_zip.rb +1 -1
- data/lib/clacky/default_skills/skill-creator/SKILL.md +3 -3
- data/lib/clacky/json_ui_controller.rb +5 -0
- data/lib/clacky/message_format/open_ai.rb +7 -1
- data/lib/clacky/plain_ui_controller.rb +5 -0
- data/lib/clacky/server/browser_manager.rb +308 -0
- data/lib/clacky/server/channel/adapters/feishu/adapter.rb +80 -15
- data/lib/clacky/server/channel/adapters/feishu/bot.rb +216 -7
- data/lib/clacky/server/channel/adapters/feishu/message_parser.rb +12 -0
- data/lib/clacky/server/channel/adapters/wecom/adapter.rb +2 -8
- data/lib/clacky/server/channel/adapters/weixin/adapter.rb +391 -0
- data/lib/clacky/server/channel/adapters/weixin/api_client.rb +374 -0
- data/lib/clacky/server/channel/channel_config.rb +6 -0
- data/lib/clacky/server/channel/channel_manager.rb +50 -15
- data/lib/clacky/server/channel/channel_ui_controller.rb +18 -0
- data/lib/clacky/server/channel.rb +1 -0
- data/lib/clacky/server/http_server.rb +210 -45
- data/lib/clacky/server/scheduler.rb +1 -1
- data/lib/clacky/server/session_registry.rb +131 -40
- data/lib/clacky/server/web_ui_controller.rb +21 -2
- data/lib/clacky/session_manager.rb +43 -73
- data/lib/clacky/skill.rb +64 -42
- data/lib/clacky/tools/browser.rb +728 -183
- data/lib/clacky/tools/invoke_skill.rb +18 -11
- data/lib/clacky/tools/shell.rb +16 -7
- data/lib/clacky/tools/web_fetch.rb +3 -1
- data/lib/clacky/ui2/README.md +1 -1
- data/lib/clacky/ui2/components/input_area.rb +5 -2
- data/lib/clacky/ui2/components/welcome_banner.rb +23 -0
- data/lib/clacky/ui2/ui_controller.rb +16 -2
- data/lib/clacky/ui_interface.rb +7 -0
- data/lib/clacky/utils/file_processor.rb +116 -139
- data/lib/clacky/utils/model_pricing.rb +5 -4
- data/lib/clacky/utils/parser_manager.rb +93 -0
- data/lib/clacky/utils/workspace_rules.rb +46 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +696 -62
- data/lib/clacky/web/app.js +181 -51
- data/lib/clacky/web/brand.js +14 -0
- data/lib/clacky/web/channels.js +10 -1
- data/lib/clacky/web/i18n.js +49 -7
- data/lib/clacky/web/index.html +67 -10
- data/lib/clacky/web/onboard.js +2 -1
- data/lib/clacky/web/sessions.js +255 -69
- data/lib/clacky/web/settings.js +113 -1
- data/lib/clacky/web/skills.js +16 -3
- data/lib/clacky/web/tasks.js +11 -6
- data/lib/clacky/web/weixin-qr.html +104 -0
- data/lib/clacky.rb +2 -0
- data/scripts/install.sh +41 -11
- metadata +48 -7
- data/lib/clacky/default_skills/pdf-reader/SKILL.md +0 -90
- data/lib/clacky/utils/file_parser/docx_parser.rb +0 -156
- data/lib/clacky/utils/file_parser/pptx_parser.rb +0 -116
- data/lib/clacky/utils/file_parser/xlsx_parser.rb +0 -95
- data/lib/clacky/utils/file_parser/zip_parser.rb +0 -60
- data/scripts/install_agent_browser.sh +0 -67
data/lib/clacky/web/app.js
CHANGED
|
@@ -102,6 +102,9 @@ const Router = (() => {
|
|
|
102
102
|
_current = view;
|
|
103
103
|
_params = params;
|
|
104
104
|
|
|
105
|
+
// Close sidebar on mobile when navigating to any view
|
|
106
|
+
_mobileCloseSidebar();
|
|
107
|
+
|
|
105
108
|
// ── Clean up previous state ──────────────────────────────────────────
|
|
106
109
|
if (Sessions.activeId) {
|
|
107
110
|
Sessions._cacheActiveAndDeselect();
|
|
@@ -290,7 +293,9 @@ WS.onEvent(ev => {
|
|
|
290
293
|
|
|
291
294
|
// ── Session list ───────────────────────────────────────────────────
|
|
292
295
|
case "session_list": {
|
|
293
|
-
|
|
296
|
+
// Accept new per-source has_more map or legacy boolean
|
|
297
|
+
const hasMoreArg = ev.has_more_by_source || !!ev.has_more;
|
|
298
|
+
Sessions.setAll(ev.sessions || [], hasMoreArg);
|
|
294
299
|
Sessions.renderList();
|
|
295
300
|
|
|
296
301
|
// Restore URL hash once on initial connect; ignore subsequent session_list events.
|
|
@@ -423,6 +428,11 @@ WS.onEvent(ev => {
|
|
|
423
428
|
break;
|
|
424
429
|
|
|
425
430
|
// ── Info / errors ──────────────────────────────────────────────────
|
|
431
|
+
case "idle_status":
|
|
432
|
+
// Two-phase: "start" shows a spinner line, "end" updates it in-place
|
|
433
|
+
Sessions.updateIdleStatus(ev.phase, ev.message);
|
|
434
|
+
break;
|
|
435
|
+
|
|
426
436
|
case "info":
|
|
427
437
|
Sessions.appendInfo(ev.message);
|
|
428
438
|
break;
|
|
@@ -543,11 +553,9 @@ function _addGenericFile(file) {
|
|
|
543
553
|
.then(data => {
|
|
544
554
|
if (!data.ok) { alert(`Upload failed: ${data.error}`); return; }
|
|
545
555
|
_pendingFiles.push({
|
|
546
|
-
name:
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
preview_path: data.preview_path || null,
|
|
550
|
-
mime_type: file.type
|
|
556
|
+
name: data.name,
|
|
557
|
+
path: data.path,
|
|
558
|
+
mime_type: file.type
|
|
551
559
|
});
|
|
552
560
|
_renderAttachmentPreviews();
|
|
553
561
|
})
|
|
@@ -593,17 +601,30 @@ function _renderAttachmentPreviews() {
|
|
|
593
601
|
strip.appendChild(item);
|
|
594
602
|
});
|
|
595
603
|
|
|
596
|
-
// Render file
|
|
604
|
+
// Render file cards (PDF, ZIP, DOC, XLS, PPT, etc.)
|
|
597
605
|
_pendingFiles.forEach((f, idx) => {
|
|
598
606
|
const item = document.createElement("div");
|
|
599
607
|
item.className = "pdf-preview-item";
|
|
600
608
|
item.title = f.name;
|
|
609
|
+
|
|
601
610
|
const icon = document.createElement("div");
|
|
602
611
|
icon.className = "pdf-preview-icon";
|
|
603
612
|
icon.textContent = _docTypeIcon(f.mime_type);
|
|
613
|
+
|
|
614
|
+
const info = document.createElement("div");
|
|
615
|
+
info.className = "pdf-preview-info";
|
|
616
|
+
|
|
604
617
|
const name = document.createElement("div");
|
|
605
618
|
name.className = "pdf-preview-name";
|
|
606
619
|
name.textContent = f.name;
|
|
620
|
+
|
|
621
|
+
const typeLabel = document.createElement("div");
|
|
622
|
+
typeLabel.className = "pdf-preview-type";
|
|
623
|
+
typeLabel.textContent = (f.name.split(".").pop() || "file").toUpperCase();
|
|
624
|
+
|
|
625
|
+
info.appendChild(name);
|
|
626
|
+
info.appendChild(typeLabel);
|
|
627
|
+
|
|
607
628
|
const removeBtn = document.createElement("button");
|
|
608
629
|
removeBtn.className = "pdf-preview-remove";
|
|
609
630
|
removeBtn.textContent = "✕";
|
|
@@ -612,8 +633,9 @@ function _renderAttachmentPreviews() {
|
|
|
612
633
|
_pendingFiles.splice(idx, 1);
|
|
613
634
|
_renderAttachmentPreviews();
|
|
614
635
|
});
|
|
636
|
+
|
|
615
637
|
item.appendChild(icon);
|
|
616
|
-
item.appendChild(
|
|
638
|
+
item.appendChild(info);
|
|
617
639
|
item.appendChild(removeBtn);
|
|
618
640
|
strip.appendChild(item);
|
|
619
641
|
});
|
|
@@ -642,9 +664,17 @@ function sendMessage() {
|
|
|
642
664
|
bubbleHtml = thumbs + (bubbleHtml ? "<br>" + bubbleHtml : "");
|
|
643
665
|
}
|
|
644
666
|
if (_pendingFiles.length > 0) {
|
|
645
|
-
const badges = _pendingFiles
|
|
646
|
-
|
|
647
|
-
.
|
|
667
|
+
const badges = _pendingFiles.map(f => {
|
|
668
|
+
const icon = _docTypeIcon(f.mime_type);
|
|
669
|
+
const ext = (f.name.split(".").pop() || "file").toUpperCase();
|
|
670
|
+
return `<span class="msg-pdf-badge">` +
|
|
671
|
+
`<span class="msg-pdf-badge-icon">${icon}</span>` +
|
|
672
|
+
`<span class="msg-pdf-badge-info">` +
|
|
673
|
+
`<span class="msg-pdf-badge-name">${escapeHtml(f.name)}</span>` +
|
|
674
|
+
`<span class="msg-pdf-badge-type">${escapeHtml(ext)}</span>` +
|
|
675
|
+
`</span>` +
|
|
676
|
+
`</span>`;
|
|
677
|
+
}).join(" ");
|
|
648
678
|
bubbleHtml = badges + (bubbleHtml ? "<br>" + bubbleHtml : "");
|
|
649
679
|
}
|
|
650
680
|
Sessions.appendMsg("user", bubbleHtml);
|
|
@@ -657,11 +687,9 @@ function sendMessage() {
|
|
|
657
687
|
data_url: img.dataUrl
|
|
658
688
|
})),
|
|
659
689
|
..._pendingFiles.map(f => ({
|
|
660
|
-
name:
|
|
661
|
-
|
|
662
|
-
mime_type:
|
|
663
|
-
path: f.path,
|
|
664
|
-
preview_path: f.preview_path || null
|
|
690
|
+
name: f.name,
|
|
691
|
+
path: f.path,
|
|
692
|
+
mime_type: f.mime_type
|
|
665
693
|
}))
|
|
666
694
|
];
|
|
667
695
|
_pendingImages.length = 0;
|
|
@@ -675,22 +703,92 @@ function sendMessage() {
|
|
|
675
703
|
}
|
|
676
704
|
|
|
677
705
|
// ── DOM event listeners ───────────────────────────────────────────────────
|
|
678
|
-
// Sidebar toggle
|
|
706
|
+
// Sidebar toggle (with mobile overlay support)
|
|
707
|
+
function _isMobile() { return window.innerWidth <= 768; }
|
|
708
|
+
|
|
709
|
+
function _closeSidebar() {
|
|
710
|
+
$("sidebar").classList.add("hidden");
|
|
711
|
+
$("sidebar-overlay").classList.remove("active");
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function _openSidebar() {
|
|
715
|
+
$("sidebar").classList.remove("hidden");
|
|
716
|
+
if (_isMobile()) $("sidebar-overlay").classList.add("active");
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function _toggleSidebar() {
|
|
720
|
+
const isHidden = $("sidebar").classList.contains("hidden");
|
|
721
|
+
isHidden ? _openSidebar() : _closeSidebar();
|
|
722
|
+
}
|
|
723
|
+
|
|
679
724
|
if ($("btn-toggle-sidebar")) {
|
|
680
|
-
$("btn-toggle-sidebar").addEventListener("click",
|
|
681
|
-
const sidebar = $("sidebar");
|
|
682
|
-
sidebar.classList.toggle("hidden");
|
|
683
|
-
});
|
|
725
|
+
$("btn-toggle-sidebar").addEventListener("click", _toggleSidebar);
|
|
684
726
|
}
|
|
685
727
|
|
|
686
|
-
//
|
|
728
|
+
// Tap overlay to close sidebar on mobile
|
|
729
|
+
$("sidebar-overlay").addEventListener("click", _closeSidebar);
|
|
730
|
+
|
|
731
|
+
// On mobile: start with sidebar hidden
|
|
732
|
+
if (_isMobile()) _closeSidebar();
|
|
733
|
+
|
|
734
|
+
// Mobile back button in chat-header: tap to go back to welcome
|
|
735
|
+
const _btnBack = document.querySelector(".chat-back-btn");
|
|
736
|
+
if (_btnBack) _btnBack.addEventListener("click", () => Router.navigate("welcome"));
|
|
737
|
+
|
|
738
|
+
// On mobile: shorten textarea placeholder so it doesn't wrap
|
|
739
|
+
if (_isMobile()) {
|
|
740
|
+
const inp = $("user-input");
|
|
741
|
+
if (inp) inp.setAttribute("placeholder", inp.placeholder.replace(/\s*[\((].*[\))]/, "").trim());
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// On mobile: auto-close sidebar when switching sessions/pages
|
|
745
|
+
function _mobileCloseSidebar() {
|
|
746
|
+
if (_isMobile()) _closeSidebar();
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// ── New session split button [+ ▾] ────────────────────────────────────────
|
|
750
|
+
// btn-new-session-inline: click → create General session immediately
|
|
751
|
+
// btn-new-session-arrow: click → toggle dropdown
|
|
752
|
+
// dropdown items: click → create session with chosen agent_profile
|
|
687
753
|
if ($("btn-new-session")) {
|
|
688
|
-
$("btn-new-session").addEventListener("click", () => Sessions.create());
|
|
754
|
+
$("btn-new-session").addEventListener("click", () => Sessions.create("general"));
|
|
689
755
|
}
|
|
690
756
|
if ($("btn-new-session-inline")) {
|
|
691
|
-
$("btn-new-session-inline").addEventListener("click", () => Sessions.create());
|
|
757
|
+
$("btn-new-session-inline").addEventListener("click", () => Sessions.create("general"));
|
|
758
|
+
}
|
|
759
|
+
if ($("btn-new-session-arrow")) {
|
|
760
|
+
$("btn-new-session-arrow").addEventListener("click", (e) => {
|
|
761
|
+
e.stopPropagation();
|
|
762
|
+
const dd = $("new-session-dropdown");
|
|
763
|
+
if (dd) dd.hidden = !dd.hidden;
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
// Dropdown item clicks
|
|
767
|
+
document.querySelectorAll("#new-session-dropdown .dropdown-item").forEach(item => {
|
|
768
|
+
item.addEventListener("click", (e) => {
|
|
769
|
+
e.stopPropagation();
|
|
770
|
+
const profile = item.dataset.profile || "general";
|
|
771
|
+
$("new-session-dropdown").hidden = true;
|
|
772
|
+
Sessions.create(profile);
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
// Close dropdown when clicking elsewhere
|
|
776
|
+
document.addEventListener("click", () => {
|
|
777
|
+
const dd = $("new-session-dropdown");
|
|
778
|
+
if (dd && !dd.hidden) dd.hidden = true;
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
$("btn-welcome-new").addEventListener("click", () => Sessions.create("general"));
|
|
782
|
+
|
|
783
|
+
// ── Session source tabs ───────────────────────────────────────────────────
|
|
784
|
+
document.querySelectorAll(".session-tab").forEach(btn => {
|
|
785
|
+
btn.addEventListener("click", () => Sessions.setTab(btn.dataset.tab));
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
// Load-more sessions button
|
|
789
|
+
if ($("btn-load-more-sessions")) {
|
|
790
|
+
$("btn-load-more-sessions").addEventListener("click", () => Sessions.loadMoreSessions());
|
|
692
791
|
}
|
|
693
|
-
$("btn-welcome-new").addEventListener("click", () => Sessions.create());
|
|
694
792
|
|
|
695
793
|
// Theme toggle in header
|
|
696
794
|
if ($("theme-toggle-header")) {
|
|
@@ -763,12 +861,16 @@ $("user-input").addEventListener("paste", e => {
|
|
|
763
861
|
attachItems.forEach(it => _addAttachmentFile(it.getAsFile()));
|
|
764
862
|
});
|
|
765
863
|
|
|
766
|
-
//
|
|
767
|
-
//
|
|
768
|
-
//
|
|
769
|
-
//
|
|
770
|
-
//
|
|
771
|
-
//
|
|
864
|
+
// Cross-browser IME composition fix:
|
|
865
|
+
// Safari fires compositionend BEFORE keydown (violating W3C spec), and the
|
|
866
|
+
// gap between compositionend and keydown is ~5ms on Safari. We record the
|
|
867
|
+
// timestamp of compositionend and treat any Enter keydown within 20ms as
|
|
868
|
+
// still-composing. Chrome is unaffected because e.isComposing is still true.
|
|
869
|
+
// Reference: https://bugs.webkit.org/show_bug.cgi?id=165004
|
|
870
|
+
let _lastCompositionEndTime = -Infinity;
|
|
871
|
+
$("user-input").addEventListener("compositionend", () => {
|
|
872
|
+
_lastCompositionEndTime = Date.now();
|
|
873
|
+
});
|
|
772
874
|
|
|
773
875
|
// Hide skill autocomplete when input loses focus (unless clicking a dropdown item)
|
|
774
876
|
$("user-input").addEventListener("blur", () => {
|
|
@@ -780,20 +882,24 @@ $("user-input").addEventListener("blur", () => {
|
|
|
780
882
|
const SkillAC = (() => {
|
|
781
883
|
let _visible = false;
|
|
782
884
|
let _activeIndex = -1;
|
|
783
|
-
let _items = []; // filtered [{ name, description }]
|
|
784
|
-
let
|
|
885
|
+
let _items = []; // filtered [{ name, description, encrypted, source }]
|
|
886
|
+
let _currentSession = null; // track active session id for live fetch
|
|
785
887
|
|
|
786
|
-
/**
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
888
|
+
/** Called whenever the active session changes — just store the id, no prefetch. */
|
|
889
|
+
function _loadForSession(sessionId) {
|
|
890
|
+
_currentSession = sessionId || null;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/** Fetch live skill list from server for the current session. */
|
|
894
|
+
async function _fetchSkills() {
|
|
895
|
+
if (!_currentSession) return [];
|
|
790
896
|
try {
|
|
791
|
-
const res = await fetch(`/api/sessions/${
|
|
897
|
+
const res = await fetch(`/api/sessions/${_currentSession}/skills`);
|
|
792
898
|
const data = await res.json();
|
|
793
|
-
|
|
899
|
+
return data.skills || [];
|
|
794
900
|
} catch (e) {
|
|
795
|
-
console.error("[SkillAC]
|
|
796
|
-
|
|
901
|
+
console.error("[SkillAC] fetchSkills failed", e);
|
|
902
|
+
return [];
|
|
797
903
|
}
|
|
798
904
|
}
|
|
799
905
|
|
|
@@ -807,9 +913,8 @@ const SkillAC = (() => {
|
|
|
807
913
|
return null;
|
|
808
914
|
}
|
|
809
915
|
|
|
810
|
-
function _render(query) {
|
|
811
|
-
|
|
812
|
-
const all = _sessionSkills.length > 0 ? _sessionSkills : Skills.all;
|
|
916
|
+
async function _render(query) {
|
|
917
|
+
const all = await _fetchSkills();
|
|
813
918
|
_items = all.filter(s => s.name.toLowerCase().startsWith(query));
|
|
814
919
|
|
|
815
920
|
if (_items.length === 0) { _hide(); return; }
|
|
@@ -833,11 +938,36 @@ const SkillAC = (() => {
|
|
|
833
938
|
nameEl.className = "skill-ac-name";
|
|
834
939
|
nameEl.textContent = "/" + skill.name;
|
|
835
940
|
|
|
941
|
+
// meta: encrypted badge + source type label (subtle)
|
|
942
|
+
const metaEl = document.createElement("span");
|
|
943
|
+
metaEl.className = "skill-ac-meta";
|
|
944
|
+
if (skill.encrypted) {
|
|
945
|
+
const encBadge = document.createElement("span");
|
|
946
|
+
encBadge.className = "skill-ac-enc";
|
|
947
|
+
encBadge.textContent = "🔒";
|
|
948
|
+
metaEl.appendChild(encBadge);
|
|
949
|
+
}
|
|
950
|
+
const sourceLabel = {
|
|
951
|
+
"default": "built-in",
|
|
952
|
+
"global_clacky": "user",
|
|
953
|
+
"global_claude": "user",
|
|
954
|
+
"project_clacky": "project",
|
|
955
|
+
"project_claude": "project",
|
|
956
|
+
"brand": "brand",
|
|
957
|
+
}[skill.source_type];
|
|
958
|
+
if (sourceLabel) {
|
|
959
|
+
const srcEl = document.createElement("span");
|
|
960
|
+
srcEl.className = "skill-ac-src";
|
|
961
|
+
srcEl.textContent = sourceLabel;
|
|
962
|
+
metaEl.appendChild(srcEl);
|
|
963
|
+
}
|
|
964
|
+
|
|
836
965
|
const descEl = document.createElement("span");
|
|
837
966
|
descEl.className = "skill-ac-desc";
|
|
838
967
|
descEl.textContent = skill.description || "";
|
|
839
968
|
|
|
840
969
|
item.appendChild(nameEl);
|
|
970
|
+
item.appendChild(metaEl);
|
|
841
971
|
item.appendChild(descEl);
|
|
842
972
|
|
|
843
973
|
item.addEventListener("mousedown", e => {
|
|
@@ -884,18 +1014,18 @@ const SkillAC = (() => {
|
|
|
884
1014
|
}
|
|
885
1015
|
|
|
886
1016
|
/** Open the dropdown showing all skills, used by the / button. */
|
|
887
|
-
function _openAll() {
|
|
1017
|
+
async function _openAll() {
|
|
888
1018
|
_activeIndex = 0; // Default to first item
|
|
889
|
-
_render("");
|
|
1019
|
+
await _render("");
|
|
890
1020
|
$("user-input").focus();
|
|
891
1021
|
}
|
|
892
1022
|
|
|
893
1023
|
/** Toggle the dropdown (open if hidden, close if visible). */
|
|
894
|
-
function _toggle() {
|
|
1024
|
+
async function _toggle() {
|
|
895
1025
|
if (_visible) {
|
|
896
1026
|
_hide();
|
|
897
1027
|
} else {
|
|
898
|
-
_openAll();
|
|
1028
|
+
await _openAll();
|
|
899
1029
|
}
|
|
900
1030
|
}
|
|
901
1031
|
|
|
@@ -908,7 +1038,7 @@ const SkillAC = (() => {
|
|
|
908
1038
|
const query = _getSlashQuery(value);
|
|
909
1039
|
if (query === null) { _hide(); return; }
|
|
910
1040
|
_activeIndex = 0; // Always highlight the first match
|
|
911
|
-
_render(query);
|
|
1041
|
+
_render(query); // async, fire-and-forget
|
|
912
1042
|
},
|
|
913
1043
|
|
|
914
1044
|
/** Open dropdown with all skills (triggered by / button). */
|
|
@@ -933,7 +1063,7 @@ const SkillAC = (() => {
|
|
|
933
1063
|
_select(targetIdx);
|
|
934
1064
|
return true;
|
|
935
1065
|
}
|
|
936
|
-
if (e.key === "Enter" && !e.isComposing) {
|
|
1066
|
+
if (e.key === "Enter" && !e.isComposing && (Date.now() - _lastCompositionEndTime) > 20) {
|
|
937
1067
|
if (_activeIndex >= 0) {
|
|
938
1068
|
e.preventDefault();
|
|
939
1069
|
_select(_activeIndex);
|
|
@@ -960,7 +1090,7 @@ $("user-input").addEventListener("keydown", e => {
|
|
|
960
1090
|
// Let skill autocomplete consume arrow/enter/escape first
|
|
961
1091
|
if (SkillAC.handleKey(e)) return;
|
|
962
1092
|
|
|
963
|
-
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
|
|
1093
|
+
if (e.key === "Enter" && !e.shiftKey && !e.isComposing && (Date.now() - _lastCompositionEndTime) > 20) {
|
|
964
1094
|
e.preventDefault();
|
|
965
1095
|
sendMessage();
|
|
966
1096
|
}
|
data/lib/clacky/web/brand.js
CHANGED
|
@@ -213,6 +213,20 @@ const Brand = (() => {
|
|
|
213
213
|
const logoText = document.getElementById("header-logo");
|
|
214
214
|
const brandWrap = document.getElementById("header-brand");
|
|
215
215
|
|
|
216
|
+
// Apply theme color — overrides --color-accent-primary and --color-button-primary
|
|
217
|
+
if (info.theme_color) {
|
|
218
|
+
const root = document.documentElement;
|
|
219
|
+
root.style.setProperty("--color-accent-primary", info.theme_color);
|
|
220
|
+
root.style.setProperty("--color-accent-hover", info.theme_color);
|
|
221
|
+
root.style.setProperty("--color-button-primary", info.theme_color);
|
|
222
|
+
root.style.setProperty("--color-button-primary-hover", info.theme_color);
|
|
223
|
+
// Also update browser tab color on mobile
|
|
224
|
+
const metaTheme = document.querySelector("meta[name='theme-color']");
|
|
225
|
+
if (metaTheme) metaTheme.setAttribute("content", info.theme_color);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// header-brand already has onclick="Router.navigate('chat')" in HTML, no extra link needed
|
|
229
|
+
|
|
216
230
|
const hasLogo = !!(info.logo_url && logoImg);
|
|
217
231
|
|
|
218
232
|
if (hasLogo) {
|
data/lib/clacky/web/channels.js
CHANGED
|
@@ -27,6 +27,14 @@ const Channels = (() => {
|
|
|
27
27
|
setupCmd: "/channel-setup setup wecom",
|
|
28
28
|
testCmd: "/channel-setup doctor",
|
|
29
29
|
},
|
|
30
|
+
weixin: {
|
|
31
|
+
logo: "信",
|
|
32
|
+
logoClass: "channel-logo-weixin",
|
|
33
|
+
name: "Weixin (微信)",
|
|
34
|
+
desc: I18n.t("channels.weixin.desc"),
|
|
35
|
+
setupCmd: "/channel-setup setup weixin",
|
|
36
|
+
testCmd: "/channel-setup doctor",
|
|
37
|
+
},
|
|
30
38
|
};
|
|
31
39
|
}
|
|
32
40
|
|
|
@@ -164,7 +172,7 @@ const Channels = (() => {
|
|
|
164
172
|
const res = await fetch("/api/sessions", {
|
|
165
173
|
method: "POST",
|
|
166
174
|
headers: { "Content-Type": "application/json" },
|
|
167
|
-
body: JSON.stringify({ name }),
|
|
175
|
+
body: JSON.stringify({ name, source: "setup" }),
|
|
168
176
|
});
|
|
169
177
|
const data = await res.json();
|
|
170
178
|
if (!res.ok) throw new Error(data.error || I18n.t("channels.sessionError"));
|
|
@@ -173,6 +181,7 @@ const Channels = (() => {
|
|
|
173
181
|
|
|
174
182
|
// Register in Sessions, refresh sidebar, queue command, then navigate
|
|
175
183
|
Sessions.add(session);
|
|
184
|
+
Sessions.setTab("setup");
|
|
176
185
|
Sessions.renderList();
|
|
177
186
|
Sessions.setPendingMessage(session.id, command);
|
|
178
187
|
Sessions.select(session.id);
|
data/lib/clacky/web/i18n.js
CHANGED
|
@@ -45,6 +45,13 @@ const I18n = (() => {
|
|
|
45
45
|
"sessions.createError": "Error: ",
|
|
46
46
|
"sessions.thinking": "Thinking…",
|
|
47
47
|
"sessions.default_name": "Session {{n}}",
|
|
48
|
+
"sessions.tab.manual": "Manual",
|
|
49
|
+
"sessions.tab.scheduled": "Scheduled",
|
|
50
|
+
"sessions.tab.channel": "Channel",
|
|
51
|
+
"sessions.tab.setup": "Setup",
|
|
52
|
+
"sessions.newSession": "+ New Session",
|
|
53
|
+
"sessions.loadMore": "Load more sessions",
|
|
54
|
+
"sessions.loadingMore": "Loading…",
|
|
48
55
|
|
|
49
56
|
// ── Modal ──
|
|
50
57
|
"modal.yes": "Yes",
|
|
@@ -135,7 +142,7 @@ const I18n = (() => {
|
|
|
135
142
|
|
|
136
143
|
// ── Channels panel ──
|
|
137
144
|
"channels.title": "Channels",
|
|
138
|
-
"channels.subtitle": "Connect IM platforms so your users can chat with the assistant via Feishu or
|
|
145
|
+
"channels.subtitle": "Connect IM platforms so your users can chat with the assistant via Feishu, WeCom or Weixin",
|
|
139
146
|
"channels.loading": "Loading…",
|
|
140
147
|
"channels.badge.running": "Running",
|
|
141
148
|
"channels.badge.enabled": "Enabled",
|
|
@@ -155,6 +162,7 @@ const I18n = (() => {
|
|
|
155
162
|
"channels.noSession": "No session returned",
|
|
156
163
|
"channels.feishu.desc": "Connect via Feishu open platform WebSocket long connection",
|
|
157
164
|
"channels.wecom.desc": "Connect via WeCom intelligent robot WebSocket",
|
|
165
|
+
"channels.weixin.desc": "Connect via WeChat iLink bot (QR login, HTTP long-poll)",
|
|
158
166
|
|
|
159
167
|
// ── Settings panel ──
|
|
160
168
|
"settings.title": "Settings",
|
|
@@ -197,13 +205,23 @@ const I18n = (() => {
|
|
|
197
205
|
"settings.personalize.btn": "✨ Re-run Onboard",
|
|
198
206
|
"settings.personalize.btn.starting": "Starting…",
|
|
199
207
|
"settings.personalize.btn.rerun": "✨ Re-run Onboard",
|
|
208
|
+
"settings.browser.title": "Browser",
|
|
209
|
+
"settings.browser.desc": "Connect your browser to enable browser automation.",
|
|
210
|
+
"settings.browser.configured": "✅ Browser connected",
|
|
211
|
+
"settings.browser.disabled": "⏸ Browser disabled",
|
|
212
|
+
"settings.browser.btn": "🌐 Configure Browser",
|
|
213
|
+
"settings.browser.btn.reconfigure": "🌐 Reconfigure Browser",
|
|
214
|
+
"settings.browser.btn.starting": "Starting…",
|
|
200
215
|
"settings.brand.title": "Brand & License",
|
|
201
216
|
"settings.brand.label.brand": "Brand",
|
|
202
217
|
"settings.brand.label.status": "Status",
|
|
203
218
|
"settings.brand.label.expires": "Expires",
|
|
219
|
+
"settings.brand.label.homepage": "Homepage",
|
|
220
|
+
"settings.brand.label.supportContact": "Support",
|
|
204
221
|
"settings.brand.label.supportQr": "Tech Support",
|
|
205
222
|
"settings.brand.label.qrHint": "Scan with your phone camera",
|
|
206
223
|
"settings.brand.btn.change": "Change License Key",
|
|
224
|
+
"settings.brand.confirmRebind": "Warning: all previously installed brand skills will be deleted and cannot be used. Continue?",
|
|
207
225
|
"settings.brand.badge.active": "Active",
|
|
208
226
|
"settings.brand.badge.warning": "Expiring Soon",
|
|
209
227
|
"settings.brand.badge.expired": "Expired",
|
|
@@ -296,6 +314,13 @@ const I18n = (() => {
|
|
|
296
314
|
"sessions.createError": "错误:",
|
|
297
315
|
"sessions.thinking": "思考中…",
|
|
298
316
|
"sessions.default_name": "对话 {{n}}",
|
|
317
|
+
"sessions.tab.manual": "默认",
|
|
318
|
+
"sessions.tab.scheduled": "定时",
|
|
319
|
+
"sessions.tab.channel": "频道",
|
|
320
|
+
"sessions.tab.setup": "配置",
|
|
321
|
+
"sessions.newSession": "+ 新会话",
|
|
322
|
+
"sessions.loadMore": "加载更多会话",
|
|
323
|
+
"sessions.loadingMore": "加载中…",
|
|
299
324
|
|
|
300
325
|
// ── Modal ──
|
|
301
326
|
"modal.yes": "确认",
|
|
@@ -386,13 +411,13 @@ const I18n = (() => {
|
|
|
386
411
|
|
|
387
412
|
// ── Channels panel ──
|
|
388
413
|
"channels.title": "频道",
|
|
389
|
-
"channels.subtitle": "
|
|
414
|
+
"channels.subtitle": "连接即时通讯平台,让用户通过飞书、企业微信或微信与助手对话",
|
|
390
415
|
"channels.loading": "加载中…",
|
|
391
416
|
"channels.loadError": "加载频道失败:{{msg}}",
|
|
392
|
-
"channels.badge.running": "
|
|
393
|
-
"channels.badge.enabled": "
|
|
394
|
-
"channels.badge.disabled": "
|
|
395
|
-
"channels.badge.notConfigured": "
|
|
417
|
+
"channels.badge.running": "运行中",
|
|
418
|
+
"channels.badge.enabled": "已启用",
|
|
419
|
+
"channels.badge.disabled": "未配置",
|
|
420
|
+
"channels.badge.notConfigured": "未配置",
|
|
396
421
|
"channels.hint.running": "适配器运行中,正在接受用户消息",
|
|
397
422
|
"channels.hint.enabled": "⚠ 已启用但未运行 — 适配器可能连接失败,请点击「诊断问题」排查。",
|
|
398
423
|
"channels.hint.enabledNotRunning":"已启用但未运行 — 适配器可能连接失败,请点击「诊断问题」排查。",
|
|
@@ -406,6 +431,7 @@ const I18n = (() => {
|
|
|
406
431
|
"channels.noSession": "未返回对话",
|
|
407
432
|
"channels.feishu.desc": "通过飞书开放平台 WebSocket 长连接接入",
|
|
408
433
|
"channels.wecom.desc": "通过企业微信智能机器人 WebSocket 接入",
|
|
434
|
+
"channels.weixin.desc": "通过微信 iLink 机器人接入(扫码登录,HTTP 长轮询)",
|
|
409
435
|
|
|
410
436
|
// ── Settings panel ──
|
|
411
437
|
"settings.title": "设置",
|
|
@@ -448,13 +474,23 @@ const I18n = (() => {
|
|
|
448
474
|
"settings.personalize.btn": "✨ 重新引导",
|
|
449
475
|
"settings.personalize.btn.starting": "启动中…",
|
|
450
476
|
"settings.personalize.btn.rerun": "✨ 重新引导",
|
|
477
|
+
"settings.browser.title": "浏览器",
|
|
478
|
+
"settings.browser.desc": "连接浏览器以启用浏览器自动化功能。",
|
|
479
|
+
"settings.browser.configured": "✅ 浏览器已连接",
|
|
480
|
+
"settings.browser.disabled": "⏸ 浏览器已禁用",
|
|
481
|
+
"settings.browser.btn": "🌐 配置浏览器",
|
|
482
|
+
"settings.browser.btn.reconfigure": "🌐 重新配置浏览器",
|
|
483
|
+
"settings.browser.btn.starting": "启动中…",
|
|
451
484
|
"settings.brand.title": "品牌 & 授权",
|
|
452
485
|
"settings.brand.label.brand": "品牌",
|
|
453
486
|
"settings.brand.label.status": "状态",
|
|
454
487
|
"settings.brand.label.expires": "到期时间",
|
|
488
|
+
"settings.brand.label.homepage": "主页",
|
|
489
|
+
"settings.brand.label.supportContact": "联系支持",
|
|
455
490
|
"settings.brand.label.supportQr": "技术支持",
|
|
456
491
|
"settings.brand.label.qrHint": "使用手机扫描二维码",
|
|
457
492
|
"settings.brand.btn.change": "更换授权码",
|
|
493
|
+
"settings.brand.confirmRebind": "警告:所有已安装的历史品牌技能将被删除,无法继续使用。确认继续?",
|
|
458
494
|
"settings.brand.badge.active": "已激活",
|
|
459
495
|
"settings.brand.badge.warning": "即将过期",
|
|
460
496
|
"settings.brand.badge.expired": "已过期",
|
|
@@ -528,6 +564,7 @@ const I18n = (() => {
|
|
|
528
564
|
_lang = TRANSLATIONS[code] ? code : DEFAULT_LANG;
|
|
529
565
|
localStorage.setItem(STORAGE_KEY, _lang);
|
|
530
566
|
applyAll();
|
|
567
|
+
document.dispatchEvent(new CustomEvent("langchange", { detail: { lang: _lang } }));
|
|
531
568
|
}
|
|
532
569
|
|
|
533
570
|
/**
|
|
@@ -558,7 +595,12 @@ const I18n = (() => {
|
|
|
558
595
|
document.querySelectorAll("[data-i18n-placeholder]").forEach(el => {
|
|
559
596
|
const key = el.getAttribute("data-i18n-placeholder");
|
|
560
597
|
const vars = _extractVars(el);
|
|
561
|
-
|
|
598
|
+
let ph = t(key, vars);
|
|
599
|
+
// On mobile: strip the parenthetical hint so placeholder doesn't wrap
|
|
600
|
+
if (window.innerWidth <= 768) {
|
|
601
|
+
ph = ph.replace(/\s*[\((].*[\))]/, "").trim();
|
|
602
|
+
}
|
|
603
|
+
el.placeholder = ph;
|
|
562
604
|
});
|
|
563
605
|
document.querySelectorAll("[data-i18n-title]").forEach(el => {
|
|
564
606
|
const key = el.getAttribute("data-i18n-title");
|