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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.clacky/skills/gem-release/SKILL.md +99 -356
  3. data/.clacky/skills/gem-release/scripts/release.sh +304 -0
  4. data/CHANGELOG.md +42 -0
  5. data/docs/system-skill-authoring-guide.md +1 -1
  6. data/lib/clacky/agent/tool_executor.rb +3 -1
  7. data/lib/clacky/agent.rb +12 -7
  8. data/lib/clacky/agent_config.rb +9 -3
  9. data/lib/clacky/brand_config.rb +19 -4
  10. data/lib/clacky/cli.rb +1 -1
  11. data/lib/clacky/default_skills/{channel-setup → channel-manager}/SKILL.md +180 -18
  12. data/lib/clacky/default_skills/channel-manager/dingtalk_setup.rb +191 -0
  13. data/lib/clacky/default_skills/channel-manager/discord_setup.rb +199 -0
  14. data/lib/clacky/default_skills/channel-manager/install_feishu_skills.rb +105 -0
  15. data/lib/clacky/default_skills/onboard/SKILL.md +2 -2
  16. data/lib/clacky/default_skills/onboard/scripts/import_external_skills.rb +2 -4
  17. data/lib/clacky/default_skills/onboard/scripts/install_builtin_skills.rb +18 -96
  18. data/lib/clacky/default_skills/product-help/SKILL.md +10 -2
  19. data/lib/clacky/message_history.rb +26 -1
  20. data/lib/clacky/providers.rb +29 -4
  21. data/lib/clacky/server/channel/adapters/dingtalk/adapter.rb +177 -0
  22. data/lib/clacky/server/channel/adapters/dingtalk/api_client.rb +82 -0
  23. data/lib/clacky/server/channel/adapters/dingtalk/stream_client.rb +205 -0
  24. data/lib/clacky/server/channel/adapters/discord/adapter.rb +229 -0
  25. data/lib/clacky/server/channel/adapters/discord/api_client.rb +108 -0
  26. data/lib/clacky/server/channel/adapters/discord/gateway_client.rb +272 -0
  27. data/lib/clacky/server/channel/adapters/telegram/adapter.rb +375 -0
  28. data/lib/clacky/server/channel/adapters/telegram/api_client.rb +205 -0
  29. data/lib/clacky/server/channel/channel_config.rb +26 -0
  30. data/lib/clacky/server/channel.rb +3 -0
  31. data/lib/clacky/server/http_server.rb +75 -4
  32. data/lib/clacky/server/server_master.rb +35 -13
  33. data/lib/clacky/server/session_registry.rb +54 -3
  34. data/lib/clacky/server/web_ui_controller.rb +7 -1
  35. data/lib/clacky/telemetry.rb +1 -16
  36. data/lib/clacky/tools/browser.rb +8 -5
  37. data/lib/clacky/tools/glob.rb +11 -38
  38. data/lib/clacky/tools/grep.rb +7 -16
  39. data/lib/clacky/ui2/markdown_renderer.rb +1 -1
  40. data/lib/clacky/ui2/ui_controller.rb +2 -1
  41. data/lib/clacky/utils/file_ignore_helper.rb +49 -0
  42. data/lib/clacky/utils/gitignore_parser.rb +27 -0
  43. data/lib/clacky/version.rb +1 -1
  44. data/lib/clacky/web/app.css +248 -31
  45. data/lib/clacky/web/app.js +51 -1
  46. data/lib/clacky/web/channels.js +98 -28
  47. data/lib/clacky/web/datepicker.js +205 -0
  48. data/lib/clacky/web/i18n.js +48 -9
  49. data/lib/clacky/web/index.html +33 -6
  50. data/lib/clacky/web/onboard.js +46 -4
  51. data/lib/clacky/web/sessions.js +33 -72
  52. data/lib/clacky/web/settings.js +42 -4
  53. data/lib/clacky/web/version.js +52 -1
  54. metadata +21 -10
  55. data/docs/proposals/2026-05-11-system-prompt-alignment.md +0 -325
  56. data/docs/proposals/2026-05-12-memory-mechanism-optimization.md +0 -89
  57. /data/lib/clacky/default_skills/{channel-setup → channel-manager}/feishu_setup.rb +0 -0
  58. /data/lib/clacky/default_skills/{channel-setup → channel-manager}/import_lark_skills.rb +0 -0
  59. /data/lib/clacky/default_skills/{channel-setup → channel-manager}/weixin_setup.rb +0 -0
@@ -687,11 +687,11 @@ body {
687
687
  opacity: 0.5;
688
688
  }
689
689
 
