@zaofan/dsh-qqbot 1.0.1 → 1.1.2

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 (105) hide show
  1. package/README.md +6 -0
  2. package/client/qqbot-settings.js +667 -54
  3. package/dist/api/group-admin.d.ts +37 -0
  4. package/dist/api/group-admin.d.ts.map +1 -1
  5. package/dist/api/group-admin.js +57 -0
  6. package/dist/api/group-admin.js.map +1 -1
  7. package/dist/channel-tools.d.ts.map +1 -1
  8. package/dist/channel-tools.js +373 -25
  9. package/dist/channel-tools.js.map +1 -1
  10. package/dist/commands/botplay.d.ts.map +1 -1
  11. package/dist/commands/botplay.js +6 -2
  12. package/dist/commands/botplay.js.map +1 -1
  13. package/dist/commands/index.d.ts.map +1 -1
  14. package/dist/commands/index.js +6 -2
  15. package/dist/commands/index.js.map +1 -1
  16. package/dist/commands/inject.d.ts +17 -0
  17. package/dist/commands/inject.d.ts.map +1 -0
  18. package/dist/commands/inject.js +70 -0
  19. package/dist/commands/inject.js.map +1 -0
  20. package/dist/commands/misc.d.ts +1 -1
  21. package/dist/commands/misc.d.ts.map +1 -1
  22. package/dist/commands/misc.js +25 -3
  23. package/dist/commands/misc.js.map +1 -1
  24. package/dist/commands/session.d.ts +2 -0
  25. package/dist/commands/session.d.ts.map +1 -1
  26. package/dist/commands/session.js +41 -3
  27. package/dist/commands/session.js.map +1 -1
  28. package/dist/config.d.ts +25 -0
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +18 -1
  31. package/dist/config.js.map +1 -1
  32. package/dist/features/botplay-store.d.ts +13 -0
  33. package/dist/features/botplay-store.d.ts.map +1 -0
  34. package/dist/features/botplay-store.js +99 -0
  35. package/dist/features/botplay-store.js.map +1 -0
  36. package/dist/features/botplay.d.ts +8 -0
  37. package/dist/features/botplay.d.ts.map +1 -1
  38. package/dist/features/botplay.js +22 -7
  39. package/dist/features/botplay.js.map +1 -1
  40. package/dist/features/broadcast.d.ts +63 -0
  41. package/dist/features/broadcast.d.ts.map +1 -0
  42. package/dist/features/broadcast.js +225 -0
  43. package/dist/features/broadcast.js.map +1 -0
  44. package/dist/features/card-callback.d.ts +52 -0
  45. package/dist/features/card-callback.d.ts.map +1 -0
  46. package/dist/features/card-callback.js +133 -0
  47. package/dist/features/card-callback.js.map +1 -0
  48. package/dist/features/group-hub.d.ts +14 -0
  49. package/dist/features/group-hub.d.ts.map +1 -1
  50. package/dist/features/group-hub.js +87 -4
  51. package/dist/features/group-hub.js.map +1 -1
  52. package/dist/features/group-join-request.d.ts.map +1 -1
  53. package/dist/features/group-join-request.js +9 -2
  54. package/dist/features/group-join-request.js.map +1 -1
  55. package/dist/features/poll-join-requests.d.ts +9 -0
  56. package/dist/features/poll-join-requests.d.ts.map +1 -0
  57. package/dist/features/poll-join-requests.js +249 -0
  58. package/dist/features/poll-join-requests.js.map +1 -0
  59. package/dist/features/preset-switcher.d.ts +39 -0
  60. package/dist/features/preset-switcher.d.ts.map +1 -0
  61. package/dist/features/preset-switcher.js +172 -0
  62. package/dist/features/preset-switcher.js.map +1 -0
  63. package/dist/features/session-registry.d.ts +6 -0
  64. package/dist/features/session-registry.d.ts.map +1 -1
  65. package/dist/features/session-registry.js +80 -0
  66. package/dist/features/session-registry.js.map +1 -1
  67. package/dist/gateway/bootstrap.d.ts.map +1 -1
  68. package/dist/gateway/bootstrap.js +74 -4
  69. package/dist/gateway/bootstrap.js.map +1 -1
  70. package/dist/gateway/debounce.d.ts.map +1 -1
  71. package/dist/gateway/debounce.js +7 -0
  72. package/dist/gateway/debounce.js.map +1 -1
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +24 -5
  75. package/dist/index.js.map +1 -1
  76. package/dist/middleware/media-history.d.ts.map +1 -1
  77. package/dist/middleware/media-history.js +23 -6
  78. package/dist/middleware/media-history.js.map +1 -1
  79. package/dist/model/settings-reader.d.ts +7 -0
  80. package/dist/model/settings-reader.d.ts.map +1 -1
  81. package/dist/model/settings-reader.js +14 -0
  82. package/dist/model/settings-reader.js.map +1 -1
  83. package/dist/session/session-manager.d.ts +26 -0
  84. package/dist/session/session-manager.d.ts.map +1 -1
  85. package/dist/session/session-manager.js +101 -0
  86. package/dist/session/session-manager.js.map +1 -1
  87. package/dist/transport/inbound.d.ts.map +1 -1
  88. package/dist/transport/inbound.js +46 -50
  89. package/dist/transport/inbound.js.map +1 -1
  90. package/dist/transport/inject-rules.d.ts.map +1 -1
  91. package/dist/transport/inject-rules.js +8 -2
  92. package/dist/transport/inject-rules.js.map +1 -1
  93. package/dist/transport/media-kind.d.ts +37 -0
  94. package/dist/transport/media-kind.d.ts.map +1 -0
  95. package/dist/transport/media-kind.js +60 -0
  96. package/dist/transport/media-kind.js.map +1 -0
  97. package/dist/transport/outbound-buffer.d.ts +20 -2
  98. package/dist/transport/outbound-buffer.d.ts.map +1 -1
  99. package/dist/transport/outbound-buffer.js +25 -5
  100. package/dist/transport/outbound-buffer.js.map +1 -1
  101. package/dist/transport/outbound.d.ts.map +1 -1
  102. package/dist/transport/outbound.js +68 -4
  103. package/dist/transport/outbound.js.map +1 -1
  104. package/package.json +1 -1
  105. package/settings-host.js +406 -21
