@zaofan/dsh-qqbot 1.1.0 → 1.1.3
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/client/qqbot-settings.js +571 -52
- package/dist/api/group-admin.d.ts +22 -0
- package/dist/api/group-admin.d.ts.map +1 -1
- package/dist/api/group-admin.js +32 -0
- package/dist/api/group-admin.js.map +1 -1
- package/dist/channel-tools.d.ts.map +1 -1
- package/dist/channel-tools.js +192 -77
- 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 +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -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 +2 -0
- package/dist/features/group-hub.d.ts.map +1 -1
- package/dist/features/group-hub.js +4 -5
- 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 +7 -1
- package/dist/features/group-join-request.js.map +1 -1
- 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 +1 -1
- package/dist/features/session-registry.d.ts.map +1 -1
- package/dist/features/session-registry.js +30 -1
- package/dist/features/session-registry.js.map +1 -1
- package/dist/gateway/bootstrap.d.ts.map +1 -1
- package/dist/gateway/bootstrap.js +51 -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/session/session-manager.d.ts +10 -0
- package/dist/session/session-manager.d.ts.map +1 -1
- package/dist/session/session-manager.js +45 -0
- package/dist/session/session-manager.js.map +1 -1
- package/dist/setup.d.ts +15 -3
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +51 -32
- package/dist/setup.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 +538 -44
package/client/qqbot-settings.js
CHANGED
|
@@ -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, '&').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
|
+
}
|
|
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="
|
|
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
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
body += '<div class="dk-row" style="margin:
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
var
|
|
2422
|
-
|
|
2423
|
-
:
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
+ '
|
|
2428
|
-
+ '<
|
|
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
|
|
2431
|
-
|
|
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"
|
|
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></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>'
|
|
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 模板(可嵌网络图 ); 留空=默认模板(## 🎮 事件名 + 引导)。支持 {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
|
-
// 🎮 互动事件装配器:
|
|
3379
|
+
// 🎮 互动事件装配器: 读/存走独立文件路由(2026-09-10 M4.3: {dataRoot}/.qqbot/botplay-events.json)
|
|
2859
3380
|
function loadBotplay() {
|
|
2860
3381
|
if (state.tab !== 'bp') return
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
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
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
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">'
|