moderation.js 3.94 KB
/**
 * `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)
    },
  }
}