@@ -508,7 +508,7 @@ window.__ModuleLoader__.load({
508
508
  return {
509
509
  username: j.username || '(未知昵称)',
510
510
  source: j.apply_source === 'invited' ? '被邀请' : '主动申请',
511
- verify: (j.verify_info && j.verify_info.verify_message) || (j.verify_info && j.verify_info.method) || '',
511
+ verify: j.verify_human || (j.verify_info && j.verify_info.verify_message) || (j.verify_info && j.verify_info.method) || '',
512
512
  risk: j.risk_tips || '',
513
513
  member_openid: j.member_openid,
514
514
  }
@@ -1701,7 +1701,7 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
1701
1701
  // · 群管理(发消息/审批入群/禁言) → 悬浮球点开成操作台
1702
1702
  var DOCK_CSS = "#qqs-dock-wrap{position:fixed;right:18px;bottom:190px;z-index:9997;font-family:-apple-system,'PingFang SC','Microsoft YaHei',sans-serif}#qqs-dock-ball{width:52px;height:52px;border-radius:50%;background:linear-gradient(160deg,#7c6cf0,#5b4fd8);color:#fff;font-size:24px;line-height:52px;text-align:center;cursor:pointer;box-shadow:0 6px 20px rgba(90,70,220,.4);user-select:none;transition:transform .12s,box-shadow .12s;position:relative}#qqs-dock-ball:hover{transform:scale(1.06)}#qqs-dock-badge{position:absolute;top:-4px;right:-4px;min-width:18px;height:18px;border-radius:9px;background:#ff4d4f;color:#fff;font-size:11px;font-weight:700;line-height:18px;padding:0 4px;box-sizing:border-box;text-align:center;display:none}#qqs-dock-panel{position:fixed;right:18px;bottom:190px;z-index:9998;width:min(720px,94vw);max-height:68vh;display:none;flex-direction:column;background:#fff;border:1px solid #d9c6ff;border-radius:16px;box-shadow:0 12px 40px rgba(60,40,140,.25);overflow:hidden;font-family:-apple-system,'PingFang SC','Microsoft YaHei',sans-serif}#qqs-dock-panel .dk-h{display:flex;align-items:center;gap:8px;padding:10px 14px;background:linear-gradient(90deg,#7c6cf01f,#7c6cf008);font-size:14px;font-weight:700;color:#4a3a9f;border-bottom:1px solid #efe8ff}#qqs-dock-panel .dk-b{padding:10px 14px;overflow:auto;font-size:13px;color:#1f2329}#qqs-dock-panel .dk-tab{display:flex;gap:4px;border-bottom:1px solid #eee;margin-bottom:10px}#qqs-dock-panel .dk-tab button{font:inherit;font-size:13px;padding:6px 14px;border:none;background:none;cursor:pointer;color:#666;border-bottom:2px solid transparent}#qqs-dock-panel .dk-tab button.on{color:#4a3a9f;font-weight:700;border-bottom-color:#7c6cf0}#qqs-dock-panel select.qqs-sel,#qqs-dock-panel input.qqs-txt,#qqs-dock-panel textarea.qqs-txt{font:inherit;color:#1f2329;background:#fff;border:1px solid #d0d5dd;border-radius:8px;padding:5px 8px;outline:none}#qqs-dock-panel .dk-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:6px 0}#qqs-dock-panel .dk-btn{font:inherit;font-size:13px;padding:5px 12px;border-radius:8px;cursor:pointer;border:1px solid #d9c6ff;background:#f1ecff;color:#4a3a9f}#qqs-dock-panel .dk-btn.ok{background:#e6f7ec;color:#187a3d;border-color:#b8e6c8}#qqs-dock-panel .dk-btn.no{background:#fdeeee;color:#c23131;border-color:#f3c4c4}#qqs-dock-panel .dk-btn:disabled{opacity:.5;cursor:default}#qqs-dock-panel .dk-msg{color:#888;font-size:12px;padding:2px 0}#qqs-dock-panel .dk-list{max-height:34vh;overflow:auto;border:1px solid #f0ecff;border-radius:10px;padding:4px}#qqs-dock-panel .dk-item{display:flex;align-items:center;gap:8px;padding:6px 8px;border-bottom:1px solid #f5f2ff;flex-wrap:wrap;font-size:13px}#qqs-dock-panel .dk-item:last-child{border-bottom:none}#qqs-dock-panel .dk-empty{color:#aaa;text-align:center;padding:18px 0;font-size:12px}"
1703
1703
  // ── 💬 聊天视图样式(dock 追加段, 2026-09-07): QQ 风格气泡, 群友左(bot)右 ──
1704
- var DOCK_CSS2 = "#qqs-dock-panel .dk-chat-head{display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin:0 0 6px}#qqs-dock-panel .dk-chat-box{overflow-y:auto;overscroll-behavior:contain;background:#f5f6f8;border:1px solid #e6e8ec;border-radius:10px;padding:10px 12px;box-sizing:border-box;height:min(36vh,300px);min-height:140px;scroll-behavior:auto}#qqs-dock-panel .dk-chat-box::-webkit-scrollbar{width:6px}#qqs-dock-panel .dk-chat-box::-webkit-scrollbar-thumb{background:#d3d7dd;border-radius:3px}#qqs-dock-panel .dk-crow{display:flex;gap:8px;align-items:flex-start;margin:0 0 12px}#qqs-dock-panel .dk-crow.out{flex-direction:row-reverse}#qqs-dock-panel .dk-ava{width:32px;height:32px;border-radius:50%;flex:none;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:600;color:#fff;overflow:hidden;user-select:none;background:linear-gradient(150deg,#8fb3e8,#5f8fd9)}#qqs-dock-panel .dk-crow.out .dk-ava{background:linear-gradient(150deg,#5ec7f2,#3b8fe0)}#qqs-dock-panel .dk-cmain{display:flex;flex-direction:column;max-width:calc(100% - 40px);min-width:0}#qqs-dock-panel .dk-crow.in .dk-cmain{align-items:flex-start}#qqs-dock-panel .dk-crow.out .dk-cmain{align-items:flex-end}#qqs-dock-panel .dk-cmeta{font-size:11px;color:#9aa0a8;margin:0 6px 2px;max-width:100%;display:flex;align-items:center;gap:5px;flex-wrap:wrap}#qqs-dock-panel .dk-crow.out .dk-cmeta{flex-direction:row-reverse}#qqs-dock-panel .dk-cbubble{padding:7px 11px;font-size:13px;line-height:1.55;white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere;box-shadow:0 1px 2px rgba(20,30,60,.06);max-width:100%}#qqs-dock-panel .dk-crow.in .dk-cbubble{background:#fff;border:1px solid #e3e6ea;color:#1f2329;border-radius:3px 10px 10px 10px}#qqs-dock-panel .dk-crow.out .dk-cbubble{background:linear-gradient(180deg,#69a6ff,#3d7df5);color:#fff;border-radius:10px 3px 10px 10px}#qqs-dock-panel .dk-img{display:block;max-width:min(230px,52vw);max-height:200px;border-radius:6px;margin:0 0 3px;object-fit:cover;cursor:zoom-in}#qqs-dock-panel .dk-audio{display:block;max-width:min(260px,60vw);width:100%;height:34px;margin:0 0 2px}#qqs-dock-panel .dk-file{display:inline-flex;align-items:center;gap:5px;max-width:100%;padding:6px 12px;border-radius:8px;background:#f0f6ff;border:1px solid #cfe0fa;color:#2b6bd8;font-size:13px;text-decoration:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#qqs-dock-panel .dk-file:hover{background:#e2edff}#qqs-dock-panel .dk-video{display:block;max-width:min(260px,60vw);max-height:180px;border-radius:6px;margin:0 0 2px}#qqs-dock-panel .dk-ctag{display:inline-block;font-size:10px;color:#5b8ff0;background:#eaf2ff;border:1px solid #d4e3fd;border-radius:8px;padding:0 6px}#qqs-dock-panel .dk-chat-top{text-align:center;color:#b0b4bb;font-size:11px;padding:2px 0 6px;user-select:none}#qqs-dock-panel .dk-chat-bottom{text-align:center;color:#c3c7cd;font-size:11px;padding:6px 0 0}#qqs-dock-panel .dk-composer{margin-top:8px;border:1px solid #e3e6ea;border-radius:10px;background:#fff;overflow:visible}#qqs-dock-panel .dk-composer textarea{width:100%;box-sizing:border-box;border:none;outline:none;resize:none;font:inherit;font-size:13px;color:#1f2329;background:transparent;padding:8px 10px 4px;line-height:1.5;max-height:120px}#qqs-dock-panel .dk-cbar{display:flex;align-items:center;gap:4px;padding:4px 8px 6px;flex-wrap:wrap}#qqs-dock-panel .dk-cbar .dk-btn{padding:3px 10px;font-size:12px;border-radius:7px}#qqs-dock-panel .dk-cbar .dk-send{background:linear-gradient(180deg,#69a6ff,#3d7df5);color:#fff;border:none;border-radius:8px;padding:5px 18px;font-size:13px;font-weight:600;cursor:pointer}#qqs-dock-panel .dk-cbar .dk-send:disabled{opacity:.5;cursor:default}#qqs-lightbox{position:fixed;inset:0;z-index:2147483000;background:rgba(8,10,18,.82);display:flex;align-items:center;justify-content:center;cursor:zoom-out}#qqs-lightbox img{max-width:92vw;max-height:92vh;border-radius:8px;box-shadow:0 10px 60px rgba(0,0,0,.6)}#qqs-dock-panel .dk-composer{position:relative}#qqs-dock-panel .dk-at-pop{position:absolute;left:6px;bottom:calc(100% - 4px);z-index:30;min-width:200px;max-width:90%;max-height:190px;overflow-y:auto;background:#fff;border:1px solid #e0e4ea;border-radius:10px;box-shadow:0 8px 24px rgba(30,40,80,.16);padding:4px;display:none}#qqs-dock-panel .dk-at-item{display:flex;align-items:center;gap:6px;padding:5px 9px;border-radius:7px;cursor:pointer;font-size:12px;color:#1f2329;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}#qqs-dock-panel .dk-at-item.on,#qqs-dock-panel .dk-at-item:hover{background:#eef3ff;color:#2b5fd0}#qqs-dock-panel .dk-at-empty{color:#aaa;font-size:12px;padding:6px 9px}#qqs-lightbox .lb-x{position:fixed;right:16px;top:10px;color:#fff;font-size:30px;cursor:pointer;line-height:1;padding:6px}#qqs-dock-panel.dk-full{left:0!important;top:0!important;right:0!important;bottom:0!important;width:100vw!important;max-width:100vw!important;height:100vh!important;max-height:100vh!important;border-radius:0;z-index:2147482000;display:flex;flex-direction:column}#qqs-dock-panel.dk-full .dk-h,#qqs-dock-panel.dk-full .dk-detect{flex:none}#qqs-dock-panel.dk-full .dk-b{flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column;padding:8px 14px 6px}#qqs-dock-panel.dk-full .dk-chat-wrap{display:flex;flex-direction:column;flex:1;min-height:0}#qqs-dock-panel.dk-full .dk-chat-head{flex:none}#qqs-dock-panel.dk-full .dk-chat-box{flex:1;height:auto!important;min-height:0!important;max-height:none!important;overflow-y:auto;overscroll-behavior:contain}#qqs-dock-panel.dk-full .dk-composer{flex:none;margin-top:6px}"
1704
+ var DOCK_CSS2 = "#qqs-dock-panel .dk-chat-head{display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin:0 0 6px}#qqs-dock-panel .dk-chat-box{overflow-y:auto;overscroll-behavior:contain;background:#f5f6f8;border:1px solid #e6e8ec;border-radius:10px;padding:10px 12px;box-sizing:border-box;height:min(36vh,300px);min-height:140px;scroll-behavior:auto}#qqs-dock-panel .dk-chat-box::-webkit-scrollbar{width:6px}#qqs-dock-panel .dk-chat-box::-webkit-scrollbar-thumb{background:#d3d7dd;border-radius:3px}#qqs-dock-panel .dk-crow{display:flex;gap:8px;align-items:flex-start;margin:0 0 12px}#qqs-dock-panel .dk-crow.out{flex-direction:row-reverse}#qqs-dock-panel .dk-ava{width:32px;height:32px;border-radius:50%;flex:none;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:600;color:#fff;overflow:hidden;user-select:none;background:linear-gradient(150deg,#8fb3e8,#5f8fd9)}#qqs-dock-panel .dk-crow.out .dk-ava{background:linear-gradient(150deg,#5ec7f2,#3b8fe0)}#qqs-dock-panel .dk-cmain{display:flex;flex-direction:column;max-width:calc(100% - 40px);min-width:0}#qqs-dock-panel .dk-crow.in .dk-cmain{align-items:flex-start}#qqs-dock-panel .dk-crow.out .dk-cmain{align-items:flex-end}#qqs-dock-panel .dk-cmeta{font-size:11px;color:#9aa0a8;margin:0 6px 2px;max-width:100%;display:flex;align-items:center;gap:5px;flex-wrap:wrap}#qqs-dock-panel .dk-crow.out .dk-cmeta{flex-direction:row-reverse}#qqs-dock-panel .dk-cbubble{padding:7px 11px;font-size:13px;line-height:1.55;white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere;box-shadow:0 1px 2px rgba(20,30,60,.06);max-width:100%}#qqs-dock-panel .dk-cbubble a.dk-link{color:#1a66d6;text-decoration:underline;word-break:break-all;cursor:pointer}#qqs-dock-panel .dk-crow.out .dk-cbubble a.dk-link{color:#eaf3ff}#qqs-dock-panel .dk-crow.in .dk-cbubble{background:#fff;border:1px solid #e3e6ea;color:#1f2329;border-radius:3px 10px 10px 10px}#qqs-dock-panel .dk-crow.out .dk-cbubble{background:linear-gradient(180deg,#69a6ff,#3d7df5);color:#fff;border-radius:10px 3px 10px 10px}#qqs-dock-panel .dk-img{display:block;max-width:min(230px,52vw);max-height:200px;border-radius:6px;margin:0 0 3px;object-fit:cover;cursor:zoom-in}#qqs-dock-panel .dk-audio{display:block;max-width:min(260px,60vw);width:100%;height:34px;margin:0 0 2px}#qqs-dock-panel .dk-file{display:inline-flex;align-items:center;gap:5px;max-width:100%;padding:6px 12px;border-radius:8px;background:#f0f6ff;border:1px solid #cfe0fa;color:#2b6bd8;font-size:13px;text-decoration:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#qqs-dock-panel .dk-file:hover{background:#e2edff}#qqs-dock-panel .dk-video{display:block;max-width:min(260px,60vw);max-height:180px;border-radius:6px;margin:0 0 2px}#qqs-dock-panel .dk-ctag{display:inline-block;font-size:10px;color:#5b8ff0;background:#eaf2ff;border:1px solid #d4e3fd;border-radius:8px;padding:0 6px}#qqs-dock-panel .dk-chat-top{text-align:center;color:#b0b4bb;font-size:11px;padding:2px 0 6px;user-select:none}#qqs-dock-panel .dk-chat-bottom{text-align:center;color:#c3c7cd;font-size:11px;padding:6px 0 0}#qqs-dock-panel .dk-composer{margin-top:8px;border:1px solid #e3e6ea;border-radius:10px;background:#fff;overflow:visible}#qqs-dock-panel .dk-composer textarea{width:100%;box-sizing:border-box;border:none;outline:none;resize:none;font:inherit;font-size:13px;color:#1f2329;background:transparent;padding:8px 10px 4px;line-height:1.5;max-height:120px}#qqs-dock-panel .dk-cbar{display:flex;align-items:center;gap:4px;padding:4px 8px 6px;flex-wrap:wrap}#qqs-dock-panel .dk-cbar .dk-btn{padding:3px 10px;font-size:12px;border-radius:7px}#qqs-dock-panel .dk-cbar .dk-send{background:linear-gradient(180deg,#69a6ff,#3d7df5);color:#fff;border:none;border-radius:8px;padding:5px 18px;font-size:13px;font-weight:600;cursor:pointer}#qqs-dock-panel .dk-cbar .dk-send:disabled{opacity:.5;cursor:default}#qqs-lightbox{position:fixed;inset:0;z-index:2147483000;background:rgba(8,10,18,.82);display:flex;align-items:center;justify-content:center;cursor:zoom-out}#qqs-lightbox img{max-width:92vw;max-height:92vh;border-radius:8px;box-shadow:0 10px 60px rgba(0,0,0,.6)}#qqs-dock-panel .dk-composer{position:relative}#qqs-dock-panel .dk-at-pop{position:absolute;left:6px;bottom:calc(100% - 4px);z-index:30;min-width:200px;max-width:90%;max-height:190px;overflow-y:auto;background:#fff;border:1px solid #e0e4ea;border-radius:10px;box-shadow:0 8px 24px rgba(30,40,80,.16);padding:4px;display:none}#qqs-dock-panel .dk-at-item{display:flex;align-items:center;gap:6px;padding:5px 9px;border-radius:7px;cursor:pointer;font-size:12px;color:#1f2329;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}#qqs-dock-panel .dk-at-item.on,#qqs-dock-panel .dk-at-item:hover{background:#eef3ff;color:#2b5fd0}#qqs-dock-panel .dk-at-empty{color:#aaa;font-size:12px;padding:6px 9px}#qqs-lightbox .lb-x{position:fixed;right:16px;top:10px;color:#fff;font-size:30px;cursor:pointer;line-height:1;padding:6px}#qqs-dock-panel.dk-full{left:0!important;top:0!important;right:0!important;bottom:0!important;width:100vw!important;max-width:100vw!important;height:100vh!important;max-height:100vh!important;border-radius:0;z-index:2147482000;display:flex;flex-direction:column}#qqs-dock-panel.dk-full .dk-h,#qqs-dock-panel.dk-full .dk-detect{flex:none}#qqs-dock-panel.dk-full .dk-b{flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column;padding:8px 14px 6px}#qqs-dock-panel.dk-full .dk-chat-wrap{display:flex;flex-direction:column;flex:1;min-height:0}#qqs-dock-panel.dk-full .dk-chat-head{flex:none}#qqs-dock-panel.dk-full .dk-chat-box{flex:1;height:auto!important;min-height:0!important;max-height:none!important;overflow-y:auto;overscroll-behavior:contain}#qqs-dock-panel.dk-full .dk-composer{flex:none;margin-top:6px}"
1705
1705
  function ensureDockCss2() { try { if (!document.getElementById('qqs-dock-css2')) { var st = document.createElement('style'); st.id = 'qqs-dock-css2'; st.textContent = DOCK_CSS2; document.head.appendChild(st) } } catch (e) {} };
1706
1706
  function ensureDockCss() { try { if (!document.getElementById('qqs-dock-css')) { var st = document.createElement('style'); st.id = 'qqs-dock-css'; st.textContent = DOCK_CSS; document.head.appendChild(st) } } catch (e) {} }
1707
1707
  function startQqDock(sessionsSvc) {
@@ -1886,7 +1886,16 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
1886
1886
  setInterval(refreshBadge, 20000)
1887
1887
 
1888
1888
  // ── 面板状态(每个实例独立保存, 切回不丢) ──
1889
- var state = { ns: '', accts: [], gid: '', groups: [], tab: 'chat', sendScope: 'group', sendTo: '', sendName: '', sendText: '', insertCtx: true, targetQ: '', c2cs: [], joins: null, mutes: null, members: null, muteSecs: '60', bindGid: '', bindName: '', msg: '', busy: '', wantPeer: null, lookedUp: false, detected: null, detectedHit: null, chatItems: [], chatMore: false, chatBusy: '', chatErr: '', chatOldest: 0, chatText: '', chatIns: true, outMode: '', outRev: undefined, bpEvents: [], bpSel: null, bpDraft: null, rosterSel: {}, rosterScope: 'all', rosterQ: '', hubSid: '', hubRev: undefined, hubBusy: '', hubMsg: '' }
1889
+ var state = { ns: '', accts: [], gid: '', groups: [], tab: 'chat', sendScope: 'group', sendTo: '', sendName: '', sendText: '', insertCtx: true, targetQ: '', c2cs: [], joins: null, mutes: null, members: null, muteSecs: '60', bindGid: '', bindName: '', msg: '', busy: '', wantPeer: null, lookedUp: false, detected: null, detectedHit: null, chatItems: [], chatMore: false, chatBusy: '', chatErr: '', chatOldest: 0, chatText: '', chatIns: true, outMode: '', outRev: undefined, bpEvents: [], bpSel: null, bpDraft: null, rosterSel: {}, rosterScope: 'all', rosterQ: '', hubSid: '', hubRev: undefined, hubBusy: '', hubMsg: '', gaEnabled: false, gaPoll: false, gaPollWake: true, gaHubNotify: true, gaNotifyGroup: true, gaInterval: 5, gaMinCount: 1, gaMsg: '', gaBusy: '', bcDraft: null, bcTasks: null, cardMd: '', cardBtns: '', cardGid: '', cardBusy: '', cardQ: '', tgGroups: [], tgCur: '' }
1890
+ // 🗂 自定义目标分组(仿 QQ 分组, 2026-09-10 主人要求): localStorage 持久化
1891
+ // 结构: [{ id, name, members: ['group:xxx' | 'c2c:yyy', ...] }]
1892
+ try {
1893
+ var _tg = localStorage.getItem('qqs-target-groups')
1894
+ if (_tg) { var _tgo = JSON.parse(_tg); if (Array.isArray(_tgo)) state.tgGroups = _tgo }
1895
+ } catch (e) {}
1896
+ function saveTgGroups() {
1897
+ try { localStorage.setItem('qqs-target-groups', JSON.stringify(state.tgGroups || [])) } catch (e) {}
1898
+ }
1890
1899
  // 📇 群组管理 M1: 勾选集合本地持久化(刷新/重开不丢, 供后续群发/批量操作使用)
1891
1900
  try { var _rs = localStorage.getItem('qqs-roster-sel'); if (_rs) { var _rso = JSON.parse(_rs); if (_rso && typeof _rso === 'object') state.rosterSel = _rso } } catch (e) {}
1892
1901
  var chatFlash = '' // 发送结果/错误提示(短时展示, 不被列表计数覆盖)
@@ -2069,6 +2078,12 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2069
2078
  var box = panel.querySelector('#dk-roster-list')
2070
2079
  if (!box) return
2071
2080
  var rows = rosterRows()
2081
+ // 🗂 分组筛选(仅"选目标"页生效): 选中某分组 → 只列该组成员(2026-09-10)
2082
+ if (state.tab === 'send' && state.tgCur) {
2083
+ var curG = (state.tgGroups || []).filter(function (g2) { return g2 && g2.id === state.tgCur })[0]
2084
+ var mem = (curG && curG.members) || []
2085
+ rows = rows.filter(function (r) { return mem.indexOf(r.key) >= 0 })
2086
+ }
2072
2087
  var stat = panel.querySelector('#dk-roster-stat')
2073
2088
  if (stat) stat.textContent = '共 ' + rows.length + ' 条 · 已选 ' + rosterSelN()
2074
2089
  if (!rows.length) {
@@ -2125,6 +2140,64 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2125
2140
  if (!cb || cb === box) return
2126
2141
  rosterToggle(cb.getAttribute('data-rkey'))
2127
2142
  }
2143
+ // 🗂 分组管理(仿 QQ 分组, 2026-09-10 主人要求; 选择器页专属元素, 不在时自动跳过)
2144
+ var tgCurGroup = function () { return (state.tgGroups || []).filter(function (g2) { return g2 && g2.id === state.tgCur })[0] }
2145
+ var tgPersistSel = function () { try { localStorage.setItem('qqs-roster-sel', JSON.stringify(state.rosterSel)) } catch (e) {} }
2146
+ var tgSel = panel.querySelector('#dk-tg-sel')
2147
+ if (tgSel) tgSel.onchange = function () { state.tgCur = tgSel.value; paintBody(); bindBodyEvents() }
2148
+ var tgNew = panel.querySelector('#dk-tg-new')
2149
+ if (tgNew) tgNew.onclick = function () {
2150
+ var nm = window.prompt('新建分组名称:', '新分组')
2151
+ if (!nm) return
2152
+ var id = 'tg' + Date.now().toString(36).slice(-5)
2153
+ state.tgGroups.push({ id: id, name: String(nm).slice(0, 20), members: [] })
2154
+ state.tgCur = id
2155
+ saveTgGroups(); paintBody(); bindBodyEvents()
2156
+ }
2157
+ var tgRen = panel.querySelector('#dk-tg-rename')
2158
+ if (tgRen) tgRen.onclick = function () {
2159
+ var cur = tgCurGroup(); if (!cur) return
2160
+ var nm = window.prompt('改名:', cur.name || '')
2161
+ if (!nm) return
2162
+ cur.name = String(nm).slice(0, 20)
2163
+ saveTgGroups(); paintBody(); bindBodyEvents()
2164
+ }
2165
+ var tgDel = panel.querySelector('#dk-tg-del')
2166
+ if (tgDel) tgDel.onclick = function () {
2167
+ var cur = tgCurGroup(); if (!cur) return
2168
+ if (!window.confirm('删除分组「' + (cur.name || '') + '」? (只删分组, 不影响目标本身)')) return
2169
+ state.tgGroups = (state.tgGroups || []).filter(function (g2) { return g2.id !== state.tgCur })
2170
+ state.tgCur = ''
2171
+ saveTgGroups(); paintBody(); bindBodyEvents()
2172
+ }
2173
+ var tgAdd = panel.querySelector('#dk-tg-add')
2174
+ if (tgAdd) tgAdd.onclick = function () {
2175
+ var cur = tgCurGroup(); if (!cur) return
2176
+ var sel = Object.keys(state.rosterSel || {}).filter(function (k) { return state.rosterSel[k] })
2177
+ if (!sel.length) { window.alert('先在下面勾选目标, 再点加入本组'); return }
2178
+ var mem = cur.members || (cur.members = [])
2179
+ var n = 0
2180
+ sel.forEach(function (k) { if (mem.indexOf(k) < 0) { mem.push(k); n++ } })
2181
+ saveTgGroups(); paintBody(); bindBodyEvents()
2182
+ window.alert('已加入 ' + n + ' 个(本组共 ' + mem.length + ' 个)')
2183
+ }
2184
+ var tgRm = panel.querySelector('#dk-tg-remove')
2185
+ if (tgRm) tgRm.onclick = function () {
2186
+ var cur = tgCurGroup(); if (!cur) return
2187
+ var sel = Object.keys(state.rosterSel || {}).filter(function (k) { return state.rosterSel[k] })
2188
+ var before = (cur.members || []).length
2189
+ cur.members = (cur.members || []).filter(function (k) { return sel.indexOf(k) < 0 })
2190
+ saveTgGroups(); paintBody(); bindBodyEvents()
2191
+ window.alert('已移出 ' + (before - cur.members.length) + ' 个')
2192
+ }
2193
+ var tgPickAll = panel.querySelector('#dk-tg-pickall')
2194
+ if (tgPickAll) tgPickAll.onclick = function () {
2195
+ var cur = tgCurGroup(); if (!cur) return
2196
+ ;(cur.members || []).forEach(function (k) { state.rosterSel[k] = true })
2197
+ tgPersistSel(); paintBody(); bindBodyEvents()
2198
+ }
2199
+ var tgGoto = panel.querySelector('#dk-pick-goto')
2200
+ if (tgGoto) tgGoto.onclick = function () { state.tab = 'broadcast'; paintBody(); bindBodyEvents() }
2128
2201
  // 🎯 群组管理器: 设本会话/取消(走 settings groupAdmin.hubSessionId, live 热更)
2129
2202
  var hubSet = panel.querySelector('#dk-hub-set')
2130
2203
  if (hubSet) hubSet.onclick = function () {
@@ -2134,6 +2207,26 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2134
2207
  }
2135
2208
  var hubClr = panel.querySelector('#dk-hub-clear')
2136
2209
  if (hubClr) hubClr.onclick = function () { saveHub('') }
2210
+ // ⚙ 群组设置(轮询等): 读控件 → 保存到 settings groupAdmin(live 热更)
2211
+ var gaSave = panel.querySelector('#dk-ga-save')
2212
+ if (gaSave) gaSave.onclick = function () {
2213
+ var read = function (id) { var el = panel.querySelector(id); return el ? el.checked : false }
2214
+ var num = function (id, dft) {
2215
+ var el = panel.querySelector(id)
2216
+ var v = parseInt(el ? el.value : '', 10)
2217
+ return isNaN(v) || v < 1 ? dft : v
2218
+ }
2219
+ state.gaEnabled = read('#dk-ga-enabled')
2220
+ state.gaPoll = read('#dk-ga-poll')
2221
+ state.gaPollWake = read('#dk-ga-pollwake')
2222
+ state.gaHubNotify = read('#dk-ga-hubnotify')
2223
+ state.gaNotifyGroup = read('#dk-ga-notifygroup')
2224
+ state.gaInterval = num('#dk-ga-interval', 5)
2225
+ state.gaMinCount = num('#dk-ga-mincount', 1)
2226
+ saveGroupAdmin()
2227
+ }
2228
+ // M3 群发(自动审批策略已按主人要求移除 2026-09-10)
2229
+ bindBroadcastEvents()
2137
2230
  }
2138
2231
  function hubStatusText() {
2139
2232
  if (state.hubSid) return '已设: 会话 ' + state.hubSid.slice(0, 8) + '… ← 各群群事件(入群申请/新成员/被拉群)汇总注入这里'
@@ -2154,8 +2247,66 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2154
2247
  if (st) st.textContent = hubStatusText()
2155
2248
  var clr = panel && panel.querySelector('#dk-hub-clear')
2156
2249
  if (clr) clr.disabled = !state.hubSid
2250
+ // ⚙ 群组设置: 载入轮询/开关当前值
2251
+ var poll = ga.pollJoinRequests || {}
2252
+ state.gaEnabled = ga.enabled === true
2253
+ state.gaPoll = poll.enabled === true
2254
+ state.gaPollWake = poll.wakeLlm !== false
2255
+ state.gaHubNotify = poll.hubNotify !== false
2256
+ state.gaNotifyGroup = poll.notifyGroup !== false
2257
+ state.gaInterval = poll.intervalMin || 5
2258
+ state.gaMinCount = poll.minCount || 1
2259
+ var e1 = panel && panel.querySelector('#dk-ga-enabled'); if (e1) e1.checked = state.gaEnabled
2260
+ var e2 = panel && panel.querySelector('#dk-ga-poll'); if (e2) e2.checked = state.gaPoll
2261
+ var e3 = panel && panel.querySelector('#dk-ga-pollwake'); if (e3) e3.checked = state.gaPollWake
2262
+ var e4 = panel && panel.querySelector('#dk-ga-hubnotify'); if (e4) e4.checked = state.gaHubNotify
2263
+ var e4b = panel && panel.querySelector('#dk-ga-notifygroup'); if (e4b) e4b.checked = state.gaNotifyGroup
2264
+ var e5 = panel && panel.querySelector('#dk-ga-interval'); if (e5) e5.value = state.gaInterval
2265
+ var e6 = panel && panel.querySelector('#dk-ga-mincount'); if (e6) e6.value = state.gaMinCount
2157
2266
  }).catch(function () {})
2158
2267
  }
