# V1 技术方案:内容合规检测模块(敏感词 / 广告法) > **⚠️ 本文档已归档(Superseded)** > > 当前权威设计已迁移到 superpowers 规范: > [`docs/superpowers/specs/2026-04-28-compliance-detection-design.md`](superpowers/specs/2026-04-28-compliance-detection-design.md) > > 新文档相对本文的差异: > > - 显式列出 Non-Goals 与 Open Questions > - 每个关键技术决策附 **Chosen / Why / Not chosen** > - 工时分期已移出 spec(plan 文档承担) > - 新增引擎熔断 fail-open / fail-closed 语义、ack 防伪、正则单条超时等遗漏点 > > 本文保留供回溯,新工作请以 superpowers spec 为准。 --- > 版本:v1(2026-04-28) > > 适用范围:省心推(仓库代号 geo-rankly)整套发布器内文章发布前的合规检测。 > 与现有架构关键耦合点:`server/internal/tenant/app/publish_job_service.go`、`server/internal/shared/llm/`、`server/internal/shared/config/`、`apps/admin-web` 编辑器与发布弹窗、`apps/ops-web` ops 控制台。 --- ## 1. 文档目标 落地一套覆盖"敏感词 + 广告法 + 行业违禁词"的内容合规检测系统,满足以下产品诉求: 1. 系统管理员(ops)可在控制台切换"强制检测"与"用户自愿检测"两种模式; 2. 强制模式下,发布器内命中违规的文章直接禁止发布; 3. 自愿模式下,发布器仅展示风险提示,不阻断发布; 4. 文章编辑器内提供"一键合规检测"按钮,任意时刻可手动触发; 5. 模块整体可由配置层硬关闭(kill-switch),凌驾于 ops 策略之上,便于运维快速止血; 6. 与现有租户隔离、AI 点数计费、ops/tenant 双 schema 体系无缝衔接。 --- ## 2. 产品边界 ### 2.1 V1 包含 - ops 平台维护的全局词库、策略、租户级覆盖 - 租户自维护的白名单(豁免)、黑名单(自家敏感词) - 词典精确匹配 + 正则匹配 + 可选 LLM 语义兜底(开关控制) - 编辑器手动一键检测、发布器入口拦截、纯文本 API 三种调用入口 - 检测记录留存(可审计、可申诉) - ops 控制台词库管理、策略管理、命中统计 ### 2.2 V1 不包含 - 图片 OCR 检测、外链落地页检测(占位接口预留,二期再上) - 实时流式检测(生成边写边查) - 自动改写违规词(仅给出修改建议) - 跨租户 ML 模型训练与个性化词库 - 词库 Redis pub/sub 秒级同步(一期用 30s 轮询版本号) --- ## 3. 设计目标与边界 | 目标 | 说明 | |---|---| | **双模式可切换** | ops 全局策略:`mandatory`(违规阻断发布) / `advisory`(仅提示) / `disabled`(关闭)。租户级可继承或覆盖(默认继承) | | **三处接入点** | ① 编辑器一键检测按钮(手动) ② 保存草稿后异步预扫(可选,P3) ③ 发布器入口拦截(强制模式) | | **检测分层** | 词库精确匹配 → 正则 / AC 自动机 → LLM 语义兜底(可关)。前两层毫秒级,第三层走 [`server/internal/shared/llm/client.go`](../server/internal/shared/llm/client.go) | | **分级处置** | `block`(禁发) / `high`(强警告) / `medium`(建议改) / `info`(提示),由词库条目自带等级,不在引擎硬编码 | | **租户隔离** | 词库分平台级(ops 维护)和租户级白 / 黑名单;检测记录打 `tenant_id`,与 articles 同一隔离模型(参考 [`server/migrations/20260331100011_create_articles.up.sql`](../server/migrations/20260331100011_create_articles.up.sql)) | | **词库热更新** | 启动全量加载 → 内存 AC 自动机 → 版本号 + 30s 轮询 reload;支持灰度(按租户 / 百分比) | | **配置硬开关** | `cfg.Compliance.Enabled = false` 时全站短路,门面层早返 `GatePass`,检测彻底不跑 | --- ## 4. 总体架构 ```mermaid flowchart LR subgraph admin-web[管理后台 admin-web] E1[文章编辑器
合规检测按钮] E2[发布弹窗
PublishArticleModal] end subgraph ops-web[运营控制台 ops-web] O1[策略管理] O2[词库管理] O3[检测记录与统计] end subgraph tenant-api[tenant API server] T1[ComplianceHandler] T2[ComplianceService
门面] T3[Engine
词典+正则+LLM] T4[PolicyResolver] T5[SnapshotLoader
词库快照] T6[PublishJobService
发布闸] end subgraph ops-api[ops API server] P1[DictionaryHandler] P2[PolicyHandler] P3[StatsHandler] end DB[(PostgreSQL
tenant + ops schema)] LLM[Shared LLM Client] E1 -->|POST /api/articles/:id/compliance:check| T1 E2 -->|发布前同步检测| T1 E2 -.提交发布.-> T6 T6 -->|GateForPublish| T2 T1 --> T2 T2 --> T4 T2 --> T3 T3 --> T5 T3 -. 可选 .-> LLM T5 -- 30s 轮询版本号 --> DB T2 -- 写检测记录 --> DB T4 --> DB O1 --> P2 O2 --> P1 O3 --> P3 P1 --> DB P2 --> DB P3 --> DB ``` --- ## 5. 模块分层 新增后端 domain:`server/internal/tenant/compliance/`(租户面)+ `server/internal/ops/compliance/`(平台面),沿用现有 `transport / app / repository / domain` 四层(参考 [`server/internal/tenant/app/article_service.go`](../server/internal/tenant/app/article_service.go))。 ``` server/internal/ ├── ops/compliance/ # ops 平台面:词库、策略管理 │ ├── transport/dictionary_handler.go │ ├── transport/policy_handler.go │ ├── app/dictionary_service.go │ ├── app/policy_service.go │ └── repository/... │ ├── tenant/compliance/ # 租户面:检测引擎、租户词库、记录 │ ├── transport/compliance_handler.go # 编辑器/发布器调用 │ ├── app/ │ │ ├── compliance_service.go # 对外门面,承担 kill-switch 短路 │ │ ├── engine/ # 检测引擎 │ │ │ ├── engine.go # 编排:词典 → 正则 → LLM │ │ │ ├── dict_matcher.go # AC 自动机 │ │ │ ├── regex_matcher.go │ │ │ ├── llm_judge.go # 语义兜底 │ │ │ └── text_extractor.go # 富文本 → 纯文本 + 偏移映射 │ │ ├── policy_resolver.go # 解析 "租户该用哪条策略" │ │ └── snapshot_loader.go # 词库快照 + 热更新 │ ├── repository/ │ └── domain/violation.go # 风险等级、命中结构 │ └── shared/compliance/ # 跨边界类型(DTO、常量) └── types.go ``` 发布器拦截:在 [`server/internal/tenant/app/publish_job_service.go`](../server/internal/tenant/app/publish_job_service.go) 的 `Create()` 入口最前面、`tx.Begin` 之前,调用: ```go decision, record, err := s.complianceSvc.GateForPublish(ctx, articleID, articleVersionID) ``` 强制模式下命中 `block` 等级直接返回 `ErrComplianceBlocked`;自愿模式或带 `ack_record_id` 时放行。 --- ## 6. 数据模型 > 迁移目录约定按现有规则:ops 用 `server/migrations_ops/`,tenant 用 `server/migrations/`,分别走独立 `schema_migrations` 表(沿用 `migrate_per_schema_table` 约定,避免 ops/main 互相看见对方版本号)。 ### 6.1 ops 侧(`server/migrations_ops/`) ```sql -- 平台级词库表(一条词典 = 一组规则) CREATE TABLE compliance_dictionaries ( id BIGSERIAL PRIMARY KEY, code VARCHAR(64) UNIQUE NOT NULL, -- e.g. 'ad_law_extreme', 'porn', 'politics' name VARCHAR(128) NOT NULL, category VARCHAR(32) NOT NULL, -- ad_law | sensitive | industry_med | custom default_level VARCHAR(16) NOT NULL, -- block | high | medium | info enabled BOOLEAN NOT NULL DEFAULT TRUE, version BIGINT NOT NULL DEFAULT 1, -- 词库版本号,引擎据此热加载 description TEXT, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL ); 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 pattern TEXT NOT NULL, level_override VARCHAR(16), -- 该词单独覆盖等级 hint TEXT, -- 命中后给用户的修改建议 reference_law VARCHAR(255), -- 法条引用,例如《广告法》第九条 enabled BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL ); CREATE INDEX idx_terms_dict ON compliance_dictionary_terms(dictionary_id) WHERE enabled = TRUE; -- 词库版本快照(用于回滚) CREATE TABLE compliance_dictionary_versions ( id BIGSERIAL PRIMARY KEY, dictionary_id BIGINT NOT NULL REFERENCES compliance_dictionaries(id) ON DELETE CASCADE, version BIGINT NOT NULL, snapshot JSONB NOT NULL, -- 词条全量快照 published_by BIGINT, published_at TIMESTAMPTZ NOT NULL, 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 时填 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) ); ``` ### 6.2 tenant 侧(`server/migrations/`) ```sql -- 租户自定义词条(白名单豁免 / 自家品牌黑名单) CREATE TABLE compliance_tenant_terms ( id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL REFERENCES tenants(id), kind VARCHAR(16) NOT NULL, -- whitelist | blacklist pattern TEXT NOT NULL, match_type VARCHAR(16) NOT NULL, -- exact | regex level VARCHAR(16), -- 仅 blacklist 用 hint TEXT, created_by BIGINT, created_at TIMESTAMPTZ NOT NULL ); CREATE INDEX idx_tenant_terms ON compliance_tenant_terms(tenant_id, kind); -- 检测记录(一次检测一行,作为审计与申诉依据) CREATE TABLE compliance_check_records ( id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL, article_id BIGINT, -- 可空:纯文本检测 article_version_id BIGINT, trigger_source VARCHAR(32) NOT NULL, -- editor_manual | publish_gate | auto_draft | api enforcement_mode VARCHAR(16) NOT NULL, -- 当时生效的模式 highest_level VARCHAR(16) NOT NULL, -- block | high | medium | info | pass passed BOOLEAN NOT NULL, -- 是否允许放行 hit_count INT NOT NULL, dict_versions JSONB NOT NULL, -- 命中时各词库版本(可复现) llm_used BOOLEAN NOT NULL DEFAULT FALSE, duration_ms INT NOT NULL, checked_by BIGINT, checked_at TIMESTAMPTZ NOT NULL ); CREATE INDEX idx_check_tenant_article ON compliance_check_records(tenant_id, article_id, checked_at DESC); -- 命中明细(前端高亮用) CREATE TABLE compliance_violations ( id BIGSERIAL PRIMARY KEY, record_id BIGINT NOT NULL REFERENCES compliance_check_records(id) ON DELETE CASCADE, tenant_id BIGINT NOT NULL, source VARCHAR(16) NOT NULL, -- dict | regex | llm | tenant_blacklist dictionary_code VARCHAR(64), term_pattern TEXT NOT NULL, matched_text TEXT NOT NULL, start_offset INT NOT NULL, -- 纯文本偏移 end_offset INT NOT NULL, dom_path TEXT, -- 富文本节点路径,前端高亮用 level VARCHAR(16) NOT NULL, hint TEXT, reference_law VARCHAR(255), acknowledged BOOLEAN NOT NULL DEFAULT FALSE -- 用户已知悉/忽略 ); CREATE INDEX idx_violations_record ON compliance_violations(record_id); ``` --- ## 7. 检测引擎 ### 7.1 调用流程 ``` ComplianceService.Check(ctx, ArticleContent) │ ├─ 0. KillSwitch: cfg.Compliance.Enabled == false → 直接 GatePass / Skipped ├─ 1. PolicyResolver: 拿到当前租户生效的策略 + 词库白名单(带 LRU 缓存,TTL 60s) ├─ 2. TextExtractor: 富文本 HTML/JSON → 纯文本 + (offset → dom_path) 映射 ├─ 3. DictMatcher: AC 自动机一次扫描所有 exact 词条 ├─ 4. RegexMatcher: 并发跑各正则(每条带 100ms 单条上限,超时丢弃并打告警) ├─ 5. WhitelistFilter: 命中后剔除租户白名单覆盖项 ├─ 6. LLMJudge(可选 / 异步): 把 "未命中关键词但风险类目开启" 的段落丢给 LLM └─ 7. 聚合 → 落库 compliance_check_records + compliance_violations → 返回 DTO ``` ### 7.2 性能与热更新 - **AC 自动机** 单次构建后驻留内存(`goahocorasick` 或等价库),万级词典 P99 < 5ms。 - **快照加载器** `snapshot_loader.go`:启动时全量拉取 → 构建快照 → 注册到 `atomic.Pointer[Snapshot]`。**每 30s 轮询** `compliance_dictionaries.version`,发现变更则后台构建新快照、原子替换。Ops 端改词库后立即 bump version,避免 Pub/Sub 强依赖;后续可加 Redis pub/sub 做秒级刷新(P3)。 - **LLM 兜底**:仅对开启了 `llm_categories` 的类目跑;编辑器手动检测**同步**等待结果(500ms 超时降级为不含 LLM 结论),发布器强制拦截**异步**走(写一条 `待复核` violation 后依策略放行或卡住)。 - **AI 点数计费**:LLM 检测复用现有 [`server/internal/tenant/app/ai_point_usage.go`](../server/internal/tenant/app/ai_point_usage.go) 计费体系,新增计费场景 `compliance_llm_judge`,避免被刷。 ### 7.3 偏移映射(前端高亮关键) 富文本编辑器(看 [`apps/admin-web/src/views/ArticleEditorView.vue`](../apps/admin-web/src/views/ArticleEditorView.vue) 用的是 ArticleEditorCanvas)一般是 ProseMirror / TipTap 之类节点树。`TextExtractor` 输出: ```go type ExtractedText struct { PlainText string OffsetMap []OffsetSegment // {start, end, domPath, blockID} } ``` 引擎拿到 `(start_offset, end_offset)` 后反查 `domPath`,前端按 path 注入高亮 mark,避免前后端各自用文本搜一遍导致错位。 ### 7.4 引擎接口(门面) ```go // server/internal/tenant/compliance/app/compliance_service.go type GateDecision int const ( GatePass GateDecision = iota // 放行 GateBlock // 必须拦截(强制 + block 等级) GateNeedsAck // 需要前端二次确认(advisory 或 强制 + high/medium) ) type ComplianceService interface { // 编辑器手动检测、纯文本检测公用入口 Check(ctx context.Context, req CheckRequest) (*CheckResult, error) // 发布闸:PublishJobService.Create() 内部调用 GateForPublish(ctx context.Context, articleID, versionID int64) (GateDecision, *CheckRecord, error) // 用户对某条违规标记 "已知悉,仍要发布" Acknowledge(ctx context.Context, violationID int64, actor int64) error // 给前端拉取运行时状态(kill-switch 状态、当前策略、LLM 是否启用) RuntimeStatus(ctx context.Context) (*RuntimeStatus, error) } ``` --- ## 8. Config Kill-Switch(凌驾于 ops 策略之上) 接入现有 [`server/internal/shared/config/config.go`](../server/internal/shared/config/config.go) 配置体系,与 `cfg.LLM` 同级新增: ```go // shared/config/config.go type ComplianceConfig struct { Enabled bool `mapstructure:"enabled" env:"COMPLIANCE_ENABLED" default:"true"` LLMJudgeEnabled bool `mapstructure:"llm_judge_enabled" env:"COMPLIANCE_LLM_ENABLED" default:"false"` SnapshotReloadInterval time.Duration `mapstructure:"snapshot_reload_interval" env:"COMPLIANCE_SNAPSHOT_RELOAD_INTERVAL" default:"30s"` PublishGateTimeout time.Duration `mapstructure:"publish_gate_timeout" env:"COMPLIANCE_PUBLISH_GATE_TIMEOUT" default:"800ms"` } ``` 调用面短路逻辑(写在 `compliance_service.go` 门面入口,不是各 handler): ```go func (s *ComplianceService) Check(ctx context.Context, req CheckRequest) (*CheckResult, error) { if !s.cfg.Compliance.Enabled { return &CheckResult{ Decision: GatePass, Skipped: true, Reason: "compliance_disabled_by_config", }, nil } // ... 正常流程 } func (s *ComplianceService) GateForPublish(ctx context.Context, articleID, versionID int64) (GateDecision, *CheckRecord, error) { if !s.cfg.Compliance.Enabled { return GatePass, nil, nil } // ... 正常流程 } ``` 层级覆盖关系: ``` config.compliance.enabled = false │ ↓ 全站短路(kill-switch,运维专用) │ └─ ops.compliance_policies(scope=global).enforcement_mode = disabled │ ↓ 默认关闭,但词库管理 UI 仍可访问 │ └─ ops.compliance_policies(scope=tenant_override).enforcement_mode = disabled ↓ 仅该租户关闭 ``` 前端也要感知运行时状态:编辑器进入时调一次 `GET /api/compliance/runtime-status`,返回 `{enabled, mode, llm_enabled, dict_versions}`,**引擎关闭时按钮直接隐藏(不是置灰)**,避免空跑误导用户。 --- ## 9. 对外 API ### 9.1 租户面(`/api/...`) | Method | Path | 用途 | 调用方 | |---|---|---|---| | GET | `/api/compliance/runtime-status` | 读取运行时状态(kill-switch、模式、LLM 开关) | 编辑器初始化 | | POST | `/api/articles/:id/compliance:check` | 编辑器一键检测,传 `{content, source: "manual"}` | 编辑器按钮 | | POST | `/api/compliance:check` | 无文章上下文的纯文本检测 | 标题 / 摘要单独检测 | | GET | `/api/articles/:id/compliance/latest` | 拿最近一次检测结果 + violations | 编辑器进入时回显 | | POST | `/api/compliance/violations/:id/ack` | 用户标记 "已知悉,仍要发布" | 自愿模式 | | GET / POST / DELETE | `/api/compliance/tenant-terms` | 租户白 / 黑名单管理 | 租户管理员 | 发布器拦截**不暴露 HTTP**,是 PublishJobService 内部调用 `GateForPublish`。 ### 9.2 ops 面(`/api/ops/...`) 走独立部署的 ops-api 服务(`server/cmd/ops-api`),handler 落在 `server/internal/ops/compliance/transport/`。 | Method | Path | 用途 | |---|---|---| | GET / POST / PUT / DELETE | `/api/ops/compliance/dictionaries` | 词库 CRUD | | GET | `/api/ops/compliance/dictionaries/:id/terms` | 词条列表(分页 / 搜索) | | POST | `/api/ops/compliance/dictionaries/:id/terms` | 单条新增 | | POST | `/api/ops/compliance/dictionaries/:id/terms:batch-import` | 词条批量导入(CSV,每次几千条) | | 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` | 全局检测记录搜索 | --- ## 10. 两种模式的处置矩阵 | 触发点 | 模式 = 强制(`mandatory`) | 模式 = 自愿(`advisory`) | 模式 = 关闭(`disabled` 或 config kill) | |---|---|---|---| | 编辑器手动检测按钮 | 跑全量检测,UI 标红 + 给修改建议 | 同左(**手动按钮两种模式行为相同**,仅展示) | 按钮隐藏(不是置灰) | | 保存草稿(P3 可选) | 异步预扫,不阻断保存,仅在编辑器侧角标提醒 | 同左 | 不跑 | | **发布按钮** | 同步检测:命中 `block` → 直接 `409 ErrComplianceBlocked` 拒发,前端弹层列出违规项;命中 `high/medium/info` → 弹"二次确认"对话框 | 同步检测:仅展示提醒面板,用户点 "仍然发布" 即放行(写一条 ack) | 不跑,直接进入 PublishJobService | 实现要点:拦截逻辑放在 [`server/internal/tenant/app/publish_job_service.go`](../server/internal/tenant/app/publish_job_service.go) 的 `Create()` 入口最前面,**在 `tx.Begin` 之前**调用 `complianceSvc.GateForPublish(ctx, articleID, articleVersionID)`: ```go type GateDecision int const ( GatePass GateDecision = iota GateBlock // 必须拦截(强制 + block 等级) GateNeedsAck // 需要前端二次确认(advisory 模式 或 强制模式 high/medium 等级) ) ``` 前端在 `apps/admin-web/src/components/PublishArticleModal.vue`(待新增 / 拓展)内部对 `GateNeedsAck` 渲染违规清单 + "知悉并继续发布" 按钮,点击后带 `ack_record_id` 重发请求,后端校验 `ack_record_id` 与 `tenant_id + article_version_id` 匹配后放行。 --- ## 11. ops 控制台 UI 新增 ops-web 视图(参照已有 [`apps/ops-web/src/views/AuditLogsView.vue`](../apps/ops-web/src/views/AuditLogsView.vue) 风格): ``` apps/ops-web/src/views/compliance/ ├── CompliancePolicyView.vue # 全局策略:模式开关 + 启用词库勾选 ├── ComplianceDictionariesView.vue # 词库列表(含版本 / 启用 / 导入) ├── ComplianceDictionaryEditView.vue # 词条 CRUD + CSV 导入 ├── ComplianceTenantOverrideView.vue # 租户级覆盖 ├── ComplianceRecordsView.vue # 全局检测记录、误报率 └── ComplianceStatsView.vue # 命中 Top 词、拦截率、按租户分布 ``` 侧边栏分组建议归到"内容安全",与现有审计日志并列。 --- ## 12. 租户面编辑器集成 ### 12.1 编辑器按钮 在 [`apps/admin-web/src/components/editor-common/EditorActionMenu.vue`](../apps/admin-web/src/components/editor-common/EditorActionMenu.vue) 增加一个独立按钮组件 `EditorComplianceCheck.vue`: ``` [AI助手] [合规检测] [发布] │ ├─ 点击 → POST /api/articles/:id/compliance:check ├─ 抽屉式结果面板:违规项列表(按 level 分组) ├─ 每条支持「定位到原文」「加入白名单」「忽略」 └─ 文档中违规处加 mark 高亮(按 dom_path) ``` 按钮可见性绑定 `runtime-status` 接口返回的 `enabled`:关闭时整组隐藏。 ### 12.2 发布弹窗改造 发布弹窗 `PublishArticleModal` 改造:发布前自动调一次 `compliance:check`(同步),根据返回的 `GateDecision` 渲染: - `pass`:直接进入正常发布流程 - `needs_ack`:替换为合规警示视图,列出违规项;用户必须勾选 "已了解风险,仍要发布" 才能继续,提交时携带 `ack_record_id` - `block`:禁用发布按钮,给出修改指引;不允许带 ack 强发 --- ## 13. 关键工程细节(大厂常踩点) | 点 | 做法 | |---|---| | **词库版本可回滚** | `compliance_dictionaries.version` + `compliance_dictionary_versions` 快照表。Ops 端"发布"是 bump version + 写快照;保留最近 N 个版本可一键回滚。 | | **灰度发布词库** | `compliance_policies.enabled_dictionaries[*]` 增加 `rollout_percent` 与 `rollout_tenant_ids`,PolicyResolver 按 `tenant_id % 100` 做一致性灰度。 | | **误报申诉闭环** | 用户 ack 后,记录写入 `compliance_violations.acknowledged=true`;ops 侧统计某词条的 ack 率,> X% 自动标记 "疑似误报" 提示运营复核。 | | **富文本不踩坑** | 检测**只看纯文本**,但保留 dom_path 用于回写高亮;图片 OCR、链接落地页 V1 不做(前置说明)。 | | **性能预算** | 编辑器手动按钮:纯词典 + 正则 P99 ≤ 50ms;含 LLM ≤ 1.5s。发布拦截:纯词典 + 正则 P99 ≤ 30ms(不含 LLM,LLM 异步进 review 队列),总闸超时由 `cfg.Compliance.PublishGateTimeout` 控制。 | | **可观测** | 给 [`server/internal/shared/middleware`](../server/internal/shared/middleware) 加埋点:每次 check 的 `duration_ms / hit_count / level / source / dict_version` 进 metrics;ops 侧拉曲线。 | | **审计日志** | 写词库、改策略、租户覆盖等管理操作通过现有 `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 已经生效。 | | **跨服务调用一致性** | tenant-api 和 ops-api 都引用同一个 `internal/shared/compliance/types.go`,避免 DTO 漂移。 | --- ## 14. 实施分期 | 阶段 | 范围 | 大致工时 | |---|---|---| | **P0**(最小闭环) | config kill-switch + ops 词库 / 策略表 + 引擎(词典 + 正则)+ 编辑器手动按钮 + 发布器拦截 + 强制 / 自愿模式 + 极限词库种子 | 5–8 人日 | | **P1**(运营可用) | ops-web 词库管理 UI + 租户白 / 黑名单 + 检测记录查询 + CSV 导入 + 误报率统计 | 3–4 人日 | | **P2**(智能化) | LLM 语义兜底 + 灰度发布 + 命中统计 + 误报申诉闭环 + AI 点数计费打通 | 4–6 人日 | | **P3**(增强) | 富文本节点路径高亮联动 + 异步保存预扫 + 词库版本回滚 UI + Redis pub/sub 秒级热更 + 词库 Diff 预览 | 4–5 人日 | --- ## 15. 与现有架构的衔接清单 | 现有模块 | 衔接方式 | |---|---| | `server/internal/shared/config/config.go` | 新增 `ComplianceConfig` 段,承担 kill-switch | | `server/internal/shared/llm/client.go` | LLMJudge 复用,新增 `compliance_llm_judge` 计费场景 | | `server/internal/tenant/app/publish_job_service.go` | `Create()` 入口前置 `GateForPublish` 闸 | | `server/internal/tenant/app/ai_point_usage.go` | LLM 检测计费走此服务 | | `server/internal/shared/auditlog/async.go` | 词库 / 策略管理操作落审计 | | `server/internal/shared/middleware/tenant_scope.go` | 租户作用域中间件保证 `tenant_id` 注入 | | `server/migrations_ops/` | 新增 4 张 ops 表迁移 | | `server/migrations/` | 新增 3 张 tenant 表迁移 | | `apps/admin-web/src/views/ArticleEditorView.vue` | 集成 `EditorComplianceCheck.vue` | | `apps/admin-web/src/components/editor-common/EditorActionMenu.vue` | 新增按钮挂载点 | | `apps/ops-web/src/views/` | 新增 `compliance/` 视图组 | --- ## 16. 风险与未决项 | 项 | 风险 / 待定 | 备选 | |---|---|---| | 富文本偏移映射准确度 | TipTap 在分块编辑后 dom_path 可能漂移 | 退化方案:仅返回 plain offset,前端基于 textContent 自行 search & highlight | | LLM 兜底成本 | 每篇文章 +1 次 LLM 调用,AI 点数消耗显著 | P2 上线前先做小流量灰度,验证 ack 率 / 误报率回报值 | | 词库初版覆盖度 | 公开版敏感词库可能不够覆盖行业(医美、教育、金融) | V1 后基于 ack 数据 + 运营反馈持续补 | | 30s 轮询延迟 | 词库新增后租户最长 30s 才生效 | P3 上 Redis pub/sub 做秒级;运营误操作时也提供"立即重载"按钮(仅 ops 可见) | | 配置 kill-switch 误触 | 运维误关导致全站合规失效 | metrics + Slack 告警 + 启动日志 WARNING;ops 控制台首页显著标注 "config 已关闭" 状态 | --- ## 17. 文档维护 - 词库结构 / 等级分级 / 模式覆盖关系如有调整,本文同步升版本号(v1 → v1.1 → v2)。 - 实施过程产生的偏差(例如 P1 提前并入 P0、引擎库选型变更)回写到本文 `§14 实施分期` 与对应章节。 - 与 `geo-platform-prd-v1.md`、`geo-platform-ops-admin-prd-v1.md` 中"内容安全 / 合规"段落保持口径一致;如冲突以本设计文档为后端实现口径,PRD 为产品验收口径。