docs: refine moderation and signup policy
This commit is contained in:
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
| 功能 | 用户能做什么 | 优先级 |
|
| 功能 | 用户能做什么 | 优先级 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 用户注册/登录 | 终端用户在用户端自助注册、登录(邮箱验证);**注册不送免费点数** | P0 |
|
| 用户注册/登录 | 终端用户在用户端自助注册、登录(**免邮箱验证、注册即可用;邮箱仍必填且唯一**);**注册不送免费点数** | P0 |
|
||||||
| API Key 自助管理 | 登录后自助生成/删除 API Key;Key 只在生成时显示一次(库内哈希存储) | P0 |
|
| API Key 自助管理 | 登录后自助生成/删除 API Key;Key 只在生成时显示一次(库内哈希存储) | P0 |
|
||||||
| 扫码充值 | 用户端发起充值→展示支付二维码→扫码付款→回调到账;金额按汇率转点数 | P0 |
|
| 扫码充值 | 用户端发起充值→展示支付二维码→扫码付款→回调到账;金额按汇率转点数 | P0 |
|
||||||
| 个人中心 | 查看剩余点数、充值总额、充值记录、点数使用(消费)记录 | P0 |
|
| 个人中心 | 查看剩余点数、充值总额、充值记录、点数使用(消费)记录 | P0 |
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
- **余额查询 API**:返回的余额等于该账号点数流水累加结果。
|
- **余额查询 API**:返回的余额等于该账号点数流水累加结果。
|
||||||
- **充值转点数**:模拟一次支付系统回调(含订单号、金额、签名),验签通过后按汇率加点;**同一订单号重复回调只入账一次**(幂等);验签失败不入账。
|
- **充值转点数**:模拟一次支付系统回调(含订单号、金额、签名),验签通过后按汇率加点;**同一订单号重复回调只入账一次**(幂等);验签失败不入账。
|
||||||
- **调用记录 / 点数流水**:每次成功/失败调用、每次充值/退款/调整都各生成一条记录,字段完整、可在后台检索。
|
- **调用记录 / 点数流水**:每次成功/失败调用、每次充值/退款/调整都各生成一条记录,字段完整、可在后台检索。
|
||||||
- **用户注册/登录**:新用户能自助注册(邮箱验证)、登录;注册后点数为 0(不送免费点数)。
|
- **用户注册/登录**:新用户能自助注册(**免邮箱验证、注册即可用;邮箱仍必填且唯一**)、登录;注册后点数为 0(不送免费点数)。
|
||||||
- **API Key 自助管理**:登录用户能生成 API Key(明文只显示一次,库内存哈希)、能删除;删除落库为吊销,吊销后该 Key 调用返回 403,缺失/无效/不存在 Key 返回 401。
|
- **API Key 自助管理**:登录用户能生成 API Key(明文只显示一次,库内存哈希)、能删除;删除落库为吊销,吊销后该 Key 调用返回 403,缺失/无效/不存在 Key 返回 401。
|
||||||
- **扫码充值**:用户端发起充值→展示二维码→(mock 或真实)支付成功回调后点数按汇率到账,充值记录与流水各一条;同一订单号重复回调只入账一次。
|
- **扫码充值**:用户端发起充值→展示二维码→(mock 或真实)支付成功回调后点数按汇率到账,充值记录与流水各一条;同一订单号重复回调只入账一次。
|
||||||
- **个人中心**:剩余点数 = 点数流水累加;充值总额、充值记录、消费记录与实际一致。
|
- **个人中心**:剩余点数 = 点数流水累加;充值总额、充值记录、消费记录与实际一致。
|
||||||
|
|||||||
+18
-2
@@ -23,6 +23,7 @@
|
|||||||
- **API 层(DRF)**:对外生成接口、余额查询、支付回调接收、扫码下单。入口 `apps/api/`。生成/余额这类对外业务 API **只认 API Key,不接受 Web session**;充值下单/状态查询属于用户端流程,走 Web session + CSRF;支付回调走平台验签。
|
- **API 层(DRF)**:对外生成接口、余额查询、支付回调接收、扫码下单。入口 `apps/api/`。生成/余额这类对外业务 API **只认 API Key,不接受 Web session**;充值下单/状态查询属于用户端流程,走 Web session + CSRF;支付回调走平台验签。
|
||||||
- **计费层**:点数计算、原子扣减(锁 `UserWallet` 行)、退点、充值入账、流水记账。入口 `apps/billing/`。
|
- **计费层**:点数计算、原子扣减(锁 `UserWallet` 行)、退点、充值入账、流水记账。入口 `apps/billing/`。
|
||||||
- **AI 调用层(Provider Adapter 架构)**:对外只暴露稳定能力,内部用「能力别名 → 具体供应商适配器」解耦。入口 `apps/ai/`,适配器在 `apps/ai/providers/`。
|
- **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` 流水。
|
- **运营后台**:django-admin,注册各模型的 Admin。入口各 app 的 `admin.py`。T-401 已补齐用户、钱包、API Key(脱敏)、计费规则、汇率、充值订单、点数流水和调用记录的管理/检索;点数手工调整只能从钱包专用入口提交原因和非 0 变动,经计费层写 `adjust` 流水。
|
||||||
- **外部服务**:上游 AI(vectorengine)、外部支付系统(跳转 + 回调)。
|
- **外部服务**:上游 AI(vectorengine)、外部支付系统(跳转 + 回调)。
|
||||||
- **数据库**:MySQL 8.4 LTS(cmhub **专用独立实例**,不复用 VPS 已有的 MySQL 5.7);引擎必须 InnoDB、字符集 utf8mb4。开发与生产同用 MySQL,勿用 SQLite(SQLite 会静默忽略 `select_for_update`,测不出并发扣点)。
|
- **数据库**: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*`)。
|
- 输入: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、点数余额、计费规则、汇率、充值订单、点数流水、调用记录、模型配置。
|
- 持久化:账号、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;只读 |
|
| AiConfigAuditLog | 后台自动写入 | AiModel / ModelAlias / api_key 变更审计;记录 actor、action、target、changed_fields、changes、created_at;只读 |
|
||||||
| PricingRule | 运营配置 | 操作类型 × **能力别名字符串**(+ 可选分辨率)→ 点数单价;不外键到具体 AiModel |
|
| PricingRule | 运营配置 | 操作类型 × **能力别名字符串**(+ 可选分辨率)→ 点数单价;不外键到具体 AiModel |
|
||||||
| ExchangeRate | 运营配置 | 金额 → 点数 的汇率(如 1 元 = N 点),按 `currency + effective_from` 取当前有效值 |
|
| 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 密文。
|
- `AiConfigAuditLog` 只追加、不在后台编辑删除;密钥变更只记录 `api_key: empty/set` 状态变化,不记录明文 key 或 Fernet 密文。
|
||||||
- 汇率在**充值下单创建订单时**锁定并记录到订单,同时计算预计到账点数;后续改汇率不影响已创建订单。支付回调入账时使用订单上的 `exchange_rate` / `points_granted`,不重新读取当前汇率。
|
- 汇率在**充值下单创建订单时**锁定并记录到订单,同时计算预计到账点数;后续改汇率不影响已创建订单。支付回调入账时使用订单上的 `exchange_rate` / `points_granted`,不重新读取当前汇率。
|
||||||
- Provider 适配器只允许白名单安全参数透传;外部调用方或后台 `extra_body` 传入核心字段时忽略,由服务端按别名解析和计费口径固定。
|
- Provider 适配器只允许白名单安全参数透传;外部调用方或后台 `extra_body` 传入核心字段时忽略,由服务端按别名解析和计费口径固定。
|
||||||
|
- `SensitiveWord` 的 matcher 由 active 词库构建;词库变更后更新共享 cache 版本号,各 Gunicorn worker 在下一次请求检查版本并懒重建。
|
||||||
|
|
||||||
#### 配置表目标 schema
|
#### 配置表目标 schema
|
||||||
|
|
||||||
@@ -346,7 +357,7 @@ CREATE TABLE call_record (
|
|||||||
| 配置热生效 | 后台改模型/密钥后运行时仍用旧值 | 不在进程内长缓存;每次查库或保存时失效缓存 |
|
| 配置热生效 | 后台改模型/密钥后运行时仍用旧值 | 不在进程内长缓存;每次查库或保存时失效缓存 |
|
||||||
| 配置变更审计 | 改密钥/模型/别名映射无痕 | `AiConfigAuditLog` 自动记录后台 create/update/delete;密钥只记录 empty/set 变化,日志只读 |
|
| 配置变更审计 | 改密钥/模型/别名映射无痕 | `AiConfigAuditLog` 自动记录后台 create/update/delete;密钥只记录 empty/set 变化,日志只读 |
|
||||||
| API Key 泄露 | 明文存库一旦泄露全泄 | Key **哈希存储**(sha256),明文只在创建时显示一次,库内存 `key_hash`+`key_prefix`,鉴权做哈希比对 |
|
| 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) |
|
| Web/API 抢 worker | 图片同步长请求占满 worker、拖慢用户端页面 | 单体下按路径把 `/api/v1/generate/*` 与用户端页面**分流到不同 gunicorn/worker 池**(见 5.1) |
|
||||||
| 充错账户 | 扫码订单未绑定发起用户 | 订单创建即绑定 `user`;回调按 `order_no` 定位订单→其 user 入账 |
|
| 充错账户 | 扫码订单未绑定发起用户 | 订单创建即绑定 `user`;回调按 `order_no` 定位订单→其 user 入账 |
|
||||||
| SSRF / 任意 URL 下载 | `image_url` 让服务端请求调用方指定地址 | 仅允许公网 http/https;请求前解析 IP 并拒绝内网/回环/链路本地/保留地址;重定向逐跳校验;流式读取并限制大小 |
|
| SSRF / 任意 URL 下载 | `image_url` 让服务端请求调用方指定地址 | 仅允许公网 http/https;请求前解析 IP 并拒绝内网/回环/链路本地/保留地址;重定向逐跳校验;流式读取并限制大小 |
|
||||||
@@ -407,4 +418,9 @@ cmhub/
|
|||||||
- 点数余额存 `UserWallet`(与 auth `User` 表分离);扣点锁 wallet 行,不与登录/资料更新抢锁。
|
- 点数余额存 `UserWallet`(与 auth `User` 表分离);扣点锁 wallet 行,不与登录/资料更新抢锁。
|
||||||
- **API Key 哈希存储**(sha256),明文只在创建时返回一次;库内存 `key_hash`+`key_prefix`,任何页面不回显明文。
|
- **API Key 哈希存储**(sha256),明文只在创建时返回一次;库内存 `key_hash`+`key_prefix`,任何页面不回显明文。
|
||||||
- **对外 API 只接受 API Key 认证,不挂 SessionAuthentication**,防止浏览器带 cookie 绕过 Key 与计费归属。
|
- **对外 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` 为准,不跨层偷写逻辑。
|
- 模块职责以 `04-architecture.md` 为准,不跨层偷写逻辑。
|
||||||
- **点数余额只能经 `apps/billing` 修改**:余额存 `UserWallet`(与 auth `User` 分离),扣点锁 wallet 行;API 层/Admin/用户端都不得直接写 `points_balance`。
|
- **点数余额只能经 `apps/billing` 修改**:余额存 `UserWallet`(与 auth `User` 分离),扣点锁 wallet 行;API 层/Admin/用户端都不得直接写 `points_balance`。
|
||||||
- **API Key 哈希存储**(sha256),明文只在生成时返回一次;不存明文、任何页面不回显。
|
- **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 层写供应商分支逻辑。
|
- **对外接口只接受能力别名,不接受具体模型名**;新增供应商必须走 `apps/ai/providers/` 的适配器,不在 API 层写供应商分支逻辑。
|
||||||
- 计费按别名定价,不按具体模型 SKU;不要把模型名写进对外契约或调用方文档。
|
- 计费按别名定价,不按具体模型 SKU;不要把模型名写进对外契约或调用方文档。
|
||||||
- 供应商 `api_key` 必须加密存储、使用时解密,不落明文、不在 admin 回显。
|
- 供应商 `api_key` 必须加密存储、使用时解密,不落明文、不在 admin 回显。
|
||||||
|
- **内容安全顺序**:若启用 prompt 敏感词过滤,必须在 `image_url` 下载 / `image_base64` 大图处理 / 别名解析 / 计费预扣之前审核 prompt;命中时返回 `content_blocked`,不扣点、不写 `CallRecord` / `PointsLedger`、不调上游。详细规则见 `docs/moderation.md`。
|
||||||
- 接口形状以 `api.md` 为准,运营后台路由以 `routes.md` 为准。
|
- 接口形状以 `api.md` 为准,运营后台路由以 `routes.md` 为准。
|
||||||
|
|
||||||
## 5. 代码规范
|
## 5. 代码规范
|
||||||
|
|||||||
+4
-2
@@ -61,7 +61,7 @@
|
|||||||
|
|
||||||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
| 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-502 | API Key 自助管理页 | T-501, T-301 | 登录用户生成/删除 Key;明文只显示一次、库内只存哈希;列表只显示 prefix;删除即吊销,吊销后该 Key 调用 403(无效/不存在 Key 仍为 401) | DONE |
|
||||||
| T-503 | 个人中心 / 记录页 | T-501, T-203 | 剩余点数、充值总额、充值记录、消费(调用)记录;数据与流水一致;仅见本人 | DONE |
|
| T-503 | 个人中心 / 记录页 | T-501, T-203 | 剩余点数、充值总额、充值记录、消费(调用)记录;数据与流水一致;仅见本人 | DONE |
|
||||||
| T-504 | 充值页(扫码 + 轮询到账) | T-501, T-305 | 发起充值→展示二维码→轮询订单状态→到账后余额刷新;到账以回调为权威 | 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-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-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 验收项的逐项结论、测试证据和已知限制。
|
- [MVP 完整验收报告](mvp-acceptance.md):T-402 对 `02-requirements.md` P0 验收项的逐项结论、测试证据和已知限制。
|
||||||
- [部署 / 运行文档](deployment.md):T-403 生产部署步骤、宝塔 / Nginx / Gunicorn 配置、静态文件、共享 cache、图片超时与上线检查。
|
- [部署 / 运行文档](deployment.md):T-403 生产部署步骤、宝塔 / Nginx / Gunicorn 配置、静态文件、共享 cache、图片超时与上线检查。
|
||||||
- [API 合约](api.md):对外接口、支付回调、AI 调用模块合约、错误码。
|
- [API 合约](api.md):对外接口、支付回调、AI 调用模块合约、错误码。
|
||||||
|
- [内容安全与本地敏感词过滤](moderation.md):T-604 的 prompt 敏感词过滤设计、时序、缓存和验收口径。
|
||||||
- [路由与页面结构](routes.md):API 路由与 django-admin 后台职责。
|
- [路由与页面结构](routes.md):API 路由与 django-admin 后台职责。
|
||||||
- [环境变量与配置](env.md):Django、数据库、AI 密钥加密、支付、对象存储等配置项。
|
- [环境变量与配置](env.md):Django、数据库、AI 密钥加密、支付、对象存储等配置项。
|
||||||
- [当前实现状态](current-state.md):可覆盖的当前快照、可运行命令、下一步任务。
|
- [当前实现状态](current-state.md):可覆盖的当前快照、可运行命令、下一步任务。
|
||||||
|
|||||||
+7
-4
@@ -13,7 +13,7 @@
|
|||||||
- 用户或 Key 被禁用/吊销(disabled/revoked):返回 `403`。
|
- 用户或 Key 被禁用/吊销(disabled/revoked):返回 `403`。
|
||||||
- **对外 API 只接受 API Key 认证,不接受 Web session**(浏览器带 cookie 也不能调 API,防绕过计费归属)。
|
- **对外 API 只接受 API Key 认证,不接受 Web session**(浏览器带 cookie 也不能调 API,防绕过计费归属)。
|
||||||
- 图片生成为**同步**接口,可能耗时较长,调用方与网关需设置足够超时(≥ 300s)。
|
- 图片生成为**同步**接口,可能耗时较长,调用方与网关需设置足够超时(≥ 300s)。
|
||||||
- 用户端注册使用同一个 `User` 账本主体;注册邮箱必须验证且唯一,避免同邮箱对应多个点数账户。
|
- 用户端注册使用同一个 `User` 账本主体;注册邮箱**必填且唯一**(`ACCOUNT_EMAIL_VERIFICATION="none"`,**不做邮箱验证**、注册即可用),唯一约束避免同邮箱对应多个点数账户。
|
||||||
- API Key 库内只存 `key_hash`(SHA-256)与 `key_prefix`,明文只在创建时返回一次,不在 admin、日志或调用记录中回显。
|
- API Key 库内只存 `key_hash`(SHA-256)与 `key_prefix`,明文只在创建时返回一次,不在 admin、日志或调用记录中回显。
|
||||||
- 每次生成调用写 `CallRecord`;只允许保存 `result_ref` / `result_summary` 这类引用或摘要,不保存 provider `raw`、base64 图片或敏感上游字段。
|
- 每次生成调用写 `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-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`。
|
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 |
|
| `insufficient_points` | 点数不足,请先充值 | 402 |
|
||||||
| `no_pricing_rule` | 未配置对应计费规则 | 400 |
|
| `no_pricing_rule` | 未配置对应计费规则 | 400 |
|
||||||
| `model_not_allowed` | 该模型不支持此操作(如图片模型生成标题) | 400 |
|
| `model_not_allowed` | 该模型不支持此操作(如图片模型生成标题) | 400 |
|
||||||
|
| `content_blocked` | 输入内容未通过安全审核;T-604 中表示 prompt 命中本地敏感词,未扣点 | 400 |
|
||||||
| `upstream_error` | 上游 AI 失败(已退点) | 502 |
|
| `upstream_error` | 上游 AI 失败(已退点) | 502 |
|
||||||
| `signature_invalid` | 支付回调验签失败 | 400 |
|
| `signature_invalid` | 支付回调验签失败 | 400 |
|
||||||
| `amount_mismatch` | 支付回调金额与本地订单金额不一致 | 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`
|
### `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`
|
### `GET /api/v1/balance`
|
||||||
|
|
||||||
|
|||||||
+19
-4
@@ -71,7 +71,21 @@ AI 上游连接超时与读取超时不走全局环境变量:连接超时由 `
|
|||||||
|
|
||||||
`image_url` 只允许 `http` / `https`,服务端会在请求前解析域名,拒绝私有网段、回环、链路本地、保留地址、组播、未指定地址;重定向后的目标地址也会重复执行同样校验。内网图片不应通过 `image_url` 传入,调用方应改用 `image_base64`。
|
`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 依赖。
|
生产限流依赖 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 必须在代码和测试中标明,不得伪装成真实支付。
|
缺少真实商户配置时,充值任务只能使用 mock 支付客户端;mock 必须在代码和测试中标明,不得伪装成真实支付。
|
||||||
|
|
||||||
## 八、对象存储配置
|
## 九、对象存储配置
|
||||||
|
|
||||||
图片结果默认返回 URL,避免大 base64 进入同步响应体。对象存储选型未最终落地前,可使用本地开发存储,但生产必须给出可公开访问或可签名访问的 URL。
|
图片结果默认返回 URL,避免大 base64 进入同步响应体。对象存储选型未最终落地前,可使用本地开发存储,但生产必须给出可公开访问或可签名访问的 URL。
|
||||||
|
|
||||||
@@ -127,7 +141,7 @@ AI 上游连接超时与读取超时不走全局环境变量:连接超时由 `
|
|||||||
| `S3_ACCESS_KEY_ID` | s3 是 | `change-me` | access key |
|
| `S3_ACCESS_KEY_ID` | s3 是 | `change-me` | access key |
|
||||||
| `S3_SECRET_ACCESS_KEY` | s3 是 | `change-me` | secret key |
|
| `S3_SECRET_ACCESS_KEY` | s3 是 | `change-me` | secret key |
|
||||||
|
|
||||||
## 九、上线前检查
|
## 十、上线前检查
|
||||||
|
|
||||||
- `DJANGO_DEBUG=false`。
|
- `DJANGO_DEBUG=false`。
|
||||||
- `DJANGO_SECRET_KEY`、`AI_KEY_ENCRYPTION_KEY`、数据库密码、支付密钥均已使用生产值。
|
- `DJANGO_SECRET_KEY`、`AI_KEY_ENCRYPTION_KEY`、数据库密码、支付密钥均已使用生产值。
|
||||||
@@ -138,4 +152,5 @@ AI 上游连接超时与读取超时不走全局环境变量:连接超时由 `
|
|||||||
- 全站 HTTPS 稳定后再设置 `DJANGO_SECURE_HSTS_SECONDS`;未确认子域名 HTTPS 前不要启用 includeSubDomains / preload。
|
- 全站 HTTPS 稳定后再设置 `DJANGO_SECURE_HSTS_SECONDS`;未确认子域名 HTTPS 前不要启用 includeSubDomains / preload。
|
||||||
- MySQL 为 8.4 LTS / InnoDB / `utf8mb4`,不是 SQLite 或已有 MySQL 5.7。
|
- MySQL 为 8.4 LTS / InnoDB / `utf8mb4`,不是 SQLite 或已有 MySQL 5.7。
|
||||||
- `image_url` 下载上限、生成限流、认证失败限流、单笔充值金额上限已按生产容量调整。
|
- `image_url` 下载上限、生成限流、认证失败限流、单笔充值金额上限已按生产容量调整。
|
||||||
|
- 若启用 `MODERATION_ENABLED`,`MODERATION_PROVIDER=keyword`,敏感词词库已配置,且共享 cache 可用于多 worker matcher 版本失效。
|
||||||
- 图片同步链路的客户端、Nginx、Gunicorn、上游 read timeout 均按最慢图片模型放大到同一量级。
|
- 图片同步链路的客户端、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 |
|
| `/dashboard` | GET | 个人中心:剩余点数、充值总额、快捷入口 | session |
|
||||||
| `/recharge` | GET/POST | 发起充值:选金额→展示支付二维码→轮询到账 | session |
|
| `/recharge` | GET/POST | 发起充值:选金额→展示支付二维码→轮询到账 | session |
|
||||||
| `/records/recharge` | GET | 充值记录 | session |
|
| `/records/recharge` | GET | 充值记录 | session |
|
||||||
|
|||||||
+67
@@ -1149,3 +1149,70 @@
|
|||||||
- `py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests.test_recharge_page_requires_login_and_shows_form apps.portal.tests.PortalAccountFlowTests.test_recharge_page_rejects_amount_above_configured_maximum apps.portal.tests.PortalAccountFlowTests.test_recharge_page_rejects_hidden_alipay_submit --keepdb --noinput --verbosity 2`:通过,3 tests OK。
|
- `py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests.test_recharge_page_requires_login_and_shows_form apps.portal.tests.PortalAccountFlowTests.test_recharge_page_rejects_amount_above_configured_maximum apps.portal.tests.PortalAccountFlowTests.test_recharge_page_rejects_hidden_alipay_submit --keepdb --noinput --verbosity 2`:通过,3 tests OK。
|
||||||
- 线上:同步热修后 `python3.12 manage.py check` 通过,`cmhub-web` / `cmhub-generate` 重启后均为 `active`;修复后的微信 1 分诊断预支付请求返回 `weixin://wxpay/bizpayurl` 票据;用户端表单 choices 仅剩 `微信`,支付宝提交无效。
|
- 线上:同步热修后 `python3.12 manage.py check` 通过,`cmhub-web` / `cmhub-generate` 重启后均为 `active`;修复后的微信 1 分诊断预支付请求返回 `weixin://wxpay/bizpayurl` 票据;用户端表单 choices 仅剩 `微信`,支付宝提交无效。
|
||||||
- 后续:微信下单已通,但线上微信回调曾出现 `PaymentVerificationError`,仍需修复并做真实支付到账闭环;支付宝需在开放平台把 VPS 出口 IP `43.128.3.240` 加入可信 IP 后再恢复页面选项。
|
- 后续:微信下单已通,但线上微信回调曾出现 `PaymentVerificationError`,仍需修复并做真实支付到账闭环;支付宝需在开放平台把 VPS 出口 IP `43.128.3.240` 加入可信 IP 后再恢复页面选项。
|
||||||
|
|
||||||
|
## 2026-07-06 需求+方案:中文敏感词本地过滤(T-604,ahocorapy)
|
||||||
|
|
||||||
|
- 需求:对中文 prompt 做本地敏感词过滤,命中即在**预扣点之前**拦截(不扣点、不调上游、返回 400 content_blocked)。
|
||||||
|
- 定位(重要):本地词表 = **免费快筛 + 运营自定义黑名单**,**不替代合规内容审核**。国内 AIGC 上线通常仍需有资质的云内容安全服务;云 API 作为后续可插拔槽位,不在 T-604 范围。
|
||||||
|
- 选型:匹配引擎用 **`ahocorapy`(纯 Python Aho-Corasick,免编译,VPS 上 pip 一定装得上)**,放弃需编译的 `pyahocorasick`。繁简归一化 `opencc` 可选。
|
||||||
|
- 方案要点:
|
||||||
|
1. 接现有 `apps/moderation/` 骨架(provider 接口 + `moderate_input`/`moderate_output_*` 钩子 + fail-closed),本任务把骨架一并转正提交;`MODERATION_ENABLED` 默认 False → no-op,现有测试不受影响。
|
||||||
|
2. `KeywordModerationProvider`:**先归一化再匹配**(全半角/去空白分隔标点/零宽/大小写/可选繁简)以抗低级绕过;ahocorapy 做子串匹配。
|
||||||
|
3. `SensitiveWord` 模型(word/category/action/is_active)+ admin 维护 + 迁移。
|
||||||
|
4. **缓存 matcher**:启动/词库变更时构建自动机缓存内存,信号或版本号失效重建,绝不每请求重建;多 worker 各建各的(只读)。
|
||||||
|
5. 命中默认 block(脱敏/flag 后续可配);留痕只记分类/词 id,不落违规原文。
|
||||||
|
6. 可选 `seed_sensitive_words` 命令导开源词库打底,真实词库数据不进仓库。
|
||||||
|
- 依赖:现有 `apps/moderation/` 骨架(当前未提交),本任务转正;`requirements.txt` 加 `ahocorapy`。
|
||||||
|
- 待确认(已给默认):本地是「补充」非「替代」云 API(推荐);命中动作默认 block;词库 admin 维护 + 可选 seed。
|
||||||
|
- 任务:`06-tasks.md` T-604。下一步:实现 T-604(并处理 T-603 的 `ACCOUNT_EMAIL_VERIFICATION` 阻塞)。
|
||||||
|
|
||||||
|
## 2026-07-06 决策:永久免邮箱验证(ACCOUNT_EMAIL_VERIFICATION="none")
|
||||||
|
|
||||||
|
- 决策:用户端注册**不做邮箱验证**,`ACCOUNT_EMAIL_VERIFICATION="none"` 为**长期既定策略**(不再考虑改回 mandatory)。注册填邮箱 + 密码即注册成功、可直接登录使用。
|
||||||
|
- 影响:
|
||||||
|
- 邮箱**仍必填且唯一**(`User.email` unique 约束不变),只是不验证。
|
||||||
|
- 注册流程**不再发验证邮件**——注册本身不再依赖 SMTP/邮件服务(找回密码等其它邮件功能如启用才需要)。
|
||||||
|
- 原假设 mandatory 的 2 条 portal 测试(`test_signup_creates_unverified_user_wallet_...`、`test_unverified_email_cannot_establish_login_session`)需**按新策略更新**,不是恢复 mandatory。
|
||||||
|
- `settings.py` 现为 `"none"#"mandatory"` 的临时写法,需清理成干净的 `"none"`。
|
||||||
|
- 防刷改由「注册限流 / 图形验证码」补位(邮箱验证不再承担这个作用)。
|
||||||
|
- 本决策为**全项目权威口径**:`02-requirements`/`05-coding-rules`/`api`/`04-architecture`/`03-tech-stack`/`routes`/`env`/`deployment`/`00-ai-start-here` 中所有「邮箱验证」表述以本决策为准,统一改为「免邮箱验证,邮箱仍唯一」,由 **T-605** 落实。
|
||||||
|
- T-603 解封路径随之改变:不再「恢复 mandatory」,而是等 T-605 更新测试后重跑标 DONE。
|
||||||
|
|
||||||
|
## 2026-07-06 发现+决策:用户端与后台共用会话 → 账号分离(方案 A)
|
||||||
|
|
||||||
|
- 发现:portal(`/`)、`/admin/`、`/api/` 挂同一域名/同一 Django 工程,共用同一个 `sessionid` cookie;`/admin/` 不是另一套登录,只是在同一登录态上加 `is_staff=True` 门槛。所以 staff/superuser 账号登了 portal,去 `/admin/` 也是登录态(观察到的「用户端登录后台跟着登」根因)。属单体复用 Django auth 的必然结果,非 bug。
|
||||||
|
- 澄清:普通用户 `is_staff=False` 登 portal **进不了后台**,无越权、无泄露。
|
||||||
|
- 决策:采用**方案 A·账号分离(零代码)**——终端用户一律 `is_staff=False`;运营用**专用 admin superuser 只登 `/admin/`,不与 portal 账号混用**。不采用「同账号双会话」(需 admin 独立子域名/独立 cookie,成本高、MVP 不值)。
|
||||||
|
- 加固(上线前,deployment):`/admin/` 加访问保护(改路径 / IP 白名单 / 反代 basic auth / 2FA),永不给终端用户 `is_staff`。
|
||||||
|
- 文档落点:`04-architecture.md`(设计事实 + 方案 A)、`05-coding-rules.md`(「用户端与后台账号分离」规则)。属**运营/编码约定,无代码改动**。
|
||||||
|
|
||||||
|
## 2026-07-06 T-604 文档方案收紧:本地 prompt 敏感词过滤
|
||||||
|
|
||||||
|
- 状态:DONE(文档先行,未改代码)。
|
||||||
|
- 背景:复核 Claude Code 提出的“提示词敏感词检查”方案后,结论为方向合理,但原方案把本地关键词、云内容安全、输出审核和图片审核混在一起,且现有 `generation.py` 接入顺序存在“先下载 image_url 再审 prompt”的实现风险。
|
||||||
|
- 变更:
|
||||||
|
- 新增 `docs/moderation.md`,作为 T-604 权威设计:T-604 只做输入 prompt 本地敏感词快筛;命中必须在扣点和调上游前拦截;云内容安全、输出审核、图片审核后续另做。
|
||||||
|
- 更新 `docs/06-tasks.md`:T-604 范围收紧为 keyword provider + `SensitiveWord` + matcher 缓存 + 共享 cache 版本号失效;明确 `ahocorapy` 只是候选依赖,编码前必须验证 PyPI 可用性和 API 形状。
|
||||||
|
- 更新 `docs/04-architecture.md`:新增内容安全层职责、`SensitiveWord` 配置数据、prompt 先审再下载图片/计费的时序,以及多 worker 下不能只依赖 Django signal。
|
||||||
|
- 更新 `docs/api.md`:补 `content_blocked` 错误码,明确命中不扣点、不写调用/流水、不调上游,也不得先下载 `image_url`。
|
||||||
|
- 更新 `docs/env.md`:补 `MODERATION_ENABLED`、`MODERATION_PROVIDER`、`MODERATION_FAIL_CLOSED`、`MODERATION_BLOCK_ON_REVIEW`、`MODERATION_CACHE_VERSION_KEY` 等配置,并说明生产依赖共享 cache。
|
||||||
|
- 更新 `docs/05-coding-rules.md` 与 `docs/README.md`:加入内容安全顺序规则和文档导航。
|
||||||
|
- 决策:
|
||||||
|
- T-604 不启用输出审核;`MODERATION_REFUND_ON_OUTPUT_BLOCK` 不作为 T-604 交付项,避免配置存在但行为未实现。
|
||||||
|
- `SensitiveWord.action` MVP 只支持 `block`,后续再扩展 `review` / `flag`。
|
||||||
|
- 词库变更后用共享 cache 版本号让各 worker 懒重建 matcher,不能只靠 `post_save` / `post_delete` signal。
|
||||||
|
- 验证:文档修改;未运行代码测试。
|
||||||
|
- 下一步:实现 T-604 时先清理 `apps/moderation/__pycache__`,把 `apps.moderation` 接入 `INSTALLED_APPS`,再按 `docs/moderation.md` 落模型、admin、provider、缓存和测试。
|
||||||
|
|
||||||
|
## 2026-07-06 复核:codex 更新的 T-604 敏感词方案(Claude Code review,非任务)
|
||||||
|
|
||||||
|
- 对象:codex 审核并扩写的中文 prompt 敏感词方案(新增 `docs/moderation.md`,更新 `04-architecture`/`api`/`05-coding-rules`/`06-tasks`/`env`/`README`)。
|
||||||
|
- 结论:**合理,且是明显改进,可放行**。
|
||||||
|
- codex 改对/改好:
|
||||||
|
- 范围收紧为「只审 prompt 文本」,砍掉 skeleton 里对输入图片做关键词匹配(无意义)。
|
||||||
|
- 请求时序:审 prompt 在下载 image 之前,命中即不下载图片/不别名/不计费/不调上游。
|
||||||
|
- 多 worker 缓存失效用共享 cache 版本号(`moderation:sensitive_words:version`),点破 post_save signal 只在当前进程生效——与 T-403 限流共享 cache 口径一致。
|
||||||
|
- 「ahocorapy 编码前必须验证 PyPI 可用性和 API 形状」——先核实真实 API 的纪律。
|
||||||
|
- 诚实标注归一化误伤风险并要求正常文本测试;不暴露未实现的输出审核配置。
|
||||||
|
- 实现时须盯的一点(skeleton 与新规格冲突,非文档错):现有未提交的 `generation.py` skeleton 是「先 load_image_input 再 moderate_input(含 image)」+ 有 output 钩子;T-604 实现必须按新规格翻正——① prompt 审核挪到图片下载之前;② moderate_input 改纯 prompt;③ 去掉/休眠 output 钩子与未实现的退点配置。
|
||||||
|
- 小注意(codex 已自标、可接受):归一化误伤需正常文本测试兜底;每请求一次共享 cache 版本检查(DatabaseCache 为一次 DB 读)。
|
||||||
|
|||||||
Reference in New Issue
Block a user