2268
+ function gaHint(t) {
2269
+ state.gaMsg = t || ''
2270
+ var el = panel && panel.querySelector('#dk-ga-hint')
2271
+ if (el) el.textContent = state.gaMsg
2272
+ }
2273
+ function saveGroupAdmin() {
2274
+ state.gaBusy = 'ga'; gaHint('保存中…')
2275
+ var doSave = function () {
2276
+ var trySave = function (cur, rev) {
2277
+ var patch = Object.assign({}, cur || {})
2278
+ var ga = Object.assign({}, (cur && cur.groupAdmin) || {}, {
2279
+ enabled: state.gaEnabled,
2280
+ pollJoinRequests: {
2281
+ enabled: state.gaPoll,
2282
+ intervalMin: state.gaInterval,
2283
+ minCount: state.gaMinCount,
2284
+ wakeLlm: state.gaPollWake,
2285
+ hubNotify: state.gaHubNotify,
2286
+ notifyGroup: state.gaNotifyGroup,
2287
+ },
2288
+ })
2289
+ patch.groupAdmin = ga
2290
+ return fetch(UPDATE, {
2291
+ method: 'POST', headers: { 'content-type': 'application/json' },
2292
+ body: JSON.stringify({ ns: state.ns || undefined, patch: patch, expectedRevision: rev }),
2293
+ }).then(function (r) { return r.json().catch(function () { return null }) }).then(function (d2) {
2294
+ state.gaBusy = ''
2295
+ if (d2 && d2.value) {
2296
+ state.hubRev = d2.revision
2297
+ gaHint('✓ 群组设置已保存(live 生效, 轮询无需重启)')
2298
+ } else {
2299
+ gaHint('保存失败: ' + ((d2 && d2.error) || '未知错误') + (d2 && String(d2.error || '').indexOf('changed since') >= 0 ? '(冲突, 请重试)' : ''))
2300
+ }
2301
+ }).catch(function () { state.gaBusy = ''; gaHint('保存异常') })
2302
+ }
2303
+ fetch(READ + (state.ns ? '?' + outNsQ() : '')).then(function (r) { return r.json() }).then(function (d) {
2304
+ if (d && d.value) trySave(d.value, d.revision)
2305
+ else { state.gaBusy = ''; gaHint('保存失败: 无法读取当前设置') }
2306
+ }).catch(function () { state.gaBusy = ''; gaHint('保存异常: 读取失败') })
2307
+ }
2308
+ doSave()
2309
+ }
2159
2310
  function saveHub(sid) {
2160
2311
  state.hubBusy = 'hub'; hubHint('保存中…')
2161
2312
  var doSave = function () {
@@ -2200,6 +2351,267 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2200
2351
  doSave()
2201
2352
  }).catch(function () { doSave() })
2202
2353
  }
