koishi-plugin-onebot-group-manage 0.0.2 → 0.0.4

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/lib/index.d.ts CHANGED
@@ -17,6 +17,7 @@ export interface Config {
17
17
  kick: ActionRule;
18
18
  title: ActionRule;
19
19
  selfMute: SelfMuteConfig;
20
+ chatlunaTools?: boolean;
20
21
  debug?: DebugOptions;
21
22
  }
22
23
  export declare const Config: Schema<Config>;
package/lib/index.js CHANGED
@@ -26,193 +26,553 @@ __export(src_exports, {
26
26
  });
27
27
  module.exports = __toCommonJS(src_exports);
28
28
  var import_koishi = require("koishi");
29
+
30
+ // src/tools.ts
31
+ var import_tools = require("@langchain/core/tools");
32
+ var import_zod = require("zod");
33
+ var GroupMuteTool = class extends import_tools.StructuredTool {
34
+ constructor(runMute, verbose) {
35
+ super();
36
+ this.runMute = runMute;
37
+ this.verbose = verbose;
38
+ }
39
+ static {
40
+ __name(this, "GroupMuteTool");
41
+ }
42
+ name = "group_mute";
43
+ description = "在群内禁言成员";
44
+ schema = import_zod.z.object({
45
+ groupId: import_zod.z.string().trim().min(1).describe("目标群号,不填则使用当前会话群").optional(),
46
+ userId: import_zod.z.string().trim().min(1).describe("要禁言的成员 QQ 号"),
47
+ duration: import_zod.z.number().int().positive().describe("禁言时长(秒),0秒为解除禁言")
48
+ });
49
+ async _call(input, _runManager, runnableConfig) {
50
+ const session = runnableConfig?.configurable?.session;
51
+ if (!session) return "缺少 session,无法执行禁言";
52
+ const result = await this.runMute(session, {
53
+ targetId: input.userId,
54
+ duration: input.duration,
55
+ guildId: input.groupId
56
+ });
57
+ return typeof result === "string" ? result : JSON.stringify(result);
58
+ }
59
+ };
60
+ var GroupTitleTool = class extends import_tools.StructuredTool {
61
+ constructor(runTitle, verbose) {
62
+ super();
63
+ this.runTitle = runTitle;
64
+ this.verbose = verbose;
65
+ }
66
+ static {
67
+ __name(this, "GroupTitleTool");
68
+ }
69
+ name = "group_set_title";
70
+ description = "为成员设置群头衔(需群主权限)";
71
+ schema = import_zod.z.object({
72
+ groupId: import_zod.z.string().trim().min(1).describe("目标群号,不填则使用当前会话群").optional(),
73
+ userId: import_zod.z.string().trim().min(1).describe("要设置头衔的成员 QQ 号"),
74
+ title: import_zod.z.string().trim().min(1).describe("头衔文案")
75
+ });
76
+ async _call(input, _runManager, runnableConfig) {
77
+ const session = runnableConfig?.configurable?.session;
78
+ if (!session) return "缺少 session,无法设置头衔";
79
+ const result = await this.runTitle(session, {
80
+ targetId: input.userId,
81
+ title: input.title,
82
+ guildId: input.groupId
83
+ });
84
+ return typeof result === "string" ? result : JSON.stringify(result);
85
+ }
86
+ };
87
+ function registerChatlunaTools(ctx, config, logger, verbose, handlers) {
88
+ const platform = ctx.chatluna?.platform;
89
+ if (!platform) {
90
+ logger.debug("chatluna 未安装,跳过群管工具注册");
91
+ return;
92
+ }
93
+ ctx.effect(
94
+ () => platform.registerTool("group_mute", {
95
+ createTool: /* @__PURE__ */ __name(() => new GroupMuteTool(
96
+ (session, params) => handlers.performMute(ctx, session, params, config, logger, verbose),
97
+ verbose
98
+ ), "createTool"),
99
+ selector: /* @__PURE__ */ __name(() => true, "selector")
100
+ })
101
+ );
102
+ ctx.effect(
103
+ () => platform.registerTool("group_set_title", {
104
+ createTool: /* @__PURE__ */ __name(() => new GroupTitleTool(
105
+ (session, params) => handlers.performTitle(ctx, session, params, config, logger, verbose),
106
+ verbose
107
+ ), "createTool"),
108
+ selector: /* @__PURE__ */ __name(() => true, "selector")
109
+ })
110
+ );
111
+ }
112
+ __name(registerChatlunaTools, "registerChatlunaTools");
113
+
114
+ // src/index.ts
29
115
  var name = "onebot-group-manage";
30
116
  var ROLE_CACHE_TTL = 3e4;
31
117
  var ROLE_QUERY_TIMEOUT = 1e4;
32
118
  var roleCache = /* @__PURE__ */ new Map();
