@zaofan/dsh-qqbot 1.1.6 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +23 -6
  2. package/README_EN.md +10 -0
  3. package/client/qqbot-settings.js +114 -26
  4. package/dist/api/group-admin.d.ts +13 -3
  5. package/dist/api/group-admin.d.ts.map +1 -1
  6. package/dist/api/group-admin.js +49 -5
  7. package/dist/api/group-admin.js.map +1 -1
  8. package/dist/channel-tools.d.ts.map +1 -1
  9. package/dist/channel-tools.js +386 -14
  10. package/dist/channel-tools.js.map +1 -1
  11. package/dist/commands/answer.d.ts +29 -0
  12. package/dist/commands/answer.d.ts.map +1 -0
  13. package/dist/commands/answer.js +29 -0
  14. package/dist/commands/answer.js.map +1 -0
  15. package/dist/commands/index.d.ts.map +1 -1
  16. package/dist/commands/index.js +5 -0
  17. package/dist/commands/index.js.map +1 -1
  18. package/dist/commands/misc.d.ts.map +1 -1
  19. package/dist/commands/misc.js +15 -1
  20. package/dist/commands/misc.js.map +1 -1
  21. package/dist/commands/outmode.d.ts.map +1 -1
  22. package/dist/commands/outmode.js +3 -2
  23. package/dist/commands/outmode.js.map +1 -1
  24. package/dist/config.d.ts +4 -4
  25. package/dist/config.d.ts.map +1 -1
  26. package/dist/config.js +2 -2
  27. package/dist/config.js.map +1 -1
  28. package/dist/features/broadcast.d.ts.map +1 -1
  29. package/dist/features/broadcast.js +3 -1
  30. package/dist/features/broadcast.js.map +1 -1
  31. package/dist/features/group-hub.d.ts +3 -1
  32. package/dist/features/group-hub.d.ts.map +1 -1
  33. package/dist/features/group-hub.js +5 -3
  34. package/dist/features/group-hub.js.map +1 -1
  35. package/dist/features/group-join-request.d.ts.map +1 -1
  36. package/dist/features/group-join-request.js +6 -5
  37. package/dist/features/group-join-request.js.map +1 -1
  38. package/dist/features/outbound-mode-switch.d.ts +8 -2
  39. package/dist/features/outbound-mode-switch.d.ts.map +1 -1
  40. package/dist/features/outbound-mode-switch.js +2 -2
  41. package/dist/features/outbound-mode-switch.js.map +1 -1
  42. package/dist/features/poll-join-requests.js +2 -2
  43. package/dist/features/poll-join-requests.js.map +1 -1
  44. package/dist/features/qq-user-questions.d.ts +127 -4
  45. package/dist/features/qq-user-questions.d.ts.map +1 -1
  46. package/dist/features/qq-user-questions.js +314 -74
  47. package/dist/features/qq-user-questions.js.map +1 -1
  48. package/dist/gateway/bootstrap.d.ts.map +1 -1
  49. package/dist/gateway/bootstrap.js +17 -0
  50. package/dist/gateway/bootstrap.js.map +1 -1
  51. package/dist/gateway/data-root.d.ts +13 -4
  52. package/dist/gateway/data-root.d.ts.map +1 -1
  53. package/dist/gateway/data-root.js +22 -12
  54. package/dist/gateway/data-root.js.map +1 -1
  55. package/dist/index.js +2 -2
  56. package/dist/index.js.map +1 -1
  57. package/dist/transport/outbound.d.ts.map +1 -1
  58. package/dist/transport/outbound.js +60 -5
  59. package/dist/transport/outbound.js.map +1 -1
  60. package/package.json +1 -1
  61. package/settings-host.js +75 -6