2354
+ // ── M3 群发(勾选集合 → 草稿 → 二次确认 → 任务列表) ──
2355
+ function rosterSelectedTargets() {
2356
+ var out = []
2357
+ rosterRows().forEach(function (r) {
2358
+ if (state.rosterSel[r.key]) out.push({ scope: r.scope, peerId: r.id, name: r.name || '' })
2359
+ })
2360
+ return out
2361
+ }
2362
+ function openBroadcastDraft(text) {
2363
+ var tg = rosterSelectedTargets()
2364
+ if (!tg.length) { state.msg = '请先勾选至少一个目标'; paintBody(); return }
2365
+ state.bcDraft = { text: text || '', targets: tg }
2366
+ paintBody()
2367
+ var ta = panel && panel.querySelector('#dk-bc-text')
2368
+ if (ta) ta.focus()
2369
+ }
2370
+ function bcHint(t) {
2371
+ state.msg = t || ''
2372
+ var el = panel && panel.querySelector('.dk-msg#dk-bc-hint')
2373
+ if (el) el.textContent = t || ''
2374
+ }
2375
+ /**
2376
+ * 拉取群发任务列表。
2377
+ * ⚠️ 2026-09-10 修复死循环: 本函数原本无条件 paintBody(), 而 paintBody() 内又会调用本函数
2378
+ * (roster / broadcast 两个 tab) → 请求返回即重绘、重绘又请求 → 无限重建 DOM,
2379
+ * 表现为 dock 卡死、按钮刚绑定就被替换掉点不动。
2380
+ * 现: ①防重入(bcTaskLoading); ②silent=true 时只更新 state 不重绘(paintBody 内一律用 silent)。
2381
+ */
2382
+ var bcTaskLoading = false
2383
+ function loadBroadcastTasks(advance, silent) {
2384
+ // advance=true 是"推进任务"(会真的发消息), 必须执行, 不被防重入挡
2385
+ // (2026-09-10 修复: 原实现一律挡, paintBody 的静默请求在飞时用户点"确认群发"会被吞 → 任务一直 queued 发不出去)
2386
+ if (bcTaskLoading && !advance) return
2387
+ bcTaskLoading = true
2388
+ api('group/broadcast/list' + (advance ? '' : '?noadvance=1')).then(function (d) {
2389
+ if (d && d.ok && Array.isArray(d.tasks)) {
2390
+ state.bcTasks = d.tasks
2391
+ if (silent !== true) paintBody()
2392
+ }
2393
+ }).catch(function () {}).then(function () { bcTaskLoading = false })
2394
+ }
2395
+ // 广播子页: 有活跃任务时自动推进(每 3s 一次, 直到无 queued/sending) —— 2026-09-10
2396
+ // 任务推进靠 list 请求驱动(host 侧串行 advanceTask 天然限频), 所以必须有节奏地拉取
2397
+ var bcAdvTimer = null
2398
+ function startBroadcastAdvance() {
2399
+ if (bcAdvTimer) return
2400
+ bcAdvTimer = setInterval(function () {
2401
+ var tasks = state.bcTasks || []
2402
+ var active = tasks.some(function (t) { return t.state === 'queued' || t.state === 'sending' })
2403
+ if (!active) { clearInterval(bcAdvTimer); bcAdvTimer = null; return }
2404
+ loadBroadcastTasks(true, false)
2405
+ }, 3000)
2406
+ }
2407
+ function bindBroadcastEvents() {
2408
+ if (!panel) return
2409
+ var sendB = panel.querySelector('#dk-roster-send')
2410
+ if (sendB) sendB.onclick = function () { openBroadcastDraft('') }
2411
+ // 广播子页: 跳去「📇 群组管理」勾选目标(2026-09-10)
2412
+ var gotoB = panel.querySelector('#dk-bc-goto')
2413
+ if (gotoB) gotoB.onclick = function () { state.tab = 'roster'; paintBody(); bindBodyEvents() }
2414
+ var cc = panel.querySelector('#dk-bc-cancel')
2415
+ if (cc) cc.onclick = function () { state.bcDraft = null; paintBody() }
2416
+ var cf = panel.querySelector('#dk-bc-confirm')
2417
+ if (cf) cf.onclick = function () {
2418
+ if (!state.bcDraft) return
2419
+ var ta = panel.querySelector('#dk-bc-text')
2420
+ var text = ta ? ta.value.trim() : state.bcDraft.text
2421
+ if (!text) { bcHint('内容不能为空'); return }
2422
+ var targets = state.bcDraft.targets
2423
+ apiPost('group/broadcast/create', { ns: state.ns || undefined, type: 'text', text: text, targets: targets }).then(function (d) {
2424
+ if (!d || !d.ok || !d.task) { bcHint((d && d.error) || '创建失败'); return }
2425
+ // 二次确认: 已弹「确认群发」按钮, 这里直接 confirm 入队
2426
+ return apiPost('group/broadcast/confirm', { ns: state.ns || undefined, task_id: d.task.task_id }).then(function (d2) {
2427
+ state.bcDraft = null
2428
+ if (d2 && d2.ok) { bcHint('✅ 已确认群发 ' + targets.length + ' 个目标(逐目标发送中)'); loadBroadcastTasks(true) }
2429
+ else bcHint((d2 && (d2.msg || d2.error)) || '确认失败')
2430
+ })
2431
+ }).catch(function () { bcHint('创建异常') })
2432
+ }
2433
+ var refreshB = panel.querySelector('#dk-bc-refresh')
2434
+ if (refreshB) refreshB.onclick = function () { loadBroadcastTasks(true) }
2435
+ var tlist = panel.querySelector('#dk-bc-tasks')
2436
+ if (tlist) tlist.onclick = function (e) {
2437
+ var b = e.target
2438
+ var cn = b.getAttribute && b.getAttribute('data-bccancel')
2439
+ if (cn) { apiPost('group/broadcast/cancel', { ns: state.ns || undefined, task_id: cn }).then(function () { loadBroadcastTasks(true) }); return }
2440
+ var rn = b.getAttribute && b.getAttribute('data-bcrecall')
2441
+ if (rn) {
2442
+ var tk = (state.bcTasks || []).find(function (x) { return x.task_id === rn })
2443
+ if (!tk) return
2444
+ var peer = window.prompt('撤回哪个目标的已发消息? 填目标 peerId(…后6位即可)\n' + tk.targets.map(function (t2) { return t2.peerId.slice(-6) + ' ' + (t2.name || '') }).join('\n'))
2445
+ if (!peer) return
2446
+ var hit = tk.targets.find(function (t2) { return t2.peerId.endsWith(peer) })
2447
+ if (!hit) { bcHint('未匹配到该目标'); return }
2448
+ apiPost('group/broadcast/recall', { ns: state.ns || undefined, task_id: rn, peerId: hit.peerId }).then(function (d) { bcHint(d && d.ok ? '✅ 已撤回' : ((d && d.err) || '撤回失败')) })
2449
+ }
2450
+ }
2451
+ }
2452
+ // ── 📝 Markdown 卡片编辑器(2026-09-10): 源码+实时预览+按钮 → send-card 发送 ──
2453
+ function mdEscapeHtml(s) { return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') }
2454
+ function mdRender(src) {
2455
+ var s = String(src || '')
2456
+ // 代码块(先保护)
2457
+ var codeBlocks = []
2458
+ s = s.replace(/```([\s\S]*?)```/g, function (_, c) { codeBlocks.push(c); return '\u0000CODE' + (codeBlocks.length - 1) + '\u0000' })
2459
+ var lines = s.split('\n')
2460
+ var html = '', inList = false, inTable = false, table = []
2461
+ function closeList() { if (inList) { html += '</ul>'; inList = false } }
2462
+ function closeTable() { if (inTable) { html += '</table>'; inTable = false } }
2463
+ lines.forEach(function (line) {
2464
+ var t = line.trim()
2465
+ if (t === '') { closeList(); closeTable(); html += '<div style="height:6px"></div>'; return }
2466
+ if (/^```/.test(t)) return
2467
+ if (/^\u0000CODE\d+\u0000$/.test(t)) { closeList(); closeTable(); var i = parseInt(t.replace(/\D/g, ''), 10); html += '<pre style="background:#f6f8fa;border-radius:4px;padding:6px;font-size:11px;overflow:auto;margin:2px 0">' + mdEscapeHtml(codeBlocks[i]) + '</pre>'; return }
2468
+ var inline = function (x) {
2469
+ x = x.replace(/!\[([^\]]*)\]\(([^)\s]+)(?: #(\d+)px #(\d+)px)?\)/g, function (_, alt, url, w, h) { var st = w ? ' style="width:' + w + 'px;height:' + (h || 'auto') + 'px"' : ''; return '<img src="' + mdEscapeHtml(url) + '" alt="' + mdEscapeHtml(alt || '') + '"' + st + ' style="max-width:100%;border-radius:6px;margin:2px 0">' })
2470
+ x = x.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, '<a href="' + '$2' + '" target="_blank" style="color:#4b7bec">$1</a>')
2471
+ x = x.replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>').replace(/\*([^*]+)\*/g, '<i>$1</i>').replace(/`([^`]+)`/g, '<code style="background:#f1f3f5;border-radius:3px;padding:0 3px">$1</code>')
2472
+ return x
2473
+ }
2474
+ if (/^###\s+/.test(t)) { closeList(); closeTable(); html += '<div style="font-weight:700;font-size:13px;margin:4px 0 2px">' + inline(t.replace(/^###\s+/, '')) + '</div>' }
2475
+ else if (/^##\s+/.test(t)) { closeList(); closeTable(); html += '<div style="font-weight:700;font-size:15px;margin:4px 0 2px">' + inline(t.replace(/^##\s+/, '')) + '</div>' }
2476
+ else if (/^#\s+/.test(t)) { closeList(); closeTable(); html += '<div style="font-weight:700;font-size:17px;margin:4px 0 2px">' + inline(t.replace(/^#\s+/, '')) + '</div>' }
2477
+ else if (/^>\s*/.test(t)) { closeList(); closeTable(); html += '<div style="border-left:3px solid #4b7bec;padding:2px 8px;color:#666;background:#f4f7ff;border-radius:0 4px 4px 0;margin:2px 0">' + inline(t.replace(/^>\s*/, '')) + '</div>' }
2478
+ else if (/^[-*]\s+/.test(t)) { closeTable(); if (!inList) { inList = true; html += '<ul style="margin:2px 0;padding-left:18px">' } html += '<li>' + inline(t.replace(/^[-*]\s+/, '')) + '</li>' }
2479
+ else if (/^\d+\.\s+/.test(t)) { closeTable(); if (!inList) { inList = true; html += '<ul style="margin:2px 0;padding-left:18px">' } html += '<li>' + inline(t.replace(/^\d+\.\s+/, '')) + '</li>' }
2480
+ else if (/^\|/.test(t)) {
2481
+ closeList()
2482
+ var cells = t.replace(/^\||\|$/g, '').split('|').map(function (c) { return c.trim() })
2483
+ if (/^[-:]+$/.test(cells.join('')) && table.length) { return } // 分隔行跳过
2484
+ if (!inTable) { inTable = true; table = []; html += '<table style="border-collapse:collapse;font-size:11px;margin:2px 0"><tr>' }
2485
+ html += '<tr>' + cells.map(function (c) { return '<td style="border:1px solid #dde;padding:2px 6px">' + inline(c) + '</td>' }).join('') + '</tr>'
2486
+ }
2487
+ else { closeList(); closeTable(); html += '<div style="margin:2px 0">' + inline(t) + '</div>' }
2488
+ })
2489
+ closeList(); closeTable()
2490
+ return html
2491
+ }
2492
+ // 按钮动作判定(2026-09-10 主人要求: 卡片按钮要能写「回文本 / 跳链接 / 执行命令」):
2493
+ // 第三段显式指定 → url/链接 | cmd/命令 | text/回复; 不写则智能判断:
2494
+ // 以 http(s) 开头 = 跳链接; 以 / 开头 = 执行命令; 其余 = 回文本
2495
+ function cardBtnAction(typeRaw, data, hasType) {
2496
+ var t = String(typeRaw || '').toLowerCase()
2497
+ if (hasType) {
2498
+ if (t === 'url' || t === 'link' || t === '跳转') return 'url'
2499
+ if (t === 'cmd' || t === 'command' || t === '命令') return 'cmd'
2500
+ if (t === 'text' || t === 'reply' || t === '文本') return 'text'
2501
+ }
2502
+ if (/^https?:\/\//i.test(data)) return 'url'
2503
+ if (String(data).charAt(0) === '/') return 'cmd'
2504
+ return 'text'
2505
+ }
2506
+ function parseCardButtons(text) {
2507
+ // 每行: 文字|内容 或 文字|内容|url|cmd|text
2508
+ var out = []
2509
+ var rows = String(text || '').split('\n').map(function (s) { return s.trim() }).filter(Boolean)
2510
+ for (var i = 0; i < rows.length; i++) {
2511
+ var parts = rows[i].split('|').map(function (s) { return s.trim() })
2512
+ if (parts.length < 2 || !parts[0]) continue
2513
+ var label = parts[0], data = parts[1]
2514
+ var action = cardBtnAction(parts.length >= 3 ? parts[2] : '', data, parts.length >= 3)
2515
+ out.push({ id: 'btn' + (i + 1), label: label, data: data, action: action, isUrl: action === 'url' })
2516
+ }
2517
+ return out
2518
+ }
2519
+ function bindCardEvents() {
2520
+ if (!panel) return
2521
+ var q = function (sel) { return panel.querySelector(sel) }
2522
+ var mdEl = q('#dk-card-md')
2523
+ var prevEl = q('#dk-card-preview')
2524
+ var render = function () { if (mdEl && prevEl) prevEl.innerHTML = mdRender(mdEl.value) }
2525
+ if (mdEl) mdEl.oninput = function () { state.cardMd = mdEl.value; render() }
2526
+ var btnsEl = q('#dk-card-btns')
2527
+ if (btnsEl) btnsEl.oninput = function () { state.cardBtns = btnsEl.value }
2528
+ var gsel = q('#dk-card-gid')
2529
+ if (gsel) {
2530
+ // 默认目标同步: 避免"没手动选过"就报未选目标(2026-09-10 修复)
2531
+ if (gsel.value && !state.cardGid) state.cardGid = gsel.value
2532
+ gsel.onchange = function () { state.cardGid = gsel.value }
2533
+ }
2534
+ // 目标搜索(2026-09-10 主人要求): 输入即过滤群/私聊列表
2535
+ var cq = q('#dk-card-q')
2536
+ if (cq) {
2537
+ cq.value = state.cardQ || ''
2538
+ cq.oninput = function () { state.cardQ = cq.value }
2539
+ cq.onchange = function () { state.cardQ = cq.value; paintBody() }
2540
+ cq.onkeydown = function (e) { if (e && e.key === 'Enter') { state.cardQ = cq.value; paintBody() } }
2541
+ }
2542
+ render()
2543
+ var chHint = function (t) { var h = q('#dk-card-hint'); if (h) h.textContent = t || '' }
2544
+ // 实时读当前值: DOM 可能被 paintBody 重建, 不能依赖绑定时捕获的引用(2026-09-10 修复发送无反应)
2545
+ var readCard = function () {
2546
+ var m = q('#dk-card-md'), b = q('#dk-card-btns'), g = q('#dk-card-gid')
2547
+ var md = m ? m.value.trim() : String(state.cardMd || '').trim()
2548
+ var btns = parseCardButtons(b ? b.value : (state.cardBtns || ''))
2549
+ var raw = String((g && g.value) || state.cardGid || '')
2550
+ var isC2c = raw.indexOf('c2c:') === 0
2551
+ return { md: md, btns: btns, raw: raw, gid: isC2c ? '' : raw, openid: isC2c ? raw.slice(4) : '' }
2552
+ }
2553
+ var sendB = q('#dk-card-send')
2554
+ if (sendB) sendB.onclick = function () {
2555
+ var v = readCard()
2556
+ if (!v.md) { chHint('markdown 内容不能为空'); return }
2557
+ if (!v.raw) { chHint('请先选目标(群或私聊)'); return }
2558
+ state.cardBusy = 'card'; sendB.disabled = true; chHint(v.openid ? '发送中(私聊)…' : '发送中(群)…')
2559
+ // 按钮走结构化路径交给 host: host 生成 cardId + 回调表(回文本/执行命令靠它), 并编好 bpk: 键盘
2560
+ var cardButtons = v.btns.map(function (b) { return { label: b.label, action: b.action, payload: b.data } })
2561
+ apiPost('chat/send-card', { ns: state.ns || undefined, gid: v.gid || undefined, openid: v.openid || undefined, markdown: v.md, cardButtons: cardButtons.length ? cardButtons : undefined }).then(function (d) {
2562
+ state.cardBusy = ''; sendB.disabled = false
2563
+ chHint(d && d.ok ? ((d.msg || '✅ 卡片已发送!')) : ((d && d.err && d.err.human) || (d && d.error) || '发送失败'))
2564
+ }).catch(function () { state.cardBusy = ''; sendB.disabled = false; chHint('发送异常') })
2565
+ }
2566
+ // 💾 存为事件: 当前 markdown+按钮 → botplay 事件(独立文件, /botplay 一键发卡)
2567
+ var saveB = q('#dk-card-save')
2568
+ if (saveB) saveB.onclick = function () {
2569
+ var v = readCard()
2570
+ if (!v.md) { chHint('markdown 内容不能为空'); return }
2571
+ if (!v.btns.length) { chHint('请至少配一个按钮(存为事件需要按钮)'); return }
2572
+ state.cardBusy = 'save'; saveB.disabled = true; chHint('保存中…')
2573
+ api('group/botplay-events', state.ns ? 'ns=' + encodeURIComponent(state.ns) : '').then(function (d) {
2574
+ var events = (d && d.ok && Array.isArray(d.events)) ? d.events : []
2575
+ var ev = {
2576
+ id: 'card' + Date.now().toString(36).slice(-6),
2577
+ name: v.md.replace(/^#+\s*/, '').split('\n')[0].slice(0, 12) || '卡片',
2578
+ contentText: v.md, maxClicks: 0, expireSec: 600, buttonsPerRow: 1,
2579
+ perm: { type: 'all', userIds: [] },
2580
+ buttons: v.btns.map(function (b, i) {
2581
+ // 三型动作 → botplay 的 botAction(回文本/命令/跳转), 与卡片发送行为一致
2582
+ var act = b.action === 'url' ? { type: 'jump_url', text: '', url: b.data }
2583
+ : (b.action === 'cmd' ? { type: 'command', text: b.data.replace(/^\//, ''), url: '' }
2584
+ : { type: 'reply_text', text: b.data, url: '' })
2585
+ return { id: 'b' + (i + 1), label: b.label, style: 1, botAction: act, llmEffect: { mode: 'no_append', contextText: '' } }
2586
+ }),
2587
+ }
2588
+ events.push(ev)
2589
+ return apiPost('group/botplay-events', { ns: state.ns || 'im-qqbot', events: events }).then(function (d2) {
2590
+ state.cardBusy = ''; saveB.disabled = false
2591
+ chHint(d2 && d2.ok ? ('✅ 已存为事件「' + ev.name + '」(id=' + ev.id + '), 群内发 /botplay ' + ev.id + ' 可一键发卡') : '保存失败: ' + ((d2 && (d2.error || d2.msg)) || '未知错误'))
2592
+ })
2593
+ }).catch(function () { state.cardBusy = ''; saveB.disabled = false; chHint('保存异常') })
2594
+ }
2595
+ // 📤 卡片群发(2026-09-10 主人要求): 把当前卡片群发到「🎯 选目标」勾选的全部目标(二次确认)
2596
+ var bcB = q('#dk-card-broadcast')
2597
+ if (bcB) bcB.onclick = function () {
2598
+ var v = readCard()
2599
+ if (!v.md) { chHint('markdown 内容不能为空'); return }
2600
+ var tg = (typeof rosterSelectedTargets === 'function') ? rosterSelectedTargets() : []
2601
+ if (!tg.length) { chHint('先在「🎯 选目标」里勾选要群发的目标'); return }
2602
+ if (!window.confirm('确认把这卡片群发到 ' + tg.length + ' 个目标?\n\n' + tg.slice(0, 8).map(function (t2) { return '· ' + (t2.name || t2.peerId) }).join('\n') + (tg.length > 8 ? '\n…' : '') + '\n\n(高影响操作, 发送后 2 分钟内可逐目标撤回)')) return
2603
+ state.cardBusy = 'bc'; bcB.disabled = true; chHint('创建群发任务…')
2604
+ var cardButtons2 = v.btns.map(function (b) { return { label: b.label, action: b.action, payload: b.data } })
2605
+ apiPost('group/broadcast/create', { ns: state.ns || undefined, type: 'card', text: v.md, cardButtons: cardButtons2.length ? cardButtons2 : undefined, targets: tg }).then(function (d) {
2606
+ if (!d || !d.ok || !d.task) { state.cardBusy = ''; bcB.disabled = false; chHint((d && d.error) || '创建失败'); return }
2607
+ return apiPost('group/broadcast/confirm', { ns: state.ns || undefined, task_id: d.task.task_id }).then(function (d2) {
2608
+ state.cardBusy = ''; bcB.disabled = false
2609
+ chHint(d2 && d2.ok ? ('✅ 已确认群发卡片到 ' + tg.length + ' 个目标(后台逐条发送)') : ((d2 && (d2.msg || d2.error)) || '确认失败'))
2610
+ loadBroadcastTasks(false, true)
2611
+ })
2612
+ }).catch(function () { state.cardBusy = ''; bcB.disabled = false; chHint('群发异常') })
2613
+ }
2614
+ }
2203
2615
  function sendNow() {
2204
2616
  var t = (state.sendText || '').trim()
2205
2617
  if (!t) { state.msg = '先输入内容'; paintBody(); return }
@@ -2224,7 +2636,7 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2224
2636
  if (!state.gid) { state.joins = []; paintBody(); return }
2225
2637
  var q = 'gid=' + encodeURIComponent(state.gid) + (state.ns ? '&ns=' + encodeURIComponent(state.ns) : '')
2226
2638
  api('group/join_requests', q).then(function (d) {
2227
- state.joins = (d && d.ok && Array.isArray(d.list) ? d.list : []).map(function (j) { return { member_openid: j.member_openid, username: j.username || '(未知昵称)', verify: (j.verify_info && (j.verify_info.verify_message || j.verify_info.method)) || '', risk: j.risk_tips || '', source: j.apply_source === 'invited' ? '被邀请' : '主动申请' } })
2639
+ state.joins = (d && d.ok && Array.isArray(d.list) ? d.list : []).map(function (j) { return { member_openid: j.member_openid, username: j.username || '(未知昵称)', verify: j.verify_human || (j.verify_info && (j.verify_info.verify_message || j.verify_info.method)) || '', risk: j.risk_tips || '', source: j.apply_source === 'invited' ? '被邀请' : '主动申请' } })
2228
2640
  paintBody()
2229
2641
  })
2230
2642
  }
@@ -2308,6 +2720,53 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2308
2720
  var cnt = panel.querySelector('#dk-target-count')
2309
2721
  if (cnt) cnt.textContent = q ? ('匹配 ' + shown + '/' + total) : ''
2310
2722
  }
2723
+ // 📤 群发 次级标签栏(2026-09-10 主人要求: 互动事件/卡片并入主级「群发」)
2724
+ // 四个子页共享此栏: ✉️发送消息(send) / 📢广播(broadcast) / 🎮互动事件(bp) / 📝卡片(card)
2725
+ function sendSubTabs() {
2726
+ return '<div class="dk-row" style="gap:4px;margin:4px 0 6px;flex-wrap:wrap;border-bottom:1px dashed #e2d9ff;padding-bottom:6px">'
2727
+ + [['send', '🎯 选目标'], ['broadcast', '📢 广播'], ['bp', '🎮 互动事件'], ['card', '📝 卡片']]
2728
+ .map(function (s) {
2729
+ return '<button class="dk-btn' + (state.tab === s[0] ? ' on' : '') + '" data-subtab="' + s[0] + '">' + s[1] + '</button>'
2730
+ }).join('')
2731
+ + '</div>'
2732
+ }
2733
+ // 🚀 M3 群发面板(确认草稿 + 任务列表); 「📇群组管理」与「📤群发·广播」共用(2026-09-10 抽函数)
2734
+ function broadcastPanel() {
2735
+ if (!state.bcDraft && !(state.bcTasks && state.bcTasks.length)) return ''
2736
+ var out = '<div style="border:1px solid #d3f0d3;border-radius:8px;padding:6px 8px;margin:6px 0;background:#f6fdf6">'
2737
+ if (state.bcDraft) {
2738
+ var bc = state.bcDraft
2739
+ out += '<div class="dk-row"><b style="font-size:13px">🚀 群发确认(M3)</b>'
2740
+ + '<span class="dk-msg" style="flex:1;text-align:right;color:#666;font-size:11px">' + bc.targets.length + ' 个目标</span></div>'
2741
+ out += '<textarea class="qqs-txt" id="dk-bc-text" style="width:100%;box-sizing:border-box;min-height:64px;font-size:12px;margin:2px 0" placeholder="要群发的内容…">' + esc(bc.text) + '</textarea>'
2742
+ out += '<div class="dk-item" style="border:1px solid #eef7ee;border-radius:6px;margin:1px 0;padding:3px 6px;max-height:14vh;overflow:auto">'
2743
+ bc.targets.forEach(function (t2) {
2744
+ out += '<div style="font-size:11px;color:#555;line-height:1.5">' + (t2.scope === 'group' ? '👥' : '👤') + ' ' + esc(t2.name || String(t2.peerId).slice(0, 12)) + ' <span style="color:#aaa">…' + esc(String(t2.peerId).slice(-6)) + '</span></div>'
2745
+ })
2746
+ out += '</div>'
2747
+ out += '<div class="dk-row" style="gap:8px">'
2748
+ + '<button class="dk-btn no" id="dk-bc-cancel">✕ 取消</button>'
2749
+ + '<button class="dk-btn ok" id="dk-bc-confirm">✅ 确认群发(发 ' + bc.targets.length + ' 个目标)</button>'
2750
+ + '<span class="dk-msg" style="flex:1;text-align:right;font-size:11px;color:#c23131">⚠️ 高影响操作: 发送后 2 分钟内可逐目标撤回</span></div>'
2751
+ }
2752
+ if (state.bcTasks && state.bcTasks.length) {
2753
+ out += '<div class="dk-row"><b style="font-size:12px">📋 群发任务</b>'
2754
+ + '<button class="dk-btn" id="dk-bc-refresh" style="margin-left:auto">🔄 刷新进度</button></div>'
2755
+ state.bcTasks.forEach(function (tk) {
2756
+ var stc = { draft: '#888', queued: '#1971c2', sending: '#f08c00', partial_failed: '#e03131', done: '#2f9e44', cancelled: '#868e96' }[tk.state] || '#888'
2757
+ var okN = tk.targets.filter(function (t2) { return tk.results[t2.peerId] && tk.results[t2.peerId].ok }).length
2758
+ out += '<div class="dk-item" style="border:1px solid #eee;border-radius:6px;margin:1px 0;padding:3px 6px;flex-wrap:wrap">'
2759
+ + '<span style="font-size:11px;font-weight:700;color:' + stc + '">' + ({ draft: '草稿', queued: '排队中', sending: '发送中', partial_failed: '部分失败', done: '已完成', cancelled: '已中止' }[tk.state] || tk.state) + '</span>'
2760
+ + '<span style="flex:1;font-size:11px;color:#555;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"> ' + esc(String(tk.payload.content || '').slice(0, 30)) + '</span>'
2761
+ + '<span style="font-size:11px;color:#666">' + okN + '/' + tk.targets.length + '</span>'
2762
+ + (tk.state === 'queued' || tk.state === 'sending' ? '<button class="dk-btn no" data-bccancel="' + esc(tk.task_id) + '" style="margin-left:6px">⏹ 中止</button>' : '')
2763
+ + (tk.state === 'done' || tk.state === 'partial_failed' ? '<button class="dk-btn" data-bcrecall="' + esc(tk.task_id) + '" style="margin-left:6px" title="撤回该任务 2 分钟内已发的消息(逐目标)">↩ 撤回</button>' : '')
2764
+ + '</div>'
2765
+ })
2766
+ }
2767
+ out += '</div>'
2768
+ return out
2769
+ }
2311
2770
  function paintBody() {
2312
2771
  if (!panel || !open) return
2313
2772
  var b = panel.querySelector('.dk-b')
@@ -2329,30 +2788,51 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2329
2788
  + '<button data-t="join" class="' + (state.tab === 'join' ? 'on' : '') + '">📥 入群审批<span class="dk-join-badge" style="display:none;background:#ff4d4f;color:#fff;border-radius:8px;font-size:11px;padding:0 5px;margin-left:4px">0</span></button>'
2330
2789
  + '<button data-t="mute" class="' + (state.tab === 'mute' ? 'on' : '') + '">🔇 禁言</button>'
2331
2790
  + '<button data-t="out" class="' + (state.tab === 'out' ? 'on' : '') + '">⚙️ 出站</button>'
2332
- + '<button data-t="bp" class="' + (state.tab === 'bp' ? 'on' : '') + '">🎮 互动事件</button>'
2791
+ + '<button data-t="send" class="' + (['send','broadcast','bp','card'].indexOf(state.tab) >= 0 ? 'on' : '') + '">📤 群发</button>'
2333
2792
  + '</div>'
2334
2793
  var body = tabs
2335
2794
  var status = state.msg ? '<div class="dk-msg" style="color:#2f9e44;margin:4px 0">' + esc(state.msg) + '</div>' : ''
2336
2795
  if (!state.ns || state.accts.length === 0) {
2337
2796
  body += '<div class="dk-empty">还没有配置机器人账号 → 到「账号与预设」页添加后再回来</div>'
2338
2797
  } else if (state.tab === 'send') {
2339
- body += '<div class="dk-row">目标类型: '
2340
- + '<label style="display:inline-flex;align-items:center;gap:4px"><input type="radio" name="dk-scope" value="group"' + (state.sendScope !== 'c2c' ? ' checked' : '') + '> 群聊</label> '
2341
- + '<label style="display:inline-flex;align-items:center;gap:4px"><input type="radio" name="dk-scope" value="c2c"' + (state.sendScope === 'c2c' ? ' checked' : '') + '> 私聊</label></div>'
2342
- body += '<div class="dk-row" style="margin:2px 0 4px">'
2343
- + '<input class="qqs-txt" id="dk-target-search" placeholder="' + (state.sendScope === 'c2c' ? '🔍 搜私聊对象(昵称/ID)…' : '🔍 搜群(备注/昵称/ID)…') + '" value="' + esc(state.targetQ || '') + '" style="width:100%;box-sizing:border-box;font-size:12px;padding:4px 8px">'
2344
- + '<span class="dk-msg" id="dk-target-count"></span></div>'
2345
- var targetSel = state.sendScope === 'c2c'
2346
- ? '<select class="qqs-sel" id="dk-c2c" style="min-width:220px">' + (c2cOpts || '<option value="">暂无私聊对象</option>') + '</select>'
2347
- : groupSelHtml
2348
- body += '<div class="dk-row" id="dk-target-row">' + targetSel + '</div>'
2349
- body += '<textarea class="qqs-txt" id="dk-text" rows="3" style="width:100%;box-sizing:border-box;margin:6px 0" placeholder="输入内容…(@某人 用 &lt;@对方openid&gt; 无斜杠)">' + esc(state.sendText) + '</textarea>'
2350
- body += '<div class="dk-row">'
2351
- + '<label style="display:inline-flex;align-items:center;gap:5px;font-size:12px;color:#666;cursor:pointer" title="发完后往该会话写入一条「用户代你发送: …」的模拟用户消息(web 流可见、不唤醒、不开回合;标记不会发到 QQ)">'
2352
- + '<input type="checkbox" id="dk-insctx"' + (state.insertCtx ? ' checked' : '') + '> 🧠 记入 bot 上下文(模拟用户消息·不唤醒)</label>'
2798
+ // 🎯 选目标(2026-09-10 主人要求): 把原「发送消息」改成勾选式的目标选择器,
2799
+ // 分组(群/私聊)+搜索, 与「📇 群组管理」共用同一份勾选(state.rosterSel) → 供 📢 广播 群发。
2800
+ body += sendSubTabs()
2801
+ body += '<div class="dk-row" style="font-weight:700;font-size:13px;margin:4px 0 2px">🎯 选目标(勾选后到「📢 广播」群发)</div>'
2802
+ body += '<div class="dk-row"><span class="dk-msg" style="flex:1;line-height:1.5">勾选要群发的 <b>👥群 / 👤私聊</b> 对象(与「📇 群组管理」共用同一份勾选)。可用 <b>🗂 分组</b> 把常用目标归组(仿 QQ 分组), 选好切到 <b>📢 广播</b> 发送。</span></div>'
2803
+ // 🗂 分组栏(仿 QQ 下拉分组 + 编辑, 2026-09-10 主人要求)
2804
+ var tg = (state.tgGroups || []).filter(function (g2) { return g2 && g2.id })
2805
+ body += '<div class="dk-row" style="gap:6px;margin:2px 0;flex-wrap:wrap">'
2806
+ + '<span style="font-size:12px;color:#666;flex:none">🗂 分组:</span>'
2807
+ + '<select class="qqs-sel" id="dk-tg-sel" style="min-width:140px">'
2808
+ + '<option value="">全部目标</option>'
2809
+ + tg.map(function (g2) { return '<option value="' + esc(g2.id) + '"' + (state.tgCur === g2.id ? ' selected' : '') + '>' + esc(g2.name || g2.id) + '(' + ((g2.members || []).length) + ')</option>' }).join('')
2810
+ + '</select>'
2811
+ + '<button class="dk-btn" id="dk-tg-new">➕ 新建分组</button>'
2812
+ + '<button class="dk-btn" id="dk-tg-rename"' + (state.tgCur ? '' : ' disabled') + '>✏️ 改名</button>'
2813
+ + '<button class="dk-btn no" id="dk-tg-del"' + (state.tgCur ? '' : ' disabled') + '>🗑 删除</button>'
2814
+ + '</div>'
2815
+ body += '<div class="dk-row" style="gap:6px;margin:0 0 4px;flex-wrap:wrap">'
2816
+ + '<button class="dk-btn ok" id="dk-tg-add"' + (state.tgCur ? '' : ' disabled') + ' title="把上面勾选的目标加入当前分组">➕ 勾选的加入本组</button>'
2817
+ + '<button class="dk-btn" id="dk-tg-remove"' + (state.tgCur ? '' : ' disabled') + ' title="把勾选的从当前分组移出">➖ 从本组移出</button>'
2818
+ + '<button class="dk-btn" id="dk-tg-pickall"' + (state.tgCur ? '' : ' disabled') + ' title="勾选当前分组的全部成员">☑ 选中本组</button>'
2819
+ + '<span class="dk-msg" style="font-size:11px;color:#888">分组仅存本机浏览器, 用于快速批量勾选</span>'
2820
+ + '</div>'
2821
+ body += '<div class="dk-row">范围: '
2822
+ + '<label style="display:inline-flex;align-items:center;gap:4px;cursor:pointer;font-size:12px"><input type="radio" name="dk-rscope" value="all"' + (state.rosterScope === 'all' ? ' checked' : '') + '> 全部</label>'
2823
+ + '<label style="display:inline-flex;align-items:center;gap:4px;cursor:pointer;font-size:12px"><input type="radio" name="dk-rscope" value="c2c"' + (state.rosterScope === 'c2c' ? ' checked' : '') + '> 👤 私聊过的人</label>'
2824
+ + '<label style="display:inline-flex;align-items:center;gap:4px;cursor:pointer;font-size:12px"><input type="radio" name="dk-rscope" value="group"' + (state.rosterScope === 'group' ? ' checked' : '') + '> 👥 群</label>'
2353
2825
  + '<span style="flex:1"></span>'
2354
- + '<button class="dk-btn ok" id="dk-send">🚀 发送到 ' + (state.sendScope === 'c2c' ? ('私聊「' + esc(c2cSelName()) + '」') : '群「' + esc(groupSelName()) + '」') + '</button>'
2355
- + '<span class="dk-msg">已输 ' + state.sendText.length + '/2000</span></div>'
2826
+ + '<button class="dk-btn" id="dk-roster-refresh" title="重新拉取群注册表+聊天台账">🔄 刷新</button></div>'
2827
+ body += '<div class="dk-row" style="margin:2px 0 4px">'
2828
+ + '<input class="qqs-txt" id="dk-roster-q" placeholder="🔍 搜群名/昵称/ID…" value="' + esc(state.rosterQ || '') + '" style="flex:1;box-sizing:border-box;font-size:12px;padding:4px 8px">'
2829
+ + '<span class="dk-msg" id="dk-roster-stat" style="white-space:nowrap"></span></div>'
2830
+ body += '<div class="dk-row" style="margin:0 0 4px">'
2831
+ + '<button class="dk-btn" id="dk-roster-all">全选(当前范围)</button>'
2832
+ + '<button class="dk-btn" id="dk-roster-clear">清空</button>'
2833
+ + '<span class="dk-msg" style="flex:1;text-align:right;color:#2f9e44">已选 ' + rosterSelN() + ' 个</span>'
2834
+ + '<button class="dk-btn ok" id="dk-pick-goto">📢 去广播 →</button></div>'
2835
+ body += '<div class="dk-list" id="dk-roster-list"></div>'
2356
2836
  } else if (state.tab === 'chat') {
2357
2837
  body += '<div class="dk-row">目标类型: '
2358
2838
  + '<label style="display:inline-flex;align-items:center;gap:4px"><input type="radio" name="dk-scope" value="group"' + (state.sendScope !== 'c2c' ? ' checked' : '') + '> 群聊</label> '
@@ -2407,6 +2887,24 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2407
2887
  + '<button class="dk-btn ok" id="dk-hub-set"' + (state.hubBusy ? ' disabled' : '') + ' title="把当前 web 会话设为群组管理器: 各群群事件(入群申请/新成员加入/被拉群)将汇总注入此会话">📌 设为本会话</button>'
2408
2888
  + '<button class="dk-btn" id="dk-hub-clear"' + (state.hubSid ? '' : ' disabled') + '>取消</button></div>'
2409
2889
  + '<div class="dk-msg" id="dk-hub-hint" style="color:#2f9e44"></div></div>'
2890
+ // ⚙ 群组设置(热更新): 群管理开关 + 入群申请轮询 —— 集中在此, 保存即 live 生效
2891
+ body += '<div style="border:1px solid #e2d9ff;border-radius:8px;padding:6px 8px;margin:2px 0 6px;background:#faf8ff">'
2892
+ + '<div class="dk-row" style="margin:0"><b style="font-size:13px">⚙ 群组设置(热更新)</b>'
2893
+ + '<span class="dk-msg" style="flex:1" id="dk-ga-hint"></span></div>'
2894
+ + '<div class="dk-row" style="gap:8px;flex-wrap:wrap">'
2895
+ + '<label style="display:inline-flex;align-items:center;gap:4px;cursor:pointer;font-size:12px"><input type="checkbox" id="dk-ga-enabled"' + (state.gaEnabled ? ' checked' : '') + '> 群管理</label>'
2896
+ + '<label style="display:inline-flex;align-items:center;gap:4px;cursor:pointer;font-size:12px"><input type="checkbox" id="dk-ga-poll"' + (state.gaPoll ? ' checked' : '') + '> 轮询审批</label>'
2897
+ + '<label style="display:inline-flex;align-items:center;gap:4px;cursor:pointer;font-size:12px"><input type="checkbox" id="dk-ga-pollwake"' + (state.gaPollWake ? ' checked' : '') + '> 唤醒AI</label>'
2898
+ + '<label style="display:inline-flex;align-items:center;gap:4px;cursor:pointer;font-size:12px"><input type="checkbox" id="dk-ga-hubnotify"' + (state.gaHubNotify ? ' checked' : '') + '> 注入群管会话</label>'
2899
+ + '<label style="display:inline-flex;align-items:center;gap:4px;cursor:pointer;font-size:12px" title="取消勾选=只注入群组管理器会话, 普通群会话不再收到审批提醒"><input type="checkbox" id="dk-ga-notifygroup"' + (state.gaNotifyGroup ? ' checked' : '') + '> 通知普通群</label>'
2900
+ + '</div>'
2901
+ + '<div class="dk-row" style="gap:8px;flex-wrap:wrap">'
2902
+ + '<span style="font-size:12px;color:#666;display:inline-flex;align-items:center;gap:4px">间隔 <input type="number" id="dk-ga-interval" min="1" value="' + (state.gaInterval || 5) + '" style="width:52px;font-size:12px;padding:2px 4px"> 分钟</span>'
2903
+ + '<span style="font-size:12px;color:#666;display:inline-flex;align-items:center;gap:4px">攒够<input type="number" id="dk-ga-mincount" min="1" value="' + (state.gaMinCount || 1) + '" style="width:44px;font-size:12px;padding:2px 4px">个才提醒</span>'
2904
+ + '<button class="dk-btn ok" id="dk-ga-save" style="margin-left:auto">💾 保存设置</button>'
2905
+ + '</div>'
2906
+ + '<div class="dk-msg" style="line-height:1.5;color:#888">轮询=定时拉各群审批列表, 有新增就唤醒AI提醒你(默认1个也报, 不攒死); 大量申请靠轮询周期自然攒批。保存即热更新, 不用重启。</div>'
2907
+ + '</div>'
2410
2908
  body += '<div class="dk-row">范围: '
2411
2909
  + '<label style="display:inline-flex;align-items:center;gap:4px;cursor:pointer;font-size:12px"><input type="radio" name="dk-rscope" value="all"' + (state.rosterScope === 'all' ? ' checked' : '') + '> 全部</label>'
2412
2910
  + '<label style="display:inline-flex;align-items:center;gap:4px;cursor:pointer;font-size:12px"><input type="radio" name="dk-rscope" value="c2c"' + (state.rosterScope === 'c2c' ? ' checked' : '') + '> 👤 私聊过的人</label>'
@@ -2419,8 +2917,29 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2419
2917
  body += '<div class="dk-row" style="margin:0 0 4px">'
2420
2918
  + '<button class="dk-btn" id="dk-roster-all">全选(当前范围)</button>'
2421
2919
  + '<button class="dk-btn" id="dk-roster-clear">清空</button>'
2422
- + '<span class="dk-msg" style="flex:1;text-align:right">勾选集合用途待开发(后续: 群发/批量操作目标)</span></div>'
2920
+ + '<span class="dk-msg" style="flex:1;text-align:right;color:#2f9e44">已选 ' + rosterSelN() + ' 个</span>'
2921
+ + '<button class="dk-btn ok" id="dk-roster-send">🚀 群发勾选项</button></div>'
2423
2922
  body += '<div class="dk-list" id="dk-roster-list"></div>'
2923
+ // ── M3 群发面板(与「📤 群发 · 广播」子页共用, 2026-09-10 抽成 broadcastPanel) ──
2924
+ body += broadcastPanel()
2925
+ } else if (state.tab === 'broadcast') {
2926
+ // 📢 广播子页(2026-09-10 主人要求: 群发并入「📤 群发」主标签; 目标复用 📇群组管理 的勾选)
2927
+ body += sendSubTabs()
2928
+ body += '<div class="dk-row" style="font-weight:700;font-size:13px;margin:4px 0 2px">📢 群发广播</div>'
2929
+ var btg = rosterSelectedTargets()
2930
+ body += '<div class="dk-row"><span class="dk-msg" style="flex:1;line-height:1.5">目标来自「📇 群组管理」里勾选的对象(当前已选 <b>' + btg.length + '</b> 个)。群发<b>一律二次确认</b>, 发送后 2 分钟内可逐目标撤回; 任务后台串行推进(天然限频)。</span></div>'
2931
+ body += '<div class="dk-row" style="gap:6px;flex-wrap:wrap">'
2932
+ + '<button class="dk-btn" id="dk-bc-goto">📇 去勾选目标</button>'
2933
+ + '<button class="dk-btn ok" id="dk-roster-send"' + (btg.length ? '' : ' disabled') + ' title="把勾选的目标纳入群发">🚀 群发勾选项</button>'
2934
+ + '</div>'
2935
+ if (btg.length) {
2936
+ body += '<div class="dk-item" style="border:1px solid #eef7ee;border-radius:6px;margin:4px 0;padding:3px 6px;max-height:16vh;overflow:auto">'
2937
+ btg.forEach(function (t2) {
2938
+ body += '<div style="font-size:11px;color:#555;line-height:1.5">' + (t2.scope === 'group' ? '👥' : '👤') + ' ' + esc(t2.name || String(t2.peerId).slice(0, 12)) + ' <span style="color:#aaa">…' + esc(String(t2.peerId).slice(-6)) + '</span></div>'
2939
+ })
2940
+ body += '</div>'
2941
+ }
2942
+ body += broadcastPanel()
2424
2943
  } else if (state.tab === 'out') {
2425
2944
  var om = state.outMode || 'adaptive'
2426
2945
  if (om === 'nothink') body += '<div class="dk-msg" style="color:#c23131;margin:2px 0">⚠️ 当前为「完全不思考」(设置页开启): QQ 入站不唤醒 AI。发 /outmode adaptive 可唤醒。</div>'
@@ -2431,7 +2950,55 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2431
2950
  body += '<div class="dk-msg" style="line-height:1.6">适配主动=和QQ有关的会话回复都发到QQ: 刚收到真人消息时前5条带引用回你(能看到回的是哪句), 第6条起自动转独立新消息, 连发不被QQ吞; 定时/后台等没有新真人消息的主动推送也走独立消息。'
2432
2951
  body += '被动=始终以「回复你那条」发出, 连发约4~5条后会被QQ吞掉。完全不出站=本机静默, 不向QQ发任何回复(鲸鱼娘可用工具随时切回)。本开关对纯web(没绑QQ)的会话不生效。</div>'
2433
2952
  body += '<span class="dk-msg" id="dk-out-hint" style="color:#2f9e44;margin:4px 0"></span>'
2953
+ } else if (state.tab === 'card') {
2954
+ // 📝 自定义 markdown 卡片(2026-09-10): 源码 + 实时预览 + 按钮配置 → 发送到选中群/私聊
2955
+ body += sendSubTabs()
2956
+ body += '<div class="dk-row" style="font-weight:700;font-size:13px;margin:4px 0 2px">📝 Markdown 卡片编辑器(自定义互动卡)</div>'
2957
+ body += '<div class="dk-row"><span class="dk-msg" style="flex:1;line-height:1.5">写 markdown(支持 <b>![图 #宽px #高px](网络图URL)</b> 嵌图) → 加按钮(每行一个, 格式 <b>文字|指令</b> 或 <b>文字|url|跳转</b>) → 选目标发送。markdown 图只能用<b>公网直链图</b>(带扩展名/有 Content-Length 的图床最稳)。</span></div>'
2958
+ // 目标: 群 + 私聊(c2c: 前缀) + 搜索过滤(2026-09-10 主人要求)
2959
+ // ⚠️ 字段名: 群对象用 gid(state.groups = {gid,name,from,lastAt}), 私聊对象用 id(state.c2cs)。
2960
+ // 2026-09-10 修复: 原写成 g.id → String(undefined)="undefined" → 请求打到
2961
+ // /v2/groups/undefined/messages → 11255 invalid request(群里发不出去)。
2962
+ var cTargets = (state.groups || []).map(function (g) {
2963
+ var gid = String((g && (g.gid || g.id)) || '')
2964
+ return { v: gid, n: (g && g.name) || gid.slice(0, 8), kind: 'group' }
2965
+ }).filter(function (t) { return t.v && t.v !== 'undefined' && t.v !== 'null' })
2966
+ ;(state.c2cs || []).forEach(function (c) {
2967
+ var cid = String((c && (c.id || c.peerId || c.openid)) || '')
2968
+ if (!cid || cid === 'undefined' || cid === 'null') return
2969
+ cTargets.push({ v: 'c2c:' + cid, n: (c.name || ('私聊 ' + cid.slice(0, 8))) + '(私聊)', kind: 'c2c' })
2970
+ })
2971
+ var cq = String(state.cardQ || '').toLowerCase()
2972
+ if (cq) cTargets = cTargets.filter(function (t) { return (t.n + ' ' + t.v).toLowerCase().indexOf(cq) >= 0 })
2973
+ if (!state.cardGid && cTargets.length) state.cardGid = cTargets[0].v // 默认选中第一项, 避免"未选目标"
2974
+ body += '<div class="dk-row" style="gap:6px;margin:2px 0">目标: <select class="qqs-sel" id="dk-card-gid" style="flex:1;min-width:0">'
2975
+ body += cTargets.map(function (t) { return '<option value="' + esc(t.v) + '"' + (t.v === state.cardGid ? ' selected' : '') + '>' + esc(t.n) + '</option>' }).join('')
2976
+ || '<option value="">(无可选目标)</option>'
2977
+ body += '</select>'
2978
+ + '<input class="qqs-txt" id="dk-card-q" placeholder="🔍 搜群/私聊…" value="' + esc(state.cardQ || '') + '" style="width:120px;font-size:12px;padding:3px 6px">'
2979
+ + '</div>'
2980
+ body += '<div style="display:flex;gap:6px;margin:2px 0">'
2981
+ + '<textarea id="dk-card-md" placeholder="# 标题&#10;&#10;正文 **加粗**&#10;&#10;![图 #208px #160px](https://example.com/img.png)" style="flex:1;min-height:140px;font-size:12px;font-family:monospace;box-sizing:border-box;padding:6px;border:1px solid #ddd;border-radius:6px;resize:vertical">' + esc(state.cardMd) + '</textarea>'
2982
+ + '<div id="dk-card-preview" style="flex:1;min-height:140px;max-height:200px;overflow:auto;border:1px dashed #c9d8ff;border-radius:6px;padding:6px;font-size:12px;background:#fafbff;box-sizing:border-box"></div>'
2983
+ + '</div>'
2984
+ body += '<div class="dk-row" style="gap:6px;margin:2px 0 0"><span style="font-size:12px;color:#888;flex:none">按钮:</span>'
2985
+ + '<textarea id="dk-card-btns" placeholder="每行一个: 文字|内容|类型&#10;先回文本: ✅ 签到|✅ 签到成功 +1 🎉&#10;执行命令: 查天气|/weather|cmd&#10;跳转链接: 🔗官网|https://qq.com|url" style="flex:1;min-height:72px;font-size:12px;font-family:monospace;box-sizing:border-box;padding:6px;border:1px solid #ddd;border-radius:6px;resize:vertical">' + esc(state.cardBtns) + '</textarea></div>'
2986
+ // 常显格式说明(2026-09-10 主人要求: 文本怎么写按钮要有说明; 三型动作 2026-09-10 晚扩充)
2987
+ body += '<div class="dk-msg" style="font-size:11px;line-height:1.6;color:#666;background:#f8f6ff;border:1px solid #eee7ff;border-radius:6px;padding:4px 8px;margin:2px 0">'
2988
+ + '<b>按钮写法</b>(上面框里一行一个, 竖线 <code>|</code> 分隔, 第三段可省):<br>'
2989
+ + '· <b>回文本</b> — <code>文字|要回的文本</code> 例: <code>✅ 签到|✅ 签到成功 +1 🎉</code>(点击后机器人直接回这句, 不吵 AI)<br>'
2990
+ + '· <b>执行命令</b> — <code>文字|/指令</code> 例: <code>查天气|/weather</code>(点击后执行该斜杠命令)<br>'
2991
+ + '· <b>跳转链接</b> — <code>文字|网址</code> 例: <code>🔗官网|https://qq.com</code>(点击直接跳网页)<br>'
2992
+ + '· 想写死类型就加第三段: <code>|text</code>(回文本) / <code>|cmd</code>(命令) / <code>|url</code>(跳转)<br>'
2993
+ + '· 每行一个按钮, 最多 25 个(5行×5列, 由"每行按钮"控制布局)</div>'
2994
+ body += '<div class="dk-row" style="gap:8px">'
2995
+ + '<button class="dk-btn ok" id="dk-card-send"' + (state.cardBusy ? ' disabled' : '') + '>🚀 发送卡片</button>'
2996
+ + '<button class="dk-btn" id="dk-card-save"' + (state.cardBusy ? ' disabled' : '') + '>💾 存为事件</button>'
2997
+ + '<button class="dk-btn" id="dk-card-broadcast"' + (state.cardBusy ? ' disabled' : '') + ' title="群发到「🎯 选目标」里勾选的全部目标(二次确认)">📤 群发到勾选目标(' + rosterSelN() + ')</button>'
2998
+ + '<span class="dk-msg" style="flex:1;text-align:right;font-size:11px;color:#888">最多 25 按钮(5行×5列)</span></div>'
2999
+ body += '<div class="dk-msg" id="dk-card-hint" style="color:#2f9e44;margin:2px 0"></div>'
2434
3000
  } else if (state.tab === 'bp') {
3001
+ body += sendSubTabs()
2435
3002
  body += '<div class="dk-row" style="font-weight:700;font-size:13px;margin:4px 0 2px">🎮 互动事件装配器(/botplay 触发)</div>'
2436
3003
  body += '<div class="dk-row"><span class="dk-msg" style="flex:1">定义事件与按钮 → 保存即热更 → QQ 里发 <b>/botplay 事件名</b> 发卡。点击按钮走 bot 行为(回文本等)并可选影响 AI。</span></div>'
2437
3004
  // 事件列表
@@ -2444,6 +3011,7 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2444
3011
  var overL = rowc.rows > 5
2445
3012
  body += '<div class="dk-item" style="cursor:pointer;background:' + (sel ? '#f1ecff' : 'transparent') + '" data-bpidx="' + i + '">'
2446
3013
  + '<span style="flex:1"><b>' + esc(ev.name || '(未命名)') + '</b> <span style="color:#888;font-size:11px">/ ' + esc(ev.id || '?') + ' · ' + btnN + ' 按钮·每行' + rowc.per + ' → ' + rowc.rows + '行' + (overL ? ' <b style="color:#e03131">⚠超限</b>' : '') + ' · ' + (ev.perm && ev.perm.type ? esc(ev.perm.type) : 'all') + ' · ' + (ev.expireSec || 600) + 's</span></span>'
3014
+ + '<button class="dk-btn" data-bp2card="' + i + '" title="把此事件的卡片预设(正文+按钮)复制进「📝 卡片」编辑器">📋</button>'
2447
3015
  + '<button class="dk-btn no" data-bpdel="' + i + '">🗑</button></div>'
2448
3016
  })
2449
3017
  body += '</div>'
@@ -2456,6 +3024,8 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2456
3024
  body += '<div style="border-top:1px dashed #e2d9ff;margin-top:8px;padding-top:8px">'
2457
3025
  body += '<div class="dk-row"><label style="font-size:12px;color:#555;width:56px">事件名</label><input class="qqs-txt" id="dk-bp-name" value="' + esc(D.name) + '" style="flex:1" placeholder="如: 签到"></div>'
2458
3026
  body += '<div class="dk-row"><label style="font-size:12px;color:#555;width:56px">id</label><input class="qqs-txt" id="dk-bp-id" value="' + esc(D.id) + '" style="flex:1" placeholder="英文/数字/_- (触发用)"></div>'
3027
+ body += '<div class="dk-row" style="align-items:flex-start"><label style="font-size:12px;color:#555;width:56px;padding-top:4px">卡片正文</label>'
3028
+ + '<textarea id="dk-bp-content" placeholder="markdown 模板(可嵌网络图 ![text #宽px #高px](url)); 留空=默认模板(## 🎮 事件名 + 引导)。支持 {name} 占位。" style="flex:1;min-height:64px;font-size:12px;font-family:monospace;box-sizing:border-box;padding:6px;border:1px solid #ddd;border-radius:6px;resize:vertical">' + esc(D.contentText || '') + '</textarea></div>'
2459
3029
  body += '<div class="dk-row"><label style="font-size:12px;color:#555;width:56px">权限</label>'
2460
3030
  + '<select class="qqs-sel" id="dk-bp-perm">' + ['all','triggerer','owner','users'].map(function (p) { return '<option value="' + p + '"' + (D.perm && D.perm.type === p ? ' selected' : '') + '>' + ({ all: '所有人', triggerer: '仅触发者本人(推荐)', owner: '主人白名单', users: '指定openid' }[p]) + '</option>' }).join('') + '</select>'
2461
3031
  + (D.perm && D.perm.type === 'users'
@@ -2500,7 +3070,13 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2500
3070
  if (state.tab === 'chat') renderChatList()
2501
3071
  if (state.tab === 'join') renderJoinList()
2502
3072
  if (state.tab === 'mute') { renderMemberList(); renderMuteList() }
2503
- if (state.tab === 'roster') { renderRosterList(); bindRosterEvents(); loadHubState() }
3073
+ if (state.tab === 'roster' || state.tab === 'send') { renderRosterList(); bindRosterEvents(); loadHubState(); loadBroadcastTasks(false, true) }
3074
+ // 📝 卡片: 必须在 paintBody 内绑定(2026-09-10 修复: 原在 tab 点击处先 bind 后 paintBody,
3075
+ // 绑的是即将被替换的旧 DOM → 发送/存为事件无反应、预览不渲染)
3076
+ if (state.tab === 'card') bindCardEvents()
3077
+ // 📤 广播子页: 绑定群发按钮 + 刷新任务列表(任务列表静默拉取, 不重绘 → 防死循环)
3078
+ if (state.tab === 'broadcast') { bindBroadcastEvents(); loadBroadcastTasks(false, true); startBroadcastAdvance() }
3079
+ // 注: 📤 群发子标签栏的点击绑定由上面 bindBodyEvents() 统一处理(它含 [data-subtab])
2504
3080
  // 首次进入聊天 tab 自动拉最新一页
2505
3081
  if (state.tab === 'chat' && !state.chatItems.length && !state.chatBusy) loadChat(true)
2506
3082
  setTimeout(layoutPanel, 0)
@@ -2517,6 +3093,15 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2517
3093
  paintBody()
2518
3094
  }
2519
3095
  })
3096
+ // 📤 群发 · 次级标签切换(2026-09-10)
3097
+ panel.querySelectorAll('[data-subtab]').forEach(function (sb) {
3098
+ sb.onclick = function () {
3099
+ var v = sb.getAttribute('data-subtab')
3100
+ state.tab = v; state.msg = ''
3101
+ if (v === 'bp') loadBotplay()
3102
+ paintBody()
3103
+ }
3104
+ })
2520
3105
  var gsel = panel.querySelector('#dk-gid')
2521
3106
  if (gsel) gsel.onchange = function (e) {
2522
3107
  state.gid = e.target.value; state.msg = ''
@@ -2650,10 +3235,38 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2650
3235
  paintBody()
2651
3236
  }
2652
3237
  })
3238
+ // 📋 事件 → 卡片编辑器(2026-09-10 主人要求): 把事件的正文预设+按钮复制进「📝 卡片」, 便于改完再发
3239
+ bpList && bpList.querySelectorAll('[data-bp2card]').forEach(function (b) {
3240
+ b.onclick = function (e) {
3241
+ e.stopPropagation()
3242
+ var idx = Number(b.getAttribute('data-bp2card'))
3243
+ var ev = state.bpEvents[idx]
3244
+ if (!ev) return
3245
+ // 正文: 事件自定义 contentText; 空则用事件默认模板
3246
+ state.cardMd = (ev.contentText && String(ev.contentText).trim())
3247
+ ? String(ev.contentText)
3248
+ : ('## 🎮 ' + (ev.name || '互动事件') + '\n\n点下方按钮完成互动 👇')
3249
+ // 按钮 → 卡片编辑器的文本格式(**显式带类型后缀**, 三种动作都能无损搬过来):
3250
+ // jump_url → 文字|网址|url command → 文字|指令|cmd reply_text → 文字|文本|text
3251
+ state.cardBtns = (ev.buttons || []).map(function (bb) {
3252
+ var act = (bb && bb.botAction) || {}
3253
+ var label = (bb && bb.label) || '按钮'
3254
+ if (act.type === 'jump_url') return label + '|' + String(act.url || '') + '|url'
3255
+ if (act.type === 'command') return label + '|' + String(act.text || '').replace(/^\//, '') + '|cmd'
3256
+ return label + '|' + String(act.text || '') + '|text'
3257
+ }).join('\n')
3258
+ state.tab = 'card'
3259
+ paintBody()
3260
+ setTimeout(function () {
3261
+ var h = panel.querySelector('#dk-card-hint')
3262
+ if (h) h.textContent = '📋 已从事件「' + (ev.name || '') + '」载入预设(正文 ' + state.cardMd.length + ' 字 / 按钮 ' + (ev.buttons || []).length + ' 个) — 可直接发送, 也可改完再发'
3263
+ }, 0)
3264
+ }
3265
+ })
2653
3266
  var bpNew = panel.querySelector('#dk-bp-new')
2654
3267
  if (bpNew) bpNew.onclick = function () {
2655
3268
  state.bpSel = null
2656
- state.bpDraft = { id: '', name: '', maxClicks: 0, expireSec: 600, buttonsPerRow: 1, perm: { type: 'all', userIds: [] }, buttons: [{ id: 'b1', label: '按钮', botAction: { type: 'reply_text', text: '' }, llmEffect: { mode: 'no_append', contextText: '' } }] }
3269
+ state.bpDraft = { id: '', name: '', contentText: '', maxClicks: 0, expireSec: 600, buttonsPerRow: 1, perm: { type: 'all', userIds: [] }, buttons: [{ id: 'b1', label: '按钮', botAction: { type: 'reply_text', text: '' }, llmEffect: { mode: 'no_append', contextText: '' } }] }
2657
3270
  paintBody()
2658
3271
  }
2659
3272
  // 编辑器字段(名称/id/有效期/总次数/权限)
@@ -2661,6 +3274,8 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2661
3274
  if (bpName) bpName.oninput = function (e) { state.bpDraft.name = e.target.value }
2662
3275
  var bpId = panel.querySelector('#dk-bp-id')
2663
3276
  if (bpId) bpId.oninput = function (e) { state.bpDraft.id = e.target.value.trim() }
3277
+ var bpContent = panel.querySelector('#dk-bp-content')
3278
+ if (bpContent) bpContent.oninput = function (e) { state.bpDraft.contentText = e.target.value }
2664
3279
  var bpPerm = panel.querySelector('#dk-bp-perm')
2665
3280
  if (bpPerm) bpPerm.onchange = function (e) {
2666
3281
  state.bpDraft.perm = state.bpDraft.perm || {}
@@ -2761,17 +3376,18 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2761
3376
  el.textContent = '共 ' + c.n + ' 个 · 每行 ' + c.per + ' 个 → ' + c.rows + ' 行' + (over ? '(超 QQ 上限 5 行, 发卡会报错!)' : '(≤5 行 ✅)')
2762
3377
  el.style.color = over ? '#e03131' : '#2f9e44'
2763
3378
  }
2764
- // 🎮 互动事件装配器: 读/存(全量 patch 只带 botplayEvents; settings.update 部分合并 + revision 乐观锁)
3379
+ // 🎮 互动事件装配器: 读/存走独立文件路由(2026-09-10 M4.3: {dataRoot}/.qqbot/botplay-events.json)
2765
3380
  function loadBotplay() {
2766
3381
  if (state.tab !== 'bp') return
2767
- fetch(READ + (state.ns ? '?' + outNsQ() : '')).then(function (r) { return r.json() }).then(function (d) {
2768
- if (d && d.value) {
2769
- state.bpEvents = Array.isArray(d.value.botplayEvents) ? JSON.parse(JSON.stringify(d.value.botplayEvents)) : []
2770
- if (!state.bpDraft && state.bpEvents.length) { state.bpSel = 0; state.bpDraft = JSON.parse(JSON.stringify(state.bpEvents[0])) }
2771
- else if (!state.bpEvents.length) { state.bpSel = null; state.bpDraft = null }
2772
- paintBody()
2773
- }
2774
- }).catch(function () {})
3382
+ api('group/botplay-events', state.ns ? 'ns=' + encodeURIComponent(state.ns) : '')
3383
+ .then(function (d) {
3384
+ if (d && d.ok && Array.isArray(d.events)) {
3385
+ state.bpEvents = JSON.parse(JSON.stringify(d.events))
3386
+ if (!state.bpDraft && state.bpEvents.length) { state.bpSel = 0; state.bpDraft = JSON.parse(JSON.stringify(state.bpEvents[0])) }
3387
+ else if (!state.bpEvents.length) { state.bpSel = null; state.bpDraft = null }
3388
+ paintBody()
3389
+ }
3390
+ }).catch(function () {})
2775
3391
  }
2776
3392
  function saveBotplay() {
2777
3393
  var hint = panel.querySelector('#dk-bp-hint')
@@ -2794,26 +3410,10 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2794
3410
  state.bpEvents.forEach(function (ev) { var c = bpRowCount(ev); if (c.rows > 5 && !badEv) badEv = { name: ev.name, per: c.per, rows: c.rows } })
2795
3411
  if (badEv) { ok('⚠️ 事件「' + badEv.name + '」每行 ' + badEv.per + ' 个 → ' + badEv.rows + ' 行, 超 QQ 5 行上限, 未保存'); return }
2796
3412
  if (hint) hint.textContent = '保存中…'
2797
- var trySave = function (curVal, curRev) {
2798
- var patch = Object.assign({}, curVal || {}, { botplayEvents: state.bpEvents })
2799
- return fetch(UPDATE, {
2800
- method: 'POST', headers: { 'content-type': 'application/json' },
2801
- body: JSON.stringify({ ns: state.ns || undefined, patch: patch, expectedRevision: curRev }),
2802
- }).then(function (r) { return r.json().catch(function () { return null }) }).then(function (d) {
2803
- if (d && d.value) {
2804
- state.bpEvents = Array.isArray(d.value.botplayEvents) ? d.value.botplayEvents : []
2805
- var h2 = panel.querySelector('#dk-bp-hint'); if (h2) h2.textContent = '✅ 已保存(共 ' + state.bpEvents.length + ' 个事件, live 热更已生效)'
2806
- } else {
2807
- var conflicted = !!(d && d.error && String(d.error).indexOf('changed since it was read') >= 0)
2808
- if (conflicted) { fetch(READ + (state.ns ? '?' + outNsQ() : '')).then(function (r) { return r.json() }).then(function (dd) { if (dd && dd.value) trySave(dd.value, dd.revision) }).catch(function () {}) }
2809
- else { var h3 = panel.querySelector('#dk-bp-hint'); if (h3) h3.textContent = '保存失败: ' + ((d && d.error) || '未知错误') }
2810
- }
2811
- }).catch(function () { var h4 = panel.querySelector('#dk-bp-hint'); if (h4) h4.textContent = '保存异常' })
2812
- }
2813
- fetch(READ + (state.ns ? '?' + outNsQ() : '')).then(function (r) { return r.json() }).then(function (d0) {
2814
- if (d0 && d0.value) trySave(d0.value, d0.revision)
2815
- else { var h5 = panel.querySelector('#dk-bp-hint'); if (h5) h5.textContent = '保存失败: 无法读取当前设置' }
2816
- }).catch(function () { var h6 = panel.querySelector('#dk-bp-hint'); if (h6) h6.textContent = '保存失败: 读取异常' })
3413
+ apiPost('group/botplay-events', { ns: state.ns || 'im-qqbot', events: state.bpEvents }).then(function (d) {
3414
+ if (d && d.ok) { if (hint) hint.textContent = '✅ 已保存(共 ' + state.bpEvents.length + ' 个事件, live 热更已生效)' }
3415
+ else { if (hint) hint.textContent = '保存失败: ' + ((d && (d.error || d.msg)) || '未知错误') }
3416
+ }).catch(function () { if (hint) hint.textContent = '保存异常' })
2817
3417
  }
2818
3418
  // ── 💬 聊天视图: 数据拉取 / QQ 风格气泡渲染 / 顶部滚动分页 ──
2819
3419
  function chatPeerReady() {
@@ -2892,6 +3492,19 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2892
3492
  renderChatList()
2893
3493
  })
2894
3494
  }
3495
+ // 文本里的 URL → 可点链接(2026-09-10 主人实测: "这个 jump_url 进不了 dock 聊天界面" ——
3496
+ // 卡片分享/ARK 里的 jump_url 在气泡里是纯文本, 点不动)。
3497
+ // ⚠️ 必须在 esc() 之后调用: 只对**已转义**的纯文本做替换, 拼进 href 的也是转义后的串 → 不开新的 XSS 面。
3498
+ function linkifyText(escaped) {
3499
+ return String(escaped || '').replace(/https?:\/\/[^\s<>"']+/g, function (u) {
3500
+ // 尾随标点不入链接(中文标点也剥), 避免把「。」「)」吃进 URL
3501
+ var trail = ''
3502
+ var m = u.match(/[.,;:!?,。;:!?、))】」』"']+$/)
3503
+ if (m) { trail = m[0]; u = u.slice(0, u.length - trail.length) }
3504
+ if (!u) return trail
3505
+ return '<a class="dk-link" href="' + u + '" target="_blank" rel="noopener noreferrer" title="' + u + '">' + u + '</a>' + trail
3506
+ })
3507
+ }
2895
3508
  function renderChatList(keepScrollOffset) {
2896
3509
  var box = document.getElementById('dk-chat-box')
2897
3510
  if (!box) return
@@ -2927,7 +3540,7 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2927
3540
  if (k === 'video') return '<video class="dk-video" controls preload="metadata" src="' + esc(u) + '"></video>'
2928
3541
  return '<img class="dk-img" loading="lazy" data-lb="' + esc(u) + '" src="' + esc(u) + '" referrerpolicy="no-referrer" alt="[图片]" onerror="this.outerHTML=\'<span style=color:#888>[图加载失败]</span>\'">'
2929
3542
  }).join('')
2930
- var txt = it.text ? '<div>' + esc(mdPlain(it.text)) + '</div>' : ''
3543
+ var txt = it.text ? '<div>' + linkifyText(esc(mdPlain(it.text))) + '</div>' : ''
2931
3544
  return '<div class="dk-crow ' + (isOut ? 'out' : 'in') + '">'
2932
3545
  + '<div class="dk-ava">' + ava + '</div>'
2933
3546
  + '<div class="dk-cmain">'