@zaofan/dsh-qqbot 1.6.1 → 1.6.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 +58 -1
- package/client/qqbot-settings.js +197 -10
- package/dist/channel-tools.d.ts.map +1 -1
- package/dist/channel-tools.js +228 -10
- package/dist/channel-tools.js.map +1 -1
- package/dist/features/qun-admin.d.ts +73 -0
- package/dist/features/qun-admin.d.ts.map +1 -0
- package/dist/features/qun-admin.js +327 -0
- package/dist/features/qun-admin.js.map +1 -0
- package/dist/features/qun-login.d.ts +39 -0
- package/dist/features/qun-login.d.ts.map +1 -0
- package/dist/features/qun-login.js +608 -0
- package/dist/features/qun-login.js.map +1 -0
- package/docs/USER-GUIDE.md +42 -0
- package/package.json +1 -1
- package/settings-host.js +167 -0
package/docs/USER-GUIDE.md
CHANGED
|
@@ -130,6 +130,48 @@ npx @deepseek-ai/dsh web --patch /path/to/dsh-qqbot/cordis.dev.yml
|
|
|
130
130
|
- **`target_group`(v1.2.0)**: **分组管理** —— list/create/rename/delete/add/remove, 读写的正是 dock「📇 群组管理 → 🗂 分组」那份数据 → **AI 与主人共用同一份分组**: 主人在面板分好组, AI 直接"发给群友"就能群发。
|
|
131
131
|
- **`group_join_auto`(v1.3.0)**: **按关键词自动审批入群申请** —— 命中任一关键词的**直接放行**、未命中的**直接拒绝**(不可逆, 可 `dry_run` 预览); 源数据是**官方待审列表**(非本地流水), 同一人重复申请**去重按最新一条判**; 仅在主人明确要求时使用。
|
|
132
132
|
|
|
133
|
+
**🔨 锤子面板: 禁言 / 踢人 (v1.6.2+)**
|
|
134
|
+
|
|
135
|
+
dock 里的「🔨 锤子」(原「🔇 禁言」)下面分两页:**🔇 禁言**(走官方接口,机器人为管理员即可)和 **👢 踢人**(官方**没开放**踢人接口,改走**你自己的 QQ 号** + 群官网 `qun.qq.com`)。
|
|
136
|
+
|
|
137
|
+
**怎么用(踢人)**:
|
|
138
|
+
|
|
139
|
+
1. 面板 → 🔨 锤子 → 👢 踢人 → 点「📱 刷新二维码」;
|
|
140
|
+
2. 页面里直接出二维码 —— 手机 QQ「扫一扫」,**摄像头对着电脑屏幕**扫
|
|
141
|
+
(腾讯只认摄像头:相册选图 / 长按识别会提示「本次请求不支持图片识别或长按扫描二维码授权」);
|
|
142
|
+
3. 扫完自动切到已登录页:**选群 + 搜成员(uin 精确 / 昵称模糊)+ 点 👢 踢**(点前有确认框)。
|
|
143
|
+
|
|
144
|
+

