@zhin.js/plugin-content-moderation 1.1.15 → 1.1.17
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/README.md +8 -5
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +2 -3
- package/lib/config.js.map +1 -1
- package/lib/engine.d.ts +2 -0
- package/lib/engine.d.ts.map +1 -1
- package/lib/engine.js +2 -1
- package/lib/engine.js.map +1 -1
- package/lib/group-moderation.d.ts +21 -0
- package/lib/group-moderation.d.ts.map +1 -1
- package/lib/group-moderation.js +35 -18
- package/lib/group-moderation.js.map +1 -1
- package/lib/messages.d.ts +2 -0
- package/lib/messages.d.ts.map +1 -0
- package/lib/messages.js +10 -0
- package/lib/messages.js.map +1 -0
- package/lib/types.d.ts +2 -1
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js.map +1 -1
- package/middlewares/inbound/index.js +33 -6
- package/middlewares/inbound/index.ts +31 -5
- package/package.json +5 -5
- package/schema.json +257 -45
- package/src/config.ts +2 -3
- package/src/engine.ts +4 -1
- package/src/group-moderation.ts +53 -18
- package/src/messages.ts +10 -0
- package/src/types.ts +2 -1
|
@@ -3,7 +3,9 @@ import type { Message } from '@zhin.js/core/runtime';
|
|
|
3
3
|
import { aiCompletionToken, createDeliveryTarget, databaseHostToken, getLogger, hasResource,
|
|
4
4
|
outboundHostToken, usageSubjectFromMessage } from 'zhin.js';
|
|
5
5
|
import { adapterFeatureId, type AdapterIndex } from 'zhin.js/adapter';
|
|
6
|
+
import { groupRuleViolationToken, type GroupRuleViolationSink } from '@zhin.js/core/runtime';
|
|
6
7
|
import { generationAdmissionSource } from 'zhin.js/plugin-runtime';
|
|
8
|
+
import { resolveLocale } from 'zhin.js/i18n';
|
|
7
9
|
import {
|
|
8
10
|
buildScanContext,
|
|
9
11
|
extractFromTextAndSegments,
|
|
@@ -30,6 +32,7 @@ export default defineMiddleware<Message, ModerationConfig>({
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
const message = context.input;
|
|
35
|
+
const locale = resolveLocale(message.locale);
|
|
33
36
|
const sender = message.sender?.id;
|
|
34
37
|
const conversationId = message.conversation.id;
|
|
35
38
|
const endpointMasters = resolveEndpointMasters(message, config);
|
|
@@ -70,6 +73,7 @@ export default defineMiddleware<Message, ModerationConfig>({
|
|
|
70
73
|
|
|
71
74
|
const result = await engine.apply({
|
|
72
75
|
direction: 'inbound',
|
|
76
|
+
locale,
|
|
73
77
|
recallManaged: review?.done,
|
|
74
78
|
extracted,
|
|
75
79
|
scanInput,
|
|
@@ -115,6 +119,7 @@ async function enqueueGroupModeration(
|
|
|
115
119
|
config: ModerationConfig, bypass: boolean,
|
|
116
120
|
): Promise<{ done: Promise<boolean> } | undefined> {
|
|
117
121
|
const message = context.input;
|
|
122
|
+
const locale = resolveLocale(message.locale);
|
|
118
123
|
const conversation = structuredClone(message.conversation);
|
|
119
124
|
const target = config.groupModeration.find((item) => conversation.kind === 'group'
|
|
120
125
|
&& item.adapter === conversation.endpoint.adapter
|
|
@@ -132,6 +137,19 @@ async function enqueueGroupModeration(
|
|
|
132
137
|
const destination = createDeliveryTarget({ conversation });
|
|
133
138
|
const scope = JSON.stringify(destination);
|
|
134
139
|
const remembered = engine.groupModerator.remember(scope, message.sender?.id ?? '', text);
|
|
140
|
+
// ADR 0039 §5: punishment is the group rule's job. A configured group gets default pending rules once,
|
|
141
|
+
// and stays reminder / warning only until an owner or admin confirms them.
|
|
142
|
+
const rules: GroupRuleViolationSink | undefined = hasResource(context, groupRuleViolationToken)
|
|
143
|
+
? context.use(groupRuleViolationToken) : undefined;
|
|
144
|
+
if (rules) {
|
|
145
|
+
engine.groupModerator.ensureRules(scope, async () => {
|
|
146
|
+
for (const trigger of ['advertising', 'abuse'] as const) {
|
|
147
|
+
const outcome = await rules.ensureDefaultRule({ conversation, trigger });
|
|
148
|
+
if (outcome === 'unavailable') throw new Error('Group rule storage unavailable');
|
|
149
|
+
if (outcome === 'created') logger.info({ event: 'group_rule_migrated', trigger, endpoint: conversation.endpoint.id, group: conversation.id });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
135
153
|
if (!bypass && message.sender?.id && message.origin !== 'self'
|
|
136
154
|
&& hasResource(context, aiCompletionToken) && hasResource(context, databaseHostToken)
|
|
137
155
|
&& hasResource(context, outboundHostToken)) {
|
|
@@ -151,7 +169,7 @@ async function enqueueGroupModeration(
|
|
|
151
169
|
};
|
|
152
170
|
try {
|
|
153
171
|
const queued = await engine.groupReviewQueue.enqueue({
|
|
154
|
-
scope, group: conversation.id, sender: message.sender.id,
|
|
172
|
+
scope, group: conversation.id, sender: message.sender.id, locale,
|
|
155
173
|
messageId, sentAt: Number(message.metadata?.platformSentAt), text, ...remembered,
|
|
156
174
|
signal,
|
|
157
175
|
state: context.use(databaseHostToken).state.forConversation(destination),
|
|
@@ -159,17 +177,25 @@ async function enqueueGroupModeration(
|
|
|
159
177
|
inspectGroupMember: endpoint.management?.inspectGroupMember ? async (group, member, operationSignal) => {
|
|
160
178
|
assertTarget(); return endpoint.management!.inspectGroupMember!(group, member, operationSignal);
|
|
161
179
|
} : undefined,
|
|
162
|
-
moderateGroupMember: endpoint.management?.moderateGroupMember ? async (group, member, action, operationSignal) => {
|
|
163
|
-
assertTarget(); return endpoint.management!.moderateGroupMember!(group, member, action, operationSignal);
|
|
164
|
-
} : undefined,
|
|
165
180
|
},
|
|
181
|
+
...(rules ? { rules: async (hit) => {
|
|
182
|
+
assertTarget();
|
|
183
|
+
return rules.report({
|
|
184
|
+
conversation, memberId: message.sender!.id, trigger: hit.category, newcomer: hit.newcomer,
|
|
185
|
+
sentAt: Number(message.metadata?.platformSentAt), evidence: hit.evidence,
|
|
186
|
+
...(message.sender?.name ? { memberName: message.sender.name } : {}),
|
|
187
|
+
...(message.sender?.roles ? { memberRoles: message.sender.roles } : {}),
|
|
188
|
+
...(messageId ? { messageId } : {}),
|
|
189
|
+
...(hit.recall ? { recall: hit.recall } : {}),
|
|
190
|
+
});
|
|
191
|
+
} } : {}),
|
|
166
192
|
onBehalfOf: usageSubjectFromMessage(message),
|
|
167
193
|
reply: async (content, mention) => {
|
|
168
194
|
assertTarget();
|
|
169
195
|
const receipt = await host.sendTarget!(destination, mention ? [
|
|
170
196
|
{ type: 'mention', data: { target: mention } },
|
|
171
197
|
{ type: 'text', data: { text: ` ${content}` } },
|
|
172
|
-
] : content);
|
|
198
|
+
] : content, { locale });
|
|
173
199
|
if (!receipt) throw new Error('Moderation notice was not confirmed');
|
|
174
200
|
},
|
|
175
201
|
recall: async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/plugin-content-moderation",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.17",
|
|
4
4
|
"description": "Content moderation plugin for Zhin.js — inbound/outbound multi-source scanning (Plugin Runtime)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -37,13 +37,13 @@
|
|
|
37
37
|
},
|
|
38
38
|
"license": "MIT",
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@zhin.js/core": "1.1.
|
|
40
|
+
"@zhin.js/core": "1.1.52",
|
|
41
41
|
"@zhin.js/logger": "1.1.2"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"typescript": "^6.0.3",
|
|
45
45
|
"vitest": "^4.1.10",
|
|
46
|
-
"zhin.js": "1.1.
|
|
46
|
+
"zhin.js": "1.1.17"
|
|
47
47
|
},
|
|
48
48
|
"repository": {
|
|
49
49
|
"type": "git",
|
|
@@ -72,8 +72,8 @@
|
|
|
72
72
|
"plugins": []
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
|
-
"@zhin.js/middleware": "^1.1.
|
|
76
|
-
"zhin.js": "^1.1.
|
|
75
|
+
"@zhin.js/middleware": "^1.1.10",
|
|
76
|
+
"zhin.js": "^1.1.17"
|
|
77
77
|
},
|
|
78
78
|
"peerDependenciesMeta": {
|
|
79
79
|
"@zhin.js/middleware": {
|
package/schema.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"groupModeration": {
|
|
7
7
|
"type": "array",
|
|
8
8
|
"default": [],
|
|
9
|
-
"description": "自动广告/辱骂审核的精确范围;未配置时关闭。需要 AI
|
|
9
|
+
"description": "自动广告/辱骂审核的精确范围;未配置时关闭。需要 AI、原子持久状态和适配器群管支持。禁言 / 踢人交给 Agent 群规(ADR 0039),名单内的群会生成待确认的默认规则,确认前只提醒 / 警告。",
|
|
10
10
|
"items": {
|
|
11
11
|
"type": "object",
|
|
12
12
|
"additionalProperties": false,
|
|
@@ -34,46 +34,88 @@
|
|
|
34
34
|
"minLength": 1
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
},
|
|
38
|
+
"x-i18n": {
|
|
39
|
+
"en": {
|
|
40
|
+
"description": "Exact scope for automatic advertising/abuse moderation. Disabled when unconfigured. Requires AI, atomic persistent state and adapter group-management support."
|
|
41
|
+
}
|
|
37
42
|
}
|
|
38
43
|
},
|
|
39
44
|
"enabled": {
|
|
40
45
|
"type": "boolean",
|
|
41
46
|
"default": true,
|
|
42
|
-
"description": "总开关"
|
|
47
|
+
"description": "总开关",
|
|
48
|
+
"x-i18n": {
|
|
49
|
+
"en": {
|
|
50
|
+
"description": "Enable moderation."
|
|
51
|
+
}
|
|
52
|
+
}
|
|
43
53
|
},
|
|
44
54
|
"onError": {
|
|
45
55
|
"type": "string",
|
|
46
|
-
"enum": [
|
|
56
|
+
"enum": [
|
|
57
|
+
"open",
|
|
58
|
+
"closed"
|
|
59
|
+
],
|
|
47
60
|
"default": "open",
|
|
48
|
-
"description": "审查源失败时的全局默认策略:open=视为通过,closed=视为 critical"
|
|
61
|
+
"description": "审查源失败时的全局默认策略:open=视为通过,closed=视为 critical",
|
|
62
|
+
"x-i18n": {
|
|
63
|
+
"en": {
|
|
64
|
+
"description": "Default policy when a review source fails: open treats it as passed; closed treats it as critical."
|
|
65
|
+
}
|
|
66
|
+
}
|
|
49
67
|
},
|
|
50
68
|
"maskChar": {
|
|
51
69
|
"type": "string",
|
|
52
70
|
"default": "*",
|
|
53
71
|
"minLength": 1,
|
|
54
72
|
"maxLength": 4,
|
|
55
|
-
"description": "文本打码字符"
|
|
73
|
+
"description": "文本打码字符",
|
|
74
|
+
"x-i18n": {
|
|
75
|
+
"en": {
|
|
76
|
+
"description": "Text masking character."
|
|
77
|
+
}
|
|
78
|
+
}
|
|
56
79
|
},
|
|
57
80
|
"replyTemplate": {
|
|
58
81
|
"type": "string",
|
|
59
|
-
"
|
|
60
|
-
"
|
|
82
|
+
"description": "reply 动作时的提示文案;省略时按本次消息语言选择默认文案,显式文本保持原样",
|
|
83
|
+
"x-i18n": {
|
|
84
|
+
"en": {
|
|
85
|
+
"description": "Reply text for the reply action. When omitted, use the message language; explicit text is preserved."
|
|
86
|
+
}
|
|
87
|
+
}
|
|
61
88
|
},
|
|
62
89
|
"masters": {
|
|
63
90
|
"type": "array",
|
|
64
|
-
"items": {
|
|
91
|
+
"items": {
|
|
92
|
+
"type": "string"
|
|
93
|
+
},
|
|
65
94
|
"default": [],
|
|
66
|
-
"description": "额外视为 master 的 userId(兜底,与 endpoint master 合并)"
|
|
95
|
+
"description": "额外视为 master 的 userId(兜底,与 endpoint master 合并)",
|
|
96
|
+
"x-i18n": {
|
|
97
|
+
"en": {
|
|
98
|
+
"description": "Additional user IDs treated as masters, merged with endpoint masters as a fallback."
|
|
99
|
+
}
|
|
100
|
+
}
|
|
67
101
|
},
|
|
68
102
|
"inbound": {
|
|
69
103
|
"type": "object",
|
|
70
104
|
"additionalProperties": false,
|
|
71
105
|
"properties": {
|
|
72
|
-
"enabled": {
|
|
106
|
+
"enabled": {
|
|
107
|
+
"type": "boolean",
|
|
108
|
+
"default": true
|
|
109
|
+
},
|
|
73
110
|
"bypassMasters": {
|
|
74
111
|
"type": "boolean",
|
|
75
112
|
"default": true,
|
|
76
|
-
"description": "master 跳过入站审查"
|
|
113
|
+
"description": "master 跳过入站审查",
|
|
114
|
+
"x-i18n": {
|
|
115
|
+
"en": {
|
|
116
|
+
"description": "Skip inbound review for masters."
|
|
117
|
+
}
|
|
118
|
+
}
|
|
77
119
|
},
|
|
78
120
|
"whitelist": {
|
|
79
121
|
"type": "object",
|
|
@@ -81,12 +123,16 @@
|
|
|
81
123
|
"properties": {
|
|
82
124
|
"userIds": {
|
|
83
125
|
"type": "array",
|
|
84
|
-
"items": {
|
|
126
|
+
"items": {
|
|
127
|
+
"type": "string"
|
|
128
|
+
},
|
|
85
129
|
"default": []
|
|
86
130
|
},
|
|
87
131
|
"conversationIds": {
|
|
88
132
|
"type": "array",
|
|
89
|
-
"items": {
|
|
133
|
+
"items": {
|
|
134
|
+
"type": "string"
|
|
135
|
+
},
|
|
90
136
|
"default": []
|
|
91
137
|
}
|
|
92
138
|
}
|
|
@@ -97,11 +143,19 @@
|
|
|
97
143
|
"type": "object",
|
|
98
144
|
"additionalProperties": false,
|
|
99
145
|
"properties": {
|
|
100
|
-
"enabled": {
|
|
146
|
+
"enabled": {
|
|
147
|
+
"type": "boolean",
|
|
148
|
+
"default": true
|
|
149
|
+
},
|
|
101
150
|
"bypass": {
|
|
102
151
|
"type": "boolean",
|
|
103
152
|
"default": false,
|
|
104
|
-
"description": "为 true 时跳过出站审查"
|
|
153
|
+
"description": "为 true 时跳过出站审查",
|
|
154
|
+
"x-i18n": {
|
|
155
|
+
"en": {
|
|
156
|
+
"description": "Skip outbound review when true."
|
|
157
|
+
}
|
|
158
|
+
}
|
|
105
159
|
}
|
|
106
160
|
}
|
|
107
161
|
},
|
|
@@ -114,14 +168,28 @@
|
|
|
114
168
|
"anyOf": [
|
|
115
169
|
{
|
|
116
170
|
"type": "string",
|
|
117
|
-
"enum": [
|
|
171
|
+
"enum": [
|
|
172
|
+
"allow",
|
|
173
|
+
"log",
|
|
174
|
+
"reply",
|
|
175
|
+
"redact",
|
|
176
|
+
"drop",
|
|
177
|
+
"recall"
|
|
178
|
+
]
|
|
118
179
|
},
|
|
119
180
|
{
|
|
120
181
|
"type": "array",
|
|
121
182
|
"minItems": 1,
|
|
122
183
|
"items": {
|
|
123
184
|
"type": "string",
|
|
124
|
-
"enum": [
|
|
185
|
+
"enum": [
|
|
186
|
+
"allow",
|
|
187
|
+
"log",
|
|
188
|
+
"reply",
|
|
189
|
+
"redact",
|
|
190
|
+
"drop",
|
|
191
|
+
"recall"
|
|
192
|
+
]
|
|
125
193
|
}
|
|
126
194
|
}
|
|
127
195
|
]
|
|
@@ -130,14 +198,28 @@
|
|
|
130
198
|
"anyOf": [
|
|
131
199
|
{
|
|
132
200
|
"type": "string",
|
|
133
|
-
"enum": [
|
|
201
|
+
"enum": [
|
|
202
|
+
"allow",
|
|
203
|
+
"log",
|
|
204
|
+
"reply",
|
|
205
|
+
"redact",
|
|
206
|
+
"drop",
|
|
207
|
+
"recall"
|
|
208
|
+
]
|
|
134
209
|
},
|
|
135
210
|
{
|
|
136
211
|
"type": "array",
|
|
137
212
|
"minItems": 1,
|
|
138
213
|
"items": {
|
|
139
214
|
"type": "string",
|
|
140
|
-
"enum": [
|
|
215
|
+
"enum": [
|
|
216
|
+
"allow",
|
|
217
|
+
"log",
|
|
218
|
+
"reply",
|
|
219
|
+
"redact",
|
|
220
|
+
"drop",
|
|
221
|
+
"recall"
|
|
222
|
+
]
|
|
141
223
|
}
|
|
142
224
|
}
|
|
143
225
|
]
|
|
@@ -146,14 +228,28 @@
|
|
|
146
228
|
"anyOf": [
|
|
147
229
|
{
|
|
148
230
|
"type": "string",
|
|
149
|
-
"enum": [
|
|
231
|
+
"enum": [
|
|
232
|
+
"allow",
|
|
233
|
+
"log",
|
|
234
|
+
"reply",
|
|
235
|
+
"redact",
|
|
236
|
+
"drop",
|
|
237
|
+
"recall"
|
|
238
|
+
]
|
|
150
239
|
},
|
|
151
240
|
{
|
|
152
241
|
"type": "array",
|
|
153
242
|
"minItems": 1,
|
|
154
243
|
"items": {
|
|
155
244
|
"type": "string",
|
|
156
|
-
"enum": [
|
|
245
|
+
"enum": [
|
|
246
|
+
"allow",
|
|
247
|
+
"log",
|
|
248
|
+
"reply",
|
|
249
|
+
"redact",
|
|
250
|
+
"drop",
|
|
251
|
+
"recall"
|
|
252
|
+
]
|
|
157
253
|
}
|
|
158
254
|
}
|
|
159
255
|
]
|
|
@@ -162,14 +258,28 @@
|
|
|
162
258
|
"anyOf": [
|
|
163
259
|
{
|
|
164
260
|
"type": "string",
|
|
165
|
-
"enum": [
|
|
261
|
+
"enum": [
|
|
262
|
+
"allow",
|
|
263
|
+
"log",
|
|
264
|
+
"reply",
|
|
265
|
+
"redact",
|
|
266
|
+
"drop",
|
|
267
|
+
"recall"
|
|
268
|
+
]
|
|
166
269
|
},
|
|
167
270
|
{
|
|
168
271
|
"type": "array",
|
|
169
272
|
"minItems": 1,
|
|
170
273
|
"items": {
|
|
171
274
|
"type": "string",
|
|
172
|
-
"enum": [
|
|
275
|
+
"enum": [
|
|
276
|
+
"allow",
|
|
277
|
+
"log",
|
|
278
|
+
"reply",
|
|
279
|
+
"redact",
|
|
280
|
+
"drop",
|
|
281
|
+
"recall"
|
|
282
|
+
]
|
|
173
283
|
}
|
|
174
284
|
}
|
|
175
285
|
]
|
|
@@ -178,18 +288,37 @@
|
|
|
178
288
|
"anyOf": [
|
|
179
289
|
{
|
|
180
290
|
"type": "string",
|
|
181
|
-
"enum": [
|
|
291
|
+
"enum": [
|
|
292
|
+
"allow",
|
|
293
|
+
"log",
|
|
294
|
+
"reply",
|
|
295
|
+
"redact",
|
|
296
|
+
"drop",
|
|
297
|
+
"recall"
|
|
298
|
+
]
|
|
182
299
|
},
|
|
183
300
|
{
|
|
184
301
|
"type": "array",
|
|
185
302
|
"minItems": 1,
|
|
186
303
|
"items": {
|
|
187
304
|
"type": "string",
|
|
188
|
-
"enum": [
|
|
305
|
+
"enum": [
|
|
306
|
+
"allow",
|
|
307
|
+
"log",
|
|
308
|
+
"reply",
|
|
309
|
+
"redact",
|
|
310
|
+
"drop",
|
|
311
|
+
"recall"
|
|
312
|
+
]
|
|
189
313
|
}
|
|
190
314
|
}
|
|
191
315
|
]
|
|
192
316
|
}
|
|
317
|
+
},
|
|
318
|
+
"x-i18n": {
|
|
319
|
+
"en": {
|
|
320
|
+
"description": "Severity-to-action mapping (string or array); uses the stricter default mapping when omitted."
|
|
321
|
+
}
|
|
193
322
|
}
|
|
194
323
|
},
|
|
195
324
|
"sources": {
|
|
@@ -197,17 +326,43 @@
|
|
|
197
326
|
"default": [],
|
|
198
327
|
"items": {
|
|
199
328
|
"type": "object",
|
|
200
|
-
"required": [
|
|
329
|
+
"required": [
|
|
330
|
+
"id",
|
|
331
|
+
"type"
|
|
332
|
+
],
|
|
201
333
|
"additionalProperties": false,
|
|
202
334
|
"properties": {
|
|
203
|
-
"id": {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
335
|
+
"id": {
|
|
336
|
+
"type": "string",
|
|
337
|
+
"minLength": 1
|
|
338
|
+
},
|
|
339
|
+
"type": {
|
|
340
|
+
"type": "string",
|
|
341
|
+
"enum": [
|
|
342
|
+
"local",
|
|
343
|
+
"http"
|
|
344
|
+
]
|
|
345
|
+
},
|
|
346
|
+
"enabled": {
|
|
347
|
+
"type": "boolean",
|
|
348
|
+
"default": true
|
|
349
|
+
},
|
|
350
|
+
"onError": {
|
|
351
|
+
"type": "string",
|
|
352
|
+
"enum": [
|
|
353
|
+
"open",
|
|
354
|
+
"closed"
|
|
355
|
+
]
|
|
356
|
+
},
|
|
207
357
|
"includeBuiltin": {
|
|
208
358
|
"type": "boolean",
|
|
209
359
|
"default": true,
|
|
210
|
-
"description": "是否合并内置分级违禁词词库"
|
|
360
|
+
"description": "是否合并内置分级违禁词词库",
|
|
361
|
+
"x-i18n": {
|
|
362
|
+
"en": {
|
|
363
|
+
"description": "Include the built-in graded blocked-word dictionary."
|
|
364
|
+
}
|
|
365
|
+
}
|
|
211
366
|
},
|
|
212
367
|
"words": {
|
|
213
368
|
"type": "array",
|
|
@@ -215,45 +370,97 @@
|
|
|
215
370
|
"description": "自定义词:字符串(用 defaultSeverity)或 { word, severity }",
|
|
216
371
|
"items": {
|
|
217
372
|
"anyOf": [
|
|
218
|
-
{
|
|
373
|
+
{
|
|
374
|
+
"type": "string",
|
|
375
|
+
"minLength": 1
|
|
376
|
+
},
|
|
219
377
|
{
|
|
220
378
|
"type": "object",
|
|
221
379
|
"additionalProperties": false,
|
|
222
|
-
"required": [
|
|
380
|
+
"required": [
|
|
381
|
+
"word"
|
|
382
|
+
],
|
|
223
383
|
"properties": {
|
|
224
|
-
"word": {
|
|
225
|
-
|
|
384
|
+
"word": {
|
|
385
|
+
"type": "string",
|
|
386
|
+
"minLength": 1
|
|
387
|
+
},
|
|
388
|
+
"text": {
|
|
389
|
+
"type": "string",
|
|
390
|
+
"minLength": 1
|
|
391
|
+
},
|
|
226
392
|
"severity": {
|
|
227
393
|
"type": "string",
|
|
228
|
-
"enum": [
|
|
394
|
+
"enum": [
|
|
395
|
+
"low",
|
|
396
|
+
"medium",
|
|
397
|
+
"high",
|
|
398
|
+
"critical"
|
|
399
|
+
]
|
|
229
400
|
}
|
|
230
401
|
}
|
|
231
402
|
}
|
|
232
403
|
]
|
|
404
|
+
},
|
|
405
|
+
"x-i18n": {
|
|
406
|
+
"en": {
|
|
407
|
+
"description": "Custom words: strings using defaultSeverity, or { word, severity } objects."
|
|
408
|
+
}
|
|
233
409
|
}
|
|
234
410
|
},
|
|
235
411
|
"wordFiles": {
|
|
236
412
|
"type": "array",
|
|
237
|
-
"items": {
|
|
413
|
+
"items": {
|
|
414
|
+
"type": "string"
|
|
415
|
+
},
|
|
238
416
|
"default": [],
|
|
239
|
-
"description": "词库文件;行格式 word / severity:word / word|severity"
|
|
417
|
+
"description": "词库文件;行格式 word / severity:word / word|severity",
|
|
418
|
+
"x-i18n": {
|
|
419
|
+
"en": {
|
|
420
|
+
"description": "Dictionary file; line formats: word, severity:word, or word|severity."
|
|
421
|
+
}
|
|
422
|
+
}
|
|
240
423
|
},
|
|
241
424
|
"defaultSeverity": {
|
|
242
425
|
"type": "string",
|
|
243
|
-
"enum": [
|
|
426
|
+
"enum": [
|
|
427
|
+
"low",
|
|
428
|
+
"medium",
|
|
429
|
+
"high",
|
|
430
|
+
"critical"
|
|
431
|
+
],
|
|
244
432
|
"default": "high",
|
|
245
|
-
"description": "未标注分级的自定义词默认 severity"
|
|
433
|
+
"description": "未标注分级的自定义词默认 severity",
|
|
434
|
+
"x-i18n": {
|
|
435
|
+
"en": {
|
|
436
|
+
"description": "Default severity for custom words without an explicit severity."
|
|
437
|
+
}
|
|
438
|
+
}
|
|
246
439
|
},
|
|
247
440
|
"severity": {
|
|
248
441
|
"type": "string",
|
|
249
|
-
"enum": [
|
|
442
|
+
"enum": [
|
|
443
|
+
"low",
|
|
444
|
+
"medium",
|
|
445
|
+
"high",
|
|
446
|
+
"critical"
|
|
447
|
+
],
|
|
250
448
|
"default": "high",
|
|
251
|
-
"description": "兼容旧字段,等同 defaultSeverity"
|
|
449
|
+
"description": "兼容旧字段,等同 defaultSeverity",
|
|
450
|
+
"x-i18n": {
|
|
451
|
+
"en": {
|
|
452
|
+
"description": "Legacy alias for defaultSeverity."
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
"url": {
|
|
457
|
+
"type": "string"
|
|
252
458
|
},
|
|
253
|
-
"url": { "type": "string" },
|
|
254
459
|
"headers": {
|
|
255
460
|
"type": "object",
|
|
256
|
-
"additionalProperties": {
|
|
461
|
+
"additionalProperties": {
|
|
462
|
+
"type": "string"
|
|
463
|
+
}
|
|
257
464
|
},
|
|
258
465
|
"timeoutMs": {
|
|
259
466
|
"type": "number",
|
|
@@ -264,7 +471,12 @@
|
|
|
264
471
|
"forceUpload": {
|
|
265
472
|
"type": "boolean",
|
|
266
473
|
"default": false,
|
|
267
|
-
"description": "强制下载图片后上传,不传 URL"
|
|
474
|
+
"description": "强制下载图片后上传,不传 URL",
|
|
475
|
+
"x-i18n": {
|
|
476
|
+
"en": {
|
|
477
|
+
"description": "Download images before uploading them instead of passing the URL."
|
|
478
|
+
}
|
|
479
|
+
}
|
|
268
480
|
}
|
|
269
481
|
}
|
|
270
482
|
}
|
package/src/config.ts
CHANGED
|
@@ -22,7 +22,6 @@ export const DEFAULT_MODERATION_CONFIG: ModerationConfig = Object.freeze({
|
|
|
22
22
|
enabled: true,
|
|
23
23
|
onError: 'open',
|
|
24
24
|
maskChar: '*',
|
|
25
|
-
replyTemplate: '消息含不当内容,已拦截。',
|
|
26
25
|
masters: Object.freeze([] as string[]),
|
|
27
26
|
inbound: Object.freeze({
|
|
28
27
|
enabled: true,
|
|
@@ -51,7 +50,7 @@ export function resolveModerationConfig(raw: unknown): ModerationConfig {
|
|
|
51
50
|
: DEFAULT_MODERATION_CONFIG.maskChar;
|
|
52
51
|
const replyTemplate = typeof input.replyTemplate === 'string' && input.replyTemplate.length > 0
|
|
53
52
|
? input.replyTemplate
|
|
54
|
-
:
|
|
53
|
+
: undefined;
|
|
55
54
|
|
|
56
55
|
return Object.freeze({
|
|
57
56
|
groupModeration: Object.freeze((Array.isArray(input.groupModeration) ? input.groupModeration : [])
|
|
@@ -66,7 +65,7 @@ export function resolveModerationConfig(raw: unknown): ModerationConfig {
|
|
|
66
65
|
enabled: input.enabled !== false,
|
|
67
66
|
onError,
|
|
68
67
|
maskChar,
|
|
69
|
-
replyTemplate,
|
|
68
|
+
...(replyTemplate === undefined ? {} : { replyTemplate }),
|
|
70
69
|
masters: freezeStrings(input.masters),
|
|
71
70
|
inbound: Object.freeze({
|
|
72
71
|
enabled: inboundRaw.enabled !== false,
|
package/src/engine.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { createProviders } from './providers/registry.js';
|
|
|
6
6
|
import { GroupModerator } from './group-moderation.js';
|
|
7
7
|
import { GroupReviewQueue } from './group-review-queue.js';
|
|
8
8
|
import { generationAdmissionSource } from 'zhin.js/plugin-runtime';
|
|
9
|
+
import type { Locale } from 'zhin.js/i18n';
|
|
10
|
+
import { moderationMessages } from './messages.js';
|
|
9
11
|
import type { ModerationProvider } from './providers/types.js';
|
|
10
12
|
import {
|
|
11
13
|
maxSeverity,
|
|
@@ -100,6 +102,7 @@ export class ModerationEngine {
|
|
|
100
102
|
|
|
101
103
|
async apply(options: {
|
|
102
104
|
readonly direction: Direction;
|
|
105
|
+
readonly locale?: Locale;
|
|
103
106
|
readonly extracted: ExtractedContent;
|
|
104
107
|
readonly scanInput: ScanInput;
|
|
105
108
|
readonly hooks: ApplyHooks;
|
|
@@ -147,7 +150,7 @@ export class ModerationEngine {
|
|
|
147
150
|
|
|
148
151
|
if (actionSet.has('reply') && options.direction === 'inbound') {
|
|
149
152
|
try {
|
|
150
|
-
await options.hooks.reply?.(this.#config.replyTemplate);
|
|
153
|
+
await options.hooks.reply?.(this.#config.replyTemplate ?? moderationMessages(options.locale, 'blocked'));
|
|
151
154
|
} catch (error) {
|
|
152
155
|
this.#logger.warn(formatCompact({
|
|
153
156
|
op: 'moderation_reply_failed',
|