@nestim/koishi-plugin-qq-group-manager 0.1.3 → 0.1.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/commands.js +0 -23
- package/lib/index.d.ts +2 -0
- package/lib/index.js +4 -2
- package/lib/service.d.ts +7 -1
- package/lib/service.js +123 -16
- package/package.json +1 -1
package/lib/commands.js
CHANGED
|
@@ -29,16 +29,6 @@ function registerCommands(ctx, service, config) {
|
|
|
29
29
|
return result;
|
|
30
30
|
});
|
|
31
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
32
|
root
|
|
43
33
|
.subcommand('查图', '识图调试:查看本条消息中被识别的图片信息')
|
|
44
34
|
.action(async ({ session }) => {
|
|
@@ -66,19 +56,6 @@ function registerCommands(ctx, service, config) {
|
|
|
66
56
|
}
|
|
67
57
|
return styleText('识图诊断:\n' + lines.join('\n'), config);
|
|
68
58
|
});
|
|
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
59
|
root
|
|
83
60
|
.subcommand('kick <target:string>', '踢出群成员')
|
|
84
61
|
.option('reject', '-r 拒绝再次加群请求')
|
package/lib/index.d.ts
CHANGED
|
@@ -66,6 +66,7 @@ export interface Config {
|
|
|
66
66
|
enableMemory: boolean;
|
|
67
67
|
memoryFileName: string;
|
|
68
68
|
memoryInAi: boolean;
|
|
69
|
+
memoryContent: string;
|
|
69
70
|
groupRules: GroupRule[];
|
|
70
71
|
allowedUserIds: string[];
|
|
71
72
|
allowGroupOwner: boolean;
|
|
@@ -94,6 +95,7 @@ export interface GroupRule {
|
|
|
94
95
|
autoKickEnabled?: boolean;
|
|
95
96
|
autoKickThreshold?: number;
|
|
96
97
|
autoViolationWindowMinutes?: number;
|
|
98
|
+
enableJoinRequestReview?: boolean;
|
|
97
99
|
}
|
|
98
100
|
export declare const Config: Schema<Config>;
|
|
99
101
|
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Config = exports.inject = exports.name = void 0;
|
|
4
4
|
exports.apply = apply;
|
|
@@ -42,6 +42,7 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
42
42
|
autoKickEnabled: koishi_1.Schema.boolean().description('该群是否启用自动踢人(留空则跟随全局设置)。'),
|
|
43
43
|
autoKickThreshold: koishi_1.Schema.natural().description('该群触发自动踢人的违规次数(留空则跟随全局设置)。'),
|
|
44
44
|
autoViolationWindowMinutes: koishi_1.Schema.natural().description('该群违规计数统计窗口(分钟,留空则跟随全局设置)。'),
|
|
45
|
+
enableJoinRequestReview: koishi_1.Schema.boolean().description('该群是否启用入群审核(留空则跟随全局设置)。'),
|
|
45
46
|
})).role('table').default([]).description('按群覆盖消息管控策略(未命中群号时使用全局默认)。'),
|
|
46
47
|
}).description('消息管控'),
|
|
47
48
|
koishi_1.Schema.object({
|
|
@@ -54,8 +55,9 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
54
55
|
}).description('自动管控'),
|
|
55
56
|
koishi_1.Schema.object({
|
|
56
57
|
enableMemory: koishi_1.Schema.boolean().default(true).description('启用记忆库:群内发送 [记忆]+内容 保存到 Memory.md,[记忆查询]/[记忆删除] 管理,AI 回复可引用。'),
|
|
57
|
-
memoryFileName: koishi_1.Schema.string().default('Memory.md').description('记忆库文件名(保存在 Koishi
|
|
58
|
+
memoryFileName: koishi_1.Schema.string().default('Memory.md').description('记忆库文件名(保存在 Koishi 数据目录)。'),
|
|
58
59
|
memoryInAi: koishi_1.Schema.boolean().default(true).description('AI 回复/识图时是否参考记忆库内容。'),
|
|
60
|
+
memoryContent: koishi_1.Schema.string().role('textarea').default('').description('记忆库内容(可直接在此查看/编辑 Memory.md,保存后会自动写回文件)。'),
|
|
59
61
|
}).description('记忆库'),
|
|
60
62
|
koishi_1.Schema.object({
|
|
61
63
|
enableSelfGag: koishi_1.Schema.boolean().default(true).description('启用口球娱乐:普通成员对自己使用 mute/gag 时随机 1~60 分钟禁言。'),
|
package/lib/service.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { Context, Service, Session } from 'koishi';
|
|
2
2
|
import type { Config } from './index';
|
|
3
3
|
import type { ActionResult, AuthorizationResult, MuteParseResult } from './types';
|
|
4
4
|
declare module 'koishi' {
|
|
@@ -122,6 +122,7 @@ export declare class QQGroupManagerService extends Service {
|
|
|
122
122
|
private searchMemory;
|
|
123
123
|
private decoratePromptWithMemory;
|
|
124
124
|
private handleMemoryCommand;
|
|
125
|
+
private _syncMemoryConfig;
|
|
125
126
|
private isImageSendableUrl;
|
|
126
127
|
describeImageMessage(session?: Session): {
|
|
127
128
|
hasCqImage: boolean;
|
|
@@ -139,6 +140,8 @@ export declare class QQGroupManagerService extends Service {
|
|
|
139
140
|
private toGroupId;
|
|
140
141
|
private getOneBotApi;
|
|
141
142
|
private extractTextForFilter;
|
|
143
|
+
private hasTextContent;
|
|
144
|
+
private hasImageOrFace;
|
|
142
145
|
private hasCardMessage;
|
|
143
146
|
private hasForwardMessage;
|
|
144
147
|
private normalizeGuildId;
|
|
@@ -154,6 +157,9 @@ export declare class QQGroupManagerService extends Service {
|
|
|
154
157
|
private generateJoinRequestCode;
|
|
155
158
|
private parseJoinRequestReviewText;
|
|
156
159
|
private handleJoinRequestReviewMessage;
|
|
160
|
+
private resolveJoinReviewEnabled;
|
|
161
|
+
private fetchStrangerInfo;
|
|
162
|
+
private buildJoinRequestPrompt;
|
|
157
163
|
private handleGuildMemberRequest;
|
|
158
164
|
reviewJoinRequestDecision(session: Session, code: string, approve: boolean, reason?: string): Promise<ActionResult>;
|
|
159
165
|
formatTargetDisplay(session: Session | undefined, targetId?: string, rawTarget?: string): Promise<string>;
|
package/lib/service.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
@@ -36,6 +36,26 @@ class QQGroupManagerService extends koishi_1.Service {
|
|
|
36
36
|
this._logger = ctx.logger('nestim-qq-group-manager');
|
|
37
37
|
ctx.on('message', (session) => this.handleGroupModeration(session));
|
|
38
38
|
ctx.on('guild-member-request', (session) => this.handleGuildMemberRequest(session));
|
|
39
|
+
// 让配置里的 memoryContent 编辑框与 Memory.md 双向同步
|
|
40
|
+
this._syncMemoryConfig().catch((error) => this._logger.warn(`[memory] sync failed: ${String(error)}`));
|
|
41
|
+
}
|
|
42
|
+
async _syncMemoryConfig() {
|
|
43
|
+
try {
|
|
44
|
+
const fileContent = await this.readMemoryFile();
|
|
45
|
+
const cfgContent = typeof this.config.memoryContent === 'string' ? this.config.memoryContent : '';
|
|
46
|
+
if (cfgContent && cfgContent !== fileContent) {
|
|
47
|
+
// 用户在配置编辑框里改过内容 → 写回文件
|
|
48
|
+
await this.writeMemoryFile(cfgContent);
|
|
49
|
+
this._logger.info('[memory] 配置内容已写回 Memory.md');
|
|
50
|
+
}
|
|
51
|
+
else if (!cfgContent) {
|
|
52
|
+
// 首次:把文件内容填充到编辑框,便于查看/编辑
|
|
53
|
+
this.config.memoryContent = fileContent;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
this._logger.warn(`[memory] sync failed: ${String(error)}`);
|
|
58
|
+
}
|
|
39
59
|
}
|
|
40
60
|
isPlatformAllowed(platform) {
|
|
41
61
|
return !!platform && this.config.platformFilter.includes(platform);
|
|
@@ -1012,9 +1032,16 @@ class QQGroupManagerService extends koishi_1.Service {
|
|
|
1012
1032
|
const cqList = content.match(cqPattern) ?? [];
|
|
1013
1033
|
for (const chunk of cqList) {
|
|
1014
1034
|
const match = chunk.match(/(?:url|src)=([^,\]]+)/i);
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1035
|
+
let value = match ? decodeURIComponent(match[1].trim()) : '';
|
|
1036
|
+
if (!this.isImageSendableUrl(value)) {
|
|
1037
|
+
// 部分适配器把 url 放在 file 字段
|
|
1038
|
+
const fm = chunk.match(/file=([^,\]]+)/i);
|
|
1039
|
+
if (fm) {
|
|
1040
|
+
const f = decodeURIComponent(fm[1].trim());
|
|
1041
|
+
if (this.isImageSendableUrl(f))
|
|
1042
|
+
value = f;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1018
1045
|
if (!this.isImageSendableUrl(value))
|
|
1019
1046
|
continue;
|
|
1020
1047
|
urls.push(value);
|
|
@@ -1738,7 +1765,7 @@ class QQGroupManagerService extends koishi_1.Service {
|
|
|
1738
1765
|
thresholdMode,
|
|
1739
1766
|
focusText: (directMention || followupForce || relatedContextFollowup) ? (text || '@bot') : undefined,
|
|
1740
1767
|
focusUserId: (directMention || followupForce || relatedContextFollowup) ? session.userId : undefined,
|
|
1741
|
-
focusImages:
|
|
1768
|
+
focusImages: messageImages,
|
|
1742
1769
|
focusUserName: (directMention || followupForce || relatedContextFollowup) ? this.getDisplayName(session) : undefined,
|
|
1743
1770
|
topicTokens: thresholdMode ? topicTokens : undefined,
|
|
1744
1771
|
selectedRecord,
|
|
@@ -2598,6 +2625,23 @@ class QQGroupManagerService extends koishi_1.Service {
|
|
|
2598
2625
|
const raw = (session.content ?? '').replace(/\[CQ:[^\]]+\]/g, ' ');
|
|
2599
2626
|
return raw.trim();
|
|
2600
2627
|
}
|
|
2628
|
+
hasTextContent(session) {
|
|
2629
|
+
const text = this.extractTextForFilter(session);
|
|
2630
|
+
if (text)
|
|
2631
|
+
return true;
|
|
2632
|
+
// 有的适配器把图片/表情放在 elements 里,content 可能没文字;检查是否有文字元素
|
|
2633
|
+
for (const el of session.elements ?? []) {
|
|
2634
|
+
if (el.type === 'text' && String(el.text ?? el.content ?? '').trim())
|
|
2635
|
+
return true;
|
|
2636
|
+
}
|
|
2637
|
+
return false;
|
|
2638
|
+
}
|
|
2639
|
+
hasImageOrFace(session) {
|
|
2640
|
+
const content = session.content ?? '';
|
|
2641
|
+
if (/\[CQ:(image|face|record|video|flash),/i.test(content))
|
|
2642
|
+
return true;
|
|
2643
|
+
return (session.elements ?? []).some((el) => el.type === 'image' || el.type === 'face' || el.type === 'record' || el.type === 'video' || el.type === 'flash');
|
|
2644
|
+
}
|
|
2601
2645
|
hasCardMessage(session) {
|
|
2602
2646
|
const content = session.content ?? '';
|
|
2603
2647
|
if (/\[CQ:(json|xml),/i.test(content))
|
|
@@ -2651,6 +2695,10 @@ class QQGroupManagerService extends koishi_1.Service {
|
|
|
2651
2695
|
};
|
|
2652
2696
|
}
|
|
2653
2697
|
detectViolation(session, policy) {
|
|
2698
|
+
// 防误判:纯图片/表情/贴纸(无实际文字内容)的消息不作为违规处理,避免表情包被当成违禁词
|
|
2699
|
+
if (this.hasImageOrFace(session) && !this.hasTextContent(session)) {
|
|
2700
|
+
return null;
|
|
2701
|
+
}
|
|
2654
2702
|
if (policy.blockCardMessage && this.hasCardMessage(session)) {
|
|
2655
2703
|
return { type: 'card', detail: '检测到卡片消息(json/xml)' };
|
|
2656
2704
|
}
|
|
@@ -2855,7 +2903,7 @@ class QQGroupManagerService extends koishi_1.Service {
|
|
|
2855
2903
|
return null;
|
|
2856
2904
|
}
|
|
2857
2905
|
async handleJoinRequestReviewMessage(session) {
|
|
2858
|
-
if (!this.
|
|
2906
|
+
if (!this.resolveJoinReviewEnabled(session))
|
|
2859
2907
|
return false;
|
|
2860
2908
|
if (session.platform !== 'onebot' || !session.guildId)
|
|
2861
2909
|
return false;
|
|
@@ -2870,8 +2918,53 @@ class QQGroupManagerService extends koishi_1.Service {
|
|
|
2870
2918
|
await session.send(this.styleText(result.message));
|
|
2871
2919
|
return true;
|
|
2872
2920
|
}
|
|
2921
|
+
resolveJoinReviewEnabled(session) {
|
|
2922
|
+
const guildId = this.normalizeGuildId(session?.guildId);
|
|
2923
|
+
const rule = this.config.groupRules?.find((item) => this.normalizeGuildId(item.guildId) === guildId);
|
|
2924
|
+
if (typeof rule?.enableJoinRequestReview === 'boolean')
|
|
2925
|
+
return rule.enableJoinRequestReview;
|
|
2926
|
+
return !!this.config.enableJoinRequestReview;
|
|
2927
|
+
}
|
|
2928
|
+
async fetchStrangerInfo(bot, userId) {
|
|
2929
|
+
if (!bot || !userId)
|
|
2930
|
+
return null;
|
|
2931
|
+
// 兼容不同 OneBot 适配器的方法命名
|
|
2932
|
+
try {
|
|
2933
|
+
if (typeof bot.getStrangerInfo === 'function')
|
|
2934
|
+
return await bot.getStrangerInfo(userId, true);
|
|
2935
|
+
}
|
|
2936
|
+
catch { /* ignore */ }
|
|
2937
|
+
try {
|
|
2938
|
+
if (typeof bot.get_stranger_info === 'function')
|
|
2939
|
+
return await bot.get_stranger_info(userId, true);
|
|
2940
|
+
}
|
|
2941
|
+
catch { /* ignore */ }
|
|
2942
|
+
return null;
|
|
2943
|
+
}
|
|
2944
|
+
buildJoinRequestPrompt(code, applicant, extra) {
|
|
2945
|
+
const lines = [
|
|
2946
|
+
'检测到新的入群申请,请管理员审核:',
|
|
2947
|
+
`审核编号:${code}`,
|
|
2948
|
+
`QQ号:${applicant.userId}`,
|
|
2949
|
+
];
|
|
2950
|
+
if (extra?.nickname)
|
|
2951
|
+
lines.push(`昵称:${extra.nickname}`);
|
|
2952
|
+
if (extra?.sex)
|
|
2953
|
+
lines.push(`性别:${extra.sex}`);
|
|
2954
|
+
if (extra?.age !== undefined && extra.age !== '')
|
|
2955
|
+
lines.push(`年龄:${extra.age}`);
|
|
2956
|
+
if (extra?.level)
|
|
2957
|
+
lines.push(`QQ等级:${extra.level}`);
|
|
2958
|
+
if (extra?.avatarUrl)
|
|
2959
|
+
lines.push(`头像:${extra.avatarUrl}`);
|
|
2960
|
+
lines.push(`验证信息:${applicant.comment || '(无)'}`);
|
|
2961
|
+
lines.push(`同意:${this.config.command} 审核 ${code} 同意 [理由]`);
|
|
2962
|
+
lines.push(`拒绝:${this.config.command} 审核 ${code} 拒绝 [理由]`);
|
|
2963
|
+
lines.push(`快捷回复:同意入群 ${code} / 拒绝入群 ${code}`);
|
|
2964
|
+
return lines.join('\n');
|
|
2965
|
+
}
|
|
2873
2966
|
async handleGuildMemberRequest(session) {
|
|
2874
|
-
if (!this.
|
|
2967
|
+
if (!this.resolveJoinReviewEnabled(session))
|
|
2875
2968
|
return;
|
|
2876
2969
|
if (!this.isPlatformAllowed(session.platform))
|
|
2877
2970
|
return;
|
|
@@ -2893,15 +2986,29 @@ class QQGroupManagerService extends koishi_1.Service {
|
|
|
2893
2986
|
};
|
|
2894
2987
|
this.pendingJoinRequests.set(code, item);
|
|
2895
2988
|
this.pendingJoinRequestFlags.set(item.flag, code);
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2989
|
+
// 尽力获取申请者信息(昵称/性别/年龄/等级/头像)
|
|
2990
|
+
let extra = {};
|
|
2991
|
+
const bot = this.getOneBotApi(session);
|
|
2992
|
+
const info = await this.fetchStrangerInfo(bot, session.userId);
|
|
2993
|
+
if (info) {
|
|
2994
|
+
const nickname = info.nickname || info.card || info.name || '';
|
|
2995
|
+
let sex = info.sex || info.gender || '';
|
|
2996
|
+
if (sex === 'male')
|
|
2997
|
+
sex = '男';
|
|
2998
|
+
else if (sex === 'female')
|
|
2999
|
+
sex = '女';
|
|
3000
|
+
else if (sex === 'unknown' || sex === '')
|
|
3001
|
+
sex = '未知';
|
|
3002
|
+
const level = (info.level !== undefined && info.level !== null) ? String(info.level) : '';
|
|
3003
|
+
extra = {
|
|
3004
|
+
nickname,
|
|
3005
|
+
sex: sex || '',
|
|
3006
|
+
age: info.age,
|
|
3007
|
+
level,
|
|
3008
|
+
avatarUrl: `https://q1.qlogo.cn/g?b=qq&nk=${encodeURIComponent(session.userId)}&s=640`,
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
const prompt = this.buildJoinRequestPrompt(code, item, extra);
|
|
2905
3012
|
try {
|
|
2906
3013
|
await session.send(this.styleText(prompt));
|
|
2907
3014
|
this.logCommandResult('join-request', { ok: true, message: 'join request notice sent' }, session, { code, target: item.userId });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestim/koishi-plugin-qq-group-manager",
|
|
3
3
|
"description": "Koishi QQ群管理插件(OneBot/LLOneBot):群管命令、权限校验、图片菜单、状态卡片、复读、AI群聊回复、记忆库、识图与自动管控(禁言/踢人)。",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.4",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|