@zaofan/dsh-qqbot 1.0.1 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/client/qqbot-settings.js +667 -54
- package/dist/api/group-admin.d.ts +37 -0
- package/dist/api/group-admin.d.ts.map +1 -1
- package/dist/api/group-admin.js +57 -0
- package/dist/api/group-admin.js.map +1 -1
- package/dist/channel-tools.d.ts.map +1 -1
- package/dist/channel-tools.js +373 -25
- package/dist/channel-tools.js.map +1 -1
- package/dist/commands/botplay.d.ts.map +1 -1
- package/dist/commands/botplay.js +6 -2
- package/dist/commands/botplay.js.map +1 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +6 -2
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/inject.d.ts +17 -0
- package/dist/commands/inject.d.ts.map +1 -0
- package/dist/commands/inject.js +70 -0
- package/dist/commands/inject.js.map +1 -0
- package/dist/commands/misc.d.ts +1 -1
- package/dist/commands/misc.d.ts.map +1 -1
- package/dist/commands/misc.js +25 -3
- package/dist/commands/misc.js.map +1 -1
- package/dist/commands/session.d.ts +2 -0
- package/dist/commands/session.d.ts.map +1 -1
- package/dist/commands/session.js +41 -3
- package/dist/commands/session.js.map +1 -1
- package/dist/config.d.ts +25 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -1
- package/dist/config.js.map +1 -1
- package/dist/features/botplay-store.d.ts +13 -0
- package/dist/features/botplay-store.d.ts.map +1 -0
- package/dist/features/botplay-store.js +99 -0
- package/dist/features/botplay-store.js.map +1 -0
- package/dist/features/botplay.d.ts +8 -0
- package/dist/features/botplay.d.ts.map +1 -1
- package/dist/features/botplay.js +22 -7
- package/dist/features/botplay.js.map +1 -1
- package/dist/features/broadcast.d.ts +63 -0
- package/dist/features/broadcast.d.ts.map +1 -0
- package/dist/features/broadcast.js +225 -0
- package/dist/features/broadcast.js.map +1 -0
- package/dist/features/card-callback.d.ts +52 -0
- package/dist/features/card-callback.d.ts.map +1 -0
- package/dist/features/card-callback.js +133 -0
- package/dist/features/card-callback.js.map +1 -0
- package/dist/features/group-hub.d.ts +14 -0
- package/dist/features/group-hub.d.ts.map +1 -1
- package/dist/features/group-hub.js +87 -4
- package/dist/features/group-hub.js.map +1 -1
- package/dist/features/group-join-request.d.ts.map +1 -1
- package/dist/features/group-join-request.js +9 -2
- package/dist/features/group-join-request.js.map +1 -1
- package/dist/features/poll-join-requests.d.ts +9 -0
- package/dist/features/poll-join-requests.d.ts.map +1 -0
- package/dist/features/poll-join-requests.js +249 -0
- package/dist/features/poll-join-requests.js.map +1 -0
- package/dist/features/preset-switcher.d.ts +39 -0
- package/dist/features/preset-switcher.d.ts.map +1 -0
- package/dist/features/preset-switcher.js +172 -0
- package/dist/features/preset-switcher.js.map +1 -0
- package/dist/features/session-registry.d.ts +6 -0
- package/dist/features/session-registry.d.ts.map +1 -1
- package/dist/features/session-registry.js +80 -0
- package/dist/features/session-registry.js.map +1 -1
- package/dist/gateway/bootstrap.d.ts.map +1 -1
- package/dist/gateway/bootstrap.js +74 -4
- package/dist/gateway/bootstrap.js.map +1 -1
- package/dist/gateway/debounce.d.ts.map +1 -1
- package/dist/gateway/debounce.js +7 -0
- package/dist/gateway/debounce.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -5
- package/dist/index.js.map +1 -1
- package/dist/middleware/media-history.d.ts.map +1 -1
- package/dist/middleware/media-history.js +23 -6
- package/dist/middleware/media-history.js.map +1 -1
- package/dist/model/settings-reader.d.ts +7 -0
- package/dist/model/settings-reader.d.ts.map +1 -1
- package/dist/model/settings-reader.js +14 -0
- package/dist/model/settings-reader.js.map +1 -1
- package/dist/session/session-manager.d.ts +26 -0
- package/dist/session/session-manager.d.ts.map +1 -1
- package/dist/session/session-manager.js +101 -0
- package/dist/session/session-manager.js.map +1 -1
- package/dist/transport/inbound.d.ts.map +1 -1
- package/dist/transport/inbound.js +46 -50
- package/dist/transport/inbound.js.map +1 -1
- package/dist/transport/inject-rules.d.ts.map +1 -1
- package/dist/transport/inject-rules.js +8 -2
- package/dist/transport/inject-rules.js.map +1 -1
- package/dist/transport/media-kind.d.ts +37 -0
- package/dist/transport/media-kind.d.ts.map +1 -0
- package/dist/transport/media-kind.js +60 -0
- package/dist/transport/media-kind.js.map +1 -0
- package/dist/transport/outbound-buffer.d.ts +20 -2
- package/dist/transport/outbound-buffer.d.ts.map +1 -1
- package/dist/transport/outbound-buffer.js +25 -5
- package/dist/transport/outbound-buffer.js.map +1 -1
- package/dist/transport/outbound.d.ts.map +1 -1
- package/dist/transport/outbound.js +68 -4
- package/dist/transport/outbound.js.map +1 -1
- package/package.json +1 -1
- package/settings-host.js +406 -21
package/dist/channel-tools.js
CHANGED
|
@@ -8,13 +8,16 @@
|
|
|
8
8
|
* 复制进共享 node_modules,也不得在 dependencies 里自带 dsh-tools(避免双包)。
|
|
9
9
|
*/
|
|
10
10
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
11
|
-
import { appendFileSync, existsSync, statSync } from 'node:fs';
|
|
11
|
+
import { appendFileSync, existsSync, readFileSync, statSync } from 'node:fs';
|
|
12
12
|
import { getStickerStore } from './features/sticker-store.js';
|
|
13
13
|
import { autoTagImage } from './features/sticker-tagger.js';
|
|
14
14
|
import { isStickerGateDenied } from './features/sticker-gate.js';
|
|
15
15
|
import { getScheduleStore } from './features/schedule-store.js';
|
|
16
16
|
import { switchOutboundMode } from './features/outbound-mode-switch.js';
|
|
17
17
|
import { loadExtensionTools } from './features/extension-store.js';
|
|
18
|
+
import { verifyHuman, groupRegistryPath } from './api/group-admin.js';
|
|
19
|
+
import { wakeSessionAgent, safeAppendUserMessage } from './features/group-hub.js';
|
|
20
|
+
import { managersOf, findManagerByPeer, findManagerBySessionId } from './features/session-registry.js';
|
|
18
21
|
/** 诊断日志路径: 默认关闭; 需要排查时设环境变量 QQBOT_DIAG_FILE 指向日志文件 */
|
|
19
22
|
const DIAG_FILE = process.env.QQBOT_DIAG_FILE || '';
|
|
20
23
|
/** 大文件异步发送阈值: 本地文件 >= 5MB 时走后台任务(SDK 分片上传耗时, 避免阻塞 LLM 回合) */
|
|
@@ -23,6 +26,22 @@ const FILE_ASYNC_MIN = 5 * 1024 * 1024;
|
|
|
23
26
|
const TURN_WAIT_MS = 60_000;
|
|
24
27
|
let bgSeq = 0;
|
|
25
28
|
function fmtMB(n) { return (n / 1048576).toFixed(1) + 'MB'; }
|
|
29
|
+
/** 从 exec 取 logger(ctx.logger), 拿不到用 console 兜底(类型兼容 Logger) */
|
|
30
|
+
function loggerLike(exec) {
|
|
31
|
+
try {
|
|
32
|
+
const a = exec.agent;
|
|
33
|
+
if (a?.ctx?.logger)
|
|
34
|
+
return a.ctx.logger;
|
|
35
|
+
}
|
|
36
|
+
catch { /* 忽略 */ }
|
|
37
|
+
const c = console;
|
|
38
|
+
return {
|
|
39
|
+
info: (m, ...args) => c.info(m, ...args),
|
|
40
|
+
warn: (m, ...args) => c.warn(m, ...args),
|
|
41
|
+
error: (m, ...args) => c.error(m, ...args),
|
|
42
|
+
debug: (m, ...args) => c.debug(m, ...args),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
26
45
|
/**
|
|
27
46
|
* 把后台任务结果作为 user/message 写回会话(不唤醒, 与入群申请通知同款; 失败静默)。
|
|
28
47
|
* 🔒 回合安全(主人硬约束 2026-09-09): LLM 回合进行中严禁 session.append(拆散 tool_calls 坏记录),
|
|
@@ -848,8 +867,10 @@ export async function apply(ctx) {
|
|
|
848
867
|
}
|
|
849
868
|
const listJoinRequestsTool = defineTool({
|
|
850
869
|
name: 'group_join_requests',
|
|
851
|
-
description: '群管理(读):
|
|
852
|
-
parameters: {
|
|
870
|
+
description: '群管理(读): 查看待审批的入群申请列表(申请人/验证消息/来源/风险提示)。默认查当前会话所在群/manageGroup; 也可传 gid 查指定群。仅主人要求时调用。需在设置开启"QQ群管理"且机器人为群管理员。⚠️群组管理器(收到【群管·入群申请】注入)的正确姿势: ①先用 session_list 找到【申请所在群】的会话; ②用 session_wake(session_id=该会话, mode=append, send_qq=true) 把申请转达过去; ③让【那个群的 bot 会话】用本工具核对名单、并用 group_approve_join 执行放行/拒绝 —— 每个群的审批交给该群自己的会话办(群成员表/权限/上下文都在那边); 只有在对方会话不可用时, 才由本会话显式带 gid 跨群代批。',
|
|
871
|
+
parameters: {
|
|
872
|
+
gid: { type: 'string', description: '目标群 openid(可选)。不填=当前会话群或 manageGroup; 填了则查指定群的待审批列表' },
|
|
873
|
+
},
|
|
853
874
|
output: {
|
|
854
875
|
schema: {
|
|
855
876
|
type: 'object', additionalProperties: false,
|
|
@@ -857,27 +878,38 @@ export async function apply(ctx) {
|
|
|
857
878
|
},
|
|
858
879
|
render: (_a, v) => [{ type: 'text', text: v.ok ? v.msg : `失败: ${v.msg}` }],
|
|
859
880
|
},
|
|
860
|
-
async execute(
|
|
881
|
+
async execute(args, exec) {
|
|
861
882
|
const ga = groupAdminOf(exec);
|
|
862
883
|
if (!ga)
|
|
863
884
|
return { ok: false, msg: '群管理未开启(设置→QQ群管理)或非群会话' };
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
885
|
+
const gid = args.gid || ga.gid;
|
|
886
|
+
if (!gid)
|
|
887
|
+
return { ok: false, msg: '当前不是群会话且未指定 gid, 无法确定目标群' };
|
|
888
|
+
const r = await ga.client.listJoinRequests(gid);
|
|
867
889
|
if (!r.ok)
|
|
868
890
|
return { ok: false, msg: r.err.human };
|
|
891
|
+
// 补群名(官方 info 接口, 失败不阻断)
|
|
892
|
+
let gname = '';
|
|
893
|
+
try {
|
|
894
|
+
const gi = await ga.client.getGroupInfo(gid);
|
|
895
|
+
if (gi.ok && gi.data?.group_name)
|
|
896
|
+
gname = gi.data.group_name;
|
|
897
|
+
}
|
|
898
|
+
catch { /* 群名失败不影响 */ }
|
|
869
899
|
const list = r.data.list;
|
|
870
900
|
if (list.length === 0)
|
|
871
|
-
return { ok: true, msg:
|
|
872
|
-
const lines = list.map((j, i) => `${i + 1}. ${j.username ?? '?'} (${
|
|
873
|
-
return { ok: true, msg: `入群申请 ${list.length}
|
|
901
|
+
return { ok: true, msg: `当前没有待审批的入群申请 ✓(群 ${gname ? `「${gname}」` : ''}${gid})` };
|
|
902
|
+
const lines = list.map((j, i) => `${i + 1}. ${j.username ?? '?'} (${j.member_openid}) 来源:${j.apply_source ?? '?'} 验证:${verifyHuman(j.verify_info) || '-'}${j.risk_tips ? ` ⚠️${j.risk_tips}` : ''}`);
|
|
903
|
+
return { ok: true, msg: `入群申请 ${list.length} 条(群 ${gname ? `「${gname}」` : ''}${gid}):\n${lines.join('\n')}` };
|
|
874
904
|
},
|
|
875
905
|
});
|
|
876
906
|
const approveJoinTool = defineTool({
|
|
877
907
|
name: 'group_approve_join',
|
|
878
|
-
description: '群管理(写,危险): 审批入群申请。approve=放行 / decline=拒绝(可带理由)。仅主人明确要求时调用; 调用前建议先 group_join_requests
|
|
908
|
+
description: '群管理(写,危险): 审批入群申请。approve=放行 / decline=拒绝(可带理由)。支持批量: member_openids 数组一次批多人(优先); 也兼容单数 member_openid。仅主人明确要求时调用; 调用前建议先 group_join_requests 核对申请人。⚠️跨群审批务必显式传 gid(不传=当前会话所在群, 会批错群)。由群组管理器(hub)代批时的顺序: 优先用 session_wake 唤醒【申请所在群的会话】, 让那边的 bot 调本工具执行审批; 该会话不可用(无会话/append 失败)时才在本会话带 gid 直批。',
|
|
879
909
|
parameters: {
|
|
880
|
-
|
|
910
|
+
gid: { type: 'string', description: '目标群 openid(可选)。不填=当前会话群或 manageGroup; 填了则审批指定群的申请' },
|
|
911
|
+
member_openids: { type: 'array', description: '批量审批: 申请人 member_openid 数组(来自 group_join_requests), 一次批多人' },
|
|
912
|
+
member_openid: { type: 'string', description: '单个申请人 member_openid(批量时可不填)' },
|
|
881
913
|
op: { type: 'string', required: true, enum: ['approve', 'decline'], description: 'approve 放行 / decline 拒绝' },
|
|
882
914
|
reason: { type: 'string', required: true, description: '拒绝理由(decline 时填; approve 或不需要可填 "-")' },
|
|
883
915
|
},
|
|
@@ -892,22 +924,336 @@ export async function apply(ctx) {
|
|
|
892
924
|
const ga = groupAdminOf(exec);
|
|
893
925
|
if (!ga)
|
|
894
926
|
return { ok: false, msg: '群管理未开启或非群会话' };
|
|
895
|
-
|
|
896
|
-
|
|
927
|
+
const gid = args.gid || ga.gid;
|
|
928
|
+
if (!gid)
|
|
929
|
+
return { ok: false, msg: '当前不是群会话且未指定 gid' };
|
|
930
|
+
const targets = Array.isArray(args.member_openids) && args.member_openids.length
|
|
931
|
+
? args.member_openids
|
|
932
|
+
: (args.member_openid ? [args.member_openid] : []);
|
|
933
|
+
if (targets.length === 0)
|
|
934
|
+
return { ok: false, msg: '请提供要审批的 member_openid(可多个)' };
|
|
897
935
|
// join_request_id 官方审批必填(缺省报 40103007): 先拉列表按 member_openid 匹配自动补上
|
|
898
|
-
const listR = await ga.client.listJoinRequests(
|
|
936
|
+
const listR = await ga.client.listJoinRequests(gid);
|
|
899
937
|
if (!listR.ok)
|
|
900
938
|
return { ok: false, msg: listR.err.human };
|
|
901
|
-
const
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
939
|
+
const list = listR.data.list ?? [];
|
|
940
|
+
const results = [];
|
|
941
|
+
let failed = 0;
|
|
942
|
+
for (const midRaw of targets) {
|
|
943
|
+
const mid = String(midRaw);
|
|
944
|
+
const found = list.find((j) => j.member_openid === mid);
|
|
945
|
+
const r = await ga.client.approveJoinRequest(gid, mid, args.op, {
|
|
946
|
+
...(found ? { join_request_id: String(found.join_request_id ?? '') } : {}),
|
|
947
|
+
...(args.op === 'decline' && String(args.reason ?? '') && String(args.reason) !== '-'
|
|
948
|
+
? { reject_reason: String(args.reason) }
|
|
949
|
+
: {}),
|
|
950
|
+
});
|
|
951
|
+
if (r.ok) {
|
|
952
|
+
results.push(`${String(mid).slice(0, 10)}… ${args.op === 'approve' ? '✅放行' : '已拒绝'}${found ? `(${String(found.username ?? '')})` : ''}`);
|
|
953
|
+
}
|
|
954
|
+
else {
|
|
955
|
+
failed++;
|
|
956
|
+
results.push(`${String(mid).slice(0, 10)}… ❌ ${r.err.human}`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
if (failed === 0) {
|
|
960
|
+
return { ok: true, msg: `${args.op === 'approve' ? '✅ 已全部放行' : '已全部拒绝'} ${targets.length} 人:\n${results.join('\n')}` };
|
|
961
|
+
}
|
|
962
|
+
return { ok: failed === targets.length ? false : true, msg: `${targets.length - failed}/${targets.length} 成功:\n${results.join('\n')}` };
|
|
963
|
+
},
|
|
964
|
+
});
|
|
965
|
+
const sessionListTool = defineTool({
|
|
966
|
+
name: 'session_list',
|
|
967
|
+
description: '会话(读): 列出本 bot 全部活跃会话(sessionId/范围/peer/最近活跃/预设), 用于跨会话寻址(找 session_wake 的目标 id)。',
|
|
968
|
+
parameters: {},
|
|
969
|
+
output: {
|
|
970
|
+
schema: {
|
|
971
|
+
type: 'object', additionalProperties: false,
|
|
972
|
+
properties: { ok: { type: 'boolean', required: true }, msg: { type: 'string', required: true } },
|
|
973
|
+
},
|
|
974
|
+
render: (_a, v) => [{ type: 'text', text: v.ok ? v.msg : `失败: ${v.msg}` }],
|
|
975
|
+
},
|
|
976
|
+
async execute(_args, exec) {
|
|
977
|
+
const tail = (s, n = 10) => (s && s.length > n ? '…' + s.slice(-n) : (s ?? ''));
|
|
978
|
+
const lines = [];
|
|
979
|
+
// 遍历全部已注册实例(module 级注册表, 重启后仍可枚举; 不依赖 channelBridges)
|
|
980
|
+
const managers = managersOf();
|
|
981
|
+
if (managers.length === 0) {
|
|
982
|
+
const ch = channelOf(exec);
|
|
983
|
+
const manager = ch?.manager;
|
|
984
|
+
if (manager)
|
|
985
|
+
managers.push(manager);
|
|
986
|
+
}
|
|
987
|
+
if (managers.length === 0)
|
|
988
|
+
return { ok: false, msg: '找不到会话管理器实例(无已注册实例)' };
|
|
989
|
+
let n = 0;
|
|
990
|
+
for (const manager of managers) {
|
|
991
|
+
const list = manager.listSessions();
|
|
992
|
+
const ns = manager.settingsNs;
|
|
993
|
+
for (const s of list) {
|
|
994
|
+
n++;
|
|
995
|
+
lines.push(`${n}. [${s.scope}]${ns !== 'im-qqbot' ? `(${ns})` : ''} peer=${tail(s.peerId)} sender=${tail(s.senderId, 8)} id=${s.sessionId}${s.agentPreset ? ` (${s.agentPreset})` : ''} 活跃=${new Date(s.lastActivity).toLocaleTimeString()}`);
|
|
996
|
+
}
|
|
997
|
+
// 潜在会话: 群注册表里的群可能还没 getOrCreate(无活跃记录), 但 sessionId 可确定性算出
|
|
998
|
+
try {
|
|
999
|
+
const raw = readFileSync(groupRegistryPath(manager.cwd), 'utf8');
|
|
1000
|
+
const reg = JSON.parse(raw);
|
|
1001
|
+
const ids = new Set(list.map((s) => s.sessionId));
|
|
1002
|
+
for (const gid of Object.keys(reg ?? {})) {
|
|
1003
|
+
const sid = manager.sessionIdFor('group', gid);
|
|
1004
|
+
if (!ids.has(sid))
|
|
1005
|
+
lines.push(`[潜在群] ${reg[gid]?.name ?? ''}(${tail(gid)}) id=${sid} 活跃=未创建`);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
catch { /* 无注册表/读失败则跳过 */ }
|
|
1009
|
+
}
|
|
1010
|
+
if (lines.length === 0)
|
|
1011
|
+
return { ok: true, msg: '当前无活跃会话, 也无已注册群' };
|
|
1012
|
+
return { ok: true, msg: `会话 ${lines.length} 个:\n${lines.join('\n')}` };
|
|
1013
|
+
},
|
|
1014
|
+
});
|
|
1015
|
+
/** 发到目标 QQ 群/私聊: 若给了 media 先发图(用目标 manager 归属的 sender), 再发带来源文本; 无 sender 则 groupAdmin 文本兜底 */
|
|
1016
|
+
async function sendQQWithMedia(manager, scope, peer, body, media, exec) {
|
|
1017
|
+
const ch = channelBridges.find((b) => b.manager === manager);
|
|
1018
|
+
const sent = [];
|
|
1019
|
+
if (media) {
|
|
1020
|
+
if (ch?.sender) {
|
|
1021
|
+
try {
|
|
1022
|
+
const src = /^https?:\/\//i.test(media) ? { url: media } : { localPath: media };
|
|
1023
|
+
await ch.sender.sendMedia({ scope, targetId: peer }, 'image', src);
|
|
1024
|
+
sent.push('🖼️图');
|
|
1025
|
+
}
|
|
1026
|
+
catch {
|
|
1027
|
+
sent.push('⚠️发图失败');
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
else {
|
|
1031
|
+
sent.push('⚠️无发送通道');
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
const ga = groupAdminOf(exec);
|
|
1035
|
+
if (ga) {
|
|
1036
|
+
if (scope === 'c2c') {
|
|
1037
|
+
// c2c 主动推送必须走官方 is_wakeup 召回通道(30天窗), 自建 REST sendC2cText 无 is_wakeup
|
|
1038
|
+
// → 被动回复窗口外不投递(web LLM 已唤醒但 QQ 收不到, 2026-09-10 主人实测 bug)。
|
|
1039
|
+
// 优先 ch.sender.sendC2cWakeup(SDK sendWakeup); 无 sender 才退自建 REST。
|
|
1040
|
+
if (ch?.sender && typeof ch.sender.sendC2cWakeup === 'function') {
|
|
1041
|
+
try {
|
|
1042
|
+
await ch.sender.sendC2cWakeup({ scope, targetId: peer }, body);
|
|
1043
|
+
sent.push('📨已发到QQ 私聊');
|
|
1044
|
+
}
|
|
1045
|
+
catch (err) {
|
|
1046
|
+
sent.push(`⚠️QQ私聊发送失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
else {
|
|
1050
|
+
const sr = await ga.client.sendC2cText(peer, body);
|
|
1051
|
+
sent.push(sr.ok ? '📨已发到QQ 私聊(降级)' : `⚠️QQ发送失败: ${sr.err.human}`);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
else {
|
|
1055
|
+
const sr = await ga.client.sendGroupText(peer, body);
|
|
1056
|
+
sent.push(sr.ok ? '📨已发到QQ 群' : `⚠️QQ发送失败: ${sr.err.human}`);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
else {
|
|
1060
|
+
sent.push('⚠️群管理未开启');
|
|
1061
|
+
}
|
|
1062
|
+
return `;${sent.join(' ')}`;
|
|
1063
|
+
}
|
|
1064
|
+
const sessionWakeTool = defineTool({
|
|
1065
|
+
name: 'session_wake',
|
|
1066
|
+
description: '跨会话(写,需谨慎): 向指定会话发送一条消息; mode=wake(默认)唤醒该会话的 LLM(给 AI 看), mode=append 只把消息写进该会话上下文不唤醒(省 token, 喇叭模式, AI 下次回合自然看到)。同时把带来源标注的消息通过 QQBot 通道发到该会话绑定的群/私聊(给人看)。可用 session_list 查目标 id; 也支持按 peerId(群/私聊) 寻址。支持 batch 批量: 一次向多个会话各发不同文本(每项带自己的 session_id/peer_id/text/mode/send_qq/media)。仅主人明确要求时调用。', parameters: {
|
|
1067
|
+
session_id: { type: 'string', description: '目标会话 id(完整 sessionId, 来自 session_list)' },
|
|
1068
|
+
peer_id: { type: 'string', description: '或按 peer 寻址: 群 openid/私聊 openid(需带 scope)' },
|
|
1069
|
+
scope: { type: 'string', enum: ['group', 'c2c'], description: 'peer_id 寻址时的范围(group=群 / c2c=私聊)' },
|
|
1070
|
+
text: { type: 'string', required: true, description: '要发送的消息内容(唤醒模式会作为用户消息喂给 AI)' },
|
|
1071
|
+
mode: { type: 'string', enum: ['wake', 'append'], description: 'wake=唤醒 LLM 开回合(默认); append=只追加上下文不唤醒(省 token, 喇叭模式)' },
|
|
1072
|
+
send_qq: { type: 'boolean', description: '是否同时发到绑定的 QQ 群/私聊(给人看, 默认 true)。false=只处理 LLM 侧不走 QQ 通道' },
|
|
1073
|
+
media: { type: 'string', description: '跨群发图: 图片本地路径或 http(s) URL, 随消息发到目标群(需 send_qq=true)' },
|
|
1074
|
+
batch: { type: 'array', description: '批量模式(2026-09-10): 一次向多个会话各发不同文本。每项: {session_id 或 peer_id+scope, text, mode?, send_qq?, media?}。单发参数与 batch 二选一, batch 优先。', items: {
|
|
1075
|
+
type: 'object', additionalProperties: false, properties: {
|
|
1076
|
+
session_id: { type: 'string', description: '目标会话 id' },
|
|
1077
|
+
peer_id: { type: 'string', description: '或 peer 寻址: 群/私聊 openid' },
|
|
1078
|
+
scope: { type: 'string', enum: ['group', 'c2c'], description: 'peer 寻址范围' },
|
|
1079
|
+
text: { type: 'string', required: true, description: '该会话要发的消息内容' },
|
|
1080
|
+
mode: { type: 'string', enum: ['wake', 'append'], description: '默认 wake' },
|
|
1081
|
+
send_qq: { type: 'boolean', description: '默认 true' },
|
|
1082
|
+
media: { type: 'string', description: '该会话随消息发的图片' },
|
|
1083
|
+
},
|
|
1084
|
+
} },
|
|
1085
|
+
},
|
|
1086
|
+
output: {
|
|
1087
|
+
schema: {
|
|
1088
|
+
type: 'object', additionalProperties: false,
|
|
1089
|
+
properties: { ok: { type: 'boolean', required: true }, msg: { type: 'string', required: true } },
|
|
1090
|
+
},
|
|
1091
|
+
render: (_a, v) => [{ type: 'text', text: v.ok ? v.msg : `失败: ${v.msg}` }],
|
|
1092
|
+
},
|
|
1093
|
+
async execute(args, exec) {
|
|
1094
|
+
// 来源标注(便于对方直接会话): 优先当前执行 agent 的 id(=会话 id); 再取当前会话记录;
|
|
1095
|
+
// 再回退 groupAdmin 的 hub 会话; 最后 'web'
|
|
1096
|
+
const from = (() => {
|
|
1097
|
+
try {
|
|
1098
|
+
const aid = exec.agent?.id;
|
|
1099
|
+
if (typeof aid === 'string' && aid.length >= 8)
|
|
1100
|
+
return aid.slice(0, 8) + '…';
|
|
1101
|
+
const ch0 = channelOf(exec);
|
|
1102
|
+
const src = findSessionRec(ch0, exec);
|
|
1103
|
+
if (src?.rec?.sessionId)
|
|
1104
|
+
return src.rec.sessionId.slice(0, 8) + '…';
|
|
1105
|
+
}
|
|
1106
|
+
catch { /* 忽略 */ }
|
|
1107
|
+
return 'web';
|
|
1108
|
+
})();
|
|
1109
|
+
const map = {
|
|
1110
|
+
ok: '✅ 已发送并唤醒',
|
|
1111
|
+
'no-session': `❌ 会话不存在: 目标会话不在任何已注册实例中(或已重建/换绑)`,
|
|
1112
|
+
'no-followup': '❌ 该会话 agent 不支持 followup 唤醒',
|
|
1113
|
+
'no-append': '❌ 该会话 agent 不支持上下文追加',
|
|
1114
|
+
'no-agent': '❌ 找不到目标会话 agent',
|
|
1115
|
+
busy: '⏳ 目标会话正忙(回合进行中且无安全注入通道)',
|
|
1116
|
+
fail: '❌ 处理异常',
|
|
1117
|
+
};
|
|
1118
|
+
/** 单条发送(单发与 batch 共用): one={session_id?|peer_id?+scope?, text, mode?, send_qq?, media?} */
|
|
1119
|
+
const sendOne = async (one) => {
|
|
1120
|
+
const text = String(one.text || '');
|
|
1121
|
+
if (!text)
|
|
1122
|
+
return '❌ 缺少 text';
|
|
1123
|
+
const body = `【来自会话 ${from}】\n${text}${one.media ? `\n[附带图片: ${one.media}]` : ''}`;
|
|
1124
|
+
const sid = String(one.session_id || '');
|
|
1125
|
+
if (sid) {
|
|
1126
|
+
// ① session_id 寻址: module 级注册表跨全部实例精确命中(重启后仍可查)
|
|
1127
|
+
const manager = findManagerBySessionId(sid);
|
|
1128
|
+
if (!manager)
|
|
1129
|
+
return `❌ 会话不存在: ${sid.slice(0, 8)}…(不在任何已注册实例)`;
|
|
1130
|
+
const mode = String(one.mode || 'wake');
|
|
1131
|
+
let r;
|
|
1132
|
+
if (mode === 'append') {
|
|
1133
|
+
const rec0 = manager.findBySessionId(sid);
|
|
1134
|
+
const targetAgent = rec0?.agent
|
|
1135
|
+
?? manager.findHostAgent(sid)?.agent
|
|
1136
|
+
?? (await manager.resumeHostAgent(sid).catch(() => undefined))?.agent;
|
|
1137
|
+
if (!targetAgent)
|
|
1138
|
+
r = 'no-session';
|
|
1139
|
+
else
|
|
1140
|
+
r = await safeAppendUserMessage(targetAgent, body, loggerLike(exec));
|
|
1141
|
+
}
|
|
1142
|
+
else {
|
|
1143
|
+
r = await wakeSessionAgent(manager, sid, loggerLike(exec), body);
|
|
1144
|
+
}
|
|
1145
|
+
let extra = '';
|
|
1146
|
+
if (one.send_qq !== false) {
|
|
1147
|
+
// 目标 peer: ⚠️ 不能只问 findManagerBySessionId 返回的 manager——它可能经 findHostAgent 命中
|
|
1148
|
+
// 错误实例(宿主 registry 全局), 导致 rec=undefined → 静默不发(2026-09-10 04:35 实测)。
|
|
1149
|
+
let qScope;
|
|
1150
|
+
let qPeer = '';
|
|
1151
|
+
let ownerManager;
|
|
1152
|
+
for (const m of managersOf()) {
|
|
1153
|
+
try {
|
|
1154
|
+
const r2 = m.findBySessionId(sid);
|
|
1155
|
+
if (r2) {
|
|
1156
|
+
ownerManager = m;
|
|
1157
|
+
qScope = r2.scope;
|
|
1158
|
+
qPeer = r2.peerId;
|
|
1159
|
+
break;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
catch { /* 单实例异常跳过 */ }
|
|
1163
|
+
}
|
|
1164
|
+
if (!ownerManager) {
|
|
1165
|
+
for (const m of managersOf()) {
|
|
1166
|
+
try {
|
|
1167
|
+
const raw = readFileSync(groupRegistryPath(m.cwd), 'utf8');
|
|
1168
|
+
const reg = JSON.parse(raw);
|
|
1169
|
+
for (const gid of Object.keys(reg ?? {})) {
|
|
1170
|
+
if (m.sessionIdFor('group', gid) === sid) {
|
|
1171
|
+
ownerManager = m;
|
|
1172
|
+
qScope = 'group';
|
|
1173
|
+
qPeer = gid;
|
|
1174
|
+
break;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
if (ownerManager)
|
|
1178
|
+
break;
|
|
1179
|
+
}
|
|
1180
|
+
catch { /* 无注册表则跳过 */ }
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
if (ownerManager && qScope && qPeer) {
|
|
1184
|
+
extra = await sendQQWithMedia(ownerManager, qScope, qPeer, body, String(one.media || ''), exec);
|
|
1185
|
+
}
|
|
1186
|
+
else {
|
|
1187
|
+
const diag = `未找到目标会话的 QQ peer(findBySessionId 与群注册表均 miss, sid=${sid.slice(0, 8)}…)`;
|
|
1188
|
+
try {
|
|
1189
|
+
loggerLike(exec).warn(`[session_wake] ${diag}`);
|
|
1190
|
+
}
|
|
1191
|
+
catch { /* */ }
|
|
1192
|
+
extra = `;⚠️${diag}`;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
return `${map[r] ?? r} (${sid.slice(0, 8)}…)${extra}`;
|
|
1196
|
+
}
|
|
1197
|
+
// ② peer 寻址
|
|
1198
|
+
const peer = String(one.peer_id || '');
|
|
1199
|
+
const sc = String(one.scope || '');
|
|
1200
|
+
if (!peer || !sc)
|
|
1201
|
+
return '请给 session_id, 或 scope+peer_id';
|
|
1202
|
+
let manager = findManagerByPeer(sc, peer);
|
|
1203
|
+
if (!manager) {
|
|
1204
|
+
const curCh = channelOf(exec);
|
|
1205
|
+
if (curCh?.manager && curCh.manager.findByPeer(sc, peer))
|
|
1206
|
+
manager = curCh.manager;
|
|
1207
|
+
else if (curCh?.manager)
|
|
1208
|
+
manager = curCh.manager;
|
|
1209
|
+
}
|
|
1210
|
+
if (!manager)
|
|
1211
|
+
return '找不到会话管理器实例(无已注册实例)';
|
|
1212
|
+
let rec = manager.findByPeer(sc, peer);
|
|
1213
|
+
if (!rec) {
|
|
1214
|
+
if (sc === 'c2c') {
|
|
1215
|
+
return `❌ 未找到该私聊对象的真实会话(${peer.slice(0, 8)}…): 请先让对方主动私聊本 bot 一次(dock「💬聊天」里能直接发), 或用 session_id 寻址。原因: openid 按 bot 应用隔离, 乱建会话会发到错误实例`;
|
|
1216
|
+
}
|
|
1217
|
+
rec = await manager.getOrCreate(sc, peer, 'master', { scope: sc, targetId: peer });
|
|
1218
|
+
if (!rec)
|
|
1219
|
+
return `会话创建失败: ${sc} ${peer.slice(0, 8)}…`;
|
|
1220
|
+
}
|
|
1221
|
+
const sid2 = rec.sessionId;
|
|
1222
|
+
const mode2 = String(one.mode || 'wake');
|
|
1223
|
+
let r;
|
|
1224
|
+
if (mode2 === 'append') {
|
|
1225
|
+
r = rec.agent
|
|
1226
|
+
? await safeAppendUserMessage(rec.agent, body, loggerLike(exec))
|
|
1227
|
+
: 'no-session';
|
|
1228
|
+
}
|
|
1229
|
+
else {
|
|
1230
|
+
r = await wakeSessionAgent(manager, sid2, loggerLike(exec), body);
|
|
1231
|
+
}
|
|
1232
|
+
let extra = '';
|
|
1233
|
+
if (one.send_qq !== false) {
|
|
1234
|
+
extra = await sendQQWithMedia(manager, sc, peer, body, String(one.media || ''), exec);
|
|
1235
|
+
}
|
|
1236
|
+
return `${map[r] ?? r} (${sid2.slice(0, 8)}…)${extra}`;
|
|
1237
|
+
};
|
|
1238
|
+
// batch 优先(2026-09-10 主人: 多参数=一次多个会话各发不同文本)
|
|
1239
|
+
if (Array.isArray(args.batch) && args.batch.length > 0) {
|
|
1240
|
+
const results = [];
|
|
1241
|
+
for (const item of args.batch) {
|
|
1242
|
+
if (!item || typeof item !== 'object') {
|
|
1243
|
+
results.push('❌ batch 项无效');
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
try {
|
|
1247
|
+
results.push(await sendOne(item));
|
|
1248
|
+
}
|
|
1249
|
+
catch (e) {
|
|
1250
|
+
results.push(`❌ 发送异常: ${String(e?.message || e)}`);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
return { ok: results.every((x) => x.startsWith('✅')), msg: `批量 ${results.length} 条:\n` + results.map((x, i) => `${i + 1}. ${x}`).join('\n') };
|
|
1254
|
+
}
|
|
1255
|
+
const oneR = await sendOne(args);
|
|
1256
|
+
return { ok: oneR.startsWith('✅'), msg: oneR };
|
|
911
1257
|
},
|
|
912
1258
|
});
|
|
913
1259
|
const muteStateTool = defineTool({
|
|
@@ -1007,6 +1353,8 @@ export async function apply(ctx) {
|
|
|
1007
1353
|
{ name: 'group_approve_join', tool: approveJoinTool },
|
|
1008
1354
|
{ name: 'group_mute_state', tool: muteStateTool },
|
|
1009
1355
|
{ name: 'group_mute_member', tool: muteMemberTool },
|
|
1356
|
+
{ name: 'session_list', tool: sessionListTool },
|
|
1357
|
+
{ name: 'session_wake', tool: sessionWakeTool },
|
|
1010
1358
|
{ name: 'list_stickers', tool: listStickersTool },
|
|
1011
1359
|
{ name: 'sticker_tag', tool: tagStickerTool },
|
|
1012
1360
|
{ name: 'sticker_delete', tool: deleteStickerTool },
|