docs: refine moderation and signup policy
This commit is contained in:
@@ -25,7 +25,7 @@
|
||||
|
||||
| 功能 | 用户能做什么 | 优先级 |
|
||||
| --- | --- | --- |
|
||||
| 用户注册/登录 | 终端用户在用户端自助注册、登录(邮箱验证);**注册不送免费点数** | P0 |
|
||||
| 用户注册/登录 | 终端用户在用户端自助注册、登录(**免邮箱验证、注册即可用;邮箱仍必填且唯一**);**注册不送免费点数** | P0 |
|
||||
| API Key 自助管理 | 登录后自助生成/删除 API Key;Key 只在生成时显示一次(库内哈希存储) | P0 |
|
||||
| 扫码充值 | 用户端发起充值→展示支付二维码→扫码付款→回调到账;金额按汇率转点数 | P0 |
|
||||
| 个人中心 | 查看剩余点数、充值总额、充值记录、点数使用(消费)记录 | P0 |
|
||||
@@ -67,7 +67,7 @@
|
||||
- **余额查询 API**:返回的余额等于该账号点数流水累加结果。
|
||||
- **充值转点数**:模拟一次支付系统回调(含订单号、金额、签名),验签通过后按汇率加点;**同一订单号重复回调只入账一次**(幂等);验签失败不入账。
|
||||
- **调用记录 / 点数流水**:每次成功/失败调用、每次充值/退款/调整都各生成一条记录,字段完整、可在后台检索。
|
||||
- **用户注册/登录**:新用户能自助注册(邮箱验证)、登录;注册后点数为 0(不送免费点数)。
|
||||
- **用户注册/登录**:新用户能自助注册(**免邮箱验证、注册即可用;邮箱仍必填且唯一**)、登录;注册后点数为 0(不送免费点数)。
|
||||
- **API Key 自助管理**:登录用户能生成 API Key(明文只显示一次,库内存哈希)、能删除;删除落库为吊销,吊销后该 Key 调用返回 403,缺失/无效/不存在 Key 返回 401。
|
||||
- **扫码充值**:用户端发起充值→展示二维码→(mock 或真实)支付成功回调后点数按汇率到账,充值记录与流水各一条;同一订单号重复回调只入账一次。
|
||||
- **个人中心**:剩余点数 = 点数流水累加;充值总额、充值记录、消费记录与实际一致。
|
||||
|
||||
+18
-2
@@ -23,6 +23,7 @@
|
||||
- **API 层(DRF)**:对外生成接口、余额查询、支付回调接收、扫码下单。入口 `apps/api/`。生成/余额这类对外业务 API **只认 API Key,不接受 Web session**;充值下单/状态查询属于用户端流程,走 Web session + CSRF;支付回调走平台验签。
|
||||
- **计费层**:点数计算、原子扣减(锁 `UserWallet` 行)、退点、充值入账、流水记账。入口 `apps/billing/`。
|
||||
- **AI 调用层(Provider Adapter 架构)**:对外只暴露稳定能力,内部用「能力别名 → 具体供应商适配器」解耦。入口 `apps/ai/`,适配器在 `apps/ai/providers/`。
|
||||
- **内容安全层(本地敏感词 / 后续云审核)**:入口 `apps/moderation/`。T-604 只做 prompt 文本本地敏感词快筛,命中在扣点和调上游前返回 `content_blocked`;云内容安全、图片审核和输出审核保留扩展点,不在 T-604 范围。
|
||||
- **运营后台**:django-admin,注册各模型的 Admin。入口各 app 的 `admin.py`。T-401 已补齐用户、钱包、API Key(脱敏)、计费规则、汇率、充值订单、点数流水和调用记录的管理/检索;点数手工调整只能从钱包专用入口提交原因和非 0 变动,经计费层写 `adjust` 流水。
|
||||
- **外部服务**:上游 AI(vectorengine)、外部支付系统(跳转 + 回调)。
|
||||
- **数据库**:MySQL 8.4 LTS(cmhub **专用独立实例**,不复用 VPS 已有的 MySQL 5.7);引擎必须 InnoDB、字符集 utf8mb4。开发与生产同用 MySQL,勿用 SQLite(SQLite 会静默忽略 `select_for_update`,测不出并发扣点)。
|
||||
@@ -68,6 +69,14 @@ T-306 已实现对外 API 安全加固:`download_image_input()` 在请求前
|
||||
- 输入:prompt、可选图片、能力别名、分辨率、`parameters` 安全子集;输出:文字或图片,或明确错误。`parameters` / `extra_body` 不得覆盖服务端固定的核心/计费字段(如 `model`、`n`、`size`、`resolution`、`messages`、`image*`)。
|
||||
- 处理超时、错误,向上层抛出可分类的异常(区分「上游失败」与「参数错误」)。不在本层写点数逻辑。
|
||||
|
||||
**内容安全层(`apps/moderation`)**
|
||||
|
||||
- T-604 的范围只包含输入 prompt 敏感词过滤;具体实施规则见 [`moderation.md`](moderation.md)。
|
||||
- 生成接口时序必须是 serializer 后先审 prompt,再读取 `image_base64` / 下载 `image_url`,最后才进入别名解析、计费和预扣。命中敏感词时不得下载图片、不得扣点、不得写调用/流水、不得调上游。
|
||||
- 本地词表只做免费快筛和运营自定义黑名单,不替代合规内容审核;云厂商内容安全、图片审核和输出审核后续用 provider 接口扩展。
|
||||
- 敏感词 matcher 必须缓存,禁止每请求重建;生产多 worker 下用共享 cache 的词库版本号触发各 worker 懒重建,不能只依赖当前进程的 Django signal。
|
||||
- 留痕只记录分类、命中词 ID、用户/API Key 关联和处理结果,不保存违规 prompt 原文。
|
||||
|
||||
**数据库 / 存储**
|
||||
|
||||
- 持久化:账号、API Key、点数余额、计费规则、汇率、充值订单、点数流水、调用记录、模型配置。
|
||||
@@ -87,6 +96,7 @@ T-306 已实现对外 API 安全加固:`download_image_input()` 在请求前
|
||||
| AiConfigAuditLog | 后台自动写入 | AiModel / ModelAlias / api_key 变更审计;记录 actor、action、target、changed_fields、changes、created_at;只读 |
|
||||
| PricingRule | 运营配置 | 操作类型 × **能力别名字符串**(+ 可选分辨率)→ 点数单价;不外键到具体 AiModel |
|
||||
| ExchangeRate | 运营配置 | 金额 → 点数 的汇率(如 1 元 = N 点),按 `currency + effective_from` 取当前有效值 |
|
||||
| SensitiveWord | 运营配置 | 本地敏感词快筛词库;word、normalized_word、category、action、is_active;T-604 MVP 仅支持 action=block |
|
||||
|
||||
关键事实:
|
||||
|
||||
@@ -102,6 +112,7 @@ T-306 已实现对外 API 安全加固:`download_image_input()` 在请求前
|
||||
- `AiConfigAuditLog` 只追加、不在后台编辑删除;密钥变更只记录 `api_key: empty/set` 状态变化,不记录明文 key 或 Fernet 密文。
|
||||
- 汇率在**充值下单创建订单时**锁定并记录到订单,同时计算预计到账点数;后续改汇率不影响已创建订单。支付回调入账时使用订单上的 `exchange_rate` / `points_granted`,不重新读取当前汇率。
|
||||
- Provider 适配器只允许白名单安全参数透传;外部调用方或后台 `extra_body` 传入核心字段时忽略,由服务端按别名解析和计费口径固定。
|
||||
- `SensitiveWord` 的 matcher 由 active 词库构建;词库变更后更新共享 cache 版本号,各 Gunicorn worker 在下一次请求检查版本并懒重建。
|
||||
|
||||
#### 配置表目标 schema
|
||||
|
||||
@@ -346,7 +357,7 @@ CREATE TABLE call_record (
|
||||
| 配置热生效 | 后台改模型/密钥后运行时仍用旧值 | 不在进程内长缓存;每次查库或保存时失效缓存 |
|
||||
| 配置变更审计 | 改密钥/模型/别名映射无痕 | `AiConfigAuditLog` 自动记录后台 create/update/delete;密钥只记录 empty/set 变化,日志只读 |
|
||||
| API Key 泄露 | 明文存库一旦泄露全泄 | Key **哈希存储**(sha256),明文只在创建时显示一次,库内存 `key_hash`+`key_prefix`,鉴权做哈希比对 |
|
||||
| 注册滥用 | 自助注册被批量刷 | 邮箱验证 + 生成接口限流(DRF throttle);**注册不送点数**,无免费额度可薅 |
|
||||
| 注册滥用 | 自助注册被批量刷 | **免邮箱验证**(`ACCOUNT_EMAIL_VERIFICATION="none"`),防刷改由注册限流 / 图形验证码补位 + 生成接口限流(DRF throttle);**注册不送点数**,无免费额度可薅 |
|
||||
| Web/API 抢 worker | 图片同步长请求占满 worker、拖慢用户端页面 | 单体下按路径把 `/api/v1/generate/*` 与用户端页面**分流到不同 gunicorn/worker 池**(见 5.1) |
|
||||
| 充错账户 | 扫码订单未绑定发起用户 | 订单创建即绑定 `user`;回调按 `order_no` 定位订单→其 user 入账 |
|
||||
| SSRF / 任意 URL 下载 | `image_url` 让服务端请求调用方指定地址 | 仅允许公网 http/https;请求前解析 IP 并拒绝内网/回环/链路本地/保留地址;重定向逐跳校验;流式读取并限制大小 |
|
||||
@@ -407,4 +418,9 @@ cmhub/
|
||||
- 点数余额存 `UserWallet`(与 auth `User` 表分离);扣点锁 wallet 行,不与登录/资料更新抢锁。
|
||||
- **API Key 哈希存储**(sha256),明文只在创建时返回一次;库内存 `key_hash`+`key_prefix`,任何页面不回显明文。
|
||||
- **对外 API 只接受 API Key 认证,不挂 SessionAuthentication**,防止浏览器带 cookie 绕过 Key 与计费归属。
|
||||
- 用户端页面用 session + CSRF;自助注册须邮箱验证,生成接口须限流。
|
||||
- 用户端页面用 session + CSRF;自助注册**免邮箱验证**(邮箱仍必填且唯一),生成接口须限流。
|
||||
- **用户端与后台共用同一个 Django 会话(设计事实 + 账号分离约定)**:`/`(portal)、`/admin/`、`/api/`(session 部分)挂在同一域名/同一 Django 工程,用**同一个 `sessionid` cookie**;`/admin/` 不是「另一套登录」,只是在同一登录态上加一道 `is_staff=True` 门槛。因此:
|
||||
- 普通用户 `is_staff=False`,登 portal **进不了** `/admin/`(无越权、无泄露);观察到的「用户端登录、后台跟着登」是因为用了 **staff/superuser 账号**同时登了 portal。
|
||||
- **解耦方案(采用方案 A·账号分离,零代码)**:终端用户一律 `is_staff=False`;运营用**专用 admin superuser** 只登 `/admin/`,不与 portal 账号混用。这样两个入口天然分开。
|
||||
- 不采用「同账号双会话」(需 admin 独立子域名/独立 cookie 或自定义中间件,成本高、MVP 不值)。
|
||||
- 相关加固(上线前,见 `deployment.md`):`/admin/` 加访问保护(改路径 / IP 白名单 / 反代 basic auth / 2FA);**永不给终端用户 `is_staff`**。
|
||||
|
||||
@@ -38,10 +38,12 @@
|
||||
- 模块职责以 `04-architecture.md` 为准,不跨层偷写逻辑。
|
||||
- **点数余额只能经 `apps/billing` 修改**:余额存 `UserWallet`(与 auth `User` 分离),扣点锁 wallet 行;API 层/Admin/用户端都不得直接写 `points_balance`。
|
||||
- **API Key 哈希存储**(sha256),明文只在生成时返回一次;不存明文、任何页面不回显。
|
||||
- **对外 API 只挂 API Key 认证,不挂 SessionAuthentication**;用户端页面用 session + CSRF,自助注册须邮箱验证、生成接口须限流。
|
||||
- **对外 API 只挂 API Key 认证,不挂 SessionAuthentication**;用户端页面用 session + CSRF;自助注册**免邮箱验证**(`ACCOUNT_EMAIL_VERIFICATION="none"`,邮箱仍必填且唯一)、生成接口须限流。
|
||||
- **用户端与后台账号分离**:portal 与 `/admin/` 共用同一个 Django 会话(同一 `sessionid`),后台访问只多一道 `is_staff` 门槛。因此 **终端用户一律 `is_staff=False`**(普通用户登 portal 进不了后台,无泄露);**运营用专用 admin superuser 登录 `/admin/`,不要拿这个账号登 portal**——账号分离(方案 A)就是「解耦入口」的正解,不要为同账号双会话去拆 cookie/子域名。详见 `04-architecture.md`。
|
||||
- **对外接口只接受能力别名,不接受具体模型名**;新增供应商必须走 `apps/ai/providers/` 的适配器,不在 API 层写供应商分支逻辑。
|
||||
- 计费按别名定价,不按具体模型 SKU;不要把模型名写进对外契约或调用方文档。
|
||||
- 供应商 `api_key` 必须加密存储、使用时解密,不落明文、不在 admin 回显。
|
||||
- **内容安全顺序**:若启用 prompt 敏感词过滤,必须在 `image_url` 下载 / `image_base64` 大图处理 / 别名解析 / 计费预扣之前审核 prompt;命中时返回 `content_blocked`,不扣点、不写 `CallRecord` / `PointsLedger`、不调上游。详细规则见 `docs/moderation.md`。
|
||||
- 接口形状以 `api.md` 为准,运营后台路由以 `routes.md` 为准。
|
||||
|
||||
## 5. 代码规范
|
||||
|
||||
+4
-2
@@ -61,7 +61,7 @@
|
||||
|
||||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| T-501 | 注册 / 登录(allauth) | T-201 | 自助注册(邮箱验证)、登录、登出;注册后钱包点数为 0(不送点数);session + CSRF | DONE |
|
||||
| T-501 | 注册 / 登录(allauth) | T-201 | 自助注册(**免邮箱验证、注册即可用;邮箱仍必填且唯一**)、登录、登出;注册后钱包点数为 0(不送点数);session + CSRF。**策略变更见 T-605 与 2026-07-06 决策** | DONE |
|
||||
| T-502 | API Key 自助管理页 | T-501, T-301 | 登录用户生成/删除 Key;明文只显示一次、库内只存哈希;列表只显示 prefix;删除即吊销,吊销后该 Key 调用 403(无效/不存在 Key 仍为 401) | DONE |
|
||||
| T-503 | 个人中心 / 记录页 | T-501, T-203 | 剩余点数、充值总额、充值记录、消费(调用)记录;数据与流水一致;仅见本人 | DONE |
|
||||
| T-504 | 充值页(扫码 + 轮询到账) | T-501, T-305 | 发起充值→展示二维码→轮询订单状态→到账后余额刷新;到账以回调为权威 | DONE |
|
||||
@@ -81,7 +81,9 @@
|
||||
| --- | --- | --- | --- | --- |
|
||||
| T-601 | 可用别名发现(portal 别名页 + `GET /api/v1/models`) | T-202, T-301, T-503 | 让调用方能自助发现「能填哪些 `model` 能力别名」。① 新增 `GET /api/v1/models`(**API Key 鉴权,继承 `ExternalApiView`,只复用认证失败限流;不要占用生成接口的 `GenerateRateThrottle` 额度**),仅返回 `ModelAlias.is_active=True` 且 `ai_model.is_active=True` 的可调用别名;② 响应每项只含对外安全字段:`alias`、`operation_type`、`capabilities`、`requires_image`(按 `api_type`/provider 规则推导,如 `images_edits=true`)、`pricing_status`、`prices[{resolution, points_cost}]`,缺 `PricingRule` 时标「暂未定价」而非报错;③ **实现时不得调用 `AiModel.to_resolved_model()` 或任何会解密 provider key 的路径**,列表接口不依赖 `AI_KEY_ENCRYPTION_KEY`,也不读取/输出 `api_key_encrypted`;④ **不暴露具体 SKU/模型名/url/key/provider 原始配置**,测试必须断言响应不含 `ai_model.model`、`ai_model.url`、`api_key`、`api_key_encrypted`、`extra_body` 等内部字段;⑤ portal 加只读「可用模型」页(session 鉴权),同字段、人类可读、单价以点数展示,并在导航中加入入口;⑥ 含测试:API Key 成功返回结构、缺/无效 Key 拒绝、Web session 不能调用外部 API、portal 未登录跳转、页面不泄露 SKU/URL/Key、缺定价别名显示暂未定价。**标注**:未来接 `account_alias_permission`(见 Backlog)后,本清单改为「只列该用户被授权的别名」,接口与页面均按 `user` 过滤 | DONE |
|
||||
| T-602 | django-admin 中文化(第 1-3 层) | - | 让运营后台表名/分组/框架文字显示中文,**仅显示层、不改业务逻辑与 DB 结构**。**第 1 层**:`config/settings.py` 设 `LANGUAGE_CODE='zh-hans'` + `USE_I18N=True`,使 Django 自带 admin 界面与内置 `auth`(用户/组/权限)中文化(无迁移)。**第 2 层**:各 app `AppConfig.verbose_name` 设中文分组名(`users`/`billing`/`ai`/`api`/`portal`),无迁移。**第 3 层**:各模型 `Meta.verbose_name`/`verbose_name_plural` 设中文表名(`User`/`UserWallet`/`ApiKey`/`PointsLedger`/`CallRecord`/`PricingRule`/`ExchangeRate`/`RechargeOrder`/`AiModel`/`ModelAlias`/`AiConfigAuditLog`)。**不含第 4 层字段级 `verbose_name`(字段列名保持英文,见 T-603)**。第 3 层会产出 `AlterModelOptions` 等**无 DB 变更**迁移;验收:`makemigrations`(仅 options 变更)→ `migrate` → `check` 0 issues → `test` 全绿 → `/admin/` 目视中文化,并在 `../progress.md` 留证据 | DONE |
|
||||
| T-603 | django-admin 中文化(第 4 层·字段级) | T-602 | 给各模型**字段**加中文 `verbose_name`,使 admin 列表/编辑页的字段标签显示中文,在 T-602(1-3 层)之上做。范围覆盖 `users`/`billing`/`ai`(及 portal 相关)模型的业务字段,例如 `points_balance`→点数余额、`key_prefix`→Key 前缀、`points_delta`→点数变动、`balance_after`→变动后余额、`amount_money`→金额、`exchange_rate`→汇率、`points_granted`→到账点数、`api_type`/`capabilities`/`is_active`/`created_at` 等;`admin.py` 里自定义显示方法(`@admin.display(description=...)`)同步中文。**仅显示层**:`verbose_name` 只改展示,**不改字段名、不改代码引用、英文字段名保持不变**;产出的是 `AlterField`(metadata-only,**无 DB schema 变更**)迁移。验收:`makemigrations`(仅 AlterField,无 schema 变更)→ `migrate` → `check` 0 issues → `test` 全绿 → `/admin/` 字段标签目视中文,并在 `../progress.md` 留证据。**当前阻塞**:代码与迁移已完成,完整测试被工作区既有 `config/settings.py` 中 `ACCOUNT_EMAIL_VERIFICATION="none"` 改动阻断,需恢复 `mandatory` 后重跑测试再标 `DONE` | BLOCKED |
|
||||
| T-603 | django-admin 中文化(第 4 层·字段级) | T-602 | 给各模型**字段**加中文 `verbose_name`,使 admin 列表/编辑页的字段标签显示中文,在 T-602(1-3 层)之上做。范围覆盖 `users`/`billing`/`ai`(及 portal 相关)模型的业务字段,例如 `points_balance`→点数余额、`key_prefix`→Key 前缀、`points_delta`→点数变动、`balance_after`→变动后余额、`amount_money`→金额、`exchange_rate`→汇率、`points_granted`→到账点数、`api_type`/`capabilities`/`is_active`/`created_at` 等;`admin.py` 里自定义显示方法(`@admin.display(description=...)`)同步中文。**仅显示层**:`verbose_name` 只改展示,**不改字段名、不改代码引用、英文字段名保持不变**;产出的是 `AlterField`(metadata-only,**无 DB schema 变更**)迁移。验收:`makemigrations`(仅 AlterField,无 schema 变更)→ `migrate` → `check` 0 issues → `test` 全绿 → `/admin/` 字段标签目视中文,并在 `../progress.md` 留证据。**当前阻塞**:代码与迁移已完成;`ACCOUNT_EMAIL_VERIFICATION="none"`(免邮箱验证)已定为**既定策略**(见 2026-07-06 决策),原假设 `mandatory` 的 2 条 portal 测试需**按新策略更新**(不再恢复 `mandatory`),由 **T-605** 处理;T-605 完成后本任务解封、重跑测试标 `DONE` | BLOCKED |
|
||||
| T-605 | 落实「免邮箱验证」策略 | T-501 | 把 `ACCOUNT_EMAIL_VERIFICATION="none"`(注册即可用、不发验证邮件、邮箱仍必填且唯一)作为**既定策略**清理落地:① `config/settings.py` 把 `ACCOUNT_EMAIL_VERIFICATION = "none"#"mandatory"` 改为干净的 `"none"`(去行内注释),评估 `ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION` 是否仍需要;② 更新 `apps/portal/tests.py` 里假设 mandatory 的 2 条测试(signup 不再依赖验证邮件、未验证也可直接登录),改为断言「注册后可直接登录」;③ 同步全项目文档口径(`02-requirements`/`05-coding-rules`/`api`/`04-architecture`/`03-tech-stack`/`routes`/`env`/`deployment`/`00-ai-start-here` 中「邮箱验证」→「免邮箱验证,邮箱仍唯一」);④(可选)注册防刷改用图形验证码/限流补位。验收:`check`/`test` 全绿(含更新后的注册测试)并在 `../progress.md` 留证据;**完成后解封 T-603** | TODO |
|
||||
| T-604 | 中文敏感词本地过滤(本地 keyword provider) | T-302, T-401 | 按 [`moderation.md`](moderation.md) 实施。**范围收紧**:T-604 只做输入 prompt 的本地敏感词快筛,不做云内容安全、不做输出审核、不做图片审核。**关键时序**:serializer 后先审 prompt,命中即 `400 content_blocked`;不得先下载 `image_url`,不得预扣点,不写 `CallRecord` / `PointsLedger`,不调上游。**核心实现**:新增 `apps.moderation`、`SensitiveWord` 模型/admin/迁移、keyword provider、归一化管线、Aho-Corasick matcher;`ahocorapy` 作为候选依赖,编码前必须验证 PyPI 可用性和 API 形状。**缓存**:matcher 进程内缓存,词库变更用共享 cache 版本号失效,不能只靠 `post_save` signal;生产依赖共享 cache。**配置**:`MODERATION_ENABLED=false` 默认 no-op;启用时 `MODERATION_PROVIDER=keyword`;MVP `SensitiveWord.action` 只支持 `block`。**验收**:`check`/`test` 全绿,覆盖 no-op、命中拦截且不扣点/不建记录/不调上游/不下载图片、归一化防绕过、词库变更后 matcher 重建;真实词库数据不进仓库,`__pycache__` 不进 Git | TODO |
|
||||
|
||||
## 里程碑
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
- [MVP 完整验收报告](mvp-acceptance.md):T-402 对 `02-requirements.md` P0 验收项的逐项结论、测试证据和已知限制。
|
||||
- [部署 / 运行文档](deployment.md):T-403 生产部署步骤、宝塔 / Nginx / Gunicorn 配置、静态文件、共享 cache、图片超时与上线检查。
|
||||
- [API 合约](api.md):对外接口、支付回调、AI 调用模块合约、错误码。
|
||||
- [内容安全与本地敏感词过滤](moderation.md):T-604 的 prompt 敏感词过滤设计、时序、缓存和验收口径。
|
||||
- [路由与页面结构](routes.md):API 路由与 django-admin 后台职责。
|
||||
- [环境变量与配置](env.md):Django、数据库、AI 密钥加密、支付、对象存储等配置项。
|
||||
- [当前实现状态](current-state.md):可覆盖的当前快照、可运行命令、下一步任务。
|
||||
|
||||
+7
-4
@@ -13,7 +13,7 @@
|
||||
- 用户或 Key 被禁用/吊销(disabled/revoked):返回 `403`。
|
||||
- **对外 API 只接受 API Key 认证,不接受 Web session**(浏览器带 cookie 也不能调 API,防绕过计费归属)。
|
||||
- 图片生成为**同步**接口,可能耗时较长,调用方与网关需设置足够超时(≥ 300s)。
|
||||
- 用户端注册使用同一个 `User` 账本主体;注册邮箱必须验证且唯一,避免同邮箱对应多个点数账户。
|
||||
- 用户端注册使用同一个 `User` 账本主体;注册邮箱**必填且唯一**(`ACCOUNT_EMAIL_VERIFICATION="none"`,**不做邮箱验证**、注册即可用),唯一约束避免同邮箱对应多个点数账户。
|
||||
- API Key 库内只存 `key_hash`(SHA-256)与 `key_prefix`,明文只在创建时返回一次,不在 admin、日志或调用记录中回显。
|
||||
- 每次生成调用写 `CallRecord`;只允许保存 `result_ref` / `result_summary` 这类引用或摘要,不保存 provider `raw`、base64 图片或敏感上游字段。
|
||||
|
||||
@@ -31,7 +31,9 @@ T-305 已实现扫码充值下单与轮询基线:`POST /api/v1/recharge/create
|
||||
|
||||
T-306 已实现对外 API 安全加固:`image_url` 下载只允许 `http` / `https` 公网地址,拒绝私有/回环/链路本地/保留等地址,重定向后重新校验并限制响应大小;全局 DRF 默认认证为空且默认权限为 `IsAuthenticated`,外部 API 与用户端 API 必须显式 opt-in 认证类;生成接口按 API Key / 用户限流,认证失败按 IP 限流;充值创建有 `RECHARGE_MAX_AMOUNT_CNY` 单笔上限。
|
||||
|
||||
T-501 已实现用户端注册 / 登录基线:`/signup` `/login` `/logout` 走 django-allauth + Django session + CSRF;注册邮箱必须验证,注册成功创建 0 点 `UserWallet`,不创建赠点流水。对外 API 仍只认 API Key,不接受 Web session。
|
||||
T-604 目标口径:生成接口在 serializer 基础校验后先执行 prompt 本地敏感词检查;命中返回 `400 content_blocked`,且不得下载 `image_url`、不得预扣点、不得写 `CallRecord` / `PointsLedger`、不得调用上游。T-604 不启用输出审核和图片审核;详细规则见 [`moderation.md`](moderation.md)。
|
||||
|
||||
T-501 已实现用户端注册 / 登录基线:`/signup` `/login` `/logout` 走 django-allauth + Django session + CSRF;**免邮箱验证、注册即可用(`ACCOUNT_EMAIL_VERIFICATION="none"`,邮箱仍必填且唯一)**,注册成功创建 0 点 `UserWallet`,不创建赠点流水。对外 API 仍只认 API Key,不接受 Web session。(策略见 T-605 与 2026-07-06 决策)
|
||||
|
||||
T-502 已实现用户端 API Key 自助管理基线:`/apikeys` 走 Django session + CSRF;登录用户可生成和删除自己的 Key,生成后的明文只在重定向后的首个页面显示一次,库内只保存 `key_hash` 与 `key_prefix`。用户端“删除”落库为 `revoked`,保留历史记录关联;吊销后的 Key 调用生成 / 余额接口返回 `403 account_disabled`,缺失、无效或不存在的 Key 仍返回 `401 unauthorized`。
|
||||
|
||||
@@ -60,6 +62,7 @@ T-504/T-505 已实现用户端充值页基线:`/recharge` 走 Django session +
|
||||
| `insufficient_points` | 点数不足,请先充值 | 402 |
|
||||
| `no_pricing_rule` | 未配置对应计费规则 | 400 |
|
||||
| `model_not_allowed` | 该模型不支持此操作(如图片模型生成标题) | 400 |
|
||||
| `content_blocked` | 输入内容未通过安全审核;T-604 中表示 prompt 命中本地敏感词,未扣点 | 400 |
|
||||
| `upstream_error` | 上游 AI 失败(已退点) | 502 |
|
||||
| `signature_invalid` | 支付回调验签失败 | 400 |
|
||||
| `amount_mismatch` | 支付回调金额与本地订单金额不一致 | 400 |
|
||||
@@ -99,7 +102,7 @@ T-504/T-505 已实现用户端充值页基线:`/recharge` 走 Django session +
|
||||
}
|
||||
```
|
||||
|
||||
要点:别名必须映射到声明 `text` 能力的模型;映射到图片模型时返回 `model_not_allowed`;别名无计费规则返回 `no_pricing_rule`。若传 `image_url`,服务端只会下载公网 `http` / `https` 图片,并在扣点前拒绝内网、回环、链路本地、元数据地址、跳转到内网的地址和超过大小上限的响应;被拒绝时返回 `bad_request` 且不预扣点。
|
||||
要点:别名必须映射到声明 `text` 能力的模型;映射到图片模型时返回 `model_not_allowed`;别名无计费规则返回 `no_pricing_rule`。T-604 后 prompt 命中本地敏感词时返回 `content_blocked`,不扣点、不写调用记录、不调上游。若传 `image_url`,服务端只会在 prompt 审核通过后下载公网 `http` / `https` 图片,并在扣点前拒绝内网、回环、链路本地、元数据地址、跳转到内网的地址和超过大小上限的响应;被拒绝时返回 `bad_request` 且不预扣点。
|
||||
|
||||
### `POST /api/v1/generate/image`
|
||||
|
||||
@@ -129,7 +132,7 @@ T-504/T-505 已实现用户端充值页基线:`/recharge` 走 Django session +
|
||||
}
|
||||
```
|
||||
|
||||
要点:改图类模型缺原图返回 `bad_request`,不落 500;上游失败返回 `upstream_error` 且**不扣点**(已预扣则退回)。结果默认存对象存储返回 `image_url`,避免同步响应体过大。请求里的 `image_url` 适用同标题接口相同的 SSRF 防护和大小上限;内网图片请用 `image_base64`。
|
||||
要点:改图类模型缺原图返回 `bad_request`,不落 500;上游失败返回 `upstream_error` 且**不扣点**(已预扣则退回)。T-604 后 prompt 命中本地敏感词时返回 `content_blocked`,并且不得下载 `image_url` 或解码大图后再拦截。结果默认存对象存储返回 `image_url`,避免同步响应体过大。请求里的 `image_url` 适用同标题接口相同的 SSRF 防护和大小上限;内网图片请用 `image_base64`。
|
||||
|
||||
### `GET /api/v1/balance`
|
||||
|
||||
|
||||
+19
-4
@@ -71,7 +71,21 @@ AI 上游连接超时与读取超时不走全局环境变量:连接超时由 `
|
||||
|
||||
`image_url` 只允许 `http` / `https`,服务端会在请求前解析域名,拒绝私有网段、回环、链路本地、保留地址、组播、未指定地址;重定向后的目标地址也会重复执行同样校验。内网图片不应通过 `image_url` 传入,调用方应改用 `image_base64`。
|
||||
|
||||
## 六、静态文件与限流缓存
|
||||
## 六、内容安全 / 本地敏感词配置
|
||||
|
||||
| 变量 | 必填 | 示例 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `MODERATION_ENABLED` | 否 | `false` | 内容安全总开关;默认关闭时所有审核 no-op |
|
||||
| `MODERATION_PROVIDER` | 启用时是 | `keyword` | T-604 只支持 `keyword` 本地敏感词 provider;云厂商后续扩展 |
|
||||
| `MODERATION_FAIL_CLOSED` | 否 | `true` | 审核 provider 不可用时是否拦截;生产建议 `true` |
|
||||
| `MODERATION_BLOCK_ON_REVIEW` | 否 | `false` | `review` 是否按拦截处理;T-604 keyword MVP 不使用 review |
|
||||
| `MODERATION_CACHE_VERSION_KEY` | 否 | `moderation:sensitive_words:version` | 共享 cache 中的词库版本号 key,用于多 worker matcher 失效 |
|
||||
|
||||
T-604 只做 prompt 本地敏感词快筛。命中时返回 `content_blocked`,不扣点、不写调用记录、不调上游;并且必须在下载 `image_url` 前执行 prompt 审核。详细规则见 [`moderation.md`](moderation.md)。
|
||||
|
||||
生产多 Gunicorn worker 下,敏感词 matcher 刷新依赖共享 cache 的版本号;若仍使用 `LocMemCache`,只能刷新当前进程,不能保证所有 worker 及时更新。
|
||||
|
||||
## 七、静态文件与限流缓存
|
||||
|
||||
| 变量 | 必填 | 示例 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -82,7 +96,7 @@ AI 上游连接超时与读取超时不走全局环境变量:连接超时由 `
|
||||
|
||||
生产限流依赖 Django cache。默认 `LocMemCache` 只适合单进程本地开发;多 worker 部署时每个进程各算一份限流,会放大实际请求速率。MVP 可先用 MySQL 的 `DatabaseCache`,部署时执行 `python3.12 manage.py createcachetable cmhub_cache`;高并发后再换 Redis / Memcached 等共享 cache,并同步安装对应 backend 依赖。
|
||||
|
||||
## 七、支付配置
|
||||
## 八、支付配置
|
||||
|
||||
| 变量 | 必填 | 示例 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -113,7 +127,7 @@ AI 上游连接超时与读取超时不走全局环境变量:连接超时由 `
|
||||
|
||||
缺少真实商户配置时,充值任务只能使用 mock 支付客户端;mock 必须在代码和测试中标明,不得伪装成真实支付。
|
||||
|
||||
## 八、对象存储配置
|
||||
## 九、对象存储配置
|
||||
|
||||
图片结果默认返回 URL,避免大 base64 进入同步响应体。对象存储选型未最终落地前,可使用本地开发存储,但生产必须给出可公开访问或可签名访问的 URL。
|
||||
|
||||
@@ -127,7 +141,7 @@ AI 上游连接超时与读取超时不走全局环境变量:连接超时由 `
|
||||
| `S3_ACCESS_KEY_ID` | s3 是 | `change-me` | access key |
|
||||
| `S3_SECRET_ACCESS_KEY` | s3 是 | `change-me` | secret key |
|
||||
|
||||
## 九、上线前检查
|
||||
## 十、上线前检查
|
||||
|
||||
- `DJANGO_DEBUG=false`。
|
||||
- `DJANGO_SECRET_KEY`、`AI_KEY_ENCRYPTION_KEY`、数据库密码、支付密钥均已使用生产值。
|
||||
@@ -138,4 +152,5 @@ AI 上游连接超时与读取超时不走全局环境变量:连接超时由 `
|
||||
- 全站 HTTPS 稳定后再设置 `DJANGO_SECURE_HSTS_SECONDS`;未确认子域名 HTTPS 前不要启用 includeSubDomains / preload。
|
||||
- MySQL 为 8.4 LTS / InnoDB / `utf8mb4`,不是 SQLite 或已有 MySQL 5.7。
|
||||
- `image_url` 下载上限、生成限流、认证失败限流、单笔充值金额上限已按生产容量调整。
|
||||
- 若启用 `MODERATION_ENABLED`,`MODERATION_PROVIDER=keyword`,敏感词词库已配置,且共享 cache 可用于多 worker matcher 版本失效。
|
||||
- 图片同步链路的客户端、Nginx、Gunicorn、上游 read timeout 均按最慢图片模型放大到同一量级。
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
# 内容安全与本地敏感词过滤
|
||||
|
||||
> T-604 的实施口径。目标是先做「中文 prompt 本地敏感词快筛」,命中在扣点和调上游之前拦截;云内容安全与输出审核保留接口,不在本任务做实。
|
||||
|
||||
## 定位
|
||||
|
||||
- 本地敏感词过滤是免费快筛和运营自定义黑名单,不替代合规内容审核。
|
||||
- T-604 只处理输入 prompt 的关键词拦截;图片审核、输出文本审核、输出图片审核、云厂商内容安全属于后续任务。
|
||||
- 默认关闭:`MODERATION_ENABLED=false` 时必须 no-op,现有生成接口行为不变。
|
||||
- 打开后使用 `MODERATION_PROVIDER=keyword`;未配置或配置错误时按 fail-closed 处理。
|
||||
|
||||
## 请求时序
|
||||
|
||||
生成接口必须按这个顺序执行:
|
||||
|
||||
1. DRF serializer 校验基础字段。
|
||||
2. **先审核 prompt 文本**,不得先下载 `image_url` 或预扣点。
|
||||
3. prompt 命中敏感词:返回 `400 content_blocked`,不下载图片、不解析别名、不计费、不写 `CallRecord` / `PointsLedger`、不调上游。
|
||||
4. prompt 通过后,才允许读取 `image_base64` 或下载公网 `image_url`,并继续 SSRF 防护。
|
||||
5. 别名解析、能力校验、计费规则查询。
|
||||
6. 预扣点、写 pending 调用和 consume 流水。
|
||||
7. 调上游,成功后写成功记录;失败走既有退点路径。
|
||||
|
||||
说明:当前 `apps/moderation` 骨架里有 `moderate_output_*` 钩子,但 T-604 不启用输出审核。输出审核若后续落地,必须重新定义「命中是否退点」的产品策略,并让 `MODERATION_REFUND_ON_OUTPUT_BLOCK` 真的生效;在 T-604 中不要暴露一个未实现的配置。
|
||||
|
||||
## 数据模型
|
||||
|
||||
新增 `SensitiveWord` 模型:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `word` | 原始敏感词,唯一或按 category 唯一;不能为空 |
|
||||
| `normalized_word` | 归一化后的词,用于构建 matcher;保存时生成,便于排查和唯一约束 |
|
||||
| `category` | 分类,如 `politics` / `violence` / `porn` / `custom` |
|
||||
| `action` | MVP 只支持 `block`;后续再扩展 `review` / `flag` |
|
||||
| `is_active` | 是否启用 |
|
||||
| `created_at` / `updated_at` | 时间戳 |
|
||||
|
||||
admin 要求:
|
||||
|
||||
- 支持按 `word` / `normalized_word` / `category` 搜索。
|
||||
- 支持按 `category` / `action` / `is_active` 过滤。
|
||||
- 修改词库后触发 matcher 版本失效。
|
||||
|
||||
## 归一化
|
||||
|
||||
匹配前对 prompt 与词库统一归一化:
|
||||
|
||||
- Unicode NFKC,把全角转半角。
|
||||
- 去零宽字符。
|
||||
- 去空白和常见分隔标点,用于拦截「敏 感 词」「敏-感-词」这类低级绕过。
|
||||
- 英文字母小写。
|
||||
- 繁简转换可选;如果 `opencc` 安装或运行失败,跳过繁简,不影响其他归一化。
|
||||
|
||||
风险:过度归一化会提高误伤概率。测试必须包含至少一条例外/普通文本样例,避免明显正常内容被误拦。
|
||||
|
||||
## 匹配与缓存
|
||||
|
||||
- 使用 Aho-Corasick 自动机做多词匹配;`ahocorapy` 作为候选依赖,但实现前必须验证 PyPI 包可用性和 API 形状。
|
||||
- 禁止每请求重建 matcher。
|
||||
- 每个 worker 进程内缓存 matcher;生产多 worker 下不能只依赖 Django `post_save` / `post_delete` signal,因为 signal 只影响当前进程。
|
||||
- 采用共享版本号失效:词库变更后把 `moderation:sensitive_words:version` 写入共享 cache;每次请求只做轻量版本检查,版本不一致时当前进程重建 matcher。
|
||||
- 生产必须使用共享 cache(DatabaseCache / Redis / Memcached),不能依赖 `LocMemCache` 完成跨 worker 失效。
|
||||
|
||||
## 错误与留痕
|
||||
|
||||
命中后对外响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "content_blocked",
|
||||
"message": "输入内容未通过安全审核"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
留痕原则:
|
||||
|
||||
- 不保存违规 prompt 原文。
|
||||
- 可记录分类、命中词 ID、请求用户、API Key 前缀、时间、阶段、处理结果。
|
||||
- MVP 若不建 `ModerationRecord`,至少测试中确认不会把 prompt 原文写入日志或数据库的新字段。
|
||||
|
||||
## 验收
|
||||
|
||||
T-604 完成前至少覆盖:
|
||||
|
||||
- `MODERATION_ENABLED=false` 时生成接口 no-op,既有用例不变。
|
||||
- `MODERATION_PROVIDER=keyword` 且命中 prompt 时返回 `400 content_blocked`。
|
||||
- 命中时不扣点、不创建 `CallRecord`、不写 `PointsLedger`、不调用上游 provider。
|
||||
- 命中时不会下载 `image_url`。
|
||||
- 归一化命中:空格/分隔符/全半角/零宽字符。
|
||||
- 繁简归一化若启用则有测试;未安装 `opencc` 时测试跳过或断言 graceful fallback。
|
||||
- 词库变更后 matcher 版本号变化,当前进程下一次请求能重建 matcher。
|
||||
- 多 worker 口径在文档和测试中说明:跨进程刷新依赖共享 cache 版本号,不依赖本地 signal。
|
||||
|
||||
## 不在 T-604 范围
|
||||
|
||||
- 云内容安全真实厂商接入。
|
||||
- 输出内容审核与输出命中退点/照扣策略。
|
||||
- 图片内容审核。
|
||||
- 脱敏、人工复审、灰度放行。
|
||||
- 大规模开源词库入库;可提供导入命令,但真实词库数据不进仓库。
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
|
||||
| 路由 | 方法 | 职责 | 鉴权 |
|
||||
| --- | --- | --- | --- |
|
||||
| `/signup` `/login` `/logout` | GET/POST | 自助注册(邮箱验证)/登录/登出(Django auth/allauth) | 公开 |
|
||||
| `/signup` `/login` `/logout` | GET/POST | 自助注册(**免邮箱验证、注册即可用**)/登录/登出(Django auth/allauth) | 公开 |
|
||||
| `/dashboard` | GET | 个人中心:剩余点数、充值总额、快捷入口 | session |
|
||||
| `/recharge` | GET/POST | 发起充值:选金额→展示支付二维码→轮询到账 | session |
|
||||
| `/records/recharge` | GET | 充值记录 | session |
|
||||
|
||||
Reference in New Issue
Block a user