33
119
  var actionSchema = import_koishi.Schema.object({
34
120
  requireGroupAdmin: import_koishi.Schema.boolean().default(true).description("调用者必须是群管/群主"),
35
- minAuthority: import_koishi.Schema.number().min(0).default(1).description("调用者最低 Koishi 权限,0 表示不校验")
121
+ minAuthority: import_koishi.Schema.number().min(0).default(1).description("调用者最低 Koishi 权限,为 0 表示不校验")
36
122
  });
37
123
  var Config = import_koishi.Schema.object({
38
124
  mute: actionSchema.description("禁言配置"),
39
125
  unmute: actionSchema.description("解禁配置"),
40
126
  kick: actionSchema.description("踢人配置"),
41
- title: actionSchema.description("头衔配置"),
127
+ title: actionSchema.description("头衔配置(机器人需要群主权限)"),
42
128
  selfMute: import_koishi.Schema.object({
43
- minSeconds: import_koishi.Schema.number().min(1).default(60).description("随机禁言最小秒数"),
44
- maxSeconds: import_koishi.Schema.number().min(1).default(600).description("随机禁言最大秒数")
45
- }).description("禁言我配置"),
129
+ minSeconds: import_koishi.Schema.number().min(1).default(60).description("“禁言我”随机最小秒数"),
130
+ maxSeconds: import_koishi.Schema.number().min(1).default(600).description("“禁言我”随机最大秒数")
131
+ }).description("自助禁言配置"),
132
+ chatlunaTools: import_koishi.Schema.boolean().default(true).description("启用 ChatLuna 工具注册(需安装 chatluna)"),
46
133
  debug: import_koishi.Schema.object({
47
- verbose: import_koishi.Schema.boolean().default(false)
48
- }).default({ verbose: false })
134
+ verbose: import_koishi.Schema.boolean().default(false).description("开启详细日志(仅建议排查问题时启用)")
135
+ }).default({ verbose: false }).description("调试选项")
49
136
  });
50
- function withTimeout(p, ms) {
137
+ function toCamel(action) {
138
+ return action.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
139
+ }
140
+ __name(toCamel, "toCamel");
141
+ function withTimeout(p, ms, label) {
51
142
  return Promise.race([
52
143
  p,
53
- new Promise((_, r) => setTimeout(() => r(new Error(`timeout ${ms}ms`)), ms))
144
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`${label} timeout (${ms}ms)`)), ms))
54
145
  ]);
55
146
  }
56
147
  __name(withTimeout, "withTimeout");
57
148
  function getBotOnebot(bot) {
58
- return bot?.onebot || bot?.internal?.onebot || bot?.internal || null;
149
+ const direct = bot?.onebot || bot?.internal?.onebot;
150
+ if (direct) return direct;
151
+ const internal = bot?.internal;
152
+ if (internal && (internal.setGroupBan || internal.set_group_ban || internal.callApi)) return internal;
153
+ return null;
59
154
  }
60
155
  __name(getBotOnebot, "getBotOnebot");
