test: add recorded ai generation smoke
This commit is contained in:
@@ -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,
|
||||||
|
)
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 管理、个人中心/记录页、充值页。
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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`。
|
||||||
|
|
||||||
## 维护规则
|
## 维护规则
|
||||||
|
|||||||
@@ -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`。*
|
||||||
|
|||||||
@@ -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
@@ -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 模型。
|
||||||
|
|||||||
Reference in New Issue
Block a user