koishi-plugin-temporaryban 1.0.6 → 1.0.7-beta1
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/index.js +1 -1
- package/lib/commands/info.js +1 -1
- package/lib/commands/whitelist.d.ts +2 -1
- package/lib/commands/whitelist.js +75 -3
- package/lib/config.d.ts +14 -6
- package/lib/config.js +141 -71
- package/lib/index.d.ts +16 -0
- package/lib/index.js +51 -20
- package/lib/locales/en-US.d.ts +14 -0
- package/lib/locales/en-US.js +15 -1
- package/lib/locales/zh-CN.d.ts +16 -0
- package/lib/locales/zh-CN.js +17 -1
- package/lib/services/detector.d.ts +5 -0
- package/lib/services/detector.js +71 -8
- package/lib/services/mailer.d.ts +1 -4
- package/lib/services/mailer.js +49 -60
- package/lib/services/whitelist.d.ts +2 -1
- package/lib/services/whitelist.js +31 -11
- package/package.json +3 -2
package/lib/commands/index.js
CHANGED
|
@@ -11,7 +11,7 @@ const info_1 = require("./info");
|
|
|
11
11
|
function registerCommands(ctx, config, detector, mailer, userRecords, history, whitelistService) {
|
|
12
12
|
(0, admin_1.registerAdminCommands)(ctx, config, mailer);
|
|
13
13
|
(0, dictionary_1.registerDictionaryCommands)(ctx, config, detector);
|
|
14
|
-
(0, whitelist_1.registerWhitelistCommands)(ctx, config, whitelistService);
|
|
14
|
+
(0, whitelist_1.registerWhitelistCommands)(ctx, config, whitelistService, detector);
|
|
15
15
|
(0, stats_1.registerStatsCommands)(ctx, config, userRecords);
|
|
16
16
|
(0, check_1.registerCheckCommands)(ctx, config, detector);
|
|
17
17
|
(0, history_1.registerHistoryCommands)(ctx, config, history);
|
package/lib/commands/info.js
CHANGED
|
@@ -16,7 +16,7 @@ function registerInfoCommands(ctx, config, whitelistService) {
|
|
|
16
16
|
const groupConfig = config.groups.find(g => g.groupId === session.guildId);
|
|
17
17
|
if (!groupConfig)
|
|
18
18
|
return session.text('commands.temporaryban.messages.group_not_configured');
|
|
19
|
-
const whitelistCount = whitelistService.
|
|
19
|
+
const whitelistCount = whitelistService.getWhitelist(session.guildId).length;
|
|
20
20
|
const methods = groupConfig.detectionMethods.join(', ') || 'None';
|
|
21
21
|
const smartVer = groupConfig.smartVerification ? 'ON' : 'OFF';
|
|
22
22
|
return session.text('commands.temporaryban.messages.group_info', [
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Context } from 'koishi';
|
|
2
2
|
import { Config } from '../config';
|
|
3
3
|
import { WhitelistService } from '../services/whitelist';
|
|
4
|
-
|
|
4
|
+
import { DetectorService } from '../services/detector';
|
|
5
|
+
export declare function registerWhitelistCommands(ctx: Context, config: Config, whitelistService: WhitelistService, detector: DetectorService): void;
|
|
@@ -2,9 +2,26 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerWhitelistCommands = registerWhitelistCommands;
|
|
4
4
|
const permission_1 = require("../utils/permission");
|
|
5
|
-
function registerWhitelistCommands(ctx, config, whitelistService) {
|
|
5
|
+
function registerWhitelistCommands(ctx, config, whitelistService, detector) {
|
|
6
6
|
const cmd = ctx.command('temporaryban');
|
|
7
|
-
//
|
|
7
|
+
// 10. Whitelist User List
|
|
8
|
+
cmd.subcommand('.whitelist.list')
|
|
9
|
+
.action(async ({ session }) => {
|
|
10
|
+
if (!session)
|
|
11
|
+
return;
|
|
12
|
+
if (!(0, permission_1.checkPermission)(session, config))
|
|
13
|
+
return session.text('commands.temporaryban.messages.permission_denied');
|
|
14
|
+
if (!session.guildId)
|
|
15
|
+
return session.text('commands.temporaryban.messages.group_only');
|
|
16
|
+
const groupConfig = config.groups.find(g => g.groupId === session.guildId);
|
|
17
|
+
if (!groupConfig)
|
|
18
|
+
return session.text('commands.temporaryban.messages.group_not_configured');
|
|
19
|
+
const items = whitelistService.getWhitelist(session.guildId);
|
|
20
|
+
if (items.length === 0)
|
|
21
|
+
return session.text('commands.temporaryban.messages.no_whitelist_users');
|
|
22
|
+
return session.text('commands.temporaryban.messages.whitelist_users_list', [items.length, items.join(', ')]);
|
|
23
|
+
});
|
|
24
|
+
// 5. Whitelist Add User
|
|
8
25
|
cmd.subcommand('.whitelist.add <user:string>')
|
|
9
26
|
.action(async ({ session }, user) => {
|
|
10
27
|
if (!session)
|
|
@@ -23,7 +40,7 @@ function registerWhitelistCommands(ctx, config, whitelistService) {
|
|
|
23
40
|
return session.text('commands.temporaryban.messages.already_whitelisted');
|
|
24
41
|
return session.text('commands.temporaryban.messages.user_added_whitelist', [user]);
|
|
25
42
|
});
|
|
26
|
-
// 6. Whitelist Remove
|
|
43
|
+
// 6. Whitelist Remove User
|
|
27
44
|
cmd.subcommand('.whitelist.remove <user:string>')
|
|
28
45
|
.action(async ({ session }, user) => {
|
|
29
46
|
if (!session)
|
|
@@ -40,4 +57,59 @@ function registerWhitelistCommands(ctx, config, whitelistService) {
|
|
|
40
57
|
return session.text('commands.temporaryban.messages.not_in_whitelist');
|
|
41
58
|
return session.text('commands.temporaryban.messages.user_removed_whitelist', [user]);
|
|
42
59
|
});
|
|
60
|
+
// 7. Whitelist Word Add
|
|
61
|
+
cmd.subcommand('.whitelist.word.add <word:string>')
|
|
62
|
+
.action(async ({ session }, word) => {
|
|
63
|
+
if (!session)
|
|
64
|
+
return;
|
|
65
|
+
if (!(0, permission_1.checkPermission)(session, config))
|
|
66
|
+
return session.text('commands.temporaryban.messages.permission_denied');
|
|
67
|
+
if (!session.guildId)
|
|
68
|
+
return session.text('commands.temporaryban.messages.group_only');
|
|
69
|
+
if (!word)
|
|
70
|
+
return session.text('commands.temporaryban.messages.specify_word');
|
|
71
|
+
const groupConfig = config.groups.find(g => g.groupId === session.guildId);
|
|
72
|
+
if (!groupConfig)
|
|
73
|
+
return session.text('commands.temporaryban.messages.group_not_configured');
|
|
74
|
+
const success = await detector.addIgnoredWord(session.guildId, word);
|
|
75
|
+
if (!success)
|
|
76
|
+
return session.text('commands.temporaryban.messages.word_exists');
|
|
77
|
+
return session.text('commands.temporaryban.messages.ignored_word_added', [word]);
|
|
78
|
+
});
|
|
79
|
+
// 8. Whitelist Word Remove
|
|
80
|
+
cmd.subcommand('.whitelist.word.remove <word:string>')
|
|
81
|
+
.action(async ({ session }, word) => {
|
|
82
|
+
if (!session)
|
|
83
|
+
return;
|
|
84
|
+
if (!(0, permission_1.checkPermission)(session, config))
|
|
85
|
+
return session.text('commands.temporaryban.messages.permission_denied');
|
|
86
|
+
if (!session.guildId)
|
|
87
|
+
return session.text('commands.temporaryban.messages.group_only');
|
|
88
|
+
if (!word)
|
|
89
|
+
return session.text('commands.temporaryban.messages.specify_word');
|
|
90
|
+
const groupConfig = config.groups.find(g => g.groupId === session.guildId);
|
|
91
|
+
if (!groupConfig)
|
|
92
|
+
return session.text('commands.temporaryban.messages.group_not_configured');
|
|
93
|
+
const success = await detector.removeIgnoredWord(session.guildId, word);
|
|
94
|
+
if (!success)
|
|
95
|
+
return session.text('commands.temporaryban.messages.word_not_found');
|
|
96
|
+
return session.text('commands.temporaryban.messages.ignored_word_removed', [word]);
|
|
97
|
+
});
|
|
98
|
+
// 9. Whitelist Word List
|
|
99
|
+
cmd.subcommand('.whitelist.word.list')
|
|
100
|
+
.action(async ({ session }) => {
|
|
101
|
+
if (!session)
|
|
102
|
+
return;
|
|
103
|
+
if (!(0, permission_1.checkPermission)(session, config))
|
|
104
|
+
return session.text('commands.temporaryban.messages.permission_denied');
|
|
105
|
+
if (!session.guildId)
|
|
106
|
+
return session.text('commands.temporaryban.messages.group_only');
|
|
107
|
+
const groupConfig = config.groups.find(g => g.groupId === session.guildId);
|
|
108
|
+
if (!groupConfig)
|
|
109
|
+
return session.text('commands.temporaryban.messages.group_not_configured');
|
|
110
|
+
const items = detector.getIgnoredWords(session.guildId);
|
|
111
|
+
if (items.length === 0)
|
|
112
|
+
return session.text('commands.temporaryban.messages.no_ignored_words');
|
|
113
|
+
return session.text('commands.temporaryban.messages.ignored_words_list', [items.length, items.join(', ')]);
|
|
114
|
+
});
|
|
43
115
|
}
|
package/lib/config.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ export interface GroupConfig {
|
|
|
36
36
|
smartVerification: boolean;
|
|
37
37
|
contextMsgCount: number;
|
|
38
38
|
aiThreshold?: number;
|
|
39
|
+
showCensoredWord?: boolean;
|
|
39
40
|
localBadWordDict: string;
|
|
40
41
|
whitelist: WhitelistItem[];
|
|
41
42
|
triggerThreshold?: number;
|
|
@@ -66,17 +67,24 @@ export interface OpenAIConfig {
|
|
|
66
67
|
export interface Config {
|
|
67
68
|
debug: boolean;
|
|
68
69
|
adminList: string[];
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
useApi: boolean;
|
|
71
|
+
useBaidu: boolean;
|
|
72
|
+
useAliyun: boolean;
|
|
73
|
+
useTencent: boolean;
|
|
74
|
+
useOpenAI: boolean;
|
|
75
|
+
useEmail: boolean;
|
|
76
|
+
smtp?: SmtpConfig;
|
|
77
|
+
api?: ApiConfig;
|
|
78
|
+
baidu?: BaiduConfig;
|
|
79
|
+
aliyun?: AliyunConfig;
|
|
80
|
+
tencent?: TencentConfig;
|
|
81
|
+
openai?: OpenAIConfig;
|
|
75
82
|
defaultMuteMinutes: number;
|
|
76
83
|
defaultTriggerThreshold: number;
|
|
77
84
|
defaultAiThreshold: number;
|
|
78
85
|
defaultCheckProbability: number;
|
|
79
86
|
checkAdmin: boolean;
|
|
87
|
+
defaultShowCensoredWord: boolean;
|
|
80
88
|
groups: GroupConfig[];
|
|
81
89
|
}
|
|
82
90
|
export declare const Config: Schema<Config>;
|
package/lib/config.js
CHANGED
|
@@ -2,74 +2,144 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Config = void 0;
|
|
4
4
|
const koishi_1 = require("koishi");
|
|
5
|
-
exports.Config = koishi_1.Schema.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
koishi_1.Schema.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
5
|
+
exports.Config = koishi_1.Schema.intersect([
|
|
6
|
+
koishi_1.Schema.object({
|
|
7
|
+
debug: koishi_1.Schema.boolean().description('开启全局调试日志。').default(false),
|
|
8
|
+
adminList: koishi_1.Schema.array(String).description('全局管理员列表 (OneBot 用户ID)。').role('table'),
|
|
9
|
+
checkAdmin: koishi_1.Schema.boolean().description('是否检查机器人在群内的管理权限。').default(true),
|
|
10
|
+
defaultMuteMinutes: koishi_1.Schema.number().description('全局默认禁言时长 (分钟)。').default(10).min(0.1),
|
|
11
|
+
defaultTriggerThreshold: koishi_1.Schema.number().description('全局默认触发阈值 (次数)。').default(3).min(1),
|
|
12
|
+
defaultAiThreshold: koishi_1.Schema.number().description('全局默认 AI 判定阈值 (0.0 - 1.0)。').default(0.6).min(0).max(1).step(0.1),
|
|
13
|
+
defaultCheckProbability: koishi_1.Schema.number().description('全局默认检查概率 (0.0 - 1.0)。').default(1.0).min(0).max(1).step(0.1),
|
|
14
|
+
defaultShowCensoredWord: koishi_1.Schema.boolean().description('全局默认是否在警告中显示触发的违禁词。').default(true),
|
|
15
|
+
}).description('基础设置'),
|
|
16
|
+
koishi_1.Schema.intersect([
|
|
17
|
+
koishi_1.Schema.object({
|
|
18
|
+
useEmail: koishi_1.Schema.boolean().description('启用邮件通知系统').default(false),
|
|
19
|
+
}).description('邮件通知开关'),
|
|
20
|
+
koishi_1.Schema.union([
|
|
21
|
+
koishi_1.Schema.object({
|
|
22
|
+
useEmail: koishi_1.Schema.const(true).required(),
|
|
23
|
+
smtp: koishi_1.Schema.object({
|
|
24
|
+
host: koishi_1.Schema.string().description('SMTP 服务器地址').default('smtp.example.com'),
|
|
25
|
+
port: koishi_1.Schema.number().description('SMTP 端口').default(465),
|
|
26
|
+
secure: koishi_1.Schema.boolean().description('启用 SSL/TLS 加密链接').default(true),
|
|
27
|
+
user: koishi_1.Schema.string().description('SMTP 用户名').default('user@example.com'),
|
|
28
|
+
pass: koishi_1.Schema.string().role('secret').description('SMTP 密码或授权码').default('password'),
|
|
29
|
+
senderName: koishi_1.Schema.string().description('邮件发件人显示名称').default('Koishi Bot'),
|
|
30
|
+
senderEmail: koishi_1.Schema.string().description('邮件发件人地址').default('bot@example.com'),
|
|
31
|
+
receivers: koishi_1.Schema.array(String).description('接收违规通知的管理员邮箱列表').role('table'),
|
|
32
|
+
summaryIntervalDays: koishi_1.Schema.number().description('邮件汇总周期 (天)').default(1).min(0),
|
|
33
|
+
}).description('邮件通知设置 (SMTP)'),
|
|
34
|
+
}),
|
|
35
|
+
koishi_1.Schema.object({}),
|
|
36
|
+
]),
|
|
37
|
+
]),
|
|
38
|
+
koishi_1.Schema.intersect([
|
|
39
|
+
koishi_1.Schema.object({
|
|
40
|
+
useApi: koishi_1.Schema.boolean().description('启用在线 API 检测 (ApiHz)').default(false),
|
|
41
|
+
}).description('在线 API 开关'),
|
|
42
|
+
koishi_1.Schema.union([
|
|
43
|
+
koishi_1.Schema.object({
|
|
44
|
+
useApi: koishi_1.Schema.const(true).required(),
|
|
45
|
+
api: koishi_1.Schema.object({
|
|
46
|
+
apiUrl: koishi_1.Schema.string().description('在线检测 API 地址').default('https://cn.apihz.cn/api/zici/mgc.php'),
|
|
47
|
+
apiId: koishi_1.Schema.string().description('ApiHz 开发者 ID').default(''),
|
|
48
|
+
apiKey: koishi_1.Schema.string().role('secret').description('ApiHz 开发者 Key').default(''),
|
|
49
|
+
}).description('在线检测设置 (ApiHz)'),
|
|
50
|
+
}),
|
|
51
|
+
koishi_1.Schema.object({}),
|
|
52
|
+
]),
|
|
53
|
+
]),
|
|
54
|
+
koishi_1.Schema.intersect([
|
|
55
|
+
koishi_1.Schema.object({
|
|
56
|
+
useOpenAI: koishi_1.Schema.boolean().description('启用 AI 模型检测 (OpenAI/SiliconFlow)').default(false),
|
|
57
|
+
}).description('AI 检测开关'),
|
|
58
|
+
koishi_1.Schema.union([
|
|
59
|
+
koishi_1.Schema.object({
|
|
60
|
+
useOpenAI: koishi_1.Schema.const(true).required(),
|
|
61
|
+
openai: koishi_1.Schema.object({
|
|
62
|
+
apiKey: koishi_1.Schema.string().role('secret').description('API Key').default(''),
|
|
63
|
+
baseUrl: koishi_1.Schema.string().description('API Base URL').default('https://api.siliconflow.cn/v1'),
|
|
64
|
+
model: koishi_1.Schema.string().description('模型 ID').default('deepseek-ai/DeepSeek-V2.5'),
|
|
65
|
+
}).description('AI 检测设置'),
|
|
66
|
+
}),
|
|
67
|
+
koishi_1.Schema.object({}),
|
|
68
|
+
]),
|
|
69
|
+
]),
|
|
70
|
+
koishi_1.Schema.intersect([
|
|
71
|
+
koishi_1.Schema.object({
|
|
72
|
+
useBaidu: koishi_1.Schema.boolean().description('启用百度智能云检测').default(false),
|
|
73
|
+
}).description('百度智能云开关'),
|
|
74
|
+
koishi_1.Schema.union([
|
|
75
|
+
koishi_1.Schema.object({
|
|
76
|
+
useBaidu: koishi_1.Schema.const(true).required(),
|
|
77
|
+
baidu: koishi_1.Schema.object({
|
|
78
|
+
apiKey: koishi_1.Schema.string().description('API Key').default(''),
|
|
79
|
+
secretKey: koishi_1.Schema.string().role('secret').description('Secret Key').default(''),
|
|
80
|
+
}).description('百度智能云设置'),
|
|
81
|
+
}),
|
|
82
|
+
koishi_1.Schema.object({}),
|
|
83
|
+
]),
|
|
84
|
+
]),
|
|
85
|
+
koishi_1.Schema.intersect([
|
|
86
|
+
koishi_1.Schema.object({
|
|
87
|
+
useAliyun: koishi_1.Schema.boolean().description('启用阿里云内容安全检测').default(false),
|
|
88
|
+
}).description('阿里云开关'),
|
|
89
|
+
koishi_1.Schema.union([
|
|
90
|
+
koishi_1.Schema.object({
|
|
91
|
+
useAliyun: koishi_1.Schema.const(true).required(),
|
|
92
|
+
aliyun: koishi_1.Schema.object({
|
|
93
|
+
accessKeyId: koishi_1.Schema.string().description('AccessKey ID').default(''),
|
|
94
|
+
accessKeySecret: koishi_1.Schema.string().role('secret').description('AccessKey Secret').default(''),
|
|
95
|
+
endpoint: koishi_1.Schema.string().description('Endpoint').default('green-cip.cn-shanghai.aliyuncs.com'),
|
|
96
|
+
}).description('阿里云设置'),
|
|
97
|
+
}),
|
|
98
|
+
koishi_1.Schema.object({}),
|
|
99
|
+
]),
|
|
100
|
+
]),
|
|
101
|
+
koishi_1.Schema.intersect([
|
|
102
|
+
koishi_1.Schema.object({
|
|
103
|
+
useTencent: koishi_1.Schema.boolean().description('启用腾讯云检测').default(false),
|
|
104
|
+
}).description('腾讯云开关'),
|
|
105
|
+
koishi_1.Schema.union([
|
|
106
|
+
koishi_1.Schema.object({
|
|
107
|
+
useTencent: koishi_1.Schema.const(true).required(),
|
|
108
|
+
tencent: koishi_1.Schema.object({
|
|
109
|
+
secretId: koishi_1.Schema.string().description('SecretId').default(''),
|
|
110
|
+
secretKey: koishi_1.Schema.string().role('secret').description('SecretKey').default(''),
|
|
111
|
+
region: koishi_1.Schema.string().description('Region').default('ap-shanghai'),
|
|
112
|
+
}).description('腾讯云设置'),
|
|
113
|
+
}),
|
|
114
|
+
koishi_1.Schema.object({}),
|
|
115
|
+
]),
|
|
116
|
+
]),
|
|
117
|
+
koishi_1.Schema.object({
|
|
118
|
+
groups: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
119
|
+
id: koishi_1.Schema.string().hidden().default(''),
|
|
120
|
+
groupId: koishi_1.Schema.string().description('群组 ID (群号)').required(),
|
|
121
|
+
enable: koishi_1.Schema.boolean().description('是否启用监控').default(true),
|
|
122
|
+
detectionMethods: koishi_1.Schema.array(koishi_1.Schema.union([
|
|
123
|
+
koishi_1.Schema.const('local').description('本地词库 (数据库)'),
|
|
124
|
+
koishi_1.Schema.const('ai').description('AI 模型检测'),
|
|
125
|
+
koishi_1.Schema.const('api').description('在线 API'),
|
|
126
|
+
koishi_1.Schema.const('baidu').description('百度智能云'),
|
|
127
|
+
koishi_1.Schema.const('aliyun').description('阿里云'),
|
|
128
|
+
koishi_1.Schema.const('tencent').description('腾讯云'),
|
|
129
|
+
])).role('checkbox').description('启用的检测方式').default(['local']),
|
|
130
|
+
smartVerification: koishi_1.Schema.boolean().description('开启智能验证').default(false),
|
|
131
|
+
contextMsgCount: koishi_1.Schema.number().description('智能验证上下文数量').default(3).min(1).max(10),
|
|
132
|
+
aiThreshold: koishi_1.Schema.number().description('AI 判定阈值 (留空用默认)').min(0).max(1).step(0.1),
|
|
133
|
+
checkProbability: koishi_1.Schema.number().description('检查概率 (留空用默认)').min(0).max(1).step(0.1),
|
|
134
|
+
showCensoredWord: koishi_1.Schema.boolean().description('是否显示触发的违禁词 (留空用默认)'),
|
|
135
|
+
localBadWordDict: koishi_1.Schema.string().description('【Legacy】本地违禁词库 (初始导入)').default(''),
|
|
136
|
+
whitelist: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
137
|
+
userId: koishi_1.Schema.string().description('用户 ID')
|
|
138
|
+
})).description('白名单用户列表 (管理员自动豁免)').role('table'),
|
|
139
|
+
triggerThreshold: koishi_1.Schema.number().description('触发禁言阈值 (留空用默认)').min(1),
|
|
140
|
+
triggerWindowMinutes: koishi_1.Schema.number().description('违规计数窗口 (分钟)').default(5).min(0.1),
|
|
141
|
+
muteMinutes: koishi_1.Schema.number().description('禁言时长 (分钟, 留空用默认)').min(0.1),
|
|
142
|
+
detailedLog: koishi_1.Schema.boolean().description('开启详细日志').default(false),
|
|
143
|
+
}).description('群组配置')).description('监控群组列表').role('list').default([])
|
|
144
|
+
}).description('群组监控设置')
|
|
145
|
+
]);
|
package/lib/index.d.ts
CHANGED
|
@@ -4,11 +4,27 @@ import { WhitelistTable } from './services/whitelist';
|
|
|
4
4
|
export * from './config';
|
|
5
5
|
export declare const name = "koishi-plugin-temporaryban";
|
|
6
6
|
export declare const inject: string[];
|
|
7
|
+
export interface IgnoredWordTable {
|
|
8
|
+
id: number;
|
|
9
|
+
groupId: string;
|
|
10
|
+
word: string;
|
|
11
|
+
createdAt: Date;
|
|
12
|
+
}
|
|
13
|
+
export interface ViolationTable {
|
|
14
|
+
id: number;
|
|
15
|
+
userId: string;
|
|
16
|
+
groupId: string;
|
|
17
|
+
words: string[];
|
|
18
|
+
content: string;
|
|
19
|
+
timestamp: Date;
|
|
20
|
+
}
|
|
7
21
|
declare module 'koishi' {
|
|
8
22
|
interface Tables {
|
|
9
23
|
temporaryban_badwords: BadWordTable;
|
|
10
24
|
temporaryban_message_history: MessageHistoryTable;
|
|
11
25
|
temporaryban_whitelist: WhitelistTable;
|
|
26
|
+
temporaryban_ignored_words: IgnoredWordTable;
|
|
27
|
+
temporaryban_violations: ViolationTable;
|
|
12
28
|
}
|
|
13
29
|
}
|
|
14
30
|
export interface MessageHistoryTable {
|
package/lib/index.js
CHANGED
|
@@ -29,7 +29,7 @@ const en_US_1 = __importDefault(require("./locales/en-US"));
|
|
|
29
29
|
const commands_1 = require("./commands");
|
|
30
30
|
__exportStar(require("./config"), exports);
|
|
31
31
|
exports.name = 'koishi-plugin-temporaryban';
|
|
32
|
-
exports.inject = ['database'];
|
|
32
|
+
exports.inject = ['database', 'http'];
|
|
33
33
|
const logger = new koishi_1.Logger('temporaryban');
|
|
34
34
|
function apply(ctx, config) {
|
|
35
35
|
// --- Services ---
|
|
@@ -68,6 +68,24 @@ function apply(ctx, config) {
|
|
|
68
68
|
}, {
|
|
69
69
|
autoInc: true,
|
|
70
70
|
});
|
|
71
|
+
ctx.model.extend('temporaryban_ignored_words', {
|
|
72
|
+
id: 'unsigned',
|
|
73
|
+
groupId: 'string',
|
|
74
|
+
word: 'string',
|
|
75
|
+
createdAt: 'timestamp',
|
|
76
|
+
}, {
|
|
77
|
+
autoInc: true,
|
|
78
|
+
});
|
|
79
|
+
ctx.model.extend('temporaryban_violations', {
|
|
80
|
+
id: 'unsigned',
|
|
81
|
+
userId: 'string',
|
|
82
|
+
groupId: 'string',
|
|
83
|
+
words: 'list',
|
|
84
|
+
content: 'text',
|
|
85
|
+
timestamp: 'timestamp',
|
|
86
|
+
}, {
|
|
87
|
+
autoInc: true,
|
|
88
|
+
});
|
|
71
89
|
// --- Lifecycle ---
|
|
72
90
|
ctx.on('ready', async () => {
|
|
73
91
|
logger.info('Plugin initialized.');
|
|
@@ -255,24 +273,11 @@ function apply(ctx, config) {
|
|
|
255
273
|
logger.error(`[Recall Failed] Group: ${groupId}, Msg: ${messageId}, Error: ${err}`);
|
|
256
274
|
}
|
|
257
275
|
}
|
|
258
|
-
// 2.
|
|
259
|
-
if (result.censoredText) {
|
|
260
|
-
try {
|
|
261
|
-
// Format: "您触发了违禁词检测:违禁词:(censored_text)"
|
|
262
|
-
const warningMsg = session.text('commands.temporaryban.messages.violation_detected', [result.censoredText]);
|
|
263
|
-
await session.send(koishi_1.h.at(userId) + ' ' + warningMsg);
|
|
264
|
-
}
|
|
265
|
-
catch (err) {
|
|
266
|
-
logger.error(`[Send Failed] Group: ${groupId}, Error: ${err}`);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
// 3. Record Violation (Email Notification Logic Changed)
|
|
270
|
-
await mailer.recordViolation(userId, groupId, words, session.content);
|
|
271
|
-
// 4. Track & Punish
|
|
276
|
+
// 2. Track & Punish Calculation
|
|
272
277
|
const recordKey = `${groupId}-${userId}`;
|
|
273
|
-
|
|
274
|
-
// Use effectiveConfig
|
|
278
|
+
// 'now' variable is already declared above at line 262
|
|
275
279
|
const triggerWindow = (groupConfig.triggerWindowMinutes ?? 5) * koishi_1.Time.minute;
|
|
280
|
+
let record = userRecords.get(recordKey);
|
|
276
281
|
if (!record) {
|
|
277
282
|
record = { count: 1, firstTime: now };
|
|
278
283
|
userRecords.set(recordKey, record);
|
|
@@ -288,9 +293,35 @@ function apply(ctx, config) {
|
|
|
288
293
|
record.count++;
|
|
289
294
|
}
|
|
290
295
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
296
|
+
const currentCount = record.count;
|
|
297
|
+
const maxCount = effectiveConfig.triggerThreshold;
|
|
298
|
+
const remaining = Math.max(0, maxCount - currentCount);
|
|
299
|
+
const muteMinutes = effectiveConfig.muteMinutes;
|
|
300
|
+
logger.info(`[COUNT] [Group: ${groupId}] [User: ${userId}] ${currentCount}/${maxCount}`);
|
|
301
|
+
// 3. Send Warning
|
|
302
|
+
if (result.censoredText) {
|
|
303
|
+
try {
|
|
304
|
+
const showWord = groupConfig.showCensoredWord ?? config.defaultShowCensoredWord ?? true;
|
|
305
|
+
const wordDisplay = showWord ? result.censoredText : '***';
|
|
306
|
+
// Format: "You triggered a forbidden word check: {0}\nCurrent violations: {1}/{2}\n{3} more violations will result in a {4} min mute."
|
|
307
|
+
const warningMsg = session.text('commands.temporaryban.messages.violation_detail', [
|
|
308
|
+
wordDisplay,
|
|
309
|
+
currentCount,
|
|
310
|
+
maxCount,
|
|
311
|
+
remaining,
|
|
312
|
+
muteMinutes
|
|
313
|
+
]);
|
|
314
|
+
await session.send(koishi_1.h.at(userId) + ' ' + warningMsg);
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
logger.error(`[Send Failed] Group: ${groupId}, Error: ${err}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// 4. Record Violation (Mail)
|
|
321
|
+
await mailer.recordViolation(userId, groupId, words, session.content);
|
|
322
|
+
// 5. Mute Execution
|
|
323
|
+
if (currentCount >= maxCount) {
|
|
324
|
+
const muteSeconds = Math.floor(muteMinutes * 60);
|
|
294
325
|
try {
|
|
295
326
|
if (session.bot.muteGuildMember) {
|
|
296
327
|
// Ensure parameters are correct: guildId, userId, milliseconds
|
package/lib/locales/en-US.d.ts
CHANGED
|
@@ -34,11 +34,25 @@ declare const _default: {
|
|
|
34
34
|
history_list: string;
|
|
35
35
|
cleanup_info: string;
|
|
36
36
|
group_info: string;
|
|
37
|
+
violation_detail: string;
|
|
38
|
+
ignored_word_added: string;
|
|
39
|
+
ignored_word_removed: string;
|
|
40
|
+
no_ignored_words: string;
|
|
41
|
+
ignored_words_list: string;
|
|
37
42
|
};
|
|
38
43
|
};
|
|
39
44
|
'temporaryban.info': {
|
|
40
45
|
description: string;
|
|
41
46
|
};
|
|
47
|
+
'temporaryban.whitelist.word.add': {
|
|
48
|
+
description: string;
|
|
49
|
+
};
|
|
50
|
+
'temporaryban.whitelist.word.remove': {
|
|
51
|
+
description: string;
|
|
52
|
+
};
|
|
53
|
+
'temporaryban.whitelist.word.list': {
|
|
54
|
+
description: string;
|
|
55
|
+
};
|
|
42
56
|
'temporaryban.report': {
|
|
43
57
|
description: string;
|
|
44
58
|
};
|
package/lib/locales/en-US.js
CHANGED
|
@@ -35,12 +35,26 @@ exports.default = {
|
|
|
35
35
|
no_history: 'No recent history for user {0}.',
|
|
36
36
|
history_list: 'Recent history for user {0}:\n{1}',
|
|
37
37
|
cleanup_info: 'Cache cleanup is mainly handled automatically by the system.',
|
|
38
|
-
group_info: 'Group Info ({0}):\nStatus: {1}\nMethods: {2}\nSmart Verify: {3}\nThreshold: {4}\nMute: {5}min\nWhitelist: {6}'
|
|
38
|
+
group_info: 'Group Info ({0}):\nStatus: {1}\nMethods: {2}\nSmart Verify: {3}\nThreshold: {4}\nMute: {5}min\nWhitelist: {6}',
|
|
39
|
+
violation_detail: 'You triggered a forbidden word check: {0}\nCurrent violations: {1}/{2}\n{3} more violations will result in a {4} min mute.',
|
|
40
|
+
ignored_word_added: 'Added "{0}" to ignored words list.',
|
|
41
|
+
ignored_word_removed: 'Removed "{0}" from ignored words list.',
|
|
42
|
+
no_ignored_words: 'No ignored words.',
|
|
43
|
+
ignored_words_list: 'Ignored words ({0}):\n{1}'
|
|
39
44
|
}
|
|
40
45
|
},
|
|
41
46
|
'temporaryban.info': {
|
|
42
47
|
description: 'View current group configuration'
|
|
43
48
|
},
|
|
49
|
+
'temporaryban.whitelist.word.add': {
|
|
50
|
+
description: 'Add ignored word to group'
|
|
51
|
+
},
|
|
52
|
+
'temporaryban.whitelist.word.remove': {
|
|
53
|
+
description: 'Remove ignored word from group'
|
|
54
|
+
},
|
|
55
|
+
'temporaryban.whitelist.word.list': {
|
|
56
|
+
description: 'List ignored words in group'
|
|
57
|
+
},
|
|
44
58
|
'temporaryban.report': {
|
|
45
59
|
description: 'Manually trigger violation report (Global Admin only)'
|
|
46
60
|
},
|
package/lib/locales/zh-CN.d.ts
CHANGED
|
@@ -34,11 +34,27 @@ declare const _default: {
|
|
|
34
34
|
history_list: string;
|
|
35
35
|
cleanup_info: string;
|
|
36
36
|
group_info: string;
|
|
37
|
+
violation_detail: string;
|
|
38
|
+
ignored_word_added: string;
|
|
39
|
+
ignored_word_removed: string;
|
|
40
|
+
no_ignored_words: string;
|
|
41
|
+
ignored_words_list: string;
|
|
42
|
+
no_whitelist_users: string;
|
|
43
|
+
whitelist_users_list: string;
|
|
37
44
|
};
|
|
38
45
|
};
|
|
39
46
|
'temporaryban.info': {
|
|
40
47
|
description: string;
|
|
41
48
|
};
|
|
49
|
+
'temporaryban.whitelist.word.add': {
|
|
50
|
+
description: string;
|
|
51
|
+
};
|
|
52
|
+
'temporaryban.whitelist.word.remove': {
|
|
53
|
+
description: string;
|
|
54
|
+
};
|
|
55
|
+
'temporaryban.whitelist.word.list': {
|
|
56
|
+
description: string;
|
|
57
|
+
};
|
|
42
58
|
'temporaryban.report': {
|
|
43
59
|
description: string;
|
|
44
60
|
};
|
package/lib/locales/zh-CN.js
CHANGED
|
@@ -35,12 +35,28 @@ exports.default = {
|
|
|
35
35
|
no_history: '用户 {0} 没有最近的历史记录。',
|
|
36
36
|
history_list: '用户 {0} 的最近历史记录:\n{1}',
|
|
37
37
|
cleanup_info: '缓存清理主要由系统自动进行。',
|
|
38
|
-
group_info: '群组信息 ({0}):\n状态: {1}\n检测方式: {2}\n智能验证: {3}\n触发阈值: {4}次\n禁言时长: {5}分钟\n白名单人数: {6}'
|
|
38
|
+
group_info: '群组信息 ({0}):\n状态: {1}\n检测方式: {2}\n智能验证: {3}\n触发阈值: {4}次\n禁言时长: {5}分钟\n白名单人数: {6}',
|
|
39
|
+
violation_detail: '您触发了违禁词检测:违禁词:({0})\n当前违规次数: {1}/{2}\n再违规 {3} 次将被禁言 {4} 分钟。',
|
|
40
|
+
ignored_word_added: '已添加 "{0}" 到本群忽略词列表。',
|
|
41
|
+
ignored_word_removed: '已从本群忽略词列表移除 "{0}"。',
|
|
42
|
+
no_ignored_words: '没有忽略词。',
|
|
43
|
+
ignored_words_list: '忽略词 ({0}):\n{1}',
|
|
44
|
+
no_whitelist_users: '白名单为空。',
|
|
45
|
+
whitelist_users_list: '白名单用户 ({0}):\n{1}'
|
|
39
46
|
}
|
|
40
47
|
},
|
|
41
48
|
'temporaryban.info': {
|
|
42
49
|
description: '查看当前群组的配置信息'
|
|
43
50
|
},
|
|
51
|
+
'temporaryban.whitelist.word.add': {
|
|
52
|
+
description: '添加本群忽略词'
|
|
53
|
+
},
|
|
54
|
+
'temporaryban.whitelist.word.remove': {
|
|
55
|
+
description: '移除本群忽略词'
|
|
56
|
+
},
|
|
57
|
+
'temporaryban.whitelist.word.list': {
|
|
58
|
+
description: '列出本群忽略词'
|
|
59
|
+
},
|
|
44
60
|
'temporaryban.report': {
|
|
45
61
|
description: '手动触发违规报告(仅限全局管理员)'
|
|
46
62
|
},
|
|
@@ -11,6 +11,7 @@ export declare class DetectorService {
|
|
|
11
11
|
private config;
|
|
12
12
|
private logger;
|
|
13
13
|
private localDictCache;
|
|
14
|
+
private ignoredWordsCache;
|
|
14
15
|
private history;
|
|
15
16
|
constructor(ctx: Context, config: Config, history: HistoryService);
|
|
16
17
|
private initCache;
|
|
@@ -18,6 +19,10 @@ export declare class DetectorService {
|
|
|
18
19
|
addWord(groupId: string, word: string): Promise<boolean>;
|
|
19
20
|
removeWord(groupId: string, word: string): Promise<boolean>;
|
|
20
21
|
getWords(groupId: string): BadWordItem[];
|
|
22
|
+
addIgnoredWord(groupId: string, word: string): Promise<boolean>;
|
|
23
|
+
removeIgnoredWord(groupId: string, word: string): Promise<boolean>;
|
|
24
|
+
getIgnoredWords(groupId: string): string[];
|
|
25
|
+
isIgnored(groupId: string, word: string): boolean;
|
|
21
26
|
parseLocalDict(dictStr: string): BadWordItem[];
|
|
22
27
|
reloadGroup(groupId: string): void;
|
|
23
28
|
check(content: string, groupId: string, userId: string, groupConfig: any): Promise<CheckResult>;
|
package/lib/services/detector.js
CHANGED
|
@@ -40,6 +40,7 @@ const prompt_1 = require("../utils/prompt");
|
|
|
40
40
|
class DetectorService {
|
|
41
41
|
constructor(ctx, config, history) {
|
|
42
42
|
this.localDictCache = new Map();
|
|
43
|
+
this.ignoredWordsCache = new Map(); // groupId -> Set<word>
|
|
43
44
|
this.ctx = ctx;
|
|
44
45
|
this.config = config;
|
|
45
46
|
this.history = history;
|
|
@@ -84,7 +85,17 @@ class DetectorService {
|
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
this.localDictCache = map;
|
|
88
|
+
// 3. Load Ignored Words
|
|
89
|
+
const allIgnored = await this.ctx.database.get('temporaryban_ignored_words', {});
|
|
90
|
+
this.ignoredWordsCache.clear();
|
|
91
|
+
for (const item of allIgnored) {
|
|
92
|
+
if (!this.ignoredWordsCache.has(item.groupId)) {
|
|
93
|
+
this.ignoredWordsCache.set(item.groupId, new Set());
|
|
94
|
+
}
|
|
95
|
+
this.ignoredWordsCache.get(item.groupId)?.add(item.word);
|
|
96
|
+
}
|
|
87
97
|
this.logger.info(`Loaded bad words for ${map.size} groups from database.`);
|
|
98
|
+
this.logger.info(`Loaded ignored words for ${this.ignoredWordsCache.size} groups.`);
|
|
88
99
|
}
|
|
89
100
|
async addWord(groupId, word) {
|
|
90
101
|
const exists = await this.ctx.database.get('temporaryban_badwords', { groupId, word });
|
|
@@ -119,6 +130,39 @@ class DetectorService {
|
|
|
119
130
|
getWords(groupId) {
|
|
120
131
|
return this.localDictCache.get(groupId) || [];
|
|
121
132
|
}
|
|
133
|
+
async addIgnoredWord(groupId, word) {
|
|
134
|
+
const exists = await this.ctx.database.get('temporaryban_ignored_words', { groupId, word });
|
|
135
|
+
if (exists.length > 0)
|
|
136
|
+
return false;
|
|
137
|
+
await this.ctx.database.create('temporaryban_ignored_words', {
|
|
138
|
+
groupId,
|
|
139
|
+
word,
|
|
140
|
+
createdAt: new Date()
|
|
141
|
+
});
|
|
142
|
+
if (!this.ignoredWordsCache.has(groupId)) {
|
|
143
|
+
this.ignoredWordsCache.set(groupId, new Set());
|
|
144
|
+
}
|
|
145
|
+
this.ignoredWordsCache.get(groupId)?.add(word);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
async removeIgnoredWord(groupId, word) {
|
|
149
|
+
const result = await this.ctx.database.remove('temporaryban_ignored_words', { groupId, word });
|
|
150
|
+
if (result.matched === 0)
|
|
151
|
+
return false;
|
|
152
|
+
if (this.ignoredWordsCache.has(groupId)) {
|
|
153
|
+
this.ignoredWordsCache.get(groupId)?.delete(word);
|
|
154
|
+
}
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
getIgnoredWords(groupId) {
|
|
158
|
+
return Array.from(this.ignoredWordsCache.get(groupId) || []);
|
|
159
|
+
}
|
|
160
|
+
isIgnored(groupId, word) {
|
|
161
|
+
const ignoredSet = this.ignoredWordsCache.get(groupId);
|
|
162
|
+
if (!ignoredSet || ignoredSet.size === 0)
|
|
163
|
+
return false;
|
|
164
|
+
return ignoredSet.has(word);
|
|
165
|
+
}
|
|
122
166
|
// Deprecated/Helper for migration only
|
|
123
167
|
parseLocalDict(dictStr) {
|
|
124
168
|
const result = [];
|
|
@@ -249,7 +293,7 @@ class DetectorService {
|
|
|
249
293
|
const aiRes = await this.checkWithAI(contextPrompt, groupConfig.aiThreshold);
|
|
250
294
|
if (aiRes.detected) {
|
|
251
295
|
this.logger.info(`[Smart Verification] Confirmed violation.`);
|
|
252
|
-
|
|
296
|
+
initialDetection = aiRes; // Update result with AI confirmation
|
|
253
297
|
}
|
|
254
298
|
else {
|
|
255
299
|
this.logger.info(`[Smart Verification] False positive dismissed by AI.`);
|
|
@@ -257,11 +301,23 @@ class DetectorService {
|
|
|
257
301
|
}
|
|
258
302
|
}
|
|
259
303
|
}
|
|
304
|
+
// 3. Ignored Words Filter (Whitelist)
|
|
305
|
+
if (initialDetection.detected && initialDetection.detectedWords) {
|
|
306
|
+
const filteredWords = initialDetection.detectedWords.filter(w => !this.isIgnored(groupId, w));
|
|
307
|
+
if (filteredWords.length === 0) {
|
|
308
|
+
if (this.config.debug && groupConfig.detailedLog) {
|
|
309
|
+
this.logger.debug(`[Check] Ignored: All detected words are in whitelist. (${initialDetection.detectedWords.join(', ')})`);
|
|
310
|
+
}
|
|
311
|
+
return { detected: false };
|
|
312
|
+
}
|
|
313
|
+
// Update with filtered words
|
|
314
|
+
initialDetection.detectedWords = filteredWords;
|
|
315
|
+
}
|
|
260
316
|
return initialDetection;
|
|
261
317
|
}
|
|
262
318
|
// --- OpenAI / SiliconFlow ---
|
|
263
319
|
async checkWithAI(content, threshold = 0.6) {
|
|
264
|
-
if (!this.config.openai
|
|
320
|
+
if (!this.config.openai?.apiKey) {
|
|
265
321
|
this.logger.warn('OpenAI/SiliconFlow API Key is missing.');
|
|
266
322
|
return { detected: false };
|
|
267
323
|
}
|
|
@@ -315,7 +371,7 @@ class DetectorService {
|
|
|
315
371
|
}
|
|
316
372
|
// --- Baidu Cloud ---
|
|
317
373
|
async checkWithBaidu(content) {
|
|
318
|
-
if (!this.config.baidu
|
|
374
|
+
if (!this.config.baidu?.apiKey || !this.config.baidu?.secretKey) {
|
|
319
375
|
this.logger.warn('Baidu API Key or Secret Key is missing.');
|
|
320
376
|
return { detected: false };
|
|
321
377
|
}
|
|
@@ -358,11 +414,12 @@ class DetectorService {
|
|
|
358
414
|
}
|
|
359
415
|
// --- Aliyun (Green 2.0 / Enhanced) ---
|
|
360
416
|
async checkWithAliyun(content) {
|
|
361
|
-
const
|
|
362
|
-
if (!accessKeyId || !accessKeySecret) {
|
|
417
|
+
const aliyunConfig = this.config.aliyun;
|
|
418
|
+
if (!aliyunConfig?.accessKeyId || !aliyunConfig?.accessKeySecret) {
|
|
363
419
|
this.logger.warn('Aliyun AccessKey ID or Secret is missing.');
|
|
364
420
|
return { detected: false };
|
|
365
421
|
}
|
|
422
|
+
const { accessKeyId, accessKeySecret, endpoint } = aliyunConfig;
|
|
366
423
|
// RPC Signature Implementation
|
|
367
424
|
try {
|
|
368
425
|
const params = {
|
|
@@ -419,11 +476,12 @@ class DetectorService {
|
|
|
419
476
|
}
|
|
420
477
|
// --- Tencent Cloud (TMS) ---
|
|
421
478
|
async checkWithTencent(content) {
|
|
422
|
-
const
|
|
423
|
-
if (!secretId || !secretKey) {
|
|
479
|
+
const tencentConfig = this.config.tencent;
|
|
480
|
+
if (!tencentConfig?.secretId || !tencentConfig?.secretKey) {
|
|
424
481
|
this.logger.warn('Tencent SecretId or SecretKey is missing.');
|
|
425
482
|
return { detected: false };
|
|
426
483
|
}
|
|
484
|
+
const { secretId, secretKey, region } = tencentConfig;
|
|
427
485
|
try {
|
|
428
486
|
const endpoint = 'tms.tencentcloudapi.com';
|
|
429
487
|
const service = 'tms';
|
|
@@ -453,6 +511,11 @@ class DetectorService {
|
|
|
453
511
|
const signature = crypto.createHmac('sha256', kSigning).update(stringToSign).digest('hex');
|
|
454
512
|
// 4. Authorization Header
|
|
455
513
|
const authorization = `${algorithm} Credential=${secretId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
514
|
+
if (this.config.debug) {
|
|
515
|
+
this.logger.debug(`[Tencent Request] URL: https://${endpoint}`);
|
|
516
|
+
this.logger.debug(`[Tencent Request] Payload: ${payloadStr}`);
|
|
517
|
+
this.logger.debug(`[Tencent Request] Headers: X-TC-Action=${action}, X-TC-Version=${version}, Authorization=${authorization}`);
|
|
518
|
+
}
|
|
456
519
|
const response = await this.ctx.http.post(`https://${endpoint}`, payload, {
|
|
457
520
|
headers: {
|
|
458
521
|
'Authorization': authorization,
|
|
@@ -484,7 +547,7 @@ class DetectorService {
|
|
|
484
547
|
}
|
|
485
548
|
}
|
|
486
549
|
async checkWithApi(content) {
|
|
487
|
-
if (!this.config.api
|
|
550
|
+
if (!this.config.api?.apiId || !this.config.api?.apiKey) {
|
|
488
551
|
this.logger.warn('API ID or Key is missing. Skipping API detection.');
|
|
489
552
|
return { detected: false };
|
|
490
553
|
}
|
package/lib/services/mailer.d.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { Context } from 'koishi';
|
|
2
2
|
import { Config } from '../config';
|
|
3
3
|
export declare class MailerService {
|
|
4
|
+
private ctx;
|
|
4
5
|
private logger;
|
|
5
6
|
private config;
|
|
6
|
-
private records;
|
|
7
|
-
private storagePath;
|
|
8
7
|
constructor(ctx: Context, config: Config);
|
|
9
|
-
private loadRecords;
|
|
10
|
-
private saveRecords;
|
|
11
8
|
recordViolation(userId: string, groupId: string, words: string[], content: string): Promise<void>;
|
|
12
9
|
private sendImmediate;
|
|
13
10
|
sendSummaryReport(hours: number): Promise<{
|
package/lib/services/mailer.js
CHANGED
|
@@ -36,72 +36,47 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.MailerService = void 0;
|
|
37
37
|
const koishi_1 = require("koishi");
|
|
38
38
|
const nodemailer = __importStar(require("nodemailer"));
|
|
39
|
-
const fs = __importStar(require("fs"));
|
|
40
|
-
const path = __importStar(require("path"));
|
|
41
39
|
class MailerService {
|
|
42
40
|
constructor(ctx, config) {
|
|
43
|
-
this.
|
|
41
|
+
this.ctx = ctx;
|
|
44
42
|
this.config = config;
|
|
45
43
|
this.logger = new koishi_1.Logger('temporaryban:mailer');
|
|
46
|
-
// Setup storage for persistence (simple JSON file)
|
|
47
|
-
this.storagePath = path.resolve(ctx.baseDir, 'data', 'temporaryban_records.json');
|
|
48
|
-
this.loadRecords();
|
|
49
44
|
// Start periodic task if summary interval is set
|
|
50
|
-
if (this.config.smtp.summaryIntervalDays > 0) {
|
|
45
|
+
if (this.config.smtp?.summaryIntervalDays && this.config.smtp.summaryIntervalDays > 0) {
|
|
51
46
|
// Check every hour if it's time to send (this is a simplified scheduler)
|
|
52
|
-
// For production, a more robust scheduler might be needed, but this works for now.
|
|
53
|
-
// We send reports based on interval from the *last send time* or just accumulate.
|
|
54
|
-
// Since we don't store "last sent time", we'll just run a check every day?
|
|
55
|
-
// Better: use setInterval for the configured days.
|
|
56
47
|
ctx.setInterval(() => {
|
|
57
|
-
|
|
48
|
+
if (this.config.smtp) {
|
|
49
|
+
this.sendSummaryReport(this.config.smtp.summaryIntervalDays * 24);
|
|
50
|
+
}
|
|
58
51
|
}, this.config.smtp.summaryIntervalDays * 24 * 60 * 60 * 1000);
|
|
59
52
|
}
|
|
60
53
|
}
|
|
61
|
-
loadRecords() {
|
|
62
|
-
try {
|
|
63
|
-
if (fs.existsSync(this.storagePath)) {
|
|
64
|
-
const data = fs.readFileSync(this.storagePath, 'utf-8');
|
|
65
|
-
this.records = JSON.parse(data);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
catch (err) {
|
|
69
|
-
this.logger.warn(`Failed to load records: ${err}`);
|
|
70
|
-
this.records = [];
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
saveRecords() {
|
|
74
|
-
try {
|
|
75
|
-
const dir = path.dirname(this.storagePath);
|
|
76
|
-
if (!fs.existsSync(dir)) {
|
|
77
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
78
|
-
}
|
|
79
|
-
fs.writeFileSync(this.storagePath, JSON.stringify(this.records, null, 2));
|
|
80
|
-
}
|
|
81
|
-
catch (err) {
|
|
82
|
-
this.logger.error(`Failed to save records: ${err}`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
54
|
async recordViolation(userId, groupId, words, content) {
|
|
55
|
+
if (!this.config.smtp)
|
|
56
|
+
return;
|
|
86
57
|
// If summary interval is 0, send immediately (legacy mode)
|
|
87
58
|
if (this.config.smtp.summaryIntervalDays === 0) {
|
|
88
59
|
await this.sendImmediate(userId, groupId, words, content);
|
|
89
60
|
return;
|
|
90
61
|
}
|
|
91
|
-
// Otherwise, record it
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
62
|
+
// Otherwise, record it to Database
|
|
63
|
+
try {
|
|
64
|
+
await this.ctx.database.create('temporaryban_violations', {
|
|
65
|
+
userId,
|
|
66
|
+
groupId,
|
|
67
|
+
words,
|
|
68
|
+
content,
|
|
69
|
+
timestamp: new Date()
|
|
70
|
+
});
|
|
71
|
+
this.logger.debug(`Violation recorded for user ${userId}.`);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
this.logger.error(`Failed to record violation to DB: ${err}`);
|
|
75
|
+
}
|
|
101
76
|
}
|
|
102
77
|
// Old method for immediate sending
|
|
103
78
|
async sendImmediate(userId, groupId, words, content) {
|
|
104
|
-
if (!this.config.smtp
|
|
79
|
+
if (!this.config.smtp?.host || !this.config.smtp?.receivers?.length)
|
|
105
80
|
return;
|
|
106
81
|
const transporter = this.createTransporter();
|
|
107
82
|
const html = `
|
|
@@ -150,13 +125,22 @@ class MailerService {
|
|
|
150
125
|
}
|
|
151
126
|
// New method for summary report
|
|
152
127
|
async sendSummaryReport(hours) {
|
|
153
|
-
if (!this.config.smtp
|
|
128
|
+
if (!this.config.smtp?.host || !this.config.smtp?.receivers?.length) {
|
|
154
129
|
return { success: false, error: 'smtp_not_configured' };
|
|
155
130
|
}
|
|
156
|
-
const now = Date
|
|
157
|
-
const cutoff = now - hours * 60 * 60 * 1000;
|
|
158
|
-
// Filter records within the time window
|
|
159
|
-
|
|
131
|
+
const now = new Date();
|
|
132
|
+
const cutoff = new Date(now.getTime() - hours * 60 * 60 * 1000);
|
|
133
|
+
// Filter records within the time window from Database
|
|
134
|
+
let targetRecords = [];
|
|
135
|
+
try {
|
|
136
|
+
targetRecords = await this.ctx.database.get('temporaryban_violations', {
|
|
137
|
+
timestamp: { $gte: cutoff }
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
this.logger.error(`Failed to fetch violations from DB: ${err}`);
|
|
142
|
+
return { success: false, error: 'db_error' };
|
|
143
|
+
}
|
|
160
144
|
if (targetRecords.length === 0) {
|
|
161
145
|
return { success: true, count: 0 };
|
|
162
146
|
}
|
|
@@ -164,7 +148,7 @@ class MailerService {
|
|
|
164
148
|
// Generate HTML Table
|
|
165
149
|
const tableRows = targetRecords.map((r, index) => `
|
|
166
150
|
<tr style="border-bottom: 1px solid #eee; background-color: ${index % 2 === 0 ? '#ffffff' : '#fcfcfc'};">
|
|
167
|
-
<td style="padding: 12px 8px; color: #666; font-size: 13px;">${
|
|
151
|
+
<td style="padding: 12px 8px; color: #666; font-size: 13px;">${r.timestamp.toLocaleString()}</td>
|
|
168
152
|
<td style="padding: 12px 8px; font-weight: 500;">${r.groupId}</td>
|
|
169
153
|
<td style="padding: 12px 8px;">${r.userId}</td>
|
|
170
154
|
<td style="padding: 12px 8px;"><span style="background-color: #ffebee; color: #c62828; padding: 2px 6px; border-radius: 4px; font-size: 12px;">${r.words.join(', ')}</span></td>
|
|
@@ -235,16 +219,21 @@ class MailerService {
|
|
|
235
219
|
return { success: false, error: String(err) };
|
|
236
220
|
}
|
|
237
221
|
}
|
|
238
|
-
cleanupOldRecords(days) {
|
|
239
|
-
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
222
|
+
async cleanupOldRecords(days) {
|
|
223
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
224
|
+
try {
|
|
225
|
+
await this.ctx.database.remove('temporaryban_violations', {
|
|
226
|
+
timestamp: { $lt: cutoff }
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
this.logger.error(`Failed to cleanup old records: ${err}`);
|
|
245
231
|
}
|
|
246
232
|
}
|
|
247
233
|
createTransporter() {
|
|
234
|
+
if (!this.config.smtp) {
|
|
235
|
+
throw new Error('SMTP configuration is missing');
|
|
236
|
+
}
|
|
248
237
|
return nodemailer.createTransport({
|
|
249
238
|
host: this.config.smtp.host,
|
|
250
239
|
port: this.config.smtp.port,
|
|
@@ -15,6 +15,7 @@ export declare class WhitelistService {
|
|
|
15
15
|
init(): Promise<void>;
|
|
16
16
|
add(groupId: string, userId: string): Promise<boolean>;
|
|
17
17
|
remove(groupId: string, userId: string): Promise<boolean>;
|
|
18
|
+
private syncToConfig;
|
|
19
|
+
getWhitelist(groupId: string): string[];
|
|
18
20
|
isWhitelisted(groupId: string, userId: string): boolean;
|
|
19
|
-
getList(groupId: string): string[];
|
|
20
21
|
}
|
|
@@ -56,6 +56,8 @@ class WhitelistService {
|
|
|
56
56
|
this.cache.set(groupId, new Set());
|
|
57
57
|
}
|
|
58
58
|
this.cache.get(groupId)?.add(userId);
|
|
59
|
+
// Sync to Config
|
|
60
|
+
await this.syncToConfig(groupId, userId, 'add');
|
|
59
61
|
return true;
|
|
60
62
|
}
|
|
61
63
|
async remove(groupId, userId) {
|
|
@@ -65,21 +67,39 @@ class WhitelistService {
|
|
|
65
67
|
if (this.cache.has(groupId)) {
|
|
66
68
|
this.cache.get(groupId)?.delete(userId);
|
|
67
69
|
}
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
// If the user was in Config, `isWhitelisted` will still return true because of the merge in `init`.
|
|
71
|
-
// BUT `init` is only called at start. So runtime cache is what matters.
|
|
72
|
-
// Actually, `init` merges config into cache. If we remove from cache here, it is removed from runtime check.
|
|
73
|
-
// But on restart, it will come back from Config.
|
|
74
|
-
// This is a known limitation of mixing Config and DB.
|
|
75
|
-
// We can just proceed.
|
|
70
|
+
// Sync to Config
|
|
71
|
+
await this.syncToConfig(groupId, userId, 'remove');
|
|
76
72
|
return (result.matched || 0) > 0;
|
|
77
73
|
}
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
async syncToConfig(groupId, userId, action) {
|
|
75
|
+
try {
|
|
76
|
+
const config = this.ctx.scope.config;
|
|
77
|
+
const groups = config.groups || [];
|
|
78
|
+
const groupConfig = groups.find((g) => g.groupId === groupId);
|
|
79
|
+
if (groupConfig) {
|
|
80
|
+
if (!groupConfig.whitelist)
|
|
81
|
+
groupConfig.whitelist = [];
|
|
82
|
+
if (action === 'add') {
|
|
83
|
+
if (!groupConfig.whitelist.some((w) => w.userId === userId)) {
|
|
84
|
+
groupConfig.whitelist.push({ userId });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
groupConfig.whitelist = groupConfig.whitelist.filter((w) => w.userId !== userId);
|
|
89
|
+
}
|
|
90
|
+
// Update scope config
|
|
91
|
+
await this.ctx.scope.update(config);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
this.logger.warn(`Failed to sync whitelist to config: ${err}`);
|
|
96
|
+
}
|
|
80
97
|
}
|
|
81
|
-
|
|
98
|
+
getWhitelist(groupId) {
|
|
82
99
|
return Array.from(this.cache.get(groupId) || []);
|
|
83
100
|
}
|
|
101
|
+
isWhitelisted(groupId, userId) {
|
|
102
|
+
return this.cache.get(groupId)?.has(userId) || false;
|
|
103
|
+
}
|
|
84
104
|
}
|
|
85
105
|
exports.WhitelistService = WhitelistService;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-temporaryban",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7-beta1",
|
|
4
4
|
"description": "Koishi 违禁词检测与禁言插件,支持本地词库与在线API检测,违规邮件通知。",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
},
|
|
42
42
|
"service": {
|
|
43
43
|
"required": [
|
|
44
|
-
"database"
|
|
44
|
+
"database",
|
|
45
|
+
"http"
|
|
45
46
|
],
|
|
46
47
|
"optional": []
|
|
47
48
|
}
|