@zaofan/dsh-qqbot 1.1.0 → 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 (96) hide show
  1. package/client/qqbot-settings.js +571 -52
  2. package/dist/api/group-admin.d.ts +22 -0
  3. package/dist/api/group-admin.d.ts.map +1 -1
  4. package/dist/api/group-admin.js +32 -0
  5. package/dist/api/group-admin.js.map +1 -1
  6. package/dist/channel-tools.d.ts.map +1 -1
  7. package/dist/channel-tools.js +192 -77
  8. package/dist/channel-tools.js.map +1 -1
  9. package/dist/commands/botplay.d.ts.map +1 -1
  10. package/dist/commands/botplay.js +6 -2
  11. package/dist/commands/botplay.js.map +1 -1
  12. package/dist/commands/index.d.ts.map +1 -1
  13. package/dist/commands/index.js +6 -2
  14. package/dist/commands/index.js.map +1 -1
  15. package/dist/commands/inject.d.ts +17 -0
  16. package/dist/commands/inject.d.ts.map +1 -0
  17. package/dist/commands/inject.js +70 -0
  18. package/dist/commands/inject.js.map +1 -0
  19. package/dist/commands/misc.d.ts +1 -1
  20. package/dist/commands/misc.d.ts.map +1 -1
  21. package/dist/commands/misc.js +25 -3
  22. package/dist/commands/misc.js.map +1 -1
  23. package/dist/commands/session.d.ts +2 -0
  24. package/dist/commands/session.d.ts.map +1 -1
  25. package/dist/commands/session.js +41 -3
  26. package/dist/commands/session.js.map +1 -1
  27. package/dist/config.d.ts +6 -0
  28. package/dist/config.d.ts.map +1 -1
  29. package/dist/config.js +2 -1
  30. package/dist/config.js.map +1 -1
  31. package/dist/features/botplay-store.d.ts +13 -0
  32. package/dist/features/botplay-store.d.ts.map +1 -0
  33. package/dist/features/botplay-store.js +99 -0
  34. package/dist/features/botplay-store.js.map +1 -0
  35. package/dist/features/botplay.d.ts +8 -0
  36. package/dist/features/botplay.d.ts.map +1 -1
  37. package/dist/features/botplay.js +22 -7
  38. package/dist/features/botplay.js.map +1 -1
  39. package/dist/features/broadcast.d.ts +63 -0
  40. package/dist/features/broadcast.d.ts.map +1 -0
  41. package/dist/features/broadcast.js +225 -0
  42. package/dist/features/broadcast.js.map +1 -0
  43. package/dist/features/card-callback.d.ts +52 -0
  44. package/dist/features/card-callback.d.ts.map +1 -0
  45. package/dist/features/card-callback.js +133 -0
  46. package/dist/features/card-callback.js.map +1 -0
  47. package/dist/features/group-hub.d.ts +2 -0
  48. package/dist/features/group-hub.d.ts.map +1 -1
  49. package/dist/features/group-hub.js +2 -3
  50. package/dist/features/group-hub.js.map +1 -1
  51. package/dist/features/group-join-request.d.ts.map +1 -1
  52. package/dist/features/group-join-request.js +7 -1
  53. package/dist/features/group-join-request.js.map +1 -1
  54. package/dist/features/preset-switcher.d.ts +39 -0
  55. package/dist/features/preset-switcher.d.ts.map +1 -0
  56. package/dist/features/preset-switcher.js +172 -0
  57. package/dist/features/preset-switcher.js.map +1 -0
  58. package/dist/features/session-registry.d.ts +1 -1
  59. package/dist/features/session-registry.d.ts.map +1 -1
  60. package/dist/features/session-registry.js +30 -1
  61. package/dist/features/session-registry.js.map +1 -1
  62. package/dist/gateway/bootstrap.d.ts.map +1 -1
  63. package/dist/gateway/bootstrap.js +51 -4
  64. package/dist/gateway/bootstrap.js.map +1 -1
  65. package/dist/gateway/debounce.d.ts.map +1 -1
  66. package/dist/gateway/debounce.js +7 -0
  67. package/dist/gateway/debounce.js.map +1 -1
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +24 -5
  70. package/dist/index.js.map +1 -1
  71. package/dist/middleware/media-history.d.ts.map +1 -1
  72. package/dist/middleware/media-history.js +23 -6
  73. package/dist/middleware/media-history.js.map +1 -1
  74. package/dist/session/session-manager.d.ts +10 -0
  75. package/dist/session/session-manager.d.ts.map +1 -1
  76. package/dist/session/session-manager.js +45 -0
  77. package/dist/session/session-manager.js.map +1 -1
  78. package/dist/transport/inbound.d.ts.map +1 -1
  79. package/dist/transport/inbound.js +46 -50
  80. package/dist/transport/inbound.js.map +1 -1
  81. package/dist/transport/inject-rules.d.ts.map +1 -1
  82. package/dist/transport/inject-rules.js +8 -2
  83. package/dist/transport/inject-rules.js.map +1 -1
  84. package/dist/transport/media-kind.d.ts +37 -0
  85. package/dist/transport/media-kind.d.ts.map +1 -0
  86. package/dist/transport/media-kind.js +60 -0
  87. package/dist/transport/media-kind.js.map +1 -0
  88. package/dist/transport/outbound-buffer.d.ts +20 -2
  89. package/dist/transport/outbound-buffer.d.ts.map +1 -1
  90. package/dist/transport/outbound-buffer.js +25 -5
  91. package/dist/transport/outbound-buffer.js.map +1 -1
  92. package/dist/transport/outbound.d.ts.map +1 -1
  93. package/dist/transport/outbound.js +68 -4
  94. package/dist/transport/outbound.js.map +1 -1
  95. package/package.json +1 -1
  96. package/settings-host.js +401 -20
