@nestim/koishi-plugin-qq-group-manager 0.1.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.
- package/lib/commands.d.ts +4 -0
- package/lib/commands.js +212 -0
- package/lib/index.d.ts +99 -0
- package/lib/index.js +187 -0
- package/lib/service.d.ts +167 -0
- package/lib/service.js +3160 -0
- package/lib/types.d.ts +23 -0
- package/lib/types.js +2 -0
- package/package.json +37 -0
- package/readme.md +203 -0
package/lib/commands.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerCommands = registerCommands;
|
|
4
|
+
function styleText(text, config) {
|
|
5
|
+
if (config.responseStyle !== 'meow')
|
|
6
|
+
return text;
|
|
7
|
+
return `喵~ ${text}`;
|
|
8
|
+
}
|
|
9
|
+
function formatAction(result, config, displayTarget) {
|
|
10
|
+
let text = result.message;
|
|
11
|
+
if (displayTarget && result.plan?.targetId) {
|
|
12
|
+
text = text.split(result.plan.targetId).join(displayTarget);
|
|
13
|
+
}
|
|
14
|
+
return styleText(text, config);
|
|
15
|
+
}
|
|
16
|
+
function registerCommands(ctx, service, config) {
|
|
17
|
+
ctx.command(`${config.menuCommand} [keyword:text]`, '查看 Meow 图片菜单')
|
|
18
|
+
.action(async ({ session }, keyword) => {
|
|
19
|
+
if (!keyword)
|
|
20
|
+
service.armMenuPairing(session, 30_000);
|
|
21
|
+
const result = await service.renderMenu(keyword, session);
|
|
22
|
+
service.logCommandResult('menu', { ok: true, message: 'menu rendered' }, session, { keyword });
|
|
23
|
+
return result;
|
|
24
|
+
});
|
|
25
|
+
ctx.command(`${config.command}菜单`, `查看 ${config.command} 分类菜单`)
|
|
26
|
+
.action(async ({ session }) => {
|
|
27
|
+
const result = await service.renderMenu(`${config.command}菜单`, session);
|
|
28
|
+
service.logCommandResult('menu-parent-shortcut', { ok: true, message: 'parent menu shortcut rendered' }, session, { parent: config.command });
|
|
29
|
+
return result;
|
|
30
|
+
});
|
|
31
|
+
const root = ctx.command(config.command, 'QQ群管理命令入口');
|
|
32
|
+
root
|
|
33
|
+
.subcommand('ping', '检查插件是否可用')
|
|
34
|
+
.action(async ({ session }) => {
|
|
35
|
+
const auth = await service.authorizeCommand('ping', session);
|
|
36
|
+
if (!auth.ok)
|
|
37
|
+
return auth.userMessage ?? '权限不足或上下文不符合要求。';
|
|
38
|
+
const result = { ok: true, message: 'qq-group-manager 已加载。' };
|
|
39
|
+
service.logCommandResult('ping', result, session);
|
|
40
|
+
return styleText(result.message, config);
|
|
41
|
+
});
|
|
42
|
+
root
|
|
43
|
+
.subcommand('查图', '识图调试:查看本条消息中被识别的图片信息')
|
|
44
|
+
.action(async ({ session }) => {
|
|
45
|
+
const auth = await service.authorizeCommand('img-debug', session);
|
|
46
|
+
if (!auth.ok)
|
|
47
|
+
return auth.userMessage ?? '权限不足或上下文不符合要求。';
|
|
48
|
+
const info = service.describeImageMessage(session);
|
|
49
|
+
const lines = [];
|
|
50
|
+
lines.push(`图片元素数量: ${info.elementImages.length}`);
|
|
51
|
+
lines.push(`CQ图片段: ${info.rawCq.length} 个`);
|
|
52
|
+
lines.push(`可发送链接: ${info.extractableUrls.length} 个`);
|
|
53
|
+
if (info.extractableUrls.length) {
|
|
54
|
+
info.extractableUrls.forEach((u, i) => lines.push(` ${i + 1}. ${u}`));
|
|
55
|
+
}
|
|
56
|
+
else if (info.rawCq.length) {
|
|
57
|
+
lines.push('CQ图片原始片段:');
|
|
58
|
+
info.rawCq.forEach((c, i) => lines.push(` ${i + 1}. ${c}`));
|
|
59
|
+
}
|
|
60
|
+
else if (info.elementImages.length) {
|
|
61
|
+
lines.push('图片元素原始字段:');
|
|
62
|
+
info.elementImages.forEach((im, i) => lines.push(` ${i + 1}. ${JSON.stringify(im)}`));
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
lines.push('本条消息未检测到图片。请在群内发送一张图片(可同时附加该指令)后重试。');
|
|
66
|
+
}
|
|
67
|
+
return styleText('识图诊断:\n' + lines.join('\n'), config);
|
|
68
|
+
});
|
|
69
|
+
root
|
|
70
|
+
.subcommand('plan <groupId:string>', '生成演练动作计划')
|
|
71
|
+
.option('reason', '-r <reason:string> 计划原因')
|
|
72
|
+
.action(async ({ session, options }, groupId) => {
|
|
73
|
+
if (!groupId)
|
|
74
|
+
return '请提供群号。';
|
|
75
|
+
const auth = await service.authorizeCommand('plan', session);
|
|
76
|
+
if (!auth.ok)
|
|
77
|
+
return auth.userMessage ?? '权限不足或上下文不符合要求。';
|
|
78
|
+
const result = await service.planDemoAction(groupId, options.reason);
|
|
79
|
+
service.logCommandResult('plan', result, session, { groupId });
|
|
80
|
+
return formatAction(result, config);
|
|
81
|
+
});
|
|
82
|
+
root
|
|
83
|
+
.subcommand('kick <target:string>', '踢出群成员')
|
|
84
|
+
.option('reject', '-r 拒绝再次加群请求')
|
|
85
|
+
.option('reason', '-m <reason:string> 操作原因')
|
|
86
|
+
.action(async ({ session, options }, target) => {
|
|
87
|
+
if (!target)
|
|
88
|
+
return '请提供目标 QQ 号或 @目标。';
|
|
89
|
+
const auth = await service.authorizeCommand('kick', session);
|
|
90
|
+
if (!auth.ok)
|
|
91
|
+
return auth.userMessage ?? '权限不足或上下文不符合要求。';
|
|
92
|
+
const result = await service.kick(session, target, options.reject, options.reason);
|
|
93
|
+
service.logCommandResult('kick', result, session, { target, reject: options.reject });
|
|
94
|
+
const displayTarget = await service.formatTargetDisplay(session, result.plan?.targetId, target);
|
|
95
|
+
return formatAction(result, config, displayTarget);
|
|
96
|
+
});
|
|
97
|
+
root
|
|
98
|
+
.subcommand('mute [target:string] [duration:string]', '禁言群成员(@用户或QQ号 + 时长,如 10分钟/1小时/1天)')
|
|
99
|
+
.option('reason', '-r <reason:string> 操作原因')
|
|
100
|
+
.action(async ({ session, options }) => {
|
|
101
|
+
// 参数由 service 从原始消息中智能解析,避免把禁言时长误判为目标 QQ 号
|
|
102
|
+
const parsed = await service.parseMuteArguments(session);
|
|
103
|
+
if (!parsed.ok) {
|
|
104
|
+
service.logCommandResult('mute', { ok: false, message: parsed.message }, session, { target: parsed.targetId });
|
|
105
|
+
return styleText(parsed.message, config);
|
|
106
|
+
}
|
|
107
|
+
const targetId = parsed.targetId;
|
|
108
|
+
const minutes = parsed.minutes;
|
|
109
|
+
if (await service.shouldSelfGag(session, targetId)) {
|
|
110
|
+
const result = await service.selfGag(session, 'mute-self-trigger');
|
|
111
|
+
service.logCommandResult('self-gag', result, session, { source: 'mute', target: targetId, minutes });
|
|
112
|
+
const displayTarget = await service.formatTargetDisplay(session, result.plan?.targetId, targetId);
|
|
113
|
+
return formatAction(result, config, displayTarget);
|
|
114
|
+
}
|
|
115
|
+
const auth = await service.authorizeCommand('mute', session);
|
|
116
|
+
if (!auth.ok) {
|
|
117
|
+
const punished = await service.punishUnauthorizedMuteAttempt(session, targetId);
|
|
118
|
+
if (punished) {
|
|
119
|
+
service.logCommandResult('punish-unauthorized-mute', punished, session, { source: 'mute', target: targetId, minutes });
|
|
120
|
+
const displayTarget = await service.formatTargetDisplay(session, punished.plan?.targetId, session?.userId);
|
|
121
|
+
return formatAction(punished, config, displayTarget);
|
|
122
|
+
}
|
|
123
|
+
return auth.userMessage ?? '权限不足或上下文不符合要求。';
|
|
124
|
+
}
|
|
125
|
+
const result = await service.mute(session, targetId, minutes, options.reason);
|
|
126
|
+
service.logCommandResult('mute', result, session, { target: targetId, minutes });
|
|
127
|
+
const displayTarget = await service.formatTargetDisplay(session, result.plan?.targetId, targetId);
|
|
128
|
+
return formatAction(result, config, displayTarget);
|
|
129
|
+
});
|
|
130
|
+
root
|
|
131
|
+
.subcommand('gag [target:string]', '口球(随机 1~60 分钟禁言)')
|
|
132
|
+
.alias('口球')
|
|
133
|
+
.alias('口气')
|
|
134
|
+
.option('reason', '-r <reason:string> 操作原因')
|
|
135
|
+
.action(async ({ session, options }, target) => {
|
|
136
|
+
if (!config.enableSelfGag)
|
|
137
|
+
return '口球娱乐功能未启用。';
|
|
138
|
+
if (!session?.userId)
|
|
139
|
+
return '无法识别当前账号。';
|
|
140
|
+
if (config.allowedUserIds.includes(session.userId))
|
|
141
|
+
return '喵~ 白名单用户不参与口球。';
|
|
142
|
+
const resolvedTarget = target || session.userId;
|
|
143
|
+
if (await service.shouldSelfGag(session, resolvedTarget)) {
|
|
144
|
+
const result = await service.selfGag(session, 'gag-trigger');
|
|
145
|
+
service.logCommandResult('self-gag', result, session, { source: 'gag', target: resolvedTarget });
|
|
146
|
+
const displayTarget = await service.formatTargetDisplay(session, result.plan?.targetId, resolvedTarget);
|
|
147
|
+
return formatAction(result, config, displayTarget);
|
|
148
|
+
}
|
|
149
|
+
const auth = await service.authorizeCommand('gag', session);
|
|
150
|
+
if (!auth.ok)
|
|
151
|
+
return auth.userMessage ?? '权限不足或上下文不符合要求。';
|
|
152
|
+
const randomMinutes = Math.floor(Math.random() * 60) + 1;
|
|
153
|
+
const result = await service.mute(session, resolvedTarget, randomMinutes, options.reason ?? 'gag');
|
|
154
|
+
service.logCommandResult('gag', result, session, { target: resolvedTarget, randomMinutes });
|
|
155
|
+
const displayTarget = await service.formatTargetDisplay(session, result.plan?.targetId, resolvedTarget);
|
|
156
|
+
return formatAction(result, config, displayTarget);
|
|
157
|
+
});
|
|
158
|
+
root
|
|
159
|
+
.subcommand('unmute <target:string>', '解除群成员禁言')
|
|
160
|
+
.option('reason', '-r <reason:string> 操作原因')
|
|
161
|
+
.action(async ({ session, options }, target) => {
|
|
162
|
+
if (!target)
|
|
163
|
+
return '请提供目标 QQ 号或 @目标。';
|
|
164
|
+
const auth = await service.authorizeCommand('unmute', session);
|
|
165
|
+
if (!auth.ok)
|
|
166
|
+
return auth.userMessage ?? '权限不足或上下文不符合要求。';
|
|
167
|
+
const result = await service.unmute(session, target, options.reason);
|
|
168
|
+
service.logCommandResult('unmute', result, session, { target });
|
|
169
|
+
const displayTarget = await service.formatTargetDisplay(session, result.plan?.targetId, target);
|
|
170
|
+
return formatAction(result, config, displayTarget);
|
|
171
|
+
});
|
|
172
|
+
root
|
|
173
|
+
.subcommand('admin <target:string> [state:string]', '设置或取消群管理员', {
|
|
174
|
+
dependencies: ['qqgm-admin'],
|
|
175
|
+
showWarning: false,
|
|
176
|
+
})
|
|
177
|
+
.option('reason', '-r <reason:string> 操作原因')
|
|
178
|
+
.action(async ({ session, options }, target, state) => {
|
|
179
|
+
if (!target)
|
|
180
|
+
return '请提供目标 QQ 号或 @目标。';
|
|
181
|
+
const auth = await service.authorizeCommand('admin', session);
|
|
182
|
+
if (!auth.ok)
|
|
183
|
+
return auth.userMessage ?? '权限不足或上下文不符合要求。';
|
|
184
|
+
const available = await service.canUseAdminCommand(session);
|
|
185
|
+
if (!available.ok)
|
|
186
|
+
return available.userMessage ?? '当前场景不可用该子命令。';
|
|
187
|
+
const normalized = (state ?? 'on').toLowerCase();
|
|
188
|
+
const enable = !(normalized === 'off' || normalized === '0' || normalized === 'false');
|
|
189
|
+
const result = await service.setAdmin(session, target, enable, options.reason);
|
|
190
|
+
service.logCommandResult('admin', result, session, { target, state: normalized });
|
|
191
|
+
const displayTarget = await service.formatTargetDisplay(session, result.plan?.targetId, target);
|
|
192
|
+
return formatAction(result, config, displayTarget);
|
|
193
|
+
});
|
|
194
|
+
root
|
|
195
|
+
.subcommand('审核 <code:string> <state:string>', '审核入群申请(同意/拒绝)')
|
|
196
|
+
.option('reason', '-r <reason:string> 审核理由')
|
|
197
|
+
.action(async ({ session, options }, code, state) => {
|
|
198
|
+
if (!code || !state)
|
|
199
|
+
return '请提供审核编号与状态(同意/拒绝)。';
|
|
200
|
+
const auth = await service.authorizeCommand('review-join-request', session);
|
|
201
|
+
if (!auth.ok)
|
|
202
|
+
return auth.userMessage ?? '权限不足或上下文不符合要求。';
|
|
203
|
+
const normalized = state.trim().toLowerCase();
|
|
204
|
+
const approve = ['同意', '通过', '放行', '批准', 'approve', 'pass', 'ok', 'yes'].includes(normalized);
|
|
205
|
+
const reject = ['拒绝', '驳回', 'deny', 'reject', 'no', 'refuse'].includes(normalized);
|
|
206
|
+
if (!approve && !reject)
|
|
207
|
+
return '状态仅支持:同意/拒绝。';
|
|
208
|
+
const result = await service.reviewJoinRequestDecision(session, code, approve, options.reason);
|
|
209
|
+
service.logCommandResult('review-join-request', result, session, { code, approve, source: 'command' });
|
|
210
|
+
return formatAction(result, config);
|
|
211
|
+
});
|
|
212
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Context, Schema } from 'koishi';
|
|
2
|
+
export declare const name = "qq-group-manager";
|
|
3
|
+
export declare const inject: {
|
|
4
|
+
optional: string[];
|
|
5
|
+
};
|
|
6
|
+
export interface Config {
|
|
7
|
+
command: string;
|
|
8
|
+
menuCommand: string;
|
|
9
|
+
replaceHelpAsImageMenu: boolean;
|
|
10
|
+
replaceStatusAsImage: boolean;
|
|
11
|
+
platformFilter: string[];
|
|
12
|
+
responseStyle: 'plain' | 'meow';
|
|
13
|
+
dryRun: boolean;
|
|
14
|
+
enableSelfGag: boolean;
|
|
15
|
+
enableUnauthorizedMutePunish: boolean;
|
|
16
|
+
unauthorizedMuteAttemptThreshold: number;
|
|
17
|
+
unauthorizedMuteWindowMinutes: number;
|
|
18
|
+
unauthorizedMutePunishMinMinutes: number;
|
|
19
|
+
unauthorizedMutePunishMaxMinutes: number;
|
|
20
|
+
enableAiReply: boolean;
|
|
21
|
+
aiProvider: 'openai-compatible' | 'gemini';
|
|
22
|
+
aiApiKey: string;
|
|
23
|
+
aiBaseUrl: string;
|
|
24
|
+
aiModel: string;
|
|
25
|
+
aiAgentName: string;
|
|
26
|
+
aiSystemPrompt: string;
|
|
27
|
+
aiActivePersona: string;
|
|
28
|
+
aiPersonas: AIPersona[];
|
|
29
|
+
aiReplyMode: 'threshold' | 'random' | 'hybrid';
|
|
30
|
+
aiMessageThreshold: number;
|
|
31
|
+
aiRandomReplyProbability: number;
|
|
32
|
+
aiMinReplyIntervalSeconds: number;
|
|
33
|
+
aiContextWindow: number;
|
|
34
|
+
aiTemperature: number;
|
|
35
|
+
aiMaxOutputTokens: number;
|
|
36
|
+
aiEnableImageRecognition: boolean;
|
|
37
|
+
aiImageMaxCount: number;
|
|
38
|
+
aiIgnoreCommandMessage: boolean;
|
|
39
|
+
aiEnableDirectMentionTrigger: boolean;
|
|
40
|
+
aiEnableFollowupAfterMention: boolean;
|
|
41
|
+
aiFollowupWindowSeconds: number;
|
|
42
|
+
aiFollowupMaxTurns: number;
|
|
43
|
+
aiOwnerPlatform: string;
|
|
44
|
+
aiOwnerUserId: string;
|
|
45
|
+
aiHomePlatform: string;
|
|
46
|
+
aiHomeUserId: string;
|
|
47
|
+
aiInterestMinScore: number;
|
|
48
|
+
aiInterestContextWindow: number;
|
|
49
|
+
enableRepeater: boolean;
|
|
50
|
+
repeaterThreshold: number;
|
|
51
|
+
repeaterCooldownSeconds: number;
|
|
52
|
+
repeaterEnableGetMsgRefetch: boolean;
|
|
53
|
+
enableJoinRequestReview: boolean;
|
|
54
|
+
joinRequestReviewTtlMinutes: number;
|
|
55
|
+
bannedWords: string[];
|
|
56
|
+
blockCardMessage: boolean;
|
|
57
|
+
blockForwardMessage: boolean;
|
|
58
|
+
autoDeleteViolation: boolean;
|
|
59
|
+
sendViolationNotice: boolean;
|
|
60
|
+
enableAutoMute: boolean;
|
|
61
|
+
autoMuteViolationThreshold: number;
|
|
62
|
+
autoMuteMinutes: number;
|
|
63
|
+
enableAutoKick: boolean;
|
|
64
|
+
autoKickViolationThreshold: number;
|
|
65
|
+
autoViolationWindowMinutes: number;
|
|
66
|
+
enableMemory: boolean;
|
|
67
|
+
memoryFileName: string;
|
|
68
|
+
memoryInAi: boolean;
|
|
69
|
+
groupRules: GroupRule[];
|
|
70
|
+
allowedUserIds: string[];
|
|
71
|
+
allowGroupOwner: boolean;
|
|
72
|
+
allowGroupAdmin: boolean;
|
|
73
|
+
requireBotOwnerForAdmin: boolean;
|
|
74
|
+
allowAdminBypassWhitelistMute: boolean;
|
|
75
|
+
logCommandResult: boolean;
|
|
76
|
+
logAuthCheck: boolean;
|
|
77
|
+
}
|
|
78
|
+
export interface AIPersona {
|
|
79
|
+
id: string;
|
|
80
|
+
selfName: string;
|
|
81
|
+
prompt: string;
|
|
82
|
+
}
|
|
83
|
+
export interface GroupRule {
|
|
84
|
+
guildId: string;
|
|
85
|
+
enableAiReply?: boolean;
|
|
86
|
+
bannedWords: string[];
|
|
87
|
+
blockCardMessage: boolean;
|
|
88
|
+
blockForwardMessage: boolean;
|
|
89
|
+
autoDeleteViolation: boolean;
|
|
90
|
+
sendViolationNotice: boolean;
|
|
91
|
+
autoMuteEnabled?: boolean;
|
|
92
|
+
autoMuteThreshold?: number;
|
|
93
|
+
autoMuteMinutes?: number;
|
|
94
|
+
autoKickEnabled?: boolean;
|
|
95
|
+
autoKickThreshold?: number;
|
|
96
|
+
autoViolationWindowMinutes?: number;
|
|
97
|
+
}
|
|
98
|
+
export declare const Config: Schema<Config>;
|
|
99
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Config = exports.inject = exports.name = void 0;
|
|
4
|
+
exports.apply = apply;
|
|
5
|
+
const koishi_1 = require("koishi");
|
|
6
|
+
const service_1 = require("./service");
|
|
7
|
+
const commands_1 = require("./commands");
|
|
8
|
+
exports.name = 'nestim-qq-group-manager';
|
|
9
|
+
exports.inject = {
|
|
10
|
+
optional: ['puppeteer'],
|
|
11
|
+
};
|
|
12
|
+
exports.Config = koishi_1.Schema.intersect([
|
|
13
|
+
koishi_1.Schema.object({
|
|
14
|
+
command: koishi_1.Schema.string().default('群管').description('管理命令前缀。'),
|
|
15
|
+
menuCommand: koishi_1.Schema.string().default('菜单').description('图片菜单指令名。'),
|
|
16
|
+
replaceHelpAsImageMenu: koishi_1.Schema.boolean().default(false).description('是否接管 help 指令并输出图片菜单。'),
|
|
17
|
+
replaceStatusAsImage: koishi_1.Schema.boolean().default(true).description('是否接管 status 指令并输出 Meow 状态图片。'),
|
|
18
|
+
platformFilter: koishi_1.Schema.array(String).default(['onebot']).description('允许启用管理功能的平台列表。'),
|
|
19
|
+
responseStyle: koishi_1.Schema.union([
|
|
20
|
+
koishi_1.Schema.const('plain').description('普通输出'),
|
|
21
|
+
koishi_1.Schema.const('meow').description('Meow 风格输出'),
|
|
22
|
+
]).role('radio').default('plain').description('群聊中指令返回的文本风格。'),
|
|
23
|
+
dryRun: koishi_1.Schema.boolean().default(false).description('演练模式:仅输出计划动作,不执行群管理操作。'),
|
|
24
|
+
}).description('基础设置'),
|
|
25
|
+
koishi_1.Schema.object({
|
|
26
|
+
bannedWords: koishi_1.Schema.array(String).role('table').default([]).description('违禁词列表(命中任意词即视为违规)。'),
|
|
27
|
+
blockCardMessage: koishi_1.Schema.boolean().default(true).description('是否禁止卡片消息(json/xml)。'),
|
|
28
|
+
blockForwardMessage: koishi_1.Schema.boolean().default(true).description('是否禁止合并转发消息(forward)。'),
|
|
29
|
+
autoDeleteViolation: koishi_1.Schema.boolean().default(true).description('违规消息是否自动撤回。'),
|
|
30
|
+
sendViolationNotice: koishi_1.Schema.boolean().default(true).description('处理违规消息后是否在群内发送提示。'),
|
|
31
|
+
groupRules: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
32
|
+
guildId: koishi_1.Schema.string().required().description('群号。'),
|
|
33
|
+
enableAiReply: koishi_1.Schema.boolean().description('该群是否启用 AI 回复(留空则跟随全局设置)。'),
|
|
34
|
+
bannedWords: koishi_1.Schema.array(String).role('table').default([]).description('该群专属违禁词列表。'),
|
|
35
|
+
blockCardMessage: koishi_1.Schema.boolean().default(true).description('该群是否禁止卡片消息(json/xml)。'),
|
|
36
|
+
blockForwardMessage: koishi_1.Schema.boolean().default(true).description('该群是否禁止合并转发消息(forward)。'),
|
|
37
|
+
autoDeleteViolation: koishi_1.Schema.boolean().default(true).description('该群违规消息是否自动撤回。'),
|
|
38
|
+
sendViolationNotice: koishi_1.Schema.boolean().default(true).description('该群违规后是否发送群内提示。'),
|
|
39
|
+
autoMuteEnabled: koishi_1.Schema.boolean().description('该群是否启用自动禁言(留空则跟随全局设置)。'),
|
|
40
|
+
autoMuteThreshold: koishi_1.Schema.natural().description('该群触发自动禁言的违规次数(留空则跟随全局设置)。'),
|
|
41
|
+
autoMuteMinutes: koishi_1.Schema.natural().description('该群自动禁言的时长(分钟,留空则跟随全局设置)。'),
|
|
42
|
+
autoKickEnabled: koishi_1.Schema.boolean().description('该群是否启用自动踢人(留空则跟随全局设置)。'),
|
|
43
|
+
autoKickThreshold: koishi_1.Schema.natural().description('该群触发自动踢人的违规次数(留空则跟随全局设置)。'),
|
|
44
|
+
autoViolationWindowMinutes: koishi_1.Schema.natural().description('该群违规计数统计窗口(分钟,留空则跟随全局设置)。'),
|
|
45
|
+
})).role('table').default([]).description('按群覆盖消息管控策略(未命中群号时使用全局默认)。'),
|
|
46
|
+
}).description('消息管控'),
|
|
47
|
+
koishi_1.Schema.object({
|
|
48
|
+
enableAutoMute: koishi_1.Schema.boolean().default(false).description('启用自动禁言:成员在统计窗口内累计违规达到阈值后自动禁言。'),
|
|
49
|
+
autoMuteViolationThreshold: koishi_1.Schema.natural().default(3).description('触发自动禁言所需的累计违规次数。'),
|
|
50
|
+
autoMuteMinutes: koishi_1.Schema.natural().default(10).description('自动禁言的时长(分钟)。'),
|
|
51
|
+
enableAutoKick: koishi_1.Schema.boolean().default(false).description('启用自动踢人:成员在统计窗口内累计违规达到阈值后自动移出群聊。'),
|
|
52
|
+
autoKickViolationThreshold: koishi_1.Schema.natural().default(5).description('触发自动踢人所需的累计违规次数。'),
|
|
53
|
+
autoViolationWindowMinutes: koishi_1.Schema.natural().default(60).description('违规计数统计窗口(分钟):窗口内连续违规才累计,超时未违规则重新计数。'),
|
|
54
|
+
}).description('自动管控'),
|
|
55
|
+
koishi_1.Schema.object({
|
|
56
|
+
enableMemory: koishi_1.Schema.boolean().default(true).description('启用记忆库:群内发送 [记忆]+内容 保存到 Memory.md,[记忆查询]/[记忆删除] 管理,AI 回复可引用。'),
|
|
57
|
+
memoryFileName: koishi_1.Schema.string().default('Memory.md').description('记忆库文件名(保存在 Koishi 数据目录,可通过控制台 Explorer 查看/编辑)。'),
|
|
58
|
+
memoryInAi: koishi_1.Schema.boolean().default(true).description('AI 回复/识图时是否参考记忆库内容。'),
|
|
59
|
+
}).description('记忆库'),
|
|
60
|
+
koishi_1.Schema.object({
|
|
61
|
+
enableSelfGag: koishi_1.Schema.boolean().default(true).description('启用口球娱乐:普通成员对自己使用 mute/gag 时随机 1~60 分钟禁言。'),
|
|
62
|
+
enableUnauthorizedMutePunish: koishi_1.Schema.boolean().default(true).description('启用违规惩罚:普通成员反复尝试禁言他人时禁言本人。'),
|
|
63
|
+
unauthorizedMuteAttemptThreshold: koishi_1.Schema.natural().default(2).description('触发惩罚所需的违规禁言次数。'),
|
|
64
|
+
unauthorizedMuteWindowMinutes: koishi_1.Schema.natural().default(10).description('违规计数统计窗口(分钟)。'),
|
|
65
|
+
unauthorizedMutePunishMinMinutes: koishi_1.Schema.natural().default(1).description('惩罚禁言最小分钟数。'),
|
|
66
|
+
unauthorizedMutePunishMaxMinutes: koishi_1.Schema.natural().default(10).description('惩罚禁言最大分钟数。'),
|
|
67
|
+
}).description('娱乐设置'),
|
|
68
|
+
koishi_1.Schema.object({
|
|
69
|
+
enableRepeater: koishi_1.Schema.boolean().default(false).description('启用群聊复读:连续相同消息达到阈值时 bot 复读一次。'),
|
|
70
|
+
repeaterThreshold: koishi_1.Schema.natural().default(3).description('触发复读所需连续相同消息次数。'),
|
|
71
|
+
repeaterCooldownSeconds: koishi_1.Schema.natural().default(90).description('同一内容再次允许复读的冷却时间(秒)。'),
|
|
72
|
+
repeaterEnableGetMsgRefetch: koishi_1.Schema.boolean().default(true).description('复读图片缺少可发送引用时,是否通过 get_msg 回查原消息提取图片引用。'),
|
|
73
|
+
enableJoinRequestReview: koishi_1.Schema.boolean().default(true).description('收到新的入群申请时,自动在群内发起管理员审核。'),
|
|
74
|
+
joinRequestReviewTtlMinutes: koishi_1.Schema.natural().default(30).description('入群申请审核编号的有效期(分钟)。'),
|
|
75
|
+
}).description('群聊互动'),
|
|
76
|
+
koishi_1.Schema.object({
|
|
77
|
+
enableAiReply: koishi_1.Schema.boolean().default(false).description('是否启用群聊 AI 自动回复。'),
|
|
78
|
+
aiProvider: koishi_1.Schema.union([
|
|
79
|
+
koishi_1.Schema.const('openai-compatible').description('OpenAI 兼容接口(OpenAI/火山引擎/Codex API/Auth 等)'),
|
|
80
|
+
koishi_1.Schema.const('gemini').description('Gemini 原生接口(Google Generative Language API)'),
|
|
81
|
+
]).role('radio').default('openai-compatible').description('AI 服务提供方式。'),
|
|
82
|
+
aiApiKey: koishi_1.Schema.string().role('secret').default('').description('AI 接口密钥(API Key)。'),
|
|
83
|
+
aiBaseUrl: koishi_1.Schema.string().default('https://api.openai.com/v1').description('接口基础地址(OpenAI 兼容模式需以 /v1 结尾;Gemini 模式可留默认)。'),
|
|
84
|
+
aiModel: koishi_1.Schema.string().default('gpt-4o-mini').description('模型名称,如 gpt-4o-mini / doubao-1.5-lite / gemini-2.0-flash。'),
|
|
85
|
+
aiAgentName: koishi_1.Schema.string().default('MeowBot').description('默认智能体自称。'),
|
|
86
|
+
aiSystemPrompt: koishi_1.Schema.string().role('textarea').default('你是群聊中的友好机器人,请简洁、自然、符合中文互联网语境地回复。').description('默认系统提示词。'),
|
|
87
|
+
aiActivePersona: koishi_1.Schema.string().default('').description('启用的人格 ID(为空时使用默认自称与默认提示词)。'),
|
|
88
|
+
aiPersonas: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
89
|
+
id: koishi_1.Schema.string().required().description('人格 ID(唯一)。'),
|
|
90
|
+
selfName: koishi_1.Schema.string().default('').description('该人格的自称(留空则使用默认自称)。'),
|
|
91
|
+
prompt: koishi_1.Schema.string().role('textarea').default('').description('该人格系统提示词(留空则使用默认提示词)。'),
|
|
92
|
+
})).role('table').default([]).description('可保存多个人格配置,按 aiActivePersona 切换。'),
|
|
93
|
+
aiReplyMode: koishi_1.Schema.union([
|
|
94
|
+
koishi_1.Schema.const('threshold').description('达到消息阈值后回复'),
|
|
95
|
+
koishi_1.Schema.const('random').description('按概率随机回复'),
|
|
96
|
+
koishi_1.Schema.const('hybrid').description('阈值与随机同时生效'),
|
|
97
|
+
]).role('radio').default('hybrid').description('AI 触发策略。'),
|
|
98
|
+
aiMessageThreshold: koishi_1.Schema.natural().default(16).description('阈值触发所需累计消息数。'),
|
|
99
|
+
aiRandomReplyProbability: koishi_1.Schema.percent().default(0.08).description('随机触发概率(每条消息判定一次)。'),
|
|
100
|
+
aiMinReplyIntervalSeconds: koishi_1.Schema.natural().default(45).description('同一群两次 AI 回复的最短间隔(秒)。'),
|
|
101
|
+
aiContextWindow: koishi_1.Schema.natural().default(24).description('参与推理的最近消息条数。'),
|
|
102
|
+
aiTemperature: koishi_1.Schema.number().min(0).max(2).step(0.1).default(0.8).description('生成温度(0~2)。'),
|
|
103
|
+
aiMaxOutputTokens: koishi_1.Schema.natural().default(240).description('AI 最大输出 token 数。'),
|
|
104
|
+
aiEnableImageRecognition: koishi_1.Schema.boolean().default(true).description('是否启用图片识别(支持时将图片一并发送给模型)。'),
|
|
105
|
+
aiImageMaxCount: koishi_1.Schema.natural().default(2).description('每次最多发送给模型的图片数量。'),
|
|
106
|
+
aiIgnoreCommandMessage: koishi_1.Schema.boolean().default(true).description('忽略看起来像命令的消息,避免干扰正常指令执行。'),
|
|
107
|
+
aiEnableDirectMentionTrigger: koishi_1.Schema.boolean().default(true).description('消息命中智能体名字时是否忽略阈值直接触发回复。'),
|
|
108
|
+
aiEnableFollowupAfterMention: koishi_1.Schema.boolean().default(true).description('点名触发后,是否跟随该用户一段时间继续对话。'),
|
|
109
|
+
aiFollowupWindowSeconds: koishi_1.Schema.natural().default(120).description('点名后跟随该用户的持续时间(秒)。'),
|
|
110
|
+
aiFollowupMaxTurns: koishi_1.Schema.natural().default(3).description('一次点名会话最多继续回复轮数。'),
|
|
111
|
+
aiOwnerPlatform: koishi_1.Schema.string().default('onebot').description('主人平台标识(用于识别主人用户,如 onebot)。'),
|
|
112
|
+
aiOwnerUserId: koishi_1.Schema.string().default('').description('主人平台用户 ID(如 QQ 号,用于记忆主人身份)。'),
|
|
113
|
+
aiHomePlatform: koishi_1.Schema.string().default('onebot').description('兼容字段:主页平台标识(建议改用 aiOwnerPlatform)。'),
|
|
114
|
+
aiHomeUserId: koishi_1.Schema.string().default('').description('兼容字段:主页平台用户 ID(建议改用 aiOwnerUserId)。'),
|
|
115
|
+
aiInterestMinScore: koishi_1.Schema.number().min(0).max(100).step(1).default(82).description('AI 兴趣触发最低分(非点名场景,分值越高越不容易回复)。'),
|
|
116
|
+
aiInterestContextWindow: koishi_1.Schema.number().min(4).max(16).step(1).default(8).description('非阈值兴趣判定使用的上下文窗口条数。'),
|
|
117
|
+
}).description('AI 回复'),
|
|
118
|
+
koishi_1.Schema.object({
|
|
119
|
+
allowedUserIds: koishi_1.Schema.array(String).role('table').default([]).description('允许直接使用群管理命令的账号 ID 白名单。'),
|
|
120
|
+
allowGroupOwner: koishi_1.Schema.boolean().default(true).description('是否允许群主使用管理命令。'),
|
|
121
|
+
allowGroupAdmin: koishi_1.Schema.boolean().default(true).description('是否允许群管理员使用管理命令。'),
|
|
122
|
+
requireBotOwnerForAdmin: koishi_1.Schema.boolean().default(true).description('仅当 bot 在当前群是群主时开放 admin 子命令。'),
|
|
123
|
+
allowAdminBypassWhitelistMute: koishi_1.Schema.boolean().default(true).description('白名单目标禁言保护开关:开启后允许群主/管理员对其执行禁言。'),
|
|
124
|
+
}).description('权限设置'),
|
|
125
|
+
koishi_1.Schema.object({
|
|
126
|
+
logCommandResult: koishi_1.Schema.boolean().default(true).description('记录所有群管指令的执行结果到日志。'),
|
|
127
|
+
logAuthCheck: koishi_1.Schema.boolean().default(true).description('记录白名单/群主/管理员鉴权过程到日志。'),
|
|
128
|
+
}).description('日志设置'),
|
|
129
|
+
]);
|
|
130
|
+
function apply(ctx, config) {
|
|
131
|
+
const service = new service_1.QQGroupManagerService(ctx, config);
|
|
132
|
+
const dispose = ctx.permissions.define('qqgm-admin', {
|
|
133
|
+
check: async (_data, session) => {
|
|
134
|
+
const result = await service.canUseAdminCommand(session);
|
|
135
|
+
return result.ok;
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
ctx.on('dispose', () => {
|
|
139
|
+
dispose();
|
|
140
|
+
});
|
|
141
|
+
(0, commands_1.registerCommands)(ctx, service, config);
|
|
142
|
+
if (config.replaceHelpAsImageMenu) {
|
|
143
|
+
let help;
|
|
144
|
+
try {
|
|
145
|
+
help = ctx.$commander.get('help');
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
help = null;
|
|
149
|
+
}
|
|
150
|
+
if (help) {
|
|
151
|
+
help.action(async (argv) => {
|
|
152
|
+
const keyword = typeof argv.args?.[0] === 'string' ? argv.args[0] : '';
|
|
153
|
+
if (!keyword)
|
|
154
|
+
service.armMenuPairing(argv.session, 30_000);
|
|
155
|
+
return service.renderMenu(keyword, argv.session);
|
|
156
|
+
}, true);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
ctx.command('help [keyword:text]', '查看图片菜单(由 qq-group-manager 提供)')
|
|
160
|
+
.action(async (argv, keyword) => {
|
|
161
|
+
if (!keyword)
|
|
162
|
+
service.armMenuPairing(argv.session, 30_000);
|
|
163
|
+
return service.renderMenu(keyword, argv.session);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (config.replaceStatusAsImage) {
|
|
168
|
+
let status;
|
|
169
|
+
try {
|
|
170
|
+
status = ctx.$commander.get('status');
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
status = null;
|
|
174
|
+
}
|
|
175
|
+
if (status) {
|
|
176
|
+
status.action(async (argv) => {
|
|
177
|
+
return service.renderStatusCard(argv.session);
|
|
178
|
+
}, true);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
ctx.command('status', '查看系统状态(由 qq-group-manager 提供)')
|
|
182
|
+
.action(async (argv) => {
|
|
183
|
+
return service.renderStatusCard(argv.session);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|