@sunjuntao/dsh-qqbot 0.1.10 → 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 +310 -268
  2. package/lib/index.js +16 -18
  3. package/package.json +1 -3
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": "手写命令",
@@ -660,7 +670,6 @@ var cn = Object.freeze({
660
670
  "group.summaryMarkdown": "Markdown {0}",
661
671
  "group.summaryMemory": "记忆 {0}",
662
672
  "group.summaryBannedWords": "敏感词 {0} 个",
663
- "group.summaryChatPreset": "聊天 Preset {0}",
664
673
  // ── tz ──
665
674
  "tz.shanghai": "中国标准时间 · Asia/Shanghai(UTC+8)",
666
675
  "tz.hongkong": "中国香港 · Asia/Hong_Kong(UTC+8)",
@@ -728,7 +737,6 @@ var cn = Object.freeze({
728
737
  // ── unit ──
729
738
  "unit.minutes": "{0} 分钟",
730
739
  "unit.seconds": "{0} 秒",
731
- "panel.title": "QQ 定时消息",
732
740
  "panel.subtitle": "管理定时消息:增删改查与测试发送",
733
741
  "panel.scope": "机器人",
734
742
  "panel.allBots": "所有机器人",
@@ -754,8 +762,6 @@ var cn = Object.freeze({
754
762
  "panel.bot": "机器人",
755
763
  "panel.archiveAllHint": "所有机器人模式仅显示聚合条数;选择单个机器人可展开预览记录",
756
764
  "panel.tabId": "面板",
757
- "ui.panelSwitchTitle": "右侧面板「QQ 定时消息」入口",
758
- "ui.panelSwitchDesc": "默认关闭。开启后,编辑器右侧栏显示 QQ 定时消息管理入口并记住该选择:刷新页面、重启 DSH、QQ 掉线都不会让它消失;关闭即隐藏(改动即时生效)",
759
765
  "sched.view": "查看",
760
766
  "sched.back": "返回",
761
767
  "sched.detailTitle": "执行逻辑详情",
@@ -849,7 +855,7 @@ var en = Object.freeze({
849
855
  "session.defaultWorkspace": "Default workspace (~/.dsh/file)",
850
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.",
851
857
  "session.followDefaultModel": "Follow default model",
852
- "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.",
853
859
  // ── status ──
854
860
  "status.disabled": "Disabled",
855
861
  "status.connected": "Connected",
@@ -954,7 +960,7 @@ var en = Object.freeze({
954
960
  "policy.title": "Message & reply policy",
955
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.",
956
962
  "policy.fullGroupReply": "Reply to all group messages",
957
- "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.",
958
964
  "policy.acceptDm": "Accept direct messages",
959
965
  "policy.acceptDmHint": "Whether to respond to QQ direct (C2C) messages. When off, the bot processes group messages only and ignores DMs.",
960
966
  "policy.respondBots": "Respond to bot messages",
@@ -969,8 +975,8 @@ var en = Object.freeze({
969
975
  "policy.quoteScopeAll": "all (every group reply)",
970
976
  "policy.quoteLimit": "Quote preview length",
971
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.",
972
- "policy.chatPreset": "Group chat preset",
973
- "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.",
974
980
  "policy.followAgentPreset": "Follow Agent Preset",
975
981
  "policy.secretEnv": "AppSecret credential reference (secretEnv)",
976
982
  "policy.secretEnvShort": "AppSecret credential reference",
@@ -1156,6 +1162,16 @@ var en = Object.freeze({
1156
1162
  "sched.enabledToast": "Enabled — next run {0}",
1157
1163
  "sched.enabledNoNext": "Enabled, but the next run time cannot be computed. Check the trigger settings.",
1158
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",
1159
1175
  "sched.noNextRun": "Next run time cannot be computed — check the trigger settings",
1160
1176
  "sched.commandSource": "Command source",
1161
1177
  "sched.commandManual": "Manual command",
@@ -1397,7 +1413,6 @@ var en = Object.freeze({
1397
1413
  "group.summaryMarkdown": "Markdown {0}",
1398
1414
  "group.summaryMemory": "Memory {0}",
1399
1415
  "group.summaryBannedWords": "{0} banned words",
1400
- "group.summaryChatPreset": "Chat preset {0}",
1401
1416
  // ── tz ──
1402
1417
  "tz.shanghai": "China Standard Time · Asia/Shanghai (UTC+8)",
1403
1418
  "tz.hongkong": "Hong Kong, China · Asia/Hong_Kong (UTC+8)",
@@ -1465,7 +1480,6 @@ var en = Object.freeze({
1465
1480
  // ── unit ──
1466
1481
  "unit.minutes": "{0} min",
1467
1482
  "unit.seconds": "{0} s",
1468
- "panel.title": "QQ Schedules",
1469
1483
  "panel.subtitle": "Manage scheduled messages: create, edit, delete and test",
1470
1484
  "panel.scope": "Bot",
1471
1485
  "panel.allBots": "All bots",
@@ -1491,8 +1505,6 @@ var en = Object.freeze({
1491
1505
  "panel.bot": "Bot",
1492
1506
  "panel.archiveAllHint": "All-bots mode shows aggregated counts only; pick a single bot to expand and preview records",
1493
1507
  "panel.tabId": "Tab",
1494
- "ui.panelSwitchTitle": 'Right panel "QQ Scheduled Messages" entry',
1495
- "ui.panelSwitchDesc": "Off by default. When on, the QQ scheduled-messages manager appears in the right sidebar and the choice is remembered: reloading, restarting DSH, or losing the QQ connection will not hide it; off hides it (takes effect immediately)",
1496
1508
  "sched.view": "View",
1497
1509
  "sched.back": "Back",
1498
1510
  "sched.detailTitle": "Execution details",
@@ -1743,9 +1755,6 @@ function QqBotGlyph(props) {
1743
1755
  h("rect", { x: 4.5, y: 6, width: 15, height: 13, rx: 4.5, fill: "currentColor", mask: `url(#qbot-face-${props.uid})` })
1744
1756
  );
1745
1757
  }
1746
- function QqBotGuideIcon(props) {
1747
- return h(QqBotGlyph, { uid: props.uid ?? "guide", className: props.className, size: props.size });
1748
- }
1749
1758
 
1750
1759
  // src/client/settings-nav-icon.ts
1751
1760
  var QBOT_SETTINGS_NAV_MARKER = "data-qbot-settings-nav";
@@ -2039,7 +2048,7 @@ var CSS_TEXT = `
2039
2048
  .qbot-listHeading { min-height: 0; display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 0; }
2040
2049
  .qbot-listHeading h3 { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 14px; line-height: normal; font-weight: 650; }
2041
2050
  .qbot-listTitle { min-width: 0; display: inline-flex; align-items: center; gap: 10px; }
2042
- .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; }
2043
2052
 
2044
2053
  /* ── 通知(dim-statusNotice / info 变体)──────────────────────────────── */
2045
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; }
@@ -2212,7 +2221,7 @@ select.qbot-input { cursor: pointer; font-family: inherit; }
2212
2221
  .qbot-dirRow svg { flex: none; width: 16px; height: 16px; color: var(--qbot-business); }
2213
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; }
2214
2223
  .qbot-modalError { color: var(--dsw-alias-state-error-primary, #d54941); }
2215
- .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; }
2216
2225
  /* 弹窗底部内联错误:常驻可见,不随弹窗内容滚动而移出视口 */
2217
2226
  .qbot-footError { margin: 0; color: var(--dsw-alias-state-error-primary, #d54941); font-size: 12px; line-height: 1.6; overflow-wrap: anywhere; }
2218
2227
  /* 宽弹窗:定时消息 / 消息归档 列表内容较长,放宽上限 */
@@ -2225,7 +2234,7 @@ select.qbot-input { cursor: pointer; font-family: inherit; }
2225
2234
  .qbot-schedTab:hover { background: var(--dsw-alias-interactive-bg-hover, #eef0f3); }
2226
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%); }
2227
2236
  /* 右侧面板「定时消息」工具栏:机器人作用域 + 刷新(自适应面板宽度、可换行) */
2228
- .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); }
2229
2238
  .qbot-fieldInline { min-width: 0; flex: 1 1 auto; display: inline-flex; align-items: center; gap: 8px; }
2230
2239
  .qbot-fieldInlineLabel { flex: none; font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-secondary, #646a73); }
2231
2240
  .qbot-schedToolbar .qbot-settingSelect { flex: 1 1 auto; min-width: 0; max-width: 240px; }
@@ -3375,7 +3384,6 @@ function buildDraft(overrides, openid) {
3375
3384
  memoryEnabled: tri(ov.memoryEnabled),
3376
3385
  replyChunkChars: num(ov.replyChunkChars),
3377
3386
  maxRepliesPerMessage: num(ov.maxRepliesPerMessage),
3378
- agentPresetChat: typeof ov.agentPresetChat === "string" ? ov.agentPresetChat : "",
3379
3387
  bannedWords: Array.isArray(ov.bannedWords) ? ov.bannedWords.join(", ") : ""
3380
3388
  };
3381
3389
  }
@@ -3411,7 +3419,6 @@ function OverrideDialog(props) {
3411
3419
  if (d.memoryEnabled) ov.memoryEnabled = d.memoryEnabled === "on";
3412
3420
  if (d.replyChunkChars !== "") ov.replyChunkChars = Number(d.replyChunkChars);
3413
3421
  if (d.maxRepliesPerMessage !== "") ov.maxRepliesPerMessage = Number(d.maxRepliesPerMessage);
3414
- if (d.agentPresetChat.trim()) ov.agentPresetChat = d.agentPresetChat.trim();
3415
3422
  if (d.bannedWords.trim()) {
3416
3423
  ov.bannedWords = d.bannedWords.split(/[,,]/).map((w) => w.trim()).filter(Boolean);
3417
3424
  }
@@ -3599,9 +3606,8 @@ function OverrideDialog(props) {
3599
3606
  h("option", { value: "off" }, t("conn.disable"))
3600
3607
  )
3601
3608
  ),
3602
- // 长期记忆(memoryEnabled)与聊天 Preset(agentPresetChat)已从界面移除
3603
- //(默认常开/留空跟随 Agent Preset,仅 bots.json 可配);
3604
- // 草稿仍读取/回写这两个字段,避免保存时丢掉配置文件里已设置的值。
3609
+ // 长期记忆(memoryEnabled)已从界面移除(默认常开,仅 bots.json 可配);
3610
+ // 草稿仍读取/回写该字段,避免保存时丢掉配置文件里已设置的值。
3605
3611
  editRow(
3606
3612
  t("feature.bannedWords"),
3607
3613
  t("group.bannedWordsHint"),
@@ -3808,6 +3814,32 @@ function nextCronRun(expr, tz, now) {
3808
3814
  return null;
3809
3815
  }
3810
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
+
3811
3843
  // src/client/schedule-manager.tsx
3812
3844
  var WEEKDAYS = [
3813
3845
  { v: 0, label: t("weekday.sun"), en: "Su" },
@@ -3853,6 +3885,12 @@ function localTimeZone() {
3853
3885
  return DEFAULT_TZ;
3854
3886
  }
3855
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
+ }
3856
3894
  function cmdTemplates() {
3857
3895
  return [
3858
3896
  { label: "Python", cmd: "python C:/scripts/report.py" },
@@ -3947,8 +3985,12 @@ function blankEntry(detailAppId) {
3947
3985
  };
3948
3986
  }
3949
3987
  function ScheduleManager(props) {
3950
- const { rpcCall, detailAppId, forceScope, onClose } = props;
3988
+ const { rpcCall, detailAppId, forceScope, onClose, toast } = props;
3951
3989
  useLocale();
3990
+ const notify = (ok, text) => {
3991
+ if (toast) toast(ok ? "ok" : "error", text);
3992
+ else setSchedNotice({ ok, text });
3993
+ };
3952
3994
  const [scheduleModal, setScheduleModal] = React7.useState({
3953
3995
  loading: true,
3954
3996
  error: "",
@@ -3988,10 +4030,16 @@ function ScheduleManager(props) {
3988
4030
  }
3989
4031
  };
3990
4032
  React7.useEffect(() => {
3991
- if (!schedNotice) return;
4033
+ if (!schedNotice || toast) return;
3992
4034
  const timer = setTimeout(() => setSchedNotice(null), 8e3);
3993
4035
  return () => clearTimeout(timer);
3994
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]);
3995
4043
  React7.useEffect(() => {
3996
4044
  const s = forceScope ?? "current";
3997
4045
  setScheduleViewing(null);
@@ -4165,22 +4213,39 @@ function ScheduleManager(props) {
4165
4213
  } : {},
4166
4214
  ...e.appId ? { appId: e.appId } : detailAppId ? { appId: detailAppId } : {}
4167
4215
  };
4168
- const res = await rpcCall("schedule.add", payload);
4169
- if (res.ok) {
4170
- setScheduleModal((prev) => prev ? { ...prev, editing: null, saving: false } : prev);
4171
- await loadSchedules(scheduleModal?.botScope ?? "current");
4172
- } else {
4173
- setScheduleModal((prev) => prev ? { ...prev, saving: false, editError: errText(res.error) } : prev);
4174
- }
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
+ );
4175
4230
  };
4176
4231
  const removeSchedule = async (id) => {
4177
4232
  if (!await confirmDlg({ message: t("sched.removeTaskConfirm"), danger: true })) return;
4178
4233
  setScheduleRemoving(id);
4179
4234
  const scopeNow = scheduleModal?.botScope ?? "current";
4180
4235
  try {
4181
- const res = await rpcCall("schedule.remove", { id });
4182
- if (res.ok) await loadSchedules(scopeNow);
4183
- 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
+ );
4184
4249
  } finally {
4185
4250
  setScheduleRemoving("");
4186
4251
  }
@@ -4197,15 +4262,25 @@ function ScheduleManager(props) {
4197
4262
  setScheduleToggling(id);
4198
4263
  const scopeNow = scheduleModal?.botScope ?? "current";
4199
4264
  try {
4200
- const res = await rpcCall("schedule.setEnabled", { id, enabled: !currentlyEnabled });
4201
- if (res.ok) {
4202
- const s = val(res) ?? {};
4203
- const nextAt = s.schedule?.nextRunAt ?? s.nextRunAt;
4204
- setSchedNotice(
4205
- currentlyEnabled ? { ok: true, text: t("sched.disabledToast") } : nextAt ? { ok: true, text: fmt("sched.enabledToast", formatTime(nextAt)) } : { ok: false, text: t("sched.enabledNoNext") }
4206
- );
4207
- await loadSchedules(scopeNow);
4208
- } 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
+ );
4209
4284
  } finally {
4210
4285
  setScheduleToggling("");
4211
4286
  }
@@ -4214,13 +4289,23 @@ function ScheduleManager(props) {
4214
4289
  if (!id) return;
4215
4290
  setSchedNotice(null);
4216
4291
  setScheduleTesting(id);
4217
- const res = await rpcCall("schedule.runOnce", { id });
4218
- setScheduleTesting("");
4219
- if (res.ok) {
4220
- const v = val(res) ?? {};
4221
- setSchedNotice({ ok: true, text: typeof v.message === "string" ? v.message : t("sched.testSent") });
4222
- } else {
4223
- 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("");
4224
4309
  }
4225
4310
  await loadSchedules(scheduleModal?.botScope ?? "current");
4226
4311
  };
@@ -4695,7 +4780,7 @@ function ScheduleManager(props) {
4695
4780
  ),
4696
4781
  h("div", { className: "qbot-schedDetailBody" }, h("div", { className: "qbot-flow" }, ...buildFlow(e)))
4697
4782
  );
4698
- 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" };
4699
4784
  return h(
4700
4785
  "div",
4701
4786
  { className: "qbot-schedRoot", style: ROOT },
@@ -4706,6 +4791,7 @@ function ScheduleManager(props) {
4706
4791
  h(
4707
4792
  "div",
4708
4793
  { className: "qbot-editForm qbot-schedForm" },
4794
+ scheduleModal.editError && !toast ? h("div", { key: "editErr", className: "qbot-schedNotice is-error" }, scheduleModal.editError) : null,
4709
4795
  section(
4710
4796
  t("sched.form.stepRecipient"),
4711
4797
  t("sched.form.stepRecipientHint"),
@@ -4858,7 +4944,7 @@ function ScheduleManager(props) {
4858
4944
  "div",
4859
4945
  { className: `qbot-modalList${scheduleModal.loading && scheduleModal.items.length > 0 ? " is-refreshing" : ""}` },
4860
4946
  [
4861
- schedNotice ? h(
4947
+ schedNotice && !toast ? h(
4862
4948
  "div",
4863
4949
  { key: "notice", className: `qbot-schedNotice is-autoHide${schedNotice.ok ? "" : " is-error"}` },
4864
4950
  schedNotice.text
@@ -4900,7 +4986,11 @@ function ScheduleManager(props) {
4900
4986
  h(
4901
4987
  "div",
4902
4988
  { key: "scroll", className: "qbot-modalListScroll" },
4903
- 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(
4904
4994
  "div",
4905
4995
  { key: "loading", className: "qbot-modalState" },
4906
4996
  h("span", { className: "qbot-spinner", "aria-hidden": "true" }),
@@ -5077,7 +5167,6 @@ function ScheduleManager(props) {
5077
5167
  h(
5078
5168
  "div",
5079
5169
  { className: "qbot-viewActions" },
5080
- h("button", { className: "qbot-btn", type: "button", disabled: scheduleModal.loading, onClick: () => void loadSchedules(scheduleModal.botScope) }, t("common.refresh")),
5081
5170
  onClose ? h("button", { className: "qbot-btn qbot-btnPrimary", type: "button", onClick: onClose }, t("common.close")) : null
5082
5171
  )
5083
5172
  ) : null
@@ -5230,41 +5319,16 @@ function setRpcCall(fn) {
5230
5319
  function getRpcCall() {
5231
5320
  return rpcCallRef;
5232
5321
  }
5233
- var botsListeners = /* @__PURE__ */ new Set();
5234
- function onBotsChanged(fn) {
5235
- botsListeners.add(fn);
5236
- return () => botsListeners.delete(fn);
5237
- }
5238
- function notifyBotsChanged() {
5239
- for (const fn of botsListeners) {
5240
- try {
5241
- fn();
5242
- } catch {
5243
- }
5244
- }
5245
- }
5246
- var PANEL_VISIBLE_KEY = "dsh-qqbot.panel.visible";
5247
- function getPanelVisible() {
5248
- try {
5249
- return localStorage.getItem(PANEL_VISIBLE_KEY) === "1";
5250
- } catch {
5251
- return false;
5252
- }
5253
- }
5254
- function setPanelVisible(v) {
5255
- try {
5256
- localStorage.setItem(PANEL_VISIBLE_KEY, v ? "1" : "0");
5257
- } catch {
5258
- }
5259
- notifyBotsChanged();
5260
- }
5322
+
5323
+ // src/client/workbench/schedule-view.tsx
5324
+ var import_client = require("react-dom/client");
5261
5325
 
5262
5326
  // src/client/right-panel/ScheduleTab.tsx
5263
5327
  var React9 = __toESM(require("react"), 1);
5264
5328
  var root = {
5265
5329
  display: "flex",
5266
5330
  flexDirection: "column",
5267
- height: "100%",
5331
+ flex: "1 1 auto",
5268
5332
  minHeight: 0,
5269
5333
  boxSizing: "border-box",
5270
5334
  color: "inherit"
@@ -5275,7 +5339,8 @@ var body = {
5275
5339
  display: "flex",
5276
5340
  flexDirection: "column"
5277
5341
  };
5278
- function ScheduleTab() {
5342
+ function ScheduleTab(props) {
5343
+ const { toast } = props ?? {};
5279
5344
  useLocale();
5280
5345
  const [bots, setBots] = React9.useState([]);
5281
5346
  const [scopeAppId, setScopeAppId] = React9.useState("");
@@ -5347,38 +5412,168 @@ function ScheduleTab() {
5347
5412
  botError ? h("div", { className: "qbot-schedPanelError" }, botError) : rpc ? h(
5348
5413
  "div",
5349
5414
  { style: body, key: scopeAppId || "all" },
5350
- h(ScheduleManager, { rpcCall: rpc, detailAppId, forceScope })
5415
+ h(ScheduleManager, { rpcCall: rpc, detailAppId, forceScope, toast })
5351
5416
  ) : h("div", { className: "qbot-modalState" }, t("panel.loading"))
5352
5417
  );
5353
5418
  }
5354
5419
 
5355
- // src/client/right-panel/TitleView.tsx
5356
- var import_jsx_runtime = require("react/jsx-runtime");
5357
- var wrap = {
5358
- display: "inline-flex",
5359
- alignItems: "center",
5360
- gap: 6,
5361
- overflow: "hidden",
5362
- whiteSpace: "nowrap"
5363
- };
5364
- function TitleView() {
5365
- useLocale();
5366
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: wrap, children: [
5367
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(QqBotGlyph, { uid: "panel-chip", size: 15, className: "qbot-panelMark" }),
5368
- t("panel.title")
5369
- ] });
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
+ }
5370
5571
  }
5371
5572
 
5372
5573
  // src/client/index.tsx
5373
5574
  var name = "qqbot-settings";
5374
- var inject = ["slots", "connection", "locale", "sidebarRightTabs", "sidebarRight"];
5575
+ var inject = ["slots", "connection", "locale"];
5375
5576
  var RPC_CHANNEL = "/qqbot-settings";
5376
- function eventsWsUrl() {
5377
- const scheme = typeof location !== "undefined" && location.protocol === "https:" ? "wss:" : "ws:";
5378
- return `${scheme}//${location.host}${RPC_CHANNEL}/events`;
5379
- }
5380
- var QQBOT_PANEL_ID = "@sunjuntao/dsh-qqbot";
5381
- var QQBOT_PANEL_KIND = "qqbot-sched";
5382
5577
  var NOTICE_TTL_SECONDS = 8;
5383
5578
  var UPDATE_IDLE = { busy: false, message: "", done: false };
5384
5579
  var updateStore = /* @__PURE__ */ (() => {
@@ -5423,7 +5618,6 @@ function QqbotSettingsTab({ rpcCall }) {
5423
5618
  const [loadError, setLoadError] = React10.useState("");
5424
5619
  const [refreshing, setRefreshing] = React10.useState(false);
5425
5620
  const [reconnecting, setReconnecting] = React10.useState(false);
5426
- const [panelVisible, setPanelVisibleState] = React10.useState(() => getPanelVisible());
5427
5621
  const [update, setLocalUpdate] = React10.useState(() => updateStore.snapshot());
5428
5622
  const setUpdate = publishUpdate;
5429
5623
  React10.useEffect(() => updateStore.subscribe(setLocalUpdate), []);
@@ -5442,7 +5636,6 @@ function QqbotSettingsTab({ rpcCall }) {
5442
5636
  setForm({ ...stored });
5443
5637
  }
5444
5638
  if (b.ok) setBots(val(b) ?? null);
5445
- notifyBotsChanged();
5446
5639
  if (cat.ok) setCatalogs({ ...EMPTY_CATALOGS, ...val(cat) ?? {} });
5447
5640
  } catch (error) {
5448
5641
  setLoadError(error instanceof Error ? error.message : String(error));
@@ -5587,7 +5780,6 @@ function QqbotSettingsTab({ rpcCall }) {
5587
5780
  if (ov.markdownReply !== void 0) parts.push(fmt("group.summaryMarkdown", ov.markdownReply ? t("sched.form.onShort") : t("sched.form.offShort")));
5588
5781
  if (ov.memoryEnabled !== void 0) parts.push(fmt("group.summaryMemory", ov.memoryEnabled ? t("sched.form.onShort") : t("sched.form.offShort")));
5589
5782
  if (Array.isArray(ov.bannedWords) && ov.bannedWords.length > 0) parts.push(fmt("group.summaryBannedWords", ov.bannedWords.length));
5590
- if (typeof ov.agentPresetChat === "string" && ov.agentPresetChat) parts.push(fmt("group.summaryChatPreset", ov.agentPresetChat));
5591
5783
  return parts.length > 0 ? parts.join(" · ") : t("group.noOverrides");
5592
5784
  };
5593
5785
  const removeOverride = async (openid) => {
@@ -5746,40 +5938,6 @@ function QqbotSettingsTab({ rpcCall }) {
5746
5938
  )
5747
5939
  ) : null,
5748
5940
  h("p", { className: "qbot-hint" }, t("list.cardHint"))
5749
- ),
5750
- // ── 界面偏好:右侧面板「QQ 定时消息」入口开关(客户端本地设置) ──
5751
- h(
5752
- "div",
5753
- { className: "qbot-surfaceCard" },
5754
- h(
5755
- "div",
5756
- { className: "qbot-surfaceBody" },
5757
- h(
5758
- "label",
5759
- { className: "qbot-switchRow" },
5760
- h(
5761
- "span",
5762
- { className: "qbot-switchText" },
5763
- h("strong", null, t("ui.panelSwitchTitle")),
5764
- h(
5765
- "p",
5766
- { style: { margin: "2px 0 0", fontSize: "12px", color: "var(--dsw-alias-label-tertiary, #8f959e)" } },
5767
- t("ui.panelSwitchDesc")
5768
- )
5769
- ),
5770
- h("input", {
5771
- type: "checkbox",
5772
- className: "qbot-switch",
5773
- checked: panelVisible,
5774
- onChange: (e) => {
5775
- const v = Boolean(e.target.checked);
5776
- setPanelVisibleState(v);
5777
- setPanelVisible(v);
5778
- },
5779
- "aria-label": t("ui.panelSwitchTitle")
5780
- })
5781
- )
5782
- )
5783
5941
  )
5784
5942
  );
5785
5943
  const wsInfo = detailBot ? {
@@ -6048,8 +6206,6 @@ function QqbotSettingsTab({ rpcCall }) {
6048
6206
  "aria-label": "Agent Preset"
6049
6207
  }, presetOptions(catalogs.agentPresets).map((o) => h("option", { key: o.value, value: o.value }, o.label)))
6050
6208
  })
6051
- // 群聊聊天 Preset(agentPresetChat)与 AppSecret 凭据引用(secretEnv)已从界面移除,
6052
- // 仅通过 bots.json 配置——默认 agentPresetChat 留空即跟随上方 Agent Preset(见 rule.ts)。
6053
6209
  )
6054
6210
  ),
6055
6211
  // ── 消息与回复策略(开关) ──
@@ -6262,7 +6418,7 @@ function QqbotSettingsTab({ rpcCall }) {
6262
6418
  "div",
6263
6419
  { className: "qbot-brandHeading" },
6264
6420
  h("strong", { className: "qbot-brandName" }, t("app.title")),
6265
- h("span", { className: "qbot-brandVersion" }, `v${true ? "0.1.10" : "0.0.2"}`),
6421
+ h("span", { className: "qbot-brandVersion" }, `v${true ? "0.1.13" : "0.0.2"}`),
6266
6422
  h("button", {
6267
6423
  className: `qbot-btn qbot-updateBtn${update.done ? " is-done" : ""}`,
6268
6424
  type: "button",
@@ -6394,122 +6550,8 @@ function apply(ctx) {
6394
6550
  },
6395
6551
  QqbotSettingsTab
6396
6552
  ));
6397
- let disposePanelTab = null;
6398
- const registerPanelTab = () => {
6399
- if (disposePanelTab) return;
6400
- disposePanelTab = ctx.sidebarRightTabs.register({
6401
- id: QQBOT_PANEL_ID,
6402
- kind: QQBOT_PANEL_KIND,
6403
- // title / guide 由宿主在打开面板时求值,这里每次调用都走 i18n,切换语言后即为当前语言。
6404
- title: () => t("panel.title"),
6405
- guide: [
6406
- {
6407
- order: 100,
6408
- title: () => t("panel.title"),
6409
- description: () => t("panel.subtitle"),
6410
- // 入口胶囊的图标;不传则宿主画占位方块。用插件统一的 QQ 机器人 glyph。
6411
- icon: QqBotGuideIcon
6412
- }
6413
- ]
6414
- });
6415
- };
6416
- const unregisterPanelTab = () => {
6417
- try {
6418
- disposePanelTab?.();
6419
- } catch {
6420
- }
6421
- disposePanelTab = null;
6422
- };
6423
- const syncPanelTab = () => {
6424
- if (getPanelVisible()) registerPanelTab();
6425
- else unregisterPanelTab();
6426
- };
6427
- syncPanelTab();
6428
- const offBotsChanged = onBotsChanged(() => void syncPanelTab());
6429
- const onStorage = (event) => {
6430
- if (event.storageArea === localStorage && (event.key === null || event.key === PANEL_VISIBLE_KEY)) {
6431
- syncPanelTab();
6432
- }
6433
- };
6434
- window.addEventListener("storage", onStorage);
6435
- let wsSocket = null;
6436
- let wsRetry = 0;
6437
- let wsTimer = null;
6438
- let wsDisposed = false;
6439
- const scheduleWsReconnect = () => {
6440
- if (wsDisposed || wsTimer !== null) return;
6441
- const delay = Math.min(1e3 * 2 ** wsRetry, 1e4);
6442
- wsRetry = Math.min(wsRetry + 1, 10);
6443
- wsTimer = setTimeout(() => {
6444
- wsTimer = null;
6445
- connectWs();
6446
- }, delay);
6447
- };
6448
- function connectWs() {
6449
- if (wsDisposed || typeof WebSocket === "undefined") return;
6450
- let socket;
6451
- try {
6452
- socket = new WebSocket(eventsWsUrl());
6453
- } catch (error) {
6454
- console?.debug?.("[dsh-qqbot] WebSocket 订阅失败,面板显隐依赖设置页动作触发:", error);
6455
- scheduleWsReconnect();
6456
- return;
6457
- }
6458
- wsSocket = socket;
6459
- socket.onopen = () => {
6460
- wsRetry = 0;
6461
- syncPanelTab();
6462
- };
6463
- socket.onmessage = (event) => {
6464
- if (typeof event.data !== "string") return;
6465
- try {
6466
- const message = JSON.parse(event.data);
6467
- if (message.event === "bots-changed") notifyBotsChanged();
6468
- } catch {
6469
- }
6470
- };
6471
- socket.onerror = () => {
6472
- };
6473
- socket.onclose = () => {
6474
- if (wsSocket === socket) wsSocket = null;
6475
- scheduleWsReconnect();
6476
- };
6477
- }
6478
- connectWs();
6479
- ctx.effect(
6480
- () => () => {
6481
- wsDisposed = true;
6482
- if (wsTimer !== null) clearTimeout(wsTimer);
6483
- wsTimer = null;
6484
- try {
6485
- const socket = wsSocket;
6486
- wsSocket = null;
6487
- if (socket) {
6488
- socket.onclose = null;
6489
- socket.close();
6490
- }
6491
- } catch {
6492
- }
6493
- window.removeEventListener("storage", onStorage);
6494
- offBotsChanged();
6495
- unregisterPanelTab();
6496
- },
6497
- "qqbot: right-panel tab lifecycle"
6498
- );
6499
- ctx.effect(
6500
- () => ctx.slots.inject("sidebar.right.pane.tab", () => ctx.slots.register(
6501
- { name: "sidebar.right.pane.tab", key: QQBOT_PANEL_ID },
6502
- ScheduleTab
6503
- )),
6504
- "qqbot: right-panel tab body"
6505
- );
6506
- ctx.effect(
6507
- () => ctx.slots.inject("sidebar.right.pane.tab.title", () => ctx.slots.register(
6508
- { name: "sidebar.right.pane.tab.title", key: QQBOT_PANEL_ID },
6509
- TitleView
6510
- )),
6511
- "qqbot: right-panel tab title"
6512
- );
6553
+ installScheduleWorkbenchView();
6554
+ installSchedSendTasks();
6513
6555
  }
6514
6556
  function installStyles() {
6515
6557
  if (typeof document === "undefined") return () => {
package/lib/index.js CHANGED
@@ -196,7 +196,6 @@ var init_store_file = __esm({
196
196
  BOT_CONFIG_FIELDS = [
197
197
  "workspacePath",
198
198
  "agentPreset",
199
- "agentPresetChat",
200
199
  "permissionPreset",
201
200
  "model",
202
201
  "secretEnv",
@@ -249,7 +248,6 @@ var init_store_file = __esm({
249
248
  DEFAULT_BEHAVIOR_CONFIG = {
250
249
  workspacePath: "",
251
250
  agentPreset: "",
252
- agentPresetChat: "",
253
251
  permissionPreset: "",
254
252
  model: "",
255
253
  secretEnv: "",
@@ -3292,8 +3290,7 @@ var GROUP_OVERRIDE_FIELDS = [
3292
3290
  "maxRepliesPerMessage",
3293
3291
  "markdownReply",
3294
3292
  "memoryEnabled",
3295
- "bannedWords",
3296
- "agentPresetChat"
3293
+ "bannedWords"
3297
3294
  ];
3298
3295
  function str(value) {
3299
3296
  return typeof value === "string" ? value.trim() : "";
@@ -3369,9 +3366,6 @@ function resolveGroupOverrides(raw) {
3369
3366
  ov.bannedWords = v.filter((w) => typeof w === "string" && w.trim().length > 0).map((w) => w.trim());
3370
3367
  }
3371
3368
  break;
3372
- case "agentPresetChat":
3373
- if (typeof v === "string") ov.agentPresetChat = v.trim();
3374
- break;
3375
3369
  }
3376
3370
  }
3377
3371
  if (Object.keys(ov).length > 0) out[id] = ov;
@@ -3399,7 +3393,6 @@ function resolveConfig({ entry = {}, stored = {}, credentials = {} } = {}) {
3399
3393
  adminToken: str(pick2("adminToken")),
3400
3394
  workspacePath: str(pick2("workspacePath")) || process.cwd(),
3401
3395
  agentPreset: str(pick2("agentPreset")),
3402
- agentPresetChat: str(pick2("agentPresetChat")),
3403
3396
  permissionPreset: str(pick2("permissionPreset")),
3404
3397
  model: parseModelSelection(str(pick2("model"))),
3405
3398
  allowC2c: boolOr(pick2("allowC2c"), true),
@@ -6169,7 +6162,6 @@ function createQqRule({
6169
6162
  payload,
6170
6163
  msgId: payload.id ?? "",
6171
6164
  deliveryId: delivery.deliveryId,
6172
- allowTools: isAt,
6173
6165
  quoteContext: quoteContextOf(bot, groupChatKey2, payload, sender, content),
6174
6166
  quoteMention: isAt,
6175
6167
  attachmentContext: attachCtx2,
@@ -6228,7 +6220,6 @@ function createQqRule({
6228
6220
  payload,
6229
6221
  msgId,
6230
6222
  deliveryId: delivery.deliveryId,
6231
- allowTools: true,
6232
6223
  quoteContext: quoteContextOf(bot, chatKey, payload, sender, content),
6233
6224
  quoteMention: true,
6234
6225
  attachmentContext: attachCtx,
@@ -6242,7 +6233,6 @@ function createQqRule({
6242
6233
  function newMessageId2() {
6243
6234
  return randomUUID2();
6244
6235
  }
6245
- var CHAT_ONLY_HINT = "(群聊全量模式·非 @ 触发)不要调用任何工具、不要读写文件、不要执行命令。";
6246
6236
  async function enterConversation(args) {
6247
6237
  const {
6248
6238
  bot,
@@ -6252,7 +6242,6 @@ async function enterConversation(args) {
6252
6242
  target,
6253
6243
  msgId,
6254
6244
  deliveryId,
6255
- allowTools,
6256
6245
  quoteContext,
6257
6246
  attachmentContext,
6258
6247
  memoryBlock,
@@ -6292,9 +6281,7 @@ async function enterConversation(args) {
6292
6281
  bot.state.mergeRecords.set(mergeFp, { record, at: Date.now() });
6293
6282
  }
6294
6283
  const sender = senderIdOf(payload);
6295
- const rawBody = allowTools ? promptBuilder() : `${CHAT_ONLY_HINT}
6296
-
6297
- ${promptBuilder()}`;
6284
+ const rawBody = promptBuilder();
6298
6285
  if (!rawBody.trim() && !attachmentContext) return null;
6299
6286
  if (target.scope === "c2c" && config.typingIndicator && msgId) {
6300
6287
  bot.typing.start(chatKey, target, msgId);
@@ -6338,7 +6325,7 @@ ${prompt}`;
6338
6325
  workspacePath: config.workspacePath,
6339
6326
  title: `QQ: ${(payload.content ?? "").trim().slice(0, 40)}`,
6340
6327
  prompt,
6341
- agentPreset: allowTools ? presets.agentPreset : config.agentPresetChat || presets.agentPreset,
6328
+ agentPreset: presets.agentPreset,
6342
6329
  permissionPreset: presets.permissionPreset,
6343
6330
  ...config.model ? { model: config.model } : {}
6344
6331
  };
@@ -6842,6 +6829,7 @@ var WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
6842
6829
  var HEARTBEAT_INTERVAL_MS = 25e3;
6843
6830
  var MAX_PAYLOAD_BYTES = 1024 * 1024;
6844
6831
  var WS_EVENT_BOTS_CHANGED = "bots-changed";
6832
+ var WS_EVENT_SCHED_SEND = "sched-send";
6845
6833
  function acceptKey(key) {
6846
6834
  return createHash2("sha1").update(`${key}${WS_GUID}`).digest("base64");
6847
6835
  }
@@ -8757,6 +8745,11 @@ function nextRunFor(entry, now) {
8757
8745
  if (entry.type === "at") return entry.at ? new Date(entry.at) : null;
8758
8746
  return null;
8759
8747
  }
8748
+ function sendEventSummary(entry) {
8749
+ const raw = entry.mode === "tool" ? entry.command ?? entry.content ?? "" : entry.content ?? "";
8750
+ const text = String(raw).trim();
8751
+ return text ? text.slice(0, 60) : entry.id;
8752
+ }
8760
8753
  var Scheduler = class {
8761
8754
  #ctx;
8762
8755
  #timer = null;
@@ -8869,11 +8862,13 @@ var Scheduler = class {
8869
8862
  this.#ctx.logger.info(
8870
8863
  `[dsh-qqbot] 定时任务跳过发送(机器人 ${bot.appId},mode=tool,gate=${entry.gate ?? "always"}):${reason}`
8871
8864
  );
8865
+ this.#ctx.onSendEvent?.({ summary: sendEventSummary(entry), appId: bot.appId, ok: true, skipped: true, reason });
8872
8866
  } else {
8873
8867
  bot.state.counters.proactive += 1;
8874
8868
  entry.lastSentAt = toShanghaiISO(now);
8875
8869
  entry.lastError = void 0;
8876
8870
  entry.lastSkipReason = void 0;
8871
+ this.#ctx.onSendEvent?.({ summary: sendEventSummary(entry), appId: bot.appId, ok: true, skipped: false });
8877
8872
  }
8878
8873
  if (entry.type === "at") {
8879
8874
  await this.#ctx.store.removeById(entry.id);
@@ -8889,6 +8884,7 @@ var Scheduler = class {
8889
8884
  scheduleNext(entry);
8890
8885
  if (entry.nextRunAt) entry.lastError = message;
8891
8886
  this.#ctx.logger.error("[dsh-qqbot] 定时任务执行失败:", error);
8887
+ this.#ctx.onSendEvent?.({ summary: sendEventSummary(entry), appId: bot.appId, ok: false, skipped: false, reason: message });
8892
8888
  }
8893
8889
  void bot.archiver.append({
8894
8890
  kind: "proactive",
@@ -10573,6 +10569,7 @@ async function apply(ctx, entryConfig) {
10573
10569
  sanitizeOutgoingText(collected, { enabled: bot.config.sanitizeReplies })
10574
10570
  );
10575
10571
  };
10572
+ const wsHub = createWsHub({ logger });
10576
10573
  const scheduler = new Scheduler({
10577
10574
  store: schedules,
10578
10575
  resolveBot: (appId) => (appId ? bots.get(appId) : void 0) ?? bots.primary(),
@@ -10582,7 +10579,9 @@ async function apply(ctx, entryConfig) {
10582
10579
  logger,
10583
10580
  // 宿主会话总线:测试执行时监听 session/event 等待真实投递结果
10584
10581
  //(cordis ctx 运行时有 on/off,静态类型缺 off,此处断言)。
10585
- bus: ctx
10582
+ bus: ctx,
10583
+ // 到点触发结果 → 客户端后台任务面板(文件工作台底部任务按钮)。
10584
+ onSendEvent: (info) => wsHub.broadcast(WS_EVENT_SCHED_SEND, info)
10586
10585
  });
10587
10586
  scheduler.start();
10588
10587
  scriptGen.flushPending();
@@ -10602,7 +10601,6 @@ async function apply(ctx, entryConfig) {
10602
10601
  }, 6e4);
10603
10602
  outboxTimer.unref?.();
10604
10603
  let runtime = null;
10605
- const wsHub = createWsHub({ logger });
10606
10604
  const bots = new BotRuntimeManager({
10607
10605
  logger,
10608
10606
  entryConfig,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sunjuntao/dsh-qqbot",
3
- "version": "0.1.10",
3
+ "version": "0.1.13",
4
4
  "description": "把 QQ 官方机器人(Webhook 回调)接入本机 DeepSeek Harness:QQ 群/单聊消息(含群全量消息上下文)→ DSH 会话 → 回复发回 QQ,支持被动回复与群主动消息。Connect the official QQ bot (webhook callback) to a local DeepSeek Harness.",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -46,7 +46,6 @@
46
46
  "@deepseek-ai/dsh-client-runtime",
47
47
  "@deepseek-ai/dsh-client-ui-settings",
48
48
  "@deepseek-ai/dsh-client-ui-slots",
49
- "@deepseek-ai/dsh-client-ui-sidebar-right",
50
49
  "@deepseek-ai/dsh-client-locale"
51
50
  ],
52
51
  "platform": "web"
@@ -60,7 +59,6 @@
60
59
  },
61
60
  "scripts": {
62
61
  "link-dsh-deps": "node scripts/deploy/link-dsh-deps.mjs",
63
- "postinstall": "node scripts/deploy/link-dsh-deps.mjs",
64
62
  "build": "node scripts/build/build.mjs",
65
63
  "build:cli": "node scripts/build/build-cli.mjs",
66
64
  "build:types": "tsc --emitDeclarationOnly --declaration --rootDir src --outDir types --skipLibCheck",