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
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:
@@ -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 保留
|
||||
+160
@@ -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 说明请求会等待 Guard,Block 或 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 可识别且可操作
|
||||
Reference in New Issue
Block a user