moderation.js
3.94 KB
1
2
3
4
5
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
/**
* `createModeration` —— 装配层(§3 唯一工厂入口)。
*
* 只做装配 + 编排:把注入的 `rules` 与可选 `llmGuardPort` 接到三个能力上——
* `checkText`(敏感词/长度/格式闸)、`guardGeneration`(生成前置闸)、`asModerationPort`
* (供 llm 注入回调)。具体判定全在纯函数 `rules-check.js` / `decision.js` 与编排
* `guard.js`,本文件不藏判定逻辑(§6 SRP/可读性)。
*
* 边界纪律(架构红线):包内不读 `process.env`、不建任何顶层连接;规则与 Port 全部
* 由本工厂参数注入。
*
* @typedef {import('./rules-check.js').Rules} Rules
* @typedef {import('./guard.js').GuardPort} GuardPort
* @typedef {import('./guard.js').GuardResult} GuardResult
* @typedef {import('./port.js').ModerationPort} ModerationPort
*
* @typedef {object} CheckTextResult
* @property {'allow'|'block'|'flag'} decision
* @property {string[]} hits
* @property {string[]} reasons
*
* @typedef {object} Moderation
* @property {(args:{ text:string, kind?:string, policy?:string, region?:string }) =>
* Promise<CheckTextResult>} checkText
* @property {(args:{ text:string, context?:any, policy?:string, region?:string }) =>
* Promise<GuardResult>} guardGeneration
* @property {() => ModerationPort} asModerationPort
*/
import { ModerationError, ModerationErrorCode } from './errors.js'
import { resolveRules, runRulesCheck } from './rules-check.js'
import { runGuardGeneration } from './guard.js'
import { asModerationPort } from './port.js'
/**
* 工厂:唯一入口(§3)。`rules` 必注;`llmGuardPort` 可选(缺时 `guardGeneration`
* 降级 `rules-only`)。少注入 `rules` 在**装配时**即崩(验收 1,架构 §10.3 入口自检)。
*
* @param {object} [opts]
* @param {Rules} [opts.rules] 数据驱动的规则(敏感词/阈值/格式),运营可改(缺则装配时崩)
* @param {GuardPort} [opts.llmGuardPort] 可选注入:LLM 基础安全分类 Port
* @returns {Moderation}
*/
export function createModeration({ rules, llmGuardPort } = {}) {
if (!rules || typeof rules !== 'object') {
throw new ModerationError({
code: ModerationErrorCode.MISSING_RULES,
retryable: false,
message: 'createModeration 需要注入 rules(数据驱动的词表/阈值/格式规则)',
})
}
if (llmGuardPort !== undefined && llmGuardPort !== null) {
if (typeof llmGuardPort.classify !== 'function') {
throw new ModerationError({
code: ModerationErrorCode.GUARD_PORT_BAD_SHAPE,
retryable: false,
message: 'llmGuardPort 必须形如 { classify({ text, context, region }) }',
})
}
}
const deps = { rules, llmGuardPort: llmGuardPort || undefined }
/**
* 敏感词 / 长度 / 格式闸(验收 2)。用户输入与生成文本统一入口。
* @param {{ text?:string, kind?:string, policy?:string, region?:string }} [args]
* @returns {Promise<CheckTextResult>}
*/
async function checkText({ text, kind, policy } = {}) {
if (typeof text !== 'string') {
throw new ModerationError({
code: ModerationErrorCode.BAD_REQUEST,
retryable: false,
message: 'checkText 需要 string 类型的 text',
})
}
const resolved = resolveRules(rules, policy)
const r = runRulesCheck({ text, kind, rules: resolved })
return { decision: r.decision, hits: r.hits, reasons: r.reasons }
}
/**
* 生成链路前置闸(验收 3):先本地规则,再(若注入)调 Port,取最严合并。
* @param {{ text?:string, context?:any, policy?:string, region?:string }} [args]
* @returns {Promise<GuardResult>}
*/
async function guardGeneration(args = {}) {
return runGuardGeneration(deps, args)
}
return {
checkText,
guardGeneration,
/** @returns {ModerationPort} 供 @drama/llm 注入消费(验收 5)。 */
asModerationPort() {
return asModerationPort(guardGeneration)
},
}
}