@@ -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: '', gaEnabled: false, gaPoll: false, gaPollWake: true, gaHubNotify: true, gaNotifyGroup: true, gaInterval: 5, gaMinCount: 1, gaMsg: '', gaBusy: '' }
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 () {
@@ -2152,6 +2225,8 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2152
2225
  state.gaMinCount = num('#dk-ga-mincount', 1)
2153
2226
  saveGroupAdmin()
2154
2227
  }
2228
+ // M3 群发(自动审批策略已按主人要求移除 2026-09-10)
2229
+ bindBroadcastEvents()
2155
2230
  }
2156
2231
  function hubStatusText() {
2157
2232
  if (state.hubSid) return '已设: 会话 ' + state.hubSid.slice(0, 8) + '… ← 各群群事件(入群申请/新成员/被拉群)汇总注入这里'
@@ -2276,6 +2351,267 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2276
2351
  doSave()
2277
2352
  }).catch(function () { doSave() })
2278
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
+ }
2279
2615
  function sendNow() {
2280
2616
  var t = (state.sendText || '').trim()
2281
2617
  if (!t) { state.msg = '先输入内容'; paintBody(); return }
@@ -2384,6 +2720,53 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2384
2720
  var cnt = panel.querySelector('#dk-target-count')
2385
2721
  if (cnt) cnt.textContent = q ? ('匹配 ' + shown + '/' + total) : ''
2386
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
+ }
2387
2770
  function paintBody() {
2388
2771
  if (!panel || !open) return
2389
2772
  var b = panel.querySelector('.dk-b')
@@ -2405,30 +2788,51 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2405
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>'
2406
2789
  + '<button data-t="mute" class="' + (state.tab === 'mute' ? 'on' : '') + '">🔇 禁言</button>'
2407
2790
  + '<button data-t="out" class="' + (state.tab === 'out' ? 'on' : '') + '">⚙️ 出站</button>'
2408
- + '<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>'
2409
2792
  + '</div>'
2410
2793
  var body = tabs
2411
2794
  var status = state.msg ? '<div class="dk-msg" style="color:#2f9e44;margin:4px 0">' + esc(state.msg) + '</div>' : ''
2412
2795
  if (!state.ns || state.accts.length === 0) {
2413
2796
  body += '<div class="dk-empty">还没有配置机器人账号 → 到「账号与预设」页添加后再回来</div>'
2414
2797
  } else if (state.tab === 'send') {
2415
- body += '<div class="dk-row">目标类型: '
2416
- + '<label style="display:inline-flex;align-items:center;gap:4px"><input type="radio" name="dk-scope" value="group"' + (state.sendScope !== 'c2c' ? ' checked' : '') + '> 群聊</label> '
2417
- + '<label style="display:inline-flex;align-items:center;gap:4px"><input type="radio" name="dk-scope" value="c2c"' + (state.sendScope === 'c2c' ? ' checked' : '') + '> 私聊</label></div>'
2418
- body += '<div class="dk-row" style="margin:2px 0 4px">'
2419
- + '<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">'
2420
- + '<span class="dk-msg" id="dk-target-count"></span></div>'
2421
- var targetSel = state.sendScope === 'c2c'
2422
- ? '<select class="qqs-sel" id="dk-c2c" style="min-width:220px">' + (c2cOpts || '<option value="">暂无私聊对象</option>') + '</select>'
2423
- : groupSelHtml
2424
- body += '<div class="dk-row" id="dk-target-row">' + targetSel + '</div>'
2425
- 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>'
2426
- body += '<div class="dk-row">'
2427
- + '<label style="display:inline-flex;align-items:center;gap:5px;font-size:12px;color:#666;cursor:pointer" title="发完后往该会话写入一条「用户代你发送: …」的模拟用户消息(web 流可见、不唤醒、不开回合;标记不会发到 QQ)">'
2428
- + '<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>'
2429
2825
  + '<span style="flex:1"></span>'
2430
- + '<button class="dk-btn ok" id="dk-send">🚀 发送到 ' + (state.sendScope === 'c2c' ? ('私聊「' + esc(c2cSelName()) + '」') : '群「' + esc(groupSelName()) + '」') + '</button>'
2431
- + '<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>'
2432
2836
  } else if (state.tab === 'chat') {
2433
2837
  body += '<div class="dk-row">目标类型: '
2434
2838
  + '<label style="display:inline-flex;align-items:center;gap:4px"><input type="radio" name="dk-scope" value="group"' + (state.sendScope !== 'c2c' ? ' checked' : '') + '> 群聊</label> '
@@ -2513,8 +2917,29 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2513
2917
  body += '<div class="dk-row" style="margin:0 0 4px">'
2514
2918
  + '<button class="dk-btn" id="dk-roster-all">全选(当前范围)</button>'
2515
2919
  + '<button class="dk-btn" id="dk-roster-clear">清空</button>'
2516
- + '<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>'
2517
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()
2518
2943
  } else if (state.tab === 'out') {
2519
2944
  var om = state.outMode || 'adaptive'
2520
2945
  if (om === 'nothink') body += '<div class="dk-msg" style="color:#c23131;margin:2px 0">⚠️ 当前为「完全不思考」(设置页开启): QQ 入站不唤醒 AI。发 /outmode adaptive 可唤醒。</div>'
@@ -2525,7 +2950,55 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2525
2950
  body += '<div class="dk-msg" style="line-height:1.6">适配主动=和QQ有关的会话回复都发到QQ: 刚收到真人消息时前5条带引用回你(能看到回的是哪句), 第6条起自动转独立新消息, 连发不被QQ吞; 定时/后台等没有新真人消息的主动推送也走独立消息。'
2526
2951
  body += '被动=始终以「回复你那条」发出, 连发约4~5条后会被QQ吞掉。完全不出站=本机静默, 不向QQ发任何回复(鲸鱼娘可用工具随时切回)。本开关对纯web(没绑QQ)的会话不生效。</div>'
2527
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>'
2528
3000
  } else if (state.tab === 'bp') {
3001
+ body += sendSubTabs()
2529
3002
  body += '<div class="dk-row" style="font-weight:700;font-size:13px;margin:4px 0 2px">🎮 互动事件装配器(/botplay 触发)</div>'
2530
3003
  body += '<div class="dk-row"><span class="dk-msg" style="flex:1">定义事件与按钮 → 保存即热更 → QQ 里发 <b>/botplay 事件名</b> 发卡。点击按钮走 bot 行为(回文本等)并可选影响 AI。</span></div>'
2531
3004
  // 事件列表
@@ -2538,6 +3011,7 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2538
3011
  var overL = rowc.rows > 5
2539
3012
  body += '<div class="dk-item" style="cursor:pointer;background:' + (sel ? '#f1ecff' : 'transparent') + '" data-bpidx="' + i + '">'
2540
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>'
2541
3015
  + '<button class="dk-btn no" data-bpdel="' + i + '">🗑</button></div>'
2542
3016
  })
