@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.
- package/README.md +6 -0
- package/client/qqbot-settings.js +667 -54
- package/dist/api/group-admin.d.ts +37 -0
- package/dist/api/group-admin.d.ts.map +1 -1
- package/dist/api/group-admin.js +57 -0
- package/dist/api/group-admin.js.map +1 -1
- package/dist/channel-tools.d.ts.map +1 -1
- package/dist/channel-tools.js +373 -25
- package/dist/channel-tools.js.map +1 -1
- package/dist/commands/botplay.d.ts.map +1 -1
- package/dist/commands/botplay.js +6 -2
- package/dist/commands/botplay.js.map +1 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +6 -2
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/inject.d.ts +17 -0
- package/dist/commands/inject.d.ts.map +1 -0
- package/dist/commands/inject.js +70 -0
- package/dist/commands/inject.js.map +1 -0
- package/dist/commands/misc.d.ts +1 -1
- package/dist/commands/misc.d.ts.map +1 -1
- package/dist/commands/misc.js +25 -3
- package/dist/commands/misc.js.map +1 -1
- package/dist/commands/session.d.ts +2 -0
- package/dist/commands/session.d.ts.map +1 -1
- package/dist/commands/session.js +41 -3
- package/dist/commands/session.js.map +1 -1
- package/dist/config.d.ts +25 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -1
- package/dist/config.js.map +1 -1
- package/dist/features/botplay-store.d.ts +13 -0
- package/dist/features/botplay-store.d.ts.map +1 -0
- package/dist/features/botplay-store.js +99 -0
- package/dist/features/botplay-store.js.map +1 -0
- package/dist/features/botplay.d.ts +8 -0
- package/dist/features/botplay.d.ts.map +1 -1
- package/dist/features/botplay.js +22 -7
- package/dist/features/botplay.js.map +1 -1
- package/dist/features/broadcast.d.ts +63 -0
- package/dist/features/broadcast.d.ts.map +1 -0
- package/dist/features/broadcast.js +225 -0
- package/dist/features/broadcast.js.map +1 -0
- package/dist/features/card-callback.d.ts +52 -0
- package/dist/features/card-callback.d.ts.map +1 -0
- package/dist/features/card-callback.js +133 -0
- package/dist/features/card-callback.js.map +1 -0
- package/dist/features/group-hub.d.ts +14 -0
- package/dist/features/group-hub.d.ts.map +1 -1
- package/dist/features/group-hub.js +87 -4
- package/dist/features/group-hub.js.map +1 -1
- package/dist/features/group-join-request.d.ts.map +1 -1
- package/dist/features/group-join-request.js +9 -2
- package/dist/features/group-join-request.js.map +1 -1
- package/dist/features/poll-join-requests.d.ts +9 -0
- package/dist/features/poll-join-requests.d.ts.map +1 -0
- package/dist/features/poll-join-requests.js +249 -0
- package/dist/features/poll-join-requests.js.map +1 -0
- package/dist/features/preset-switcher.d.ts +39 -0
- package/dist/features/preset-switcher.d.ts.map +1 -0
- package/dist/features/preset-switcher.js +172 -0
- package/dist/features/preset-switcher.js.map +1 -0
- package/dist/features/session-registry.d.ts +6 -0
- package/dist/features/session-registry.d.ts.map +1 -1
- package/dist/features/session-registry.js +80 -0
- package/dist/features/session-registry.js.map +1 -1
- package/dist/gateway/bootstrap.d.ts.map +1 -1
- package/dist/gateway/bootstrap.js +74 -4
- package/dist/gateway/bootstrap.js.map +1 -1
- package/dist/gateway/debounce.d.ts.map +1 -1
- package/dist/gateway/debounce.js +7 -0
- package/dist/gateway/debounce.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -5
- package/dist/index.js.map +1 -1
- package/dist/middleware/media-history.d.ts.map +1 -1
- package/dist/middleware/media-history.js +23 -6
- package/dist/middleware/media-history.js.map +1 -1
- package/dist/model/settings-reader.d.ts +7 -0
- package/dist/model/settings-reader.d.ts.map +1 -1
- package/dist/model/settings-reader.js +14 -0
- package/dist/model/settings-reader.js.map +1 -1
- package/dist/session/session-manager.d.ts +26 -0
- package/dist/session/session-manager.d.ts.map +1 -1
- package/dist/session/session-manager.js +101 -0
- package/dist/session/session-manager.js.map +1 -1
- package/dist/transport/inbound.d.ts.map +1 -1
- package/dist/transport/inbound.js +46 -50
- package/dist/transport/inbound.js.map +1 -1
- package/dist/transport/inject-rules.d.ts.map +1 -1
- package/dist/transport/inject-rules.js +8 -2
- package/dist/transport/inject-rules.js.map +1 -1
- package/dist/transport/media-kind.d.ts +37 -0
- package/dist/transport/media-kind.d.ts.map +1 -0
- package/dist/transport/media-kind.js +60 -0
- package/dist/transport/media-kind.js.map +1 -0
- package/dist/transport/outbound-buffer.d.ts +20 -2
- package/dist/transport/outbound-buffer.d.ts.map +1 -1
- package/dist/transport/outbound-buffer.js +25 -5
- package/dist/transport/outbound-buffer.js.map +1 -1
- package/dist/transport/outbound.d.ts.map +1 -1
- package/dist/transport/outbound.js +68 -4
- package/dist/transport/outbound.js.map +1 -1
- package/package.json +1 -1
- package/settings-host.js +406 -21
package/client/qqbot-settings.js
CHANGED
|
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>') }
|
|
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="
|
|
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
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
body += '<div class="dk-row" style="margin:
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
var
|
|
2346
|
-
|
|
2347
|
-
:
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
+ '
|
|
2352
|
-
+ '<
|
|
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
|
|
2355
|
-
|
|
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"
|
|
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></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="# 标题 正文 **加粗** " 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="每行一个: 文字|内容|类型 先回文本: ✅ 签到|✅ 签到成功 +1 🎉 执行命令: 查天气|/weather|cmd 跳转链接: 🔗官网|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 模板(可嵌网络图 ); 留空=默认模板(## 🎮 事件名 + 引导)。支持 {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
|
-
// 🎮 互动事件装配器:
|
|
3379
|
+
// 🎮 互动事件装配器: 读/存走独立文件路由(2026-09-10 M4.3: {dataRoot}/.qqbot/botplay-events.json)
|
|
2765
3380
|
function loadBotplay() {
|
|
2766
3381
|
if (state.tab !== 'bp') return
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
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
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
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">'
|