@sunjuntao/dsh-qqbot 0.1.7 → 0.1.13

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 (3) hide show
  1. package/lib/client.js +428 -222
  2. package/lib/index.js +335 -30
  3. package/package.json +4 -6
package/lib/client.js CHANGED
@@ -112,7 +112,7 @@ var cn = Object.freeze({
112
112
  "session.defaultWorkspace": "默认工作区(~/.dsh/file)",
113
113
  "session.modelHint": "这个机器人会话使用的模型;留空则跟随宿主默认模型。切换后已有会话需要重置才会生效。",
114
114
  "session.followDefaultModel": "跟随默认模型",
115
- "session.presetHint": "决定机器人的行事风格与可用工具。@ 机器人和单聊消息都走这个 Preset;群里非 @ 的回复走聊天 Preset(默认跟随本 Preset,仅可在 bots.json 配置),不会执行工具。",
115
+ "session.presetHint": "决定机器人的行事风格与可用工具。@ 机器人、单聊、以及群里非 @ 的回复都走这个 Preset,群聊与私聊能力完全一致,均可调用工具。",
116
116
  // ── status ──
117
117
  "status.disabled": "已停用",
118
118
  "status.connected": "已连接",
@@ -217,7 +217,7 @@ var cn = Object.freeze({
217
217
  "policy.title": "消息与回复策略",
218
218
  "policy.hint": "控制这个机器人「听哪些消息、怎么回」,每个机器人彼此独立。所有开关改完立即生效,不需要重启。",
219
219
  "policy.fullGroupReply": "群全量消息回复",
220
- "policy.fullGroupReplyHint": "开启后,群里没有 @ 机器人的消息也会参与价值评分,达到阈值才回复;@ 机器人的消息始终回复并可使用工具。关闭后,机器人只处理 @ 它的群消息。",
220
+ "policy.fullGroupReplyHint": "开启后,群里没有 @ 机器人的消息也会参与价值评分,达到阈值才回复(与 @、单聊一样可调用工具);@ 机器人的消息始终回复。关闭后,机器人只处理 @ 它的群消息。",
221
221
  "policy.acceptDm": "接受单聊消息",
222
222
  "policy.acceptDmHint": "是否响应 QQ 私聊(C2C)消息。关闭后机器人只处理群消息,私聊一律忽略。",
223
223
  "policy.respondBots": "响应机器人消息",
@@ -232,8 +232,8 @@ var cn = Object.freeze({
232
232
  "policy.quoteScopeAll": "all(群聊全部回复)",
233
233
  "policy.quoteLimit": "引用字数上限",
234
234
  "policy.quoteLimitHint": "文本引用最多显示多少字(仅主动消息回退为文本引用时使用;原生引用气泡由 QQ 客户端自行截断),超出部分以省略号结尾。",
235
- "policy.chatPreset": "群聊聊天 Preset",
236
- "policy.chatPresetHint": "群内非 @ 的全量消息(只聊天、不执行工具)使用的 Preset;留空则跟随上方 Agent Preset。用于让群全量回复风格与 @/单聊区分开。",
235
+ "policy.chatPreset": "群聊聊天 Preset(已停用)",
236
+ "policy.chatPresetHint": "群聊现已与单聊能力完全一致(均可调用工具),不再单独限制群全量消息。该选项已停用,仅供历史配置识别,新配置请勿使用。",
237
237
  "policy.followAgentPreset": "跟随 Agent Preset",
238
238
  "policy.secretEnv": "AppSecret 凭据引用(secretEnv)",
239
239
  "policy.secretEnvShort": "AppSecret 凭据引用",
@@ -419,6 +419,16 @@ var cn = Object.freeze({
419
419
  "sched.enabledToast": "已启用,下次运行 {0}",
420
420
  "sched.enabledNoNext": "已启用,但算不出下次运行时间,请检查触发配置",
421
421
  "sched.disabledToast": "已禁用,不再执行",
422
+ "sched.bg.create": "新建定时消息",
423
+ "sched.bg.update": "修改定时消息",
424
+ "sched.bg.remove": "删除定时消息",
425
+ "sched.bg.enable": "启用定时消息",
426
+ "sched.bg.disable": "禁用定时消息",
427
+ "sched.bg.runOnce": "测试执行定时消息",
428
+ "sched.bg.send": "定时消息发送",
429
+ "sched.bg.sent": "已发送",
430
+ "sched.bg.skipped": "已跳过发送",
431
+ "sched.bg.done": "操作成功",
422
432
  "sched.noNextRun": "算不出下次运行,请检查触发配置",
423
433
  "sched.commandSource": "命令来源",
424
434
  "sched.commandManual": "手写命令",
@@ -608,6 +618,7 @@ var cn = Object.freeze({
608
618
  "update.noNewVersion": "暂无新版本(当前 v{0} 已是最新)",
609
619
  "update.found": "发现新版本 v{0},正在自动更新…",
610
620
  "update.updatedTo": "已自动更新到 v{0}(备份于安装目录 .update-backup/),重启 DSH 后生效",
621
+ "update.keepOpen": "更新尚未执行完,请勿关闭本面板",
611
622
  // ── replyLocale ──
612
623
  "replyLocale.label": "回复语言(replyLocale)",
613
624
  "replyLocale.labelShort": "回复语言",
@@ -659,7 +670,6 @@ var cn = Object.freeze({
659
670
  "group.summaryMarkdown": "Markdown {0}",
660
671
  "group.summaryMemory": "记忆 {0}",
661
672
  "group.summaryBannedWords": "敏感词 {0} 个",
662
- "group.summaryChatPreset": "聊天 Preset {0}",
663
673
  // ── tz ──
664
674
  "tz.shanghai": "中国标准时间 · Asia/Shanghai(UTC+8)",
665
675
  "tz.hongkong": "中国香港 · Asia/Hong_Kong(UTC+8)",
@@ -727,7 +737,6 @@ var cn = Object.freeze({
727
737
  // ── unit ──
728
738
  "unit.minutes": "{0} 分钟",
729
739
  "unit.seconds": "{0} 秒",
730
- "panel.title": "QQ 定时消息",
731
740
  "panel.subtitle": "管理定时消息:增删改查与测试发送",
732
741
  "panel.scope": "机器人",
733
742
  "panel.allBots": "所有机器人",
@@ -753,8 +762,6 @@ var cn = Object.freeze({
753
762
  "panel.bot": "机器人",
754
763
  "panel.archiveAllHint": "所有机器人模式仅显示聚合条数;选择单个机器人可展开预览记录",
755
764
  "panel.tabId": "面板",
756
- "ui.panelSwitchTitle": "右侧面板「QQ 定时消息」入口",
757
- "ui.panelSwitchDesc": "开启后,编辑器右侧栏显示 QQ 定时消息管理入口;关闭即隐藏(仍要求至少一个机器人已连接,且改动即时生效)",
758
765
  "sched.view": "查看",
759
766
  "sched.back": "返回",
760
767
  "sched.detailTitle": "执行逻辑详情",
@@ -848,7 +855,7 @@ var en = Object.freeze({
848
855
  "session.defaultWorkspace": "Default workspace (~/.dsh/file)",
849
856
  "session.modelHint": "The model used by this bot’s sessions; leave empty to follow the host default. Existing sessions need a reset for the change to apply.",
850
857
  "session.followDefaultModel": "Follow default model",
851
- "session.presetHint": "Sets the bot’s behavior style and available tools. @-mentions and direct messages use this Preset; non-@ group replies use the chat preset (follows this Preset by default, configurable only in bots.json) and never run tools.",
858
+ "session.presetHint": "Sets the bot’s behavior style and available tools. @-mentions, direct messages, and non-@ group replies all use this Preset — group and direct chat now have identical capabilities and can both run tools.",
852
859
  // ── status ──
853
860
  "status.disabled": "Disabled",
854
861
  "status.connected": "Connected",
@@ -953,7 +960,7 @@ var en = Object.freeze({
953
960
  "policy.title": "Message & reply policy",
954
961
  "policy.hint": "Controls which messages this bot listens to and how it replies; each bot is independent. Every switch takes effect immediately — no restart needed.",
955
962
  "policy.fullGroupReply": "Reply to all group messages",
956
- "policy.fullGroupReplyHint": "When on, group messages that do not @ the bot also go through value scoring and get a reply only above the threshold; @-mentions are always answered and can use tools. When off, only messages that @ the bot are processed.",
963
+ "policy.fullGroupReplyHint": "When on, group messages that do not @ the bot also go through value scoring and get a reply only above the threshold (same tool capability as @-mentions and DMs); @-mentions are always answered. When off, only messages that @ the bot are processed.",
957
964
  "policy.acceptDm": "Accept direct messages",
958
965
  "policy.acceptDmHint": "Whether to respond to QQ direct (C2C) messages. When off, the bot processes group messages only and ignores DMs.",
959
966
  "policy.respondBots": "Respond to bot messages",
@@ -968,8 +975,8 @@ var en = Object.freeze({
968
975
  "policy.quoteScopeAll": "all (every group reply)",
969
976
  "policy.quoteLimit": "Quote preview length",
970
977
  "policy.quoteLimitHint": "How many characters of the quoted text to show when a reply falls back to a text quote (proactive messages only; native quote cards are truncated by the QQ client itself); longer text ends with an ellipsis.",
971
- "policy.chatPreset": "Group chat preset",
972
- "policy.chatPresetHint": "Preset used for full group messages that do not @ the bot (chat only, no tools); leave empty to follow the Agent Preset above. Use it to give group-wide replies a style distinct from @-mentions and DMs.",
978
+ "policy.chatPreset": "Group chat preset (retired)",
979
+ "policy.chatPresetHint": "Group chat now has identical capabilities to direct chat (both can run tools); the separate group-chat preset restriction is removed. This option is retired for historical config compatibility only — do not use it for new configs.",
973
980
  "policy.followAgentPreset": "Follow Agent Preset",
974
981
  "policy.secretEnv": "AppSecret credential reference (secretEnv)",
975
982
  "policy.secretEnvShort": "AppSecret credential reference",
@@ -1155,6 +1162,16 @@ var en = Object.freeze({
1155
1162
  "sched.enabledToast": "Enabled — next run {0}",
1156
1163
  "sched.enabledNoNext": "Enabled, but the next run time cannot be computed. Check the trigger settings.",
1157
1164
  "sched.disabledToast": "Disabled — will no longer run",
1165
+ "sched.bg.create": "Create scheduled message",
1166
+ "sched.bg.update": "Update scheduled message",
1167
+ "sched.bg.remove": "Delete scheduled message",
1168
+ "sched.bg.enable": "Enable scheduled message",
1169
+ "sched.bg.disable": "Disable scheduled message",
1170
+ "sched.bg.runOnce": "Test-run scheduled message",
1171
+ "sched.bg.send": "Scheduled message send",
1172
+ "sched.bg.sent": "Sent",
1173
+ "sched.bg.skipped": "Send skipped",
1174
+ "sched.bg.done": "Done",
1158
1175
  "sched.noNextRun": "Next run time cannot be computed — check the trigger settings",
1159
1176
  "sched.commandSource": "Command source",
1160
1177
  "sched.commandManual": "Manual command",
@@ -1344,6 +1361,7 @@ var en = Object.freeze({
1344
1361
  "update.noNewVersion": "No new version (v{0} is the latest)",
1345
1362
  "update.found": "New version v{0} found; updating automatically…",
1346
1363
  "update.updatedTo": "Updated to v{0} (old files backed up in .update-backup/ inside the install directory). Restart DSH to take effect.",
1364
+ "update.keepOpen": "Update still running — do not close this panel",
1347
1365
  // ── replyLocale ──
1348
1366
  "replyLocale.label": "Reply language (replyLocale)",
1349
1367
  "replyLocale.labelShort": "Reply language",
@@ -1395,7 +1413,6 @@ var en = Object.freeze({
1395
1413
  "group.summaryMarkdown": "Markdown {0}",
1396
1414
  "group.summaryMemory": "Memory {0}",
1397
1415
  "group.summaryBannedWords": "{0} banned words",
1398
- "group.summaryChatPreset": "Chat preset {0}",
1399
1416
  // ── tz ──
1400
1417
  "tz.shanghai": "China Standard Time · Asia/Shanghai (UTC+8)",
1401
1418
  "tz.hongkong": "Hong Kong, China · Asia/Hong_Kong (UTC+8)",
@@ -1463,7 +1480,6 @@ var en = Object.freeze({
1463
1480
  // ── unit ──
1464
1481
  "unit.minutes": "{0} min",
1465
1482
  "unit.seconds": "{0} s",
1466
- "panel.title": "QQ Schedules",
1467
1483
  "panel.subtitle": "Manage scheduled messages: create, edit, delete and test",
1468
1484
  "panel.scope": "Bot",
1469
1485
  "panel.allBots": "All bots",
@@ -1489,8 +1505,6 @@ var en = Object.freeze({
1489
1505
  "panel.bot": "Bot",
1490
1506
  "panel.archiveAllHint": "All-bots mode shows aggregated counts only; pick a single bot to expand and preview records",
1491
1507
  "panel.tabId": "Tab",
1492
- "ui.panelSwitchTitle": 'Right panel "QQ Scheduled Messages" entry',
1493
- "ui.panelSwitchDesc": "When on, the QQ scheduled-messages manager appears in the right sidebar; off hides it (still requires at least one connected bot, and takes effect immediately)",
1494
1508
  "sched.view": "View",
1495
1509
  "sched.back": "Back",
1496
1510
  "sched.detailTitle": "Execution details",
@@ -1741,8 +1755,52 @@ function QqBotGlyph(props) {
1741
1755
  h("rect", { x: 4.5, y: 6, width: 15, height: 13, rx: 4.5, fill: "currentColor", mask: `url(#qbot-face-${props.uid})` })
1742
1756
  );
1743
1757
  }
1744
- function QqBotGuideIcon(props) {
1745
- return h(QqBotGlyph, { uid: props.uid ?? "guide", className: props.className, size: props.size });
1758
+
1759
+ // src/client/settings-nav-icon.ts
1760
+ var QBOT_SETTINGS_NAV_MARKER = "data-qbot-settings-nav";
1761
+ var NAV_ICON_MASK = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.5v2.5'/%3E%3Ccircle cx='12' cy='2.5' r='.9' fill='black' stroke='none'/%3E%3Crect x='5' y='6' width='14' height='12' rx='3.5'/%3E%3Ccircle cx='9.4' cy='11.5' r='.9' fill='black' stroke='none'/%3E%3Ccircle cx='14.6' cy='11.5' r='.9' fill='black' stroke='none'/%3E%3Cpath d='M9.5 14.8q2.5 1.8 5 0'/%3E%3C/svg%3E";
1762
+ var QBOT_SETTINGS_NAV_CSS = `
1763
+ [${QBOT_SETTINGS_NAV_MARKER}] > svg:first-child {
1764
+ display: none;
1765
+ }
1766
+ [${QBOT_SETTINGS_NAV_MARKER}]::before {
1767
+ content: '';
1768
+ flex: none;
1769
+ width: 16px;
1770
+ height: 16px;
1771
+ background: currentColor;
1772
+ -webkit-mask: url("${NAV_ICON_MASK}") center / contain no-repeat;
1773
+ mask: url("${NAV_ICON_MASK}") center / contain no-repeat;
1774
+ }
1775
+ `;
1776
+ function registerQbotSettingsNavIcon(label) {
1777
+ let disposed = false;
1778
+ const sync = () => {
1779
+ if (disposed) return;
1780
+ const currentLabel = label().trim();
1781
+ const buttons = document.querySelectorAll(
1782
+ '[role="dialog"] nav button'
1783
+ );
1784
+ for (const button of Array.from(buttons)) {
1785
+ const matches2 = currentLabel.length > 0 && button.textContent?.trim() === currentLabel;
1786
+ if (matches2) button.setAttribute(QBOT_SETTINGS_NAV_MARKER, "");
1787
+ else button.removeAttribute(QBOT_SETTINGS_NAV_MARKER);
1788
+ }
1789
+ };
1790
+ sync();
1791
+ const observer = new MutationObserver(sync);
1792
+ observer.observe(document.body, {
1793
+ childList: true,
1794
+ subtree: true,
1795
+ characterData: true
1796
+ });
1797
+ return () => {
1798
+ disposed = true;
1799
+ observer.disconnect();
1800
+ document.querySelectorAll(`[${QBOT_SETTINGS_NAV_MARKER}]`).forEach((element) => {
1801
+ element.removeAttribute(QBOT_SETTINGS_NAV_MARKER);
1802
+ });
1803
+ };
1746
1804
  }
1747
1805
 
1748
1806
  // src/client/ui.tsx
@@ -1990,7 +2048,7 @@ var CSS_TEXT = `
1990
2048
  .qbot-listHeading { min-height: 0; display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 0; }
1991
2049
  .qbot-listHeading h3 { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 14px; line-height: normal; font-weight: 650; }
1992
2050
  .qbot-listTitle { min-width: 0; display: inline-flex; align-items: center; gap: 10px; }
1993
- .qbot-hint { margin: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 1.6; }
2051
+ .qbot-hint { margin: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 1.6; overflow-wrap: anywhere; }
1994
2052
 
1995
2053
  /* ── 通知(dim-statusNotice / info 变体)──────────────────────────────── */
1996
2054
  .qbot-statusNotice { display: flex; align-items: flex-start; gap: 10px; padding: 13px 15px; border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary, #d54941) 22%, var(--dsw-alias-border-l2, #dfe1e5)); border-radius: 10px; color: var(--dsw-alias-state-error-primary, #d54941); background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #d54941) 8%, var(--dsw-alias-bg-layer-1, #fff)); font-size: 13px; line-height: 1.5; }
@@ -1999,6 +2057,10 @@ var CSS_TEXT = `
1999
2057
  .qbot-noticeCount { margin-left: auto; flex: none; align-self: flex-start; font-variant-numeric: tabular-nums; font-weight: 600; font-size: 12px; line-height: 1.4; padding: 1px 9px; border-radius: 999px; white-space: nowrap; background: color-mix(in srgb, var(--qbot-business) 14%, var(--dsw-alias-bg-layer-1, #fff)); border: 1px solid color-mix(in srgb, var(--qbot-business) 28%, var(--dsw-alias-border-l2, #dfe1e5)); }
2000
2058
  /* 页面级更新提示(qbot-page 是普通块布局,无 channelPage 的 flex gap,需自带下边距与面板隔开) */
2001
2059
  .qbot-updateNotice { margin: 0 0 18px; }
2060
+ /* 进行中:不吃 8s 淡出动画(更新可能耗时较久),并改为竖排以突出「请勿关闭」警示行。 */
2061
+ .qbot-updateNotice.is-busy { flex-direction: column; align-items: stretch; gap: 6px; animation: none; }
2062
+ .qbot-updateMsg { min-width: 0; overflow-wrap: anywhere; }
2063
+ .qbot-updateKeepOpen { color: var(--dsw-alias-state-error-primary, #d54941); font-size: 13px; font-weight: 650; line-height: 1.5; }
2002
2064
 
2003
2065
  /* ── 机器人卡片(dim-botCard)─────────────────────────────────────────── */
2004
2066
  .qbot-botList { min-width: 0; width: 100%; max-width: 100%; display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; }
@@ -2159,7 +2221,7 @@ select.qbot-input { cursor: pointer; font-family: inherit; }
2159
2221
  .qbot-dirRow svg { flex: none; width: 16px; height: 16px; color: var(--qbot-business); }
2160
2222
  .qbot-modalState { display: flex; align-items: center; justify-content: center; gap: 10px; min-height: 96px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 13px; }
2161
2223
  .qbot-modalError { color: var(--dsw-alias-state-error-primary, #d54941); }
2162
- .qbot-modalFoot { min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 14px clamp(10px, 3.2%, 20px) 18px; }
2224
+ .qbot-modalFoot { min-width: 0; flex: none; display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 12px clamp(10px, 3.2%, 20px) 14px; font-variant-numeric: tabular-nums; }
2163
2225
  /* 弹窗底部内联错误:常驻可见,不随弹窗内容滚动而移出视口 */
2164
2226
  .qbot-footError { margin: 0; color: var(--dsw-alias-state-error-primary, #d54941); font-size: 12px; line-height: 1.6; overflow-wrap: anywhere; }
2165
2227
  /* 宽弹窗:定时消息 / 消息归档 列表内容较长,放宽上限 */
@@ -2172,7 +2234,7 @@ select.qbot-input { cursor: pointer; font-family: inherit; }
2172
2234
  .qbot-schedTab:hover { background: var(--dsw-alias-interactive-bg-hover, #eef0f3); }
2173
2235
  .qbot-schedTab.is-active { background: var(--dsw-alias-bg-layer-1, #fff); border-color: var(--dsw-alias-border-l2, #e5e6eb); color: var(--qbot-blue); font-weight: 600; box-shadow: 0 1px 2px rgb(31 35 41 / 6%); }
2174
2236
  /* 右侧面板「定时消息」工具栏:机器人作用域 + 刷新(自适应面板宽度、可换行) */
2175
- .qbot-schedToolbar { flex: none; min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; padding: 10px clamp(8px, 3%, 14px); border-bottom: 1px solid var(--dsw-alias-border-l1, #eef0f3); background: var(--dsw-alias-bg-module-platform, #f7f8fa); }
2237
+ .qbot-schedToolbar { flex: none; position: sticky; top: 0; z-index: 2; min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; padding: 10px clamp(8px, 3%, 14px); border-bottom: 1px solid var(--dsw-alias-border-l1, #eef0f3); background: var(--dsw-alias-bg-module-platform, #f7f8fa); }
2176
2238
  .qbot-fieldInline { min-width: 0; flex: 1 1 auto; display: inline-flex; align-items: center; gap: 8px; }
2177
2239
  .qbot-fieldInlineLabel { flex: none; font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-secondary, #646a73); }
2178
2240
  .qbot-schedToolbar .qbot-settingSelect { flex: 1 1 auto; min-width: 0; max-width: 240px; }
@@ -3322,7 +3384,6 @@ function buildDraft(overrides, openid) {
3322
3384
  memoryEnabled: tri(ov.memoryEnabled),
3323
3385
  replyChunkChars: num(ov.replyChunkChars),
3324
3386
  maxRepliesPerMessage: num(ov.maxRepliesPerMessage),
3325
- agentPresetChat: typeof ov.agentPresetChat === "string" ? ov.agentPresetChat : "",
3326
3387
  bannedWords: Array.isArray(ov.bannedWords) ? ov.bannedWords.join(", ") : ""
3327
3388
  };
3328
3389
  }
@@ -3358,7 +3419,6 @@ function OverrideDialog(props) {
3358
3419
  if (d.memoryEnabled) ov.memoryEnabled = d.memoryEnabled === "on";
3359
3420
  if (d.replyChunkChars !== "") ov.replyChunkChars = Number(d.replyChunkChars);
3360
3421
  if (d.maxRepliesPerMessage !== "") ov.maxRepliesPerMessage = Number(d.maxRepliesPerMessage);
3361
- if (d.agentPresetChat.trim()) ov.agentPresetChat = d.agentPresetChat.trim();
3362
3422
  if (d.bannedWords.trim()) {
3363
3423
  ov.bannedWords = d.bannedWords.split(/[,,]/).map((w) => w.trim()).filter(Boolean);
3364
3424
  }
@@ -3546,9 +3606,8 @@ function OverrideDialog(props) {
3546
3606
  h("option", { value: "off" }, t("conn.disable"))
3547
3607
  )
3548
3608
  ),
3549
- // 长期记忆(memoryEnabled)与聊天 Preset(agentPresetChat)已从界面移除
3550
- //(默认常开/留空跟随 Agent Preset,仅 bots.json 可配);
3551
- // 草稿仍读取/回写这两个字段,避免保存时丢掉配置文件里已设置的值。
3609
+ // 长期记忆(memoryEnabled)已从界面移除(默认常开,仅 bots.json 可配);
3610
+ // 草稿仍读取/回写该字段,避免保存时丢掉配置文件里已设置的值。
3552
3611
  editRow(
3553
3612
  t("feature.bannedWords"),
3554
3613
  t("group.bannedWordsHint"),
@@ -3755,6 +3814,32 @@ function nextCronRun(expr, tz, now) {
3755
3814
  return null;
3756
3815
  }
3757
3816
 
3817
+ // src/client/workbench/bg-tasks.ts
3818
+ function getBackgroundTasks() {
3819
+ const wb = window.__dshFileWorkbenchWorkbench__;
3820
+ if (wb && typeof wb.apiVersion === "number" && wb.apiVersion >= 3 && wb.backgroundTasks && typeof wb.backgroundTasks.start === "function") {
3821
+ return wb.backgroundTasks;
3822
+ }
3823
+ return null;
3824
+ }
3825
+ async function runAsBackgroundTask(label, fn, detail) {
3826
+ const api = getBackgroundTasks();
3827
+ if (!api) {
3828
+ await fn();
3829
+ return;
3830
+ }
3831
+ const task = api.start(label, detail ? { detail } : void 0);
3832
+ let outcome = { ok: true };
3833
+ try {
3834
+ const r = await fn();
3835
+ if (r && typeof r === "object") outcome = r;
3836
+ } catch (e) {
3837
+ outcome = { ok: false, message: e instanceof Error ? e.message : String(e) };
3838
+ }
3839
+ if (outcome.ok) task.done(outcome.message);
3840
+ else task.fail(outcome.message);
3841
+ }
3842
+
3758
3843
  // src/client/schedule-manager.tsx
3759
3844
  var WEEKDAYS = [
3760
3845
  { v: 0, label: t("weekday.sun"), en: "Su" },
@@ -3800,6 +3885,12 @@ function localTimeZone() {
3800
3885
  return DEFAULT_TZ;
3801
3886
  }
3802
3887
  }
3888
+ function schedBgDetail(entry) {
3889
+ if (!entry) return void 0;
3890
+ const s = entry.mode === "tool" ? String(entry.command ?? entry.content ?? "") : String(entry.content ?? "");
3891
+ const text = s.trim();
3892
+ return text ? text.slice(0, 60) : void 0;
3893
+ }
3803
3894
  function cmdTemplates() {
3804
3895
  return [
3805
3896
  { label: "Python", cmd: "python C:/scripts/report.py" },
@@ -3894,8 +3985,12 @@ function blankEntry(detailAppId) {
3894
3985
  };
3895
3986
  }
3896
3987
  function ScheduleManager(props) {
3897
- const { rpcCall, detailAppId, forceScope, onClose } = props;
3988
+ const { rpcCall, detailAppId, forceScope, onClose, toast } = props;
3898
3989
  useLocale();
3990
+ const notify = (ok, text) => {
3991
+ if (toast) toast(ok ? "ok" : "error", text);
3992
+ else setSchedNotice({ ok, text });
3993
+ };
3899
3994
  const [scheduleModal, setScheduleModal] = React7.useState({
3900
3995
  loading: true,
3901
3996
  error: "",
@@ -3935,10 +4030,16 @@ function ScheduleManager(props) {
3935
4030
  }
3936
4031
  };
3937
4032
  React7.useEffect(() => {
3938
- if (!schedNotice) return;
4033
+ if (!schedNotice || toast) return;
3939
4034
  const timer = setTimeout(() => setSchedNotice(null), 8e3);
3940
4035
  return () => clearTimeout(timer);
3941
4036
  }, [schedNotice]);
4037
+ React7.useEffect(() => {
4038
+ if (scheduleModal.editError && toast) toast("error", scheduleModal.editError);
4039
+ }, [scheduleModal.editError]);
4040
+ React7.useEffect(() => {
4041
+ if (scheduleModal.error && toast) toast("error", scheduleModal.error);
4042
+ }, [scheduleModal.error]);
3942
4043
  React7.useEffect(() => {
3943
4044
  const s = forceScope ?? "current";
3944
4045
  setScheduleViewing(null);
@@ -4112,22 +4213,39 @@ function ScheduleManager(props) {
4112
4213
  } : {},
4113
4214
  ...e.appId ? { appId: e.appId } : detailAppId ? { appId: detailAppId } : {}
4114
4215
  };
4115
- const res = await rpcCall("schedule.add", payload);
4116
- if (res.ok) {
4117
- setScheduleModal((prev) => prev ? { ...prev, editing: null, saving: false } : prev);
4118
- await loadSchedules(scheduleModal?.botScope ?? "current");
4119
- } else {
4120
- setScheduleModal((prev) => prev ? { ...prev, saving: false, editError: errText(res.error) } : prev);
4121
- }
4216
+ await runAsBackgroundTask(
4217
+ e.id ? t("sched.bg.update") : t("sched.bg.create"),
4218
+ async () => {
4219
+ const res = await rpcCall("schedule.add", payload);
4220
+ if (res.ok) {
4221
+ setScheduleModal((prev) => prev ? { ...prev, editing: null, saving: false } : prev);
4222
+ await loadSchedules(scheduleModal?.botScope ?? "current");
4223
+ return { ok: true, message: t("sched.bg.done") };
4224
+ }
4225
+ setScheduleModal((prev) => prev ? { ...prev, saving: false, editError: errText(res.error) } : prev);
4226
+ return { ok: false, message: errText(res.error) };
4227
+ },
4228
+ String(e.content ?? "").trim().slice(0, 60) || void 0
4229
+ );
4122
4230
  };
4123
4231
  const removeSchedule = async (id) => {
4124
4232
  if (!await confirmDlg({ message: t("sched.removeTaskConfirm"), danger: true })) return;
4125
4233
  setScheduleRemoving(id);
4126
4234
  const scopeNow = scheduleModal?.botScope ?? "current";
4127
4235
  try {
4128
- const res = await rpcCall("schedule.remove", { id });
4129
- if (res.ok) await loadSchedules(scopeNow);
4130
- else setScheduleModal((prev) => prev ? { ...prev, error: errText(res.error) } : prev);
4236
+ await runAsBackgroundTask(
4237
+ t("sched.bg.remove"),
4238
+ async () => {
4239
+ const res = await rpcCall("schedule.remove", { id });
4240
+ if (res.ok) {
4241
+ await loadSchedules(scopeNow);
4242
+ return { ok: true, message: t("sched.bg.done") };
4243
+ }
4244
+ notify(false, errText(res.error));
4245
+ return { ok: false, message: errText(res.error) };
4246
+ },
4247
+ schedBgDetail(scheduleModal?.items.find((x) => String(x.id) === id))
4248
+ );
4131
4249
  } finally {
4132
4250
  setScheduleRemoving("");
4133
4251
  }
@@ -4144,15 +4262,25 @@ function ScheduleManager(props) {
4144
4262
  setScheduleToggling(id);
4145
4263
  const scopeNow = scheduleModal?.botScope ?? "current";
4146
4264
  try {
4147
- const res = await rpcCall("schedule.setEnabled", { id, enabled: !currentlyEnabled });
4148
- if (res.ok) {
4149
- const s = val(res) ?? {};
4150
- const nextAt = s.schedule?.nextRunAt ?? s.nextRunAt;
4151
- setSchedNotice(
4152
- currentlyEnabled ? { ok: true, text: t("sched.disabledToast") } : nextAt ? { ok: true, text: fmt("sched.enabledToast", formatTime(nextAt)) } : { ok: false, text: t("sched.enabledNoNext") }
4153
- );
4154
- await loadSchedules(scopeNow);
4155
- } else setScheduleModal((prev) => prev ? { ...prev, error: errText(res.error) } : prev);
4265
+ await runAsBackgroundTask(
4266
+ currentlyEnabled ? t("sched.bg.disable") : t("sched.bg.enable"),
4267
+ async () => {
4268
+ const res = await rpcCall("schedule.setEnabled", { id, enabled: !currentlyEnabled });
4269
+ if (!res.ok) {
4270
+ notify(false, errText(res.error));
4271
+ return { ok: false, message: errText(res.error) };
4272
+ }
4273
+ const s = val(res) ?? {};
4274
+ const nextAt = s.schedule?.nextRunAt ?? s.nextRunAt;
4275
+ notify(
4276
+ currentlyEnabled ? true : nextAt ? true : false,
4277
+ currentlyEnabled ? t("sched.disabledToast") : nextAt ? fmt("sched.enabledToast", formatTime(nextAt)) : t("sched.enabledNoNext")
4278
+ );
4279
+ await loadSchedules(scopeNow);
4280
+ return { ok: true, message: t("sched.bg.done") };
4281
+ },
4282
+ schedBgDetail(scheduleModal?.items.find((x) => String(x.id) === id))
4283
+ );
4156
4284
  } finally {
4157
4285
  setScheduleToggling("");
4158
4286
  }
@@ -4161,13 +4289,23 @@ function ScheduleManager(props) {
4161
4289
  if (!id) return;
4162
4290
  setSchedNotice(null);
4163
4291
  setScheduleTesting(id);
4164
- const res = await rpcCall("schedule.runOnce", { id });
4165
- setScheduleTesting("");
4166
- if (res.ok) {
4167
- const v = val(res) ?? {};
4168
- setSchedNotice({ ok: true, text: typeof v.message === "string" ? v.message : t("sched.testSent") });
4169
- } else {
4170
- setSchedNotice({ ok: false, text: errText(res.error) });
4292
+ try {
4293
+ await runAsBackgroundTask(
4294
+ t("sched.bg.runOnce"),
4295
+ async () => {
4296
+ const res = await rpcCall("schedule.runOnce", { id });
4297
+ if (res.ok) {
4298
+ const v = val(res) ?? {};
4299
+ notify(true, typeof v.message === "string" ? v.message : t("sched.testSent"));
4300
+ return { ok: true, message: typeof v.message === "string" ? v.message : t("sched.testSent") };
4301
+ }
4302
+ notify(false, errText(res.error));
4303
+ return { ok: false, message: errText(res.error) };
4304
+ },
4305
+ schedBgDetail(scheduleModal?.items.find((x) => String(x.id) === id))
4306
+ );
4307
+ } finally {
4308
+ setScheduleTesting("");
4171
4309
  }
4172
4310
  await loadSchedules(scheduleModal?.botScope ?? "current");
4173
4311
  };
@@ -4642,7 +4780,7 @@ function ScheduleManager(props) {
4642
4780
  ),
4643
4781
  h("div", { className: "qbot-schedDetailBody" }, h("div", { className: "qbot-flow" }, ...buildFlow(e)))
4644
4782
  );
4645
- const ROOT = { display: "flex", flexDirection: "column", height: "100%", minHeight: 0, boxSizing: "border-box" };
4783
+ const ROOT = { display: "flex", flexDirection: "column", flex: "1 1 auto", minHeight: 0, boxSizing: "border-box" };
4646
4784
  return h(
4647
4785
  "div",
4648
4786
  { className: "qbot-schedRoot", style: ROOT },
@@ -4653,6 +4791,7 @@ function ScheduleManager(props) {
4653
4791
  h(
4654
4792
  "div",
4655
4793
  { className: "qbot-editForm qbot-schedForm" },
4794
+ scheduleModal.editError && !toast ? h("div", { key: "editErr", className: "qbot-schedNotice is-error" }, scheduleModal.editError) : null,
4656
4795
  section(
4657
4796
  t("sched.form.stepRecipient"),
4658
4797
  t("sched.form.stepRecipientHint"),
@@ -4805,7 +4944,7 @@ function ScheduleManager(props) {
4805
4944
  "div",
4806
4945
  { className: `qbot-modalList${scheduleModal.loading && scheduleModal.items.length > 0 ? " is-refreshing" : ""}` },
4807
4946
  [
4808
- schedNotice ? h(
4947
+ schedNotice && !toast ? h(
4809
4948
  "div",
4810
4949
  { key: "notice", className: `qbot-schedNotice is-autoHide${schedNotice.ok ? "" : " is-error"}` },
4811
4950
  schedNotice.text
@@ -4847,7 +4986,11 @@ function ScheduleManager(props) {
4847
4986
  h(
4848
4987
  "div",
4849
4988
  { key: "scroll", className: "qbot-modalListScroll" },
4850
- scheduleModal.error ? null : scheduleModal.items.length === 0 ? scheduleModal.loading ? h(
4989
+ scheduleModal.error ? h(
4990
+ "div",
4991
+ { key: "listErr", className: "qbot-schedNotice is-error" },
4992
+ scheduleModal.error
4993
+ ) : scheduleModal.items.length === 0 ? scheduleModal.loading ? h(
4851
4994
  "div",
4852
4995
  { key: "loading", className: "qbot-modalState" },
4853
4996
  h("span", { className: "qbot-spinner", "aria-hidden": "true" }),
@@ -5024,7 +5167,6 @@ function ScheduleManager(props) {
5024
5167
  h(
5025
5168
  "div",
5026
5169
  { className: "qbot-viewActions" },
5027
- h("button", { className: "qbot-btn", type: "button", disabled: scheduleModal.loading, onClick: () => void loadSchedules(scheduleModal.botScope) }, t("common.refresh")),
5028
5170
  onClose ? h("button", { className: "qbot-btn qbot-btnPrimary", type: "button", onClick: onClose }, t("common.close")) : null
5029
5171
  )
5030
5172
  ) : null
@@ -5177,41 +5319,16 @@ function setRpcCall(fn) {
5177
5319
  function getRpcCall() {
5178
5320
  return rpcCallRef;
5179
5321
  }
5180
- var botsListeners = /* @__PURE__ */ new Set();
5181
- function onBotsChanged(fn) {
5182
- botsListeners.add(fn);
5183
- return () => botsListeners.delete(fn);
5184
- }
5185
- function notifyBotsChanged() {
5186
- for (const fn of botsListeners) {
5187
- try {
5188
- fn();
5189
- } catch {
5190
- }
5191
- }
5192
- }
5193
- var PANEL_VISIBLE_KEY = "dsh-qqbot.panel.visible";
5194
- function getPanelVisible() {
5195
- try {
5196
- return localStorage.getItem(PANEL_VISIBLE_KEY) !== "0";
5197
- } catch {
5198
- return true;
5199
- }
5200
- }
5201
- function setPanelVisible(v) {
5202
- try {
5203
- localStorage.setItem(PANEL_VISIBLE_KEY, v ? "1" : "0");
5204
- } catch {
5205
- }
5206
- notifyBotsChanged();
5207
- }
5322
+
5323
+ // src/client/workbench/schedule-view.tsx
5324
+ var import_client = require("react-dom/client");
5208
5325
 
5209
5326
  // src/client/right-panel/ScheduleTab.tsx
5210
5327
  var React9 = __toESM(require("react"), 1);
5211
5328
  var root = {
5212
5329
  display: "flex",
5213
5330
  flexDirection: "column",
5214
- height: "100%",
5331
+ flex: "1 1 auto",
5215
5332
  minHeight: 0,
5216
5333
  boxSizing: "border-box",
5217
5334
  color: "inherit"
@@ -5222,7 +5339,8 @@ var body = {
5222
5339
  display: "flex",
5223
5340
  flexDirection: "column"
5224
5341
  };
5225
- function ScheduleTab() {
5342
+ function ScheduleTab(props) {
5343
+ const { toast } = props ?? {};
5226
5344
  useLocale();
5227
5345
  const [bots, setBots] = React9.useState([]);
5228
5346
  const [scopeAppId, setScopeAppId] = React9.useState("");
@@ -5294,35 +5412,197 @@ function ScheduleTab() {
5294
5412
  botError ? h("div", { className: "qbot-schedPanelError" }, botError) : rpc ? h(
5295
5413
  "div",
5296
5414
  { style: body, key: scopeAppId || "all" },
5297
- h(ScheduleManager, { rpcCall: rpc, detailAppId, forceScope })
5415
+ h(ScheduleManager, { rpcCall: rpc, detailAppId, forceScope, toast })
5298
5416
  ) : h("div", { className: "qbot-modalState" }, t("panel.loading"))
5299
5417
  );
5300
5418
  }
5301
5419
 
5302
- // src/client/right-panel/TitleView.tsx
5303
- var import_jsx_runtime = require("react/jsx-runtime");
5304
- var wrap = {
5305
- display: "inline-flex",
5306
- alignItems: "center",
5307
- gap: 6,
5308
- overflow: "hidden",
5309
- whiteSpace: "nowrap"
5310
- };
5311
- function TitleView() {
5312
- useLocale();
5313
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: wrap, children: [
5314
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(QqBotGlyph, { uid: "panel-chip", size: 15, className: "qbot-panelMark" }),
5315
- t("panel.title")
5316
- ] });
5420
+ // src/client/workbench/schedule-view.tsx
5421
+ var QQBOT_WORKBENCH_VIEW_ID = "@sunjuntao/dsh-qqbot.schedule";
5422
+ function getWorkbenchApi() {
5423
+ try {
5424
+ const w = window;
5425
+ const api = w.__dshFileWorkbenchWorkbench__;
5426
+ if (api && api.activityBar && typeof api.activityBar.register === "function") return api;
5427
+ } catch {
5428
+ }
5429
+ return null;
5430
+ }
5431
+ var installed = false;
5432
+ function installScheduleWorkbenchView() {
5433
+ if (installed) return;
5434
+ installed = true;
5435
+ const attempt = (n) => {
5436
+ const api = getWorkbenchApi();
5437
+ if (api) {
5438
+ api.activityBar.register({
5439
+ id: QQBOT_WORKBENCH_VIEW_ID,
5440
+ title: { zh: "QQ 定时消息", en: "QQ Scheduled Messages" },
5441
+ icon: "clock",
5442
+ order: 60,
5443
+ mount(el, ctx) {
5444
+ el.style.minHeight = "0";
5445
+ el.style.overflowY = "auto";
5446
+ el.style.overflowX = "hidden";
5447
+ el.style.position = "relative";
5448
+ el.style.boxSizing = "border-box";
5449
+ const onWheel = (e) => {
5450
+ if (!e.deltaY) return;
5451
+ const max = el.scrollHeight - el.clientHeight;
5452
+ if (max <= 1) return;
5453
+ const atTop = el.scrollTop <= 0;
5454
+ const atBottom = el.scrollTop >= max - 1;
5455
+ if (e.deltaY < 0 && atTop || e.deltaY > 0 && atBottom) return;
5456
+ const unit = e.deltaMode === 1 ? 18 : e.deltaMode === 2 ? el.clientHeight : 1;
5457
+ el.scrollTop += e.deltaY * unit;
5458
+ e.preventDefault();
5459
+ e.stopPropagation();
5460
+ };
5461
+ el.addEventListener("wheel", onWheel, { passive: false, capture: true });
5462
+ const KEY = "__qbotSchedReact__";
5463
+ const store = el;
5464
+ let entry = store[KEY];
5465
+ let container;
5466
+ let root2;
5467
+ if (entry && entry.root) {
5468
+ container = entry.container;
5469
+ root2 = entry.root;
5470
+ } else {
5471
+ container = document.createElement("div");
5472
+ container.className = "qbot-schedReactRoot";
5473
+ container.style.cssText = "min-height:100%;width:100%;box-sizing:border-box";
5474
+ el.appendChild(container);
5475
+ root2 = (0, import_client.createRoot)(container);
5476
+ store[KEY] = { root: root2, container };
5477
+ }
5478
+ root2.render(
5479
+ h(ScheduleTab, {
5480
+ toast: typeof ctx?.toast === "function" ? (lvl, msg) => ctx.toast(lvl, msg) : void 0
5481
+ })
5482
+ );
5483
+ return () => {
5484
+ el.removeEventListener("wheel", onWheel, { capture: true });
5485
+ try {
5486
+ store[KEY] = void 0;
5487
+ } catch {
5488
+ }
5489
+ try {
5490
+ root2.unmount();
5491
+ } catch {
5492
+ }
5493
+ try {
5494
+ container.remove();
5495
+ } catch {
5496
+ }
5497
+ };
5498
+ }
5499
+ });
5500
+ return;
5501
+ }
5502
+ if (n < 150) window.setTimeout(() => attempt(n + 1), 200);
5503
+ };
5504
+ attempt(0);
5505
+ }
5506
+
5507
+ // src/client/workbench/sched-send-tasks.ts
5508
+ var WS_PATH = "/qqbot-settings/events";
5509
+ var RECONNECT_MAX_MS = 3e4;
5510
+ var installed2 = false;
5511
+ function installSchedSendTasks() {
5512
+ if (installed2 || typeof window === "undefined") return;
5513
+ installed2 = true;
5514
+ connectWithBackoff();
5515
+ }
5516
+ function connectWithBackoff() {
5517
+ let backoff = 1e3;
5518
+ const open = () => {
5519
+ let ws;
5520
+ try {
5521
+ ws = new WebSocket(`${location.protocol === "https:" ? "wss:" : "ws:"}//${location.host}${WS_PATH}`);
5522
+ } catch {
5523
+ scheduleReconnect();
5524
+ return;
5525
+ }
5526
+ ws.onopen = () => {
5527
+ backoff = 1e3;
5528
+ };
5529
+ ws.onmessage = (ev) => {
5530
+ handleFrame(typeof ev.data === "string" ? ev.data : "");
5531
+ };
5532
+ ws.onclose = () => scheduleReconnect();
5533
+ ws.onerror = () => {
5534
+ try {
5535
+ ws.close();
5536
+ } catch {
5537
+ }
5538
+ };
5539
+ };
5540
+ const scheduleReconnect = () => {
5541
+ setTimeout(open, backoff);
5542
+ backoff = Math.min(backoff * 2, RECONNECT_MAX_MS);
5543
+ };
5544
+ open();
5545
+ }
5546
+ function handleFrame(text) {
5547
+ if (!text) return;
5548
+ let event = "";
5549
+ let data = null;
5550
+ try {
5551
+ const parsed = JSON.parse(text);
5552
+ event = typeof parsed.event === "string" ? parsed.event : "";
5553
+ data = parsed.data ?? null;
5554
+ } catch {
5555
+ return;
5556
+ }
5557
+ if (event !== "sched-send") return;
5558
+ const info = data;
5559
+ if (!info || typeof info.summary !== "string") return;
5560
+ const api = getBackgroundTasks();
5561
+ if (!api) return;
5562
+ const detail = info.appId ? `${info.appId} · ${info.summary}` : info.summary;
5563
+ const task = api.start(t("sched.bg.send"), { detail });
5564
+ if (info.skipped) {
5565
+ task.done(info.reason ?? t("sched.bg.skipped"));
5566
+ } else if (info.ok) {
5567
+ task.done(t("sched.bg.sent"));
5568
+ } else {
5569
+ task.fail(info.reason ?? "failed");
5570
+ }
5317
5571
  }
5318
5572
 
5319
5573
  // src/client/index.tsx
5320
5574
  var name = "qqbot-settings";
5321
- var inject = ["slots", "connection", "locale", "sidebarRightTabs", "sidebarRight"];
5575
+ var inject = ["slots", "connection", "locale"];
5322
5576
  var RPC_CHANNEL = "/qqbot-settings";
5323
- var QQBOT_PANEL_ID = "@sunjuntao/dsh-qqbot";
5324
- var QQBOT_PANEL_KIND = "qqbot-sched";
5325
5577
  var NOTICE_TTL_SECONDS = 8;
5578
+ var UPDATE_IDLE = { busy: false, message: "", done: false };
5579
+ var updateStore = /* @__PURE__ */ (() => {
5580
+ let current = UPDATE_IDLE;
5581
+ const listeners2 = /* @__PURE__ */ new Set();
5582
+ return {
5583
+ snapshot() {
5584
+ return current;
5585
+ },
5586
+ publish(next) {
5587
+ current = next.busy ? next : UPDATE_IDLE;
5588
+ for (const listener of [...listeners2]) {
5589
+ try {
5590
+ listener(next);
5591
+ } catch {
5592
+ }
5593
+ }
5594
+ },
5595
+ subscribe(listener) {
5596
+ listeners2.add(listener);
5597
+ return () => {
5598
+ listeners2.delete(listener);
5599
+ };
5600
+ }
5601
+ };
5602
+ })();
5603
+ function publishUpdate(next) {
5604
+ updateStore.publish(next);
5605
+ }
5326
5606
  function QqbotSettingsTab({ rpcCall }) {
5327
5607
  const [status, setStatus] = React10.useState(null);
5328
5608
  const [bots, setBots] = React10.useState(null);
@@ -5338,12 +5618,9 @@ function QqbotSettingsTab({ rpcCall }) {
5338
5618
  const [loadError, setLoadError] = React10.useState("");
5339
5619
  const [refreshing, setRefreshing] = React10.useState(false);
5340
5620
  const [reconnecting, setReconnecting] = React10.useState(false);
5341
- const [panelVisible, setPanelVisibleState] = React10.useState(() => getPanelVisible());
5342
- const [update, setUpdate] = React10.useState({
5343
- busy: false,
5344
- message: "",
5345
- done: false
5346
- });
5621
+ const [update, setLocalUpdate] = React10.useState(() => updateStore.snapshot());
5622
+ const setUpdate = publishUpdate;
5623
+ React10.useEffect(() => updateStore.subscribe(setLocalUpdate), []);
5347
5624
  const refresh = React10.useCallback(async (appId) => {
5348
5625
  try {
5349
5626
  const [s, c, b, cat] = await Promise.all([
@@ -5359,7 +5636,6 @@ function QqbotSettingsTab({ rpcCall }) {
5359
5636
  setForm({ ...stored });
5360
5637
  }
5361
5638
  if (b.ok) setBots(val(b) ?? null);
5362
- notifyBotsChanged();
5363
5639
  if (cat.ok) setCatalogs({ ...EMPTY_CATALOGS, ...val(cat) ?? {} });
5364
5640
  } catch (error) {
5365
5641
  setLoadError(error instanceof Error ? error.message : String(error));
@@ -5488,9 +5764,9 @@ function QqbotSettingsTab({ rpcCall }) {
5488
5764
  };
5489
5765
  React10.useEffect(() => {
5490
5766
  if (!update.message || update.busy) return void 0;
5491
- const timer = setTimeout(() => setUpdate((u) => ({ ...u, message: "" })), 8e3);
5767
+ const timer = setTimeout(() => publishUpdate({ ...update, message: "" }), NOTICE_TTL_SECONDS * 1e3);
5492
5768
  return () => clearTimeout(timer);
5493
- }, [update.message, update.busy]);
5769
+ }, [update]);
5494
5770
  const groupOverrides = form.groupOverrides && typeof form.groupOverrides === "object" && !Array.isArray(form.groupOverrides) ? form.groupOverrides : {};
5495
5771
  const overrideSummary = (ov) => {
5496
5772
  const parts = [];
@@ -5504,7 +5780,6 @@ function QqbotSettingsTab({ rpcCall }) {
5504
5780
  if (ov.markdownReply !== void 0) parts.push(fmt("group.summaryMarkdown", ov.markdownReply ? t("sched.form.onShort") : t("sched.form.offShort")));
5505
5781
  if (ov.memoryEnabled !== void 0) parts.push(fmt("group.summaryMemory", ov.memoryEnabled ? t("sched.form.onShort") : t("sched.form.offShort")));
5506
5782
  if (Array.isArray(ov.bannedWords) && ov.bannedWords.length > 0) parts.push(fmt("group.summaryBannedWords", ov.bannedWords.length));
5507
- if (typeof ov.agentPresetChat === "string" && ov.agentPresetChat) parts.push(fmt("group.summaryChatPreset", ov.agentPresetChat));
5508
5783
  return parts.length > 0 ? parts.join(" · ") : t("group.noOverrides");
5509
5784
  };
5510
5785
  const removeOverride = async (openid) => {
@@ -5663,40 +5938,6 @@ function QqbotSettingsTab({ rpcCall }) {
5663
5938
  )
5664
5939
  ) : null,
5665
5940
  h("p", { className: "qbot-hint" }, t("list.cardHint"))
5666
- ),
5667
- // ── 界面偏好:右侧面板「QQ 定时消息」入口开关(客户端本地设置) ──
5668
- h(
5669
- "div",
5670
- { className: "qbot-surfaceCard" },
5671
- h(
5672
- "div",
5673
- { className: "qbot-surfaceBody" },
5674
- h(
5675
- "label",
5676
- { className: "qbot-switchRow" },
5677
- h(
5678
- "span",
5679
- { className: "qbot-switchText" },
5680
- h("strong", null, t("ui.panelSwitchTitle")),
5681
- h(
5682
- "p",
5683
- { style: { margin: "2px 0 0", fontSize: "12px", color: "var(--dsw-alias-label-tertiary, #8f959e)" } },
5684
- t("ui.panelSwitchDesc")
5685
- )
5686
- ),
5687
- h("input", {
5688
- type: "checkbox",
5689
- className: "qbot-switch",
5690
- checked: panelVisible,
5691
- onChange: (e) => {
5692
- const v = Boolean(e.target.checked);
5693
- setPanelVisibleState(v);
5694
- setPanelVisible(v);
5695
- },
5696
- "aria-label": t("ui.panelSwitchTitle")
5697
- })
5698
- )
5699
- )
5700
5941
  )
5701
5942
  );
5702
5943
  const wsInfo = detailBot ? {
@@ -5965,8 +6206,6 @@ function QqbotSettingsTab({ rpcCall }) {
5965
6206
  "aria-label": "Agent Preset"
5966
6207
  }, presetOptions(catalogs.agentPresets).map((o) => h("option", { key: o.value, value: o.value }, o.label)))
5967
6208
  })
5968
- // 群聊聊天 Preset(agentPresetChat)与 AppSecret 凭据引用(secretEnv)已从界面移除,
5969
- // 仅通过 bots.json 配置——默认 agentPresetChat 留空即跟随上方 Agent Preset(见 rule.ts)。
5970
6209
  )
5971
6210
  ),
5972
6211
  // ── 消息与回复策略(开关) ──
@@ -6179,7 +6418,7 @@ function QqbotSettingsTab({ rpcCall }) {
6179
6418
  "div",
6180
6419
  { className: "qbot-brandHeading" },
6181
6420
  h("strong", { className: "qbot-brandName" }, t("app.title")),
6182
- h("span", { className: "qbot-brandVersion" }, `v${true ? "0.1.7" : "0.0.2"}`),
6421
+ h("span", { className: "qbot-brandVersion" }, `v${true ? "0.1.13" : "0.0.2"}`),
6183
6422
  h("button", {
6184
6423
  className: `qbot-btn qbot-updateBtn${update.done ? " is-done" : ""}`,
6185
6424
  type: "button",
@@ -6193,7 +6432,19 @@ function QqbotSettingsTab({ rpcCall }) {
6193
6432
  ),
6194
6433
  h("div", { className: "qbot-titleActions" }, globalBadge)
6195
6434
  ),
6196
- update.message ? h("div", { className: "qbot-infoNotice qbot-updateNotice", role: "status" }, update.message) : null,
6435
+ // 更新期间(busy)的提示条不自动淡出,并额外给出「请勿关闭」警示:
6436
+ // update.apply 会下载并覆盖安装目录,中途切走 / 关闭面板不影响宿主执行,
6437
+ // 但用户看不到结果、也容易误以为卡死,所以显式告知。
6438
+ update.message ? h(
6439
+ "div",
6440
+ {
6441
+ className: `qbot-infoNotice qbot-updateNotice${update.busy ? " is-busy" : ""}`,
6442
+ role: "status",
6443
+ "aria-busy": update.busy ? "true" : "false"
6444
+ },
6445
+ h("span", { className: "qbot-updateMsg" }, update.message),
6446
+ update.busy ? h("strong", { className: "qbot-updateKeepOpen" }, t("update.keepOpen")) : null
6447
+ ) : null,
6197
6448
  h(
6198
6449
  "div",
6199
6450
  { className: "qbot-panel", id: "qbot-panel" },
@@ -6270,82 +6521,37 @@ function apply(ctx) {
6270
6521
  };
6271
6522
  setRpcCall(rpcCall);
6272
6523
  ctx.effect(() => installStyles(), "qqbot-settings: styles");
6524
+ ctx.effect(
6525
+ () => {
6526
+ let style = document.getElementById("qbot-settings-nav-style");
6527
+ if (!style) {
6528
+ style = document.createElement("style");
6529
+ style.id = "qbot-settings-nav-style";
6530
+ style.textContent = QBOT_SETTINGS_NAV_CSS;
6531
+ document.head.appendChild(style);
6532
+ }
6533
+ const disposeMarker = registerQbotSettingsNavIcon(() => t("app.title"));
6534
+ return () => {
6535
+ disposeMarker();
6536
+ style?.remove();
6537
+ };
6538
+ },
6539
+ "qqbot-settings: settings navigation icon"
6540
+ );
6273
6541
  ctx.slots.inject("settings.section", () => ctx.slots.register(
6274
6542
  {
6275
6543
  name: "settings.section",
6276
6544
  id: "dsh-qqbot",
6277
6545
  order: 31,
6278
- label: () => h(
6279
- "span",
6280
- { className: "qbot-navLabel" },
6281
- h(QqBotGlyph, { className: "qbot-navGlyph", uid: "nav" }),
6282
- h("span", { className: "qbot-navText" }, t("app.title"))
6283
- ),
6546
+ // 宿主新版导航行自带系统图标(navIcon),label 契约是纯文本;
6547
+ // 这里不再内嵌 QqBotGlyph,否则会和宿主图标重复。
6548
+ label: () => t("app.title"),
6284
6549
  inject: () => ({ rpcCall })
6285
6550
  },
6286
6551
  QqbotSettingsTab
6287
6552
  ));
6288
- let disposePanelTab = null;
6289
- const registerPanelTab = () => {
6290
- if (disposePanelTab) return;
6291
- disposePanelTab = ctx.sidebarRightTabs.register({
6292
- id: QQBOT_PANEL_ID,
6293
- kind: QQBOT_PANEL_KIND,
6294
- // title / guide 由宿主在打开面板时求值,这里每次调用都走 i18n,切换语言后即为当前语言。
6295
- title: () => t("panel.title"),
6296
- guide: [
6297
- {
6298
- order: 100,
6299
- title: () => t("panel.title"),
6300
- description: () => t("panel.subtitle"),
6301
- // 入口胶囊的图标;不传则宿主画占位方块。用插件统一的 QQ 机器人 glyph。
6302
- icon: QqBotGuideIcon
6303
- }
6304
- ]
6305
- });
6306
- };
6307
- const unregisterPanelTab = () => {
6308
- try {
6309
- disposePanelTab?.();
6310
- } catch {
6311
- }
6312
- disposePanelTab = null;
6313
- };
6314
- const syncPanelTab = async () => {
6315
- try {
6316
- const res = await rpcCall("bots.list");
6317
- const bots = res.ok ? val(res)?.bots : void 0;
6318
- const anyConnected = Array.isArray(bots) && bots.some((b) => b?.ws?.state === "connected");
6319
- if (anyConnected && getPanelVisible()) registerPanelTab();
6320
- else unregisterPanelTab();
6321
- } catch {
6322
- }
6323
- };
6324
- void syncPanelTab();
6325
- const offBotsChanged = onBotsChanged(() => void syncPanelTab());
6326
- const panelTabTimer = setInterval(() => void syncPanelTab(), 15e3);
6327
- ctx.effect(
6328
- () => () => {
6329
- clearInterval(panelTabTimer);
6330
- offBotsChanged();
6331
- unregisterPanelTab();
6332
- },
6333
- "qqbot: right-panel tab lifecycle"
6334
- );
6335
- ctx.effect(
6336
- () => ctx.slots.inject("sidebar.right.pane.tab", () => ctx.slots.register(
6337
- { name: "sidebar.right.pane.tab", key: QQBOT_PANEL_ID },
6338
- ScheduleTab
6339
- )),
6340
- "qqbot: right-panel tab body"
6341
- );
6342
- ctx.effect(
6343
- () => ctx.slots.inject("sidebar.right.pane.tab.title", () => ctx.slots.register(
6344
- { name: "sidebar.right.pane.tab.title", key: QQBOT_PANEL_ID },
6345
- TitleView
6346
- )),
6347
- "qqbot: right-panel tab title"
6348
- );
6553
+ installScheduleWorkbenchView();
6554
+ installSchedSendTasks();
6349
6555
  }
6350
6556
  function installStyles() {
6351
6557
  if (typeof document === "undefined") return () => {