@zhin.js/plugin-content-moderation 1.0.0

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.
Files changed (68) hide show
  1. package/README.md +173 -0
  2. package/lib/bypass.d.ts +10 -0
  3. package/lib/bypass.d.ts.map +1 -0
  4. package/lib/bypass.js +27 -0
  5. package/lib/bypass.js.map +1 -0
  6. package/lib/config.d.ts +5 -0
  7. package/lib/config.d.ts.map +1 -0
  8. package/lib/config.js +184 -0
  9. package/lib/config.js.map +1 -0
  10. package/lib/engine.d.ts +40 -0
  11. package/lib/engine.d.ts.map +1 -0
  12. package/lib/engine.js +170 -0
  13. package/lib/engine.js.map +1 -0
  14. package/lib/extract.d.ts +18 -0
  15. package/lib/extract.d.ts.map +1 -0
  16. package/lib/extract.js +121 -0
  17. package/lib/extract.js.map +1 -0
  18. package/lib/index.d.ts +13 -0
  19. package/lib/index.d.ts.map +1 -0
  20. package/lib/index.js +11 -0
  21. package/lib/index.js.map +1 -0
  22. package/lib/providers/builtin-lexicon.d.ts +15 -0
  23. package/lib/providers/builtin-lexicon.d.ts.map +1 -0
  24. package/lib/providers/builtin-lexicon.js +133 -0
  25. package/lib/providers/builtin-lexicon.js.map +1 -0
  26. package/lib/providers/http.d.ts +14 -0
  27. package/lib/providers/http.d.ts.map +1 -0
  28. package/lib/providers/http.js +191 -0
  29. package/lib/providers/http.js.map +1 -0
  30. package/lib/providers/local-lexicon.d.ts +28 -0
  31. package/lib/providers/local-lexicon.d.ts.map +1 -0
  32. package/lib/providers/local-lexicon.js +129 -0
  33. package/lib/providers/local-lexicon.js.map +1 -0
  34. package/lib/providers/registry.d.ts +8 -0
  35. package/lib/providers/registry.d.ts.map +1 -0
  36. package/lib/providers/registry.js +24 -0
  37. package/lib/providers/registry.js.map +1 -0
  38. package/lib/providers/types.d.ts +6 -0
  39. package/lib/providers/types.d.ts.map +1 -0
  40. package/lib/providers/types.js +2 -0
  41. package/lib/providers/types.js.map +1 -0
  42. package/lib/redact.d.ts +18 -0
  43. package/lib/redact.d.ts.map +1 -0
  44. package/lib/redact.js +102 -0
  45. package/lib/redact.js.map +1 -0
  46. package/lib/types.d.ts +105 -0
  47. package/lib/types.d.ts.map +1 -0
  48. package/lib/types.js +26 -0
  49. package/lib/types.js.map +1 -0
  50. package/middlewares/inbound.js +97 -0
  51. package/middlewares/inbound.ts +112 -0
  52. package/middlewares/outbound.js +53 -0
  53. package/middlewares/outbound.ts +66 -0
  54. package/package.json +81 -0
  55. package/plugin.js +8 -0
  56. package/schema.json +240 -0
  57. package/src/bypass.ts +38 -0
  58. package/src/config.ts +203 -0
  59. package/src/engine.ts +220 -0
  60. package/src/extract.ts +155 -0
  61. package/src/index.ts +49 -0
  62. package/src/providers/builtin-lexicon.ts +149 -0
  63. package/src/providers/http.ts +225 -0
  64. package/src/providers/local-lexicon.ts +148 -0
  65. package/src/providers/registry.ts +37 -0
  66. package/src/providers/types.ts +6 -0
  67. package/src/redact.ts +141 -0
  68. package/src/types.ts +143 -0