61
- function randomInt(min, max) {
62
- return Math.floor(Math.random() * (max - min + 1)) + min;
156
+ async function callOneBotApi(bot, action, params, onebotOverride) {
157
+ const gid = params?.group_id ?? params?.groupId ?? params?.group?.id;
158
+ const uid = params?.user_id ?? params?.userId ?? params?.user?.id;
159
+ const payload = {
160
+ ...params,
161
+ group_id: gid != null ? Number(gid) : gid,
162
+ user_id: uid != null ? Number(uid) : uid
163
+ };
164
+ const ob = onebotOverride || getBotOnebot(bot);
165
+ if (ob) {
166
+ switch (action) {
167
+ case "get_group_member_info": {
168
+ if (typeof ob.getGroupMemberInfo === "function") {
169
+ return ob.getGroupMemberInfo(Number(payload.group_id), Number(payload.user_id), Boolean(payload.no_cache));
170
+ }
171
+ break;
172
+ }
173
+ case "set_group_ban": {
174
+ if (typeof ob.callApi === "function") return ob.callApi("set_group_ban", payload);
175
+ if (typeof ob.setGroupBan === "function") {
176
+ return ob.setGroupBan(Number(payload.group_id), Number(payload.user_id), Number(payload.duration ?? 0));
177
+ }
178
+ break;
179
+ }
180
+ case "set_group_kick": {
181
+ if (typeof ob.callApi === "function") return ob.callApi("set_group_kick", payload);
182
+ if (typeof ob.setGroupKick === "function") {
183
+ return ob.setGroupKick(Number(payload.group_id), Number(payload.user_id), Boolean(payload.reject_add_request));
184
+ }
185
+ break;
186
+ }
187
+ case "set_group_special_title": {
188
+ if (typeof ob.callApi === "function") return ob.callApi("set_group_special_title", payload);
189
+ if (typeof ob.setGroupSpecialTitle === "function") {
190
+ return ob.setGroupSpecialTitle(
191
+ Number(payload.group_id),
192
+ Number(payload.user_id),
193
+ payload.special_title ?? "",
194
+ Number(payload.duration ?? -1)
195
+ );
196
+ }
197
+ break;
198
+ }
199
+ default: {
200
+ if (typeof ob[action] === "function") return ob[action](payload);
201
+ const camel = toCamel(action);
202
+ if (typeof ob[camel] === "function") return ob[camel](payload);
203
+ if (typeof ob.callApi === "function") return ob.callApi(action, payload);
204
+ }
205
+ }
206
+ }
207
+ throw new Error("onebot instance not available on bot");
63
208
  }
64
- __name(randomInt, "randomInt");
65
- async function getMemberRole(probe, bot, guildId, userId, logger, verbose) {
66
- const key = `${bot.selfId}:${guildId}:${userId}`;
209
+ __name(callOneBotApi, "callOneBotApi");
210
+ function botKey(bot) {
211
+ return bot?.sid || `${bot?.platform || "onebot"}:${bot?.selfId || ""}`;
212
+ }
213
+ __name(botKey, "botKey");
214
+ async function getMemberRole(probeOnebot, targetBot, guildId, userId, logger, verbose) {
215
+ const key = `${botKey(targetBot)}::${guildId}::${userId}`;
67
216
  const cached = roleCache.get(key);
68
217
  if (cached && cached.expire > Date.now()) return cached.role;
69
218
  try {
70
- const info = await withTimeout(
71
- probe.getGroupMemberInfo(Number(guildId), Number(userId), false),
72
- ROLE_QUERY_TIMEOUT
73
- );
219
+ const direct = probeOnebot?.getGroupMemberInfo;
220
+ let info = null;
221
+ if (typeof direct === "function") {
222
+ info = await withTimeout(
223
+ direct.call(probeOnebot, Number(guildId), Number(userId), false),
224
+ ROLE_QUERY_TIMEOUT,
225
+ "get_group_member_info"
226
+ );
227
+ } else {
228
+ info = await withTimeout(
229
+ callOneBotApi(targetBot, "get_group_member_info", {
230
+ group_id: Number(guildId),
231
+ user_id: Number(userId),
232
+ no_cache: false
233
+ }, probeOnebot),
234
+ ROLE_QUERY_TIMEOUT,
235
+ "get_group_member_info"
236
+ );
237
+ }
74
238
  const role = info?.role ?? null;
75
- if (role) roleCache.set(key, { role, expire: Date.now() + ROLE_CACHE_TTL });
239
+ if (role) {
240
+ roleCache.set(key, { role, expire: Date.now() + ROLE_CACHE_TTL });
241
+ logger?.[verbose ? "info" : "debug"](
242
+ `get_group_member_info ok bot=${botKey(targetBot)} guild=${guildId} user=${userId} role=${role}`
243
+ );
244
+ } else {
245
+ logger?.[verbose ? "warn" : "debug"](
246
+ `get_group_member_info empty bot=${botKey(targetBot)} guild=${guildId} user=${userId}`
247
+ );
248
+ }
76
249
  return role;
77
- } catch {
250
+ } catch (error) {
251
+ logger?.[verbose ? "warn" : "debug"](error, `get_group_member_info failed bot=${botKey(targetBot)} guild=${guildId} user=${userId}`);
78
252
  return null;
79
253
  }
80
254
  }
81
255
  __name(getMemberRole, "getMemberRole");
82
- async function verifyCaller(session, rule) {
256
+ async function verifyCaller(session, rule, logger, verbose, guildIdOverride) {
83
257
  const authority = Number(session.user?.authority ?? 0);
84
258
  if (!rule.requireGroupAdmin && rule.minAuthority <= 0) return null;
85
259
  if (rule.minAuthority > 0 && authority >= rule.minAuthority) return null;
86
- if (rule.requireGroupAdmin && session.guildId) {
260
+ const guildId = guildIdOverride ?? session.guildId;
261
+ if (rule.requireGroupAdmin && !guildId) return "仅限群聊使用";
262
+ if (rule.requireGroupAdmin && guildId) {
87
263
  const probe = session.onebot || getBotOnebot(session.bot);
88
- const role = await getMemberRole(
89
- probe,
90
- session.bot,
91
- session.guildId,
92
- session.userId
93
- );
264
+ const role = await getMemberRole(probe, session.bot, guildId, session.userId, logger, verbose);
94
265
  if (role === "owner" || role === "admin") return null;
95
266
  }
96
267
  if (rule.requireGroupAdmin && rule.minAuthority > 0) {
97
- return `需要群管/群主或 Koishi≥${rule.minAuthority}`;
268
+ return `需要群管理/群主或至少 ${rule.minAuthority} 级 Koishi 权限`;
98
269
  }
99
- if (rule.requireGroupAdmin) return "需要群管或群主权限";
100
- return `需要 Koishi≥${rule.minAuthority}`;
270
+ if (rule.requireGroupAdmin) return "需要群管理或群主权限";
271
+ return `需要至少 ${rule.minAuthority} 级 Koishi 权限`;
101
272
  }
102
273
  __name(verifyCaller, "verifyCaller");
274
+ async function pickManageBot(ctx, guildId, requireOwner, logger, preferBot, verbose, probeOnebot) {
275
+ const candidates = ctx.bots.filter((b) => b.platform === "onebot");
276
+ const probeBot = probeOnebot || preferBot && preferBot.onebot || candidates.find((b) => b.onebot) || candidates.find((b) => b.internal?.onebot);
277
+ const probeInstance = probeBot?.onebot || probeBot?.internal?.onebot || probeOnebot;
278
+ if (!probeInstance) {
279
+ logger.warn(`no probe bot with onebot instance for guild=${guildId}`);
280
+ return null;
281
+ }
282
+ const seen = /* @__PURE__ */ new Set();
283
+ const ordered = [];
284
+ if (preferBot && preferBot.platform === "onebot") {
285
+ ordered.push(preferBot);
286
+ seen.add(botKey(preferBot));
287
+ }
288
+ for (const bot of candidates) {
289
+ const k = botKey(bot);
290
+ if (seen.has(k)) continue;
291
+ ordered.push(bot);
292
+ seen.add(k);
293
+ }
294
+ const roles = await Promise.all(
295
+ ordered.map(async (bot) => {
296
+ const role = await getMemberRole(probeInstance, bot, guildId, bot.selfId, logger, verbose);
297
+ return { bot, role };
298
+ })
299
+ );
300
+ for (const { bot, role } of roles) {
301
+ if (!role) continue;
302
+ const ok = requireOwner ? role === "owner" : role === "owner" || role === "admin";
303
+ if (ok) {
304
+ logger[verbose ? "info" : "debug"](`picked bot=${botKey(bot)} role=${role} guild=${guildId}`);
305
+ return { bot, role, probeOnebot: probeInstance };
306
+ }
307
+ }
308
+ logger.warn(`no available bot with manage permission in guild=${guildId}`);
309
+ return null;
310
+ }
311
+ __name(pickManageBot, "pickManageBot");
103
312
  function parseTargetId(raw, session) {
104
313
  if (raw) {
105
- const segs = import_koishi.segment.parse(raw);
106
- const at = segs.find((s) => s.type === "at" && s.attrs?.id);
107
- if (at) return String(at.attrs.id);
108
- const m = raw.match(/\d{5,}/);
109
- if (m) return m[0];
314
+ const segments = import_koishi.segment.parse(raw);
315
+ const at = segments.find((seg) => seg.type === "at" && seg.attrs?.id);
316
+ const atId = at?.attrs?.id;
317
+ if (atId) return String(atId);
318
+ const match = raw.match(/\d{5,}/);
319
+ if (match) return match[0];
110
320
  }
111
- return session.quote?.user?.id || null;
321
+ if (session.quote?.user?.id) return String(session.quote.user.id);
322
+ return null;
112
323
  }
113
324
  __name(parseTargetId, "parseTargetId");
114
- async function pickManageBot(ctx, guildId, requireOwner, preferBot) {
115
- const bots = ctx.bots.filter((b) => b.platform === "onebot");
116
- const probe = getBotOnebot(preferBot || bots[0]);
117
- if (!probe) return null;
118
- for (const bot of bots) {
119
- const role = await getMemberRole(probe, bot, guildId, bot.selfId);
120
- if (requireOwner ? role === "owner" : role === "owner" || role === "admin") {
121
- return { bot, probe };
325
+ function formatRule(rule) {
326
+ const needAdmin = rule.requireGroupAdmin;
327
+ const needAuth = rule.minAuthority > 0;
328
+ if (needAdmin && needAuth) return `群管/群主 或 Koishi≥${rule.minAuthority}`;
329
+ if (needAdmin) return "群管/群主";
330
+ if (needAuth) return `Koishi≥${rule.minAuthority}`;
331
+ return "无限制";
332
+ }
333
+ __name(formatRule, "formatRule");
334
+ function randomInt(min, max) {
335
+ return Math.floor(Math.random() * (max - min + 1)) + min;
336
+ }
337
+ __name(randomInt, "randomInt");
338
+ async function performMute(ctx, session, params, config, logger, verbose) {
339
+ const guildId = params.guildId ?? session.guildId;
340
+ if (!guildId) return "仅限群聊使用";
341
+ const targetId = params.targetId ?? parseTargetId(params.targetRaw || "", session);
342
+ if (!targetId) return "请提供要禁言的用户(@ 或 QQ 号)";
343
+ const seconds = Number(params.duration);
344
+ if (!Number.isFinite(seconds) || seconds <= 0) return "禁言时长必须为大于 0 的整数(秒)";
345
+ const msg = await verifyCaller(session, config.mute, logger, verbose, guildId);
346
+ if (msg) return msg;
347
+ verbose && logger.info(
348
+ `mute: guild=${guildId} caller=${session.userId} bot=${botKey(session.bot)} target=${targetId} seconds=${seconds}`
349
+ );
350
+ const picked = await pickManageBot(ctx, guildId, false, logger, session.bot, verbose, session.onebot);
351
+ if (!picked) return "没有可用且有群管权限的 Bot";
352
+ const targetRole = await getMemberRole(picked.probeOnebot, picked.bot, guildId, targetId, logger, verbose);
353
+ if (targetRole === "owner") return "无法对群主执行该操作";
354
+ const actionOnebot = getBotOnebot(picked.bot);
355
+ if (!actionOnebot) return "选中的 Bot 缺少 onebot 实例,无法执行";
356
+ try {
357
+ verbose && logger.info(`mute: use bot=${botKey(picked.bot)} role=${picked.role} duration=${seconds}`);
358
+ if (typeof actionOnebot.setGroupBan === "function") {
359
+ await actionOnebot.setGroupBan(Number(guildId), Number(targetId), Number(seconds));
360
+ } else {
361
+ await callOneBotApi(picked.bot, "set_group_ban", {
362
+ group_id: Number(guildId),
363
+ user_id: Number(targetId),
364
+ duration: Number(seconds)
365
+ });
122
366
  }
367
+ roleCache.clear();
368
+ return `已使用 ${picked.bot.selfId} 禁言 ${targetId} ${seconds} 秒`;
369
+ } catch (error) {
370
+ logger.warn(error);
371
+ return "禁言失败,检查 Bot 权限或参数";
123
372
  }
124
- return null;
125
373
  }
126
- __name(pickManageBot, "pickManageBot");
127
- function apply(ctx, config) {
128
- ctx.command("group.manage", "OneBot 群管理").channelFields(["platform"]);
129
- ctx.command("group.manage/mute <target:string> <duration:number>", "禁言").alias("gmute").action(async ({ session }, target, duration) => {
130
- if (!session?.guildId) return "仅限群聊";
131
- const deny = await verifyCaller(session, config.mute);
132
- if (deny) return deny;
133
- const uid = parseTargetId(target, session);
134
- if (!uid || duration <= 0) return "参数错误";
135
- const picked = await pickManageBot(ctx, session.guildId, false, session.bot);
136
- if (!picked) return "无可用群管 Bot";
137
- const role = await getMemberRole(picked.probe, picked.bot, session.guildId, uid);
138
- if (role === "owner") return "不能禁言群主";
139
- await picked.probe.setGroupBan(Number(session.guildId), Number(uid), duration);
374
+ __name(performMute, "performMute");
375
+ async function performTitle(ctx, session, params, config, logger, verbose) {
376
+ const guildId = params.guildId ?? session.guildId;
377
+ if (!guildId) return "仅限群聊使用";
378
+ const title = params.title?.trim();
379
+ if (!title) return "请提供要设置的头衔";
380
+ const userId = params.targetId ?? parseTargetId(params.targetRaw || "", session);
381
+ if (!userId) return "请提供要设置头衔的用户(@ 或 QQ 号)";
382
+ const msg = await verifyCaller(session, config.title, logger, verbose, guildId);
383
+ if (msg) return msg;
384
+ verbose && logger.info(
385
+ `title: guild=${guildId} caller=${session.userId} bot=${botKey(session.bot)} target=${userId} title=${title}`
386
+ );
387
+ const picked = await pickManageBot(ctx, guildId, true, logger, session.bot, verbose, session.onebot);
388
+ if (!picked) return "没有具备群主权限的 Bot 可用";
389
+ const actionOnebot = getBotOnebot(picked.bot);
390
+ if (!actionOnebot) return "选中的 Bot 缺少 onebot 实例,无法执行";
391
+ try {
392
+ verbose && logger.info(`title: use bot=${botKey(picked.bot)} role=${picked.role}`);
393
+ if (typeof actionOnebot.setGroupSpecialTitle === "function") {
394
+ await actionOnebot.setGroupSpecialTitle(Number(guildId), Number(userId), title, -1);
395
+ } else {
396
+ await callOneBotApi(picked.bot, "set_group_special_title", {
397
+ group_id: Number(guildId),
398
+ user_id: Number(userId),
399
+ special_title: title,
400
+ duration: -1
401
+ });
402
+ }
140
403
  roleCache.clear();
141
- return `已禁言 ${uid} ${duration} 秒`;
404
+ return `已使用 ${picked.bot.selfId} ${userId} 设置头衔:${title}`;
405
+ } catch (error) {
406
+ logger.warn(error);
407
+ return "设置头衔失败,检查 Bot 权限或参数";
408
+ }
409
+ }
410
+ __name(performTitle, "performTitle");
411
+ function apply(ctx, config) {
412
+ const logger = ctx.logger(name);
413
+ const verbose = config.debug?.verbose ?? false;
414
+ if (config.chatlunaTools) {
415
+ registerChatlunaTools(ctx, config, logger, verbose, {
416
+ performMute,
417
+ performTitle
418
+ });
419
+ }
420
+ ctx.command("group.manage", "OneBot 群管", { authority: 0 }).channelFields(["platform"]);
421
+ ctx.command("group.manage/check <target:string>", "调试:查询 get_group_member_info").alias("gcheck").action(async ({ session }, target) => {
422
+ if (!session?.guildId) return "仅限群聊使用";
423
+ const userId = parseTargetId(target, session);
424
+ if (!userId) return "请提供要查询的用户(@ 或 QQ 号)";
425
+ try {
426
+ const start = Date.now();
427
+ const probe = session.onebot || getBotOnebot(session.bot);
428
+ const info = await callOneBotApi(
429
+ session.bot,
430
+ "get_group_member_info",
431
+ {
432
+ group_id: Number(session.guildId),
433
+ user_id: Number(userId),
434
+ no_cache: false
435
+ },
436
+ probe
437
+ );
438
+ const cost = Date.now() - start;
439
+ const role = info?.role ?? "unknown";
440
+ return `查询成功,用时 ${cost}ms,role=${role},raw=${JSON.stringify(info)}`;
441
+ } catch (error) {
442
+ return `查询失败: ${error?.message || String(error)}`;
443
+ }
142
444
  });
143
- ctx.command("group.manage/unmute <target:string>", "解禁").alias("gunmute").action(async ({ session }, target) => {
144
- if (!session?.guildId) return "仅限群聊";
145
- const deny = await verifyCaller(session, config.unmute);
146
- if (deny) return deny;
147
- const uid = parseTargetId(target, session);
148
- if (!uid) return "参数错误";
149
- const picked = await pickManageBot(ctx, session.guildId, false, session.bot);
150
- if (!picked) return "无可用群管 Bot";
151
- await picked.probe.setGroupBan(Number(session.guildId), Number(uid), 0);
152
- roleCache.clear();
153
- return `已解禁 ${uid}`;
445
+ ctx.command("group.manage/status", "查看本群 Bot 权限状态").alias("gstatus").action(async ({ session }) => {
446
+ if (!session?.guildId) return "仅限群聊使用";
447
+ const probe = session.onebot || getBotOnebot(session.bot);
448
+ if (!probe) return "当前 Bot 无 onebot 实例,无法查询";
449
+ const bots = ctx.bots.filter((b) => b.platform === "onebot");
450
+ const lines = [];
451
+ for (const b of bots) {
452
+ const role = await getMemberRole(probe, b, session.guildId, b.selfId, logger, verbose);
453
+ lines.push(`${b.selfId}: ${role ?? "unknown"}`);
454
+ }
455
+ lines.push(`配置:mute=${formatRule(config.mute)} / unmute=${formatRule(config.unmute)} / kick=${formatRule(config.kick)} / title=${formatRule(config.title)}`);
456
+ lines.push(`自助:gmute.me 随机范围 ${config.selfMute.minSeconds}~${config.selfMute.maxSeconds} 秒`);
457
+ return lines.join("\n");
154
458
  });
155
- ctx.command("group.manage/kick <target:string>", "踢人").alias("gkick").action(async ({ session }, target) => {
156
- if (!session?.guildId) return "仅限群聊";
157
- const deny = await verifyCaller(session, config.kick);
158
- if (deny) return deny;
159
- const uid = parseTargetId(target, session);
160
- if (!uid) return "参数错误";
161
- const picked = await pickManageBot(ctx, session.guildId, false, session.bot);
162
- if (!picked) return "无可用群管 Bot";
163
- const role = await getMemberRole(picked.probe, picked.bot, session.guildId, uid);
164
- if (role === "owner") return "不能踢群主";
165
- await picked.probe.setGroupKick(Number(session.guildId), Number(uid), false);
166
- roleCache.clear();
167
- return `已踢出 ${uid}`;
459
+ ctx.command("group.manage/mute <target:string> <duration:number>", "禁言目标(秒,必须指定)").alias("gmute").action(async ({ session }, target, duration) => {
460
+ if (!session) return "缺少 session";
461
+ return performMute(
462
+ ctx,
463
+ session,
464
+ { targetRaw: target, duration: Number(duration) },
465
+ config,
466
+ logger,
467
+ verbose
468
+ );
168
469
  });
169
- ctx.command("group.manage/title <target:string> <title:text>", "设置头衔").alias("gtitle").action(async ({ session }, target, title) => {
170
- if (!session?.guildId) return "仅限群聊";
171
- const deny = await verifyCaller(session, config.title);
172
- if (deny) return deny;
173
- const uid = parseTargetId(target, session);
174
- if (!uid) return "参数错误";
175
- const picked = await pickManageBot(ctx, session.guildId, true, session.bot);
176
- if (!picked) return "无群主 Bot";
177
- await picked.probe.setGroupSpecialTitle(
178
- Number(session.guildId),
179
- Number(uid),
180
- title.trim(),
181
- -1
470
+ ctx.command("group.manage/unmute <target:string>", "解除禁言").alias("gunmute").action(async ({ session }, target) => {
471
+ if (!session?.guildId) return "仅限群聊使用";
472
+ const msg = await verifyCaller(session, config.unmute, logger, verbose);
473
+ if (msg) return msg;
474
+ const userId = parseTargetId(target, session);
475
+ if (!userId) return "请提供要解禁的用户(@ 或 QQ 号)";
476
+ verbose && logger.info(`unmute: guild=${session.guildId} caller=${session.userId} bot=${botKey(session.bot)} target=${userId}`);
477
+ const picked = await pickManageBot(ctx, session.guildId, false, logger, session.bot, verbose, session.onebot);
478
+ if (!picked) return "没有可用且有群管权限的 Bot";
479
+ const targetRole = await getMemberRole(picked.probeOnebot, picked.bot, session.guildId, userId, logger, verbose);
480
+ if (targetRole === "owner") return "无法对群主执行该操作";
481
+ const actionOnebot = getBotOnebot(picked.bot);
482
+ if (!actionOnebot) return "选中的 Bot 缺少 onebot 实例,无法执行";
483
+ try {
484
+ verbose && logger.info(`unmute: use bot=${botKey(picked.bot)} role=${picked.role}`);
485
+ if (typeof actionOnebot.setGroupBan === "function") {
486
+ await actionOnebot.setGroupBan(Number(session.guildId), Number(userId), 0);
487
+ } else {
488
+ await callOneBotApi(picked.bot, "set_group_ban", {
489
+ group_id: Number(session.guildId),
490
+ user_id: Number(userId),
491
+ duration: 0
492
+ });
493
+ }
494
+ roleCache.clear();
495
+ return `已使用 ${picked.bot.selfId} 解除 ${userId} 的禁言`;
496
+ } catch (error) {
497
+ logger.warn(error);
498
+ return "解禁失败,检查 Bot 权限或参数";
499
+ }
500
+ });
501
+ ctx.command("group.manage/kick <target:string> [reject:boolean]", "踢出成员").alias("gkick").option("reject", "-r 设为 true 拒绝再次申请").action(async ({ session, options }, target, rejectFlag) => {
502
+ if (!session?.guildId) return "仅限群聊使用";
503
+ const msg = await verifyCaller(session, config.kick, logger, verbose);
504
+ if (msg) return msg;
505
+ const userId = parseTargetId(target, session);
506
+ if (!userId) return "请提供要踢出的用户(@ 或 QQ 号)";
507
+ const reject = Boolean(options?.reject ?? rejectFlag);
508
+ verbose && logger.info(`kick: guild=${session.guildId} caller=${session.userId} bot=${botKey(session.bot)} target=${userId} reject=${reject}`);
509
+ const picked = await pickManageBot(ctx, session.guildId, false, logger, session.bot, verbose, session.onebot);
510
+ if (!picked) return "没有可用且有群管权限的 Bot";
511
+ const targetRole = await getMemberRole(picked.probeOnebot, picked.bot, session.guildId, userId, logger, verbose);
512
+ if (targetRole === "owner") return "无法对群主执行该操作";
513
+ const actionOnebot = getBotOnebot(picked.bot);
514
+ if (!actionOnebot) return "选中的 Bot 缺少 onebot 实例,无法执行";
515
+ try {
516
+ verbose && logger.info(`kick: use bot=${botKey(picked.bot)} role=${picked.role}`);
517
+ if (typeof actionOnebot.setGroupKick === "function") {
518
+ await actionOnebot.setGroupKick(Number(session.guildId), Number(userId), reject);
519
+ } else {
520
+ await callOneBotApi(picked.bot, "set_group_kick", {
521
+ group_id: Number(session.guildId),
522
+ user_id: Number(userId),
523
+ reject_add_request: reject
524
+ });
525
+ }
526
+ roleCache.clear();
527
+ return `已使用 ${picked.bot.selfId} 踢出 ${userId}${reject ? "(已拒绝再次申请)" : ""}`;
528
+ } catch (error) {
529
+ logger.warn(error);
530
+ return "踢人失败,检查 Bot 权限或参数";
531
+ }
532
+ });
533
+ ctx.command("group.manage/title <target:string> <title:text>", "设置群头衔(需群主 Bot)").alias("gtitle").action(async ({ session }, target, title) => {
534
+ if (!session) return "缺少 session";
535
+ return performTitle(
536
+ ctx,
537
+ session,
538
+ { targetRaw: target, title: title || "", guildId: session.guildId },
539
+ config,
540
+ logger,
541
+ verbose
182
542
  );
183
- roleCache.clear();
184
- return `已设置头衔`;
185
543
  });
186
- ctx.command("group.manage/mute.me", "禁言我").alias("gmute.me").action(async ({ session }) => {
187
- if (!session?.guildId) return "仅限群聊";
188
- const deny = await verifyCaller(session, config.mute);
189
- if (deny) return deny;
190
- const { minSeconds, maxSeconds } = config.selfMute;
544
+ ctx.command("group.manage/mute.me", "禁言我(随机时长)").alias("gmute.me").action(async ({ session }) => {
545
+ if (!session?.guildId) return "仅限群聊使用";
546
+ const minS = Math.max(1, Math.floor(Number(config.selfMute?.minSeconds ?? 60)));
547
+ const maxS = Math.max(1, Math.floor(Number(config.selfMute?.maxSeconds ?? 600)));
548
+ const minSeconds = Math.min(minS, maxS);
549
+ const maxSeconds = Math.max(minS, maxS);
191
550
  const seconds = randomInt(minSeconds, maxSeconds);
192
- const picked = await pickManageBot(ctx, session.guildId, false, session.bot);
193
- if (!picked) return "无可用群管 Bot";
194
- await picked.probe.setGroupBan(
195
- Number(session.guildId),
196
- Number(session.userId),
197
- seconds
551
+ const result = await performMute(
552
+ ctx,
553
+ session,
554
+ { targetId: session.userId, duration: seconds, guildId: session.guildId },
555
+ config,
556
+ logger,
557
+ verbose
198
558
  );
199
- roleCache.clear();
200
- return `你被禁言 ${seconds} 秒`;
559
+ if (typeof result === "string" && result.startsWith("已使用")) return `你已被禁言 ${seconds} 秒`;
560
+ return result;
201
561
  });
202
- ctx.command("group.manage/title.apply <title:text>", "申请群头衔").alias("gtitle.apply").action(async ({ session }, title) => {
203
- if (!session?.guildId) return "仅限群聊";
204
- const deny = await verifyCaller(session, config.title);
205
- if (deny) return deny;
206
- const picked = await pickManageBot(ctx, session.guildId, true, session.bot);
207
- if (!picked) return "无群主 Bot";
208
- await picked.probe.setGroupSpecialTitle(
209
- Number(session.guildId),
210
- Number(session.userId),
211
- title.trim(),
212
- -1
562
+ ctx.command("group.manage/title.apply <title:text>", "申请群头衔(给自己)").alias("gtitle.apply").action(async ({ session }, title) => {
563
+ if (!session?.guildId) return "仅限群聊使用";
564
+ const result = await performTitle(
565
+ ctx,
566
+ session,
567
+ { targetId: session.userId, title: title || "", guildId: session.guildId },
568
+ config,
569
+ logger,
570
+ verbose
213
571
  );
214
- roleCache.clear();
215
- return `你的头衔已设置为:${title}`;
572
+ if (typeof result === "string" && result.startsWith("已使用")) {
573
+ return `已为你设置头衔:${(title || "").trim()}`;
574
+ }
575
+ return result;
216
576
  });
217
577
  }
218
578
  __name(apply, "apply");
package/lib/tools.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { Context, Session } from 'koishi';
2
+ interface Handlers {
3
+ performMute: (ctx: Context, session: Session, params: {
4
+ targetId: string;
5
+ duration: number;
6
+ guildId?: string;
7
+ }, ...rest: any[]) => Promise<string>;
8
+ performTitle: (ctx: Context, session: Session, params: {
9
+ targetId: string;
10
+ title: string;
11
+ guildId?: string;
12
+ }, ...rest: any[]) => Promise<string>;
13
+ }
14
+ export declare function registerChatlunaTools(ctx: Context, config: {
15
+ chatlunaTools?: boolean;
16
+ }, logger: ReturnType<Context['logger']>, verbose: boolean | undefined, handlers: Handlers): void;
17
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-onebot-group-manage",
3
3
  "description": "主要针对多bot环境的群管插件,自动使用有权限的bot",
4
- "version": "0.0.2",
4
+ "version": "0.0.4",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -15,6 +15,9 @@
15
15
  "plugin"
16
16
  ],
17
17
  "peerDependencies": {
18
- "koishi": "^4.18.7"
18
+ "koishi": "^4.18.7",
19
+ "@langchain/core": "^0.3.0",
20
+ "koishi-plugin-chatluna": "^1.3.0",
21
+ "zod": "^3.23.8"
19
22
  }
20
23
  }