# PROGRESS · 敏感内容审核 · 2026-09-30 > 合规轮四项之一(产品 2026-09-30「合规与法律的前四个你可以帮我做吗」)。执行:Claude fork D,直接执行。分支 `codex/content-moderation-20260930`,基于 `codex/compliance-base-20260930`(= origin/staging `5208f19b` + `lib/legal-entity.ts`)。 > 产品决定:输入命中 → 不发模型、不扣点、固定一句;输出命中 → 整段替换并退点;命中留脱敏日志。 ## 做了什么 | 部分 | 位置 | 说明 | |---|---|---| | 审核接口 | `src/lib/moderation/index.ts` | `moderate(text, side, context)` → `{verdict, categories, ruleIds, providerId}`;本地词表先跑,命中即终判;配置了阿里云再跑远端(≤1.5 s);远端失败/超时 → 沿用本地判定(输入:本地兜底不放空;输出:不因服务不可用而扣下回答) | | 本地词表 | `src/lib/moderation/lexicon.ts` | 12 条规则、5 类(政治 3 / 暴恐 3 / 涉未成年色情 1 / 自伤 2 / 违法交易 3),均为多字短语;归一化去空格标点零宽、全角转半角 | | 阿里云适配 | `src/lib/moderation/aliyun-provider.ts` | TextModerationPlus(llm_query_moderation / llm_response_moderation),RPC 签名 v1;high → 拦,medium → 待复核(放行并记录);endpoint 限定 `*.aliyuncs.com`;超 2000 字分段,最多 4 段 | | 日志 | `src/lib/moderation/log.ts` + 迁移 `20260930040000_moderation_events.sql` | 只记类别、规则编号/服务标签、方向、入口、模型、request、user_id;不存原文;仅 service_role | | 普通咨询 | `app/api/consult/route.ts` | 输入:在 prompt-extraction 守卫之后、预留点数之前;输出:三条 agentic 流 `moderateOutput` + `completeResponse` 兜底(覆盖旧 text/plain 运行时);命中走新函数 `complete_consultation_moderated`(复制自免费结算,释放预留) | | 流式替换 | `lib/stream-agent-response.ts`、`lib/consultation-agent-events.ts`、`lib/consultation-run-timeline.ts`、`hooks/use-consultation-run.ts` | `answer.delta` 增加可选 `replace: true`;客户端时间线与发送钩子遇到即整段替换 | | 生时校正 | `v9/agent-route-request.ts`(输入)、`v9/agent-run-finish.ts`(输出) | 输出命中:`skipBilling` → `billing.release()`,spoken 换成撤下提示,走既有 replace | | 出生时间引导 | `app/api/birth-time-guide/route.ts` | `draft_evidence.message` / `reframe_unmatched.note` 输入审核 | | 后台 | `app/api/admin/moderation-events`、`components/admin/moderation-events-resource.tsx`、菜单「内容审核」 | 只读、`audit.read`、14 天分类计数 | 未接入(无用户自由文本进模型):报告生成、今日星语、合盘、星盘 / 星历。Python API 未发现把用户自由文本送进模型的端点(模型调用都在 Next.js 侧)。 ## 既有断言改动(原值 / 新值 / 原因) | 文件 | 原值 | 新值 | 原因 | |---|---|---|---| | `tests/test_api_server_security.py` app_routes 列表 | admin/models 后直接 admin/orders | 中间加 admin/moderation-events | 新增只读后台页 | | `frontend/tests/admin-contracts.test.ts` 只读资源 | 四个 | 加 moderation-events | 同一只读约束覆盖新页 | ## 验证(Claude fork 实跑,Linux,Node 22.14) | 项 | 结果 | |---|---| | `tsc --noEmit` | 0 错 | | `npm run lint` | 0 error,126 warning(同基线) | | 新增 `content-moderation.test.ts` | 10/10:占星用语 8 句全部放行;五类各自拦截含空格/全角/零宽规避;自伤给求助句;阿里云 high→拦、medium→待复核、none→放行,签名与表单校验;本地拦截不调远端;远端超时(50 ms)/500 → 沿用本地;无钥匙/非 aliyuncs.com 只用本地且只警告一次;日志不含原文、写失败不抛;流式回答被整段替换且 onComplete 收到提示句、时间线最终文本为提示句;路由里输入审核在预留点数之前、校正输出审核在结算之前 | | `npm test` 全量 | 4,428 / fail 26。与基线(24 条环境失败)相比多 2 条:`a lookup does not reset the answer clock…` 与 `a refresh inside a rectification conversation stays in it…`,均为并发负载下超时(后者等待 16.8 s 超时),单独各跑两次全部通过,且不涉及本次改动文件。新增 10 条,消失 0 条 | | `next build` | `/` 仍 `○ Static`;首屏 gzip-9 646,505 B(基线 646,480 B,+25 B) | | Python | `tests/test_api_server_security.py::test_capability_audit_scans_registry_and_local_sources` 通过;快速门 Python 步 1000 passed / 2 skipped(门内 `npm test` 步走系统 Node 20,属已知环境失败) | 未验证:阿里云真实调用;迁移在真实 PostgreSQL 上执行(无 Docker);浏览器中被拦与被替换的表现;遗留 text/plain 运行时(无法就地替换,只保证存档与结算为提示句)。