sub2api 当前已经存在一套完整的内容审核能力:
backend/internal/service/content_moderation*.go。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。该能力不是本次要迁移的 aicodex-api “提示词审计”:两者使用不同模型、分类、队列、事件和阻断语义。把 Qwen3Guard 直接塞入 ContentModerationService 会让现有阈值、封号统计和记录含义失真,也会继续扩大已经接近 3000 行的单文件。
参考仓库 /Users/mt/code/mt-ai/aicodex/aicodex-api 当前磁盘实现提供:
Safety/Categories 解析。参考仓库 yjb 分支当前包含未提交的同步阻止改动。因此实施开始前必须固定源 commit/tag 或生成包含未提交文件的只读 patch 清单,作为功能对照和测试移植的权威基线。
Goals:
Non-Goals:
content_moderation_logs。源模块依赖 aicodex-api 的 Ent 全局客户端、option 模型、Gin context key、日志封装、Caddy/gatewaycore 和 React 控制台,不能原样复制到目标项目。
实施时以本 change 的 specs 和验收矩阵作为权威行为契约,再选择目标项目已有的 SettingRepository、Redis、SecretEncryptor、Gin Handler、SQL migration 和 Vue 组件实现。
备选方案:直接复制 internal/service/promptaudit。 放弃,因为会引入大量适配壳、全局状态和源仓库私有依赖,并且源工作区当前未提交。
新增目录:
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) DecisionPromptService 生命周期与管理方法PromptAdminHandlerSQL migration、前端和少量路由/注入接线由于项目结构约束仍位于各自事实源目录。
备选方案:继续平铺在 internal/service、internal/repository 和 internal/handler。 放弃,因为无法满足独立模块要求,也会增加 AI 和人工定位所需上下文。
目标调用关系:
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 只承担:
优先级:
prompt_guard_blocked。prompt_guard_invalid_response。prompt_guard_unavailable。两个引擎的事件和副作用独立。Coordinator 不持久化业务事件,不修改风险分数。
同步执行策略: 当 Prompt Guard blocking 开启时,现有内容审核和 Prompt Guard 可在独立受控 goroutine 中并行执行,共享请求取消信号但不共享 mutable state。必须等待两者完成或各自 deadline 到期,以保留两个引擎的审计完整性。若实现评审认为并行引入的复杂度过高,可先串行执行,但仍必须满足既有 Block 响应优先级和无下游副作用测试。
把各协议 Handler 的 checkContentModeration 调用机械替换为 checkSecurityAudit,保持调用点仍在:
现有 content_moderation_helper.go 改为或新增 security_audit_helper.go,构造统一 securityaudit.Request:
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 读取,避免破坏转发。
有效开关:
risk_control_enabled:现有安全审计总入口和菜单开关。content_moderation_config.enabled/mode:现有内容审核。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。前端联动只提升体验,不能替代后端校验。
新增 setting key:prompt_audit_config。
配置结构包含:
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 持久化:
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;任务和事件才需要独立关系表。
PromptService 维护原子只读配置快照:
sub2api:prompt_guard:config:invalidate,消息只包含版本。运行态返回 expected 和 active version。配置加载失败不得清空最后有效快照;冷启动无有效快照时不得伪装为关闭或健康。
复用现有内容审核提供的 protocol 常量、身份/分组上下文和部分 JSON 内容块解析思路,但新模块实现独立 PromptSnapshotExtractor:
扫描顺序:
数据库预览使用统一脱敏器:移除/掩码 API Key、Bearer、常见凭据、邮箱/电话等敏感模式,随后按 rune 裁剪。Hash 使用实际待扫描文本的 SHA-256。
建议 migration 名称:backend/migrations/181_prompt_audit.sql。如果实施时已有 181,则按当前最大序号递增,不允许修改已应用 migration。
prompt_audit_jobsCREATE 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。
关键索引:
(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_eventsCREATE 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 等列。
异步投递顺序:
status=staging 的 job。SET sub2api:prompt_audit:payload:<job_id> <scan_text> EX 1800。prompt_audit.job_enqueued。Worker 只领取 queued/retry,因此不会在 Redis SET 前看到任务。
失败处理:
这比源实现“先 queued 再写 Redis”更适合多实例,避免 Worker 提前领取。
队列容量检查和 staging INSERT 必须在同一准入锁事务中完成,防止多个实例先各自看到剩余容量再共同超限。锁等待必须有很短的有界 timeout;无法及时取得锁时按 queue_admission_busy 丢弃异步审计任务并让主请求继续。Redis 写入不在该事务内。
Repository 使用短事务:
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:
退避建议:5s、30s、2m,上限 5m并加少量 jitter。401/403 和 invalid_response 不重试;429、5xx、连接错误和超时可重试。
Runner 生命周期由应用启动/停止管理:
请求:
{
"model": "sileader/qwen3guard:0.6b",
"messages": [{"role": "user", "content": "<chunk>"}],
"temperature": 0,
"max_tokens": 64,
"seed": 42
}
解析要求:
Safety: 行和一个 Categories: 行。策略映射:
| 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,不要求新增数据库列,防止复制同一风险事实。
同步 evaluator:
第一版不使用熔断器外部依赖;连续失败健康状态和冻结窗口可用模块内小状态机实现。若后续数据证明需要通用熔断库,另起 change。
保存、探测和实际调用共用同一校验:
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 需要通过最小协议适配器扩展稳定代码,不能破坏原字段:
error.type/message 或 Responses 现有结构,并设置 error.code=<prompt_guard_*>。type=error 和合法的 error.type=permission_error|api_error,增加可选 error.code=<prompt_guard_*>。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:
同步 evaluator 返回:
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:
execution_mode=blocking,status=done 的 job。新增:
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”的方式。
删除规则:
snapshot_max_id 和 filter_hash = SHA-256(canonical JSON filter summary + snapshot_max_id)。id <= snapshot_max_id;客户端自行计算 SHA-256 不能绕过预览,预览后的新事件不能被本次操作删除。frontend/src/features/prompt-audit/
├── PromptAuditView.vue
├── api.ts
├── types.ts
├── viewModel.ts
├── components/
└── __tests__/
少量外部接线:
/admin/prompt-audit,复用 requiresAuth/requiresAdmin/requiresRiskControl。页面分区:
页面不得在 localStorage/sessionStorage 保存 API Key。保存成功后立即清除输入 state。
最小事件:
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
字段采用 allowlist:request_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 计数以区分投递与扫描阶段。
核心矩阵:
| 维度 | 值 |
|---|---|
| 引擎 | 现有 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 断言。
使用现有 go-redis、database/sql、Gin、SecretEncryptor、logger、Vue 3、Axios 和测试工具。Qwen3Guard 是外部 OpenAI 兼容服务,不在本仓库启动模型进程。
不引入新的 Go 队列库、ORM、前端状态库或 UI 框架。
FOR UPDATE SKIP LOCKED 原子领取、递增 claim_version fencing token、processing 租约和带版本条件更新。git diff --stat。source-freeze/ 中的只读 tracked patch + untracked archive;base commit、SHA-256 和恢复测试已登记在 source-baseline.md。backend/internal/server/routes/gateway.go 的自动/结构枚举为事实源;所有用户文本入口必须接入 Coordinator 或提供不会旁路/重复扫描的测试证明。verification.md 的建议阈值并由安全、运营和业务责任人签字,未签字不得生产开启。