Files
sub2api/openspec/changes/add-openai-compatible-prompt-audit/implementation-guide.md
T
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

32 KiB
Raw Blame History

实施指导

1. 使用方式与不可变边界

本指南把 proposal.mddesign.md 和三个 delta specs 转换为可按文件实施、可逐阶段评审的操作顺序。若本指南与 specs 冲突,以 specs 为准,并先更新 OpenSpec 再编码。

实施前必须满足:

  • source-baseline.md 的冻结登记已完成,不再以变化中的源工作区作为唯一依据。
  • 当前内容审核后端测试、RiskControl 前端测试和路由清单已保存为基线证据。
  • 新功能的默认配置是 off;数据库迁移可以先上线,但不能自动开启审计。
  • content_moderation_logsContentModerationService/admin/risk-controlRiskControlView.vue 的业务语义不改变。
  • 完整 Prompt 只允许存在于请求内存和 Redis TTL valueGuard token 只允许存在于写入 DTO、解密后的短生命周期内存和 Authorization header。

明确不做:输出审核、自动改写/脱敏后转发、人工审批、申诉、自动封号、邮件、Prompt 命中 Hash 黑名单、现有 Moderations 分类映射。

2. 目标依赖方向

flowchart TD
    Routes["server/routes 与协议 Handler"] --> Helper["security_audit_helper.go"]
    Helper --> Coordinator["securityaudit.Coordinator"]
    Coordinator --> LegacyPort["LegacyModerationEngine 接口"]
    Coordinator --> PromptService["PromptService"]
    LegacyPort --> Existing["现有 ContentModerationService"]
    PromptService --> Ports["ConfigStore / JobRepository / PayloadStore / Scanner"]
    Ports --> Infra["settings / database/sql / Redis / SecretEncryptor / HTTP"]
    AdminRoutes["admin routes"] --> AdminHandler["PromptAdminHandler"]
    AdminHandler --> PromptService
    Frontend["features/prompt-audit"] --> AdminRoutes

依赖规则:

  1. 现有 internal/service 不得 import internal/securityaudit;否则会把新能力反向渗入既有业务层。
  2. securityaudit 可以通过小接口适配现有 service/repository/Redis/加密能力,但不得修改这些接口的全局语义来迁就新模块。
  3. Coordinator 只归并客户端决策,不写 job/event、不发送邮件、不封号、不更新现有 Hash。
  4. Handler 只负责构造可信请求、调用 Coordinator、使用本协议原有错误 helper 返回结果。
  5. 核心逻辑不得读取 Gin context、环境变量或包级全局配置;这些只在模块构造/Handler 边界转换。
  6. 构造函数不得启动 goroutine。Worker、回收器和配置订阅必须由 Start(ctx) 启动、由 Shutdown(ctx) 有界停止。
  7. 前端只能依赖公共 DTO,不得知道 token_ciphertext、Redis key 或数据库内部状态转换 SQL。
  8. 新模块不引入新的 ORM、队列库、状态库或 UI 框架。

3. 建议目录和文件职责

backend/internal/securityaudit/
├── coordinator.go                 # 双引擎编排、固定优先级
├── coordinator_test.go
├── prompt_types.go                # Request/Decision/Job/Event/Runtime 与枚举
├── prompt_config.go               # Storage/Public/Update DTO、校验、快照
├── prompt_config_test.go
├── prompt_snapshot.go             # 协议提取、Hash、脱敏预览
├── prompt_snapshot_test.go
├── prompt_scanner.go              # 分片、聚合、Scanner 接口
├── prompt_qwen3guard.go            # 请求构造、严格解析、九类风险
├── prompt_qwen3guard_test.go
├── prompt_issue_summary.go         # 从分类/脱敏证据派生管理端风险摘要
├── prompt_issue_summary_test.go
├── prompt_outbound_security.go     # URL/DNS/Dial/redirect/响应上限
├── prompt_outbound_security_test.go
├── prompt_repository.go            # database/sql jobs/events 实现
├── prompt_repository_test.go
├── prompt_payload_store.go         # Redis SET EX/GET/DEL
├── prompt_enqueue.go               # staging → payload → queued
├── prompt_enqueue_test.go
├── prompt_worker.go                # claim/lease/retry/reclaim/lifecycle
├── prompt_worker_test.go
├── prompt_guard.go                 # blocking evaluator、deadline/failover/bulkhead
├── prompt_guard_test.go
├── prompt_runtime.go               # 健康、版本、队列、指标快照
├── prompt_logging.go               # 稳定事件和 allowlist fields
├── prompt_handler.go               # 独立 admin HTTP handler
├── prompt_handler_test.go
└── prompt_module.go                # provider set、Start/Shutdown 组合