2543
3017
  body += '</div>'
@@ -2550,6 +3024,8 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2550
3024
  body += '<div style="border-top:1px dashed #e2d9ff;margin-top:8px;padding-top:8px">'
2551
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>'
2552
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>'
2553
3029
  body += '<div class="dk-row"><label style="font-size:12px;color:#555;width:56px">权限</label>'
2554
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>'
2555
3031
  + (D.perm && D.perm.type === 'users'
@@ -2594,7 +3070,13 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2594
3070
  if (state.tab === 'chat') renderChatList()
2595
3071
  if (state.tab === 'join') renderJoinList()
2596
3072
  if (state.tab === 'mute') { renderMemberList(); renderMuteList() }
2597
- 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])
2598
3080
  // 首次进入聊天 tab 自动拉最新一页
2599
3081
  if (state.tab === 'chat' && !state.chatItems.length && !state.chatBusy) loadChat(true)
2600
3082
  setTimeout(layoutPanel, 0)
@@ -2611,6 +3093,15 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2611
3093
  paintBody()
2612
3094
  }
2613
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
+ })
2614
3105
  var gsel = panel.querySelector('#dk-gid')
2615
3106
  if (gsel) gsel.onchange = function (e) {
2616
3107
  state.gid = e.target.value; state.msg = ''
@@ -2744,10 +3235,38 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2744
3235
  paintBody()
2745
3236
  }
