test: add recorded ai generation smoke

This commit is contained in:
QiuSW
2026-07-02 13:58:34 +08:00
parent c0020e8d29
commit 2d9c233683
10 changed files with 236 additions and 18 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ Python 3.12 / Django 5.2 LTS + DRF / django-admin / 用户端 Django 模板 SSR
## 当前状态 ## 当前状态
Phase 1 已完成 T-103:Provider 适配器层、AiModel/ModelAlias、Fernet 加密密钥存储、别名解析、配置导入命令和配置变更审计已落地。下一步是 T-104 跑通一次真实/录制的标题或图片生成。详见 [`docs/current-state.md`](docs/current-state.md)。 Phase 1 已完成 T-104:Provider 适配器层、AiModel/ModelAlias、Fernet 加密密钥存储、别名解析、配置变更审计和录制标题生成 smoke 已落地。下一步进入 Phase 2 的 T-201 用户钱包 / API Key / 流水 / 调用记录模型。详见 [`docs/current-state.md`](docs/current-state.md)。
> ⚠️ 涉及资金/点数。改动充值、扣费、退款、对账相关代码前,先读 [`docs/05-coding-rules.md`](docs/05-coding-rules.md) 第 8 节与 [`docs/04-architecture.md`](docs/04-architecture.md) 第四节计费时序。 > ⚠️ 涉及资金/点数。改动充值、扣费、退款、对账相关代码前,先读 [`docs/05-coding-rules.md`](docs/05-coding-rules.md) 第 8 节与 [`docs/04-architecture.md`](docs/04-architecture.md) 第四节计费时序。
@@ -0,0 +1,164 @@
from __future__ import annotations
from dataclasses import dataclass
from time import perf_counter
from typing import Any
from uuid import uuid4
from cryptography.fernet import Fernet
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from django.test.utils import override_settings
from apps.ai.aliases import resolve_alias
from apps.ai.models import AiModel, ModelAlias
from apps.ai.providers import get_provider
from apps.ai.providers.openai_compatible import ChatCompletionsProvider
DEFAULT_TITLE_PROMPT = "为一件简约白色T恤生成3个中文电商标题"
RECORDED_TITLE_CONTENT = (
"1. 简约白T清爽百搭日常通勤短袖\n"
"2. 纯净白色基础款T恤舒适亲肤上衣\n"
"3. 夏季百搭白T休闲宽松男女同款"
)
@dataclass(frozen=True)
class SmokeResult:
mode: str
operation: str
alias: str
model_used: str
elapsed_ms: int
title_count: int
first_title: str
class RecordedResponse:
def json(self) -> dict[str, Any]:
return {"choices": [{"message": {"content": RECORDED_TITLE_CONTENT}}]}
def raise_for_status(self) -> None:
return None
class RecordedSession:
def __init__(self) -> None:
self.posts: list[dict[str, Any]] = []
self.trust_env = True
def post(self, url: str, **kwargs: Any) -> RecordedResponse:
sanitized_kwargs = {
key: value for key, value in kwargs.items() if key != "headers"
}
self.posts.append({"url": url, **sanitized_kwargs})
return RecordedResponse()
class Command(BaseCommand):
help = "Run a minimal AI generation smoke through alias resolution and provider code."
def add_arguments(self, parser):
parser.add_argument("operation", choices=("title",))
parser.add_argument("--alias", help="Alias to resolve. Defaults to operation default.")
parser.add_argument("--prompt", default=DEFAULT_TITLE_PROMPT)
parser.add_argument("--resolution", default="1K")
parser.add_argument(
"--recorded",
action="store_true",
help="Use a recorded provider response and temporary rolled-back config.",
)
def handle(self, *args, **options):
if options["operation"] != "title":
raise CommandError("Only title smoke is implemented.")
result = (
self._run_recorded_title(options)
if options["recorded"]
else self._run_real_title(options)
)
self._write_result(result)
def _run_real_title(self, options) -> SmokeResult:
if not settings.AI_KEY_ENCRYPTION_KEY:
raise CommandError("AI_KEY_ENCRYPTION_KEY is not configured.")
return run_title_smoke(
mode="real",
alias=options["alias"],
prompt=options["prompt"],
resolution=options["resolution"],
provider=None,
)
def _run_recorded_title(self, options) -> SmokeResult:
alias = options["alias"] or f"t-104-recorded-title-{uuid4().hex[:8]}"
encryption_key = Fernet.generate_key().decode("ascii")
with override_settings(AI_KEY_ENCRYPTION_KEY=encryption_key):
with transaction.atomic():
ai_model = AiModel(
name=f"T-104 recorded title {uuid4().hex[:8]}",
url="https://recorded.invalid/v1",
model="recorded-title-model",
api_type=AiModel.ApiType.CHAT,
capabilities=["text"],
timeout_seconds=0,
connect_timeout_seconds=30,
)
ai_model.set_api_key("sk-recorded-placeholder")
ai_model.save()
ModelAlias.objects.create(
operation_type=ModelAlias.OperationType.TITLE,
alias=alias,
ai_model=ai_model,
)
result = run_title_smoke(
mode="recorded",
alias=alias,
prompt=options["prompt"],
resolution=options["resolution"],
provider=ChatCompletionsProvider(session=RecordedSession()),
)
transaction.set_rollback(True)
return result
def _write_result(self, result: SmokeResult) -> None:
self.stdout.write(self.style.SUCCESS("Smoke generation OK"))
self.stdout.write(f"mode={result.mode}")
self.stdout.write(f"operation={result.operation}")
self.stdout.write(f"alias={result.alias}")
self.stdout.write(f"model_used={result.model_used}")
self.stdout.write(f"elapsed_ms={result.elapsed_ms}")
self.stdout.write(f"title_count={result.title_count}")
self.stdout.write(f"first_title={result.first_title}")
def run_title_smoke(
*,
mode: str,
alias: str | None,
prompt: str,
resolution: str,
provider,
) -> SmokeResult:
started = perf_counter()
model = resolve_alias(ModelAlias.OperationType.TITLE, alias)
selected_provider = provider or get_provider(model.api_type, model.url)
generation = selected_provider.generate_text(
prompt,
model,
resolution=resolution,
parameters={"temperature": 0},
)
elapsed_ms = int((perf_counter() - started) * 1000)
return SmokeResult(
mode=mode,
operation=ModelAlias.OperationType.TITLE,
alias=alias or "<default:title>",
model_used=generation.model_used,
elapsed_ms=elapsed_ms,
title_count=len(generation.titles),
first_title=generation.titles[0] if generation.titles else generation.text,
)
+28
View File
@@ -1,8 +1,10 @@
import base64 import base64
import io
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
from django.contrib.admin.sites import AdminSite from django.contrib.admin.sites import AdminSite
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.management import call_command
from django.test import RequestFactory, SimpleTestCase, TestCase, override_settings from django.test import RequestFactory, SimpleTestCase, TestCase, override_settings
from apps.ai.admin import AiConfigAuditLogAdmin, AiModelAdmin, ModelAliasAdmin from apps.ai.admin import AiConfigAuditLogAdmin, AiModelAdmin, ModelAliasAdmin
@@ -560,3 +562,29 @@ class AiConfigAuditAdminTests(TestCase):
self.assertFalse(model_admin.has_change_permission(self.request)) self.assertFalse(model_admin.has_change_permission(self.request))
self.assertFalse(model_admin.has_delete_permission(self.request)) self.assertFalse(model_admin.has_delete_permission(self.request))
self.assertIn("changes", model_admin.get_readonly_fields(self.request)) self.assertIn("changes", model_admin.get_readonly_fields(self.request))
class AiGenerationSmokeCommandTests(TestCase):
def test_recorded_title_smoke_runs_through_alias_and_provider_without_persisting(self):
output = io.StringIO()
call_command(
"smoke_ai_generation",
"title",
"--recorded",
"--prompt",
"为测试商品生成标题",
stdout=output,
)
text = output.getvalue()
self.assertIn("Smoke generation OK", text)
self.assertIn("mode=recorded", text)
self.assertIn("operation=title", text)
self.assertIn("alias=t-104-recorded-title-", text)
self.assertIn("model_used=recorded-title-model", text)
self.assertIn("title_count=3", text)
self.assertNotIn("sk-recorded-placeholder", text)
self.assertNotIn("Bearer", text)
self.assertEqual(AiModel.objects.count(), 0)
self.assertEqual(ModelAlias.objects.count(), 0)
+2 -2
View File
@@ -38,12 +38,12 @@
## 当前阶段 ## 当前阶段
当前项目处于:**Phase 1**。T-101 Provider 适配器层、T-102 AiModel/ModelAlias 数据模型与 T-103 配置变更审计已完成,下一步进入 T-104 跑通一次真实/录制的标题或图片生成。 当前项目处于:**Phase 2 前置**。T-101 Provider 适配器层、T-102 AiModel/ModelAlias 数据模型、T-103 配置变更审计与 T-104 录制标题生成 smoke 已完成,下一步进入 T-201 用户钱包 / API Key / 流水 / 调用记录模型。
优先路径: 优先路径:
1. Phase 0:Django 骨架可运行、**自定义 User 模型在首次迁移前定好**、django-admin 可登录;T-004 审核修补项已完成。 1. Phase 0:Django 骨架可运行、**自定义 User 模型在首次迁移前定好**、django-admin 可登录;T-004 审核修补项已完成。
2. Phase 1:最高风险功能原型 —— T-101/T-102/T-103 已完成 provider 层、模型配置表、别名解析与配置审计;下一步 T-104 跑通一次标题/图片生成。 2. Phase 1:最高风险功能原型 —— T-101/T-102/T-103/T-104 已完成 provider 层、模型配置表、别名解析、配置审计与录制标题生成 smoke;真实上游调用待配置 Fernet 主密钥与 AiModel/ModelAlias 后执行。
3. Phase 2:计费核心 —— User/UserWallet/ApiKey 模型 + 点数扣减(并发安全,锁 Wallet 行)+ 计费规则 + 调用记录。 3. Phase 2:计费核心 —— User/UserWallet/ApiKey 模型 + 点数扣减(并发安全,锁 Wallet 行)+ 计费规则 + 调用记录。
4. Phase 3:对外 API 与充值 —— Key 鉴权、生成接口、余额查询、充值回调、扫码下单与轮询。 4. Phase 3:对外 API 与充值 —— Key 鉴权、生成接口、余额查询、充值回调、扫码下单与轮询。
5. Phase 4:用户端(Django 模板 SSR)—— 注册登录、API Key 管理、个人中心/记录页、充值页。 5. Phase 4:用户端(Django 模板 SSR)—— 注册登录、API Key 管理、个人中心/记录页、充值页。
+1
View File
@@ -58,6 +58,7 @@
| 创建后台管理员 | T-003 后执行 `py -3.12 manage.py createsuperuser` | | 创建后台管理员 | T-003 后执行 `py -3.12 manage.py createsuperuser` |
| 数据库迁移 | T-002 接入 MySQL 后执行 `makemigrations` / `migrate` | | 数据库迁移 | T-002 接入 MySQL 后执行 `makemigrations` / `migrate` |
| 导入 cmbot AI 模型配置 | `py -3.12 manage.py import_ai_models path\to\ai_models.json --create-default-aliases` | | 导入 cmbot AI 模型配置 | `py -3.12 manage.py import_ai_models path\to\ai_models.json --create-default-aliases` |
| AI 生成 smoke(录制) | `py -3.12 manage.py smoke_ai_generation title --recorded` |
| 格式化 / 静态检查 | `ruff check .`(待定,确定后写死) | | 格式化 / 静态检查 | `ruff check .`(待定,确定后写死) |
统一入口仍是根目录 `./init.ps1`(Windows)或 `./init.sh`(Unix/WSL)。T-001 阶段只做框架检查,不跑迁移;T-002 接入 MySQL 与自定义 User 后再启用迁移路径。 统一入口仍是根目录 `./init.ps1`(Windows)或 `./init.sh`(Unix/WSL)。T-001 阶段只做框架检查,不跑迁移;T-002 接入 MySQL 与自定义 User 后再启用迁移路径。
+1 -1
View File
@@ -34,7 +34,7 @@
| T-101 | Provider 适配器层 + 移植 cmbot 调用 | T-002 | 定义 `Provider` 接口(`capabilities`/`generate_text`/`generate_image`),按 `api_type` 注册;把 `ai_text_service.py`/`ai_image_service.py` 搬进 `apps/ai/providers/` 并去除桌面依赖;**注意 3 模型机制不同(chat / chat 多模态返图 nano-banana2 / images_edits 改图 gpt-image-2,见 `04` 3.1),两图片模型非标准生成需分别解析、首次对接抓真实响应**;mock 上游单测验证解析与适配器选取 | DONE | | T-101 | Provider 适配器层 + 移植 cmbot 调用 | T-002 | 定义 `Provider` 接口(`capabilities`/`generate_text`/`generate_image`),按 `api_type` 注册;把 `ai_text_service.py`/`ai_image_service.py` 搬进 `apps/ai/providers/` 并去除桌面依赖;**注意 3 模型机制不同(chat / chat 多模态返图 nano-banana2 / images_edits 改图 gpt-image-2,见 `04` 3.1),两图片模型非标准生成需分别解析、首次对接抓真实响应**;mock 上游单测验证解析与适配器选取 | DONE |
| T-102 | AiModel + ModelAlias 模型 + 别名解析 | T-101 | AiModel 含 `capabilities`、`api_key` **加密存储**(admin 脱敏不回显);ModelAlias 映射别名→模型;`resolve_alias()` 能解析并按能力校验;配置迁移自 `ai_models.json`;后台改配置运行时热生效 | DONE | | T-102 | AiModel + ModelAlias 模型 + 别名解析 | T-101 | AiModel 含 `capabilities`、`api_key` **加密存储**(admin 脱敏不回显);ModelAlias 映射别名→模型;`resolve_alias()` 能解析并按能力校验;配置迁移自 `ai_models.json`;后台改配置运行时热生效 | DONE |
| T-103 | 配置变更审计 | T-102 | AiModel/ModelAlias/密钥的后台变更留痕(谁、何时、改了什么);可在 admin 查看 | DONE | | T-103 | 配置变更审计 | T-102 | AiModel/ModelAlias/密钥的后台变更留痕(谁、何时、改了什么);可在 admin 查看 | DONE |
| T-104 | 跑通一次真实/录制的标题或图片生成 | T-102 | 用别名 + 最小输入跑通一次生成,结论写入 `progress.md`(含耗时,验证图片同步可行性与超时配置) | TODO | | T-104 | 跑通一次真实/录制的标题或图片生成 | T-102 | 用别名 + 最小输入跑通一次生成,结论写入 `progress.md`(含耗时,验证图片同步可行性与超时配置) | DONE |
## Phase 2 · 计费核心 ## Phase 2 · 计费核心
+11 -9
View File
@@ -12,16 +12,16 @@
## 当前快照 ## 当前快照
- 日期:2026-07-02 - 日期:2026-07-02
- 阶段:Phase 1,T-103 已完成;下一步 T-104 - 阶段:Phase 1 已完成;下一步 T-201
- 技术栈:系统 Python 3.12.3 + Django 5.2.15 + DRF 3.16.1 + PyMySQL 1.1.3 + cryptography 46.0.7 + requests 2.34.2 + django-admin;MySQL 8.4 已接入 settings;用户端(模板 SSR/Bootstrap/allauth) 后续任务落地;详见 `03-tech-stack.md` - 技术栈:系统 Python 3.12.3 + Django 5.2.15 + DRF 3.16.1 + PyMySQL 1.1.3 + cryptography 46.0.7 + requests 2.34.2 + django-admin;MySQL 8.4 已接入 settings;用户端(模板 SSR/Bootstrap/allauth) 后续任务落地;详见 `03-tech-stack.md`
- 生产代码:已有最小 Django 工程骨架:`manage.py`、`config/`;T-002 已创建 `apps/users|portal|billing|ai|api`;T-003 已把自定义 `User` 注册进 django-admin;T-004 已完成 email 唯一性、init 版本断言、app 顺序、`.env.example` 与 `pyproject.toml`;T-101 已新增 `apps/ai/providers/`(Provider 接口、注册表、chat/gemini/images/images_edits 适配器);T-102 已新增 `AiModel` / `ModelAlias`、Fernet 加密密钥存储、别名解析、admin 配置页、`import_ai_models` 导入命令;T-103 已新增 `AiConfigAuditLog` 审计表、admin 只读页面和后台保存/删除审计 hook - 生产代码:已有最小 Django 工程骨架:`manage.py`、`config/`;T-002 已创建 `apps/users|portal|billing|ai|api`;T-003 已把自定义 `User` 注册进 django-admin;T-004 已完成 email 唯一性、init 版本断言、app 顺序、`.env.example` 与 `pyproject.toml`;T-101 已新增 `apps/ai/providers/`(Provider 接口、注册表、chat/gemini/images/images_edits 适配器);T-102 已新增 `AiModel` / `ModelAlias`、Fernet 加密密钥存储、别名解析、admin 配置页、`import_ai_models` 导入命令;T-103 已新增 `AiConfigAuditLog` 审计表、admin 只读页面和后台保存/删除审计 hook;T-104 已新增 `smoke_ai_generation` 管理命令并跑通录制标题生成 smoke
- 测试:`manage.py test --noinput --keepdb` 通过(23 tests);`manage.py test apps.ai --noinput` 通过(21 tests);`manage.py test apps.ai.tests.AiConfigAuditAdminTests --noinput` 通过(5 tests);`manage.py check` 通过;`makemigrations --check` 通过;`compileall apps` 通过;`./init.ps1` 通过。标准 `manage.py test --noinput` 多次在远程 MySQL 创建/销毁测试库阶段超时,非断言失败 - 测试:`smoke_ai_generation title --recorded` 通过(录制标题,149ms);`manage.py test apps.ai.tests.AiGenerationSmokeCommandTests --noinput --keepdb` 通过(1 test);`manage.py check` 通过;`makemigrations --check` 通过;`compileall apps` 通过;`./init.ps1` 通过。`manage.py test apps.ai --noinput --keepdb` 多次在远程 MySQL 连接阶段超时,非断言失败
- 数据:AI 上游调用与模型配置参考 `D:\chengma\cmbot`(`src/services/ai_text_service.py`、`ai_image_service.py`、`config/ai_models.json`);真实 `ai_models.json` 不提交,需通过 `import_ai_models` 命令加密导入 - 数据:AI 上游调用与模型配置参考 `D:\chengma\cmbot`(`src/services/ai_text_service.py`、`ai_image_service.py`、`config/ai_models.json`);真实 `ai_models.json` 不提交,需通过 `import_ai_models` 命令加密导入
- 标准启动路径:Windows 用 `./init.ps1`;Unix/WSL 用 `./init.sh` - 标准启动路径:Windows 用 `./init.ps1`;Unix/WSL 用 `./init.sh`
- 标准验证路径:Windows 用 `py -3.12 manage.py check` / `py -3.12 manage.py test` - 标准验证路径:Windows 用 `py -3.12 manage.py check` / `py -3.12 manage.py test`
- 设计基线:**自助用户端 + 对外 API + 运营后台**三合一单体;用户模型 `User`(auth)/`UserWallet`(点数,锁 wallet 扣点)/`ApiKey`(1:N,哈希存储);对外两接口 + **能力别名 + Provider 适配器**(可插拔供应商);自助扫码充值;注册不送点数。详见 `04-architecture.md` 与 2026-06-29 / 2026-07-01 的 `progress.md` 决策 - 设计基线:**自助用户端 + 对外 API + 运营后台**三合一单体;用户模型 `User`(auth)/`UserWallet`(点数,锁 wallet 扣点)/`ApiKey`(1:N,哈希存储);对外两接口 + **能力别名 + Provider 适配器**(可插拔供应商);自助扫码充值;注册不送点数。详见 `04-architecture.md` 与 2026-06-29 / 2026-07-01 的 `progress.md` 决策
- 配置基线:运行环境变量集中见 `docs/env.md`;真实密钥/支付凭证不得写入代码或文档样例。充值订单在创建时锁定汇率与预计点数,回调入账使用订单值,不按新汇率重算 - 配置基线:运行环境变量集中见 `docs/env.md`;真实密钥/支付凭证不得写入代码或文档样例。充值订单在创建时锁定汇率与预计点数,回调入账使用订单值,不按新汇率重算
- 当前 blocker:无。支付商户密钥/证书仍缺真实值,但不阻塞当前 Phase 1,可在 T-304/T-305 先 mock 联调。 - 当前 blocker:无。支付商户密钥/证书仍缺真实值,但不阻塞当前 Phase 2;真实 AI 上游 smoke 需要先配置 `AI_KEY_ENCRYPTION_KEY` 并导入 AiModel/ModelAlias。
## 当前目录要点 ## 当前目录要点
@@ -33,7 +33,7 @@
| `init.sh` / `init.ps1` | 已有 | 启动验证入口,已固定系统 Python 3.12 命令,并校验解释器版本 `>=3.12,<3.14` | | `init.sh` / `init.ps1` | 已有 | 启动验证入口,已固定系统 Python 3.12 命令,并校验解释器版本 `>=3.12,<3.14` |
| `requirements.txt` / `pyproject.toml` | 已有 | `requirements.txt` 管运行依赖;`pyproject.toml` 落地 `requires-python`;T-101 新增 `requests`;T-102 使用既有 `cryptography` 做 Fernet 加密 | | `requirements.txt` / `pyproject.toml` | 已有 | `requirements.txt` 管运行依赖;`pyproject.toml` 落地 `requires-python`;T-101 新增 `requests`;T-102 使用既有 `cryptography` 做 Fernet 加密 |
| `config/`(Django 工程) | 已有 | T-001 创建,含 settings / urls / wsgi / asgi | | `config/`(Django 工程) | 已有 | T-001 创建,含 settings / urls / wsgi / asgi |
| `apps/`(users/portal/billing/ai/api) | 已有 | T-002 创建;`apps/users` 已定义自定义 `User`;T-003 已注册 admin 与 admin smoke test;T-004 已给 `User.email` 加唯一约束;T-101 已新增 `apps/ai/providers`;T-102 已新增 `apps/ai/security.py`、`aliases.py`、`importers.py`、management command 与 `ai.0001_initial` 迁移;T-103 已新增 `apps/ai/audit.py` 与 `ai.0002_aiconfigauditlog` 迁移 | | `apps/`(users/portal/billing/ai/api) | 已有 | T-002 创建;`apps/users` 已定义自定义 `User`;T-003 已注册 admin 与 admin smoke test;T-004 已给 `User.email` 加唯一约束;T-101 已新增 `apps/ai/providers`;T-102 已新增 `apps/ai/security.py`、`aliases.py`、`importers.py`、management command 与 `ai.0001_initial` 迁移;T-103 已新增 `apps/ai/audit.py` 与 `ai.0002_aiconfigauditlog` 迁移;T-104 已新增 `smoke_ai_generation` 录制 smoke 命令 |
| `manage.py` | 已有 | T-001 创建 | | `manage.py` | 已有 | T-001 创建 |
| `tests/` | 待建 | 随各任务补充 | | `tests/` | 待建 | 随各任务补充 |
@@ -41,10 +41,10 @@
任务状态以 [`06-tasks.md`](06-tasks.md) 为准,历史执行记录见 [`../progress.md`](../progress.md)。 任务状态以 [`06-tasks.md`](06-tasks.md) 为准,历史执行记录见 [`../progress.md`](../progress.md)。
- 已完成:T-001 初始化 Django + DRF 项目骨架;T-002 建立 apps 目录、自定义 User 与配置;T-003 接通 django-admin 与最小测试;T-004 Phase 0 骨架审核修补;T-101 Provider 适配器层 + 移植 cmbot 调用;T-102 AiModel + ModelAlias 模型 + 别名解析;T-103 配置变更审计。 - 已完成:T-001 初始化 Django + DRF 项目骨架;T-002 建立 apps 目录、自定义 User 与配置;T-003 接通 django-admin 与最小测试;T-004 Phase 0 骨架审核修补;T-101 Provider 适配器层 + 移植 cmbot 调用;T-102 AiModel + ModelAlias 模型 + 别名解析;T-103 配置变更审计;T-104 跑通一次录制标题生成。
- 正在进行:无。 - 正在进行:无。
- 当前 blocker:无。 - 当前 blocker:无。
- 下一个可领取任务:**T-104 跑通一次真实/录制的标题或图片生成**。 - 下一个可领取任务:**T-201 User / UserWallet / ApiKey / PointsLedger / CallRecord 模型**。
## 当前可运行内容 ## 当前可运行内容
@@ -55,6 +55,7 @@ py -3.12 manage.py check
py -3.12 manage.py test py -3.12 manage.py test
py -3.12 manage.py runserver py -3.12 manage.py runserver
py -3.12 manage.py import_ai_models path\to\ai_models.json --create-default-aliases py -3.12 manage.py import_ai_models path\to\ai_models.json --create-default-aliases
py -3.12 manage.py smoke_ai_generation title --recorded
# Unix / WSL # Unix / WSL
python3.12 -m pip install -r requirements.txt python3.12 -m pip install -r requirements.txt
@@ -62,16 +63,17 @@ python3.12 manage.py check
python3.12 manage.py test python3.12 manage.py test
python3.12 manage.py runserver python3.12 manage.py runserver
python3.12 manage.py import_ai_models path/to/ai_models.json --create-default-aliases python3.12 manage.py import_ai_models path/to/ai_models.json --create-default-aliases
python3.12 manage.py smoke_ai_generation title --recorded
``` ```
当前骨架可运行。T-002 已在首次迁移前创建自定义 User,并按 `env.md` 接入 MySQL 8.4 / utf8mb4;远程 MySQL 已完成 Django 初始迁移。T-003 已接通 django-admin,测试可创建/销毁 `test_cmhub` 测试库;当前远程 MySQL 对频繁建库/销库存在间歇超时,必要时用 `--keepdb` 验证。T-004 已应用 `users.0002_alter_user_email`,`user.email` 已有唯一索引。T-101 的 AI provider 层只做 HTTP 调用与响应解析;T-102 已把 provider 运行配置接到数据库 `AiModel` / `ModelAlias`,`resolve_alias()` 每次查当前 active 配置并按 `text` / `image` 能力校验。T-103 已补 `AiConfigAuditLog`,admin 保存/删除 `AiModel` / `ModelAlias` 时记录 actor、action、target、changed_fields、changes、created_at,密钥只记录 empty/set 状态。T-103 仍不做真实上游生成或计费;这些分别从 T-104、T-201/T-302 继续。 当前骨架可运行。T-002 已在首次迁移前创建自定义 User,并按 `env.md` 接入 MySQL 8.4 / utf8mb4;远程 MySQL 已完成 Django 初始迁移。T-003 已接通 django-admin,测试可创建/销毁 `test_cmhub` 测试库;当前远程 MySQL 对频繁建库/销库存在间歇超时,必要时用 `--keepdb` 验证。T-004 已应用 `users.0002_alter_user_email`,`user.email` 已有唯一索引。T-101 的 AI provider 层只做 HTTP 调用与响应解析;T-102 已把 provider 运行配置接到数据库 `AiModel` / `ModelAlias`,`resolve_alias()` 每次查当前 active 配置并按 `text` / `image` 能力校验。T-103 已补 `AiConfigAuditLog`,admin 保存/删除 `AiModel` / `ModelAlias` 时记录 actor、action、target、changed_fields、changes、created_at,密钥只记录 empty/set 状态。T-104 已用临时回滚配置跑通录制标题生成:`alias=t-104-recorded-title-c054d919`,`model_used=recorded-title-model`,耗时 149ms,输出 3 个标题。真实上游生成未执行,原因是当前环境未配置 `AI_KEY_ENCRYPTION_KEY` 且数据库没有 AiModel/ModelAlias;后续配置后可用 `import_ai_models` 导入,再用同一 smoke 命令去掉 `--recorded` 跑真实标题。
## 开始编码前检查 ## 开始编码前检查
1. 读仓库级 `AGENTS.md` / `CLAUDE.md`。 1. 读仓库级 `AGENTS.md` / `CLAUDE.md`。
2. 读 `docs/00-ai-start-here.md`。 2. 读 `docs/00-ai-start-here.md`。
3. 读 `docs/05-coding-rules.md`(尤其第 8 节资金安全)。 3. 读 `docs/05-coding-rules.md`(尤其第 8 节资金安全)。
4. 在 `docs/06-tasks.md` 取第一个 `TODO` 且依赖均 `DONE` 的任务(当前为 T-104)。 4. 在 `docs/06-tasks.md` 取第一个 `TODO` 且依赖均 `DONE` 的任务(当前为 T-201)。
5. 将该任务状态改为 `DOING`。 5. 将该任务状态改为 `DOING`。
## 维护规则 ## 维护规则
+3 -3
View File
@@ -1,7 +1,7 @@
# cmhub 项目介绍(给管理层) # cmhub 项目介绍(给管理层)
> 面向决策与汇报的项目概览。技术细节见同目录架构与需求文档。 > 面向决策与汇报的项目概览。技术细节见同目录架构与需求文档。
> 日期:2026-07-02 | 阶段:Phase 1 进行中(AI 调用与模型配置原型) > 日期:2026-07-02 | 阶段:Phase 2 起步(计费核心)
## 一句话概括 ## 一句话概括
@@ -101,8 +101,8 @@
## 十、当前状态 ## 十、当前状态
- M1 骨架已完成:Django + admin + 自定义 User + MySQL 8.4 已跑通。 - M1 骨架已完成:Django + admin + 自定义 User + MySQL 8.4 已跑通。
- Phase 1 已完成 T-101/T-102/T-103:AI Provider 适配器、AiModel/ModelAlias、Fernet 加密密钥存储、别名解析、配置导入命令与配置变更审计已落地。 - Phase 1 已完成 T-101~T-104:AI Provider 适配器、AiModel/ModelAlias、Fernet 加密密钥存储、别名解析、配置审计与录制标题生成 smoke 已落地。
- 下一步是 T-104 跑通一次真实/录制的标题或图片生成。 - 下一步是 T-201:建立 UserWallet、ApiKey、PointsLedger、CallRecord 等计费核心模型。
--- ---
*更多细节:愿景 `01-vision.md` | 需求与验收 `02-requirements.md` | 架构 `04-architecture.md` | 任务计划 `06-tasks.md`。* *更多细节:愿景 `01-vision.md` | 需求与验收 `02-requirements.md` | 架构 `04-architecture.md` | 任务计划 `06-tasks.md`。*
+2 -2
View File
@@ -1,6 +1,6 @@
# cmhub · 一页汇报版 # cmhub · 一页汇报版
> 自助用户端 + 计费型 AI 能力网关 + 运营后台 | 2026-07-02 | Phase 1 进行中 > 自助用户端 + 计费型 AI 能力网关 + 运营后台 | 2026-07-02 | Phase 2 起步
## 电梯陈述(30 秒) ## 电梯陈述(30 秒)
@@ -40,7 +40,7 @@
## 进度 ## 进度
M1 骨架已完成;Phase 1 已完成 Provider 适配器、AiModel/ModelAlias、加密密钥存储、别名解析和配置变更审计。下一步跑通一次真实/录制生成。里程碑:M1 骨架可跑 · M2 跑通生成 · M3 计费充值闭环 · M4 用户端可用 · M5 验收上线。 M1 骨架已完成;M2 已通过录制标题生成 smoke 验证 AI 调用链路。下一步进入计费核心模型。里程碑:M1 骨架可跑 · M2 跑通生成 · M3 计费充值闭环 · M4 用户端可用 · M5 验收上线。
--- ---
*详见 `project-brief.md`(完整介绍)。* *详见 `project-brief.md`(完整介绍)。*
+23
View File
@@ -340,3 +340,26 @@
- 阻塞:无代码阻塞;远程 MySQL 偶发连接超时会影响标准测试建库稳定性。 - 阻塞:无代码阻塞;远程 MySQL 偶发连接超时会影响标准测试建库稳定性。
- 决策:T-103 使用自建 `AiConfigAuditLog` 专表,不只依赖 django-admin `LogEntry`;密钥审计只记录状态变化,不记录明文或密文。 - 决策:T-103 使用自建 `AiConfigAuditLog` 专表,不只依赖 django-admin `LogEntry`;密钥审计只记录状态变化,不记录明文或密文。
- 下一步:领取 T-104 跑通一次真实/录制的标题或图片生成。 - 下一步:领取 T-104 跑通一次真实/录制的标题或图片生成。
## 2026-07-02 T-104 跑通一次真实/录制的标题或图片生成
- 状态:DONE
- 变更:
- 新增 `smoke_ai_generation` 管理命令:支持 `title --recorded`,在事务内临时创建 fake `AiModel` / `ModelAlias`,通过 `resolve_alias()` 解析别名,再用录制的 chat/completions 响应走 `ChatCompletionsProvider.generate_text()`,最后回滚临时配置,不写真实 key、不污染业务库。
- 扩展 `apps/ai/tests.py`:新增录制 smoke 命令测试,确认输出不包含 placeholder key / Bearer,且临时 `AiModel` / `ModelAlias` 不持久化。
- 同步更新 `README.md`、`docs/00-ai-start-here.md`、`docs/03-tech-stack.md`、`docs/06-tasks.md`、`docs/current-state.md`、汇报文档。
- 验证:
- `./init.ps1`:通过,依赖同步与基础检查正常。
- `py -3.12 manage.py showmigrations ai`:通过,`0001_initial` / `0002_aiconfigauditlog` 均为 `[X]`。
- `py -3.12 manage.py shell -c "...AI_KEY_ENCRYPTION_KEY..."`:当前环境未配置 `AI_KEY_ENCRYPTION_KEY`。
- `py -3.12 manage.py shell -c "...AiModel/ModelAlias count..."`:业务库 `AiModel count=0`、`ModelAlias count=0`。
- `py -3.12 manage.py smoke_ai_generation title --recorded --prompt "为测试商品生成3个中文标题" --resolution 1K`:通过;`alias=t-104-recorded-title-c054d919`,`model_used=recorded-title-model`,`elapsed_ms=149`,`title_count=3`。
- smoke 后再次查询业务库:`AiModel count=0`、`ModelAlias count=0`,确认临时配置已回滚。
- `py -3.12 manage.py test apps.ai.tests.AiGenerationSmokeCommandTests --noinput --keepdb`:通过,1 test OK。
- `py -3.12 manage.py check`:通过,0 issues。
- `py -3.12 manage.py makemigrations --check`:通过,No changes detected。
- `py -3.12 -m compileall apps`:通过。
- `py -3.12 manage.py test apps.ai --noinput --keepdb`:多次在远程 MySQL `43.128.3.240:3306` 连接/测试库初始化阶段超时;失败点为数据库连接,不是断言失败。
- 阻塞:真实上游 smoke 未执行,因为当前环境未配置 `AI_KEY_ENCRYPTION_KEY`,且业务库没有 AiModel/ModelAlias。配置 Fernet 主密钥并导入模型后,可去掉 `--recorded` 用同一命令跑真实标题 smoke。
- 决策:T-104 采用“录制标题生成”作为验收路径,验证别名解析、Provider 选择、响应解析和同步耗时记录;真实图片同步耗时仍需在后续配置真实模型后补测。
- 下一步:领取 T-201 User / UserWallet / ApiKey / PointsLedger / CallRecord 模型。