backend/migrations/181_prompt_audit.sql
backend/internal/handler/security_audit_helper.go
backend/internal/server/routes/admin.go
backend/internal/server/routes/gateway.go
backend/internal/wire/或项目实际 provider 文件

frontend/src/features/prompt-audit/
├── PromptAuditView.vue
├── api.ts
├── types.ts
├── viewModel.ts
├── components/
└── __tests__/

181 是提案编写时最大迁移号后的建议值。实施时若 181 已存在,必须使用新的最大序号;不得改写已经应用的 migration。

4. 按文件的实施顺序

4.1 第一批:契约与纯函数

  1. 创建 prompt_types.go,固定稳定枚举和 JSON 字段。
  2. 创建 prompt_config.go,先实现默认值、三态归一、字段边界和 Public DTO。
  3. 创建 prompt_snapshot.go,完成各协议纯文本提取、最新输入优先、SHA-256 和脱敏预览。
  4. 创建 prompt_scanner.goprompt_qwen3guard.goprompt_issue_summary.go,完成 rune 分片、严格解析、聚合和展示摘要派生。
  5. 同步创建上述测试;此阶段不连接 DB、Redis、Gin 或真实 Guard。

验收重点:纯函数表驱动测试覆盖中文、emoji、空输入、混合 content blocks、九类风险、额外说明、重复字段、未知类别和完整分片。

4.2 第二批:数据库与配置适配

  1. 新增 181_prompt_audit.sql 以及 migration schema 测试。
  2. prompt_repository.go 用现有 *sql.DB 实现 jobs/events;不为这两张表增加 Ent schema。
  3. 在目标项目现有 setting 常量事实源增加 prompt_audit_config
  4. prompt_config.go 复用 SettingRepositorySecretEncryptor,实现 storage ↔ active ↔ public 三类 DTO 转换。
  5. prompt_payload_store.go 适配现有 Redis Client。
  6. 完成 Repository、加密配置、多实例版本加载测试。

4.3 第三批:出站安全、异步队列与运行态

  1. prompt_outbound_security.go 先实现保存/探测/调用共用的 URL 校验和受控 Transport。
  2. prompt_enqueue.go 实现 staging 发布协议。
  3. prompt_worker.go 实现 PostgreSQL claim、租约、重试、回收和生命周期。
  4. prompt_runtime.go 汇总 active/expected config version、Worker、队列、Redis 和节点健康。
  5. prompt_logging.go 固定事件名、error_code 和允许字段。
  6. 用 fake clock、fake scanner、真实测试 PostgreSQL/Redis 分层验证,先不开网关。

4.4 第四批:管理 API 和控制台

  1. prompt_handler.go 注册 config/probe/runtime/events/delete 方法。
  2. 在 admin handler 聚合结构和 Wire 中注入 PromptAdminHandler
  3. admin.go 注册独立 /admin/prompt-audit 路由组。
  4. 创建前端 features/prompt-audit 的 types、api、viewModel,再创建页面和组件。
  5. 增加 router、Sidebar、zh/en i18n 的薄接线。
  6. 管理闭环通过后,Prompt Audit 仍默认 off。

4.5 第五批:Coordinator 与异步接入

  1. coordinator.go 用 fake engines 完成 off/async/blocking 组合测试。
  2. 新增 security_audit_helper.go,从现有 buildContentModerationInput 的可信字段构造 securityaudit.Request
  3. 机械替换所有现有 checkContentModeration 调用点为 checkSecurityAudit,保留原位置。
  4. async 模式只 best-effort 投递;Redis/DB/节点失败不得改变客户端响应或上游次数。
  5. 运行路由结构测试,证明没有漏掉已有调用点。