2746
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
+ })
2747
3266
  var bpNew = panel.querySelector('#dk-bp-new')
2748
3267
  if (bpNew) bpNew.onclick = function () {
2749
3268
  state.bpSel = null
2750
- 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: '' } }] }
2751
3270
  paintBody()
2752
3271
  }
2753
3272
  // 编辑器字段(名称/id/有效期/总次数/权限)
@@ -2755,6 +3274,8 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2755
3274
  if (bpName) bpName.oninput = function (e) { state.bpDraft.name = e.target.value }
2756
3275
  var bpId = panel.querySelector('#dk-bp-id')
2757
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 }
2758
3279
  var bpPerm = panel.querySelector('#dk-bp-perm')
2759
3280
  if (bpPerm) bpPerm.onchange = function (e) {
2760
3281
  state.bpDraft.perm = state.bpDraft.perm || {}
@@ -2855,17 +3376,18 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2855
3376
  el.textContent = '共 ' + c.n + ' 个 · 每行 ' + c.per + ' 个 → ' + c.rows + ' 行' + (over ? '(超 QQ 上限 5 行, 发卡会报错!)' : '(≤5 行 ✅)')
2856
3377
  el.style.color = over ? '#e03131' : '#2f9e44'
2857
3378
  }
2858
- // 🎮 互动事件装配器: 读/存(全量 patch 只带 botplayEvents; settings.update 部分合并 + revision 乐观锁)
3379
+ // 🎮 互动事件装配器: 读/存走独立文件路由(2026-09-10 M4.3: {dataRoot}/.qqbot/botplay-events.json)
2859
3380
  function loadBotplay() {
2860
3381
  if (state.tab !== 'bp') return
2861
- fetch(READ + (state.ns ? '?' + outNsQ() : '')).then(function (r) { return r.json() }).then(function (d) {
2862
- if (d && d.value) {
2863
- state.bpEvents = Array.isArray(d.value.botplayEvents) ? JSON.parse(JSON.stringify(d.value.botplayEvents)) : []
2864
- if (!state.bpDraft && state.bpEvents.length) { state.bpSel = 0; state.bpDraft = JSON.parse(JSON.stringify(state.bpEvents[0])) }
2865
- else if (!state.bpEvents.length) { state.bpSel = null; state.bpDraft = null }
2866
- paintBody()
2867
- }
2868
- }).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 () {})
2869
3391
  }
