本指南把 proposal.md、design.md 和三个 delta specs 转换为可按文件实施、可逐阶段评审的操作顺序。若本指南与 specs 冲突,以 specs 为准,并先更新 OpenSpec 再编码。
实施前必须满足:
source-baseline.md 的冻结登记已完成,不再以变化中的源工作区作为唯一依据。content_moderation_logs、ContentModerationService、/admin/risk-control 和 RiskControlView.vue 的业务语义不改变。明确不做:输出审核、自动改写/脱敏后转发、人工审批、申诉、自动封号、邮件、Prompt 命中 Hash 黑名单、现有 Moderations 分类映射。
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
依赖规则:
internal/service 不得 import internal/securityaudit;否则会把新能力反向渗入既有业务层。securityaudit 可以通过小接口适配现有 service/repository/Redis/加密能力,但不得修改这些接口的全局语义来迁就新模块。Start(ctx) 启动、由 Shutdown(ctx) 有界停止。token_ciphertext、Redis key 或数据库内部状态转换 SQL。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。
prompt_types.go,固定稳定枚举和 JSON 字段。prompt_config.go,先实现默认值、三态归一、字段边界和 Public DTO。prompt_snapshot.go,完成各协议纯文本提取、最新输入优先、SHA-256 和脱敏预览。prompt_scanner.go、prompt_qwen3guard.go 与 prompt_issue_summary.go,完成 rune 分片、严格解析、聚合和展示摘要派生。验收重点:纯函数表驱动测试覆盖中文、emoji、空输入、混合 content blocks、九类风险、额外说明、重复字段、未知类别和完整分片。
181_prompt_audit.sql 以及 migration schema 测试。prompt_repository.go 用现有 *sql.DB 实现 jobs/events;不为这两张表增加 Ent schema。prompt_audit_config。prompt_config.go 复用 SettingRepository 和 SecretEncryptor,实现 storage ↔ active ↔ public 三类 DTO 转换。prompt_payload_store.go 适配现有 Redis Client。prompt_outbound_security.go 先实现保存/探测/调用共用的 URL 校验和受控 Transport。prompt_enqueue.go 实现 staging 发布协议。prompt_worker.go 实现 PostgreSQL claim、租约、重试、回收和生命周期。prompt_runtime.go 汇总 active/expected config version、Worker、队列、Redis 和节点健康。prompt_logging.go 固定事件名、error_code 和允许字段。prompt_handler.go 注册 config/probe/runtime/events/delete 方法。PromptAdminHandler。admin.go 注册独立 /admin/prompt-audit 路由组。features/prompt-audit 的 types、api、viewModel,再创建页面和组件。coordinator.go 用 fake engines 完成 off/async/blocking 组合测试。security_audit_helper.go,从现有 buildContentModerationInput 的可信字段构造 securityaudit.Request。checkContentModeration 调用点为 checkSecurityAudit,保留原位置。prompt_guard.go 实现共享 deadline、节点优先级、故障切换和 bulkhead。response.create 分别接入,使用指定 close code。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。Username、UserEmail 和 APIKeyName 只用于管理员事件快照/展示,不得进入普通请求日志;API 必须分列返回,避免复制/筛选时含义混淆。
type Decision struct {
Kind string // allow | flag | block | unavailable | invalid
HTTPStatus int
ErrorCode string
ClientMessage string
Legacy *LegacyDecision
Prompt *PromptDecision
AllowNextStage bool
}
稳定优先级:
content_policy_violation。prompt_guard_blocked。prompt_guard_invalid_response。prompt_guard_unavailable。不要让 Coordinator 暴露 Qwen 原始响应,也不要用一个布尔 Blocked 吞掉 unavailable/invalid 的差异。
鉴权与 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 模式可以并行执行两个引擎,但必须遵守:
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}
异常补偿:
queue_admission_busy,不得把 Redis 调用放进事务。payload_missing 失败,不可把预览当原文扫描。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,防止为了落库再次扫描或意外持久化原文。
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 rows;0 rows 表示租约已失效,本 Worker 必须丢弃结果。禁止仅按 status 条件更新,因为任务被回收并重新领取后 status 会再次变成 processing,旧 Worker 会误覆盖新结果。
setting key 固定为 prompt_audit_config,JSON 至少包含:
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
每个 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_enabled。strategy 第一版只接受 priority。保存时 canonicalize group IDs、scanner IDs 和 endpoint IDs,拒绝重复、空 ID、越界 worker/queue/timeout/input_limit。
GET config 和 PUT 成功响应只允许:
id, name, protocol, base_url, model, timeout_ms,
input_limit, enabled, has_token, token_status
不得出现 token、token_ciphertext、Authorization、解密失败原文或完整错误响应。后端 JSON 类型应物理分离,不能依赖 json:"-" 后复用内部对象。
config_version + 1,先安装本实例只读快照,再发布 Redis invalidation。统一前缀:/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 错误不得透传。
下表是提案编写时已有 checkContentModeration 调用点,实施时应机械替换并由结构测试锁定。路由别名共享相同 Handler,因此测试必须至少覆盖主路由与每类 alias。
| 协议/入口 | 路由 | 现有 Handler 文件/方法 | Stage | 拒绝构造器 |
|---|---|---|---|---|
| Anthropic Messages | POST /v1/messages |
gateway_handler.go: Messages 或 openai_gateway_handler.go: Messages |
http | Anthropic error helper |
| OpenAI Responses | POST /v1/responses、/responses、/backend-api/codex/responses 及 subpath |
gateway_handler_responses.go: Responses 或 openai_gateway_handler.go: Responses |
http | Responses/OpenAI helper |
| OpenAI Chat Completions | POST /v1/chat/completions、/chat/completions |
gateway_handler_chat_completions.go: ChatCompletions 或 openai_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 的实际提交入口。对额外入口有两种合法结论:接入 Coordinator;或证明它已在上游公共 Handler 处检查且不会二次收费/二次扫描。结论和测试必须加入路由矩阵,不能静默跳过。
接入位置不变量:鉴权、body limit、基本 JSON/model 校验之后;账号选择、用户/账号并发 slot、订阅/余额预扣、usage 写入、上游拨号和 SSE 首字节之前。
| 情况 | 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 |
{"error":"..."}。code;Claude 保留 permission_error/api_error type 并添加可选 code。error.code 和 canonical status,只在 google.rpc.ErrorInfo.reason 放稳定代码;metadata 仅 request_id。response.create 在任何本轮资源/上游副作用前扫描。response.create 重新提取本轮输入并标记 subsequent_turn。aicodex_ 前缀。prompt_audit_jobs、prompt_audit_events。events.job_id ON DELETE CASCADE;user/api_key/group 外键 ON DELETE SET NULL。username_snapshot、user_email_snapshot、api_key_name_snapshot 与 group name 快照分列保留,以免主体删除后事件无法复核;沿用现有管理员权限和数据保留策略。FOR UPDATE SKIP LOCKED 必须在同一短事务中选择并更新为 processing。事务内不要调用 Redis、Guard 或日志网络 sink。每次 claim 后立即提交,长工作在事务外执行。
created_at DESC, id DESC。{filter_hash,snapshot_max_id,admin_id,issued_at,expires_at},返回默认 5 分钟有效的 confirmation_token。confirm=true 和强制时间范围,查询强制 id <= snapshot_max_id 后分批提交;预览后的新事件不可被删除。请求固定发送到规范化 {base_url}/v1/chat/completions,默认模型 sileader/qwen3guard:0.6b,role=user、temperature=0、max_tokens=64、seed=42。
保存、probe 和实际扫描必须走同一校验/Transport:
九类 scanner ID/展示名必须稳定:Violent、Non-violent Illegal Acts、Sexual Content or Sexual Acts、PII、Suicide & Self-Harm、Unethical Acts、Politically Sensitive Topics、Copyright Violation、Jailbreak。
建议 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。has_token/token_status 表示存在性。clear_token=true,不能把输入框空值当清除。blocking_enabled 从 false → true 必须二次确认;关闭 enabled 同时把 draft blocking 设 false。每个阶段应可单独评审、测试和回滚,建议五组 PR:
不要在同一 PR 混入无关的 ContentModeration 重构、全局 Handler 重写、前端框架升级或数据库清理。若为接线必须改现有文件,变更应机械、薄且有前后行为测试。
| 事项 | 默认建议 | 必须在何时确认 | 未确认时行为 |
|---|---|---|---|
| 源基线标识 | 专用 commit/tag | PR 1 前 | 不开始移植 |
| 自动保留期 | 第一版只安全删除 | migration 冻结前 | 不加自动清理 |
| 双引擎并行/串行 | 并行 | PR 4 前做 benchmark/race | 可先串行但保留优先级 |
| 额外文本入口 | routes 自动枚举 | PR 4 接线前 | 结构测试失败 |
| blocking 阈值 | 运营按 async 数据登记 | 生产 blocking 前 | 只允许 off/async |
ContentModerationService,导致配置、表和副作用混用。redacted_preview 当作可重试扫描正文;这会产生错误分类且破坏完整覆盖。response.create。content_moderation_logs 或改变 /admin/risk-control。只有全部成立才算实现完成:
verification.md 有测试/SQL/日志/截图证据。