4.6 第六批:同步 Guard

  1. prompt_guard.go 实现共享 deadline、节点优先级、故障切换和 bulkhead。
  2. Coordinator 接入 blocking 分支并固定现有内容审核 Block 响应优先级。
  3. HTTP/SSE 使用协议原有错误构造器;Guard 完成前 SSE 不写首字节。
  4. Responses WS 首轮和后续 response.create 分别接入,使用指定 close code。
  5. 加入账号选择、并发 slot、预扣/计费、上游拨号/写入 fake counter,断言拒绝时全部为 0。

5. 公共核心类型建议

5.1 可信请求

type Request struct {
    RequestID  string
    UserID     int64
    Username   string
    UserEmail  string
    APIKeyID   int64
    APIKeyName string
    GroupID    *int64
    GroupName  string
    Provider   string
    Endpoint   string
    Protocol   string
    Model      string
    Body       []byte
    Stage      string // http | first_turn | subsequent_turn
}

Body 必须是 Handler 在全局 body limit 下已经读取的同一字节切片。模块不得再次读 http.Request.Body,不得改写转发 body。UsernameUserEmailAPIKeyName 只用于管理员事件快照/展示,不得进入普通请求日志;API 必须分列返回,避免复制/筛选时含义混淆。

5.2 统一决策

type Decision struct {
    Kind           string // allow | flag | block | unavailable | invalid
    HTTPStatus     int
    ErrorCode      string
    ClientMessage  string
    Legacy         *LegacyDecision
    Prompt         *PromptDecision
    AllowNextStage bool
}

稳定优先级:

  1. Legacy content moderation Block:完全复用原状态码、文案和 content_policy_violation
  2. Prompt Block403 + prompt_guard_blocked
  3. Prompt Invalid503 + prompt_guard_invalid_response
  4. Prompt Unavailable503 + prompt_guard_unavailable
  5. 其他:Allow;Flag 只记录,不阻断。

不要让 Coordinator 暴露 Qwen 原始响应,也不要用一个布尔 Blocked 吞掉 unavailable/invalid 的差异。

6. Coordinator 请求流

鉴权与 body/model 基础校验
  → 构造可信 Request
  → 读取 risk_control + prompt active snapshot
  → Coordinator 调用现有 Moderation 与 Prompt 引擎
  → 按固定优先级得到 Decision
  → 若 !AllowNextStage,使用当前协议 error helper 返回
  → 否则才进入账号选择/并发/计费/上游

模式行为:

有效模式 现有 Moderation Prompt Audit 请求等待 Prompt Prompt 失败影响请求
off 原行为 不运行
async_audit 原行为 best-effort enqueue
blocking 原行为 同步扫描并复用结果记录 是,fail-closed

async 模式下应先触发/完成有界投递动作,再返回 Coordinator 结果,确保现有 Moderation 随后 Block 时 Prompt 事件仍可 best-effort 产生。投递动作必须只有短 DB/Redis 操作,不能等待 Guard。

blocking 模式可以并行执行两个引擎,但必须遵守:

  • goroutine 数量固定且可等待,不得 fire-and-forget。
  • 两个结果都在各自 deadline 内收口,或明确取消。
  • Legacy Block 的响应优先,但 Prompt 结果仍按独立规则记录。
  • 共享只读 Request;不得共享可变 decision buffer。

7. 异步时序

sequenceDiagram
    participant H as Protocol Handler
    participant C as Coordinator
    participant E as Prompt Enqueuer
    participant PG as PostgreSQL
    participant R as Redis
    participant W as Worker
    participant G as Qwen3Guard

    H->>C: Check(trusted Request)
    C->>E: Enqueue(snapshot, scan text)
    E->>PG: INSERT job status=staging
    PG-->>E: job_id
    E->>R: SET payload:{job_id} scan_text EX 1800
    R-->>E: OK
    E->>PG: UPDATE staging → queued (conditional)
    E-->>C: accepted
    C-->>H: legacy decision / allow
    H->>H: 继续原账号、计费、上游流程

    W->>PG: claim queued/retry FOR UPDATE SKIP LOCKED
    PG-->>W: status=processing job
    W->>R: GET payload:{job_id}
    loop 每个必要分片
        W->>PG: refresh processing lease
        W->>G: POST /v1/chat/completions
        G-->>W: Safety + Categories
    end
    W->>PG: transaction: event + job done
    W->>R: DEL payload:{job_id}