package/settings-host.js CHANGED
@@ -551,7 +551,8 @@ export function apply(ctx) {
551
551
  hasFile,
552
552
  instances: bots.map((b) => {
553
553
  const cwd = b.cfg?.cwd || '';
554
- const droot = b.cfg?.dataRoot || cwd;
554
+ // 数据根与 dist 侧 dataRootOf 对齐: 未配 dataRoot → `{cwd}/dshqqbot`(2026-09-12 新默认)
555
+ const droot = b.cfg?.dataRoot || (cwd ? join(cwd, 'dshqqbot') : '');
555
556
  return {
556
557
  id: b.id,
557
558
  ns: b.id, // settings 命名空间 = 实例 id(主 im-qqbot; 非主实例 render 已写 settingsNs=id)
@@ -969,8 +970,9 @@ export function apply(ctx) {
969
970
  if (!b) return null;
970
971
  const appId = b.cfg?.appId || '';
971
972
  const appSecret = b.cfg?.appSecret || '';
972
- // 数据根: dataRoot(新) > cwd(旧); nsBot 的 cwd 字段按数据根返回(全为数据目录用途)
973
- const cwd = (b.cfg?.dataRoot || b.cfg?.cwd || '');
973
+ // 数据根: dataRoot(新) > `{cwd}/dshqqbot`(**2026-09-12 新默认**, 与 dist 侧 dataRootOf 对齐);
974
+ // nsBot 的 cwd 字段按数据根返回(全为数据目录用途) —— 未配 dataRoot 的实例不再落到工作目录。
975
+ const cwd = (b.cfg?.dataRoot || (b.cfg?.cwd ? join(b.cfg.cwd, 'dshqqbot') : ''));
974
976
  if (!appId || !appSecret) return null;
975
977
  return { id, appId, appSecret, cwd, ns: id };
976
978
  }
@@ -993,6 +995,18 @@ export function apply(ctx) {
993
995
  function writeGroupsJson(cwd, g) {
994
996
  try { mkdirSync(join(cwd, '.qqbot'), { recursive: true }); writeFileSync(join(cwd, '.qqbot', 'groups.json'), JSON.stringify(g, null, 1), 'utf8'); } catch { /* ignore */ }
995
997
  }
998
+ // 🗂 目标分组(2026-09-12 从浏览器 localStorage 搬到 host): 主人与 AI **共用同一份分组** ——
999
+ // dock「📇 群组管理 → 🗂 分组」编辑它, agent 侧 broadcast_send 直接按分组名群发。
1000
+ // 结构: [{ id, name, members: ['group:openid' | 'c2c:openid', ...] }] → {cwd}/.qqbot/target-groups.json
1001
+ function readTargetGroupsJson(cwd) {
1002
+ try {
1003
+ const o = JSON.parse(readFileSync(join(cwd, '.qqbot', 'target-groups.json'), 'utf8') || '{}');
1004
+ return Array.isArray(o && o.groups) ? o.groups : [];
1005
+ } catch { return []; }
1006
+ }
1007
+ function writeTargetGroupsJson(cwd, groups) {
1008
+ try { mkdirSync(join(cwd, '.qqbot'), { recursive: true }); writeFileSync(join(cwd, '.qqbot', 'target-groups.json'), JSON.stringify({ groups }, null, 1), 'utf8'); } catch { /* ignore */ }
1009
+ }
996
1010
  function readPendingJson(cwd) {
997
1011
  try { return JSON.parse(readFileSync(join(cwd, '.qqbot', 'join-pending.json'), 'utf8') || '{}'); } catch { return {}; }
998
1012
  }
@@ -1513,6 +1527,27 @@ export function apply(ctx) {
1513
1527
  } catch (e) { writeJson(res, 500, { error: String((e && e.message) || e) }); }
1514
1528
  });
1515
1529
 
1530
+ // 🗂 目标分组读写(2026-09-12): GET=读(供 client 加载/迁移), POST=整体写回(供 client 保存)。
1531
+ // 同 path 两个 method 必须合并成一次 route 注册(webServer 按 path 唯一, 分开注册会让整插件 effect 失败→全接口 404)。
1532
+ route(ctx, ['GET', 'POST'], '/api/qqbot-settings/group/target-groups', async (req, res) => {
1533
+ try {
1534
+ if (req.method === 'GET') {
1535
+ const u = new URL(req.url ?? '/', 'http://x');
1536
+ const bot = nsBot(NSQ(u));
1537
+ if (!bot) return writeJson(res, 400, { error: '找不到该账号实例' });
1538
+ return writeJson(res, 200, { ok: true, groups: readTargetGroupsJson(bot.cwd) });
1539
+ }
1540
+ const body = await readJsonBody(req);
1541
+ if (!body || typeof body !== 'object') return writeJson(res, 400, { error: 'bad body' });
1542
+ const bot = nsBot(String(body.ns || ''));
1543
+ if (!bot) return writeJson(res, 400, { error: '找不到该账号实例' });
1544
+ const groups = Array.isArray(body.groups) ? body.groups : [];
1545
+ writeTargetGroupsJson(bot.cwd, groups);
1546
+ audit(bot.cwd, { ev: 'group.target-groups.save', ns: bot.id, n: groups.length });
1547
+ writeJson(res, 200, { ok: true, groups });
1548
+ } catch (e) { writeJson(res, 500, { error: String((e && e.message) || e) }); }
1549
+ });
1550
+
1516
1551
  // 群发任务列表(读时推进一个目标) {ns?}
1517
1552
  route(ctx, 'GET', '/api/qqbot-settings/group/broadcast/list', async (req, res) => {
1518
1553
  try {
@@ -1568,6 +1603,35 @@ export function apply(ctx) {
1568
1603
  });
1569
1604
 
1570
1605
 
1606
+ // 按"勾选目标"撤回最近一条广播消息(2026-09-12 主人要求): **不必知道 task_id** ——
1607
+ // 服务端自己在该目标的广播记录里找"最新的、已成功且带 message_id"的那条, 2 分钟窗口内撤回。
1608
+ // 存在意义: 主人只勾选了目标就该能撤, 不该被迫去任务列表里翻 task_id(那条路又长又容易看不见按钮)。
1609
+ route(ctx, 'POST', '/api/qqbot-settings/group/recall-last', async (req, res) => {
1610
+ const body = await readJsonBody(req);
1611
+ if (!body || typeof body !== 'object') return writeJson(res, 400, { error: 'bad body' });
1612
+ const targets = Array.isArray(body.targets) ? body.targets : [];
1613
+ if (targets.length === 0) return writeJson(res, 400, { error: 'targets 至少一个' });
1614
+ try {
1615
+ const bot = nsBot(String(body.ns || ''));
1616
+ const gc = await groupClientOf(String(body.ns || ''));
1617
+ if (!bot || !gc) return writeJson(res, 400, { error: '找不到该账号实例' });
1618
+ const bm = await broadcastMod();
1619
+ const tasks = bm.listTasks(bot.cwd).slice().sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
1620
+ const out = [];
1621
+ for (const t of targets) {
1622
+ const peerId = String((t && t.peerId) || '');
1623
+ if (!peerId) { out.push({ peerId: '', ok: false, err: '缺少 peerId' }); continue; }
1624
+ const hit = tasks.find((x) => x.results && x.results[peerId] && x.results[peerId].ok && x.results[peerId].message_id);
1625
+ if (!hit) { out.push({ peerId, ok: false, err: '该目标没有可撤回的广播消息(不是广播发的或没有 message_id)' }); continue; }
1626
+ const r = await bm.recallTaskMessage(bot.cwd, hit.task_id, peerId, gc.client);
1627
+ out.push({ peerId, ok: r.ok, err: r.err, message_id: r.message_id, task_id: hit.task_id });
1628
+ audit(bot.cwd, { ev: 'group.broadcast.recall-last', ns: bot.id, peerId, task_id: hit.task_id, ok: r.ok });
1629
+ }
1630
+ writeJson(res, 200, { ok: true, results: out });
1631
+ } catch (e) { writeJson(res, 500, { error: String((e && e.message) || e) }); }
1632
+ });
1633
+
1634
+
1571
1635
  // ── 悬浮球「💬 聊天视图」(2026-09-07): 读某 QQ 会话最近的入站/出站消息, 只读不建会话 ──
1572
1636
  // 会话事件结构(宿主 dsh-session): {type, seq, time, data}; user/message 的 data=消息体根(data.content),
1573
1637
  // assistant/message 的 data={turn,step,message:{role,content,…}}。只收这两型:
@@ -1694,7 +1758,7 @@ export function apply(ctx) {
1694
1758
  }
1695
1759
  continue;
1696
1760
  }
1697
- // 整行就是一个本机绝对路径(主人贴 D:\…\a.jpg / a.mp3 文本) → 按扩展名转可看/可播
1761
+ // 整行就是一个本机绝对路径(主人贴 <cwd>\a.jpg / a.mp3 文本) → 按扩展名转可看/可播
1698
1762
  const lp = raw.match(/^([A-Za-z]:[\\/].+)$/);
1699
1763
  if (lp) {
1700
1764
  const lk = chatLocalExtKind(lp[1]);
@@ -2471,9 +2535,14 @@ const WHY_MAP = { busy: '目标会话回合活跃,已等待至回合结束仍超
2471
2535
  });
2472
2536
 
2473
2537
  // ── 🚀 群发任务「后台自动推进」(2026-09-10 修: 原设计靠 client 每次 list 驱动, 页面不动就永久卡 queued) ──
2474
- // 每 5s 扫全部实例, 有 queued/sending 任务就推进一步(串行 → 天然限频); 单实例失败不影响其他实例。
2538
+ // 每隔 BC_TICK_MS 扫全部实例, 有 queued/sending 任务就推进一步(串行 → 天然限频); 单实例失败不影响其他实例。
2539
+ // 节流依据(**官方文档已核对**, 群发走的是"主动消息"):
2540
+ // · 群聊: Bot 维度(发送方) 企业认证 60/qpm、未认证 30/qpm;单关系(同一个群) 20/qpm;每群每天最多接收 1000 条;接口 100 QPS
2541
+ // · 单聊: Bot 维度 认证 10/qps、未认证 5/qps 且 30/qpm;单关系 20/qpm
2542
+ // 1500ms/条 ≈ 40 条/分钟 —— 跑满认证额度的 2/3, 给"被动回复/定时推送/其它主动消息"留余量。
2543
+ // (原为 5000ms ≈ 12 条/分钟, 仅用掉 1/5 额度, 主人实测嫌慢)
2475
2544
  try {
2476
- const BC_TICK_MS = 5000;
2545
+ const BC_TICK_MS = 1500;
2477
2546
  let bcTickRunning = false; // 定时器自身防重入: 上一轮未跑完不叠加(2026-09-10)
2478
2547
  const bcTick = async () => {
2479
2548
  if (bcTickRunning) return;