feat(compliance): add content compliance detection across tenant, ops, and clients

Implements the Revision 8/9 compliance design: tenant + ops backends, ops-web
content-safety pages, admin-web editor/publish gate integration, desktop publish
block surfacing, and supporting migrations / shared types / config plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-05 20:48:14 +08:00
parent 81577b6154
commit 745cdd79cf
73 changed files with 12747 additions and 1892 deletions
+8 -15
View File
@@ -40,7 +40,7 @@
### 2.1 V1 包含
- ops 平台维护的全局词库、策略、租户级覆盖
- ops 平台维护的全局词库和全局策略
- 租户自维护的白名单(豁免)、黑名单(自家敏感词)
- 词典精确匹配 + 正则匹配 + 可选 LLM 语义兜底(开关控制)
- 编辑器手动一键检测、发布器入口拦截、纯文本 API 三种调用入口
@@ -193,7 +193,7 @@ CREATE TABLE compliance_dictionaries (
CREATE TABLE compliance_dictionary_terms (
id BIGSERIAL PRIMARY KEY,
dictionary_id BIGINT NOT NULL REFERENCES compliance_dictionaries(id) ON DELETE CASCADE,
match_type VARCHAR(16) NOT NULL, -- exact | regex | semantic_keyword
match_type VARCHAR(16) NOT NULL, -- exact | regex
pattern TEXT NOT NULL,
level_override VARCHAR(16), -- 该词单独覆盖等级
hint TEXT, -- 命中后给用户的修改建议
@@ -214,18 +214,16 @@ CREATE TABLE compliance_dictionary_versions (
UNIQUE(dictionary_id, version)
);
-- 平台默认策略(全局开关
-- 平台默认策略(全局单例
CREATE TABLE compliance_policies (
id BIGSERIAL PRIMARY KEY,
scope VARCHAR(16) NOT NULL, -- 'global' or 'tenant_override'
tenant_id BIGINT, -- scope=tenant_override 时填
id BIGINT PRIMARY KEY DEFAULT 1,
enforcement_mode VARCHAR(16) NOT NULL, -- mandatory | advisory | disabled
enabled_dictionaries JSONB NOT NULL, -- [{"code":"ad_law_extreme","level_floor":"high","rollout_percent":100}]
llm_judge_enabled BOOLEAN NOT NULL DEFAULT FALSE,
llm_categories JSONB, -- ["politics","medical_claim"]
updated_by BIGINT,
updated_at TIMESTAMPTZ NOT NULL,
UNIQUE(scope, tenant_id)
CONSTRAINT ck_compliance_policy_singleton CHECK (id = 1)
);
```
@@ -396,11 +394,8 @@ func (s *ComplianceService) GateForPublish(ctx context.Context, articleID, versi
config.compliance.enabled = false
│ ↓ 全站短路(kill-switch,运维专用)
└─ ops.compliance_policies(scope=global).enforcement_mode = disabled
↓ 默认关闭,但词库管理 UI 仍可访问
└─ ops.compliance_policies(scope=tenant_override).enforcement_mode = disabled
↓ 仅该租户关闭
└─ ops.compliance_policies(id=1).enforcement_mode = disabled
↓ 默认关闭,但词库管理 UI 仍可访问
```
前端也要感知运行时状态:编辑器进入时调一次 `GET /api/compliance/runtime-status`,返回 `{enabled, mode, llm_enabled, dict_versions}`,**引擎关闭时按钮直接隐藏(不是置灰)**,避免空跑误导用户。
@@ -435,7 +430,6 @@ config.compliance.enabled = false
| POST | `/api/ops/compliance/dictionaries/:id/publish` | 词库版本发布(bump version + 写 versions 快照) |
| POST | `/api/ops/compliance/dictionaries/:id/rollback` | 回滚到指定版本 |
| GET / PUT | `/api/ops/compliance/policy/global` | 全局策略:强制 / 自愿 / 关闭、启用哪些词库 |
| GET / PUT | `/api/ops/compliance/policy/tenants/:tenantId` | 租户级覆盖 |
| GET | `/api/ops/compliance/stats` | 命中分布、拦截率、误报率(来自 ack 数据) |
| GET | `/api/ops/compliance/records` | 全局检测记录搜索 |
@@ -473,7 +467,6 @@ apps/ops-web/src/views/compliance/
├── CompliancePolicyView.vue # 全局策略:模式开关 + 启用词库勾选
├── ComplianceDictionariesView.vue # 词库列表(含版本 / 启用 / 导入)
├── ComplianceDictionaryEditView.vue # 词条 CRUD + CSV 导入
├── ComplianceTenantOverrideView.vue # 租户级覆盖
├── ComplianceRecordsView.vue # 全局检测记录、误报率
└── ComplianceStatsView.vue # 命中 Top 词、拦截率、按租户分布
```
@@ -519,7 +512,7 @@ apps/ops-web/src/views/compliance/
| **富文本不踩坑** | 检测**只看纯文本**,但保留 dom_path 用于回写高亮;图片 OCR、链接落地页 V1 不做(前置说明)。 |
| **性能预算** | 编辑器手动按钮:纯词典 + 正则 P99 ≤ 50ms;含 LLM ≤ 1.5s。发布拦截:纯词典 + 正则 P99 ≤ 30ms(不含 LLMLLM 异步进 review 队列),总闸超时由 `cfg.Compliance.PublishGateTimeout` 控制。 |
| **可观测** | 给 [`server/internal/shared/middleware`](../server/internal/shared/middleware) 加埋点:每次 check 的 `duration_ms / hit_count / level / source / dict_version` 进 metricsops 侧拉曲线。 |
| **审计日志** | 写词库、改策略、租户覆盖等管理操作通过现有 `auditlog.AsyncWriter`[`server/internal/shared/auditlog/async.go`](../server/internal/shared/auditlog/async.go))记录。 |
| **审计日志** | 写词库、改策略等管理操作通过现有 `auditlog.AsyncWriter`[`server/internal/shared/auditlog/async.go`](../server/internal/shared/auditlog/async.go))记录。 |
| **隐私** | check_records 默认只存命中片段(`matched_text`,截断 ≤ 50 字符)和偏移;全文不入审计。LLM 调用走现有 LLM client 的脱敏链路。 |
| **极限词库种子** | V1 ops 预装:①《广告法》极限词约 200 条 ② 政治敏感词(公开版) ③ 涉黄涉暴 ④ 通用违禁。提供 CSV 模板,运营可自行扩。种子数据走数据库迁移落地,避免运维额外操作。 |
| **kill-switch 可观测** | 配置短路时仍 emit 一条 metric `compliance_skipped_total{reason="config_disabled"}`,便于运维通过监控确认 kill-switch 已经生效。 |