异常补偿:

  • active count 与 staging INSERT 在 PostgreSQL advisory-lock 短事务中完成;锁超时使用 queue_admission_busy,不得把 Redis 调用放进事务。
  • staging INSERT 失败:不写 Redis,记录 dropped,主请求继续。
  • Redis SET 失败:job 条件标 failed;主请求继续。
  • staging → queued 条件更新失败:删除 Redis key;回收器处理残留 staging。
  • Worker 找不到 payload:按稳定 payload_missing 失败,不可把预览当原文扫描。
  • event 写入失败:异步 job retry 或 failed,不能产生虚假 done。
  • Redis DEL 失败:依靠 TTL,记录脱敏警告。

8. 同步阻断时序

sequenceDiagram
    participant H as HTTP/SSE/WS Handler
    participant C as Coordinator
    participant M as Existing Moderation
    participant P as Prompt Guard
    participant G as Guard Pool
    participant D as DB Recorder
    participant A as Account/Billing/Upstream

    H->>C: Check(Request, blocking snapshot)
    par 保持现有审核语义
        C->>M: Check
        M-->>C: legacy decision
    and 共享总预算扫描
        C->>P: Evaluate(snapshot)
        P->>G: chunks × ordered failover
        G-->>P: normalized result
        P-->>C: Allow/Flag/Block/Unavailable/Invalid
    end
    C-->>D: record redacted result (no scan text)
    D-->>C: best-effort record status
    C-->>H: prioritized Decision
    alt Block/Unavailable/Invalid
        H-->>H: protocol-compatible error/close
        Note over H,A: account selection=0, billing=0, upstream=0
    else Allow/Flag
        H->>A: continue original flow
    end

同步记录失败不得反转已确定结果。一次同步评估只调用 Guard 一次;记录 adapter 禁止接收 scan_text,防止为了落库再次扫描或意外持久化原文。

9. Job 状态机

stateDiagram-v2
    [*] --> staging: INSERT
    staging --> queued: Redis SET 成功且条件发布
    staging --> failed: Redis/发布失败或 staging 超时回收
    queued --> processing: 原子 claim
    retry --> processing: 到达 next_attempt_at 后原子 claim
    processing --> done: 必要分片完成且事件事务成功
    processing --> retry: 可重试错误且 attempts < max_attempts
    processing --> failed: 不可重试或达到上限
    processing --> retry: 租约超时回收且仍可重试
    processing --> failed: 租约超时且达到上限
    done --> [*]
    failed --> [*]

每次 queued/retry → processing 必须把 claim_version 原子加一并返回给 Worker。租约刷新、event+done 事务和 retry/failed 更新必须使用“id + processing + claim_version”条件并检查 affected rows0 rows 表示租约已失效,本 Worker 必须丢弃结果。禁止仅按 status 条件更新,因为任务被回收并重新领取后 status 会再次变成 processing,旧 Worker 会误覆盖新结果。

10. 配置、Storage DTO 与 Public DTO

10.1 存储结构

setting key 固定为 prompt_audit_configJSON 至少包含:

enabled, blocking_enabled, store_pass_events,
strategy=priority, worker_count, queue_capacity,
scanners[], all_groups, group_ids[], endpoints[],
config_version, updated_at, updated_by, change_summary

Endpoint storage 字段:

id, name, protocol=openai_compatible, base_url,
model=sileader/qwen3guard:0.6b,
token_ciphertext, timeout_ms, input_limit, enabled

10.2 写入 DTO

每个 endpoint 的写入必须区分:

  • token 非空:校验后加密并替换旧密文。
  • token 空且 clear_token=false:保留旧密文;新 endpoint 没有旧密文时校验失败。
  • clear_token=true:清除密文;启用的 endpoint 若必须认证则保存失败或明确显示不可用。

