Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
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

This commit is contained in:
李建琦
2026-08-21 18:30:13 +08:00
commit 6d655c9903
3584 changed files with 1270640 additions and 0 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-16
@@ -0,0 +1,5 @@
# add-openai-compatible-prompt-audit
在不改变现有内容审核行为的前提下,新增独立的 OpenAI 兼容 Qwen3Guard 提示词安全审计模块,完整支持异步审计、同步阻断、持久任务队列、事件工作台与独立管理页面。
阅读顺序:`proposal.md``source-baseline.md` / `source-feature-map.md``design.md` → 三个 `specs/*/spec.md``implementation-guide.md``tasks.md``verification.md`
@@ -0,0 +1,752 @@
## Context
### 当前系统
sub2api 当前已经存在一套完整的内容审核能力:
- 核心实现位于 `backend/internal/service/content_moderation*.go`
- 管理 API 位于 `backend/internal/handler/admin/content_moderation_handler.go`,路由前缀为 `/admin/risk-control`
- 网关统一接线位于 `backend/internal/handler/content_moderation_helper.go`,各协议 Handler 在解析完请求体和模型后调用 `checkContentModeration`
- 数据保存在 `content_moderation_logs`,配置保存在 settings 的 `content_moderation_config`
- 管理页面为 `frontend/src/views/admin/RiskControlView.vue`
- 能力包括 OpenAI Moderations、关键词阻断、命中 Hash、异步观察、同步前置阻断、API Key 健康、邮件、违规计数和自动封号。
该能力不是本次要迁移的 aicodex-api “提示词审计”:两者使用不同模型、分类、队列、事件和阻断语义。把 Qwen3Guard 直接塞入 ContentModerationService 会让现有阈值、封号统计和记录含义失真,也会继续扩大已经接近 3000 行的单文件。
### 参考能力
参考仓库 `/Users/mt/code/mt-ai/aicodex/aicodex-api` 当前磁盘实现提供:
- OpenAI 兼容 Qwen3Guard 审计池。
- 持久 PromptAuditJob / PromptAuditEvent。
- Redis 30 分钟临时原文载荷。
- 进程内 Worker、重试、租约和滞留回收。
- 脱敏快照、Hash、Unicode 分片、最新输入优先。
- 九类风险和严格 `Safety/Categories` 解析。
- 异步审计与同步 fail-closed 阻断。
- HTTP、SSE、Responses WebSocket 错误映射。
- 节点探测、运行态、事件筛选/详情/硬删除和独立控制台页面。
参考仓库 `yjb` 分支当前包含未提交的同步阻止改动。因此实施开始前必须固定源 commit/tag 或生成包含未提交文件的只读 patch 清单,作为功能对照和测试移植的权威基线。
### 目标项目约束
- PostgreSQL SQL migrations 是 schema 的事实源,Ent 自动迁移不是生产建表入口。
- 后端是 Go + Gin + Wire;前端是 Vue 3 + TypeScript + pnpm。
- Redis 已是运行基础设施,可作为短 TTL 敏感载荷存储和配置失效通知通道。
- 新模块必须尽量集中在独立目录,并只通过显式接口接入现有 Handler。
- 新功能默认关闭,不能改变升级前行为。
- 完整提示词和 Guard 凭据不能进入数据库、日志、API、前端或错误响应。
### 参与边界
- 网关请求处理:提供可信身份上下文、协议、模型和原始请求体。
- 安全审计协调器:调用两个独立引擎并归并阻断结果。
- 现有内容审核:保持原实现和副作用。
- 新 Prompt Audit 模块:负责配置、提取、队列、Guard、事件、运行态和管理 API。
- PostgreSQL:持久任务与事件。
- Redis:扫描正文 TTL、配置失效通知、可选跨实例心跳/指标汇总。
- 控制台:独立提示词审计页面。
## Goals / Non-Goals
**Goals:**
- 在不改变现有内容审核语义的前提下完整引入提示词输入审计。
- 使用模块化垂直目录封装新能力,限制对现有代码的修改面。
- 保持所有现有 OpenAI/Claude/Gemini/媒体兼容入口的请求和响应 envelope。
- 提供异步不阻塞和同步 fail-closed 两种模式。
- 在同步 Block/Unavailable 时保证无账号、无计费、无上游副作用。
- 支持多实例持久任务消费和配置最终一致。
- 只持久化脱敏、可关联、可复核的数据。
- 把运行态、日志、指标和测试设计为第一等反馈信号。
- 提供完整、独立、可访问的管理页面。
**Non-Goals:**
- 不审核模型输出,不在流式输出中途截断。
- 不实现请求正文 Redact 或自动改写。
- 不实现人工审批、申诉、逐请求放行或策略工作流。
- 不把 Qwen3Guard 分类映射为现有 OpenAI Moderations 分数。
- 不让提示词审计命中触发自动封号、邮件或 Hash 黑名单。
- 不删除、合并或迁移 `content_moderation_logs`
- 不新增目标项目不存在的 AICodex 专属产品路由;只对目标项目实际存在的文本入口提供等价覆盖。
- 不在本 change 中重构整个 Handler、计费或账号调度架构。
## Decisions
### 1. 迁移行为契约,而不是直接复制源目录
源模块依赖 aicodex-api 的 Ent 全局客户端、option 模型、Gin context key、日志封装、Caddy/gatewaycore 和 React 控制台,不能原样复制到目标项目。
实施时以本 change 的 specs 和验收矩阵作为权威行为契约,再选择目标项目已有的 SettingRepository、Redis、SecretEncryptor、Gin Handler、SQL migration 和 Vue 组件实现。
**备选方案:直接复制 `internal/service/promptaudit`。** 放弃,因为会引入大量适配壳、全局状态和源仓库私有依赖,并且源工作区当前未提交。
### 2. 使用模块化垂直目录承载新能力
新增目录:
```text
backend/internal/securityaudit/
├── coordinator.go
├── prompt_config.go
├── prompt_types.go
├── prompt_snapshot.go
├── prompt_scanner.go
├── prompt_qwen3guard.go
├── prompt_outbound_security.go
├── prompt_repository.go
├── prompt_payload_store.go
├── prompt_enqueue.go
├── prompt_worker.go
├── prompt_guard.go
├── prompt_runtime.go
├── prompt_handler.go
├── prompt_logging.go
├── prompt_module.go
└── *_test.go
```
该目录内部允许用文件划分子职责,但对外只暴露:
- `Coordinator.Check(ctx, Request) Decision`
- `PromptService` 生命周期与管理方法
- `PromptAdminHandler`
- Wire provider set
SQL migration、前端和少量路由/注入接线由于项目结构约束仍位于各自事实源目录。
**备选方案:继续平铺在 `internal/service`、`internal/repository` 和 `internal/handler`。** 放弃,因为无法满足独立模块要求,也会增加 AI 和人工定位所需上下文。
### 3. 使用薄协调器组合两个引擎
目标调用关系:
```mermaid
flowchart LR
H[Protocol Handler] --> C[SecurityAudit Coordinator]
C --> M[Existing ContentModerationService]
C --> P[PromptAuditService]
M --> MD[Moderation Decision]
P --> PD[Prompt Decision]
MD --> C
PD --> C
C --> D[Normalized gateway decision]
```
Coordinator 只承担:
1. 接收可信身份和请求快照。
2. 确保新异步任务即使现有引擎随后阻断也能 best-effort 投递。
3. 在新同步模式下执行两个引擎并等待结果。
4. 使用固定优先级生成客户端决策。
优先级:
1. 现有内容审核 Block:保留原状态、错误码和文案。
2. Prompt Guard Block403 + `prompt_guard_blocked`
3. Prompt Guard Invalid503 + `prompt_guard_invalid_response`
4. Prompt Guard Unavailable503 + `prompt_guard_unavailable`
5. 否则 Allow。
两个引擎的事件和副作用独立。Coordinator 不持久化业务事件,不修改风险分数。
**同步执行策略:** 当 Prompt Guard blocking 开启时,现有内容审核和 Prompt Guard 可在独立受控 goroutine 中并行执行,共享请求取消信号但不共享 mutable state。必须等待两者完成或各自 deadline 到期,以保留两个引擎的审计完整性。若实现评审认为并行引入的复杂度过高,可先串行执行,但仍必须满足既有 Block 响应优先级和无下游副作用测试。
### 4. 复用现有接入位置,但显式改名为安全审计
把各协议 Handler 的 `checkContentModeration` 调用机械替换为 `checkSecurityAudit`,保持调用点仍在:
- 身份鉴权、基本请求体读取和协议格式校验之后。
- 账号选择、账户并发、计费资格、预扣、上游拨号/写入之前。
现有 `content_moderation_helper.go` 改为或新增 `security_audit_helper.go`,构造统一 `securityaudit.Request`
```go
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
}
```
请求体必须在 Handler 已受全局大小限制后传入。模块不得再次从 `http.Request.Body` 读取,避免破坏转发。
### 5. 保持三个独立开关层级
有效开关:
1. `risk_control_enabled`:现有安全审计总入口和菜单开关。
2. `content_moderation_config.enabled/mode`:现有内容审核。
3. `prompt_audit_config.enabled/blocking_enabled`:新提示词审计。
Prompt Audit 有效模式:
| risk_control | enabled | blocking_enabled | 有效行为 |
| --- | --- | --- | --- |
| false | 任意 | 任意 | off |
| true | false | false | off |
| true | true | false | async_audit |
| true | true | true | blocking |
后端必须拒绝 `enabled=false && blocking_enabled=true`。前端联动只提升体验,不能替代后端校验。
### 6. 配置使用 settings JSON,但凭据独立加密
新增 setting key`prompt_audit_config`
配置结构包含:
```text
enabled
blocking_enabled
store_pass_events
strategy=priority
worker_count
queue_capacity
scanners[]
all_groups
group_ids[]
config_version
updated_at
updated_by
change_summary
endpoints[]
```
每个 endpoint 持久化:
```text
id, name, protocol=openai_compatible, base_url, model,
token_ciphertext, timeout_ms, input_limit, enabled
```
读取 API 只返回 `has_token`/`token_status`。保存请求使用:
- `token` 非空:替换并加密。
- `token` 空且 `clear_token=false`:保留已有密文。
- `clear_token=true`:删除密文。
config_version 每次成功保存单调加一。change_summary 只保存节点数量、开关、分类数量、分组数量及其 Hash 等脱敏摘要。
保存请求必须携带管理员读取草稿时的 `expected_config_version`。ConfigStore 在 PostgreSQL 短事务中获取 `prompt_audit_config` 专用 advisory transaction lock,重新读取 settings 当前值并比较版本;不一致时返回 409 `prompt_audit_config_conflict`,不得覆盖其他管理员的新配置。版本一致时才计算 current+1、加密并写回。首次无 setting 时按 version=1/default-off 参与比较。进程内 mutex 不能代替该多实例 CAS。
**备选方案:新增配置表。** 第一版放弃,因为目标项目已有 settings 配置模式,源实现也使用 option JSON;任务和事件才需要独立关系表。
### 7. 配置使用内存快照和 Redis 失效通知
PromptService 维护原子只读配置快照:
- 启动时加载并校验。
- 保存成功后先安装本实例快照,再 publish `sub2api:prompt_guard:config:invalidate`,消息只包含版本。
- 其他实例收到通知后重新从 settings 加载、解密、校验并原子替换。
- Redis publish 失败时保留最后有效配置,并通过 5 秒有界 TTL 后台刷新。
- 请求热路径只读取快照,不查询数据库。
运行态返回 expected 和 active version。配置加载失败不得清空最后有效快照;冷启动无有效快照时不得伪装为关闭或健康。
### 8. 使用提示词专用快照提取器,不直接复用现有截断结果
复用现有内容审核提供的 protocol 常量、身份/分组上下文和部分 JSON 内容块解析思路,但新模块实现独立 `PromptSnapshotExtractor`
- Chat Completions:只提取 role=user 的文本内容。
- Responses:支持 input 字符串、消息数组和 content blocks。
- Claude Messages:提取 role=user 文本块。
- Gemini:提取 user contents/parts 文本。
- Images/媒体:只提取 prompt 文本,忽略图片载荷。
- Responses WS:解析每个 response.create 帧。
扫描顺序:
1. 最新非空用户输入独立作为首段。
2. 其余用户历史保持确定顺序。
3. 每段再按 Unicode rune 分片。
数据库预览使用统一脱敏器:移除/掩码 API Key、Bearer、常见凭据、邮箱/电话等敏感模式,随后按 rune 裁剪。Hash 使用实际待扫描文本的 SHA-256。
### 9. PostgreSQL 使用两个新表,SQL migration 为事实源
建议 migration 名称:`backend/migrations/181_prompt_audit.sql`。如果实施时已有 181,则按当前最大序号递增,不允许修改已应用 migration。
#### `prompt_audit_jobs`
```sql
CREATE TABLE prompt_audit_jobs (
id BIGSERIAL PRIMARY KEY,
request_id VARCHAR(128) NOT NULL DEFAULT '',
user_id BIGINT REFERENCES users(id) ON DELETE SET NULL,
username_snapshot VARCHAR(255) NOT NULL DEFAULT '',
user_email_snapshot VARCHAR(320) NOT NULL DEFAULT '',
api_key_id BIGINT REFERENCES api_keys(id) ON DELETE SET NULL,
api_key_name_snapshot VARCHAR(255) NOT NULL DEFAULT '',
group_id BIGINT REFERENCES groups(id) ON DELETE SET NULL,
group_name VARCHAR(255) NOT NULL DEFAULT '',
provider VARCHAR(64) NOT NULL DEFAULT '',
endpoint VARCHAR(128) NOT NULL DEFAULT '',
protocol VARCHAR(64) NOT NULL DEFAULT '',
model VARCHAR(255) NOT NULL DEFAULT '',
prompt_hash VARCHAR(64) NOT NULL DEFAULT '',
redacted_preview TEXT NOT NULL DEFAULT '',
prompt_length INT NOT NULL DEFAULT 0,
message_count INT NOT NULL DEFAULT 0,
execution_mode VARCHAR(32) NOT NULL DEFAULT 'async_audit',
config_version BIGINT NOT NULL DEFAULT 1,
status VARCHAR(32) NOT NULL DEFAULT 'staging',
attempts INT NOT NULL DEFAULT 0,
max_attempts INT NOT NULL DEFAULT 3,
claim_version BIGINT NOT NULL DEFAULT 0,
next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
processing_started_at TIMESTAMPTZ,
processed_at TIMESTAMPTZ,
last_error_code VARCHAR(64) NOT NULL DEFAULT '',
last_error_message TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
状态集合:`staging|queued|processing|retry|done|failed`
关键索引:
```text
(status, next_attempt_at, id)
(request_id)
(user_id, created_at DESC)
(api_key_id, created_at DESC)
(group_id, created_at DESC)
(prompt_hash)
(created_at DESC)
```
#### `prompt_audit_events`
```sql
CREATE TABLE prompt_audit_events (
id BIGSERIAL PRIMARY KEY,
job_id BIGINT NOT NULL REFERENCES prompt_audit_jobs(id) ON DELETE CASCADE,
request_id VARCHAR(128) NOT NULL DEFAULT '',
user_id BIGINT REFERENCES users(id) ON DELETE SET NULL,
username_snapshot VARCHAR(255) NOT NULL DEFAULT '',
user_email_snapshot VARCHAR(320) NOT NULL DEFAULT '',
api_key_id BIGINT REFERENCES api_keys(id) ON DELETE SET NULL,
api_key_name_snapshot VARCHAR(255) NOT NULL DEFAULT '',
group_id BIGINT REFERENCES groups(id) ON DELETE SET NULL,
group_name VARCHAR(255) NOT NULL DEFAULT '',
provider VARCHAR(64) NOT NULL DEFAULT '',
endpoint VARCHAR(128) NOT NULL DEFAULT '',
protocol VARCHAR(64) NOT NULL DEFAULT '',
model VARCHAR(255) NOT NULL DEFAULT '',
prompt_hash VARCHAR(64) NOT NULL DEFAULT '',
redacted_preview TEXT NOT NULL DEFAULT '',
decision VARCHAR(32) NOT NULL DEFAULT 'pass',
risk_level VARCHAR(32) NOT NULL DEFAULT 'low',
action VARCHAR(32) NOT NULL DEFAULT 'Allow',
categories JSONB NOT NULL DEFAULT '[]'::jsonb,
matched_scanners JSONB NOT NULL DEFAULT '[]'::jsonb,
scanner_scores JSONB NOT NULL DEFAULT '{}'::jsonb,
scanner_evidence JSONB NOT NULL DEFAULT '{}'::jsonb,
scanner_backend VARCHAR(64) NOT NULL DEFAULT 'qwen3guard-openai',
scanner_version VARCHAR(128) NOT NULL DEFAULT '',
guard_endpoint_id VARCHAR(128) NOT NULL DEFAULT '',
policy_id VARCHAR(128) NOT NULL DEFAULT '',
policy_version INT NOT NULL DEFAULT 0,
config_version BIGINT NOT NULL DEFAULT 1,
chunk_total INT NOT NULL DEFAULT 0,
latency_ms INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
事件保留请求快照列用于稳定查询,即使 user/API key/group 后续删除仍保留管理员可复核上下文。用户名、邮箱和 API Key 名称必须作为不同字段返回,不能拼成不可筛选的单一展示串;这些身份快照沿用现有管理员数据访问和保留规则,不得写入普通请求日志。外键使用 SET NULL,快照字段保留。
事件索引:job、request、decision/time、risk/time、user/time、API key/time、group/time、Hash、created_at。
不得创建 raw_prompt、raw_request、payload、token 等列。
### 10. 跨 PostgreSQL/Redis 投递使用 staging 状态避免竞态
异步投递顺序:
1. 检查有效模式、范围和节点;在 PostgreSQL 短事务内获取 Prompt Audit 队列准入 advisory transaction lock,重新统计 active jobs,并仅在低于 snapshot queue_capacity 时插入 staging job。
2. 提取快照。
3. 插入 `status=staging` 的 job。
4. `SET sub2api:prompt_audit:payload:<job_id> <scan_text> EX 1800`
5. 条件更新 staging → queued。
6. 输出 `prompt_audit.job_enqueued`
Worker 只领取 queued/retry,因此不会在 Redis SET 前看到任务。
失败处理:
- 步骤 3 失败:不写 Redis。
- 步骤 4 失败:job → failed,原请求继续。
- 步骤 5 失败:删除 Redis keyjob 由 staging 清理器标记 failed。
- 进程在 4/5 之间退出:Redis 自动过期,staging 回收器标记 failed。
这比源实现“先 queued 再写 Redis”更适合多实例,避免 Worker 提前领取。
队列容量检查和 staging INSERT 必须在同一准入锁事务中完成,防止多个实例先各自看到剩余容量再共同超限。锁等待必须有很短的有界 timeout;无法及时取得锁时按 `queue_admission_busy` 丢弃异步审计任务并让主请求继续。Redis 写入不在该事务内。
### 11. Worker 使用 PostgreSQL 原子领取与租约
Repository 使用短事务:
```sql
WITH candidate AS (
SELECT id
FROM prompt_audit_jobs
WHERE status IN ('queued', 'retry')
AND next_attempt_at <= NOW()
ORDER BY next_attempt_at, id
FOR UPDATE SKIP LOCKED
LIMIT 1
)
UPDATE prompt_audit_jobs j
SET status = 'processing',
attempts = attempts + 1,
claim_version = claim_version + 1,
processing_started_at = NOW(),
updated_at = NOW()
FROM candidate
WHERE j.id = candidate.id
RETURNING j.*;
```
Worker 必须把 RETURNING 得到的 `claim_version` 作为 fencing token 保存到本次执行上下文。每处理一个分片前以 `id + status=processing + claim_version` 条件更新 `processing_started_at`;创建事件、标记 done/retry/failed 同样必须校验 claim_version 并检查 affected rows。回收后再次领取会递增版本,因此旧 Worker 即使稍后恢复也不能覆盖新领取者的结果。
回收器每分钟扫描一小批超时 processing
- attempts < max_attempts → retry。
- attempts >= max_attempts → failed。
退避建议:5s、30s、2m,上限 5m并加少量 jitter。401/403 和 invalid_response 不重试;429、5xx、连接错误和超时可重试。
Runner 生命周期由应用启动/停止管理:
- Start 验证 DB、Redis、配置。
- Worker panic 单任务恢复并记录,不能杀死进程。
- Shutdown 停止领取新任务,等待活动任务到有界超时。
### 12. OpenAI 兼容 Client 使用严格 Qwen3Guard 契约
请求:
```json
{
"model": "sileader/qwen3guard:0.6b",
"messages": [{"role": "user", "content": "<chunk>"}],
"temperature": 0,
"max_tokens": 64,
"seed": 42
}
```
解析要求:
- 响应体上限 256 KiB。
- 只接受一个非空 `Safety:` 行和一个 `Categories:` 行。
- 只接受 Safe、Controversial、Unsafe。
- 不允许额外非空说明。
- 类别做大小写/标点别名归一,但未知类别必须保留风险事实。
策略映射:
| Safety | 已启用类别 | 结果 |
| --- | --- | --- |
| Safe | 任意 | Pass / Allow |
| Controversial | 普通类别 | Flag / Warn |
| Controversial | Jailbreak/PII/Suicide & Self-Harm | Critical / Block |
| Unsafe | 至少一个启用类别 | Critical / Block |
| Unsafe | 未知类别 | Critical / Block + unknown_unsafe |
| Unsafe | 仅命中明确禁用类别 | Flag / Warn,保留事实 |
scanner score 只用于展示排序,不得被解释为真实置信度阈值。
管理 API 还应从 categories、scanner evidence 和 Guard policy 确定性派生 `issue_summaries`。每项至少包含 category、scanner_id、title、description、severity/label、action/label、code、score 和脱敏 evidence;可选位置必须是 rune 范围和不可逆命中 Hash,不能返回原文。该摘要是展示 DTO,不要求新增数据库列,防止复制同一风险事实。
### 13. 同步 Guard 使用共享 deadline、故障切换和 bulkhead
同步 evaluator
- 全局并发上限默认 64。
- 每节点并发上限默认 16。
- 总 deadline 使用第一启用节点 timeout。
- 所有分片和节点故障切换共享 deadline。
- 顺序扫描,最新输入优先。
- Block 可早停;Allow 必须所有必要分片成功。
- 连接失败、429、5xx、超时可切下一节点。
- 401/403、invalid_response 终止。
- 所有节点失败或 bulkhead 满 → Unavailable。
第一版不使用熔断器外部依赖;连续失败健康状态和冻结窗口可用模块内小状态机实现。若后续数据证明需要通用熔断库,另起 change。
### 14. 出站 HTTP Client 使用管理员配置的网络目标
保存、探测和实际调用共用同一校验:
- 仅接受结构有效的 http/https Base URL,并禁止会破坏固定 API 路径拼接的 userinfo、query、fragment。
- 私网、回环、link-local、metadata、保留地址及域名解析结果均不做目标类别拦截。
- HTTP 与 HTTPS 均可由管理员选择;使用标准 DialContext 和标准重定向行为。
- 节点目标的可信性、网络可达性和协议安全由管理员负责。
- 独立连接池、Dial/TLS/Header timeout、响应上限。
- 日志只记录 endpoint ID,不记录完整 URL。
### 15. HTTP、SSE 和 WebSocket 使用协议原有错误构造器
HTTP 错误:
| 情况 | HTTP | error_code |
| --- | ---: | --- |
| Block | 403 | prompt_guard_blocked |
| Unavailable | 503 | prompt_guard_unavailable |
| Invalid response | 503 | prompt_guard_invalid_response |
Handler 使用自己已有的 OpenAI、Claude 或 Gemini error helper。正文只包含通用中文消息、code 和 request ID。
现有 helper 需要通过最小协议适配器扩展稳定代码,不能破坏原字段:
- OpenAI Chat/Responses:保持 `error.type/message` 或 Responses 现有结构,并设置 `error.code=<prompt_guard_*>`
- Claude Messages:保持 `type=error` 和合法的 `error.type=permission_error|api_error`,增加可选 `error.code=<prompt_guard_*>`
- Gemini:保持 Google envelope 的数值 `error.code`、message 和 canonical status;在 `error.details[]` 增加 `type.googleapis.com/google.rpc.ErrorInfo`,其 `reason=<prompt_guard_*>`、domain=`sub2api.securityaudit`metadata 只允许 request_id。
不得把 Gemini 数值 `error.code` 替换为字符串,也不得把类别、Prompt、节点或内部错误放入 details。协议 golden test 必须锁定三类 envelope。
SSE 必须在 Guard 完成前不写 response header/首字节。
Responses WebSocket
- 握手本身无 prompt,不执行输入分类。
- 首个 response.create 在用户/账号 slot、计费和上游拨号前检查。
- 每个后续 response.create 在本轮 slot、计费和上游发送前重新检查。
- Blockclose 4403reason prompt_guard_blocked。
- Unavailable/Invalidclose 1013,对应稳定 reason。
- 日志 stage=first_turn/subsequent_turn。
### 16. 同步结果采用独立轻量记录路径
同步 evaluator 返回:
```text
decision, action, risk_level, categories,
matched_scanners, scores, evidence,
scanner_backend/version, endpoint_id,
policy_id/version, chunk_total, latency,
error_code, allow_next_stage
```
记录 adapter
- 不接受完整 scan_text,只接受脱敏 PromptSnapshot。
- 创建 `execution_mode=blocking,status=done` 的 job。
- 按 store_pass_events 决定是否创建事件。
- 在单个 DB transaction 内完成 job + event。
- 记录失败只增加指标和日志,不改变 evaluator 已确定结果。
- 禁止再次调用 Guard。
### 17. 管理 API 使用独立前缀和现有管理员审计
新增:
```text
GET /admin/prompt-audit/config
PUT /admin/prompt-audit/config
POST /admin/prompt-audit/endpoints/probe
GET /admin/prompt-audit/runtime
GET /admin/prompt-audit/events
GET /admin/prompt-audit/events/:id
DELETE /admin/prompt-audit/events/:id
POST /admin/prompt-audit/events/batch-delete
POST /admin/prompt-audit/events/delete-preview
POST /admin/prompt-audit/events/delete-by-filter
```
所有写操作和敏感探测复用 AdminAuth 和现有管理操作审计。审计 detail 采用 allowlist 字段,不使用“先记录完整结构再删除敏感 key”的方式。
删除规则:
- 单次批量 ID 数量有上限。
- 按筛选删除必须带开始/结束时间、预览 Hash、服务端认证 confirmation_token 和 confirm。
- preview 在同一数据库快照中返回 matched_count、`snapshot_max_id``filter_hash = SHA-256(canonical JSON filter summary + snapshot_max_id)`
- confirmation_token 是由现有 SecretEncryptor 认证加密的短期 claim,绑定 filter_hash、snapshot_max_id、管理员 ID、签发/过期时间(默认 5 分钟)。delete-by-filter 必须解密、校验操作者/过期时间/Hash,并强制 `id <= snapshot_max_id`;客户端自行计算 SHA-256 不能绕过预览,预览后的新事件不能被本次操作删除。
- 删除分批执行,避免长事务。
- 删除事件后只删除无任何事件引用且非 processing 的孤立 job。
- 尝试清理对应 Redis key。
### 18. 控制台使用独立 feature 目录
```text
frontend/src/features/prompt-audit/
├── PromptAuditView.vue
├── api.ts
├── types.ts
├── viewModel.ts
├── components/
└── __tests__/
```
少量外部接线:
- router 增加 `/admin/prompt-audit`,复用 requiresAuth/requiresAdmin/requiresRiskControl。
- Sidebar 把现有 risk-control 单项改为 expandOnly “安全审计”分组,子项保留原路由并新增提示词路由。
- i18n 增加 zh/en 对称键。
页面分区:
1. 运行概览。
2. 审计池表格和参数/探测对话框。
3. 分组范围和九类 scanner。
4. Worker/队列/配置版本/Guard 指标。
5. 事件筛选、表格、详情、删除。
6. 固定保存栏:enabled、blocking、store pass、保存/重置。
页面不得在 localStorage/sessionStorage 保存 API Key。保存成功后立即清除输入 state。
### 19. 日志和指标使用稳定词典
最小事件:
```text
prompt_audit.config_updated
prompt_guard.config_loaded
prompt_guard.config_reload_degraded
prompt_audit.endpoint_probe_started
prompt_audit.endpoint_probe_finished
prompt_audit.endpoint_probe_failed
prompt_audit.job_enqueued
prompt_audit.enqueue_skipped
prompt_audit.enqueue_dropped
prompt_audit.started
prompt_audit.processing_reclaimed
prompt_audit.processed
prompt_audit.process_failed
prompt_audit.finding_recorded
prompt_audit.scan_chunk_started
prompt_audit.scan_chunk_completed
prompt_audit.scan_chunk_failed
prompt_audit.scan_chunks_aggregated
prompt_guard.evaluation_started
prompt_guard.allowed
prompt_guard.blocked
prompt_guard.failed
prompt_guard.result_record_failed
prompt_audit.event_deleted
prompt_audit.events_deleted
prompt_audit.events_delete_previewed
prompt_audit.events_filter_deleted
```
字段采用 allowlistrequest_id、user_id、api_key_id、group_id、provider、protocol、endpoint、model、job_id、event_id、config_version、guard_endpoint_id、decision、risk_level、action、chunk_index、chunk_total、chunk_chars、input_chars、input_limit、latency_ms、status、error_code、error_kind、queue_length/capacity、stage、upstream_dispatched、billing_preconsumed。
禁止:body、raw_prompt、payload、token、authorization、完整 Base URL/query、Redis value。
指标:异步 enqueue/dropped、队列各状态、processed/failed、Worker active、Guard total/allow/flag/block/unavailable/invalid/timeout/failover/bulkhead/record_failure、延迟直方图。Guard 结果与延迟由同步 evaluator 和异步 Worker 使用同一稳定指标结构观测,使 blocking 启用前可以先在 async 测试分组建立 P50/P95/P99、失败率和事件增长率基线;runtime 同时返回 async enqueue/dropped 计数以区分投递与扫描阶段。
### 20. 测试按行为矩阵而不是文件覆盖率验收
核心矩阵:
| 维度 | 值 |
| --- | --- |
| 引擎 | 现有 moderation / prompt audit / 两者 |
| Prompt 模式 | off / async / blocking |
| 协议 | chat / responses / messages / gemini / images-media / responses-ws |
| 返回 | allow / flag / block / unavailable / invalid |
| 流式 | non-stream / SSE / WS first / WS subsequent |
| 副作用 | account selection / billing / upstream |
必须有结构测试验证所有现有调用点经过 Coordinator;必须有 stub 统计 Block/Unavailable 时账号选择、计费和上游调用均为 0。
敏感信息测试对日志、DB row、API JSON、前端 state snapshot 做 canary secret 断言。
### 21. 不新增外部运行时依赖
使用现有 go-redis、database/sql、Gin、SecretEncryptor、logger、Vue 3、Axios 和测试工具。Qwen3Guard 是外部 OpenAI 兼容服务,不在本仓库启动模型进程。
不引入新的 Go 队列库、ORM、前端状态库或 UI 框架。
## Risks / Trade-offs
- [两个同步引擎会增加首字节延迟] → 只有管理员显式开启 blocking 才发生;并行执行、最新输入优先、Block 早停、共享 deadline、连接池和分组灰度。
- [Guard 故障在 fail-closed 下影响可用性] → 多节点有序故障切换、bulkhead、真实探测、运行态告警和一键关闭 blockingUnavailable 与 Block 使用不同错误码。
- [Qwen3Guard 误报导致合法请求被拒绝] → 先运行 async 建立误报基线,再按 group 灰度 blocking;保留独立事件,不直接触发封号。
- [两个引擎同时 Block 时语义冲突] → 固定现有内容审核响应优先级,两个事件仍独立记录。
- [PostgreSQL/Redis 非事务导致悬挂状态] → staging → Redis SET → queued 发布协议;staging 回收和 TTL 清理。
- [多实例重复消费或旧 Worker 覆盖新结果] → `FOR UPDATE SKIP LOCKED` 原子领取、递增 claim_version fencing token、processing 租约和带版本条件更新。
- [长提示词导致超时] → Unicode 分片、总 deadline、最新输入优先;Allow 必须完整覆盖,禁止部分结果放行。
- [管理员配置的节点可访问服务端可达的任意网络目标] → 产品明确由管理员负责节点目标;继续使用加密密文、日志/API allowlist、响应上限和 canary 泄露测试保护凭据与数据。
- [手工接入多个 Handler 造成漏路由] → 将现有调用统一替换为 Coordinator 并增加静态/结构路由矩阵测试。
- [新模块仍反向侵入现有 service] → 新模块依赖现有端口;现有 ContentModerationService 不导入新模块,Handler 仅注入 Coordinator。
- [事件量过大] → 默认不保存 Pass,分页索引、分批删除;后续根据真实规模单独设计自动保留期。
- [源参考继续变化] → 实施前冻结源基线,本 change specs 作为目标实现最终权威。
## Migration Plan
### 阶段 0:冻结和对照
1. 记录参考仓库 commit、branch 和 `git diff --stat`
2. 对未提交的同步阻止文件生成只读 patch 或提交到专用分支。
3. 建立“源功能 → 本 change requirement → 目标测试”追踪表。
### 阶段 1:纯数据和配置基础
1. 新增 SQL migration 和 Repository 测试。
2. 新增加密配置、Public DTO、URL 校验和 config cache。
3. 新增管理 API 的 config/probe/runtime 骨架。
4. 保持 enabled=false,不接网关。
### 阶段 2:异步审计
1. 实现 PromptSnapshot、脱敏、Hash 和协议提取。
2. 实现 staging 投递、Redis Payload Store、Worker、重试和回收。
3. 实现 OpenAI 兼容 Client、Qwen parser、分片聚合和事件。
4. 接入 Coordinator 的 async 分支;队列故障不影响请求。
### 阶段 3:控制台和运营闭环
1. 完成页面、节点探测、配置、运行态和事件列表/详情。
2. 完成单条、批量和按筛选删除。
3. 运行前后端 lint、typecheck、unit/integration test。
### 阶段 4:同步门禁
1. 实现 evaluator、bulkhead、deadline、故障切换和错误映射。
2. 完成 HTTP/SSE 入口接线。
3. 完成 Responses WS 首轮与后续帧接线。
4. 用副作用 stub 证明 Block/Unavailable 无账号、无计费、无上游。
### 阶段 5:灰度上线
1. 生产先保持 Prompt Audit off。
2. 开启 async,只选测试 group,观察 Guard 延迟、失败、误报和事件量。
3. 建立良性/恶意回归语料。
4. 仅在多节点稳定、Unavailable 率和 P99 满足阈值后开启 blocking。
5. 按 group 扩大范围。
### 回滚
- 首选:关闭 blocking_enabled,立即回到 async。
- 次选:关闭 enabled,完全停止新 Prompt Audit。
- 必要时关闭 risk_control_enabled,但这也会停用现有内容审核入口,应作为最后手段。
- 回滚不删除表、配置或历史事件,不回退已应用 migration。
- Worker 停止后 queued/retry 任务保留;恢复时继续处理,或由管理员按明确策略清理。
## Resolved Decisions
1. **源基线标识**:采用 `source-freeze/` 中的只读 tracked patch + untracked archivebase commit、SHA-256 和恢复测试已登记在 `source-baseline.md`
2. **事件自动保留期**:第一版只提供管理员安全删除,不增加自动保留清理;真实事件量稳定后另起 change。
3. **同步两个引擎并行或串行**:采用受控并行;实现必须通过 race test,并保持 Legacy Block 优先级和两引擎独立记录。
4. **目标项目额外文本入口**:以实施时 `backend/internal/server/routes/gateway.go` 的自动/结构枚举为事实源;所有用户文本入口必须接入 Coordinator 或提供不会旁路/重复扫描的测试证明。
5. **生产启用阈值**:实现和部署验证期间只允许 off/async;blocking 生产启用必须满足 `verification.md` 的建议阈值并由安全、运营和业务责任人签字,未签字不得生产开启。
@@ -0,0 +1,31 @@
# Prompt Audit implementation evidence
This file records reproducible implementation-time evidence. It contains no prompt bodies, Guard credentials, Authorization values, or Redis payloads.
## 2026-07-16 — source freeze and target baseline
### Frozen source restore
- Base commit: `7a50378851a80650cb0c086260b23abeb3469e6b`
- Freeze manifest: `source-freeze/MANIFEST.md`
- Manifest SHA-256: `badab312bf6af4d2c77857a9400381f4da4fbf45722d9f4a6df23bc7005273b6`
- Restore result: tracked patch and untracked archive restored into a detached worktree; `git diff --check` passed.
- `go test ./internal/service/promptaudit -count=1`: passed.
- `go test ./internal/router ./internal/relay ./internal/gatewayadapter/transport -run 'PromptGuard|PromptAudit|ConcurrencyOrder' -count=1`: passed.
### Target pre-change baseline
- `cd backend && go test ./internal/service -run ContentModeration -count=1`: passed (`1.138s`).
- `pnpm --dir frontend exec vitest run src/views/admin/__tests__/RiskControlView.spec.ts src/router/__tests__/feature-access.spec.ts`: passed (2 files, 9 tests).
### Review slices
Implementation is partitioned into independently reviewable slices without changing the final scope:
1. Data and core contracts.
2. Async audit engine.
3. Admin API and console.
4. Coordinator and synchronous guard.
5. Observability, verification, rollout, and deployment evidence.
The feature remains default-off throughout implementation. Production blocking remains prohibited until the signed rollout gates in `verification.md` are satisfied.
@@ -0,0 +1,581 @@
# 实施指导
## 1. 使用方式与不可变边界
本指南把 `proposal.md``design.md` 和三个 delta specs 转换为可按文件实施、可逐阶段评审的操作顺序。若本指南与 specs 冲突,以 specs 为准,并先更新 OpenSpec 再编码。
实施前必须满足:
- `source-baseline.md` 的冻结登记已完成,不再以变化中的源工作区作为唯一依据。
- 当前内容审核后端测试、RiskControl 前端测试和路由清单已保存为基线证据。
- 新功能的默认配置是 off;数据库迁移可以先上线,但不能自动开启审计。
- `content_moderation_logs``ContentModerationService``/admin/risk-control``RiskControlView.vue` 的业务语义不改变。
- 完整 Prompt 只允许存在于请求内存和 Redis TTL valueGuard token 只允许存在于写入 DTO、解密后的短生命周期内存和 Authorization header。
明确不做:输出审核、自动改写/脱敏后转发、人工审批、申诉、自动封号、邮件、Prompt 命中 Hash 黑名单、现有 Moderations 分类映射。
## 2. 目标依赖方向
```mermaid
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. 建议目录和文件职责
```text
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.go``prompt_qwen3guard.go``prompt_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` 复用 `SettingRepository``SecretEncryptor`,实现 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 可信请求
```go
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 必须分列返回,避免复制/筛选时含义混淆。
### 5.2 统一决策
```go
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 请求流
```text
鉴权与 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. 异步时序
```mermaid
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. 同步阻断时序
```mermaid
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 状态机
```mermaid
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_config`JSON 至少包含:
```text
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 字段:
```text
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_enabled``strategy` 第一版只接受 `priority`。保存时 canonicalize group IDs、scanner IDs 和 endpoint IDs,拒绝重复、空 ID、越界 worker/queue/timeout/input_limit。
### 10.3 Public DTO
GET config 和 PUT 成功响应只允许:
```text
id, name, protocol, base_url, model, timeout_ms,
input_limit, enabled, has_token, token_status
```
不得出现 `token``token_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: 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 的实际提交入口。
- 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 对象添加稳定 `code`Claude 保留 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_jobs``prompt_audit_events`
- 所有状态、计数和非负值加 CHECK;JSONB 加可接受类型检查更佳。
- `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 快照分列保留,以免主体删除后事件无法复核;沿用现有管理员权限和数据保留策略。
- 不新增 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.6b`role=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 分成:
```text
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 阈值、告警、值班步骤和一键回滚已由责任人签字确认。
@@ -0,0 +1,51 @@
## Why
当前项目的“风控中心”只提供基于 OpenAI Moderations 的内容审核,异步观察依赖进程内队列,且没有 aicodex-api 已具备的持久任务队列、短期敏感载荷存储、Qwen3Guard 分类、同步 fail-closed 门禁和独立提示词事件工作台。直接替换或扩写现有内容审核会混淆两种风险模型,并可能改变关键词、Hash、邮件和自动封号等既有行为,因此需要以并列、默认关闭的独立能力引入。
本变更以 `/Users/mt/code/mt-ai/aicodex/aicodex-api` 当前磁盘实现为功能参考基线,把其中与目标项目实际协议入口相适配的提示词输入审计能力迁入 sub2api,同时保持现有 OpenAI 兼容接口、内容审核页面、数据库记录和错误语义不变。
## What Changes
- 新增独立的 OpenAI 兼容提示词审计引擎,审计节点通过 `{base_url}/v1/chat/completions` 调用 Qwen3Guard,并严格解析 `Safety``Categories`
- 新增三态运行模式:关闭、异步只审计、同步审计并阻止;所有新增开关默认关闭。
- 新增 PostgreSQL 持久任务队列、Redis 短 TTL 原文载荷、进程内 Worker、重试退避、processing 租约刷新和滞留任务回收。
- 新增脱敏提示词快照、Hash、Unicode 分片、最新用户输入优先和九类 Qwen3Guard 风险分类。
- 新增逐分片安全日志、结构化风险摘要,以及用户名、邮箱、API Key 名称分列的管理员复核信息;风险摘要只使用脱敏证据。
- 新增同步 fail-closed 门禁,在账号选择、计费检查和上游调用之前完成;覆盖目标项目现有 Chat Completions、Responses、Claude Messages、Gemini、图像/媒体文本入口及 Responses WebSocket 首轮与后续轮次。
- 新增独立管理 API、运行态、审计节点探测、事件查询/详情/删除能力和“提示词审计”页面。
- 将侧栏现有“风控中心”入口组织为“安全审计”分组;保留原 `/admin/risk-control` 页面和行为,新增 `/admin/prompt-audit` 页面。
- 新增安全审计协调器,只负责给两个独立引擎分发同一份可信请求上下文和归并最终阻断结果,不合并配置、风险分类、事件表或副作用。
- 复用现有 SettingRepository、Redis Client、SecretEncryptor、管理员鉴权、管理操作审计、请求身份上下文、分页、日志和前端基础组件。
- 新增结构化日志、运行指标、路由覆盖测试、无上游副作用断言和敏感信息泄露门禁。
- 不删除、不迁移、不重命名现有 `content_moderation_logs`,不改变现有 Moderations 阈值、关键词、Hash、邮件、封号或清理策略。
## Capabilities
### New Capabilities
- `prompt-input-audit`: 定义提示词快照、异步投递、持久任务队列、OpenAI 兼容 Qwen3Guard 扫描、脱敏事件、运行态、配置和事件管理 API。
- `prompt-input-guard`: 定义同步阻止模式、跨协议入口覆盖、fail-closed 错误语义、WebSocket 每轮门禁、配置快照和无计费/无上游副作用不变量。
- `security-audit-console`: 定义安全审计导航、独立提示词审计页面、节点探测、配置保存、运行态观测、事件筛选/详情/安全删除和响应式可访问体验。
### Modified Capabilities
无。仓库当前没有已发布的 OpenSpec capability;现有内容审核行为在本变更中作为兼容基线,不修改其正式需求语义。
## Impact
- **后端模块**:新增 `backend/internal/securityaudit/` 垂直模块;现有 Handler 仅增加协调器依赖和接入调用。
- **网关入口**:机械替换现有统一内容审核调用点为安全审计协调调用,保持其位于鉴权之后、账号选择/计费/上游之前;WebSocket 保持逐轮检查。
- **管理 API**:新增 `/admin/prompt-audit/*`,复用现有管理员鉴权和管理操作审计。
- **数据库**:新增 `prompt_audit_jobs``prompt_audit_events` 和相应索引;配置存入现有 `settings`,API Key 加密保存;不修改现有内容审核表。
- **Redis**:新增短 TTL 提示词载荷和配置失效通知 key/channelRedis 不可用时异步 Worker 必须显式降级或报错,不得伪装健康。
- **前端**:新增 `frontend/src/features/prompt-audit/`,少量修改路由、侧栏和 i18n;原 `RiskControlView.vue` 业务逻辑保持不变。
- **兼容性**:没有外部 API breaking change;新能力默认关闭。只有管理员显式开启同步阻止后,适用请求才可能新增 403/503 或 WebSocket 4403/1013 响应。
- **安全与隐私**:完整提示词只允许存在于请求内存和 Redis 短 TTL 载荷,不得进入 PostgreSQL、日志、管理 API、前端状态或错误响应;审计节点凭据必须使用现有 SecretEncryptor 加密。
- **实施基线风险**:参考仓库当前 `yjb` 分支包含未提交的同步阻止相关改动。开始编码前必须固定源 commit/tag 或保存可审计 diff,避免“完整迁移”范围漂移。
## Execution References
- `source-baseline.md`:源仓库状态、dirty 文件和实施前冻结门禁。
- `source-feature-map.md`AICodex 功能到目标 Requirement、代码位置和证据的逐项映射。
- `implementation-guide.md`:按文件实施顺序、时序、状态机、API/路由矩阵和常见错误。
- `verification.md`35 条 Requirement 的证据矩阵、协议测试、泄露门禁、灰度阈值和回滚手册。
@@ -0,0 +1,146 @@
# AICodex Prompt Audit 源基线
## 1. 基线状态
本文件记录用于本 change 功能对照的源仓库状态。参考工作区仍可继续变化,但本 change 已通过第 6 节登记的只读 patch bundle 固定实施基线;后续实现只以该冻结包和本 change specs 为依据。
| 字段 | 值 |
| --- | --- |
| 源仓库 | `/Users/mt/code/mt-ai/aicodex/aicodex-api` |
| 采集时间 | `2026-07-16 20:21:19 CST (+0800)` |
| 分支 | `yjb` |
| HEAD | `7a50378851a80650cb0c086260b23abeb3469e6b` |
| 工作区 | dirty |
| 已跟踪差异 | 38 files changed, 1306 insertions(+), 227 deletions(-) |
| 未跟踪范围 | Prompt Guard 实现/测试 6 个文件,加 1 个 OpenSpec change 目录 |
| 冻结状态 | **已用只读 patch bundle 冻结并在 detached worktree 恢复验证** |
当前 HEAD 只代表已提交历史,不能单独代表要迁移的完整功能。同步 fail-closed Guard、出站安全校验、WebSocket/路由顺序测试以及相应 OpenSpec 当前存在于未提交或未跟踪状态。因此,本 change 的临时功能参考是“上述 HEAD + 采集时磁盘工作区”,最终行为权威仍是本 change 的 specs。
## 2. 与迁移直接相关的已跟踪修改
### 后端入口与启动接线
- `ai-gateway/cmd/aicodex/main.go`
- `ai-gateway/internal/controller/prompt_audit.go`
- `ai-gateway/internal/router/relay-router.go`
- `ai-gateway/internal/router/video-router.go`
- `ai-gateway/internal/relay/ws_responses.go`
- `ai-gateway/internal/gatewayadapter/transport/anthropic.go`
- `ai-gateway/internal/gatewayadapter/transport/gemini.go`
- `ai-gateway/internal/gatewayadapter/transport/jimeng.go`
- `ai-gateway/internal/gatewayadapter/transport/kling.go`
- `ai-gateway/internal/gatewayadapter/transport/midjourney.go`
- `ai-gateway/internal/gatewayadapter/transport/openai.go`
- `ai-gateway/internal/gatewayadapter/transport/suno.go`
- `ai-gateway/internal/gatewayadapter/transport/task.go`
### Prompt Audit 核心
- `ai-gateway/internal/service/promptaudit/client.go`
- `ai-gateway/internal/service/promptaudit/config.go`
- `ai-gateway/internal/service/promptaudit/enqueue.go`
- `ai-gateway/internal/service/promptaudit/openai_client.go`
- `ai-gateway/internal/service/promptaudit/probe.go`
- `ai-gateway/internal/service/promptaudit/qwen3guard.go`
- `ai-gateway/internal/service/promptaudit/runtime.go`
- `ai-gateway/internal/service/promptaudit/runtime_coverage_test.go`
- `ai-gateway/internal/service/promptaudit/types.go`
- `ai-gateway/internal/service/promptaudit/worker.go`
- 同目录的 config、diagnostics、probe 测试
### 协议、错误和回归测试
- `ai-gateway/internal/types/error.go`
- `ai-gateway/internal/gatewayadapter/transport/user_concurrency_order_test.go`
### 控制台和类型
- `webui/src/api/promptAudit.test.ts`
- `webui/src/features/prompt-audit/PromptAuditPage.tsx`
- `webui/src/features/prompt-audit/PromptAuditPage.test.tsx`
- `webui/src/features/prompt-audit/promptAuditViewModel.ts`
- `webui/src/features/prompt-audit/promptAuditViewModel.test.ts`
- `webui/src/types/promptAudit.ts`
### 运行说明
- `deploy/.env.example`
- `docs/constraints/41-ai-readable-logging.md`
- `docs/workflows/02-local-dev.md`
## 3. 必须纳入冻结基线的未跟踪文件
以下文件不在 HEAD 中,但属于“完整功能必须要有”的关键证据:
- `ai-gateway/internal/gatewaycore/prompt_guard.go`
- `ai-gateway/internal/service/promptaudit/outbound_security.go`
- `ai-gateway/internal/service/promptaudit/synchronous_guard.go`
- `ai-gateway/internal/service/promptaudit/synchronous_guard_test.go`
- `ai-gateway/internal/relay/ws_responses_prompt_guard_order_test.go`
- `ai-gateway/internal/router/prompt_guard_order_test.go`
- `openspec/changes/add-prompt-audit-synchronous-blocking/`
不得只执行 `git diff HEAD` 后就声称已冻结,因为普通 diff 不包含这些未跟踪文件。
## 4. 功能对照优先级
遇到源实现、源测试和本 change 描述不一致时,按以下顺序决策:
1. 本 change 的三个 delta specs:目标行为契约。
2. 本 change 的 `design.md``implementation-guide.md`:目标架构与落地约束。
3. 冻结后的源测试及源 OpenSpec:功能完整性参考。
4. 冻结后的源实现:算法、边界和交互参考。
5. 当前已提交 HEAD:历史参考。
目标项目不得复制源仓库的 Ent、Caddy/gatewaycore、React 或全局 option 依赖;只迁移可以被规格和测试证明的行为。
## 5. 实施前冻结步骤
在源仓库所有者确认工作区内容属于迁移基线后,选择一种方式:
### 方案 A:专用 commit/tag(推荐)
1. 在源仓库专用分支提交与 Prompt Audit/Guard 有关的已跟踪和未跟踪文件。
2. 运行源模块及路由/WS 顺序测试。
3. 创建不可移动 tag,或记录完整 commit SHA。
4. 把最终标识和测试结果回写本文件。
### 方案 B:只读 patch 包
1. 生成 tracked diff。
2. 使用能够包含未跟踪文件的归档或补丁流程补齐第 3 节文件。
3. 生成文件清单和 SHA-256;在干净临时目录中恢复并运行测试。
4. 把 patch 路径、清单路径和校验和回写本文件。
禁止把包含真实 API Key、Redis payload、`.env` 私密值或运行日志中的完整 Prompt 放入基线包。
## 6. 最终冻结登记
| 字段 | 待填写值 |
| --- | --- |
| 冻结方式 | 只读 tracked patch + untracked tar archive |
| 冻结 commit/tag | base commit `7a50378851a80650cb0c086260b23abeb3469e6b`detached restore |
| patch/archive 绝对路径 | `/Users/mt/code/mt-ai/sub2api/sub2api-mt/openspec/changes/add-openai-compatible-prompt-audit/source-freeze/` |
| manifest SHA-256 | `badab312bf6af4d2c77857a9400381f4da4fbf45722d9f4a6df23bc7005273b6` |
| tracked patch SHA-256 | `f751a13cce3f3a73cd60cae3aececcef6e1e76dcec8c551a7a4747f032234d2b` |
| untracked archive SHA-256 | `1536e2781703b7620e26f2d08b249431fa5846ad9e32b2e8b0d547c3fa3b3632` |
| 冻结人/复核人 | Codex;由恢复后的文件清单、`git diff --check` 和测试命令复核 |
| 冻结时间 | `2026-07-16 20:21:19 CST (+0800)` |
| 源测试结果 | 恢复副本中 Prompt Audit 核心、router、relay、gateway transport 目标测试全部通过,详见 `source-freeze/MANIFEST.md` |
## 7. 复核命令
```bash
cd /Users/mt/code/mt-ai/aicodex/aicodex-api
git branch --show-current
git rev-parse HEAD
git status --short
git diff --stat
git diff --name-only
git ls-files --others --exclude-standard
cd ai-gateway
go test ./internal/service/promptaudit
```
本提案编写时上述模块测试已在当前 dirty 磁盘状态通过;冻结后必须再次执行,并记录最终 commit/patch 校验和、执行目录和完整输出。
@@ -0,0 +1,127 @@
# AICodex 源功能迁移映射
## 1. 目的
本表用于证明“完整功能都必须要有”不是一句笼统目标。每个 AICodex 当前用户可见或运行时能力都必须映射到目标 Requirement、预期代码位置和验证证据;实施中发现新源能力时,先更新本表和相关 spec/tasks,再编码。
源参考状态见 `source-baseline.md`。只读冻结包已在 detached worktree 中恢复,以下测试在恢复副本执行:
```text
cd /Users/mt/code/mt-ai/aicodex/aicodex-api/ai-gateway
go test ./internal/service/promptaudit -count=1
ok github.com/mt21625457/aicodex/internal/service/promptaudit 2.081s
go test ./internal/router ./internal/relay ./internal/gatewayadapter/transport \
-run 'PromptGuard|PromptAudit|ConcurrencyOrder' -count=1
ok github.com/mt21625457/aicodex/internal/router 1.184s
ok github.com/mt21625457/aicodex/internal/relay 2.201s
ok github.com/mt21625457/aicodex/internal/gatewayadapter/transport 3.233s
```
这证明冻结包可恢复且源参考测试通过,但不证明目标实现已完成;目标代码和证据仍须逐行补齐。
## 2. 功能映射
| # | AICodex 当前能力与源证据 | 目标 OpenSpec 契约 | 目标主要代码 | 验证证据 |
| ---: | --- | --- | --- | --- |
| 1 | 独立 Prompt Audit 开关、默认关闭;`config.go` | prompt-input-audit:独立且默认关闭;prompt-input-guard:显式三态 | `prompt_config.go``coordinator.go` | A01、G01 |
| 2 | enabled + blocking_enabled 表达 off/async/blocking`config.go``synchronous_guard.go` | prompt-input-guard:显式启用、即时回滚 | `prompt_config.go``prompt_guard.go` | G01、G12 |
| 3 | 配置持久化、版本、updated_by/change_summary`config.go` | prompt-input-guard:版本化快照/CASconsole:可验证保存 | `prompt_config.go` | G10、C06、C10 |
| 4 | token 加密、空值保留、替换、clear;`config.go``config_test.go` | prompt-input-audit:凭据安全;console:池管理/保存 | `prompt_config.go``prompt_handler.go` | A03、C03、C06 |
| 5 | OpenAI-compatible endpoint、Qwen3Guard 默认模型;`openai_client.go` | prompt-input-auditOpenAI 兼容节点 | `prompt_qwen3guard.go` | A02 |
| 6 | Base URL 规范化,固定 `/v1/chat/completions``openai_client.go` | prompt-input-auditOpenAI 兼容节点/出站安全 | `prompt_qwen3guard.go``prompt_outbound_security.go` | A02、A03 |
| 7 | `/v1/models` readiness + scan fallback probe`openai_client.go``probe.go` | prompt-input-audit:管理员探测;console:真实探测 | `prompt_qwen3guard.go``prompt_handler.go` | A02、C03 |
| 8 | probe 对话框阶段、结果、状态/耗时/错误;`PromptAuditPage.tsx` | console:完整审计池和真实探测 | `features/prompt-audit/components` | C03 |
| 9 | Guard SSRF、DNS/Dial 复检、重定向/响应上限;`outbound_security.go` | prompt-input-audit:凭据和出站地址安全 | `prompt_outbound_security.go` | A03 |
| 10 | Qwen3Guard `Safety/Categories` 解析;`qwen3guard.go` | prompt-input-audit:严格归一 | `prompt_qwen3guard.go` | A08 |
| 11 | 九类官方输入风险;`qwen3guard.go`、页面 scanner catalog | prompt-input-audit:九类;console:九类配置 | `prompt_qwen3guard.go`、前端 types/viewModel | A08、C04 |
| 12 | Safe/Controversial/Unsafe → Allow/Warn/Block`openai_client.go``normalize.go` | prompt-input-audit:严格归一;guardfail-closed | `prompt_qwen3guard.go``prompt_scanner.go` | A08、G06 |
| 13 | 高风险 Controversial 提升、未知 Unsafe 保持 Block`openai_client.go` | prompt-input-audit:严格归一 | `prompt_qwen3guard.go` | A08 |
| 14 | Chat/Responses/Claude 多协议快照;`snapshot.go``multiprotocol.go` | prompt-input-audit:按协议提取 | `prompt_snapshot.go` | A04 |
| 15 | Gemini、图片/媒体等 transport 传递提示词上下文;gatewayadapter changes | prompt-input-audit:所有文本入口;guard:路由覆盖 | `prompt_snapshot.go`、各 Handler 薄接线 | A04、G04 |
| 16 | Responses WS 首轮和后续帧;`ws_responses.go`、顺序测试 | prompt-input-guard:每个 response.create 门禁 | `openai_gateway_handler.go` 薄接线 | G08 |
| 17 | 最新用户输入优先;`snapshot.go` | prompt-input-audit:提取/Unicode 分片 | `prompt_snapshot.go``prompt_scanner.go` | A04、A09 |
| 18 | rune input_limit 完整分片;`openai_client.go` | prompt-input-auditUnicode 完整分片 | `prompt_scanner.go` | A09 |
| 19 | 多片最严重聚合、证据 metadata/去重、Block 早停;`openai_client.go` | prompt-input-audit:分片;guard:共享预算 | `prompt_scanner.go``prompt_guard.go` | A09、G05 |
| 20 | 每片前刷新 processing lease`openai_client.go``worker.go` | prompt-input-auditWorker/Unicode 分片 | `prompt_worker.go` | A07、A09 |
| 21 | scan_chunk_started/completed/failed/aggregated 日志;`openai_client.go` | prompt-input-auditUnicode 分片;guard:可观测 | `prompt_logging.go``prompt_scanner.go` | A09、G11 |
| 22 | Prompt hash、脱敏 preview、敏感模式处理;`snapshot.go` | prompt-input-audit:不可恢复快照 | `prompt_snapshot.go` | A05 |
| 23 | 完整 scan text 使用 Redis 30 分钟 TTL`payload_store.go` | prompt-input-audit:持久任务 + Redis TTL | `prompt_payload_store.go` | A06 |
| 24 | 异步 enqueue、范围/容量检查;`enqueue.go` | prompt-input-audit:异步持久投递 | `prompt_enqueue.go` | A06 |
| 25 | PromptAuditJob/Event 持久事实;Ent schema/store | prompt-input-auditjobs/events | SQL migration、`prompt_repository.go` | A05、A07、A10 |
| 26 | 进程内 Worker、可配置数量、Start/Stop`worker.go` | prompt-input-audit:可靠 Worker | `prompt_worker.go``prompt_module.go` | A07 |
| 27 | retry/backoff/max attempts`worker.go` | prompt-input-audit:可靠 Worker | `prompt_worker.go` | A07 |
| 28 | processing stale reclaim`worker.go` | prompt-input-audit:可靠 Worker | `prompt_worker.go`、Repository | A07 |
| 29 | runtime queue/Worker/DB/payload/connectivity/heartbeat`runtime.go` | prompt-input-audit:真实运行态 | `prompt_runtime.go` | A11、C07 |
| 30 | config active/expected version 和失效通知;`config.go``runtime.go` | prompt-input-guard:版本化热路径快照 | `prompt_config.go``prompt_runtime.go` | G10、C07 |
| 31 | 同步 evaluator 不依赖 Worker`synchronous_guard.go` | prompt-input-guard:同步门禁/结果复用 | `prompt_guard.go` | G03、G09 |
| 32 | 总 deadline、ordered failover、bulkhead`synchronous_guard.go` | prompt-input-guard:共享预算/故障切换 | `prompt_guard.go` | G05、G06 |
| 33 | HTTP fail-closed 403/503`prompt_guard.go`、router 接线 | prompt-input-guardHTTP 稳定错误 | Handler helper + OpenAI/Claude code、Gemini ErrorInfo adapter | G03、G07 |
| 34 | WS 4403/1013`ws_responses.go` | prompt-input-guard:每轮 WS 门禁 | Responses WS Handler | G08 |
| 35 | 同步结果轻量记录、不重复 Guard;`synchronous_guard.go` | prompt-input-guard:结果复用 | `prompt_guard.go`、Repository | G09 |
| 36 | Guard metrics Allow/Flag/Block/Unavailable/timeout/failover/bulkhead`synchronous_guard.go``runtime.go` | prompt-input-guard:可观测;console:运行态 | `prompt_runtime.go`、metrics adapter | G11、C07 |
| 37 | 事件列表/详情、复合筛选;`store.go`、controller | prompt-input-audit:查询事件;console:列表详情 | `prompt_repository.go``prompt_handler.go`、前端 | A12、C08 |
| 38 | 用户名/邮箱分别展示和复制;probe-dialog change + controller/UI tests | prompt-input-audit:分列身份快照;console:复核身份 | Request/snapshot、event DTO、前端详情 | A04、A10、C08 |
| 39 | scanner evidence、Guard policy、结构化 issue summaries`issue_summary.go` | prompt-input-audit:事件/风险摘要;console:具体风险 | `prompt_issue_summary.go`、event DTO | A10、C08 |
| 40 | 单条/批量硬删除;controller/store | prompt-input-audit:安全删除;console:防误操作 | Repository/Admin Handler/前端 | A12、C09 |
| 41 | delete preview + canonical filter hash + confirmfilter helper | prompt-input-audit:安全删除;console:防误操作 | Repository/Admin Handler/前端,增加 max_id/认证 token | A12、C09 |
| 42 | 配置、probe、删除的管理审计;controller/router tests | console:管理员操作审计 | `prompt_handler.go` + 现有 audit | C10 |
| 43 | 独立控制台、运行概览、池/策略/事件/保存栏;`PromptAuditPage.tsx` | console:独立工作区 | `frontend/src/features/prompt-audit/` | C01、C02 |
| 44 | dirty snapshot、统一保存、重置;页面/viewModel | console:工作区/可验证保存 | 前端 viewModel/page | C02、C06 |
| 45 | all/selected group、搜索、stale group;页面/config | prompt-input-audit:范围;console:范围配置 | config + 前端 selector | C04 |
| 46 | endpoint 新增/编辑/启停/删除、参数对话框;页面 | console:审计池管理 | 前端 components | C03 |
| 47 | blocking 二次确认和保存栏开关联动;页面 | console:开启风险确认 | 前端 viewModel/page | C05 |
| 48 | 事件技术/具体风险/结构化返回 tabs 和 JSON 查看;页面 | console:可复核详情 | 前端 detail components | C08 |
| 49 | 响应式、可访问状态、页面测试;redesign change | console:响应式/可访问/i18n | 前端 + i18n | C11 |
| 50 | AI 可读稳定日志和敏感字段约束;logging.go/constraints | prompt-input-guard:可观测且不泄密 | `prompt_logging.go` | G11 |
## 3. 架构适配而非逐行复制
以下差异是目标架构适配,不是功能删减:
| AICodex 实现细节 | sub2api 目标实现 | 等价性理由/门禁 |
| --- | --- | --- |
| Ent PromptAuditJob/Event | PostgreSQL migration + `database/sql` | 目标项目以 SQL migration 为 schema 事实源;字段和行为由 A05/A07/A10/A12 验证 |
| 表/对象可能带 AICodex 命名 | `prompt_audit_jobs/events` | 不复制 `aicodex_` 前缀;管理能力不变 |
| `PromptAuditConfigJSON` option | settings `prompt_audit_config` | 复用目标 SettingRepositoryPublic/Storage DTO 行为不变 |
| AICodex secret helper | 现有 `SecretEncryptor` | A03 canary 和加密往返证明 |
| React/Ant Design 页面 | Vue 3 既有组件体系 | C01-C11 以行为和可访问性验收,不按框架验收 |
| `/api/prompt-audit` | `/admin/prompt-audit` | 复用目标 AdminAuth/管理审计;API 能力一一对应 |
| token/channel/group 字符串 | API key/group/provider 可信 ID + 快照 | 使用目标身份域,保留查询/复核能力 |
| 6068/9068 双端口一致性 | `/v1`、root alias、`/backend-api/codex` 等目标路由一致性 | G04 以目标实际 routes 自动枚举,不复制不存在的端口拓扑 |
| 源 queued 后再写 payload 的竞态 | staging → Redis SET EX → queued | 是可靠性增强;A06/A07 证明 Worker 不提前领取 |
| 源进程内唤醒队列 + DB 事实 | PostgreSQL 原子 claim + 递增 claim_version fencing + 进程内 Worker | 支持多实例并防旧 Worker 覆盖,无功能损失;A07 并发测试证明 |
| 源 MemoryRepository | 只作为目标测试 fake,不作为生产 fallback | 生产需要持久任务;依赖失败由 A11 显示 degraded,不伪装成功 |
| `scan_url`/旧 llm_guard 协议兼容 | 只接受 Base URL + OpenAI compatible | 目标是新增 setting、无旧 Prompt Audit 配置;A02 明确禁止旧协议 |
| 源旧 strategy 迁移 | 第一版仅 `priority`,其他值拒绝 | 目标无历史 Prompt configG01/配置测试保证确定性 |
| endpoint `weight` 兼容展示字段 | 显式数组顺序作为 priority | 源当前只允许 priority,扫描代码未使用 weight 做选择;目标去除无效歧义,故障切换能力由 G06 证明 |
| endpoint `policy_id/tenant_id` 历史兼容输入 | Qwen 结果固定 policy_id/versionevent 持久化 | 当前 Qwen 请求不发送这两个 endpoint 字段;目标保留实际策略结果而不暴露无效输入 |
| 源 env 默认配置 | settings 管理页面初始化默认值 | 目标配置事实源是 settings;默认 off 和完整可配置性由 A01/C03/C06 证明 |
| 源 event API 查询时解析用户 | 事件保存用户名/邮箱/API Key 名称分列快照 | 删除主体后仍可复核;访问与保留沿用现有管理员政策 |
| 源 `issue_summaries` 由 evidence 派生 | 目标同样派生,不新增数据库列 | 防止双份风险事实漂移;A10/C08 golden 测试证明 |
## 4. 源专属能力的明确处理
以下内容不作为目标运行功能移植,但必须明确原因:
- AICodex 旧 `/v1/scan/prompt` 和 llm_guard 配置迁移:目标项目从未发布 Prompt Audit,无历史配置需要兼容;目标只实现当前 OpenAI-compatible Qwen3Guard 行为。
- AICodex Caddy/gatewaycore、6068/9068 端口和 channel dispatch:目标使用 Gin Handler、目标账号调度和目标路由 alias;以 G04/G03 证明等价接入顺序。
- AICodex React/旧 deprecated 页面:只迁移当前管理行为到 Vue 独立 feature,不同时维护两套前端。
- AICodex 产品特有 transport:目标只覆盖目标项目实际存在且可触发模型的文本入口;`implementation-guide.md` 的路由枚举是硬门禁。
- 输出审核、Redact、人工审批和申诉:当前迁移范围是用户输入 Prompt Audit/Guard,且本 change 明确列为 Non-Goals;不得把源旧 LLM Guard 的 `Redact` 兼容文案误当成当前 Qwen 输入审计功能。
如果实施评审发现上述任一项实际上在目标项目有已发布数据或用户依赖,必须把它从本节移回第 2 节,新增 Requirement/Scenario 后才能继续。
## 5. 完整性复核步骤
每次源基线或目标设计变化后执行:
1. 对源 `internal/service/promptaudit`、Prompt Audit controller/router、WS/transport 和当前前端目录重新列出文件/公开符号。
2. 对源主 spec 和所有未归档 Prompt Audit changes 提取 Requirement/Scenario。
3. 为新发现功能在第 2 节新增一行;若无目标 Requirement,先更新 specs。
4. 检查每行同时有目标代码位置和 `verification.md` ID。
5. 检查第 3/4 节每项确实是架构适配或源专属,而不是为了缩小实现范围。
6. 冻结时把最终源 commit/tag/patch SHA-256 写入 `source-baseline.md`
7. 实现完成后把每行的计划证据替换为实际测试名/CI artifact 链接。
本表没有“以后再做”状态。除第 4 节经解释的源专属项外,第 2 节任一行没有通过证据都表示“完整迁移”未完成。
@@ -0,0 +1,52 @@
# AICodex Prompt Audit source freeze manifest
- Frozen at: `2026-07-16 20:21:19 CST (+0800)`
- Source repository: `/Users/mt/code/mt-ai/aicodex/aicodex-api`
- Source branch at capture: `yjb`
- Base commit: `7a50378851a80650cb0c086260b23abeb3469e6b`
- Freeze method: immutable tracked patch plus untracked tar archive
- Restored verification worktree: detached from the base commit, then populated only from the two artifacts below
## Artifacts
| Artifact | Size | SHA-256 |
| --- | ---: | --- |
| `aicodex-prompt-audit-tracked.patch` | 124674 bytes | `f751a13cce3f3a73cd60cae3aececcef6e1e76dcec8c551a7a4747f032234d2b` |
| `aicodex-prompt-audit-untracked.tar.gz` | 39342 bytes | `1536e2781703b7620e26f2d08b249431fa5846ad9e32b2e8b0d547c3fa3b3632` |
The tracked patch contains 38 files with 1306 insertions and 227 deletions. It is applied to the base commit above using `git apply`.
## Untracked archive entries
- `ai-gateway/internal/gatewaycore/prompt_guard.go`
- `ai-gateway/internal/relay/ws_responses_prompt_guard_order_test.go`
- `ai-gateway/internal/router/prompt_guard_order_test.go`
- `ai-gateway/internal/service/promptaudit/outbound_security.go`
- `ai-gateway/internal/service/promptaudit/synchronous_guard.go`
- `ai-gateway/internal/service/promptaudit/synchronous_guard_test.go`
- `openspec/changes/add-prompt-audit-synchronous-blocking/.openspec.yaml`
- `openspec/changes/add-prompt-audit-synchronous-blocking/design.md`
- `openspec/changes/add-prompt-audit-synchronous-blocking/proposal.md`
- `openspec/changes/add-prompt-audit-synchronous-blocking/specs/prompt-input-audit/spec.md`
- `openspec/changes/add-prompt-audit-synchronous-blocking/specs/prompt-input-guard/spec.md`
- `openspec/changes/add-prompt-audit-synchronous-blocking/tasks.md`
## Restore and verification result
The artifacts were restored into `/tmp/aicodex-prompt-audit-freeze-7a503788`, a detached worktree at the base commit. `git diff --check` passed.
The following commands passed against the restored copy:
```text
cd ai-gateway
go test ./internal/service/promptaudit -count=1
ok github.com/mt21625457/aicodex/internal/service/promptaudit 2.081s
go test ./internal/router ./internal/relay ./internal/gatewayadapter/transport \
-run 'PromptGuard|PromptAudit|ConcurrencyOrder' -count=1
ok github.com/mt21625457/aicodex/internal/router 1.184s
ok github.com/mt21625457/aicodex/internal/relay 2.201s
ok github.com/mt21625457/aicodex/internal/gatewayadapter/transport 3.233s
```
The source worktree remains untouched. The target OpenSpec specs remain authoritative if this frozen implementation differs from the target architecture.
@@ -0,0 +1,248 @@
## ADDED Requirements
### Requirement: 提示词审计必须是独立且默认关闭的安全审计引擎
系统 SHALL 在现有内容审核之外提供独立的提示词审计引擎。新引擎 MUST 拥有独立配置、运行态、任务、事件和开关,并 MUST 默认关闭;现有 OpenAI Moderations 内容审核的配置、判定、关键词、Hash、邮件、自动封号、日志表和清理行为 MUST NOT 因本能力而改变。
#### Scenario: 升级后未启用新引擎
- **WHEN** 系统完成包含本能力的升级且管理员尚未保存提示词审计配置
- **THEN** 所有模型请求 MUST 继续按升级前的内容审核和转发链路执行
- **THEN** 系统 MUST NOT 创建提示词审计任务、写入提示词审计事件或调用外部 Guard
#### Scenario: 两个审计引擎同时启用
- **WHEN** 现有内容审核和新增提示词审计都已启用
- **THEN** 两个引擎 MUST 使用各自的配置与风险语义独立执行
- **THEN** 提示词审计命中 MUST NOT 自动触发现有内容审核的邮件、封号或 Hash 黑名单副作用
### Requirement: 提示词审计节点必须使用 OpenAI 兼容协议
系统 SHALL 仅支持通过 OpenAI 兼容 Chat Completions 接口调用提示词审计节点。节点配置 MUST 支持名称、Base URL、API Key、Model、超时、单片输入上限、启用状态和有序优先级;默认模型 MUST 为 `sileader/qwen3guard:0.6b`
#### Scenario: Worker 调用已配置节点
- **WHEN** Worker 领取到可处理任务并选择一个启用节点
- **THEN** 系统 MUST 向 `{base_url}/v1/chat/completions` 发送请求
- **THEN** 请求 MUST 使用 `role=user``temperature=0`、确定性的输出限制和管理员配置的模型
- **THEN** 系统 MUST NOT 调用旧的 `/v1/scan/prompt``llm_guard` 专用协议
#### Scenario: 管理员保存未填写模型的节点
- **WHEN** 管理员保存一个 Base URL 有效但 Model 为空的节点
- **THEN** 系统 MUST 将节点模型归一为 `sileader/qwen3guard:0.6b`
#### Scenario: 管理员探测节点
- **WHEN** 管理员请求探测一个节点
- **THEN** 后端 MUST 使用服务端网络环境执行真实的认证与模型连通性探测
- **THEN** 响应 MUST 包含成功状态、稳定错误码、HTTP 状态、耗时、是否可重试和检查时间
- **THEN** 响应 MUST NOT 回显 API Key
### Requirement: 审计节点凭据必须受到安全保护且出站目标由管理员负责
系统 MUST 使用现有 SecretEncryptor 加密持久化节点 API Key,并 MUST 对响应体实施大小限制。节点地址及其网络目标由管理员自行配置和负责;系统 MUST NOT 按公网、私网、回环、link-local、元数据、保留地址或 DNS 解析结果阻止保存、探测和实际调用,也 MUST NOT 禁止 HTTP 或正常 HTTP 重定向。完整凭据只允许短暂存在于管理员写入请求、前端未持久化输入内存、服务端解密内存和发往 Guard 的 Authorization Header;它们以及 URL query、提示词正文 MUST NOT 出现在日志、错误响应、管理读取响应或前端持久化/调试状态中。
#### Scenario: 保存带 API Key 的节点
- **WHEN** 管理员保存一个包含 API Key 的节点
- **THEN** settings 中 MUST 只保存加密密文和是否已配置标记
- **THEN** 后续读取配置 MUST 只返回 `has_token=true` 或等价状态
#### Scenario: 保存管理员配置的内网或特殊地址
- **WHEN** Base URL 使用 HTTP(S) 且指向私网、回环、link-local、元数据、保留地址或解析到这些地址的域名
- **THEN** 系统 MUST 接受该节点配置并从服务端网络环境执行探测和实际调用
- **THEN** 系统 MUST NOT 对 DNS 结果进行地址类别拦截
#### Scenario: 节点返回重定向或超大响应
- **WHEN** Guard 返回正常 HTTP 重定向
- **THEN** 系统 MUST 使用标准 HTTP 客户端行为跟随重定向
- **WHEN** Guard 返回超过配置上限的响应体
- **THEN** 系统 MUST 将响应判定为无效或不可用
### Requirement: 系统必须按协议提取用户输入提示词快照
系统 SHALL 从目标项目所有已支持、包含用户文本的模型入口提取提示词快照。快照 MUST 包含 request ID、user ID、用户名、用户邮箱、API key ID/名称、group ID/名称、provider、endpoint、protocol、model、提示词 Hash、脱敏预览、Unicode 字符数和消息数量;文本审计 MUST 优先扫描最新用户输入,同时完整覆盖需要审计的历史用户文本。
#### Scenario: 提取 OpenAI Chat Completions 输入
- **WHEN** `/v1/chat/completions` 或等价兼容入口包含一个或多个 `role=user` 消息
- **THEN** 系统 MUST 提取用户文本内容并把最新用户输入置于扫描顺序最前
- **THEN** 系统 MUST 不把 assistant 或 tool 输出当作用户提示词主体
#### Scenario: 提取 OpenAI Responses 输入
- **WHEN** `/v1/responses` 请求使用字符串、消息数组或内容块表达用户输入
- **THEN** 系统 MUST 提取其中的用户文本并保留 Responses 协议标识
#### Scenario: 提取 Claude 和 Gemini 输入
- **WHEN** Claude Messages 或 Gemini 兼容入口包含用户角色文本
- **THEN** 系统 MUST 提取可审计文本并保留真实 protocol、endpoint 和 model
#### Scenario: 提取图像或媒体生成提示词
- **WHEN** OpenAI Images、Grok 媒体或目标项目其他生成入口包含文本 prompt
- **THEN** 新引擎 MUST 审计文本 prompt
- **THEN** 新引擎 MUST NOT 把图片二进制、base64 图片或远程图片内容发送给 Qwen3Guard
- **THEN** 图片内容审核 MUST 继续由现有内容审核引擎负责
#### Scenario: 请求没有用户文本
- **WHEN** 请求体有效但没有可审计的用户文本
- **THEN** 系统 MUST 跳过提示词任务并记录稳定的 skipped reason
### Requirement: 提示词数据库快照必须脱敏且不可恢复原文
系统 SHALL 在写入数据库前计算 SHA-256 Hash 和脱敏裁剪预览。PostgreSQL、结构化日志、管理 API 和前端 MUST NOT 保存或返回完整原始提示词;用于实际扫描的正文只允许保存在请求内存或 Redis 短 TTL 载荷中。
#### Scenario: 创建异步任务
- **WHEN** 系统为用户输入创建异步审计任务
- **THEN** `prompt_audit_jobs` MUST 保存 Hash、脱敏预览、字符数、消息数、分列的用户/API Key 展示快照和可关联请求上下文
- **THEN** 表中 MUST 不存在 raw_prompt、payload 或等价原文字段
#### Scenario: 管理员查看事件详情
- **WHEN** 管理员打开提示词审计事件详情
- **THEN** 页面和 API MUST 只展示脱敏预览、Hash、分类、结构化风险摘要、证据摘要和技术元数据
- **THEN** 任何证据片段 MUST 经过脱敏、长度限制并包含不可逆 Hash,而不是完整命中正文
### Requirement: 异步审计必须使用持久任务和短期 Redis 载荷
系统 SHALL 使用 PostgreSQL `prompt_audit_jobs` 作为任务事实源,并使用 Redis 保存默认 30 分钟 TTL 的完整扫描正文。异步任务投递 MUST 不阻塞或改变主模型请求结果。
#### Scenario: 成功投递异步任务
- **WHEN** 提示词审计处于 async_audit、请求在审计范围内且队列未满
- **THEN** 系统 MUST 先创建不可被 Worker 领取的 staging 任务
- **THEN** 系统 MUST 成功写入 Redis 载荷后再把任务发布为 queued
- **THEN** 主请求 MUST 继续进入现有网关链路
#### Scenario: Redis 载荷写入失败
- **WHEN** 数据库任务已创建但 Redis 载荷写入失败
- **THEN** 系统 MUST 将任务标记为 failed 或保持可清理的 staging 状态
- **THEN** 系统 MUST 输出 `prompt_audit.enqueue_dropped` 和稳定错误码
- **THEN** 主模型请求 MUST 不受影响
#### Scenario: 队列达到容量上限
- **WHEN** queued、retry、processing 和 staging 活跃任务达到配置容量
- **THEN** 系统 MUST 拒绝创建新的异步任务并记录 `reason=queue_full`
- **THEN** 主模型请求 MUST 继续转发
#### Scenario: 多实例同时争抢最后队列容量
- **WHEN** 多个实例并发入队且剩余容量不足以容纳全部请求
- **THEN** active count 检查与 staging INSERT MUST 在同一数据库准入锁事务中串行化
- **THEN** 已接受的 active jobs MUST NOT 超过该配置快照的 queue_capacity
- **THEN** 未获准任务 MUST 按 queue_full 或 queue_admission_busy 丢弃且不影响主请求
### Requirement: 进程内 Worker 必须可靠消费持久任务
系统 SHALL 在主服务进程内启动可配置数量的 Worker。多实例 Worker MUST 通过 PostgreSQL 原子领取任务,并为每次领取生成单调递增的 claim version fencing token;租约刷新、事件提交和终态更新 MUST 校验该 token。系统还 MUST 支持重试退避、processing 租约刷新、滞留任务回收、最大尝试次数和优雅关闭。
#### Scenario: 多 Worker 并发领取任务
- **WHEN** 多个进程或 Worker 同时寻找可执行任务
- **THEN** 每个任务 MUST 只被一个 Worker 原子领取
- **THEN** 领取过程 MUST 使用数据库行锁/条件更新或等价的无重复执行机制
#### Scenario: 已回收的旧 Worker 恢复
- **WHEN** Worker A 的 processing 租约已被回收且任务随后由 Worker B 以更高 claim version 重新领取
- **THEN** Worker A 的租约刷新、事件写入和终态更新 MUST 因 claim version 不匹配而失败
- **THEN** Worker A MUST NOT 覆盖 Worker B 的任务状态或创建重复事件
#### Scenario: 可重试节点故障
- **WHEN** Guard 返回 429、5xx、连接失败或超时且任务仍有剩余尝试次数
- **THEN** Worker MUST 将任务置为 retry 并设置有界退避的 next_attempt_at
#### Scenario: 不可重试错误或达到最大尝试次数
- **WHEN** Guard 返回认证失败、严格解析失败或任务达到最大尝试次数
- **THEN** Worker MUST 将任务标记为 failed 并保存脱敏后的稳定错误码
- **THEN** Redis 载荷 MUST 被删除或等待短 TTL 自动清理
#### Scenario: 回收滞留 processing 任务
- **WHEN** processing 任务的租约超过允许时长
- **THEN** 系统 MUST 按剩余尝试次数把任务回收到 retry 或标记 failed
- **THEN** 系统 MUST 输出可关联 job ID 的回收日志
#### Scenario: Worker 启动失败
- **WHEN** 数据库、Redis、配置或加密依赖导致 Worker 无法启动
- **THEN** 主 API MUST 继续提供非提示词审计能力
- **THEN** 运行态 MUST 显示 error/degraded 和稳定错误码,而不是显示健康
### Requirement: Qwen3Guard 返回必须被严格归一化
系统 SHALL 严格解析单一 `Safety` 行和单一 `Categories` 行,并支持 Violent、Non-violent Illegal Acts、Sexual Content or Sexual Acts、PII、Suicide & Self-Harm、Unethical Acts、Politically Sensitive Topics、Copyright Violation、Jailbreak 九类输入风险。额外非空说明、重复字段、未知 Safety 或无法解析响应 MUST 视为 invalid_response。
#### Scenario: Safe 结果
- **WHEN** Guard 返回 `Safety: Safe`
- **THEN** 归一化结果 MUST 为 pass/low/Allow
#### Scenario: Controversial 结果
- **WHEN** Guard 返回 `Safety: Controversial`
- **THEN** 默认结果 MUST 为 flag/Warn
- **THEN** 命中已启用的 Jailbreak、PII 或 Suicide & Self-Harm 时 MUST 提升为 critical/Block
#### Scenario: Unsafe 结果
- **WHEN** Guard 返回 `Safety: Unsafe` 且命中至少一个已启用类别
- **THEN** 结果 MUST 为 critical/Block
#### Scenario: Unsafe 包含未知类别
- **WHEN** Guard 返回 Unsafe 但类别未知或不可映射
- **THEN** 系统 MUST 记录 `unknown_unsafe` 并保持 Block 语义
#### Scenario: 严格响应解析失败
- **WHEN** Guard 响应缺少字段、包含重复字段、出现额外非空说明或 Safety 不在允许枚举中
- **THEN** 系统 MUST 返回 `prompt_guard_invalid_response`
- **THEN** 系统 MUST NOT 把该结果伪装为 Safe
### Requirement: 长提示词必须完整进行 Unicode 分片审计
系统 SHALL 按 Unicode rune 而不是字节对提示词分片。最新用户输入 MUST 作为优先片段,其他输入按确定顺序完整覆盖;异步任务必须在每片开始前刷新 processing 租约,并为每片开始、完成、失败及最终聚合输出不含正文的结构化日志。
#### Scenario: 输入超过节点单片上限
- **WHEN** 提示词 Unicode 字符数超过节点 input_limit
- **THEN** 系统 MUST 生成覆盖全部非空文本的连续分片
- **THEN** 任一分片 Block MUST 使聚合结果为 Block
- **THEN** 只有全部必要分片成功后才能产生 Allow
#### Scenario: 最新输入包含风险
- **WHEN** 最新用户输入位于长会话尾部并包含 Block 风险
- **THEN** 该输入 MUST 在历史文本之前接受扫描
- **THEN** 同步模式 MAY 在确认 Block 后停止后续分片,但 MUST NOT 部分放行
#### Scenario: 多分片扫描完成
- **WHEN** 一个提示词被拆成多个分片并完成聚合
- **THEN** 日志 MUST 包含 chunk_index、chunk_total、chunk_chars、input_chars、input_limit、guard endpoint、action 和 latency
- **THEN** 日志 MUST NOT 包含分片正文、脱敏前证据或内部优先级分隔符
### Requirement: 审计事件必须独立、可关联且可安全管理
系统 SHALL 把归一化结果写入 `prompt_audit_events`,并支持是否保存 Pass 事件。事件 MUST 包含请求上下文、分列的用户名/邮箱/API Key 名称快照、脱敏提示词快照、decision、risk_level、action、分类、scanner、证据、策略、节点、配置版本、分片数和耗时;管理 DTO MUST 从这些事实确定性派生结构化 `issue_summaries`,不得复制保存第二套风险事实。
#### Scenario: 风险事件被记录
- **WHEN** Worker 或同步 Guard 得到 flag/critical 结果
- **THEN** 系统 MUST 创建独立提示词审计事件
- **THEN** 事件 MUST 可通过 request_id、user_id、api_key_id、group_id 和 prompt_hash 检索
#### Scenario: Pass 事件存储关闭
- **WHEN** 结果为 pass 且 store_pass_events=false
- **THEN** 系统 MUST 完成任务但 MAY 不创建事件
#### Scenario: 同步结果写入失败
- **WHEN** 同步 Guard 已完成判定但事件持久化失败
- **THEN** 系统 MUST 输出 `prompt_guard.result_record_failed`
- **THEN** 持久化失败 MUST NOT 把已确定的 Allow 改成 Block,也 MUST NOT 撤销已确定的 Block
### Requirement: 提示词审计运行态必须反映真实依赖和处理状态
系统 SHALL 提供运行态接口,返回有效模式、期望/生效配置版本、配置加载时间与错误、Worker 心跳、队列容量与各状态数量、处理/失败统计、最近错误、节点连通性、数据库/Redis 状态和同步 Guard 指标。
#### Scenario: 管理员查询健康运行态
- **WHEN** Worker 正常心跳、数据库与 Redis 可用且至少一个节点探测成功
- **THEN** 运行态 MUST 显示 running/ok 和真实统计值
#### Scenario: Redis 不可用
- **WHEN** 提示词审计已启用但 Redis 载荷存储不可用
- **THEN** 异步运行态 MUST 显示 error 或 degraded
- **THEN** 页面 MUST NOT 仅因 Base URL 已配置而显示健康
### Requirement: 管理员必须能够查询和安全删除提示词审计事件
系统 SHALL 提供分页列表、详情、单条删除、批量 ID 删除和按筛选删除。筛选 MUST 支持 decision、risk level、endpoint、group、user、API key、request ID、prompt Hash、关键字和时间范围。
#### Scenario: 按筛选查询事件
- **WHEN** 管理员提交一个或多个受支持筛选条件
- **THEN** 系统 MUST 返回稳定排序的分页事件和总数
#### Scenario: 预览按筛选删除
- **WHEN** 管理员提交包含明确时间范围的删除筛选
- **THEN** 系统 MUST 返回 matched_count、规范化筛选摘要、snapshot_max_id、filter_hash 和绑定当前管理员且短期有效的 confirmation_token
- **THEN** 系统 MUST 不立即删除数据
#### Scenario: 确认按筛选删除
- **WHEN** 管理员提交相同筛选、有效 filter_hash、未过期 confirmation_token 和显式 confirm=true
- **THEN** 系统 MUST 只分批删除匹配且 id 不高于预览 snapshot_max_id 的事件,以及已无事件引用的孤立任务
- **THEN** 系统 MUST 清理相关 Redis 载荷并写入管理操作审计
#### Scenario: 伪造或重放其他管理员的删除确认
- **WHEN** confirmation_token 无法认证、已过期、操作者不匹配、Hash 不匹配或缺失
- **THEN** 系统 MUST 拒绝删除并返回稳定错误码
- **THEN** 客户端自行计算 filter_hash MUST NOT 绕过 delete-preview
#### Scenario: 无时间范围的大范围删除
- **WHEN** 管理员尝试按筛选删除但未提供明确时间范围
- **THEN** 系统 MUST 拒绝操作并返回稳定错误码
@@ -0,0 +1,201 @@
## ADDED Requirements
### Requirement: 同步提示词门禁必须由显式配置启用
系统 SHALL 使用 `enabled``blocking_enabled` 表达关闭、异步只审计、同步审计并阻止三态。旧配置或缺失字段 MUST 归一为 `blocking_enabled=false`;系统 MUST 拒绝 `enabled=false && blocking_enabled=true` 的配置。
#### Scenario: 关闭提示词审计
- **WHEN** enabled=false
- **THEN** 有效模式 MUST 为 off
- **THEN** blocking_enabled MUST 被视为 false
#### Scenario: 启用异步审计
- **WHEN** enabled=true 且 blocking_enabled=false
- **THEN** 有效模式 MUST 为 async_audit
- **THEN** Guard 故障 MUST NOT 改变主请求结果
#### Scenario: 启用同步阻止
- **WHEN** enabled=true 且 blocking_enabled=true
- **THEN** 有效模式 MUST 为 blocking
- **THEN** 适用请求 MUST 等待 Guard 判定后才能进入账号选择、计费和上游阶段
#### Scenario: 保存非法开关组合
- **WHEN** 管理员保存 enabled=false 且 blocking_enabled=true
- **THEN** 后端 MUST 返回 400 和 `prompt_guard_requires_audit_enabled`
### Requirement: 安全审计协调器必须保持两个引擎的独立语义
系统 SHALL 通过一个薄协调器把可信请求上下文交给现有内容审核和新增提示词审计。协调器 MUST 不转换两套风险分类、不共用事件表、不让提示词审计触发内容审核副作用,并 MUST 使用确定性的阻断优先级。
#### Scenario: 现有内容审核阻断
- **WHEN** 现有内容审核返回 Block
- **THEN** 客户端 MUST 继续收到升级前的状态码、错误码和文案
- **THEN** 提示词审计异步模式 MAY 继续完成自己的独立记录
#### Scenario: 仅提示词 Guard 阻断
- **WHEN** 现有内容审核允许但提示词 Guard 返回 Block
- **THEN** 客户端 MUST 收到 `prompt_guard_blocked`
#### Scenario: 两个引擎同时阻断
- **WHEN** 两个引擎都返回 Block
- **THEN** 现有内容审核错误语义 MUST 具有客户端响应优先级
- **THEN** 两个引擎 MUST 各自记录其结果和结构化日志
### Requirement: 同步门禁必须位于外部副作用之前
系统 MUST 在鉴权和请求格式校验完成后、账号选择、账户并发、计费资格检查、任何预扣、上游连接和上游写入之前完成同步判定。被 Block 或 fail-closed 拒绝的请求 MUST 不产生这些下游副作用。
#### Scenario: HTTP 请求被 Guard 阻断
- **WHEN** 任一支持的 HTTP 模型请求得到 Block
- **THEN** 账号选择次数、计费检查/预扣次数和上游请求次数 MUST 均为 0
- **THEN** 流式请求 MUST 在拒绝前未写出 SSE 响应头或首字节
#### Scenario: Guard 不可用
- **WHEN** 同步模式下所有可用节点均失败
- **THEN** 请求 MUST 在任何账号、计费或上游副作用之前返回 503
### Requirement: 同步门禁必须覆盖所有目标协议入口
系统 SHALL 覆盖现有内容审核已接入的所有用户文本入口,并通过结构测试防止后续路由绕过。至少包括 OpenAI Chat Completions、OpenAI Responses、Claude Messages、Gemini、OpenAI Images/Grok 媒体文本 prompt,以及 Responses WebSocket 首轮和后续轮次。
#### Scenario: OpenAI 兼容 HTTP 入口
- **WHEN** 客户端调用 Chat Completions 或 Responses 兼容入口
- **THEN** 系统 MUST 使用对应协议提取器并执行同一 Guard evaluator
- **THEN** 现有 OpenAI 请求和响应 envelope MUST 保持兼容
#### Scenario: Claude 或 Gemini 入口
- **WHEN** 客户端调用 Claude Messages 或 Gemini 入口
- **THEN** 系统 MUST 执行相同策略判定
- **THEN** 拒绝响应 MUST 使用该协议现有错误 envelope 和共享稳定 error_code
#### Scenario: 新增用户文本入口
- **WHEN** 后续代码新增一个可触发模型执行且包含用户文本的路由
- **THEN** 路由覆盖门禁 MUST 在缺少安全审计接线时失败
### Requirement: 同步分片必须共享总预算并完整覆盖
系统 SHALL 以有序节点列表中首个启用节点的 timeout 作为一次同步 evaluation 的总预算。所有分片和节点故障切换 MUST 共享该 deadline;任一必要分片失败、超时或无合法结果时 MUST fail-closed。
#### Scenario: 所有分片均为安全
- **WHEN** 每个非空分片都在总预算内返回 Safe 或允许的 Warn
- **THEN** 请求 MAY 进入下一阶段
#### Scenario: 中间分片阻断
- **WHEN** 任一分片返回 Block
- **THEN** evaluator MAY 立即早停
- **THEN** 请求 MUST 被阻断且不得部分转发
#### Scenario: 最后一个必要分片失败
- **WHEN** 前面分片安全但最后一个必要分片超时或响应无效
- **THEN** 系统 MUST 返回 unavailable/invalid_response
- **THEN** 系统 MUST NOT 根据部分结果放行
### Requirement: 同步节点故障切换必须有序且 fail-closed
系统 SHALL 按配置顺序尝试启用节点。连接失败、429、5xx 和超时 MAY 在总 deadline 尚有剩余时切换到下一节点;401/403、严格解析失败或耗尽节点 MUST 结束为不可用/非法响应。同步模式 MUST NOT 提供隐式 fail-open。
#### Scenario: 首节点暂时失败而次节点成功
- **WHEN** 首节点返回可重试错误且次节点在剩余预算内返回合法结果
- **THEN** 系统 MUST 使用次节点结果
- **THEN** failover 指标 MUST 增加
#### Scenario: 认证失败
- **WHEN** 节点返回 401 或 403
- **THEN** 系统 MUST 视为不可重试配置错误
- **THEN** 请求 MUST 返回 503 而不是按 Safe 放行
#### Scenario: 所有节点容量饱和
- **WHEN** 全局或每节点 bulkhead 均无法接受 evaluation
- **THEN** 系统 MUST 快速返回 `prompt_guard_unavailable`
- **THEN** 系统 MUST 不无限排队
### Requirement: HTTP 拒绝必须保持协议兼容和稳定错误码
同步 Guard MUST 使用现有 Handler 的协议错误构造器和最小扩展,且只向客户端暴露通用消息、稳定 Prompt Guard code/reason 和 request ID。OpenAI/Claude MUST 在 error 对象的可选 `code` 字段携带稳定代码并保留原合法 type;Gemini MUST 保留数值 `error.code` 与 canonical status,并在 `google.rpc.ErrorInfo.reason` 携带稳定代码。响应 MUST 不包含风险正文、类别细节、内部节点地址或凭据。
#### Scenario: HTTP Block
- **WHEN** 同步 Guard 判定为 Block
- **THEN** HTTP 状态 MUST 为 403
- **THEN** error_code MUST 为 `prompt_guard_blocked`
#### Scenario: Gemini HTTP Block
- **WHEN** Gemini 入口的同步 Guard 判定为 Block
- **THEN** Google error envelope 的 `error.code` MUST 保持数值 403 且 status MUST 为对应 canonical status
- **THEN** `error.details` 中 ErrorInfo reason MUST 为 `prompt_guard_blocked`
#### Scenario: HTTP Guard 不可用
- **WHEN** 节点超时、连接失败、熔断或容量不足
- **THEN** HTTP 状态 MUST 为 503
- **THEN** error_code MUST 为 `prompt_guard_unavailable`
#### Scenario: HTTP Guard 响应非法
- **WHEN** Guard 输出无法严格解析
- **THEN** HTTP 状态 MUST 为 503
- **THEN** error_code MUST 为 `prompt_guard_invalid_response`
### Requirement: Responses WebSocket 必须对每个 response.create 执行门禁
系统 SHALL 在 WebSocket 首次和后续每个 `response.create` 帧进入本轮用户/账号并发、计费和上游发送之前执行同步 Guard。一次安全结果 MUST NOT 被复用于不同的后续帧。
#### Scenario: 首轮 Block
- **WHEN** 首个 response.create 被判定为 Block
- **THEN** 服务端 MUST 不建立本轮上游请求或计费记录
- **THEN** 服务端 MUST 使用 close code 4403 和 reason `prompt_guard_blocked` 关闭连接
#### Scenario: 后续轮次 Block
- **WHEN** 已建立连接的后续 response.create 被判定为 Block
- **THEN** 该帧 MUST 不发送给上游且不得创建本轮计费记录
- **THEN** 服务端 MUST 使用 4403 关闭连接并记录 stage=subsequent_turn
#### Scenario: WebSocket Guard 不可用
- **WHEN** 首轮或后续轮次 Guard 不可用或响应非法
- **THEN** 服务端 MUST 使用 close code 1013
- **THEN** reason MUST 为 `prompt_guard_unavailable``prompt_guard_invalid_response`
### Requirement: 同步结果必须复用到脱敏事件且不得重复扫描
系统 SHALL 在一次同步 evaluation 后把已得到的归一化结果交给独立记录路径。记录路径 MUST NOT 重新调用 Guard,也 MUST NOT 需要完整提示词正文;同步结果最多对应一个任务事实和一个按存储策略决定的事件。
#### Scenario: 同步 Block 被记录
- **WHEN** evaluator 已得到 Block
- **THEN** 系统 MUST 用脱敏快照和既有结果创建 done 任务及风险事件
- **THEN** Guard 调用次数 MUST 等于 evaluation 实际需要的节点/分片次数,而不是因记录而增加
#### Scenario: 同步 Allow 且不保存 Pass
- **WHEN** evaluator 得到 Allow 且 store_pass_events=false
- **THEN** 系统 MAY 只保存任务/指标而不创建 Pass 事件
### Requirement: 配置必须以版本化快照发布到请求热路径
系统 SHALL 为提示词审计配置维护单调递增 config_version、updated_at、updated_by 和 change_summary。保存后 MUST 原子替换本实例快照并通过 Redis 发布失效通知;请求热路径 MUST 读取内存快照而不是逐请求查询数据库。
#### Scenario: 多实例收到配置更新
- **WHEN** 管理员成功保存新配置
- **THEN** 保存实例 MUST 立即安装新版本并发布 Redis 失效通知
- **THEN** 其他实例 MUST 重新加载并原子替换快照
#### Scenario: 两个管理员并发保存配置
- **WHEN** 两个保存请求携带相同 expected_config_version 且第一个已提交新版本
- **THEN** 第二个请求 MUST 返回 409 `prompt_audit_config_conflict`
- **THEN** 第二个请求 MUST NOT 静默覆盖第一个请求或复用相同 config_version
#### Scenario: Redis 通知不可用
- **WHEN** 配置已保存但 Redis publish 失败
- **THEN** 系统 MUST 记录 `prompt_guard.config_reload_degraded`
- **THEN** 其他实例 MUST 通过有界 TTL 刷新最终获得新版本
#### Scenario: 冷启动无法加载严格配置
- **WHEN** 实例冷启动且无法获得有效配置快照
- **THEN** 对已知要求同步阻止的适用请求 MUST fail-closed
- **THEN** 运行态 MUST 暴露配置加载错误
### Requirement: Guard 关键路径必须可观测且不得泄密
系统 SHALL 输出稳定结构化事件并提供计数/耗时指标。日志至少 MUST 覆盖配置更新/加载/降级、evaluation 开始、Allow、Block、失败、结果记录失败、异步投递/丢弃、Worker 处理/重试/失败、逐分片开始/完成/失败、分片聚合和滞留回收。
#### Scenario: 同步请求被阻断
- **WHEN** Guard 阻断一个请求
- **THEN** 日志 MUST 包含 request_id、user_id、api_key_id、group_id、protocol、endpoint、model、config_version、guard_endpoint_id、decision、action、chunk_total、latency_ms、status 和 error_code
- **THEN** 日志 MUST 明确包含 `upstream_dispatched=false``billing_preconsumed=false` 或目标项目等价字段
#### Scenario: 检查日志敏感字段
- **WHEN** 测试捕获提示词审计日志
- **THEN** 日志中 MUST 不包含原始提示词、API Key、Authorization、完整 Guard URL query 或 Redis 载荷
### Requirement: 禁用或回滚同步阻止必须即时恢复异步行为
系统 SHALL 支持仅通过关闭 blocking_enabled 回到异步只审计,无需删除表、清空历史事件或停止现有内容审核。
#### Scenario: 管理员关闭同步阻止
- **WHEN** blocking_enabled 从 true 保存为 false 且新配置已生效
- **THEN** 后续适用请求 MUST 不再等待 Guard 同步结果
- **THEN** enabled=true 时后续请求 MUST 改为异步投递
- **THEN** 历史任务和事件 MUST 保留
@@ -0,0 +1,160 @@
## ADDED Requirements
### Requirement: 管理台必须提供安全审计分组和独立提示词审计页面
控制台 SHALL 把安全相关的内容审核页面组织到“安全审计”导航分组中,并新增独立“提示词审计”页面。原 `/admin/risk-control` 路由、页面状态和功能 MUST 保持兼容;新页面路由 MUST 为 `/admin/prompt-audit` 或经实现评审确认的等价稳定路由。
#### Scenario: 管理员查看侧栏
- **WHEN** 管理员已登录且 risk_control_enabled=true
- **THEN** 侧栏 MUST 展示“安全审计”可展开分组
- **THEN** 分组 MUST 至少包含“内容审核”和“提示词审计”两个子入口
#### Scenario: 管理员打开原内容审核页面
- **WHEN** 管理员访问 `/admin/risk-control`
- **THEN** 页面 MUST 继续展示原有 Moderations、关键词、Hash、封号、邮件和记录功能
- **THEN** 页面 MUST NOT 被提示词审计配置或事件替换
#### Scenario: 功能总开关关闭
- **WHEN** risk_control_enabled=false
- **THEN** 安全审计导航和提示词审计网关执行 MUST 按现有功能开关策略停用
- **THEN** 已存储的配置和历史事件 MUST 不被删除
### Requirement: 提示词审计页面必须提供清晰的独立工作区
页面 SHALL 在同一工作区展示运行概览、审计池、审计策略、事件列表和固定保存操作区。页面 MUST 清楚区分“异步只审计”和“同步阻止”,并 MUST 展示未保存状态和最终生效状态。
#### Scenario: 初次打开页面
- **WHEN** 管理员打开提示词审计页面
- **THEN** 页面 MUST 并行或有界加载配置、运行态、分组列表和事件列表
- **THEN** 页面 MUST 展示有效模式、Worker 状态、队列状态、节点连通性和最近错误
#### Scenario: 修改但未保存配置
- **WHEN** 管理员修改审计池、分类、范围或模式开关
- **THEN** 页面 MUST 显示“有未保存的更改”
- **THEN** 运行态 MUST 继续标识服务端当前生效版本,不能把草稿显示为已生效
### Requirement: 页面必须支持完整审计池管理和真实探测
页面 SHALL 支持新增、编辑、启用、禁用和删除审计池,并允许配置 Base URL、API Key、Model、超时和 input_limit。API Key 已保存后 MUST 只显示配置状态,不能回显明文。
#### Scenario: 编辑已保存节点
- **WHEN** 管理员打开已配置 API Key 的节点
- **THEN** API Key 输入框 MUST 为空或显示不可逆占位状态
- **THEN** 未填写新 Key 保存时 MUST 保留原密文
- **THEN** 页面 MUST 提供显式清除凭据操作
#### Scenario: 执行连接测试
- **WHEN** 管理员点击节点“连接测试”
- **THEN** 页面 MUST 展示配置校验、发送请求、服务响应和测试结论状态
- **THEN** 结果 MUST 展示耗时、HTTP 状态、稳定错误码和脱敏消息
### Requirement: 页面必须支持审计范围和九类风险配置
页面 SHALL 支持全部分组或指定 group ID 范围,并展示九类 Qwen3Guard 风险分类。页面 MUST 使用目标项目真实分组数据,已删除但仍存在于配置中的分组 MUST 显示为失效项而不是被静默丢弃。
#### Scenario: 选择指定分组
- **WHEN** 管理员把范围切换为 selected 并选择一个或多个分组
- **THEN** 保存载荷 MUST 使用稳定 group ID
- **THEN** 页面 MUST 展示已选数量并支持搜索
#### Scenario: 查看风险分类
- **WHEN** 管理员查看扫描器配置
- **THEN** 页面 MUST 展示 Violent、Non-violent Illegal Acts、Sexual Content or Sexual Acts、PII、Suicide & Self-Harm、Unethical Acts、Politically Sensitive Topics、Copyright Violation、Jailbreak
### Requirement: 开启同步阻止必须有明确的风险确认
页面 SHALL 把 enabled、blocking_enabled 和 store_pass_events 作为独立开关。关闭 enabled 时 MUST 自动关闭并禁用 blocking_enabled;开启 blocking_enabled 时 MUST 展示二次确认,说明请求延迟、Block 和 Guard 不可用的 fail-closed 行为。
#### Scenario: 开启同步阻止
- **WHEN** 管理员把 blocking_enabled 从 false 切换为 true
- **THEN** 页面 MUST 在保存前展示风险确认
- **THEN** 确认文案 MUST 说明请求会等待 GuardBlock 或 Guard 不可用时不会访问上游
#### Scenario: 关闭审计总开关
- **WHEN** 管理员关闭 enabled
- **THEN** 页面草稿 MUST 同时把 blocking_enabled 设为 false
### Requirement: 配置保存必须可验证且不得泄露凭据
页面 SHALL 通过一个统一保存动作提交完整规范化配置。保存成功后 MUST 用后端返回值刷新页面快照、清除已提交 API Key 明文并显示 config_version;保存失败 MUST 保留草稿并展示稳定错误信息。
#### Scenario: 保存成功
- **WHEN** 后端成功保存配置
- **THEN** 页面 MUST 显示配置已同步和新的 config_version
- **THEN** 浏览器状态、调试日志和缓存 MUST 不再保留刚提交的 API Key 明文
#### Scenario: 保存校验失败
- **WHEN** 后端返回节点地址、模式组合或策略校验错误
- **THEN** 页面 MUST 保留用户草稿
- **THEN** 页面 MUST 展示稳定错误码及可行动的中文说明
#### Scenario: 配置被其他管理员更新
- **WHEN** 保存返回 409 `prompt_audit_config_conflict`
- **THEN** 页面 MUST 保留本地草稿并提示服务端配置已变化
- **THEN** 页面 MUST 提供重新加载/对比入口,不得自动用旧草稿覆盖新配置
### Requirement: 页面必须展示真实运行态和同步 Guard 指标
页面 SHALL 展示 process_status、Worker 总数/活动数、队列容量/长度、queued/processing/done/failed 数、处理/失败总数、最近时间、节点连通性、配置版本一致性、Redis Payload Store 状态和同步 Guard Allow/Flag/Block/Unavailable/timeout/failover/bulkhead 指标。
#### Scenario: 配置版本未同步
- **WHEN** expected_config_version 与 active_config_version 不一致
- **THEN** 页面 MUST 显示明确的配置未同步或加载中状态
- **THEN** 页面 MUST 展示最近加载错误和时间(如存在)
#### Scenario: Worker 心跳过期
- **WHEN** heartbeat_at 超过后端定义的健康窗口
- **THEN** 页面 MUST 显示 stale 而不是 running
### Requirement: 页面必须提供可复核的事件列表和详情
页面 SHALL 提供事件分页、总数、decision/risk/endpoint/group/user/API key/request ID/prompt Hash/关键字/时间范围筛选、行选择和详情抽屉或弹窗。详情 MUST 只展示脱敏数据。
#### Scenario: 查看事件列表
- **WHEN** 管理员应用筛选
- **THEN** 表格 MUST 展示时间、用户/API key、分组、入口/模型、判定、风险、分类、预览和操作
#### Scenario: 查看事件详情
- **WHEN** 管理员打开一条事件
- **THEN** 页面 MUST 展示脱敏预览、审计摘要、结构化返回、具体风险摘要和技术信息
- **THEN** 页面 MUST 提供 request ID、prompt Hash、scanner、策略、节点、配置版本、分片数和耗时
- **THEN** 页面 MUST 不展示完整提示词或节点 API Key
#### Scenario: 复核用户身份和具体风险
- **WHEN** 事件拥有用户名、用户邮箱、API Key 名称和一个或多个风险分类
- **THEN** 页面 MUST 将用户名、邮箱和 API Key 名称分列展示并提供独立复制操作
- **THEN** 页面 MUST 为每个风险展示 category、标题、说明、严重度、动作、scanner、score 和脱敏证据摘要
- **THEN** 用户不存在或字段为空时 MUST 显示稳定 fallback,而不是把其他身份字段冒充为该字段
### Requirement: 页面必须提供防误操作的事件删除流程
页面 SHALL 支持单条删除、选中项批量删除和按筛选删除。按筛选删除 MUST 先调用预览接口,并要求明确时间范围、matched_count、snapshot_max_id、filter_hash、服务端认证 confirmation_token 和二次确认。
#### Scenario: 单条删除
- **WHEN** 管理员确认删除一条事件
- **THEN** 页面 MUST 调用单条删除接口并在成功后刷新列表与运行统计
#### Scenario: 按筛选删除
- **WHEN** 管理员已设置明确时间范围并请求按筛选删除
- **THEN** 页面 MUST 先展示匹配数量和规范化筛选摘要
- **THEN** 只有管理员再次确认后才能提交 filter_hash、confirmation_token 和 confirm=true
#### Scenario: 筛选在预览后发生变化
- **WHEN** 管理员预览后修改任意筛选条件
- **THEN** 旧 filter_hash MUST 失效
- **THEN** 旧 confirmation_token MUST 同时失效
- **THEN** 页面 MUST 要求重新预览
### Requirement: 管理 API 操作必须纳入现有管理员审计
所有配置写入、节点探测和事件删除 SHALL 复用现有管理员鉴权与管理操作审计。审计详情 MUST 使用脱敏摘要,禁止记录 API Key、完整提示词或完整请求载荷。
#### Scenario: 配置更新成功
- **WHEN** 管理员成功保存提示词审计配置
- **THEN** 管理操作审计 MUST 记录操作者、request ID、enabled、blocking_enabled、config_version、节点数量、分类数量和分组范围摘要
#### Scenario: 节点探测失败
- **WHEN** 管理员探测节点失败
- **THEN** 管理操作审计 MUST 记录节点 ID、稳定错误码、HTTP 状态和耗时
- **THEN** 审计详情 MUST 不包含 API Key 或完整 Base URL query
### Requirement: 页面必须满足响应式、可访问和国际化要求
页面 SHALL 使用现有 Vue 3、i18n 和通用组件体系,支持桌面与窄屏,所有开关、输入、按钮、状态和对话框 MUST 具有可访问名称;新增中英文文案键 MUST 成对提供且通过现有 lint、typecheck 和 Vitest。
#### Scenario: 窄屏使用
- **WHEN** 页面宽度小于桌面断点
- **THEN** 配置区、筛选区、表格和固定保存栏 MUST 可滚动或重排而不遮挡关键操作
#### Scenario: 键盘和读屏操作
- **WHEN** 用户只使用键盘或读屏访问页面
- **THEN** 审计池操作、模式开关、筛选、详情和确认对话框 MUST 可识别且可操作
@@ -0,0 +1,200 @@
## 1. 固定源基线与实施边界
- [x] 1.1 记录 aicodex-api 参考仓库的绝对路径、分支、HEAD commit、`git status --short``git diff --stat`,写入本 change 的 `source-baseline.md`
- [x] 1.2 为参考仓库未提交的 Prompt Audit/Prompt Guard 文件生成只读 patch 或固定到专用 commit/tag,并在 `source-baseline.md` 中记录校验和
- [x] 1.3 复核并维护 `source-feature-map.md` 的“源功能 → OpenSpec Requirement → 目标代码 → 目标测试”追踪表,覆盖异步、同步、HTTP、SSE、WS、配置、风险摘要、身份展示、事件、运行态和页面
- [x] 1.4 确认设计中的五个 Open Questions,并把已确认结论回写 `design.md`,未确认项不得在实现中自行漂移
- [x] 1.5 运行并保存现有基线信号:`cd backend && go test ./internal/service -run ContentModeration -count=1`
- [x] 1.6 运行并保存前端现有风控页面和路由相关 Vitest,证明修改前基线通过
- [x] 1.7 将实现拆成数据基础、异步审计、控制台、同步门禁和灰度五个可独立评审的提交/PR 阶段
## 2. 建立独立模块和公共契约
- [x] 2.1 创建 `backend/internal/securityaudit/` 并按 design 的文件职责建立最小包骨架,不在现有 `content_moderation.go` 中加入 Prompt Audit 实现
- [x] 2.2 定义可信 `Request`、分列身份快照、脱敏 `PromptSnapshot``Decision``NormalizedResult``IssueSummary``RuntimeSnapshot` 和稳定枚举/错误码
- [x] 2.3 定义 ConfigStore、JobRepository、PayloadStore、PromptScanner、Clock 和 Metrics 等可注入接口,避免核心逻辑依赖包级全局变量
- [x] 2.4 实现 Coordinator 的 off/async/blocking 分支和固定阻断优先级,并用 fake engine 单测覆盖两个引擎所有组合
- [x] 2.5 证明 Coordinator 不转换现有 Moderations 分类、不触发其额外副作用、不写入两个引擎的业务表
- [x] 2.6 为 PromptService 实现显式 `Start(ctx)``Shutdown(ctx)` 生命周期,禁止构造函数启动不可控 goroutine
- [x] 2.7 增加 Wire provider 和应用启动/停止接线,并保证 Prompt Worker 启动失败不会阻止主 API 提供非审计能力
## 3. 创建数据库迁移和 Repository
- [x] 3.1 基于实施时最大迁移序号新增不可变 SQL migration,创建 `prompt_audit_jobs``prompt_audit_events`
- [x] 3.2 为 jobs 添加 staging/queued/processing/retry/done/failed 状态字段、递增 claim_version fencing token、租约、尝试次数、配置版本、执行模式、用户名/邮箱/API Key 名称和请求快照列
- [x] 3.3 为 events 添加分列身份快照、脱敏提示词快照、decision/risk/action、JSONB scanner 数据、节点/策略/版本、分片数和耗时列
- [x] 3.4 添加 jobs 的调度、request、user、API key、group、Hash、时间索引,并检查索引名不与现有 schema 冲突
- [x] 3.5 添加 events 的 job、request、decision/time、risk/time、user/API key/group/time、Hash 和时间索引
- [x] 3.6 为 events.job_id 配置 `ON DELETE CASCADE`,为 user/api_key/group 配置 `ON DELETE SET NULL`,保留快照字符串
- [x] 3.7 添加数据库约束,拒绝负 attempts/max_attempts/claim_version/prompt_length/message_count/chunk_total/latency_ms 和不支持的关键状态
- [x] 3.8 实现 JobRepository 的 staging 创建、queued 发布、原子 `FOR UPDATE SKIP LOCKED` 领取并递增 claim_version,以及所有携带 claim_version 条件的租约刷新、事件提交和 done/retry/failed 更新
- [x] 3.9 实现 staging 和 processing 滞留任务的有界批量回收
- [x] 3.10 实现 EventRepository 的创建、分页、详情、复合筛选、计数和稳定排序
- [x] 3.11 实现单条、批量 ID、同一快照下的 snapshot_max_id/filter_hash 预览、短期管理员绑定 confirmation_token 和分批筛选删除,并只删除高水位内事件及无事件引用且非 processing 的孤立 job
- [x] 3.12 添加 migration 重复执行、索引存在、外键行为、原子领取并发、旧 Worker fencing 和 Repository 集成测试
- [x] 3.13 添加 schema 泄露门禁,断言两张表不存在 raw_prompt、payload、token、authorization 或等价原文/凭据列
## 4. 实现配置、凭据和多实例快照
- [x] 4.1 新增 `SettingKeyPromptAuditConfig`,实现 DefaultConfig、存储 DTO、公共 DTO 和保存请求 DTO
- [x] 4.2 实现 enabled/blocking_enabled 三态归一和 `prompt_guard_requires_audit_enabled` 校验
- [x] 4.3 实现唯一 `strategy=priority`、worker_count、queue_capacity、timeout、input_limit、group_ids 和 scanners 边界校验
- [x] 4.4 复用 SecretEncryptor 保存 endpoint token_ciphertext,并实现“保留原密文、替换、显式清除”三种写入语义
- [x] 4.5 确保公共配置只返回 has_token/token_status,任何 JSON marshal 路径都不会输出密文或明文
- [x] 4.6 实现携带 expected_config_version 的 PostgreSQL advisory-lock CAS 保存、单调 config_version、409 conflict、updated_at、updated_by 和脱敏 change_summary
- [x] 4.7 实现原子内存配置快照、最后有效版本、加载错误和有界 TTL 刷新
- [x] 4.8 实现 Redis `sub2api:prompt_guard:config:invalidate` publish/subscribe 和 publish 失败降级日志
- [x] 4.9 添加配置加密往返、旧字段缺失、非法组合、边界值、两管理员/两实例并发 CAS、多实例失效和 Redis 不可用测试
- [x] 4.10 添加 canary secret 测试,断言 settings 公共读取、日志和错误均不出现节点 API Key
## 5. 实现安全出站 Client 和节点探测
- [x] 5.1 实现统一 Base URL 规范化并固定调用 `{base}/v1/chat/completions`
- [x] 5.2 实现 scheme、userinfo、query、fragment、metadata host、link-local、multicast、unspecified 和保留地址校验
- [x] 5.3 实现公网必须 HTTPS、本机/显式私网 HTTP 例外和 DNS 解析后 DialContext IP 二次校验
- [x] 5.4 创建独立 HTTP Transport,配置 Dial/TLS/ResponseHeader timeout、连接池和 256 KiB 响应上限
- [x] 5.5 禁止 HTTP 重定向,并确保每个错误只暴露 endpoint ID 和稳定错误码
- [x] 5.6 实现节点 `/models` 就绪检查以及必要时的真实 Qwen3Guard fallback probe
- [x] 5.7 实现探测结果 DTOok/status/error_code/message/latency_ms/http_status/retryable/checked_at/token_applied
- [x] 5.8 添加 SSRF、DNS rebinding、重定向、超大响应、认证失败、429、5xx、连接失败和超时测试
## 6. 实现协议快照、脱敏和分片
- [x] 6.1 实现 OpenAI Chat Completions 用户消息提取,支持字符串和文本内容块并把最新用户输入置于首段
- [x] 6.2 实现 OpenAI Responses input 字符串、消息数组和内容块提取
- [x] 6.3 实现 Claude Messages 用户文本块提取
- [x] 6.4 实现 Gemini contents/parts 用户文本提取
- [x] 6.5 实现 OpenAI Images、Grok 媒体和目标项目其他生成请求的纯文本 prompt 提取,明确排除图片/base64 数据
- [x] 6.6 实现 Responses WebSocket `response.create` 帧提取并支持 first_turn/subsequent_turn stage
- [x] 6.7 实现 SHA-256、消息数、Unicode 字符数和确定性 metadata 计算
- [x] 6.8 实现凭据、Bearer、邮箱、电话及常见敏感模式的预览脱敏和 rune 安全裁剪
- [x] 6.9 实现最新输入优先的 scan text 组合和按 rune 的 input_limit 分片
- [x] 6.10 添加中文、emoji、组合字符、超长文本、空输入、混合 content block、媒体 payload 和最新输入优先测试
- [x] 6.11 添加 canary prompt 测试,断言预览不可恢复完整输入且 Hash 与实际 scan text 一致
## 7. 实现 Qwen3Guard 严格解析和结果聚合
- [x] 7.1 定义九类 Qwen3Guard 官方输入类别和目标项目展示标签
- [x] 7.2 构建 OpenAI Chat Completions 请求,固定 role=user、temperature=0、max_tokens=64、seed=42
- [x] 7.3 实现 choices/message/content 提取,兼容目标审计节点允许的最小合法响应形态
- [x] 7.4 实现严格单 Safety 行、单 Categories 行、无额外非空说明解析
- [x] 7.5 实现类别别名归一、未知类别保留和启用类别过滤
- [x] 7.6 实现 Safe/Controversial/Unsafe 到 pass/flag/critical 与 Allow/Warn/Block 的确定性映射
- [x] 7.7 实现 Jailbreak、PII、Suicide & Self-Harm 的高风险 Controversial 提升规则
- [x] 7.8 实现多分片最严重结果聚合、分类/证据去重、分片 metadata 和 Block 早停
- [x] 7.9 确保只有全部必要分片成功才能 Allow,部分成功不得产生 Safe
- [x] 7.10 添加模型合法输出、重复字段、额外说明、未知 Safety、未知类别、禁用类别和多分片聚合测试
- [x] 7.11 从分类、策略和脱敏 evidence 确定性生成 IssueSummary,覆盖标题/说明/严重度/动作/score/位置/Hash,且不新增重复数据库事实列
## 8. 实现异步投递和 Worker
- [x] 8.1 实现 Prompt Audit 有效模式、risk_control_enabled、分组范围、节点可用性,以及 advisory-lock 事务内 active count + staging INSERT 的多实例严格队列容量准入
- [x] 8.2 实现 staging job → Redis SET EX 1800 → queued 的发布协议
- [x] 8.3 实现所有投递失败 reason 和 `prompt_audit.enqueue_skipped/enqueue_dropped/job_enqueued` 结构化日志
- [x] 8.4 保证异步投递复制必要请求数据并使用有界后台 context,不引用 Gin request 生命周期后的可变内存
- [x] 8.5 实现 Redis PayloadStore 的 Set/Get/Delete 和命名空间 key
- [x] 8.6 实现 Worker 轮询、活动计数、processing 租约、节点有序故障切换和任务处理
- [x] 8.7 实现 5s/30s/2m 有界退避、可重试分类和 max_attempts 终止
- [x] 8.8 实现 store_pass_events=false 时仅完成 job、不写 Pass event
- [x] 8.9 实现风险事件和 `prompt_audit.finding_recorded/processed/process_failed` 日志
- [x] 8.10 实现 Worker panic 单任务恢复、优雅停止和 shutdown timeout 日志
- [x] 8.11 添加队列满、Redis SET 失败、发布失败、进程中断、重复领取、租约刷新、滞留回收、旧 Worker claim_version 失效和重试集成测试
- [x] 8.12 证明异步模式所有失败都不改变模型请求状态、错误体和上游转发次数
- [x] 8.13 为逐分片开始/完成/失败和聚合输出稳定日志,字段只含索引、字符数、限制、节点、动作、耗时和错误码
## 9. 实现同步 Guard evaluator
- [x] 9.1 实现全局 64、每节点 16 的非阻塞 bulkhead,并允许测试注入更小容量
- [x] 9.2 实现以首个启用节点 timeout 为总 deadline,分片和节点切换共享剩余预算
- [x] 9.3 实现连接/429/5xx/超时切换下一节点,401/403/invalid_response 终止
- [x] 9.4 实现 Allow/Flag/Block/Unavailable Decision 和 allow_next_stage
- [x] 9.5 实现 prompt_guard total/allowed/flagged/blocked/unavailable/invalid/timeouts/failovers/bulkhead_full 指标
- [x] 9.6 实现同步结果轻量记录 adapter,在单事务中创建 done job 和可选 event,禁止再次扫描
- [x] 9.7 实现记录失败 `prompt_guard.result_record_failed`,并证明不改变已确定的 Allow/Block
- [x] 9.8 添加完整分片、Block 早停、最后分片失败、所有节点失败、bulkhead 满和 context cancel 测试
## 10. 接入网关并保持兼容
- [x] 10.1 在 GatewayHandler 和 OpenAIGatewayHandler 中注入 SecurityAudit Coordinator,同时保留现有 ContentModerationService 供 cyber policy 记录使用
- [x] 10.2 将 Chat Completions 现有审核调用替换为统一 `checkSecurityAudit`
- [x] 10.3 将 HTTP Responses 现有审核调用替换为统一 `checkSecurityAudit`
- [x] 10.4 将 Claude Messages 现有审核调用替换为统一 `checkSecurityAudit`
- [x] 10.5 将 Gemini 现有审核调用替换为统一 `checkSecurityAudit`
- [x] 10.6 将 OpenAI Images 和 Grok 媒体文本 prompt 审核调用替换为统一 `checkSecurityAudit`
- [x] 10.7 接入 Responses WebSocket 首个 response.create 门禁,置于用户/账号 slot、计费和上游拨号前
- [x] 10.8 接入 Responses WebSocket 后续每个 response.create 门禁,置于本轮 slot、计费和上游发送前
- [x] 10.9 为现有错误 helper 增加最小 Prompt Guard adapterOpenAI/Claude 可选 error.codeGemini 保留数值 code/status 并使用 google.rpc.ErrorInfo.reason,映射 blocked/unavailable/invalid_response
- [x] 10.10 确保 SSE 在 Guard 完成前没有写 response header 或首字节
- [x] 10.11 增加静态/结构测试,枚举所有现有用户文本路由并在缺少 Coordinator 接线时失败
- [x] 10.12 用账号选择、计费和上游 fake counter 证明 Block/Unavailable/Invalid 时三者调用均为 0
- [x] 10.13 回归现有 ContentModeration Block 响应优先级、文案、封号、邮件和记录语义
## 11. 实现管理 API 和管理操作审计
- [x] 11.1 创建 PromptAdminHandler 并注册 `/admin/prompt-audit` 独立路由组,复用 AdminAuth 和现有安全中间件
- [x] 11.2 实现 GET/PUT configPUT 强制 expected_config_version 并映射 409 conflict,返回公共 DTO 并记录脱敏配置更新审计
- [x] 11.3 实现 POST endpoints/probe,支持使用已保存密文或请求中的临时 token 且绝不回显
- [x] 11.4 实现 GET runtime,聚合配置版本、Worker、DB 队列、Redis、节点连通性和 Guard 指标
- [x] 11.5 实现 GET events 和 GET events/:id,支持完整筛选、分页、用户名/邮箱/API Key 名称分列快照和派生 issue_summaries
- [x] 11.6 实现 DELETE 单事件和 POST batch-delete,并限制单批 ID 数量
- [x] 11.7 实现 delete-preview 和 delete-by-filter,强制时间范围、snapshot_max_id、canonical filter_hash、SecretEncryptor 认证的管理员绑定/5 分钟 confirmation_token 和 confirm=true
- [x] 11.8 为配置、探测和删除的成功/失败写入现有管理员操作审计,detail 使用字段 allowlist
- [x] 11.9 添加未认证、非管理员、非法 ID/时间、Hash/token/操作者/过期不匹配、预览后新事件、并发删除和敏感字段响应测试
## 12. 实现独立控制台页面
- [x] 12.1 创建 `frontend/src/features/prompt-audit/` 的 api、types、viewModel、components、PromptAuditView 和测试目录
- [x] 12.2 新增 `/admin/prompt-audit` 路由并复用 requiresAuth/requiresAdmin/requiresRiskControl guard
- [x] 12.3 将侧栏现有风控入口改为 expandOnly“安全审计”分组,保留 `/admin/risk-control` 子入口并新增 Prompt Audit 子入口
- [x] 12.4 实现配置、运行态、分组和事件的有界并行加载及独立错误状态
- [x] 12.5 实现审计池新增/编辑/启停/删除、参数对话框和真实探测进度/结果
- [x] 12.6 实现 API Key 空值保留、显式替换/清除和保存成功后清除明文 state
- [x] 12.7 实现 all/selected group 范围、分组搜索、失效分组提示和九类 scanner 选择
- [x] 12.8 实现 enabled/blocking/store pass 固定保存栏、dirty snapshot、重置和同步阻止二次确认
- [x] 12.9 实现配置版本、Worker/队列、Redis、连通性、最近错误和 Guard 指标概览
- [x] 12.10 实现事件复合筛选、时间范围、分页、行选择、用户名/邮箱/API Key 名称分列复制、模型信息和风险展示
- [x] 12.11 实现事件详情的脱敏预览、审计返回、IssueSummary 具体风险和技术信息 tabs
- [x] 12.12 实现单条、批量和按筛选 snapshot/Hash/token/确认删除流程,筛选变化后使旧 Hash 与 confirmation_token 同时失效
- [x] 12.13 新增中英文对称 i18n,并为所有输入、开关、按钮、状态和对话框提供可访问名称
- [x] 12.14 添加桌面、窄屏、键盘操作、dirty 状态、探测、模式联动、事件删除和 secret state 清理 Vitest
- [x] 12.15 回归原 RiskControlView 的路由、功能开关和关键测试,确认业务逻辑未改变
## 13. 补齐日志、指标和敏感信息门禁
- [x] 13.1 实现 design 中列出的 prompt_audit/prompt_guard 稳定事件词典和字段 allowlist helper
- [x] 13.2 为关键路径补齐 request_id、user/api-key/group、protocol、endpoint、model、config/job/event/node/version、结果、耗时和错误字段
- [x] 13.3 在同步拒绝日志中明确 upstream_dispatched=false 和 billing_preconsumed=false
- [x] 13.4 实现运行计数和延迟指标,并确保 runtime API 的字段与日志错误码使用同一词典
- [x] 13.5 添加日志捕获测试,使用 canary prompt、API Key、Authorization 和带 query URL 证明敏感内容不出现
- [x] 13.6 添加数据库/API/前端快照泄露测试,统一扫描 canary secret
- [x] 13.7 对错误消息和 last_error_message 做长度限制与脱敏,禁止保存 Guard 原始响应正文
## 14. 完成验证、质量门禁和灰度准备
- [x] 14.1 运行 `openspec validate add-openai-compatible-prompt-audit --type change --strict --no-interactive`
- [x] 14.2 运行 `cd backend && go test ./internal/securityaudit/... -count=1`
- [x] 14.3 运行 `cd backend && go test ./internal/handler/... ./internal/server/... -count=1` 并保存路由矩阵结果
- [x] 14.4 运行 `cd backend && go test -race ./internal/securityaudit/... -count=1`
- [x] 14.5 在可用 PostgreSQL/Redis 环境运行 migration、Repository、多 Worker 和配置失效集成测试
- [x] 14.6 运行 `make test-backend`,记录全量 Go test 和 golangci-lint 结果
- [x] 14.7 运行 `pnpm --dir frontend run lint:check``pnpm --dir frontend run typecheck` 和 Prompt Audit/RiskControl Vitest
- [x] 14.8 运行 `make build`,验证后端和前端生产构建
- [x] 14.9 执行 HTTP/SSE/WS 端到端矩阵,保存 Block/Unavailable 无账号、无计费、无上游证据
- [x] 14.10 执行敏感信息审查,检查 PostgreSQL、Redis key metadata、日志、API JSON、浏览器存储和页面截图
- [x] 14.11 在 async 模式对测试 group 记录 Guard P50/P95/P99、失败率、误报率和事件增长率基线
- [x] 14.12 定义 blocking 灰度准入阈值、告警阈值、值班检查步骤和一键关闭 blocking 的回滚手册
- [x] 14.13 更新 `verification.md`,为每条验收 Requirement 关联测试、日志、指标、SQL 或截图证据
- [x] 14.14 在实现偏离设计时先回写 proposal/design/specs/tasks,再继续编码,禁止让 OpenSpec 落后于代码
## 15. 管理员自主管理审计节点网络目标
- [x] 15.1 更新规格与设计,明确节点目标安全由管理员负责,不再实施地址类别、DNS 结果、HTTP 或重定向拦截
- [x] 15.2 移除 Base URL 私网/特殊地址限制和 DialContext DNS/IP 二次拦截,恢复标准重定向行为
- [x] 15.3 更新出站客户端测试,覆盖 HTTP、私网/特殊地址配置和重定向,并回归响应上限与凭据保护
- [x] 15.4 运行 OpenSpec 严格校验和 securityaudit 测试,并用真实内网节点探测验证
## 16. 优化审计池节点列表并重新部署
- [x] 16.1 将审计池改为紧凑、响应式的节点列表,修复开关与节点名称拥挤并强化状态、限制和操作层级
- [x] 16.2 回归 Prompt Audit 前端组件测试、类型检查和生产构建
- [x] 16.3 在 deploy 目录按现有 Compose 配置重建镜像、重启容器并验证页面和节点探测
@@ -0,0 +1,492 @@
# 验证与灰度手册
## 1. 验证原则
本文件既是实现期验收矩阵,也是上线前证据索引模板。所有“待实现”项必须替换为可重复执行的测试名、命令输出、SQL 结果、日志查询或页面截图路径;仅写“人工验证通过”不算证据。
验证顺序:
1. OpenSpec 结构和需求完整性。
2. 纯函数、配置和核心单元测试。
3. PostgreSQL/Redis 集成与多 Worker/多实例测试。
4. Handler 协议矩阵和无副作用断言。
5. 前端功能、凭据状态、可访问和原页面回归。
6. 全量构建、canary 泄露检查、async 灰度和 blocking 准入。
关键不变量:
- off 等于升级前行为。
- async 的任何失败都不改变主请求结果。
- blocking 的 Block/Unavailable/Invalid 必须发生在账号、计费和上游之前。
- 现有 Content Moderation Block 响应永远优先且原副作用保持不变。
- PostgreSQL、日志、管理 API、前端和错误响应中没有完整 Prompt 或 Guard token。
## 2. Requirement → Evidence 追踪矩阵
状态词:`待实现``通过``失败``豁免(必须有批准链接)`。证据路径建议统一放到实现 PR 的 CI artifact 或 `docs/evidence/prompt-audit/<date>/`,不要把包含真实 Prompt/token 的原始数据提交到仓库。
### 2.1 prompt-input-audit
| ID | Requirement | 必备自动化证据 | 补充证据 | 状态 |
| --- | --- | --- | --- | --- |
| A01 | 独立且默认关闭 | Coordinator off 单测;默认 config 单测;现有 Moderation 回归 | 升级后 config/runtime 截图 | 通过(自动化) |
| A02 | OpenAI 兼容节点 | request builder goldenmock server 断言 `/v1/chat/completions`、model/messages/temperature/max_tokens/seed | probe 脱敏结果 | 通过(自动化) |
| A03 | 凭据和出站地址安全 | 加密往返;Public DTO canarySSRF/DNS rebinding/redirect/256 KiB 测试 | 配置 JSON 与日志扫描 | 通过(自动化) |
| A04 | 按协议提取输入快照 | Chat/Responses/Claude/Gemini/images/media/WS 表驱动测试;用户名/邮箱/API Key 名称分列 | 路由覆盖清单 | 通过(自动化) |
| A05 | 数据库快照脱敏不可恢复 | canary Prompt 入库后全列扫描;预览/hash 单测 | schema 禁止列 SQL | 通过(自动化+SQL) |
| A06 | 持久任务 + Redis TTL | staging→SET EX→queued;多实例队列 admission lockRedis/发布失败补偿测试 | TTL 1800 秒窗口证据 | 通过(集成) |
| A07 | Worker 可靠消费 | SKIP LOCKED、claim_version fencing、retry、lease refresh/reclaim、panic、shutdown 测试 | 多 Worker 运行指标 | 通过(集成+race) |
| A08 | Qwen3Guard 严格归一 | Safe/Controversial/Unsafe、九类、未知类、重复/额外/缺失字段测试 | golden response 语料 | 通过(自动化) |
| A09 | Unicode 完整分片 | 中文/emoji/组合字符/超长文本覆盖与顺序测试;部分失败不 Allow;逐片日志无正文 | chunk_total 事件样本 | 通过(自动化) |
| A10 | 独立可关联事件 | event transaction、store_pass_events、身份快照、FK/筛选、IssueSummary 派生测试 | 管理事件详情截图 | 通过(集成) |
| A11 | 真实运行态 | healthy/degraded/error、Redis/DB/Worker/节点/config version 测试 | runtime JSON 样本 | 通过(自动化) |
| A12 | 安全查询和删除 | 复合筛选、分页、单条/批量、snapshot max ID、认证 token/actor/expiry/hash、分批删除测试 | 管理审计日志 | 通过(集成) |
### 2.2 prompt-input-guard
| ID | Requirement | 必备自动化证据 | 补充证据 | 状态 |
| --- | --- | --- | --- | --- |
| G01 | 显式启用三态 | 配置真值表和非法组合测试 | 页面联动截图 | 通过(自动化) |
| G02 | 两引擎独立语义 | fake engines 全组合;Legacy Block 优先;两类事件独立 | 现有邮件/封号/Hash 回归 | 通过(自动化) |
| G03 | 门禁在副作用之前 | Block/Unavailable/Invalid 的 account/billing/upstream counter 均为 0 | Ops 请求链日志 | 通过(矩阵) |
| G04 | 覆盖所有协议入口 | routes 自动枚举/结构测试;HTTP/SSE/WS E2E 矩阵 | 已签字路由清单 | 通过(矩阵) |
| G05 | 同步分片共享预算且完整 | fake clock 总 deadlineBlock 早停;Allow 全片;最后片失败测试 | p95/p99 指标 | 通过(自动化) |
| G06 | 有序 fail-closed 故障切换 | 连接/429/5xx/timeout failover401/403/invalid 终止;bulkhead 测试 | 节点运行态 | 通过(自动化) |
| G07 | HTTP 协议兼容错误 | OpenAI/Claude 可选 code、Gemini 数值 code/status + ErrorInfo reason golden403/503 | curl 样本(脱敏) | 通过(golden) |
| G08 | WS 每个 response.create 门禁 | 首轮/后续轮次 Allow/Block/Unavailable/Invalid 测试;4403/1013 | WS trace(无正文) | 通过(结构+golden) |
| G09 | 同步结果复用且不重复扫描 | Guard fake 调用次数=chunk 数;record failure 不改 decision;无二次调用 | event/job 关联 SQL | 通过(自动化) |
| G10 | 版本化热路径快照 | PostgreSQL CAS 并发保存、双实例 invalidation、last-known-good、cold-start fail-closed、无热路径 DB 测试 | expected/active version 指标 | 通过(集成) |
| G11 | 可观测且不泄密 | 稳定日志/指标词典测试;canary 全介质扫描 | Dashboard/runtime 截图 | 通过(自动化+扫描) |
| G12 | 禁用/回滚即时生效 | blocking→async→off 多实例测试;进行中请求边界测试 | 回滚演练记录 | 通过(自动化;生产演练待签字) |
### 2.3 security-audit-console
| ID | Requirement | 必备自动化证据 | 补充证据 | 状态 |
| --- | --- | --- | --- | --- |
| C01 | 安全审计分组和独立页面 | router/Sidebar/feature guard 测试;旧路由回归 | 侧栏和双页面截图 | 通过(自动化) |
| C02 | 清晰独立工作区 | 页面分区、独立加载/错误、dirty/reload 测试 | 桌面页面截图 | 通过(Vitest) |
| C03 | 审计池和真实探测 | endpoint CRUD draft、probe 进度/结果、token preserve/replace/clear 测试 | probe 对话框截图 | 通过(Vitest+API |
| C04 | 范围和九类风险 | all/selected、搜索、失效 group、九类对称展示测试 | 选择器截图 | 通过(Vitest |
| C05 | blocking 风险确认 | 开启二次确认;关闭 enabled 联动;取消确认测试 | 确认文案截图 | 通过(Vitest) |
| C06 | 保存可验证且不泄凭据 | 成功快照刷新、409 冲突保留草稿、secret state 清理、无 storage/console 测试 | Public DTO 捕获 | 通过(Vitest+扫描) |
| C07 | 真实运行态和 Guard 指标 | expected/active mismatch、Worker stale、Redis degraded、指标渲染测试 | 概览截图 | 通过(Vitest |
| C08 | 可复核列表和详情 | filter/page/table/detail tabs、用户名/邮箱/API Key 分列复制、IssueSummary、脱敏预览测试 | 详情截图 | 通过(Vitest+API |
| C09 | 防误删除 | 单条/批量/preview/max ID/认证 token/筛选变化失效/时间范围测试 | 删除确认截图 | 通过(集成+Vitest) |
| C10 | 管理操作审计 | config/probe/delete 成功失败审计测试;detail allowlist | audit_logs SQL/API 样本 | 通过(自动化) |
| C11 | 响应式/可访问/i18n | zh/en key 对称;键盘/focus/accessible name;窄屏测试 | 桌面与窄屏截图 | 通过(Vitest+lint |
## 3. 标准验证命令
### 3.1 OpenSpec
```bash
cd /Users/mt/code/mt-ai/sub2api/sub2api-mt
openspec status --change add-openai-compatible-prompt-audit
openspec validate add-openai-compatible-prompt-audit --type change --strict --no-interactive
openspec show add-openai-compatible-prompt-audit
```
### 3.2 后端快速门禁
```bash
cd /Users/mt/code/mt-ai/sub2api/sub2api-mt/backend
go test ./internal/securityaudit/... -count=1
go test ./internal/handler/... ./internal/server/... -count=1
go test ./internal/service -run ContentModeration -count=1
go test -race ./internal/securityaudit/... -count=1
```
如果新模块采用单一 package,第一条可以写成 `go test ./internal/securityaudit -count=1`;以最终目录结构为准,但不能省略 race。
### 3.3 PostgreSQL/Redis 集成
项目已有基于 Testcontainers 的 integration harness。实现时把 Prompt Audit migration/Repository/Redis 场景接入同一模式:
```bash
cd /Users/mt/code/mt-ai/sub2api/sub2api-mt/backend
go test -tags=integration ./internal/repository ./internal/securityaudit/... -run 'PromptAudit|PromptGuard' -count=1
go test -tags=integration -race ./internal/securityaudit/... -run 'MultiWorker|MultiInstance|Lease|ConfigInvalidation' -count=1
```
CI 中 Docker 不可用必须失败;本地跳过要在证据中明确写“未执行”,不能标为通过。
### 3.4 前端
```bash
cd /Users/mt/code/mt-ai/sub2api/sub2api-mt
pnpm --dir frontend run lint:check
pnpm --dir frontend run typecheck
pnpm --dir frontend exec vitest run \
src/features/prompt-audit \
src/views/admin/__tests__/RiskControlView.spec.ts \
src/router/__tests__/feature-access.spec.ts
pnpm --dir frontend run build
```
### 3.5 全量
```bash
cd /Users/mt/code/mt-ai/sub2api/sub2api-mt
make test-backend
make test-frontend
make build
```
保存命令、commit SHA、开始/结束时间、退出码和 CI artifact URL。不要只保存终端截图。
## 4. 模式和协议验收矩阵
### 4.1 运行模式
| risk_control | prompt enabled | blocking | 期望 Prompt 行为 | 主请求 |
| --- | --- | --- | --- | --- |
| false | 任意 | 任意 | off | 完全保持升级前行为 |
| true | false | false | off | 完全保持升级前行为 |
| true | false | true | 配置保存失败 | 无运行态变化 |
| true | true | false | async enqueue | 无论审计依赖成败都按原流程 |
| true | true | true | blocking evaluate | Block/Unavailable/Invalid fail-closed |
### 4.2 HTTP/SSE/WS
每行都要分别验证 benign、flag、block、Guard unavailable、invalid responseLegacy moderation 还需追加“Legacy 单独 Block”和“两者同时 Block”。
| 入口 | 非流式 Allow | SSE/流式 Allow | Prompt Block | Unavailable | Invalid | 必查副作用 |
| --- | --- | --- | --- | --- | --- | --- |
| OpenAI Chat Completions | 原 envelope | Guard 前 0 bytes,之后原流 | 403 `prompt_guard_blocked` | 503 `prompt_guard_unavailable` | 503 `prompt_guard_invalid_response` | account/billing/upstream |
| OpenAI Responses + aliases | 原 envelope | 同上 | 403 OpenAI-compatible | 503 | 503 | account/billing/upstream |
| Claude Messages | 原 envelope | 同上 | 403 Anthropic envelope | 503 Anthropic envelope | 503 Anthropic envelope | account/billing/upstream |
| Gemini generateContent | 原 envelope | 原流式行为 | 403 Google envelope + ErrorInfo reason | 503 + ErrorInfo reason | 503 + ErrorInfo reason | account/billing/upstream |
| Images/Grok media 文本入口 | 原 envelope | 保持原 keepalive 时序 | 403 | 503 | 503 | image slot/billing/upstream/task |
| Responses WS first turn | 正常继续 | N/A | close 4403 blocked | close 1013 unavailable | close 1013 invalid | user/account slot、billing、dial |
| Responses WS subsequent | 本轮继续 | N/A | close 4403stage=subsequent_turn | close 1013 | close 1013 | 本轮 slot、billing、upstream write |
SSE 测试不能只断言最终状态;必须在 Guard fake 阻塞时读取连接并证明还没有 header/首字节/keepalive。
WS 测试必须检查 close code、短 reason、stage 日志和上游帧计数;不能把所有 1013 错误都写成同一内部错误事实。
## 5. 无账号、无计费、无上游证明
### 5.1 测试装置
在每类 Handler E2E 测试注入以下可计数 fake/stub:
```text
account_select_calls
user_slot_acquire_calls
account_slot_acquire_calls
subscription_or_balance_check_calls
billing_preconsume_calls
usage_write_calls
upstream_dial_calls
upstream_http_calls
upstream_ws_write_calls
async_media_task_create_calls
```
对 Prompt Block、Unavailable、Invalid 分别断言所有适用计数为 0。若基础鉴权必须读取 API key/user/group,这不算“账号选择”;证据需区分认证主体读取与上游 account scheduler。
### 5.2 数据库前后快照
除 fake counter 外,测试还应记录请求前后这些业务表/统计不变:
- usage/billing/余额/订阅消费记录。
- API key/account quota 与 rate limit 计数。
- 上游请求/任务记录。
- 图片/媒体占用或预扣记录。
允许新增的只有 Prompt Audit 自己的脱敏 blocking job/event、结构化日志和指标。现有 Content Moderation 同时命中时,它原本会产生的记录/封号/邮件仍按既有行为执行。
### 5.3 日志断言
同步拒绝事件必须包含:
```text
upstream_dispatched=false
billing_preconsumed=false
stage=http|first_turn|subsequent_turn
error_code=<stable code>
```
不得仅依赖日志证明无副作用;日志必须与 counter 和 DB snapshot 同时通过。
## 6. PostgreSQL 验证
### 6.1 Schema
在 migration 集成测试中验证:
- 两表、所有列、默认值、CHECK、索引和 FK 删除行为。
- username/email/API Key name 快照分别落在显式列,API 分列返回;`issue_summaries` 从事件事实派生而非重复落列。
- migration 从空库和当前生产前一版本都能应用。
- 不改 `content_moderation_logs` 定义和数据。
- 关键索引被典型筛选/claim 查询使用;对代表性数据运行 `EXPLAIN`
禁止列检查示例:
```sql
SELECT table_name, column_name
FROM information_schema.columns
WHERE table_name IN ('prompt_audit_jobs', 'prompt_audit_events')
AND lower(column_name) ~ '(raw|prompt_text|request_body|payload|token|authorization|secret)';
```
期望 0 行。`prompt_hash``redacted_preview` 是允许字段,但必须用 canary 行为测试证明内容安全。
### 6.2 原子性与并发
至少覆盖:
- 1000 个 queued/retry jobs,由 8 个 Worker、2 个 service 实例消费,每个 job 最多一个最终 event。
- 两实例同时争抢最后 N 个 queue slotsadmission lock 后 active jobs 不超过 capacity;锁超时只丢弃审计任务。
- Worker 在 claim 后崩溃,租约到期由另一 Worker reclaim。
- 旧 Worker 恢复后,旧 claim_version 的 refresh/event/done/retry/failed 全部 affected rows=0,无法覆盖新领取者状态或创建重复事件。
- staging 在 Redis SET 前不可领取。
- 进程在 Redis SET 与 queued publish 间退出,staging 被回收且 payload TTL 到期。
- event + done 事务中 event insert 失败时不得留下 done 无 event 的风险任务。
- delete-by-filter 与并发新事件/查询同时运行时,只删除 id≤snapshot_max_id;伪造、过期或其他管理员的 confirmation_token 均失败。
## 7. Redis 验证
必须验证:
- key 格式仅包含 job ID,不包含 user email、Prompt hash、模型文本或 token。
- value 是唯一允许的完整 scan text 存放处,默认 TTL 为 1800 秒,测试容差考虑执行时间。
- worker 成功/终态后主动 DELDEL 失败最终依靠 TTL。
- Redis 不可用时 async 主请求继续、job 明确 failed/staging 回收、runtime degraded。
- blocking 不依赖 Redis payload 才能决定当前请求,但配置通知失败时按 last-known-good/TTL refresh 规则运行。
- invalidation channel 只发布 config version,不发布 JSON 配置或 token。
测试代码可以读取 canary value 做等值/TTL 断言,但不得把 value 输出到 `t.Log`、CI artifact 或失败消息。失败时只输出 job ID 和长度/hash。
## 8. 多实例配置测试
启动两个 PromptService 实例,共享 PostgreSQL/Redis、使用独立内存快照:
1. A 保存 config v2A 立即 active=v2。
2. B 收到 invalidation,从 settings 加载并 active=v2。
3. 在 B 人为制造一次解密/加载失败,B 保持 v1 且 runtime expected=v2/active=v1/degraded。
4. 修复依赖后,B 通过下一通知或 5 秒有界刷新到 v2。
5. Redis Pub/Sub 中断时保存 v3A active=v3B 最迟通过 TTL refresh 收敛。
6. 冷启动 C 无法加载、期望 blocking 时,C 不得按 off 放行,runtime 必须 error/degraded。
7. A/B 两个管理员以同一 expected version 并发保存,只有一个成功,另一个得到 409;config_version 不重复且成功配置不被静默覆盖。
证据记录版本和错误码,不记录 endpoint token/base URL query。
## 9. Canary 敏感信息门禁
### 9.1 Canary 设计
每次测试生成唯一、不像普通文本的随机 canary:
```text
PROMPT_CANARY_<random>
GUARD_TOKEN_CANARY_<random>
AUTH_CANARY_<random>
URL_QUERY_CANARY_<random>
```
不要在 shell 命令行或 CI 参数中直接传真实 secret;由测试进程生成并只在测试内存保存。
### 9.2 检查介质
| 介质 | 允许 | 禁止 | 证据 |
| --- | --- | --- | --- |
| PostgreSQL | SHA-256、脱敏预览、长度、分类 | 完整 Prompt、token、Authorization、Guard raw body | 扫描所有 text/json 列 |
| Redis key/metadata | job ID、TTL | Prompt/hash/email/token 出现在 key/channel | SCAN/channel payload 断言 |
| Redis value | 完整 PromptTTL≤1800 | token/Authorization;终态长期残留 | 程序内检查,不打印 value |
| 应用日志 | ID、长度、状态、稳定错误码 | 四类 canary、完整 URL/query、raw response | 捕获 sink 后字节扫描 |
| 管理 API | 脱敏 preview、has_token/status、分列身份、派生风险摘要 | token/ciphertext/canary Prompt 原文 | 序列化响应扫描 |
| 客户错误 | 通用消息、code、request ID | 分类证据、Prompt、endpoint、内部错误 | HTTP/WS body/reason 扫描 |
| 前端状态 | 公共 DTO、空 secret state | 保存后的 token、session/local storage、console | Vitest spies/state snapshot |
| 页面截图 | 脱敏预览、状态 | token、完整 Prompt | OCR/文本与人工复核 |
数据库扫描必须覆盖 `TEXT/VARCHAR/JSON/JSONB`,不能只查两张新表;至少还要查 settings、audit_logs、ops/error logs 和可能的 request log 表。日志捕获应覆盖成功、探测失败、超时、invalid response、DB/Redis 错误和删除操作。
发现任何 Guard token/Authorization 泄露是 blocking release 级别 P0:立即停止启用、删除不安全 artifact、轮换凭据并做影响范围调查。Prompt canary 出现在 PostgreSQL/日志/API/前端同样禁止发布。
## 10. 现有内容审核兼容回归
必须保存迁移前后同一套结果:
- offOpenAI Moderations、关键词、Hash、API Key 健康、异步/同步模式行为相同。
- Legacy Block:状态码、`content_policy_violation`、客户端文案不变。
- 违规计数、邮件、auto-ban、unban、hash delete/clear 不变。
- `/admin/risk-control` config/status/logs/API-key test/unban/hash API 不变。
- `content_moderation_logs` 行内容和清理行为不变。
- `RiskControlView.vue` 加载、保存、测试、列表和功能总开关不变。
- 两引擎同时 Block:客户端仍得到 Legacy BlockPrompt event 独立存在,不触发现有副作用第二次执行。
建议在新增模块前把现有 `ContentModeration``RiskControlView` 测试输出保存为基线,最终对同 commit 运行一次差分对比。
## 11. Async 灰度观测
### 11.0 当前实施基线(非生产准入)
2026-07-16 在本地 async Worker 完整处理路径运行 `TestPromptAuditSyntheticAsyncBaseline`,使用 100 条无敏感信息的确定性测试分组语料:90 benign、5 flag、3 critical、1 invalid、1 timeout。结果为 P50=5ms、P95=5ms、P99=5ms、Guard 失败率=2%、已知 benign 误报率=0%、已知 critical 阻断率=100%、`store_pass_events=false` 时事件增长=8/100。该结果只证明指标链路、分母和事件策略可用,不代表真实 Guard/网络/业务流量性能,也不能替代下述 72h/10k 生产前 async 观测。
复现命令:
```bash
cd /Users/mt/code/mt-ai/sub2api/sub2api-mt/backend
go test ./internal/securityaudit -run TestPromptAuditSyntheticAsyncBaseline -count=1 -v
```
### 11.1 先决条件
- Prompt Audit enabled=true、blocking=false。
- 只选择内部测试 group,不全量。
- 至少两个通过真实 probe 的 Guard endpointtoken 已验证且未出现在任何日志/API。
- runtime active=expectedDB/Redis/Worker healthy。
- store_pass_events 默认 false,避免一开始放大事件量;指标仍统计 Pass。
### 11.2 最少观测窗口
推荐至少连续 72 小时且 ≥10,000 个合格请求;流量不足时延长到 7 天。记录:
- enqueue total/skipped/dropped 及原因。
- queued/processing/retry/failed/staging age 和队列容量占比。
- Worker active、处理吞吐、claim/reclaim、payload missing。
- Guard Allow/Flag/Block/Unavailable/Invalid/timeout/failover/bulkhead。
- 每 endpoint 与总体 P50/P95/P99。
- 分类分布、人工抽检误报率、已知恶意回归漏报率。
- 事件增长率、索引查询 P95、删除批次耗时。
- config version 收敛时间与 reload failure。
完整 Prompt 不得作为人工抽检材料从 Redis 导出。复核使用脱敏预览、类别证据和专门构造的无敏感测试语料;如业务确需原文复核,必须另起隐私/审批 change。
## 12. Blocking 准入和退出阈值
以下是建议初始门槛,最终值必须由安全、运营和业务责任人在上线记录中签字;未签字只能保持 async:
| 指标 | 建议准入阈值 | 建议紧急退出阈值 |
| --- | --- | --- |
| 健康 endpoint | ≥2,连续 72h | <1 个可用立即退出 |
| Guard Unavailable | 24h <0.1% | 5 分钟 ≥1% |
| Invalid response | 24h <0.01% | 5 分钟 ≥0.1% 或连续出现 |
| Guard 延迟 | P95 ≤500msP99 ≤1000ms | P99 >2000ms 持续 10 分钟 |
| bulkhead reject | 24h <0.05% | 5 分钟 ≥0.5% |
| async dropped/payload missing | <0.01%payload missing=0 | 任一持续增长 |
| 人工确认误报率 | <0.5%,高价值流程为 0 | 任一严重合法流量阻断事件 |
| 已知恶意语料 | critical 用例 100% Block | 任一 critical 漏报 |
| config version 收敛 | 99.9% 实例 <10s | 任一实例 stale >60s |
| canary 泄露 | 0 | 任意命中立即停用并轮换 |
延迟阈值还必须低于目标接口现有首字节 SLO 允许的新增预算;若业务 SLO 更严格,以更严格值为准。
首次 blocking
1. 仅一个内部 group,短窗口、有人值守。
2. 确认页面二次提示、指标和告警均工作。
3. 执行 benign/flag/block/unavailable/invalid 合成请求。
4. 证明拒绝时 account/billing/upstream 仍为 0。
5. 观察至少一个高峰窗口后再扩大 group。
### 12.1 值班检查步骤
开启 blocking 前、每次扩组前和收到告警后,值班人员按以下固定顺序执行;任一项不满足立即保持/恢复 async:
1. 打开 `/admin/prompt-audit`,确认 effective mode、expected/active config version、Worker heartbeat、PostgreSQL、Redis 和至少两个 endpoint 均健康。
2. 检查最近 5 分钟 Guard Unavailable、Invalid、timeout、bulkhead、P95/P99 和 async dropped 是否超过本节阈值。
3. 检查 queued/retry/staging 最老年龄和容量占比;队列持续增长或 staging 未回收时禁止扩组。
4. 对 benign、flag、block、unavailable、invalid 合成用例各执行一次,核对协议 envelope、错误码及 `upstream_dispatched=false``billing_preconsumed=false`
5. 抽查最新风险事件的脱敏预览、身份分列、分类和 IssueSummary,禁止从 Redis 导出原文。
6. 记录值班人、时间、config version、测试 group、指标快照和结论;扩组必须由安全、运营、业务责任人共同确认。
一键回滚动作固定为:在独立页面关闭 `blocking_enabled` 并保存,等待所有实例 `active_config_version == expected_config_version` 且 effective mode=`async_audit`。若管理页面不可用,使用同一管理员 API 的 GET config 取得版本,只修改 `blocking_enabled=false` 并携带 `expected_config_version` PUT 回去;不得直接修改 settings JSON。若 async 仍造成压力,再关闭 `enabled`。全局 `risk_control_enabled` 只作最后手段,因为它也会停用既有内容审核。
## 13. 回滚清单
### 13.1 一键功能回滚
1.`/admin/prompt-audit` 关闭 `blocking_enabled` 并保存。
2. 确认所有实例 `active_version == expected_version`,有效模式变为 async_audit。
3. 用 benign 请求证明立即恢复原主流程;用 Guard unavailable 合成请求证明不再返回 503。
4. 继续观察 async,保留事件用于复盘。
若 async 本身引发 DB/Redis 压力或隐私问题:
1. 关闭 `enabled`,有效模式变为 off。
2. 停止 Worker 领取新任务;有界等待活动任务。
3. queued/retry 保留等待明确处置,不自动删除历史证据。
4. 若发生 secret 泄露,轮换 endpoint token。
只有在 Prompt 配置通道无法使用且影响仍持续时,才关闭全局 `risk_control_enabled`;这会同时停用现有内容审核,是最后手段。
### 13.2 数据和部署回滚
- 不回退已应用的 migration,不 drop 两张表,不删除历史事件。
- 可部署上一版本应用;新表和 setting key 保持向后兼容、无人读取。
- 停 Worker 不改变现有网关能力;恢复后按状态继续或由管理员明确清理。
- 回滚后记录触发时间、阈值、config version、影响 group、错误码分布和恢复时间。
### 13.3 回滚验收
- 所有实例模式正确,stale config=0。
- 新 403/503/4403/1013 已停止(除现有审核自己的响应)。
- 上游成功率、首字节延迟恢复基线。
- 队列不继续增长,Redis payload 最迟按 TTL 清除。
- 现有 `/admin/risk-control` 和 Content Moderation 仍正常。
## 14. 最终发布签字模板
| 项目 | 结果/链接 | 责任人 | 时间 |
| --- | --- | --- | --- |
| 源基线冻结 | TODO | TODO | TODO |
| OpenSpec strict validate | TODO | TODO | TODO |
| 后端 unit/race/integration | TODO | TODO | TODO |
| 前端 lint/typecheck/Vitest/build | TODO | TODO | TODO |
| 协议与无副作用矩阵 | TODO | TODO | TODO |
| canary 泄露门禁 | TODO | TODO | TODO |
| async 72h/10k 报告 | TODO | TODO | TODO |
| blocking 阈值批准 | TODO | TODO | TODO |
| 告警和值班人 | TODO | TODO | TODO |
| 回滚演练 | TODO | TODO | TODO |
任何必填项为 TODO、失败或无证据时,不得开启生产 blocking。
## 15. 2026-07-16 实施验证记录
验证基线:branch=`dev`HEAD=`a2779cd5f30d6d3904a9d59088aed09507678dfe`,工作区包含本 change 的未提交实现;时间为 2026-07-16 CST。以下命令退出码均为 0,除首次发现并修复的 lint 问题外不隐藏失败。
| 门禁 | 实际证据 |
| --- | --- |
| OpenSpec | `openspec validate add-openai-compatible-prompt-audit --type change --strict --no-interactive` → valid |
| SecurityAudit 单元/集成 | PostgreSQL `127.0.0.1:32768`、Redis `127.0.0.1:32769``go test ./internal/securityaudit/... -count=1` → pass |
| Race | 同一真实依赖下 `go test -race ./internal/securityaudit/... -count=1` → pass |
| Migration/Repository/Config | `TestPromptAuditConfigCASSecretRoundTripInvalidationAndTTL`、migration/schema、admission/fencing/FK/high-water/concurrent delete、Redis TTL、Worker lifecycle 全部 pass |
| Handler/Routes | `go test ./internal/handler/... ./internal/server/... -count=1` → pass;路由矩阵由 `TestEveryGatewayPOSTRouteIsClassifiedForPromptAuditCoverage` 固定 |
| 全量后端 | 临时安装 CI 同版 golangci-lint v2.9 后 `make test-backend` → 全量 Go tests pass`0 issues` |
| 前端 | ESLint passvue-tsc passPrompt Audit、RiskControl、Sidebar、router 共 8 个文件 34 tests pass |
| 生产构建 | `make build` → Go binary 与 Vite production build pass,独立 `PromptAuditView` chunk 生成 |
| 协议/副作用矩阵 | 13 个实际入口的 Guard-before-side-effect 结构测试;Block/Unavailable/Invalid counter=0OpenAI/Responses/Claude/Gemini goldenWS 4403/1013first/subsequent gate;媒体 task/billing gate 全部 pass |
| 泄露门禁 | 统一 canary 覆盖日志、DB row、管理 JSON、前端保存后 DOM;测试 PostgreSQL 39 个 text/json 列全库扫描 0 命中;Redis key/channel scan 0 命中;feature 源码无 local/session storage 或 console |
| Async 指标基线 | 100 条合成 async Worker 样本:P50/P95/P99=5/5/5msfailure=2%known-benign false-positive=0%event growth=8/100;只用于验证观测链路 |
| Deploy 容器 | Docker Hub 超时后使用已缓存的正式运行层 + 当前 `linux/arm64` embed release binary 构建离线增量镜像 `sha256:c86353b0...`Compose 重建后 app/PostgreSQL/Redis healthymigration 181 已登记,两张表存在,`/health`=200 |
| Deploy 管理 API | 本地测试管理员登录成功;`GET config/runtime/events` 均为 200;默认 config=`enabled=false, blocking=false, mode=off, version=1, group_ids=[], endpoints=[]`runtime active/expected=1/1 |
| Deploy 页面 | 首次容器检查发现并修复默认 `group_ids:null` 导致的运行时错误;重建后桌面/390px 窄屏 DOM 与截图均通过,截图不含 token/Prompt canary |
| Deploy 全介质扫描 | 完整生产测试库所有 public text/varchar/json/jsonb 列动态扫描:hit_columns=0/hit_rows=0;两表禁用列=0Redis canary key=`0`、channel=`[]`、payload key=`0`;容器日志 canary=0 |
### 15.1 Requirement 自动化证据索引
- A01/G01/G02/G12`coordinator_test.go``prompt_config_test.go``prompt_config_integration_test.go`、原 ContentModeration/RiskControl 回归。
- A02/A03/G06`prompt_outbound_security_test.go``prompt_qwen3guard_test.go`、配置 secret 往返与 probe handler 测试。
- A04/A05/A09`prompt_snapshot_test.go``TestPromptAuditDatabaseAndAdminJSONNeverPersistCanaryPromptOrRawErrors`、schema leakage gate。
- A06/A07`prompt_worker_test.go`、Redis payload 集成、Repository admission/claim_version/reclaim 集成和 race。
- A08Qwen3Guard strict/alias/unknown/aggregate/IssueSummary tests。
- A10/A12/G09event transaction、FK/filter/high-water/confirmation/concurrent delete、record-once tests。
- A11/G10/G11runtime aggregation、config invalidation/TTL、metrics/log dictionary/canary tests。
- G03/G04/G07/G08`security_audit_order_test.go``security_audit_media_submit_test.go``security_audit_errors_test.go``prompt_audit_route_coverage_test.go`
- G05Guard complete chunks、last-chunk failure、Block early-stop、shared deadline/context tests。
- C01C11`frontend/src/features/prompt-audit/__tests__/`、Sidebar/router/RiskControl tests、handler/admin route tests、lint/typecheck/build。
### 15.2 尚未构成生产 blocking 批准的事项
本地实现验证通过不等于生产启用批准。最终签字表中的真实 async 72h/10k 流量报告、至少两个真实 endpoint 连续健康、业务流量误报抽检、告警接线、值班人和回滚演练仍为 TODO;在这些外部运营证据完成前,生产只能保持 off 或受控 async,不能开启 blocking。
### 15.3 页面证据
- 桌面:`/Users/mt/.codex/visualizations/2026/07/16/019f6a2c-ce90-7ae2-8eca-6a3a66b837f2/prompt-audit-desktop.png`
- 390px 窄屏:`/Users/mt/.codex/visualizations/2026/07/16/019f6a2c-ce90-7ae2-8eca-6a3a66b837f2/prompt-audit-narrow.png`
截图只包含默认关闭状态、空事件/空节点和测试管理员展示名;未配置节点 token、未产生 Prompt 事件,并已目视确认没有 canary、Authorization、完整 Prompt 或内部错误。截图完成后测试库已恢复 `risk_control_enabled=false`Prompt Audit 保持默认 off。