@bolloon/bolloon-agent 0.6.1 → 0.6.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.
Files changed (78) hide show
  1. package/README.md +128 -111
  2. package/dist/agents/gateway-group.js +231 -6
  3. package/dist/agents/group-autopilot.js +256 -0
  4. package/dist/agents/pi-sdk-tools.js +302 -0
  5. package/dist/agents/pi-sdk.js +89 -140
  6. package/dist/agents/task-group.js +21 -4
  7. package/dist/cli/log-gate.js +68 -4
  8. package/dist/cli/mention-data.js +1 -1
  9. package/dist/cli/model-selector.js +84 -104
  10. package/dist/cli/setup-wizard.js +2 -10
  11. package/dist/cli/tui-select.js +99 -10
  12. package/dist/cli-entry.js +1 -9
  13. package/dist/electron-build/agents/gateway-group.js +814 -0
  14. package/dist/electron-build/agents/gateway-group.js.map +1 -0
  15. package/dist/electron-build/agents/group-autopilot.js +302 -0
  16. package/dist/electron-build/agents/group-autopilot.js.map +1 -0
  17. package/dist/electron-build/agents/pi-sdk-tools.js +302 -0
  18. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  19. package/dist/electron-build/agents/pi-sdk.js +88 -139
  20. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  21. package/dist/electron-build/agents/task-group.js +601 -0
  22. package/dist/electron-build/agents/task-group.js.map +1 -0
  23. package/dist/electron-build/agents/task-public-text.js +97 -0
  24. package/dist/electron-build/agents/task-public-text.js.map +1 -0
  25. package/dist/electron-build/efficode/compress.js +86 -0
  26. package/dist/electron-build/efficode/compress.js.map +1 -0
  27. package/dist/electron-build/efficode/crc32.js +44 -0
  28. package/dist/electron-build/efficode/crc32.js.map +1 -0
  29. package/dist/electron-build/efficode/did.js +74 -0
  30. package/dist/electron-build/efficode/did.js.map +1 -0
  31. package/dist/electron-build/efficode/lz77.js +175 -0
  32. package/dist/electron-build/efficode/lz77.js.map +1 -0
  33. package/dist/electron-build/efficode/negotiate.js +233 -0
  34. package/dist/electron-build/efficode/negotiate.js.map +1 -0
  35. package/dist/electron-build/efficode/ops.js +96 -0
  36. package/dist/electron-build/efficode/ops.js.map +1 -0
  37. package/dist/electron-build/efficode/packet.js +279 -0
  38. package/dist/electron-build/efficode/packet.js.map +1 -0
  39. package/dist/electron-build/efficode/types.js +50 -0
  40. package/dist/electron-build/efficode/types.js.map +1 -0
  41. package/dist/electron-build/kernel/code-write-gate.js +55 -0
  42. package/dist/electron-build/kernel/code-write-gate.js.map +1 -0
  43. package/dist/electron-build/kernel/run-lifecycle.js +23 -0
  44. package/dist/electron-build/kernel/run-lifecycle.js.map +1 -1
  45. package/dist/electron-build/kernel/run-resume.js +62 -0
  46. package/dist/electron-build/kernel/run-resume.js.map +1 -0
  47. package/dist/electron-build/kernel/session-lifecycle.js +223 -0
  48. package/dist/electron-build/kernel/session-lifecycle.js.map +1 -0
  49. package/dist/electron-build/kernel/turn-selfcheck.js +65 -0
  50. package/dist/electron-build/kernel/turn-selfcheck.js.map +1 -0
  51. package/dist/electron-build/llm/config-store.js +2 -2
  52. package/dist/electron-build/llm/config-store.js.map +1 -1
  53. package/dist/electron-build/llm/model-selection.js +16 -2
  54. package/dist/electron-build/llm/model-selection.js.map +1 -1
  55. package/dist/electron-build/llm/pi-ai.js +2 -2
  56. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  57. package/dist/electron-build/orbitdb/cid-database.js +98 -19
  58. package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
  59. package/dist/electron-build/orbitdb/group-access.js +399 -0
  60. package/dist/electron-build/orbitdb/group-access.js.map +1 -0
  61. package/dist/index.js +143 -10
  62. package/dist/ios/bolloon-web.json +3 -3
  63. package/dist/ios/sw.js +1 -1
  64. package/dist/kernel/code-write-gate.js +47 -0
  65. package/dist/kernel/gate-scan.js +5 -1
  66. package/dist/kernel/plan-harness.js +3 -2
  67. package/dist/kernel/roster.js +6 -6
  68. package/dist/kernel/run-lifecycle.js +21 -0
  69. package/dist/kernel/run-resume.js +58 -0
  70. package/dist/kernel/session-lifecycle.js +210 -0
  71. package/dist/kernel/turn-selfcheck.js +60 -0
  72. package/dist/llm/config-store.js +2 -2
  73. package/dist/llm/model-selection.js +16 -2
  74. package/dist/llm/pi-ai.js +2 -2
  75. package/dist/orbitdb/cid-database.js +95 -19
  76. package/dist/web/bolloon-web.json +3 -3
  77. package/dist/web/sw.js +1 -1
  78. package/package.json +1 -1