保存请求必须携带 expected_config_version。后端在 PostgreSQL 短事务中取得该 setting 专用 advisory transaction lock、重读当前值并做 CAS;冲突返回 409 prompt_audit_config_conflict,不写 settings、不安装快照、不发 Redis 通知。enabled=false && blocking_enabled=true 必须返回稳定错误 prompt_guard_requires_audit_enabledstrategy 第一版只接受 priority。保存时 canonicalize group IDs、scanner IDs 和 endpoint IDs,拒绝重复、空 ID、越界 worker/queue/timeout/input_limit。

10.3 Public DTO

GET config 和 PUT 成功响应只允许:

id, name, protocol, base_url, model, timeout_ms,
input_limit, enabled, has_token, token_status

不得出现 tokentoken_ciphertext、Authorization、解密失败原文或完整错误响应。后端 JSON 类型应物理分离,不能依赖 json:"-" 后复用内部对象。

10.4 活动快照

  • 保存成功后 config_version + 1,先安装本实例只读快照,再发布 Redis invalidation。
  • Pub/Sub 消息只含版本,不含配置。
  • 其他实例重新从 settings 加载、解密、验证,成功后原子替换。
  • 加载失败保留 last-known-good,并在 runtime 同时展示 expected/active version 和错误。
  • 冷启动无 last-known-good 且 blocking 期望启用时必须 degraded/error,不能当作 off 放行。
  • 请求热路径只读内存快照,不查 settings/DB。

11. 管理 API 映射

统一前缀:/admin/prompt-audit。全部复用现有管理员鉴权、安全中间件和管理操作审计。

方法 路径 用途 关键约束
GET /config 读取公共配置 不回显密文/明文 token
PUT /config 原子保存完整配置 版本递增、allowlist 审计
POST /endpoints/probe 测试保存或临时凭据 禁重定向、SSRF 防护、结果脱敏
GET /runtime 运行态与指标 显示真实 degraded/error
GET /events 复合筛选分页 稳定排序;用户名/邮箱/API Key 名称分列
GET /events/:id 事件详情 脱敏预览、归一结果和派生 issue_summaries
DELETE /events/:id 单条硬删除 审计、孤立 job 安全清理
POST /events/batch-delete 按 ID 批量删除 限制 ID 数量、事务分批
POST /events/delete-preview 预览筛选删除 强制起止时间,返回 count/max_id/hash/token
POST /events/delete-by-filter 确认筛选删除 confirm=true,认证 token/actor/hash,限制 id≤max_id

分组选择复用目标项目现有管理员 group 查询 API,不为 Prompt Audit 复制一份分组事实源。若现有 API 不适合轻量选择器,只新增薄的只读适配,并在实现前回写本表。

建议错误 envelope 继续使用项目管理 API 的统一结构;业务错误码稳定,内部 SQL/Redis/HTTP 错误不得透传。

12. 网关 Handler 路由矩阵

下表是提案编写时已有 checkContentModeration 调用点,实施时应机械替换并由结构测试锁定。路由别名共享相同 Handler,因此测试必须至少覆盖主路由与每类 alias。