2870
3392
  function saveBotplay() {
2871
3393
  var hint = panel.querySelector('#dk-bp-hint')
@@ -2888,26 +3410,10 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2888
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 } })
2889
3411
  if (badEv) { ok('⚠️ 事件「' + badEv.name + '」每行 ' + badEv.per + ' 个 → ' + badEv.rows + ' 行, 超 QQ 5 行上限, 未保存'); return }
2890
3412
  if (hint) hint.textContent = '保存中…'
2891
- var trySave = function (curVal, curRev) {
2892
- var patch = Object.assign({}, curVal || {}, { botplayEvents: state.bpEvents })
2893
- return fetch(UPDATE, {
2894
- method: 'POST', headers: { 'content-type': 'application/json' },
2895
- body: JSON.stringify({ ns: state.ns || undefined, patch: patch, expectedRevision: curRev }),
2896
- }).then(function (r) { return r.json().catch(function () { return null }) }).then(function (d) {
2897
- if (d && d.value) {
2898
- state.bpEvents = Array.isArray(d.value.botplayEvents) ? d.value.botplayEvents : []
2899
- var h2 = panel.querySelector('#dk-bp-hint'); if (h2) h2.textContent = '✅ 已保存(共 ' + state.bpEvents.length + ' 个事件, live 热更已生效)'
2900
- } else {
2901
- var conflicted = !!(d && d.error && String(d.error).indexOf('changed since it was read') >= 0)
2902
- 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 () {}) }
2903
- else { var h3 = panel.querySelector('#dk-bp-hint'); if (h3) h3.textContent = '保存失败: ' + ((d && d.error) || '未知错误') }
2904
- }
2905
- }).catch(function () { var h4 = panel.querySelector('#dk-bp-hint'); if (h4) h4.textContent = '保存异常' })
2906
- }
2907
- fetch(READ + (state.ns ? '?' + outNsQ() : '')).then(function (r) { return r.json() }).then(function (d0) {
2908
- if (d0 && d0.value) trySave(d0.value, d0.revision)
2909
- else { var h5 = panel.querySelector('#dk-bp-hint'); if (h5) h5.textContent = '保存失败: 无法读取当前设置' }
2910
- }).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 = '保存异常' })
2911
3417
  }
2912
3418
  // ── 💬 聊天视图: 数据拉取 / QQ 风格气泡渲染 / 顶部滚动分页 ──
2913
3419
  function chatPeerReady() {
@@ -2986,6 +3492,19 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
2986
3492
  renderChatList()
2987
3493
  })
2988
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
+ }
2989
3508
  function renderChatList(keepScrollOffset) {
2990
3509
  var box = document.getElementById('dk-chat-box')
2991
3510
  if (!box) return
@@ -3021,7 +3540,7 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
3021
3540
  if (k === 'video') return '<video class="dk-video" controls preload="metadata" src="' + esc(u) + '"></video>'
3022
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>\'">'
3023
3542
  }).join('')
3024
- var txt = it.text ? '<div>' + esc(mdPlain(it.text)) + '</div>' : ''
3543
+ var txt = it.text ? '<div>' + linkifyText(esc(mdPlain(it.text))) + '</div>' : ''
3025
3544
  return '<div class="dk-crow ' + (isOut ? 'out' : 'in') + '">'
3026
3545
  + '<div class="dk-ava">' + ava + '</div>'
3027
3546
  + '<div class="dk-cmain">'