/** * `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} checkText * @property {(args:{ text:string, context?:any, policy?:string, region?:string }) => * Promise} 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} */ 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} */ async function guardGeneration(args = {}) { return runGuardGeneration(deps, args) } return { checkText, guardGeneration, /** @returns {ModerationPort} 供 @drama/llm 注入消费(验收 5)。 */ asModerationPort() { return asModerationPort(guardGeneration) }, } }