|
|
145
|
+
|
|
146
|
+
**免扫码是怎么做到的**: 登录成功后的凭据存 `{dataRoot}/qun-cookie.json`。此后每次需要登录时,插件会:
|
|
147
|
+
`静默起无头浏览器 → 用 CDP 把已存凭据注入进去 → 页面直接就是已登录 → 读出凭据 → 关掉浏览器`,
|
|
148
|
+
**全程不弹窗口、不用扫码**。只有**第一次**(或 `skey` 约 1 天过期后彻底失效)才要扫一次,重登同样走这套静默流程。
|
|
149
|
+
💡 本机开着 **QQ 客户端**时,登录页会直接给出你的头像,点一下就登 —— 连第一次都不用扫。
|
|
150
|
+
|
|
151
|
+
**登录态与 AI 共享**: 面板和 AI 读写的是**同一份** `qun-cookie.json` —— 面板登过 AI 立刻能用;AI 登过面板打开即已登录。
|
|
152
|
+
|
|
153
|
+
**AI 侧工具 `qq_group_admin`**(v1.6.2+):
|
|
154
|
+
|
|
155
|
+
| action | 作用 |
|
|
156
|
+
|---|---|
|
|
157
|
+
| `status` | 查登录状态 / 继续等扫码 |
|
|
158
|
+
| `login` | 登录(静默优先;需要扫码时给出二维码文件的本地路径) |
|
|
159
|
+
| `groups` | 列出你创建 / 管理的群 |
|
|
160
|
+
| `members` | 查成员(`keyword` 给 uin = 服务端精确查;给昵称 = 全量拉取后本地过滤) |
|
|
161
|
+
| `kick` | 踢人(`uins` 逗号分隔;**必须带 `confirmed=true`**,否则只回「请先跟主人核对群和人」) |
|
|
162
|
+
|
|
163
|
+
**HTTP 端点**(面板走这些,也可自行调用;前缀 `/api/qqbot-settings`):
|
|
164
|
+
`GET /qun/status`、`POST /qun/login`、`GET /qun/poll`、`GET /qun/members?gc=&q=`、`POST /qun/kick`、`POST /qun/logout`、`GET /qun-qr`(直接返回二维码 PNG)。
|
|
165
|
+
|
|
166
|
+
**进程与隐私**: 浏览器用**独立 profile**(放在插件数据目录里,**看不到你日常浏览器的任何东西**);**不常驻** —— 拿到凭据立刻关、等扫码最多留 5 分钟、宿主退出时一起带走(不留孤儿进程)。若启动撞上 `profile 被占用`(code=21),会先杀掉占用该 profile 的残留进程 + 清锁,再重试一次。
|
|
167
|
+
|
|
168
|
+
**踩过的坑(供维护者参考)**:
|
|
169
|
+
|
|
170
|
+
- 踢人 `delete_group_member` 的成员参数是 **`ul`**(不是 `uin` —— 传 `uin` 返回 `ec:5`),且要放 **POST body**;返回 `ec:0` 成功 / `ec:3` 人不在群里 / `-100005` 群不存在或没权限。
|
|
171
|
+
- 成员分页 `st/end` 是**闭区间**(`st=0&end=20` 回 21 条),必须按**实际返回条数**步进,否则每页重叠 1 条、去重后总数偏少(683 人的群只拉回 661)。
|
|
172
|
+
- 宿主进程 spawn 出来的**浏览器窗口到不了用户桌面**(`headless=False` 也一样,进程查不到可见窗口),所以"弹窗口让用户扫"这条路不通 —— 二维码只能走**本地文件路径 / 网页端点**。
|
|
173
|
+
- 浏览器 profile 里的 **session cookie 不会持久化**,所以凭据的唯一真相源是 `qun-cookie.json`,每次启动靠 CDP 注入恢复登录态。
|
|
174
|
+
|
|
133
175
|
**配置项**(Web 面板 ⑥ 可改, 见下表 `groupAdmin.*`)
|
|
134
176
|
|
|
135
177
|
## 🧠 智能回复(本地小模型 · 可选,省 token)(v1.4.5+)
|
package/package.json
CHANGED
package/settings-host.js
CHANGED
|
@@ -410,6 +410,173 @@ export function apply(ctx) {
|
|
|
410
410
|
return mod;
|
|
411
411
|
}
|
|
412
412
|
|
|
413
|
+
// ── QQ 群管理登录二维码 → 直接送到 dsh web 界面 ──
|
|
414
|
+
// 为什么需要它(2026-10-02):AI 登录 qun.qq.com 时用的是**无头**浏览器(没有窗口),
|
|
415
|
+
// 二维码只能"发图"给主人;而主人在手机上收到图后**扫不了自己的屏幕**
|
|
416
|
+
// (腾讯只认摄像头对着另一块屏幕扫:"不支持图片识别或长按扫描二维码授权")。
|
|
417
|
+
// 把二维码挂到 dsh web 上,主人就能在**自己电脑的浏览器**里打开、用手机对着屏幕扫。
|
|
418
|
+
// 用法:http://127.0.0.1:<dsh端口>/api/qqbot-settings/qun-qr
|
|
419
|
+
route(ctx, 'GET', '/api/qqbot-settings/qun-qr', async (_req, res) => {
|
|
420
|
+
try {
|
|
421
|
+
const cands = [];
|
|
422
|
+
const dr = resolveContextlessDataRoot();
|
|
423
|
+
if (dr) cands.push(join(dr, 'qun-login-qr.png'));
|
|
424
|
+
// 兜底:插件写二维码用的 dataRoot 可能与桥推出来的不一致(实测出现过两个数据根)
|
|
425
|
+
try {
|
|
426
|
+
const parsed = parsePatch();
|
|
427
|
+
for (const b of ((parsed && parsed.bots) || [])) {
|
|
428
|
+
const c = (b && b.cfg && (b.cfg.dataRoot || b.cfg.cwd)) || '';
|
|
429
|
+
if (c) cands.push(join(String(c), 'dshqqbot', 'qun-login-qr.png'));
|
|
430
|
+
}
|
|
431
|
+
} catch { /* ignore */ }
|
|
432
|
+
let hit = '';
|
|
433
|
+
for (const c of cands) {
|
|
434
|
+
try { if (c && existsSync(c)) { hit = c; break; } } catch { /* ignore */ }
|
|
435
|
+
}
|
|
436
|
+
if (!hit) {
|
|
437
|
+
return writeJson(res, 404, { ok: false, error: '还没有二维码(先让 AI 调 qq_group_admin 的 action=login)', tried: cands });
|
|
438
|
+
}
|
|
439
|
+
const buf = readFileSync(hit);
|
|
440
|
+
res.writeHead(200, { 'content-type': 'image/png', 'cache-control': 'no-store, must-revalidate' });
|
|
441
|
+
res.end(buf);
|
|
442
|
+
} catch (e) {
|
|
443
|
+
writeJson(res, 500, { ok: false, error: String((e && e.message) || e) });
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// ── 🔨 锤子 · 踢人面板(2026-10-02 主人定)──
|
|
448
|
+
// 登录态与 AI **完全共享**:两边读写同一个 {dataRoot}/qun-cookie.json,
|
|
449
|
+
// 所以「面板登录完 AI 立刻能用 / AI 登录完面板立刻已登录」,不需要任何同步逻辑。
|
|
450
|
+
async function qunAdminMod() { return import('./dist/features/qun-admin.js'); }
|
|
451
|
+
async function qunLoginMod() { return import('./dist/features/qun-login.js'); }
|
|
452
|
+
/** 踢人面板统一取数据根(与插件/工具同源) */
|
|
453
|
+
function qunRoot() { return resolveContextlessDataRoot() || ''; }
|
|
454
|
+
function qunErrHuman(m) {
|
|
455
|
+
return m.indexOf('NOT_LOGGED_IN') >= 0 ? '还没登录(先刷新二维码扫码)' : m;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** 登录状态(顺手拿群列表验证凭据是否还有效) */
|
|
459
|
+
route(ctx, 'GET', '/api/qqbot-settings/qun/status', async (_req, res) => {
|
|
460
|
+
try {
|
|
461
|
+
const dr = qunRoot();
|
|
462
|
+
if (!dr) return writeJson(res, 200, { ok: true, logged: false, hint: '推不出数据根' });
|
|
463
|
+
const mod = await qunAdminMod();
|
|
464
|
+
const c = mod.loadQunCookie(dr);
|
|
465
|
+
if (!c) return writeJson(res, 200, { ok: true, logged: false });
|
|
466
|
+
let valid = true;
|
|
467
|
+
let groups = [];
|
|
468
|
+
try { groups = await mod.qunGroups(dr); } catch { valid = false; }
|
|
469
|
+
writeJson(res, 200, {
|
|
470
|
+
ok: true, logged: true, valid, uin: c.uin,
|
|
471
|
+
groups: (groups || []).map((g) => ({ gc: g.gc, gn: g.gn, role: g.role })),
|
|
472
|
+
});
|
|
473
|
+
} catch (e) { writeJson(res, 500, { ok: false, error: String((e && e.message) || e) }); }
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
/** 触发/刷新登录:静默无头先试(有凭据或有本机 QQ 头像就免扫码),需要扫码就写出二维码文件 */
|
|
477
|
+
route(ctx, 'POST', '/api/qqbot-settings/qun/login', async (_req, res) => {
|
|
478
|
+
try {
|
|
479
|
+
const dr = qunRoot();
|
|
480
|
+
if (!dr) return writeJson(res, 400, { ok: false, error: '推不出数据根' });
|
|
481
|
+
const admin = await qunAdminMod();
|
|
482
|
+
let qr = false;
|
|
483
|
+
let msg = '';
|
|
484
|
+
try {
|
|
485
|
+
const lg = await qunLoginMod();
|
|
486
|
+
const r = await lg.qunBrowserLoginStart(dr, { headless: true });
|
|
487
|
+
msg = r.msg || '';
|
|
488
|
+
if (r.cookie) {
|
|
489
|
+
admin.saveQunCookie(dr, { ...r.cookie, at: Date.now() });
|
|
490
|
+
return writeJson(res, 200, { ok: true, logged: true, uin: r.cookie.uin, msg });
|
|
491
|
+
}
|
|
492
|
+
qr = !!r.qrPath;
|
|
493
|
+
} catch (e) { msg = String((e && e.message) || e); }
|
|
494
|
+
if (!qr) {
|
|
495
|
+
// 浏览器这条路不通 → 退回纯网络二维码(写的是同一个文件路径)
|
|
496
|
+
try {
|
|
497
|
+
const s = await admin.qunLoginStart(dr);
|
|
498
|
+
qr = !!(s && s.ok && s.qrPath);
|
|
499
|
+
if (s && s.msg) msg = (msg ? msg + ' / ' : '') + s.msg;
|
|
500
|
+
} catch { /* ignore */ }
|
|
501
|
+
}
|
|
502
|
+
writeJson(res, 200, { ok: true, logged: false, needScan: qr, msg });
|
|
503
|
+
} catch (e) { writeJson(res, 500, { ok: false, error: String((e && e.message) || e) }); }
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
/** 轮询扫码结果(顺带把挂着的登录会话推进一次) */
|
|
507
|
+
route(ctx, 'GET', '/api/qqbot-settings/qun/poll', async (_req, res) => {
|
|
508
|
+
try {
|
|
509
|
+
const dr = qunRoot();
|
|
510
|
+
if (!dr) return writeJson(res, 200, { ok: true, logged: false });
|
|
511
|
+
const admin = await qunAdminMod();
|
|
512
|
+
try {
|
|
513
|
+
const lg = await qunLoginMod();
|
|
514
|
+
if (lg.qunLoginSessionAlive(dr)) {
|
|
515
|
+
const w = await lg.qunBrowserLoginWait(dr, 2500);
|
|
516
|
+
if (w.cookie) {
|
|
517
|
+
admin.saveQunCookie(dr, { ...w.cookie, at: Date.now() });
|
|
518
|
+
return writeJson(res, 200, { ok: true, logged: true, uin: w.cookie.uin });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
} catch { /* ignore */ }
|
|
522
|
+
try {
|
|
523
|
+
if (admin.qunLoginPending(dr)) {
|
|
524
|
+
const p = await admin.qunLoginPoll(dr, 2500);
|
|
525
|
+
if (p.done) return writeJson(res, 200, { ok: true, logged: true });
|
|
526
|
+
}
|
|
527
|
+
} catch { /* ignore */ }
|
|
528
|
+
const c = admin.loadQunCookie(dr);
|
|
529
|
+
writeJson(res, 200, { ok: true, logged: !!(c && c.skey), uin: c ? c.uin : '' });
|
|
530
|
+
} catch (e) { writeJson(res, 500, { ok: false, error: String((e && e.message) || e) }); }
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
/** 成员搜索:?gc=群号&q=uin或昵称(q 可空 = 全量) */
|
|
534
|
+
route(ctx, 'GET', '/api/qqbot-settings/qun/members', async (req, res) => {
|
|
535
|
+
try {
|
|
536
|
+
const dr = qunRoot();
|
|
537
|
+
if (!dr) return writeJson(res, 400, { ok: false, error: '推不出数据根' });
|
|
538
|
+
const u = new URL(req.url ?? '/', 'http://x');
|
|
539
|
+
const gc = String(u.searchParams.get('gc') || '').trim();
|
|
540
|
+
const q = String(u.searchParams.get('q') || '').trim();
|
|
541
|
+
if (!gc) return writeJson(res, 400, { ok: false, error: 'gc 必填' });
|
|
542
|
+
const mod = await qunAdminMod();
|
|
543
|
+
const r = await mod.qunMembers(dr, gc, q || undefined);
|
|
544
|
+
writeJson(res, 200, { ok: true, count: r.count, note: r.note, members: (r.members || []).slice(0, 300) });
|
|
545
|
+
} catch (e) { writeJson(res, 200, { ok: false, error: qunErrHuman(String((e && e.message) || e)) }); }
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
/** 踢人 {gc, uins:[...]} */
|
|
549
|
+
route(ctx, 'POST', '/api/qqbot-settings/qun/kick', async (req, res) => {
|
|
550
|
+
const body = await readJsonBody(req);
|
|
551
|
+
if (!body || typeof body !== 'object') return writeJson(res, 400, { ok: false, error: 'bad body' });
|
|
552
|
+
const gc = String(body.gc || '').trim();
|
|
553
|
+
const list = (Array.isArray(body.uins) ? body.uins : []).map((x) => String(x).trim()).filter(Boolean);
|
|
554
|
+
if (!gc || !list.length) return writeJson(res, 400, { ok: false, error: 'gc / uins 必填' });
|
|
555
|
+
try {
|
|
556
|
+
const dr = qunRoot();
|
|
557
|
+
if (!dr) return writeJson(res, 400, { ok: false, error: '推不出数据根' });
|
|
558
|
+
const mod = await qunAdminMod();
|
|
559
|
+
const rs = await mod.qunKick(dr, gc, list.slice(0, 20));
|
|
560
|
+
const okCount = rs.filter((x) => x.ec === 0).length;
|
|
561
|
+
writeJson(res, 200, { ok: okCount > 0, results: rs, msg: '成功 ' + okCount + '/' + rs.length });
|
|
562
|
+
} catch (e) { writeJson(res, 200, { ok: false, error: qunErrHuman(String((e && e.message) || e)) }); }
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
/** 退出登录(清掉本地凭据,下次要重新扫码) */
|
|
566
|
+
route(ctx, 'POST', '/api/qqbot-settings/qun/logout', async (_req, res) => {
|
|
567
|
+
try {
|
|
568
|
+
const dr = qunRoot();
|
|
569
|
+
if (!dr) return writeJson(res, 400, { ok: false, error: '推不出数据根' });
|
|
570
|
+
const mod = await qunAdminMod();
|
|
571
|
+
mod.clearQunCookie(dr);
|
|
572
|
+
try {
|
|
573
|
+
const lg = await qunLoginMod();
|
|
574
|
+
lg.qunLoginSessionClose(dr);
|
|
575
|
+
} catch { /* ignore */ }
|
|
576
|
+
writeJson(res, 200, { ok: true, msg: '已退出登录' });
|
|
577
|
+
} catch (e) { writeJson(res, 500, { ok: false, error: String((e && e.message) || e) }); }
|
|
578
|
+
});
|
|
579
|
+
|
|
413
580
|
// ── 无上下文模式(2026-09-27 主人定)──
|
|
414
581
|
// 会话级开关:dock 面板里对**某个会话**开启;开启后该会话每轮只带
|
|
415
582
|
// 「@ 之前 N 条群消息 + 系统规则」,历史对话全部丢弃(大幅省 token)。
|