/** * `guardGeneration` —— 生成链路**前置闸**编排(§4 职责)。 * * 这是一期产品要点:护栏发生在生成产物落库/回流**之前**,不是事后扫描。 * 流程:①先跑本地 `rules`(敏感词/长度/格式硬红线);②若注入了 `llmGuardPort`, * 再调它做基础安全类目(暴力/色情/仇恨/未成年)分类;③`decision.js` 取最严合并。 * * 基础安全类目判定**只来自 `llmGuardPort`**,本包不自训/自写分类模型(验收 4/7)。 * 未注入 `llmGuardPort` 时优雅降级为 `rules-only` 且如实标注,不假装做了 LLM 护栏 * (验收 1,§5 review 关注点 4)。 * * @typedef {object} GuardPort * @property {(args: { text:string, context?:any, region?:string }) => * Promise<{ categories?:string[], unsafe?:boolean, reasons?:string[] }> | { categories?:string[], unsafe?:boolean, reasons?:string[] } * } classify 薄封装厂商 moderation 端点 或 轻量提示词裁判 * * @typedef {object} GuardResult * @property {'allow'|'flag'|'block'} decision * @property {string[]} reasons * @property {string[]} categories * @property {'rules+llm'|'rules-only'} guardLevel */ import { ModerationError, ModerationErrorCode } from './errors.js' import { resolveRules, runRulesCheck } from './rules-check.js' import { mergeDecisions, guardLevelOf } from './decision.js' /** * 运行生成前置护栏。 * * @param {object} deps * @param {import('./rules-check.js').Rules} deps.rules 注入的规则 * @param {GuardPort} [deps.llmGuardPort] 可选注入的 LLM 护栏 Port * @param {object} [args] * @param {string} [args.text] * @param {any} [args.context] 生成上下文(喂给 llmGuardPort) * @param {string} [args.policy] 二期分级位(一期走默认 policy) * @param {string} [args.region] 二期区域位(一期透传给 Port,可忽略) * @returns {Promise} */ export async function runGuardGeneration({ rules, llmGuardPort }, { text, context, policy, region } = {}) { if (typeof text !== 'string') { throw new ModerationError({ code: ModerationErrorCode.BAD_REQUEST, retryable: false, message: 'guardGeneration 需要 string 类型的 text', }) } const resolved = resolveRules(rules, policy) // ① 本地规则(前置硬红线)。 const local = runRulesCheck({ text, kind: 'generated', rules: resolved }) /** @type {Array<{decision?:string, reasons?:string[], categories?:string[]}>} */ const sources = [{ decision: local.decision, reasons: local.reasons, categories: [] }] const hasLlm = Boolean(llmGuardPort && typeof llmGuardPort.classify === 'function') // ② LLM 基础安全类目(仅当注入了 Port)。 if (hasLlm && llmGuardPort) { let cls try { cls = await llmGuardPort.classify({ text, context, region }) } catch (err) { throw new ModerationError({ code: ModerationErrorCode.GUARD_PORT_FAILED, retryable: true, cause: err, message: `llmGuardPort.classify 调用失败: ${err instanceof Error ? err.message : String(err)}`, }) } if (!cls || typeof cls !== 'object') { throw new ModerationError({ code: ModerationErrorCode.GUARD_PORT_BAD_SHAPE, retryable: false, message: 'llmGuardPort.classify 必须返回 { categories, unsafe, reasons }', }) } const categories = Array.isArray(cls.categories) ? cls.categories : [] const llmReasons = Array.isArray(cls.reasons) ? cls.reasons : [] // unsafe=true → block(前置阻断回流);否则该路 allow。 sources.push({ decision: cls.unsafe ? 'block' : 'allow', reasons: llmReasons, categories, }) } // ③ 取最严合并。 const merged = mergeDecisions(sources) return { decision: merged.decision, reasons: merged.reasons, categories: merged.categories, guardLevel: guardLevelOf(hasLlm), } }