package/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # @zhin.js/plugin-content-moderation
2
+
3
+ 内容安全审查插件 —— 入站 / 出站多源扫描,按可配置 severity→动作表处置。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install @zhin.js/plugin-content-moderation
9
+ ```
10
+
11
+ ## 配置
12
+
13
+ 在 `zhin.config.yml` 中添加:
14
+
15
+ ```yaml
16
+ plugins:
17
+ - "@zhin.js/plugin-content-moderation"
18
+
19
+ content-moderation:
20
+ enabled: true
21
+ onError: open # 源失败默认:open | closed
22
+ maskChar: "*"
23
+ replyTemplate: "消息含不当内容,已拦截。"
24
+ masters: [] # 额外 master userId(兜底)
25
+ inbound:
26
+ enabled: true
27
+ bypassMasters: true
28
+ whitelist:
29
+ userIds: []
30
+ conversationIds: []
31
+ outbound:
32
+ enabled: true
33
+ bypass: false # true 才跳过出站审查
34
+ actions: # 可整表覆盖;缺省偏严
35
+ pass: allow
36
+ low: log
37
+ medium: redact
38
+ high: drop
39
+ critical: [drop, recall]
40
+ sources:
41
+ - id: local
42
+ type: local
43
+ enabled: true
44
+ includeBuiltin: true # 默认 true:合并内置分级词库
45
+ defaultSeverity: high # 未标注分级的自定义词默认等级(兼容旧字段 severity)
46
+ words:
47
+ - 自定义敏感词 # → defaultSeverity
48
+ - word: 擦边广告
49
+ severity: low
50
+ - word: 诈骗话术
51
+ severity: high
52
+ # wordFiles: ["./data/bad-words.txt"]
53
+ # 词库文件行:word / severity:word / word|severity
54
+ # - id: http-1
55
+ # type: http
56
+ # url: "https://example.com/moderate"
57
+ # onError: closed
58
+ # timeoutMs: 5000outbound
59
+ # forceUpload: false
60
+ ```
61
+
62
+
63
+
64
+ ## 动作说明(`actions`)
65
+
66
+ 扫描得到最终 severity 后,查 `actions.<severity>` 得到一个动作或动作列表(可叠加)。
67
+ 不调用 `next()` = 中断中间件链:入站不进入命令/Agent;出站不发给平台。
68
+
69
+
70
+ | 动作 | 入站(inbound) | 出站(outbound) |
71
+ | -------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
72
+ | `allow` | 放行(调用 `next()`) | 放行 |
73
+ | `log` | 打结构化审计日志后**继续**(可与其它动作叠加;非 `pass` 时引擎也会记一条) | 同左 |
74
+ | `reply` | 用 `replyTemplate` 回复发送者;是否继续取决于是否同时有 `drop` / `redact` | **不适用**(忽略并 debug 日志) |
75
+ | `redact` | Runtime `Message` 只读,**无法改写原文**;仍 **正常** `next()` **放行**(可与 `reply` 叠加做提示)。若要拦截请用 `drop` | 对 payload **真打码**:命中 span 用 `maskChar` 替换;无 span 则整段打码;违规图段删除;然后 `envelope.replace` 并**继续发送** |
76
+ | `drop` | 丢弃:不 `next()`,命令/Agent 看不到该消息 | 丢弃:不 `next()`,消息不会发出 |
77
+ | `recall` | 尝试撤回原消息(`OutboundHost.recall`);无 message id / 平台不支持 → 降级并 warn | 发送前无意义,**忽略** |
78
+
79
+
80
+
81
+
82
+ ### 叠加与中断规则
83
+
84
+ - 配置写成数组即可叠加,例如 `critical: [drop, recall]`:先按列表执行副作用,再决定是否放行。
85
+ - **中断(不** `next()`**)**:含 `drop`;或入站含 `recall`(且无 `allow`)。
86
+ - **继续**:`allow` / `log` / `reply` / `redact`(入站 redact 不改原文但放行;出站 redact 先 `replace`)。
87
+
88
+
89
+
90
+ ### 偏严默认表
91
+
92
+
93
+ | severity | 默认动作 | 实际效果简述 |
94
+ | ---------- | ---------------- | --------------------------- |
95
+ | `pass` | `allow` | 放行 |
96
+ | `low` | `log` | 只记日志,照常处理 |
97
+ | `medium` | `redact` | 入站放行(不改原文);出站打码后发出 |
98
+ | `high` | `drop` | 入站丢弃;出站不发 |
99
+ | `critical` | `[drop, recall]` | 入站丢弃并尽量撤回;出站不发(`recall` 忽略) |
100
+
101
+
102
+ 示例:入站 medium 想「提示但仍放行」,可改成:
103
+
104
+ ```yaml
105
+ actions:
106
+ medium: [redact, reply]
107
+ ```
108
+
109
+ 若入站要拦截,请用 `drop`(或 `high` 默认表)。
110
+
111
+ ## 本地词库分级
112
+
113
+
114
+ | 等级 | 内置示例(节选) | 默认动作 |
115
+ | ---------- | --------- | ----------------- |
116
+ | `low` | 轻度不当用语 | `log` |
117
+ | `medium` | 辱骂粗口 | `redact` |
118
+ | `high` | 色情/诈骗引流;中国政治敏感(台独/藏独、六四相关、法轮功等) | `drop` |
119
+ | `critical` | 毒品/极端违法;煽动暴力颠覆 | `drop` + `recall` |
120
+
121
+
122
+ 命中多词时取**最高** severity。自定义词可覆盖同词更高等级。不需要政治词库时设 `includeBuiltin: false` 并自建 `words` / `wordFiles`。
123
+
124
+ ## 其它行为
125
+
126
+
127
+ | 方向 | 扩展点 | 说明 |
128
+ | --- | ------------------------------- | -------------------------------- |
129
+ | 入站 | `middlewares` `target: inbound` | master / 白名单可跳过 |
130
+ | 出站 | `target: outbound` | 默认仍审;`outbound.bypass: true` 才跳过 |
131
+
132
+
133
+ - 多源全跑,取最高 severity。
134
+ - 图片:HTTP 源优先传 URL;不可达或 `forceUpload` 时下载后上传。
135
+ - 审计:仅结构化 logger(无落库)。
136
+
137
+
138
+
139
+ ## HTTP 源契约
140
+
141
+ `POST` JSON(或 multipart):
142
+
143
+ ```json
144
+ {
145
+ "text": "...",
146
+ "images": [{ "url": "https://..." }],
147
+ "direction": "inbound",
148
+ "context": {
149
+ "adapter": "...",
150
+ "endpoint": "...",
151
+ "conversationKind": "group",
152
+ "conversationId": "...",
153
+ "sender": "..."
154
+ }
155
+ }
156
+ ```
157
+
158
+ 响应:
159
+
160
+ ```json
161
+ {
162
+ "severity": "medium",
163
+ "matches": [{ "start": 0, "end": 2 }],
164
+ "images": [{ "index": 0, "flagged": true }],
165
+ "reason": "optional"
166
+ }
167
+ ```
168
+
169
+
170
+
171
+ ## 许可
172
+
173
+ MIT
@@ -0,0 +1,10 @@
1
+ import type { ModerationConfig } from './types.js';
2
+ export interface BypassInput {
3
+ readonly sender?: string;
4
+ readonly conversationId: string;
5
+ /** Endpoint / plugin-declared masters already resolved by caller. */
6
+ readonly endpointMasters?: readonly string[];
7
+ }
8
+ export declare function shouldBypassInbound(config: ModerationConfig, input: BypassInput): boolean;
9
+ export declare function shouldBypassOutbound(config: ModerationConfig): boolean;
10
+ //# sourceMappingURL=bypass.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bypass.d.ts","sourceRoot":"","sources":["../src/bypass.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,qEAAqE;IACrE,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9C;AAED,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,gBAAgB,EACxB,KAAK,EAAE,WAAW,GACjB,OAAO,CAoBT;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAGtE"}
package/lib/bypass.js ADDED
@@ -0,0 +1,27 @@
1
+ export function shouldBypassInbound(config, input) {
2
+ if (!config.enabled || !config.inbound.enabled)
3
+ return true;
4
+ const sender = input.sender?.trim();
5
+ if (sender) {
6
+ if (config.inbound.whitelist.userIds.includes(sender))
7
+ return true;
8
+ if (config.inbound.bypassMasters) {
9
+ const masters = new Set([
10
+ ...config.masters,
11
+ ...(input.endpointMasters ?? []),
12
+ ].map(String));
13
+ if (masters.has(sender))
14
+ return true;
15
+ }
16
+ }
17
+ if (config.inbound.whitelist.conversationIds.includes(input.conversationId)) {
18
+ return true;
19
+ }
20
+ return false;
21
+ }
22
+ export function shouldBypassOutbound(config) {
23
+ if (!config.enabled || !config.outbound.enabled)
24
+ return true;
25
+ return config.outbound.bypass === true;
26
+ }
27
+ //# sourceMappingURL=bypass.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bypass.js","sourceRoot":"","sources":["../src/bypass.ts"],"names":[],"mappings":"AASA,MAAM,UAAU,mBAAmB,CACjC,MAAwB,EACxB,KAAkB;IAElB,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAE5D,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC;IACpC,IAAI,MAAM,EAAE,CAAC;QACX,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,OAAO,IAAI,CAAC;QACnE,IAAI,MAAM,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC;YACjC,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC;gBACtB,GAAG,MAAM,CAAC,OAAO;gBACjB,GAAG,CAAC,KAAK,CAAC,eAAe,IAAI,EAAE,CAAC;aACjC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;YACf,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC;gBAAE,OAAO,IAAI,CAAC;QACvC,CAAC;IACH,CAAC;IAED,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,eAAe,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,EAAE,CAAC;QAC5E,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,MAAwB;IAC3D,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC7D,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,KAAK,IAAI,CAAC;AACzC,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { type Action, type ModerationConfig, type Severity } from './types.js';
2
+ export declare const DEFAULT_ACTIONS: Readonly<Record<Severity, readonly Action[]>>;
3
+ export declare const DEFAULT_MODERATION_CONFIG: ModerationConfig;
4
+ export declare function resolveModerationConfig(raw: unknown): ModerationConfig;
5
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,MAAM,EAGX,KAAK,gBAAgB,EAErB,KAAK,QAAQ,EAEd,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC,CAMxE,CAAC;AAEH,eAAO,MAAM,yBAAyB,EAAE,gBAoBtC,CAAC;AAEH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,gBAAgB,CAsCtE"}
package/lib/config.js ADDED
@@ -0,0 +1,184 @@
1
+ import { isAction, } from './types.js';
2
+ export const DEFAULT_ACTIONS = Object.freeze({
3
+ pass: Object.freeze(['allow']),
4
+ low: Object.freeze(['log']),
5
+ medium: Object.freeze(['redact']),
6
+ high: Object.freeze(['drop']),
7
+ critical: Object.freeze(['drop', 'recall']),
8
+ });
9
+ export const DEFAULT_MODERATION_CONFIG = Object.freeze({
10
+ enabled: true,
11
+ onError: 'open',
12
+ maskChar: '*',
13
+ replyTemplate: '消息含不当内容,已拦截。',
14
+ masters: Object.freeze([]),
15
+ inbound: Object.freeze({
16
+ enabled: true,
17
+ bypassMasters: true,
18
+ whitelist: Object.freeze({
19
+ userIds: Object.freeze([]),
20
+ conversationIds: Object.freeze([]),
21
+ }),
22
+ }),
23
+ outbound: Object.freeze({
24
+ enabled: true,
25
+ bypass: false,
26
+ }),
27
+ actions: DEFAULT_ACTIONS,
28
+ sources: Object.freeze([]),
29
+ });
30
+ export function resolveModerationConfig(raw) {
31
+ const input = raw && typeof raw === 'object' ? raw : {};
32
+ const inboundRaw = asRecord(input.inbound);
33
+ const outboundRaw = asRecord(input.outbound);
34
+ const whitelistRaw = asRecord(inboundRaw.whitelist);
35
+ const onError = parseOnError(input.onError, DEFAULT_MODERATION_CONFIG.onError);
36
+ const maskChar = typeof input.maskChar === 'string' && input.maskChar.length > 0
37
+ ? input.maskChar.slice(0, 4)
38
+ : DEFAULT_MODERATION_CONFIG.maskChar;
39
+ const replyTemplate = typeof input.replyTemplate === 'string' && input.replyTemplate.length > 0
40
+ ? input.replyTemplate
41
+ : DEFAULT_MODERATION_CONFIG.replyTemplate;
42
+ return Object.freeze({
43
+ enabled: input.enabled !== false,
44
+ onError,
45
+ maskChar,
46
+ replyTemplate,
47
+ masters: freezeStrings(input.masters),
48
+ inbound: Object.freeze({
49
+ enabled: inboundRaw.enabled !== false,
50
+ bypassMasters: inboundRaw.bypassMasters !== false,
51
+ whitelist: Object.freeze({
52
+ userIds: freezeStrings(whitelistRaw.userIds),
53
+ conversationIds: freezeStrings(whitelistRaw.conversationIds),
54
+ }),
55
+ }),
56
+ outbound: Object.freeze({
57
+ enabled: outboundRaw.enabled !== false,
58
+ bypass: outboundRaw.bypass === true,
59
+ }),
60
+ actions: resolveActions(input.actions),
61
+ sources: Object.freeze((Array.isArray(input.sources) ? input.sources : [])
62
+ .map((item) => parseSource(item, onError))
63
+ .filter((item) => item != null)),
64
+ });
65
+ }
66
+ function resolveActions(raw) {
67
+ const input = asRecord(raw);
68
+ return Object.freeze({
69
+ pass: resolveActionList(input.pass, DEFAULT_ACTIONS.pass),
70
+ low: resolveActionList(input.low, DEFAULT_ACTIONS.low),
71
+ medium: resolveActionList(input.medium, DEFAULT_ACTIONS.medium),
72
+ high: resolveActionList(input.high, DEFAULT_ACTIONS.high),
73
+ critical: resolveActionList(input.critical, DEFAULT_ACTIONS.critical),
74
+ });
75
+ }
76
+ function resolveActionList(raw, fallback) {
77
+ if (typeof raw === 'string' && isAction(raw))
78
+ return Object.freeze([raw]);
79
+ if (Array.isArray(raw)) {
80
+ const actions = raw.filter(isAction);
81
+ if (actions.length > 0)
82
+ return Object.freeze([...actions]);
83
+ }
84
+ return fallback;
85
+ }
86
+ function parseSource(raw, globalOnError) {
87
+ if (!raw || typeof raw !== 'object')
88
+ return null;
89
+ const input = raw;
90
+ const id = typeof input.id === 'string' ? input.id.trim() : '';
91
+ if (!id)
92
+ return null;
93
+ const enabled = input.enabled !== false;
94
+ const onError = parseOnError(input.onError, globalOnError);
95
+ if (input.type === 'local') {
96
+ const defaultSeverity = parseHitSeverity(input.defaultSeverity ?? input.severity);
97
+ const local = Object.freeze({
98
+ id,
99
+ type: 'local',
100
+ enabled,
101
+ onError,
102
+ words: freezeLexiconWords(input.words, defaultSeverity),
103
+ wordFiles: freezeStrings(input.wordFiles),
104
+ includeBuiltin: input.includeBuiltin !== false,
105
+ defaultSeverity,
106
+ });
107
+ return local;
108
+ }
109
+ if (input.type === 'http') {
110
+ const url = typeof input.url === 'string' ? input.url.trim() : '';
111
+ if (!url)
112
+ return null;
113
+ const headersRaw = asRecord(input.headers);
114
+ const headers = {};
115
+ for (const [key, value] of Object.entries(headersRaw)) {
116
+ if (typeof value === 'string')
117
+ headers[key] = value;
118
+ }
119
+ const timeoutMs = clampNumber(input.timeoutMs, 5_000, 500, 60_000);
120
+ const http = Object.freeze({
121
+ id,
122
+ type: 'http',
123
+ enabled,
124
+ onError,
125
+ url,
126
+ headers: Object.freeze(headers),
127
+ timeoutMs,
128
+ forceUpload: input.forceUpload === true,
129
+ });
130
+ return http;
131
+ }
132
+ return null;
133
+ }
134
+ function parseHitSeverity(raw) {
135
+ if (raw === 'low' || raw === 'medium' || raw === 'high' || raw === 'critical')
136
+ return raw;
137
+ return 'high';
138
+ }
139
+ function parseOnError(raw, fallback) {
140
+ return raw === 'open' || raw === 'closed' ? raw : fallback;
141
+ }
142
+ function asRecord(value) {
143
+ return value && typeof value === 'object' && !Array.isArray(value)
144
+ ? value
145
+ : {};
146
+ }
147
+ function freezeStrings(value) {
148
+ if (!Array.isArray(value))
149
+ return Object.freeze([]);
150
+ return Object.freeze(value.filter((item) => typeof item === 'string' && item.length > 0));
151
+ }
152
+ function freezeLexiconWords(value, defaultSeverity) {
153
+ if (!Array.isArray(value))
154
+ return Object.freeze([]);
155
+ const out = [];
156
+ for (const item of value) {
157
+ if (typeof item === 'string') {
158
+ const word = item.trim();
159
+ if (word)
160
+ out.push(Object.freeze({ word, severity: defaultSeverity }));
161
+ continue;
162
+ }
163
+ if (!item || typeof item !== 'object')
164
+ continue;
165
+ const record = item;
166
+ const word = typeof record.word === 'string'
167
+ ? record.word.trim()
168
+ : typeof record.text === 'string'
169
+ ? record.text.trim()
170
+ : '';
171
+ if (!word)
172
+ continue;
173
+ out.push(Object.freeze({
174
+ word,
175
+ severity: parseHitSeverity(record.severity ?? defaultSeverity),
176
+ }));
177
+ }
178
+ return Object.freeze(out);
179
+ }
180
+ function clampNumber(value, fallback, min, max) {
181
+ const n = typeof value === 'number' && Number.isFinite(value) ? value : fallback;
182
+ return Math.min(max, Math.max(min, Math.trunc(n)));
183
+ }
184
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import { type Logger } from '@zhin.js/logger';
2
+ import { type ExtractedContent } from './extract.js';
3
+ import type { ModerationProvider } from './providers/types.js';
4
+ import { type Action, type Direction, type MergedResult, type ModerationConfig, type ProviderResult, type ScanInput, type Severity } from './types.js';
5
+ export interface ApplyHooks {
6
+ readonly reply?: (content: string) => Promise<void>;
7
+ readonly recall?: () => Promise<boolean>;
8
+ readonly replacePayload?: (payload: unknown) => void;
9
+ readonly getPayload?: () => unknown;
10
+ }
11
+ export interface ApplyResult {
12
+ readonly continue: boolean;
13
+ readonly severity: Severity;
14
+ readonly actions: readonly Action[];
15
+ readonly merged: MergedResult;
16
+ readonly redactedPayload?: unknown;
17
+ }
18
+ export declare class ModerationEngine {
19
+ #private;
20
+ constructor(options?: {
21
+ readonly logger?: Logger;
22
+ readonly cwd?: string;
23
+ readonly fetch?: typeof fetch;
24
+ });
25
+ get config(): ModerationConfig;
26
+ configure(raw: unknown): void;
27
+ /** Test helper: inject providers without going through config sources. */
28
+ setProvidersForTest(providers: readonly ModerationProvider[]): void;
29
+ scan(input: ScanInput): Promise<MergedResult>;
30
+ apply(options: {
31
+ readonly direction: Direction;
32
+ readonly extracted: ExtractedContent;
33
+ readonly scanInput: ScanInput;
34
+ readonly hooks: ApplyHooks;
35
+ }): Promise<ApplyResult>;
36
+ }
37
+ export declare function mergeResults(results: readonly ProviderResult[]): MergedResult;
38
+ export declare function getModerationEngine(): ModerationEngine;
39
+ export declare function resetModerationEngine(): void;
40
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,KAAK,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAExE,OAAO,EAAgB,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGnE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAEL,KAAK,MAAM,EACX,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,QAAQ,EACd,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACrD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC;CACrC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;CACpC;AAED,qBAAa,gBAAgB;;gBAOf,OAAO,GAAE;QACnB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;KAC1B;IAMN,IAAI,MAAM,IAAI,gBAAgB,CAE7B;IAED,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI;IAQ7B,0EAA0E;IAC1E,mBAAmB,CAAC,SAAS,EAAE,SAAS,kBAAkB,EAAE,GAAG,IAAI;IAI7D,IAAI,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,YAAY,CAAC;IA4B7C,KAAK,CAAC,OAAO,EAAE;QACnB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;QAC9B,QAAQ,CAAC,SAAS,EAAE,gBAAgB,CAAC;QACrC,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;QAC9B,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;KAC5B,GAAG,OAAO,CAAC,WAAW,CAAC;CA+EzB;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,GAAG,YAAY,CAmB7E;AAaD,wBAAgB,mBAAmB,IAAI,gBAAgB,CAGtD;AAED,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C"}
package/lib/engine.js ADDED
@@ -0,0 +1,170 @@
1
+ import { formatCompact, getLogger } from '@zhin.js/logger';
2
+ import { resolveModerationConfig } from './config.js';
3
+ import { mergeMatches } from './extract.js';
4
+ import { redactOutboundPayload } from './redact.js';
5
+ import { createProviders } from './providers/registry.js';
6
+ import { maxSeverity, } from './types.js';
7
+ export class ModerationEngine {
8
+ #logger;
9
+ #cwd;
10
+ #fetch;
11
+ #providers = Object.freeze([]);
12
+ #config = resolveModerationConfig({});
13
+ constructor(options = {}) {
14
+ this.#logger = options.logger ?? getLogger('content-moderation');
15
+ this.#cwd = options.cwd ?? process.cwd();
16
+ this.#fetch = options.fetch;
17
+ }
18
+ get config() {
19
+ return this.#config;
20
+ }
21
+ configure(raw) {
22
+ this.#config = resolveModerationConfig(raw);
23
+ this.#providers = createProviders(this.#config, {
24
+ cwd: this.#cwd,
25
+ fetch: this.#fetch,
26
+ });
27
+ }
28
+ /** Test helper: inject providers without going through config sources. */
29
+ setProvidersForTest(providers) {
30
+ this.#providers = Object.freeze([...providers]);
31
+ }
32
+ async scan(input) {
33
+ if (this.#providers.length === 0) {
34
+ return Object.freeze({
35
+ severity: 'pass',
36
+ matches: Object.freeze([]),
37
+ flaggedImageIndexes: Object.freeze([]),
38
+ sources: Object.freeze([]),
39
+ });
40
+ }
41
+ const results = await Promise.all(this.#providers.map(async (provider) => {
42
+ try {
43
+ return await provider.scan(input);
44
+ }
45
+ catch (error) {
46
+ return Object.freeze({
47
+ sourceId: provider.id,
48
+ severity: 'pass',
49
+ error: true,
50
+ reason: error instanceof Error ? error.message : String(error),
51
+ });
52
+ }
53
+ }));
54
+ return mergeResults(results);
55
+ }
56
+ async apply(options) {
57
+ const merged = await this.scan(options.scanInput);
58
+ const actions = [...this.#config.actions[merged.severity]];
59
+ const actionSet = new Set(actions);
60
+ if (merged.severity !== 'pass' || actionSet.has('log')) {
61
+ this.#logger.info(formatCompact({
62
+ op: 'moderation',
63
+ direction: options.direction,
64
+ severity: merged.severity,
65
+ actions: actions.join(','),
66
+ sources: merged.sources.map((s) => `${s.sourceId}:${s.severity}${s.error ? '!' : ''}`).join('|'),
67
+ conversation: options.scanInput.context.conversationId,
68
+ sender: options.scanInput.context.sender,
69
+ reason: merged.reason,
70
+ }));
71
+ }
72
+ if (actionSet.has('log') && merged.severity === 'pass') {
73
+ // already logged above when log is present
74
+ }
75
+ let redactedPayload;
76
+ if (actionSet.has('redact')) {
77
+ if (options.direction === 'outbound') {
78
+ const payload = options.hooks.getPayload?.();
79
+ redactedPayload = redactOutboundPayload(payload, options.extracted, {
80
+ maskChar: this.#config.maskChar,
81
+ matches: merged.matches,
82
+ flaggedImageIndexes: merged.flaggedImageIndexes,
83
+ });
84
+ options.hooks.replacePayload?.(redactedPayload);
85
+ }
86
+ // inbound redact:Message 只读,无法改写原文;仍 next() 放行
87
+ }
88
+ if (actionSet.has('reply') && options.direction === 'inbound') {
89
+ try {
90
+ await options.hooks.reply?.(this.#config.replyTemplate);
91
+ }
92
+ catch (error) {
93
+ this.#logger.warn(formatCompact({
94
+ op: 'moderation_reply_failed',
95
+ error: error instanceof Error ? error.message : String(error),
96
+ }));
97
+ }
98
+ }
99
+ else if (actionSet.has('reply') && options.direction === 'outbound') {
100
+ this.#logger.debug(formatCompact({
101
+ op: 'moderation_reply_skipped',
102
+ direction: 'outbound',
103
+ }));
104
+ }
105
+ if (actionSet.has('recall') && options.direction === 'inbound') {
106
+ try {
107
+ const ok = await options.hooks.recall?.();
108
+ if (!ok) {
109
+ this.#logger.warn(formatCompact({
110
+ op: 'moderation_recall_degraded',
111
+ reason: 'unsupported_or_missing_id',
112
+ }));
113
+ }
114
+ }
115
+ catch (error) {
116
+ this.#logger.warn(formatCompact({
117
+ op: 'moderation_recall_degraded',
118
+ error: error instanceof Error ? error.message : String(error),
119
+ }));
120
+ }
121
+ }
122
+ const stop = shouldStop(options.direction, actionSet);
123
+ return Object.freeze({
124
+ continue: !stop,
125
+ severity: merged.severity,
126
+ actions: Object.freeze(actions),
127
+ merged,
128
+ ...(redactedPayload !== undefined ? { redactedPayload } : {}),
129
+ });
130
+ }
131
+ }
132
+ export function mergeResults(results) {
133
+ let severity = 'pass';
134
+ const matches = mergeMatches(...results.map((r) => r.matches));
135
+ const flagged = new Set();
136
+ const reasons = [];
137
+ for (const result of results) {
138
+ severity = maxSeverity(severity, result.severity);
139
+ for (const idx of result.flaggedImageIndexes ?? [])
140
+ flagged.add(idx);
141
+ if (result.reason)
142
+ reasons.push(`${result.sourceId}: ${result.reason}`);
143
+ }
144
+ return Object.freeze({
145
+ severity,
146
+ matches,
147
+ flaggedImageIndexes: Object.freeze([...flagged].sort((a, b) => a - b)),
148
+ sources: Object.freeze([...results]),
149
+ ...(reasons.length ? { reason: reasons.join('; ') } : {}),
150
+ });
151
+ }
152
+ function shouldStop(direction, actions) {
153
+ if (actions.has('drop'))
154
+ return true;
155
+ // 入站 recall:尽量撤回后不再进入后续处理(无 allow 时中断)
156
+ if (direction === 'inbound' && actions.has('recall') && !actions.has('allow')) {
157
+ return true;
158
+ }
159
+ return false;
160
+ }
161
+ let sharedEngine = null;
162
+ export function getModerationEngine() {
163
+ if (!sharedEngine)
164
+ sharedEngine = new ModerationEngine();
165
+ return sharedEngine;
166
+ }
167
+ export function resetModerationEngine() {
168
+ sharedEngine = null;
169
+ }
170
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}