协议/入口 路由 现有 Handler 文件/方法 Stage 拒绝构造器
Anthropic Messages POST /v1/messages gateway_handler.go: Messagesopenai_gateway_handler.go: Messages http Anthropic error helper
OpenAI Responses POST /v1/responses/responses/backend-api/codex/responses 及 subpath gateway_handler_responses.go: Responsesopenai_gateway_handler.go: Responses http Responses/OpenAI helper
OpenAI Chat Completions POST /v1/chat/completions/chat/completions gateway_handler_chat_completions.go: ChatCompletionsopenai_chat_completions.go: ChatCompletions http Chat/OpenAI helper
Gemini Generate/Stream POST /v1beta/models/*modelAction gemini_v1beta_handler.go: GeminiV1BetaModels http Google error helper
OpenAI Images POST /v1/images/generations/v1/images/edits openai_images.go: Images http OpenAI helper
Grok image/video 文本请求 images/videos 路由 grok_media.go: handleGrokMedia http OpenAI helper
Responses WebSocket 首轮 GET /v1/responses/responses/backend-api/codex/responses openai_gateway_handler.go: ResponsesWebSocket first_turn close 4403/1013
Responses WebSocket 后续轮次 每个 response.create 同上 BeforeRequest/turn callback subsequent_turn close 4403/1013

实施时还必须从 backend/internal/server/routes/gateway.go 枚举所有携带用户文本的新增/旁路入口,重点复核:

  • /v1/images/generations/async/v1/images/edits/async
  • /v1/images/batches 及 batch item 的实际提交入口。
  • Grok video generation/edit/extension。
  • 任何不经过上述公共 Handler 的内部转发、兼容 alias 或后续新增路由。

对额外入口有两种合法结论:接入 Coordinator;或证明它已在上游公共 Handler 处检查且不会二次收费/二次扫描。结论和测试必须加入路由矩阵,不能静默跳过。

接入位置不变量:鉴权、body limit、基本 JSON/model 校验之后;账号选择、用户/账号并发 slot、订阅/余额预扣、usage 写入、上游拨号和 SSE 首字节之前。

13. HTTP、SSE、WebSocket 处理细节

情况 HTTP/SSE WS close reason/code
Prompt Block 403 4403 prompt_guard_blocked
Guard Unavailable 503 1013 prompt_guard_unavailable
Guard Invalid response 503 1013 prompt_guard_invalid_response
  • HTTP/SSE 必须保留各协议 envelope,不能所有协议统一成 Gin {"error":"..."}
  • OpenAI Chat/Responses 在 error 对象添加稳定 codeClaude 保留 permission_error/api_error type 并添加可选 code
  • Gemini 保留数值 HTTP error.code 和 canonical status,只在 google.rpc.ErrorInfo.reason 放稳定代码;metadata 仅 request_id。
  • SSE 在 Guard 结果前不得写 status/header/data/comment/keepalive;否则无法返回 403/503。
  • WS 握手本身没有 Prompt,不扫描。首个 response.create 在任何本轮资源/上游副作用前扫描。
  • 后续每个 response.create 重新提取本轮输入并标记 subsequent_turn
  • WS close reason 长度必须在协议限制内,只使用稳定短码;详细内部错误只进脱敏指标/日志。
  • Legacy moderation 同时 Block 时,继续使用其原错误/close 行为和文案。

14. SQL 和 Repository 注意事项

14.1 Migration

  • PostgreSQL migration 是事实源;不要复制源仓库的 aicodex_ 前缀。
  • 表名固定 prompt_audit_jobsprompt_audit_events
  • 所有状态、计数和非负值加 CHECK;JSONB 加可接受类型检查更佳。
  • events.job_id ON DELETE CASCADEuser/api_key/group 外键 ON DELETE SET NULL
  • username_snapshotuser_email_snapshotapi_key_name_snapshot 与 group name 快照分列保留,以免主体删除后事件无法复核;沿用现有管理员权限和数据保留策略。
  • 不新增 raw_prompt、raw_request、request_body、payload、token、authorization、guard_response_body 等列。
  • 索引名全库唯一;先检查 migration 事实源,避免只在开发库检查。

14.2 原子领取

FOR UPDATE SKIP LOCKED 必须在同一短事务中选择并更新为 processing。事务内不要调用 Redis、Guard 或日志网络 sink。每次 claim 后立即提交,长工作在事务外执行。

14.3 租约和重试

  • attempts 在成功 claim 时递增,而不是失败时递增。
  • 每个必要分片前刷新租约,并以 processing 状态和本次 claim_version 作为条件。
  • 401/403、严格解析错误不可重试;429、5xx、连接、超时可重试。
  • 建议退避 5s、30s、2m,上限 5m并加小 jitter;测试使用 fake clock。
  • reclaim 批次有上限并按时间/id 稳定排序,防止全表锁和饥饿。

14.4 事件和任务事务

  • 异步成功:event insert 与 job done 应在单事务完成。
  • 同步:创建 blocking/done job 与可选 event 在单事务完成,但失败不改变门禁结果。
  • store_pass_events=false 时仍可保存 done job 的最小脱敏执行记录;若最终决定不保存 Pass job,必须回写 schema、runtime 计数和清理规格。
  • 删除 event 后只删除无事件引用且非 processing 的孤立 job;并 best-effort 删除 Redis key。

14.5 查询与删除

  • 列表使用参数化 SQL、白名单排序字段和稳定 created_at DESC, id DESC
  • 时间过滤明确采用 UTC 存储、API ISO-8601,并定义边界包含性。
  • delete-preview 在同一数据库快照得到 count 和 snapshot_max_id,对 canonical JSON filter + max_id 计算 SHA-256;字段顺序、空值和时区必须规范化。
  • 使用 SecretEncryptor 认证加密 {filter_hash,snapshot_max_id,admin_id,issued_at,expires_at},返回默认 5 分钟有效的 confirmation_token。
  • delete-by-filter 解密并校验 actor/expiry/hash,要求同一筛选、confirm=true 和强制时间范围,查询强制 id <= snapshot_max_id 后分批提交;预览后的新事件不可被删除。

15. Guard Client 和出站安全

请求固定发送到规范化 {base_url}/v1/chat/completions,默认模型 sileader/qwen3guard:0.6brole=user、temperature=0、max_tokens=64、seed=42。

保存、probe 和实际扫描必须走同一校验/Transport:

  • 只允许 http/https;禁止 userinfo、query、fragment。
  • 禁止 metadata、link-local、multicast、unspecified、保留地址。
  • 公网强制 HTTPSHTTP 只允许显式受控的 localhost/私网开发场景。
  • DNS 解析结果和真正 Dial 的 IP 都检查,防 DNS rebinding。
  • 不跟随 3xx;响应体最多 256 KiB。
  • 独立连接池和 Dial/TLS/ResponseHeader timeout;所有分片/故障切换仍受外层总 deadline。
  • 日志只写 endpoint ID、HTTP status、error_code、latency,不写完整 URL、query、header 或原始 response body。
  • 分片日志只写 chunk_index/total/chars、input_chars/limit、endpoint ID、action、latency 和错误码,不写 chunk 或内部优先级分隔符。

九类 scanner ID/展示名必须稳定:Violent、Non-violent Illegal Acts、Sexual Content or Sexual Acts、PII、Suicide & Self-Harm、Unethical Acts、Politically Sensitive Topics、Copyright Violation、Jailbreak。

16. 前端状态与凭据处理

建议 viewModel 分成:

serverSnapshot     # 最近一次后端公共配置
draft              # 可编辑非敏感配置
endpointSecrets    # 仅当前会话内的新增/替换 token
loadState          # config/runtime/groups/events 独立状态
probeStateByID     # 节点探测进度和脱敏结果
eventQuery         # canonical filter + page
deletePreview      # count + max_id + filter_hash + confirmation_token + filter snapshot
issueSummaries     # 后端从事件事实派生的只读风险展示项

规则:

  • endpointSecrets 不进入 Pinia 持久化、localStorage、sessionStorage、URL、console 或错误追踪 breadcrumb。
  • 保存成功后立即清空已提交 secret;失败时可以留在内存草稿供用户修正,但离开页面/卸载必须清空。
  • 编辑已保存节点时 token 输入默认空,使用 has_token/token_status 表示存在性。
  • “清除 API Key”使用独立明确动作设置 clear_token=true,不能把输入框空值当清除。
  • dirty 比较忽略后端时间戳,但包含 clear/replace 意图;保存返回后以 Public DTO 重建 snapshot。
  • config/runtime/groups/events 独立失败,不能一个 500 让整页白屏。
  • blocking_enabled 从 false → true 必须二次确认;关闭 enabled 同时把 draft blocking 设 false。
  • 删除预览与 filter snapshot、snapshot_max_id、confirmation_token 绑定;任何筛选变化立即废弃旧 filter_hash/token。
  • 用户名、邮箱和 API Key 名称使用不同字段/复制按钮;空值显示明确 fallback,不用邮箱冒充用户名。
  • IssueSummary 展示 category、title/description、severity/action、scanner、score 和脱敏 evidence,禁止从 evidence 重建命中原文。
  • 窄屏表格提供可读替代布局,Dialog 有 focus trap/return focus,所有控件有中英文可访问名称。

17. PR/提交切片策略

每个阶段应可单独评审、测试和回滚,建议五组 PR:

  1. 数据与核心契约migration、types/config/snapshot/Qwen parser、Repository 及测试;无路由接入。
  2. 异步引擎:出站安全、Redis payload、enqueue、Worker、runtime;功能默认 off。
  3. 管理闭环admin API、独立页面、路由/Sidebar/i18n;仍不启用 blocking。
  4. Coordinator 与同步门禁:统一接入、HTTP/SSE/WS、无副作用断言、Legacy 回归。
  5. 灰度与运维:指标、告警、canary 泄露检查、运行手册和阈值登记。

不要在同一 PR 混入无关的 ContentModeration 重构、全局 Handler 重写、前端框架升级或数据库清理。若为接线必须改现有文件,变更应机械、薄且有前后行为测试。

18. 五个待确认事项的决策门

事项 默认建议 必须在何时确认 未确认时行为
源基线标识 专用 commit/tag PR 1 前 不开始移植
自动保留期 第一版只安全删除 migration 冻结前 不加自动清理
双引擎并行/串行 并行 PR 4 前做 benchmark/race 可先串行但保留优先级
额外文本入口 routes 自动枚举 PR 4 接线前 结构测试失败
blocking 阈值 运营按 async 数据登记 生产 blocking 前 只允许 off/async

19. 常见错误

  • 直接把 Qwen3Guard 加进 ContentModerationService,导致配置、表和副作用混用。
  • 直接复制源 Ent/React/Caddy 代码,形成重复基础设施或目标项目无法维护的适配壳。
  • 先把 job 设 queued 再写 Redis,造成 Worker 抢到无 payload 任务。
  • redacted_preview 当作可重试扫描正文;这会产生错误分类且破坏完整覆盖。
  • 用 byte 长度切中文/emoji,或只扫描第一片后返回 Allow。
  • 把 Guard 401/403/invalid_response 当 Safe 或无限切节点。
  • SSE 已写 200/首字节后才运行 Guard。
  • WS 只检查首轮,不检查后续 response.create
  • Prompt 拒绝发生在账号选择、并发 slot、预扣或上游拨号之后。
  • Public DTO 复用 Storage DTO,靠前端“不显示”隐藏 token。
  • 日志记录请求 body、Guard 原始响应、完整 Base URL/query 或 Redis value。
  • 配置 reload 失败时清空 last-known-good,或冷启动失败时伪装为 off/healthy。
  • 按筛选删除没有强制时间范围、预览 Hash 或筛选变化失效。
  • 为迁移方便重命名/迁移现有 content_moderation_logs 或改变 /admin/risk-control

20. Definition of Done

只有全部成立才算实现完成:

  • 源 commit/tag/patch 已冻结并有可验证 SHA-256。
  • 三个 specs 的每个 Requirement 都在 verification.md 有测试/SQL/日志/截图证据。
  • Prompt Audit 默认 off;off 时所有外部协议、现有内容审核响应和副作用与升级前一致。
  • async 失败不改变客户端状态、响应体、计费和上游调用次数。
  • blocking 的 Block/Unavailable/Invalid 在 HTTP/SSE/WS 映射正确,且账号选择、计费、上游均为 0。
  • 所有现有用户文本路由和 alias 都有 Coordinator 覆盖证据。
  • 两张新表、Redis metadata、日志、API、浏览器状态和截图均未出现 canary Prompt/token。
  • 风险详情拥有确定性 issue_summaries,用户名/邮箱/API Key 名称可分别复核复制,逐分片日志只含安全元数据。
  • 多 Worker、多实例配置失效、租约回收和 graceful shutdown 测试通过。
  • 原 RiskControl 页面、关键词、Hash、邮件、自动封号和内容审核记录回归通过。
  • 后端 unit/race/integration、前端 lint/typecheck/Vitest、生产 build 和 OpenSpec strict validate 全部通过。
  • 已完成 async 灰度观测;blocking 阈值、告警、值班步骤和一键回滚已由责任人签字确认。