690
- /* date input: fill wrapper, hide native calendar icon */
690
+ /* date trigger button */
691
691
  .search-date {
692
692
  flex: 1;
693
693
  min-width: 0;
694
- padding: 0 20px 0 4px; /* right padding leaves room for our icon */
694
+ padding: 0 20px 0 4px;
695
695
  height: 100%;
696
696
  font-size: 11px;
697
697
  border: none;
@@ -699,22 +699,10 @@ body {
699
699
  color: var(--color-text-secondary);
700
700
  outline: none;
701
701
  cursor: pointer;
702
- color-scheme: light;
703
- /* hide native calendar picker icon */
704
- -webkit-appearance: none;
705
- appearance: none;
706
- }
707
- .search-date::-webkit-calendar-picker-indicator {
708
- position: absolute;
709
- width: 100%;
710
- height: 100%;
711
- top: 0;
712
- left: 0;
713
- opacity: 0;
714
- cursor: pointer;
715
- }
716
- [data-theme="dark"] .search-date {
717
- color-scheme: dark;
702
+ text-align: left;
703
+ white-space: nowrap;
704
+ overflow: hidden;
705
+ text-overflow: ellipsis;
718
706
  }
719
707
 
720
708
  /* Active filter highlight */
@@ -724,15 +712,104 @@ body {
724
712
  font-weight: 500;
725
713
  }
726
714
 
715
+ /* ── DatePicker popup ─────────────────────────────────────────────────────── */
716
+ .dp-popup {
717
+ position: fixed;
718
+ z-index: 9999;
719
+ background: var(--color-bg-primary, #fff);
720
+ border: 1px solid var(--color-border-primary);
721
+ border-radius: 10px;
722
+ box-shadow: 0 8px 24px rgba(0,0,0,0.12);
723
+ padding: 10px;
724
+ width: 230px;
725
+ font-size: 12px;
726
+ color: var(--color-text-primary);
727
+ user-select: none;
728
+ }
729
+ .dp-header {
730
+ display: flex;
731
+ align-items: center;
732
+ justify-content: space-between;
733
+ margin-bottom: 8px;
734
+ }
735
+ .dp-title {
736
+ font-weight: 600;
737
+ font-size: 13px;
738
+ }
739
+ .dp-nav {
740
+ background: none;
741
+ border: none;
742
+ cursor: pointer;
743
+ font-size: 16px;
744
+ color: var(--color-text-secondary);
745
+ padding: 2px 6px;
746
+ border-radius: 4px;
747
+ line-height: 1;
748
+ }
749
+ .dp-nav:hover { background: var(--color-bg-hover); }
750
+ .dp-grid {
751
+ display: grid;
752
+ grid-template-columns: repeat(7, 1fr);
753
+ gap: 2px;
754
+ }
755
+ .dp-dow {
756
+ text-align: center;
757
+ font-size: 10px;
758
+ color: var(--color-text-muted);
759
+ padding: 3px 0;
760
+ font-weight: 500;
761
+ }
762
+ .dp-day {
763
+ background: none;
764
+ border: none;
765
+ cursor: pointer;
766
+ border-radius: 6px;
767
+ padding: 4px 2px;
768
+ text-align: center;
769
+ color: var(--color-text-primary);
770
+ font-size: 12px;
771
+ line-height: 1.4;
772
+ }
773
+ .dp-day:hover { background: var(--color-bg-hover); }
774
+ .dp-day--today {
775
+ color: var(--color-accent-primary);
776
+ font-weight: 700;
777
+ }
778
+ .dp-day--selected {
779
+ background: var(--color-accent-primary);
780
+ color: var(--color-text-inverse) !important;
781
+ font-weight: 600;
782
+ }
783
+ .dp-day--selected:hover { background: var(--color-accent-primary); opacity: 0.88; }
784
+ .dp-footer {
785
+ display: flex;
786
+ justify-content: space-between;
787
+ margin-top: 8px;
788
+ padding-top: 8px;
789
+ border-top: 1px solid var(--color-border-primary);
790
+ }
791
+ .dp-btn-clear,
792
+ .dp-btn-today {
793
+ background: none;
794
+ border: none;
795
+ cursor: pointer;
796
+ font-size: 11px;
797
+ color: var(--color-accent-primary);
798
+ padding: 4px 8px;
799
+ border-radius: 4px;
800
+ }
801
+ .dp-btn-clear:hover,
802
+ .dp-btn-today:hover { background: var(--color-bg-hover); }
803
+
727
804
  /* 清除筛选 — small ✕ icon button */
728
805
  .btn-search-clear-all {
729
806
  flex-shrink: 0;
730
807
  margin-left: 4px;
731
- width: 16px;
732
- height: 16px;
808
+ width: 14px;
809
+ height: 14px;
733
810
  padding: 0;
734
- font-size: 10px;
735
- line-height: 16px;
811
+ font-size: 9px;
812
+ line-height: 14px;
736
813
  text-align: center;
737
814
  border: none;
738
815
  border-radius: 50%;
@@ -1049,7 +1126,7 @@ body {
1049
1126
  border-radius: 6px;
1050
1127
  border: 1px solid transparent;
1051
1128
  transition: background .2s ease;
1052
- margin-bottom: 4px;
1129
+ margin: 0 8px 4px;
1053
1130
  }
1054
1131
  /* Default: secondary text color */
1055
1132
  .task-item .task-name,
@@ -1086,15 +1163,15 @@ body {
1086
1163
  .task-row {
1087
1164
  display: flex;
1088
1165
  align-items: center;
1089
- gap: 10px;
1090
- padding: 10px 12px 10px 15px;
1166
+ gap: 6px;
1167
+ padding: 10px 12px 10px 12px;
1091
1168
  cursor: pointer;
1092
1169
  border-radius: 6px;
1093
1170
  transition: background 0.2s ease;
1094
1171
  }
1095
1172
  .task-icon {
1096
- width: 18px;
1097
- height: 18px;
1173
+ width: 17px;
1174
+ height: 17px;
1098
1175
  flex-shrink: 0;
1099
1176
  stroke-width: 2;
1100
1177
  }
@@ -1726,6 +1803,7 @@ body {
1726
1803
  border-top: 1px solid var(--color-border-primary);
1727
1804
  margin: 0.8em 0;
1728
1805
  }
1806
+ .msg-assistant img { max-width: 100%; height: auto; border-radius: 4px; }
1729
1807
  .msg-assistant strong { font-weight: 600; color: var(--color-text-primary); }
1730
1808
  .msg-assistant em { font-style: italic; color: var(--color-text-secondary); }
1731
1809
  .msg-tool { background: var(--color-bg-primary); border: 1px solid var(--color-border-primary); font-family: monospace; font-size: 12px; color: var(--color-text-secondary); align-self: flex-start; }
@@ -3257,6 +3335,56 @@ body {
3257
3335
  color: var(--color-accent-primary);
3258
3336
  }
3259
3337
 
3338
+ /* Provider recommended badge (inside dropdown option) */
3339
+ .provider-badge-recommended {
3340
+ display: inline-block;
3341
+ font-size: 10px;
3342
+ font-weight: 600;
3343
+ padding: 1px 6px;
3344
+ margin-left: 6px;
3345
+ border-radius: 4px;
3346
+ background: var(--color-accent-primary, #4f46e5);
3347
+ color: #fff;
3348
+ vertical-align: middle;
3349
+ letter-spacing: 0.3px;
3350
+ line-height: 1.6;
3351
+ }
3352
+
3353
+ /* Provider promo hint (shown when openclacky is selected) */
3354
+ .provider-promo-hint {
3355
+ display: none;
3356
+ margin-top: 2px;
3357
+ padding: 8px 10px;
3358
+ border-radius: 6px;
3359
+ background: var(--color-bg-hover, #fafafa);
3360
+ }
3361
+ .provider-promo-hint.visible {
3362
+ display: block;
3363
+ }
3364
+ .provider-promo-hint .promo-inner {
3365
+ padding: 0;
3366
+ }
3367
+ .provider-promo-hint .promo-title {
3368
+ font-size: 11px;
3369
+ font-weight: 600;
3370
+ color: var(--color-text-secondary);
3371
+ margin-bottom: 3px;
3372
+ }
3373
+ .provider-promo-hint .promo-item {
3374
+ display: flex;
3375
+ align-items: baseline;
3376
+ gap: 4px;
3377
+ font-size: 11px;
3378
+ line-height: 1.6;
3379
+ color: var(--color-text-tertiary, #9ca3af);
3380
+ }
3381
+ .provider-promo-hint .promo-icon {
3382
+ flex-shrink: 0;
3383
+ font-size: 6px;
3384
+ opacity: 0.3;
3385
+ line-height: 2;
3386
+ }
3387
+
3260
3388
  /* Model name combobox */
3261
3389
  .model-name-combobox {
3262
3390
  position: relative;
@@ -3449,6 +3577,13 @@ body {
3449
3577
  .modal-box.sm { max-width: 480px; }
3450
3578
  .modal-box.lg { max-width: 680px; }
3451
3579
  .modal-title { font-size: 16px; font-weight: 600; margin-bottom: 16px; color: var(--color-text-primary); }
3580
+ .modal-confirm-message {
3581
+ font-size: 15px;
3582
+ line-height: 1.6;
3583
+ margin-bottom: 20px;
3584
+ white-space: pre-wrap;
3585
+ color: var(--color-text-primary);
3586
+ }
3452
3587
  .modal-actions {
3453
3588
  display: flex;
3454
3589
  gap: 10px;
@@ -3523,6 +3658,10 @@ body {
3523
3658
  font-weight: 500;
3524
3659
  color: var(--color-text-primary);
3525
3660
  }
3661
+ .label-required {
3662
+ color: var(--color-error);
3663
+ margin-left: 2px;
3664
+ }
3526
3665
  .modal-input, .modal-select {
3527
3666
  background: var(--color-bg-primary);
3528
3667
  border: 1px solid var(--color-border-primary);
@@ -3537,6 +3676,9 @@ body {
3537
3676
  outline: none;
3538
3677
  border-color: var(--color-accent-primary);
3539
3678
  }
3679
+ .modal-input.input-error {
3680
+ border-color: var(--color-error);
3681
+ }
3540
3682
  .modal-select {
3541
3683
  cursor: pointer;
3542
3684
  appearance: none;
@@ -5744,14 +5886,22 @@ body.setup-mode[data-theme="dark"] {
5744
5886
  display: flex;
5745
5887
  align-items: center;
5746
5888
  justify-content: center;
5747
- font-size: 18px;
5748
- font-weight: 700;
5749
5889
  flex-shrink: 0;
5750
5890
  }
5751
5891
 
5892
+ .channel-logo svg {
5893
+ width: 24px;
5894
+ height: 24px;
5895
+ display: block;
5896
+ }
5897
+
5752
5898
  .channel-logo-feishu {
5753
- background: linear-gradient(135deg, #1456f0, #0099ff);
5754
- color: #fff;
5899
+ background: transparent;
5900
+ }
5901
+
5902
+ .channel-logo-feishu svg {
5903
+ width: 32px;
5904
+ height: 32px;
5755
5905
  }
5756
5906
 
5757
5907
  .channel-logo-wecom {
@@ -5764,6 +5914,21 @@ body.setup-mode[data-theme="dark"] {
5764
5914
  color: #fff;
5765
5915
  }
5766
5916
 
5917
+ .channel-logo-discord {
5918
+ background: linear-gradient(135deg, #5865f2, #4752c4);
5919
+ color: #fff;
5920
+ }
5921
+
5922
+ .channel-logo-telegram {
5923
+ background: linear-gradient(135deg, #2aabee, #229ed9);
5924
+ color: #fff;
5925
+ }
5926
+
5927
+ .channel-logo-dingtalk {
5928
+ background: linear-gradient(135deg, #3296fa, #1677ff);
5929
+ color: #fff;
5930
+ }
5931
+
5767
5932
  .channel-card-name {
5768
5933
  font-size: 15px;
5769
5934
  font-weight: 600;
@@ -5777,6 +5942,13 @@ body.setup-mode[data-theme="dark"] {
5777
5942
  }
5778
5943
 
5779
5944
  /* Status badge */
5945
+ .channel-card-status {
5946
+ display: inline-flex;
5947
+ align-items: center;
5948
+ gap: 12px;
5949
+ flex-shrink: 0;
5950
+ }
5951
+
5780
5952
  .channel-status-badge {
5781
5953
  display: inline-flex;
5782
5954
  align-items: center;
@@ -5846,6 +6018,17 @@ body.setup-mode[data-theme="dark"] {
5846
6018
  transition: background 0.15s, opacity 0.15s;
5847
6019
  }
5848
6020
 
6021
+ /* Test button uses an outline / ghost style so it does not compete with the primary action */
6022
+ .btn-channel-test {
6023
+ background: transparent;
6024
+ color: var(--color-text-secondary);
6025
+ border: 1px solid var(--color-border-primary);
6026
+ }
6027
+ .btn-channel-test:hover {
6028
+ background: var(--color-border-secondary);
6029
+ color: var(--color-text-primary);
6030
+ }
6031
+
5849
6032
  .btn-channel-test:disabled,
5850
6033
  .btn-channel-configure:disabled {
5851
6034
  opacity: 0.5;
@@ -6210,6 +6393,40 @@ body.setup-mode[data-theme="dark"] {
6210
6393
  line-height: 1.5;
6211
6394
  }
6212
6395
 
6396
+ /* Restart-failed state — shows both recovery paths (tray + CLI) */
6397
+ .vup-restart-failed {
6398
+ padding: 4px 0 2px;
6399
+ }
6400
+ .vup-restart-failed-title {
6401
+ margin: 0 0 6px;
6402
+ font-size: 13px;
6403
+ font-weight: 600;
6404
+ color: var(--color-error, #ef4444);
6405
+ }
6406
+ .vup-restart-failed-desc {
6407
+ margin: 0 0 8px;
6408
+ font-size: 12px;
6409
+ color: var(--color-text-muted);
6410
+ line-height: 1.5;
6411
+ }
6412
+ .vup-restart-failed-options {
6413
+ margin: 0 0 10px;
6414
+ padding-left: 18px;
6415
+ font-size: 12px;
6416
+ color: var(--color-text-primary);
6417
+ line-height: 1.6;
6418
+ }
6419
+ .vup-restart-failed-options li + li {
6420
+ margin-top: 4px;
6421
+ }
6422
+ .vup-cmd {
6423
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
6424
+ font-size: 11.5px;
6425
+ background: var(--color-surface-muted, rgba(127,127,127,.12));
6426
+ padding: 1px 6px;
6427
+ border-radius: 4px;
6428
+ }
6429
+
6213
6430
 
6214
6431
 
6215
6432
  /* ── Profile / My Data Panel ───────────────────────────────────────────────
@@ -330,7 +330,57 @@ const Modal = (() => {
330
330
  });
331
331
  }
332
332
 
333
- return { confirm, prompt };
333
+ /** Show a rename dialog. Returns a Promise<string|null>. */
334
+ function rename(currentName = "") {
335
+ return new Promise(resolve => {
336
+ const input = $("rename-modal-input");
337
+ input.value = currentName;
338
+ input.classList.remove("input-error");
339
+ $("rename-modal-overlay").style.display = "flex";
340
+
341
+ setTimeout(() => {
342
+ input.focus();
343
+ input.select();
344
+ }, 50);
345
+
346
+ const cleanup = (result) => {
347
+ $("rename-modal-overlay").style.display = "none";
348
+ $("rename-modal-save").onclick = null;
349
+ $("rename-modal-cancel").onclick = null;
350
+ $("rename-modal-overlay").onclick = null;
351
+ input.onkeydown = null;
352
+ input.oninput = null;
353
+ resolve(result);
354
+ };
355
+
356
+ const saveHandler = () => {
357
+ const newName = input.value.trim();
358
+ if (!newName) {
359
+ input.classList.add("input-error");
360
+ input.focus();
361
+ return;
362
+ }
363
+ cleanup(newName === currentName ? null : newName);
364
+ };
365
+
366
+ input.oninput = () => input.classList.remove("input-error");
367
+
368
+ $("rename-modal-save").onclick = saveHandler;
369
+ $("rename-modal-cancel").onclick = () => cleanup(null);
370
+
371
+ input.onkeydown = (e) => {
372
+ if (e.key === "Enter") { e.preventDefault(); saveHandler(); }
373
+ if (e.key === "Escape") cleanup(null);
374
+ };
375
+
376
+ // Close on overlay click
377
+ $("rename-modal-overlay").onclick = (e) => {
378
+ if (e.target.id === "rename-modal-overlay") cleanup(null);
379
+ };
380
+ });
381
+ }
382
+
383
+ return { confirm, prompt, rename };
334
384
  })();
335
385
 
336
386
  // ── Confirmation modal ────────────────────────────────────────────────────
@@ -2,38 +2,65 @@
2
2
  //
3
3
  // Design principle: no configuration forms here.
4
4
  // This page shows platform status only. All setup is done via Agent with browser automation.
5
- // "Auto Setup" opens a chat session with /channel-setup pre-filled — the Agent will use
5
+ // "Auto Setup" opens a chat session with /channel-manager pre-filled — the Agent will use
6
6
  // browser automation to complete the entire setup on the platform's web console.
7
- // "Test" runs /channel-setup doctor via the Agent and streams results.
7
+ // "Test" runs /channel-manager doctor via the Agent and streams results.
8
8
 
9
9
  const Channels = (() => {
10
10
 
11
11
  // Platform display metadata (use accessor to pick up runtime language)
12
+ // SVG sources: dashboard-icons (Lark, multi-color brand mark),
13
+ // TDesign icons (WeCom/WeChat, single-color), simpleicons (Discord/Telegram),
14
+ // ant-design/ant-design-icons outlined (DingTalk).
12
15
  function PLATFORM_META() {
13
16
  return {
14
17
  feishu: {
15
- logo: "",
18
+ logo: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="62.16 94.5 407.87 324.19" aria-hidden="true"><path d="M274.18 264.785q.515-.517 1.03-1.027c.685-.688 1.372-1.258 2.056-1.945l1.37-1.372 4.118-4.113 5.598-5.601 4.8-4.797 4.575-4.457 4.796-4.688 4.344-4.344 6.059-6.054c1.14-1.145 2.285-2.29 3.543-3.317 2.168-2.054 4.457-4 6.855-5.828 2.172-1.715 4.344-3.312 6.516-4.914 3.082-2.172 6.398-4.344 9.71-6.285 3.204-1.941 6.63-3.656 10.06-5.371 3.199-1.602 6.515-2.973 9.827-4.23 1.829-.684 3.774-1.372 5.602-2.055.914-.344 1.941-.688 2.856-.914-8.57-33.715-24.227-64.575-45.258-90.86-4.114-5.14-10.399-8.113-17.028-8.113H130.754c-3.203 0-4.457 4-1.945 5.941 59.543 43.66 109.144 99.887 145.03 164.801 0-.226.227-.34.34-.457m0 0" fill="#00d6b9"/><path d="M204.79 418.691c90.288 0 169.03-49.828 210.058-123.543 1.488-2.628 2.859-5.257 4.23-7.882q-3.087 6-6.86 11.312l-2.741 3.77c-1.141 1.488-2.399 2.972-3.657 4.457-1.03 1.144-2.058 2.285-3.086 3.316-2.058 2.172-4.343 4.227-6.629 6.172a53 53 0 0 1-3.886 3.2c-1.598 1.144-3.086 2.284-4.684 3.429-1.031.683-2.058 1.371-3.086 1.941-1.144.684-2.172 1.258-3.316 1.942a131 131 0 0 1-6.969 3.543c-2.059.918-4.117 1.828-6.289 2.515-2.285.801-4.57 1.602-6.969 2.285-3.543.914-7.086 1.715-10.742 2.286-2.629.457-5.258.687-8 .914-2.86.23-5.601.23-8.457.23-3.086 0-6.289-.23-9.488-.57a83 83 0 0 1-7.086-1.031c-2.055-.34-4.113-.801-6.168-1.258-1.031-.227-2.176-.57-3.203-.797-2.973-.8-6.055-1.602-9.028-2.516-1.488-.457-2.972-.914-4.457-1.258-2.172-.683-4.457-1.37-6.629-2.058-1.828-.57-3.656-1.14-5.37-1.711q-2.573-.86-5.145-1.715c-1.14-.344-2.285-.8-3.543-1.144-1.371-.457-2.856-1.028-4.227-1.485-1.027-.344-2.058-.687-2.972-1.027-1.942-.688-4-1.488-5.942-2.172-1.144-.457-2.285-.914-3.43-1.258-1.484-.57-3.085-1.144-4.57-1.828-1.601-.687-3.203-1.258-4.8-1.945-1.028-.457-2.06-.797-3.087-1.258-1.257-.57-2.628-1.027-3.886-1.598-1.028-.457-1.942-.8-2.969-1.258l-3.086-1.37c-.914-.344-1.832-.801-2.746-1.145a44 44 0 0 1-2.512-1.14c-.8-.345-1.715-.802-2.515-1.145-.914-.344-1.715-.801-2.512-1.141-1.031-.457-2.172-1.031-3.203-1.484-1.14-.575-2.285-1.032-3.426-1.602-1.258-.574-2.402-1.144-3.66-1.715-1.027-.457-2.055-1.027-3.082-1.484-54.172-26.973-102.172-63.086-143.09-106.746-2.055-2.172-5.71-.684-5.71 2.289l.112 154.398v12.57c0 7.317 3.543 14.06 9.598 18.172 38.172 24.801 83.773 39.543 132.914 39.543m0 0" fill="#3370ff"/><path d="M414.84 295.188c0 .113-.113.113-.113.226zl.8-1.489c-.343.457-.574 1.028-.8 1.488m3.793-7.05.226-.457.114-.23q-.17.513-.34.687m0 0" fill="#133c9a"/><path d="M470.035 201.121c-18.285-9.031-38.86-14.059-60.687-14.059-12.914 0-25.485 1.829-37.371 5.141-1.372.344-2.743.8-4.114 1.258-.914.344-1.941.574-2.855.914-1.945.688-3.774 1.375-5.602 2.059-3.316 1.257-6.629 2.742-9.828 4.23-3.43 1.598-6.742 3.426-10.058 5.371a128 128 0 0 0-9.715 6.285c-2.285 1.602-4.457 3.2-6.512 4.914a154 154 0 0 0-6.86 5.828c-1.14 1.141-2.398 2.172-3.542 3.313l-6.055 6.059-4.344 4.343-4.8 4.684-4.57 4.46-4.802 4.798-11.086 11.086c-.687.687-1.37 1.37-2.058 1.945l-1.028 1.027c-.457.457-1.027 1.028-1.601 1.485-.57.57-1.14 1.031-1.711 1.601a244.4 244.4 0 0 1-49.828 35.313c1.027.457 2.168 1.027 3.199 1.488.8.34 1.715.797 2.512 1.14.8.344 1.715.801 2.515 1.145.801.344 1.602.684 2.516 1.14.914.345 1.828.802 2.742 1.145l3.086 1.371c1.027.457 1.942.801 2.969 1.258 1.258.57 2.629 1.028 3.887 1.598 1.03.46 2.058.8 3.086 1.258 1.601.687 3.199 1.258 4.8 1.945 1.485.57 3.086 1.14 4.57 1.828 1.145.457 2.286.914 3.43 1.258 1.946.684 4 1.484 5.946 2.172a81 81 0 0 1 2.968 1.027c1.371.457 2.856 1.028 4.23 1.485 1.141.343 2.286.8 3.544 1.14q2.567.86 5.14 1.719c1.829.57 3.657 1.14 5.372 1.71 2.171.688 4.457 1.376 6.628 2.06 1.489.457 2.973.914 4.457 1.257 2.973.914 5.942 1.715 9.032 2.512 1.027.344 2.168.574 3.199.8 2.055.458 4.113.915 6.172 1.259 2.398.457 4.683.8 7.082 1.03 3.203.34 6.402.571 9.488.571 2.856 0 5.715 0 8.457-.23 2.63-.227 5.371-.457 8-.914 3.656-.57 7.2-1.371 10.742-2.286 2.399-.683 4.688-1.37 6.973-2.285 2.172-.8 4.227-1.601 6.285-2.515 2.399-1.028 4.684-2.285 6.973-3.543 1.14-.57 2.168-1.258 3.312-1.942 1.028-.687 2.059-1.257 3.086-1.945 1.602-1.027 3.2-2.168 4.684-3.426a52 52 0 0 0 3.887-3.203c2.289-1.941 4.457-4 6.628-6.168 1.032-1.031 2.06-2.172 3.086-3.316 1.258-1.485 2.516-2.969 3.657-4.457.918-1.258 1.828-2.512 2.742-3.77 2.515-3.543 4.8-7.316 6.86-11.199l2.284-4.688 21.145-42.171v.113c6.742-14.742 16.226-28.113 27.656-39.426m0 0" fill="#133c9a"/></svg>`,
16
19
  logoClass: "channel-logo-feishu",
17
20
  name: "Feishu / Lark",
18
21
  desc: I18n.t("channels.feishu.desc"),
19
- setupCmd: "/channel-setup setup feishu",
20
- testCmd: "/channel-setup doctor",
22
+ setupCmd: "/channel-manager setup feishu",
23
+ testCmd: "/channel-manager doctor",
21
24
  },
22
25
  wecom: {
23
- logo: "",
26
+ logo: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path fill="#fff" d="m17.326 8.158l-.003-.007a6.6 6.6 0 0 0-1.178-1.674c-1.266-1.307-3.067-2.19-5.102-2.417a9.3 9.3 0 0 0-2.124 0h-.001c-2.061.228-3.882 1.107-5.14 2.405a6.7 6.7 0 0 0-1.194 1.682A5.7 5.7 0 0 0 2 10.657c0 1.106.332 2.218.988 3.201l.006.01c.391.594 1.092 1.39 1.637 1.83l.983.793l-.208.875l.527-.267l.708-.358l.761.225c.467.137.955.227 1.517.29h.005q.515.06 1.026.059c.355 0 .724-.02 1.095-.06a9 9 0 0 0 1.346-.258c.095.7.43 1.337.932 1.81c-.658.208-1.352.358-2.061.436c-.442.048-.883.072-1.312.072q-.627 0-1.253-.072a10.7 10.7 0 0 1-1.861-.36l-2.84 1.438s-.29.131-.44.131c-.418 0-.702-.285-.702-.704c0-.252.067-.598.128-.84l.394-1.653c-.728-.586-1.563-1.544-2.052-2.287A7.76 7.76 0 0 1 0 10.658a7.7 7.7 0 0 1 .787-3.39a8.7 8.7 0 0 1 1.551-2.19c1.61-1.665 3.878-2.73 6.359-3.006a11.3 11.3 0 0 1 2.565 0c2.47.275 4.712 1.353 6.323 3.017a8.6 8.6 0 0 1 1.539 2.192c.466.945.769 1.937.769 2.978a3.06 3.06 0 0 0-2-.005c-.001-.644-.189-1.329-.564-2.09zm4.125 6.977l-.024-.024l-.024-.018l-.024-.018l-.096-.095a4.24 4.24 0 0 1-1.169-2.192q0-.038-.006-.075l-.006-.056l-.035-.144a1.3 1.3 0 0 0-.358-.61a1.386 1.386 0 0 0-1.957 0a1.4 1.4 0 0 0 0 1.963c.191.191.418.311.668.371c.024.012.06.012.084.012q.019 0 .041.006q.023.005.042.006a4.24 4.24 0 0 1 2.231 1.186c.048.048.096.095.131.143a.323.323 0 0 0 .466 0a.35.35 0 0 0 .036-.455m-1.05 4.37l-.025.025c-.119.096-.31.096-.453-.036a.326.326 0 0 1 0-.467c.047-.036.094-.083.141-.13l.002-.002a4.27 4.27 0 0 0 1.187-2.28q.005-.024.006-.043c0-.024 0-.06.012-.084a1.386 1.386 0 0 1 2.326-.67a1.4 1.4 0 0 1 0 1.964c-.167.18-.382.299-.608.359l-.143.036l-.057.005q-.035.006-.075.007a4.2 4.2 0 0 0-2.183 1.173l-.095.096q-.009.01-.018.024t-.018.024m-4.392-1.053l.024.024l.024.018q.015.009.024.018l.096.096a4.25 4.25 0 0 1 1.169 2.19q0 .04.006.076q.005.03.006.057l.035.143c.06.228.18.443.358.611c.537.539 1.42.539 1.957 0a1.4 1.4 0 0 0 0-1.964a1.4 1.4 0 0 0-.668-.371c-.024-.012-.06-.012-.084-.012q-.018 0-.041-.006l-.042-.006a4.25 4.25 0 0 1-2.231-1.185a1.4 1.4 0 0 1-.131-.144a.323.323 0 0 0-.466 0a.325.325 0 0 0-.036.455m1.039-4.358l.024-.024a.32.32 0 0 1 .453.035a.326.326 0 0 1 0 .467c-.047.036-.094.083-.141.13l-.002.002a4.27 4.27 0 0 0-1.187 2.281l-.006.042c0 .024 0 .06-.012.084a1.386 1.386 0 0 1-2.326.67a1.4 1.4 0 0 1 0-1.963c.166-.18.381-.3.608-.36l.143-.035q.026 0 .056-.006q.037-.005.075-.006a4.2 4.2 0 0 0 2.183-1.174l.096-.095l.018-.025z"/></svg>`,
24
27
  logoClass: "channel-logo-wecom",
25
- name: "WeCom (企业微信)",
28
+ name: "WeCom",
26
29
  desc: I18n.t("channels.wecom.desc"),
27
- setupCmd: "/channel-setup setup wecom",
28
- testCmd: "/channel-setup doctor",
30
+ setupCmd: "/channel-manager setup wecom",
31
+ testCmd: "/channel-manager doctor",
29
32
  },
30
33
  weixin: {
31
- logo: "",
34
+ logo: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path fill="#fff" d="M8.796 17.027H8.75c-1.153 0-2.254-.188-3.262-.53L2.65 17.92l.352-2.712C1.162 13.855 0 11.861 0 9.64c0-4.083 3.918-7.39 8.75-7.39c4.174 0 7.665 2.468 8.54 5.77a9 9 0 0 0-.6-.02c-4.364 0-8.19 3.037-8.19 7.11c0 .67.104 1.312.296 1.917M6 8a1 1 0 1 0 0-2a1 1 0 0 0 0 2m5.5.007a1 1 0 1 0 0-2a1 1 0 0 0 0 2"/><path fill="#fff" d="M21.874 19.52C23.187 18.405 24 16.863 24 15.16C24 11.758 20.754 9 16.75 9S9.5 11.758 9.5 15.161s3.246 6.161 7.25 6.161c.95 0 1.856-.155 2.686-.437l2.438 1.407zm-7.564-5.362a1 1 0 1 1 0-2a1 1 0 0 1 0 2m4.88 0a1 1 0 1 1 0-2a1 1 0 0 1 0 2"/></svg>`,
32
35
  logoClass: "channel-logo-weixin",
33
- name: "Weixin (微信)",
36
+ name: "Weixin",
34
37
  desc: I18n.t("channels.weixin.desc"),
35
- setupCmd: "/channel-setup setup weixin",
36
- testCmd: "/channel-setup doctor",
38
+ setupCmd: "/channel-manager setup weixin",
39
+ testCmd: "/channel-manager doctor",
40
+ },
41
+ dingtalk: {
42
+ logo: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" aria-hidden="true"><path fill="#fff" d="M573.7 252.5C422.5 197.4 201.3 96.7 201.3 96.7c-15.7-4.1-17.9 11.1-17.9 11.1c-5 61.1 33.6 160.5 53.6 182.8c19.9 22.3 319.1 113.7 319.1 113.7S326 357.9 270.5 341.9c-55.6-16-37.9 17.8-37.9 17.8c11.4 61.7 64.9 131.8 107.2 138.4c42.2 6.6 220.1 4 220.1 4s-35.5 4.1-93.2 11.9c-42.7 5.8-97 12.5-111.1 17.8c-33.1 12.5 24 62.6 24 62.6c84.7 76.8 129.7 50.5 129.7 50.5c33.3-10.7 61.4-18.5 85.2-24.2L565 743.1h84.6L603 928l205.3-271.9H700.8l22.3-38.7c.3.5.4.8.4.8S799.8 496.1 829 433.8l.6-1h-.1c5-10.8 8.6-19.7 10-25.8c17-71.3-114.5-99.4-265.8-154.5"/></svg>`,
43
+ logoClass: "channel-logo-dingtalk",
44
+ name: "DingTalk",
45
+ desc: I18n.t("channels.dingtalk.desc"),
46
+ setupCmd: "/channel-manager setup dingtalk",
47
+ testCmd: "/channel-manager doctor",
48
+ },
49
+ discord: {
50
+ logo: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path fill="#fff" d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/></svg>`,
51
+ logoClass: "channel-logo-discord",
52
+ name: "Discord",
53
+ desc: I18n.t("channels.discord.desc"),
54
+ setupCmd: "/channel-manager setup discord",
55
+ testCmd: "/channel-manager doctor",
56
+ },
57
+ telegram: {
58
+ logo: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path fill="#fff" d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/></svg>`,
59
+ logoClass: "channel-logo-telegram",
60
+ name: "Telegram",
61
+ desc: I18n.t("channels.telegram.desc"),
62
+ setupCmd: "/channel-manager setup telegram",
63
+ testCmd: "/channel-manager doctor",
37
64
  },
38
65
  };
39
66
  }
@@ -46,10 +73,12 @@ const Channels = (() => {
46
73
 
47
74
  // ── Data Loading ─────────────────────────────────────────────────────────────
48
75
 
49
- async function _load() {
76
+ async function _load({ silent = false } = {}) {
50
77
  const container = $("channels-list");
51
78
  if (!container) return;
52
- container.innerHTML = `<div class="channel-loading">${I18n.t("channels.loading")}</div>`;
79
+ if (!silent) {
80
+ container.innerHTML = `<div class="channel-loading">${I18n.t("channels.loading")}</div>`;
81
+ }
53
82
 
54
83
  try {
55
84
  const res = await fetch("/api/channels");
@@ -77,8 +106,9 @@ const Channels = (() => {
77
106
  }
78
107
 
79
108
  function _renderCard(platform, data, meta) {
80
- const enabled = !!data.enabled;
81
- const running = !!data.running;
109
+ const enabled = !!data.enabled;
110
+ const running = !!data.running;
111
+ const hasConfig = !!data.has_config;
82
112
 
83
113
  const card = document.createElement("div");
84
114
  card.className = "channel-card";
@@ -87,17 +117,20 @@ const Channels = (() => {
87
117
  card.innerHTML = `
88
118
  <div class="channel-card-header">
89
119
  <div class="channel-card-identity">
90
- <span class="channel-logo ${_esc(meta.logoClass)}">${_esc(meta.logo)}</span>
120
+ <span class="channel-logo ${_esc(meta.logoClass)}">${meta.logo}</span>
91
121
  <div>
92
122
  <div class="channel-card-name">${_esc(meta.name)}</div>
93
123
  <div class="channel-card-desc">${_esc(meta.desc)}</div>
94
124
  </div>
95
125
  </div>
96
- <span class="channel-status-badge" id="channel-badge-${_esc(platform)}">${_badgeHtml(enabled, running)}</span>
126
+ <div class="channel-card-status">
127
+ ${hasConfig ? _toggleHtml(platform, enabled) : ""}
128
+ <span class="channel-status-badge" id="channel-badge-${_esc(platform)}">${_badgeHtml(enabled, running, hasConfig)}</span>
129
+ </div>
97
130
  </div>
98
131
 
99
132
  <div class="channel-card-body">
100
- ${_statusHint(enabled, running)}
133
+ ${_statusHint(enabled, running, hasConfig)}
101
134
  </div>
102
135
 
103
136
  <div class="channel-card-footer">
@@ -113,7 +146,7 @@ const Channels = (() => {
113
146
  <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
114
147
  <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
115
148
  </svg>
116
- ${enabled ? I18n.t("channels.btn.reconfigure") : I18n.t("channels.btn.setup")}
149
+ ${hasConfig ? I18n.t("channels.btn.reconfigure") : I18n.t("channels.btn.setup")}
117
150
  </button>
118
151
  </div>
119
152
  </div>
@@ -122,37 +155,74 @@ const Channels = (() => {
122
155
  // Bind events
123
156
  card.querySelector(`#btn-test-${platform}`)?.addEventListener("click", () => _runTest(platform));
124
157
  card.querySelector(`#btn-configure-${platform}`)?.addEventListener("click", () => _openSetup(platform));
158
+ card.querySelector(`#toggle-${platform}`)?.addEventListener("change", (ev) => _onToggle(platform, ev.target));
125
159
 
126
160
  return card;
127
161
  }
128
162
 
163
+ function _toggleHtml(platform, enabled) {
164
+ const aria = I18n.t(enabled ? "channels.toggle.on" : "channels.toggle.off");
165
+ return `
166
+ <label class="toggle-switch" title="${_esc(aria)}">
167
+ <input type="checkbox" id="toggle-${_esc(platform)}" ${enabled ? "checked" : ""} aria-label="${_esc(aria)}">
168
+ <span class="toggle-slider"></span>
169
+ </label>
170
+ `;
171
+ }
172
+
129
173
  // ── Badge & status hint helpers ───────────────────────────────────────────────
130
174
 
131
- function _badgeHtml(enabled, running) {
132
- if (running) return `<span class="badge-running">● ${I18n.t("channels.badge.running")}</span>`;
133
- if (enabled) return `<span class="badge-enabled">● ${I18n.t("channels.badge.enabled")}</span>`;
134
- return `<span class="badge-disabled">○ ${I18n.t("channels.badge.notConfigured")}</span>`;
175
+ function _badgeHtml(enabled, running, hasConfig) {
176
+ if (running) return `<span class="badge-running">● ${I18n.t("channels.badge.running")}</span>`;
177
+ if (enabled) return `<span class="badge-enabled">● ${I18n.t("channels.badge.enabled")}</span>`;
178
+ if (hasConfig) return `<span class="badge-disabled">○ ${I18n.t("channels.badge.disabled")}</span>`;
179
+ return `<span class="badge-disabled">○ ${I18n.t("channels.badge.notConfigured")}</span>`;
135
180
  }
136
181
 
137
- function _statusHint(enabled, running) {
182
+ function _statusHint(enabled, running, hasConfig) {
138
183
  if (running) {
139
184
  return `<p class="channel-status-hint hint-ok">✓ ${I18n.t("channels.hint.running")}</p>`;
140
185
  }
141
186
  if (enabled) {
142
187
  return `<p class="channel-status-hint hint-warn">⚠ ${I18n.t("channels.hint.enabledNotRunning")}</p>`;
143
188
  }
189
+ if (hasConfig) {
190
+ return `<p class="channel-status-hint hint-idle">${I18n.t("channels.hint.disabled")}</p>`;
191
+ }
144
192
  return `<p class="channel-status-hint hint-idle">${I18n.t("channels.hint.notConfigured")}</p>`;
145
193
  }
146
194
 
195
+ // ── Toggle handler ───────────────────────────────────────────────────────────
196
+
197
+ async function _onToggle(platform, checkbox) {
198
+ const desired = checkbox.checked;
199
+ checkbox.disabled = true;
200
+ try {
201
+ const res = await fetch(`/api/channels/${encodeURIComponent(platform)}/enabled`, {
202
+ method: "PATCH",
203
+ headers: { "Content-Type": "application/json" },
204
+ body: JSON.stringify({ enabled: desired }),
205
+ });
206
+ const data = await res.json();
207
+ if (!res.ok || !data.ok) throw new Error(data.error || "toggle failed");
208
+ await _load({ silent: true });
209
+ } catch (e) {
210
+ checkbox.checked = !desired;
211
+ alert("Error: " + e.message);
212
+ } finally {
213
+ checkbox.disabled = false;
214
+ }
215
+ }
216
+
147
217
  // ── Actions ───────────────────────────────────────────────────────────────────
148
218
 
149
- // Run E2E test: open a session and send /channel-setup doctor
219
+ // Run E2E test: open a session and send /channel-manager doctor
150
220
  async function _runTest(platform) {
151
221
  const meta = PLATFORM_META()[platform];
152
222
  await _sendToAgent(meta.testCmd, `Channel E2E Test — ${meta.name}`);
153
223
  }
154
224
 
155
- // Open setup: open a session and send /channel-setup setup <platform>
225
+ // Open setup: open a session and send /channel-manager setup <platform>
156
226
  async function _openSetup(platform) {
157
227
  const meta = PLATFORM_META()[platform];
158
228
  await _sendToAgent(meta.setupCmd, `Channel Setup — ${meta.name}`);