@@ -67,6 +67,11 @@ const computer_use_js_1 = require("./computer-use.js");
67
67
  const skill_share_js_1 = require("./skill-share.js");
68
68
  // 2026-09-13: 微支付信息服务 (x402 付费信息 + 验真信封)
69
69
  const paid_info_tools_js_1 = require("./x402/paid-info-tools.js");
70
+ // 2026-10-02: 群聊的四个 AI 工具 —— 原语来自群模块与"唯一发送出口"(带隐私闸)
71
+ const gateway_group_js_1 = require("./gateway-group.js");
72
+ const task_group_js_1 = require("./task-group.js");
73
+ // 2026-10-02: 群聊自主回路 (读 → 自己判断 → 自己发言) —— AI 用 group_autopilot 工具驱动
74
+ const group_autopilot_js_1 = require("./group-autopilot.js");
70
75
  /**
71
76
  * Tools 模块 — 从 pi-sdk.ts 抽出的 registerTools() / _registerWalletTools() / _setupInboxListener()
72
77
  * / wrapToolsWithIdempotency() (2026-07-06).
@@ -229,6 +234,8 @@ async function runTerminalCommand(raw, opts = {}) {
229
234
  }
230
235
  return { success: true, output: allOutput, count: list.length, parallel: true };
231
236
  }
237
+ /** 2026-10-02: 群聊自主回路的常驻句柄 (同进程共享; stop 后置 null) */
238
+ let autopilotHandle = null;
232
239
  function registerBuiltinTools(ctx) {
233
240
  ctx.tools.set('read_document', {
234
241
  name: 'read_document',
@@ -996,6 +1003,301 @@ function registerBuiltinTools(ctx) {
996
1003
  return await runTerminalCommand(raw, { timeoutMs, cwd: ctx.cwd, commands: commands.length > 0 ? commands : undefined, background: String(args.background).toLowerCase() === 'true' });
997
1004
  }
998
1005
  });
1006
+ /**
1007
+ * 2026-10-02: @ 的归一入口 —— 模型给 `['a','b']` / `'a,b'` / `'a b'` / `'a'` 都收
1008
+ * (别因为形状不符就把 @ 静默丢掉: 真机第一次跑, 模型传的正是字符串 ⇒ @ 丢了)。
1009
+ * trim + 去 `@` 前缀 + 去空 + 去重 + 上限 20 (与记录层同一口径)。
1010
+ */
1011
+ const normalizeMentions = (raw) => {
1012
+ const flat = Array.isArray(raw) ? raw : typeof raw === 'string' ? raw.split(/[,,;;\s]+/) : raw == null ? [] : [raw];
1013
+ const out = Array.from(new Set(flat.map((x) => String(x ?? '').replace(/^@/, '').trim()).filter(Boolean))).slice(0, 20);
1014
+ return out.length ? out : undefined;
1015
+ };
1016
+ // 2026-10-02 (leo: 希望人工智能自己搞定): 群聊工具 —— 原语来自群模块 + 唯一发送出口(带隐私闸)
1017
+ ctx.tools.set('group_create', {
1018
+ name: 'group_create',
1019
+ description: '新建一个 OrbitDB 群聊并拿到邀请链接 —— **用于**你要开一个话题把别人拉进来(建好后把邀请链接发出去, 对方用 group_join 入群)。acl 默认 creator: 只有你自己能发言; 想让**谁拿到链接都能发言**就传 acl="open"(产品语义如此, 两种都合法)。',
1020
+ parameters: { name: '群名(必填)', acl: 'creator | open —— 默认 creator(创建者独占写入); open = 谁拿链接都能发言', from: '你的署名(可选; 默认本机身份)' },
1021
+ execute: async (args) => {
1022
+ const name = String(args?.name ?? '').trim();
1023
+ if (!name)
1024
+ return { success: false, error: 'name 必填(群名)' };
1025
+ const who = await (0, task_group_js_1.resolveSenderTag)(null);
1026
+ // 只认 'open'; 其它一律走产品默认 (创建者独占) —— 不猜、不替调用方放开权限
1027
+ const acl = String(args?.acl ?? '').trim() === 'open' ? 'open' : undefined;
1028
+ const r = await (0, gateway_group_js_1.createGroup)(name, { ...(who.ok ? { from: who.tag } : {}), ...(acl ? { acl } : {}) });
1029
+ const g = r.group;
1030
+ if (!g?.id)
1031
+ return { success: false, error: '建群没返回 groupId(如实报告, 不假装成功)' };
1032
+ const link = await (0, gateway_group_js_1.groupLink)(g.id).catch(() => null);
1033
+ return { success: true, output: `已建群「${g.name}」(${g.id})${acl ? ' · 开放写入(谁拿链接都能发言)' : ' · 创建者独占写入'}\n` + (link ? `邀请链接: ${link}\n把这个链接发给对方, 他用 group_join 入群。` : '(没拿到邀请链接 —— 用 group_list 再看一次)') };
1034
+ },
1035
+ });
1036
+ ctx.tools.set('group_join', {
1037
+ name: 'group_join',
1038
+ description: '加入一个 OrbitDB 群聊 —— **用于**拿到邀请链接后自助入群(幂等: 已在群里 ⇒ already)。link 传邀请链接(orbitdb:///orbitdb/<addr>?type=group&name=…)或 groupId。入群后先用 group_read 读消息、再用 group_say 发言。',
1039
+ parameters: { link: '邀请链接 或 groupId(必填)' },
1040
+ execute: async (args) => {
1041
+ const link = String(args?.link ?? '').trim();
1042
+ if (!link)
1043
+ return { success: false, error: '缺 link(邀请链接或 groupId)' };
1044
+ const r = await (0, gateway_group_js_1.joinGroup)(link);
1045
+ if (!r.ok)
1046
+ return { success: false, error: r.error || '入群失败' };
1047
+ const g = r.group;
1048
+ return {
1049
+ success: true,
1050
+ output: (r.already ? '已在群里「' : '已入群「') + String(g?.name ?? '') + '」(' + String(g?.id ?? '') + ')'
1051
+ + (g?.link ? ' · 邀请链接 ' + g.link : ''),
1052
+ };
1053
+ }
1054
+ });
1055
+ // 2026-10-02 (leo: 希望人工智能自己搞定): 群聊工具 —— 原语来自群模块 + 唯一发送出口(带隐私闸)
1056
+ ctx.tools.set('group_list', {
1057
+ name: 'group_list',
1058
+ description: '列出本机已加入的群(群名 / groupId / 加入时间 / 邀请链接)—— **用于**不知道有哪些群时先摸清楚。先用它拿 groupId, 再用 group_read / group_say。',
1059
+ parameters: {},
1060
+ execute: async (args) => {
1061
+ const groups = await (0, gateway_group_js_1.listGroups)();
1062
+ if (!groups.length)
1063
+ return { success: true, output: '(本机还没有加入任何群)' };
1064
+ const lines = [];
1065
+ for (const g of groups) {
1066
+ const link = await (0, gateway_group_js_1.groupLink)(g.id).catch(() => null);
1067
+ lines.push(`${g.name} ${g.id} 加入于 ${g.createdAt}` + (link ? `\n 邀请链接: ${link}` : ''));
1068
+ }
1069
+ return { success: true, output: lines.join('\n') };
1070
+ }
1071
+ });
1072
+ // 2026-10-02 (leo: 希望人工智能自己搞定): 群聊工具 —— 原语来自群模块 + 唯一发送出口(带隐私闸)
1073
+ ctx.tools.set('group_read', {
1074
+ name: 'group_read',
1075
+ description: '读一个群里的消息(最近 N 条, 默认 30)—— **用于**看群里发生过什么、拉取要阅读的内容。group 可直接给 groupId, 也可给邀请链接 / 群名。',
1076
+ parameters: { group: 'groupId | 邀请链接 | 群名(必填)', limit: '最多读多少条, 默认 30', branch: '只看某个分支(可选; 缺省 = 全部)' },
1077
+ execute: async (args) => {
1078
+ const res = await (0, task_group_js_1.resolveGroupRef)(String(args?.group ?? ''));
1079
+ if (!res.ok)
1080
+ return { success: false, error: res.message };
1081
+ const limit = Number(args?.limit ?? 30) || 30;
1082
+ let msgs = await (0, gateway_group_js_1.groupMessages)(res.group.groupId, limit);
1083
+ const branch = String(args?.branch ?? '').trim();
1084
+ if (branch)
1085
+ msgs = msgs.filter((m) => String(m.branch ?? '') === branch);
1086
+ if (!msgs.length)
1087
+ return { success: true, output: `(群「${res.group.name ?? res.group.groupId}」里还没有消息${branch ? ` (分支 ${branch})` : ''})` };
1088
+ // 2026-10-02: 把 @/回复/附件/分支都渲染出来 —— 只给纯文本, 模型看不出结构就没法正确回复
1089
+ const fmt = (m) => {
1090
+ const bits = [`[${new Date(m.ts).toLocaleString('zh-CN')}]`];
1091
+ if (m.id)
1092
+ bits.push(`#${m.id.slice(0, 8)}`);
1093
+ if (m.kind === 'branch')
1094
+ bits.push('[分支]');
1095
+ if (m.branch)
1096
+ bits.push(`⎇${m.branch}`);
1097
+ if (m.replyTo)
1098
+ bits.push(`↩#${String(m.replyTo).slice(0, 8)}`);
1099
+ bits.push(`${m.mentions?.length ? '@' + m.mentions.join(' @') + ' ' : ''}${m.from}:`);
1100
+ bits.push(m.text);
1101
+ if (m.attachments?.length) {
1102
+ bits.push(m.attachments.map((a) => `[${a.kind === 'image' ? '图片' : a.kind === 'audio' ? '音频' : '文件'} ${a.cid.slice(0, 14)}${a.name ? ' ' + a.name : ''}]`).join(' '));
1103
+ }
1104
+ return bits.join(' ');
1105
+ };
1106
+ return { success: true, output: msgs.map(fmt).join('\n') };
1107
+ }
1108
+ });
1109
+ // 2026-10-02 (leo: 希望人工智能自己搞定): 群聊工具 —— 原语来自群模块 + 唯一发送出口(带隐私闸)
1110
+ ctx.tools.set('group_say', {
1111
+ name: 'group_say',
1112
+ description: '在一个群里发言(写进 OrbitDB 群 store, 成员都能读到)—— **用于**把你的结论/请求讲给群成员。group 可直接给 groupId, 也可给邀请链接 / 群名。消息**先过隐私红线闸**: 不许出现地址 / DID / peerId / multiaddr / IP / 私钥形态。',
1113
+ parameters: {
1114
+ group: 'groupId | 邀请链接 | 群名(必填)',
1115
+ text: '要说的内容(必填)',
1116
+ mentions: '@ 谁 —— 短显示名数组, 例如 ["委托方","审稿人"](可选)',
1117
+ replyTo: '回复哪条 —— 该消息的 id(group_read 的输出里带)(可选)',
1118
+ branch: '发到哪个分支(可选; 缺省 = 主线)',
1119
+ },
1120
+ execute: async (args) => {
1121
+ const res = await (0, task_group_js_1.resolveGroupRef)(String(args?.group ?? ''));
1122
+ if (!res.ok)
1123
+ return { success: false, error: res.message };
1124
+ const text = String(args?.text ?? '').trim();
1125
+ if (!text)
1126
+ return { success: false, error: 'text 不能为空' };
1127
+ const who = await (0, task_group_js_1.resolveSenderTag)(null);
1128
+ if (!who.ok)
1129
+ return { success: false, error: who.message };
1130
+ const mentions = normalizeMentions(args?.mentions);
1131
+ const r = await (0, task_group_js_1.sendTrailMessage)(res.group.groupId, text, who.tag, {
1132
+ ...(mentions?.length ? { mentions } : {}),
1133
+ ...(args?.replyTo ? { replyTo: String(args.replyTo) } : {}),
1134
+ ...(args?.branch ? { branch: String(args.branch) } : {}),
1135
+ });
1136
+ if (!r.ok)
1137
+ return { success: false, error: r.error || '发送失败' };
1138
+ const extra = mentions?.length ? `(@ ${mentions.join(' ')})` : '';
1139
+ return { success: true, output: `已发到群「${res.group.name ?? res.group.groupId}」(署名 ${who.tag})${extra}: ${text}` };
1140
+ }
1141
+ });
1142
+ ctx.tools.set('group_reply', {
1143
+ name: 'group_reply',
1144
+ description: '回复群里**某一条**消息(在群里形成对话)—— **用于**回答特定的人/特定那条话,而不是发一条孤立的消息。group 给 groupId/链接/群名;replyTo 给那条消息的 id(group_read 输出里的 `#xxxxxxxx`)。',
1145
+ parameters: { group: 'groupId | 邀请链接 | 群名(必填)', replyTo: '被回复消息的 id(必填, group_read 输出里的 #xxxxxxxx)', text: '回复内容(必填)', mentions: '@ 谁(短显示名数组, 可选)' },
1146
+ execute: async (args) => {
1147
+ const res = await (0, task_group_js_1.resolveGroupRef)(String(args?.group ?? ''));
1148
+ if (!res.ok)
1149
+ return { success: false, error: res.message };
1150
+ const replyTo = String(args?.replyTo ?? '').trim().replace(/^#/, '');
1151
+ const text = String(args?.text ?? '').trim();
1152
+ if (!replyTo)
1153
+ return { success: false, error: 'replyTo 必填(被回复消息的 id)' };
1154
+ if (!text)
1155
+ return { success: false, error: 'text 不能为空' };
1156
+ const who = await (0, task_group_js_1.resolveSenderTag)(null);
1157
+ if (!who.ok)
1158
+ return { success: false, error: who.message };
1159
+ const mentions = normalizeMentions(args?.mentions);
1160
+ const r = await (0, task_group_js_1.sendTrailMessage)(res.group.groupId, text, who.tag, { replyTo, ...(mentions?.length ? { mentions } : {}) });
1161
+ if (!r.ok)
1162
+ return { success: false, error: r.error || '回复失败' };
1163
+ return { success: true, output: `已回复 #${replyTo.slice(0, 8)}(署名 ${who.tag}): ${text}` };
1164
+ },
1165
+ });
1166
+ ctx.tools.set('group_attach', {
1167
+ name: 'group_attach',
1168
+ description: '往群里发**图片/音频/文件**(消息里只放内容寻址的 CID, 字节进内容寻址层, 成员按 CID 取)—— **用于**要把一张图/一段录音/一个文件交给群里的人。给 path(本机文件, 自动算 CID 并入库)或 cid(已经在内容寻址层里)。',
1169
+ parameters: { group: 'groupId | 邀请链接 | 群名(必填)', path: '本机文件路径(与 cid 二选一)', cid: '已在内容寻址层的 CID(与 path 二选一)', kind: 'image | audio | file(默认 file; 给 path 时会按扩展名猜)', text: '随附件说的话(可选)', branch: '发到哪个分支(可选)' },
1170
+ execute: async (args) => {
1171
+ const res = await (0, task_group_js_1.resolveGroupRef)(String(args?.group ?? ''));
1172
+ if (!res.ok)
1173
+ return { success: false, error: res.message };
1174
+ const kindArg = String(args?.kind ?? '').trim();
1175
+ let cid = String(args?.cid ?? '').trim();
1176
+ let name = '';
1177
+ let bytes;
1178
+ let kind = /^(image|audio|file)$/.test(kindArg) ? kindArg : 'file';
1179
+ const p = String(args?.path ?? '').trim();
1180
+ if (p) {
1181
+ const fsp = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
1182
+ const pathMod = await Promise.resolve().then(() => __importStar(require('node:path')));
1183
+ try {
1184
+ const buf = await fsp.readFile(p);
1185
+ name = pathMod.basename(p);
1186
+ if (!kindArg) {
1187
+ const ext = pathMod.extname(p).toLowerCase();
1188
+ kind = ['.png', '.jpg', '.jpeg', '.gif', '.webp'].includes(ext) ? 'image' : ['.mp3', '.wav', '.m4a', '.ogg', '.aac'].includes(ext) ? 'audio' : 'file';
1189
+ }
1190
+ const { getCIDDatabase } = await Promise.resolve().then(() => __importStar(require('../orbitdb/cid-database.js')));
1191
+ const db = getCIDDatabase();
1192
+ if (!db.putBytes)
1193
+ return { success: false, error: '内容寻址层不支持 putBytes(该构建缺这个入口)' };
1194
+ const put = await db.putBytes(new Uint8Array(buf));
1195
+ cid = put.cid;
1196
+ bytes = put.bytes;
1197
+ }
1198
+ catch (e) {
1199
+ return { success: false, error: `读取/入库失败: ${String(e?.message || e).slice(0, 160)}` };
1200
+ }
1201
+ }
1202
+ if (!cid)
1203
+ return { success: false, error: 'path 或 cid 至少给一个' };
1204
+ const who = await (0, task_group_js_1.resolveSenderTag)(null);
1205
+ if (!who.ok)
1206
+ return { success: false, error: who.message };
1207
+ const text = String(args?.text ?? '').trim() || `[${kind === 'image' ? '图片' : kind === 'audio' ? '音频' : '文件'}] ${name || cid.slice(0, 14)}`;
1208
+ const r = await (0, task_group_js_1.sendTrailMessage)(res.group.groupId, text, who.tag, {
1209
+ attachments: [{ kind, cid, ...(name ? { name } : {}), ...(typeof bytes === 'number' ? { bytes } : {}) }],
1210
+ ...(args?.branch ? { branch: String(args.branch) } : {}),
1211
+ });
1212
+ if (!r.ok)
1213
+ return { success: false, error: r.error || '发送失败' };
1214
+ return { success: true, output: `已发附件到群「${res.group.name ?? res.group.groupId}」: ${kind} ${cid.slice(0, 14)}${name ? ' (' + name + ')' : ''}${typeof bytes === 'number' ? ' ' + bytes + 'B' : ''}(署名 ${who.tag})` };
1215
+ },
1216
+ });
1217
+ ctx.tools.set('group_branch', {
1218
+ name: 'group_branch',
1219
+ description: '在群里**开一个分支话题**(像一条独立讨论线)—— **用于**一个群里同时聊多件事, 免得主线互相打断。之后用 group_say/group_reply 带 branch 参数就发进这个分支; group_read 带 branch 只看这一支。',
1220
+ parameters: { group: 'groupId | 邀请链接 | 群名(必填)', title: '分支话题名(必填, 简短; 之后作为 branch 参数用它)' },
1221
+ execute: async (args) => {
1222
+ const res = await (0, task_group_js_1.resolveGroupRef)(String(args?.group ?? ''));
1223
+ if (!res.ok)
1224
+ return { success: false, error: res.message };
1225
+ const title = String(args?.title ?? '').trim();
1226
+ if (!title)
1227
+ return { success: false, error: 'title 必填(分支话题名)' };
1228
+ const who = await (0, task_group_js_1.resolveSenderTag)(null);
1229
+ if (!who.ok)
1230
+ return { success: false, error: who.message };
1231
+ const r = await (0, task_group_js_1.sendTrailMessage)(res.group.groupId, `分支话题: ${title}`, who.tag, { kind: 'branch', branch: title });
1232
+ if (!r.ok)
1233
+ return { success: false, error: r.error || '建分支失败' };
1234
+ return { success: true, output: `已开分支「${title}」${r.id ? ' #' + r.id.slice(0, 8) : ''} —— 之后 group_say/group_reply 带 branch="${title}" 发言即可, group_read 带 branch="${title}" 只看这一支。` };
1235
+ },
1236
+ });
1237
+ ctx.tools.set('group_members', {
1238
+ name: 'group_members',
1239
+ description: '列一个群里出现过的成员(从消息里提取) —— **用于**想知道这个群有谁、要 @ 谁之前先看一眼。',
1240
+ parameters: { group: 'groupId | 邀请链接 | 群名(必填)' },
1241
+ execute: async (args) => {
1242
+ const res = await (0, task_group_js_1.resolveGroupRef)(String(args?.group ?? ''));
1243
+ if (!res.ok)
1244
+ return { success: false, error: res.message };
1245
+ const members = await (0, gateway_group_js_1.groupMembers)(res.group.groupId);
1246
+ return { success: true, output: members.length ? `群「${res.group.name ?? res.group.groupId}」成员/发言者 ${members.length} 个: ${members.join(' · ')}` : `(群「${res.group.name ?? res.group.groupId}」里还没有成员发过言)` };
1247
+ },
1248
+ });
1249
+ ctx.tools.set('group_autopilot', {
1250
+ name: 'group_autopilot',
1251
+ description: '群聊自主回路 —— **用于**让 AI 自己读群、自己判断该不该开口、自己在群里发言(不用人盯着)。action: run(立刻跑一轮) / start(挂成常驻, 每 intervalMs 一轮) / stop / status。默认只在「被 @」或「有人提问/请人做事」时说话, 冷却 60s、每小时每群最多 6 次, 处理过的消息不会重复回应。',
1252
+ parameters: { action: 'run | start | stop | status(必填)', intervalMs: '常驻时的间隔 ms(默认 60000)', mode: 'quiet | active | maintenance(可选; maintenance 专门维护 Bolloon)', group: '只盯某个群(groupId/链接/群名, 可选; 缺省 = 全部已加入的群)' },
1253
+ execute: async (args) => {
1254
+ const action = String(args?.action ?? 'run').trim();
1255
+ const who = await (0, task_group_js_1.resolveSenderTag)(null);
1256
+ if (!who.ok)
1257
+ return { success: false, error: who.message };
1258
+ const only = String(args?.group ?? '').trim();
1259
+ const state = (0, group_autopilot_js_1.loadAutopilotState)();
1260
+ const ports = {
1261
+ me: who.tag,
1262
+ listGroups: async () => {
1263
+ const gs = await (0, gateway_group_js_1.listGroups)();
1264
+ if (!only)
1265
+ return gs.map((g) => ({ id: g.id, name: g.name }));
1266
+ const r = await (0, task_group_js_1.resolveGroupRef)(only);
1267
+ return r.ok ? [{ id: r.group.groupId, name: r.group.name }] : [];
1268
+ },
1269
+ readMessages: (gid, limit) => (0, gateway_group_js_1.groupMessages)(gid, limit),
1270
+ speak: (gid, text, opts) => (0, task_group_js_1.sendTrailMessage)(gid, text, who.tag, opts ?? {}),
1271
+ state,
1272
+ policy: { mode: String(args?.mode ?? 'quiet') },
1273
+ };
1274
+ if (action === 'stop') {
1275
+ if (!autopilotHandle)
1276
+ return { success: true, output: '没有在跑的常驻回路(无需停)' };
1277
+ autopilotHandle.stop();
1278
+ const runs = autopilotHandle.runs();
1279
+ autopilotHandle = null;
1280
+ return { success: true, output: `已停常驻回路(本次共跑 ${runs} 轮)` };
1281
+ }
1282
+ if (action === 'status') {
1283
+ const last = autopilotHandle?.lastReport() ?? null;
1284
+ return { success: true, output: autopilotHandle
1285
+ ? `常驻回路在跑: 已 ${autopilotHandle.runs()} 轮;最近一轮: 发言 ${last?.spoke ?? 0} · 静默 ${last?.silent ?? 0}${last?.errors.length ? ' · 错误 ' + last.errors.length : ''}`
1286
+ : `没有常驻回路在跑。状态文件: ${Object.keys(state.groups).length} 个群有记录` };
1287
+ }
1288
+ if (action === 'start') {
1289
+ if (autopilotHandle)
1290
+ return { success: true, output: '常驻回路已经在跑了' };
1291
+ const intervalMs = Number(args?.intervalMs ?? 60_000) || 60_000;
1292
+ autopilotHandle = (0, group_autopilot_js_1.startGroupAutopilot)({ ...ports, intervalMs });
1293
+ return { success: true, output: `已挂常驻回路(每 ${Math.round(intervalMs / 1000)}s 一轮;被 @ 或在有人提问时发言)。要停用 group_autopilot action=stop。` };
1294
+ }
1295
+ const r = await (0, group_autopilot_js_1.runGroupAutopilotOnce)(ports);
1296
+ const head = `跑了一轮: 发言 ${r.spoke} · 静默 ${r.silent}${r.errors.length ? ` · 错误 ${r.errors.length}` : ''}`;
1297
+ const tail = [...r.details, ...r.errors.map((e) => `⚠ ${e}`)].slice(0, 8).join('\n');
1298
+ return { success: true, output: tail ? `${head}\n${tail}` : head };
1299
+ },
1300
+ });
999
1301
  // 2026-08-12 (TaskD): process — 后台进程管理 (学 hermes terminal background session + poll/wait/kill).
1000
1302
  // 长命令不阻塞对话: terminal(background=true) 启动 → process 工具轮询/等待/终止.
1001
1303
  ctx.tools.set('process', {