Files
geo/docs/compliance-detection-tech-design-v1.md
T
root b36e2bf2ad docs(compliance): add content compliance detection design spec
落地敏感词 / 广告法 / 行业违禁词检测的设计文档,覆盖 ops 强制 vs 自愿模式、
PolicyResolver fail-closed 语义、ack 防伪、LLM 事后复核队列等关键决策。
v1 版本保留备查并指向 superpowers spec。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:36:15 +08:00

29 KiB
Raw Blame History

V1 技术方案:内容合规检测模块(敏感词 / 广告法)

⚠️ 本文档已归档(Superseded

当前权威设计已迁移到 superpowers 规范: docs/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 为准。


版本:v12026-04-28

适用范围:省心推(仓库代号 geo-rankly)整套发布器内文章发布前的合规检测。 与现有架构关键耦合点:server/internal/tenant/app/publish_job_service.goserver/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
分级处置 block(禁发) / high(强警告) / medium(建议改) / info(提示),由词库条目自带等级,不在引擎硬编码
租户隔离 词库分平台级(ops 维护)和租户级白 / 黑名单;检测记录打 tenant_id,与 articles 同一隔离模型(参考 server/migrations/20260331100011_create_articles.up.sql
词库热更新 启动全量加载 → 内存 AC 自动机 → 版本号 + 30s 轮询 reload;支持灰度(按租户 / 百分比)
配置硬开关 cfg.Compliance.Enabled = false 时全站短路,门面层早返 GatePass,检测彻底不跑

4. 总体架构

flowchart LR
    subgraph admin-web[管理后台 admin-web]
        E1[文章编辑器<br/>合规检测按钮]
        E2[发布弹窗<br/>PublishArticleModal]
    end

    subgraph ops-web[运营控制台 ops-web]
        O1[策略管理]
        O2[词库管理]
        O3[检测记录与统计]
    end

    subgraph tenant-api[tenant API server]
        T1[ComplianceHandler]
        T2[ComplianceService<br/>门面]
        T3[Engine<br/>词典+正则+LLM]
        T4[PolicyResolver]
        T5[SnapshotLoader<br/>词库快照]
        T6[PublishJobService<br/>发布闸]
    end

    subgraph ops-api[ops API server]
        P1[DictionaryHandler]
        P2[PolicyHandler]
        P3[StatsHandler]
    end

    DB[(PostgreSQL<br/>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. 模块分层

新增后端 domainserver/internal/tenant/compliance/(租户面)+ server/internal/ops/compliance/(平台面),沿用现有 transport / app / repository / domain 四层(参考 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.goCreate() 入口最前面、tx.Begin 之前,调用:

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/

-- 平台级词库表(一条词典 = 一组规则)
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/

-- 租户自定义词条(白名单豁免 / 自家品牌黑名单)
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 计费体系,新增计费场景 compliance_llm_judge,避免被刷。

7.3 偏移映射(前端高亮关键)

富文本编辑器(看 apps/admin-web/src/views/ArticleEditorView.vue 用的是 ArticleEditorCanvas)一般是 ProseMirror / TipTap 之类节点树。TextExtractor 输出:

type ExtractedText struct {
    PlainText string
    OffsetMap []OffsetSegment // {start, end, domPath, blockID}
}

引擎拿到 (start_offset, end_offset) 后反查 domPath,前端按 path 注入高亮 mark,避免前后端各自用文本搜一遍导致错位。

7.4 引擎接口(门面)

// 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 配置体系,与 cfg.LLM 同级新增:

// 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):

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.goCreate() 入口最前面,tx.Begin 之前调用 complianceSvc.GateForPublish(ctx, articleID, articleVersionID)

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_idtenant_id + article_version_id 匹配后放行。


11. ops 控制台 UI

新增 ops-web 视图(参照已有 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 增加一个独立按钮组件 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_percentrollout_tenant_idsPolicyResolver 按 tenant_id % 100 做一致性灰度。
误报申诉闭环 用户 ack 后,记录写入 compliance_violations.acknowledged=true;ops 侧统计某词条的 ack 率,> X% 自动标记 "疑似误报" 提示运营复核。
富文本不踩坑 检测只看纯文本,但保留 dom_path 用于回写高亮;图片 OCR、链接落地页 V1 不做(前置说明)。
性能预算 编辑器手动按钮:纯词典 + 正则 P99 ≤ 50ms;含 LLM ≤ 1.5s。发布拦截:纯词典 + 正则 P99 ≤ 30ms(不含 LLMLLM 异步进 review 队列),总闸超时由 cfg.Compliance.PublishGateTimeout 控制。
可观测 server/internal/shared/middleware 加埋点:每次 check 的 duration_ms / hit_count / level / source / dict_version 进 metricsops 侧拉曲线。
审计日志 写词库、改策略、租户覆盖等管理操作通过现有 auditlog.AsyncWriterserver/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 词库 / 策略表 + 引擎(词典 + 正则)+ 编辑器手动按钮 + 发布器拦截 + 强制 / 自愿模式 + 极限词库种子 58 人日
P1(运营可用) ops-web 词库管理 UI + 租户白 / 黑名单 + 检测记录查询 + CSV 导入 + 误报率统计 34 人日
P2(智能化) LLM 语义兜底 + 灰度发布 + 命中统计 + 误报申诉闭环 + AI 点数计费打通 46 人日
P3(增强) 富文本节点路径高亮联动 + 异步保存预扫 + 词库版本回滚 UI + Redis pub/sub 秒级热更 + 词库 Diff 预览 45 人日

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 告警 + 启动日志 WARNINGops 控制台首页显著标注 "config 已关闭" 状态

17. 文档维护

  • 词库结构 / 等级分级 / 模式覆盖关系如有调整,本文同步升版本号(v1 → v1.1 → v2)。
  • 实施过程产生的偏差(例如 P1 提前并入 P0、引擎库选型变更)回写到本文 §14 实施分期 与对应章节。
  • geo-platform-prd-v1.mdgeo-platform-ops-admin-prd-v1.md 中"内容安全 / 合规"段落保持口径一致;如冲突以本设计文档为后端实现口径,PRD 为产品验收口径。