diff --git a/README.md b/README.md
index 16bbe12..2018cc3 100644
--- a/README.md
+++ b/README.md
@@ -4,8 +4,8 @@
## 它做什么
-- 用户端(自助):注册/登录、扫码充值、查看充值记录/剩余点数/消费记录、生成与删除 API Key(注册不送免费点数)。
-- 对外提供 HTTP API:生成标题、生成图片(同步返回)、查询点数余额。
+- 用户端(自助):公开首页、注册/登录、扫码充值、查看充值记录/剩余点数/消费记录、查看可用模型、生成与删除 API Key、下载桌面端(注册不送免费点数)。
+- 对外提供 HTTP API:生成标题、生成图片(同步返回)、查询点数余额、查询可用能力别名。
- 按「操作类型 + 能力别名(+ 可选分辨率)」计费,调用消耗不同点数;余额不足返回「点数不足,请先充值」。
- **预付费点数模型**:用户在外部支付系统充值,付款成功由支付系统回调本服务,按汇率把金额转成点数存在本地;之后调用直接扣本地点数,扣费与上游解耦、低延迟。
- django-admin 运营后台:管理注册用户、点数余额、计费规则、充值订单、点数流水、调用记录。
@@ -25,7 +25,7 @@ Python 3.12 / Django 5.2 LTS + DRF / django-admin / 用户端 Django 模板 SSR
## 当前状态
-Phase 2 计费核心已完成,Phase 3 对外 API 与充值已完成到 T-306,Phase 4 用户端已完成 T-501~T-505,Phase 5 已完成 T-401 运营后台完善、T-402 MVP 完整验收与 T-403 部署 / 运行文档,Phase 6 已完成 T-601 可用别名发现、T-602 django-admin 分组/表名中文化与 T-605 免邮箱验证策略落地;T-603 字段级中文化代码和 no-op 迁移已完成,待稳定 MySQL 测试库补跑完整验证后收尾。用户可通过 allauth 自助注册(免邮箱验证、注册即可用,邮箱仍必填且唯一)、登录、登出,扫码充值并轮询到账,生成 / 删除(吊销)API Key,查看余额、充值总额、分页充值记录、分页消费记录与可用模型;运营可在 django-admin 检索用户、钱包、API Key、计费规则、汇率、充值订单、点数流水和调用记录,并通过计费层带原因手工调点;生产部署按 `docs/deployment.md` 执行。生产侧仍需配置真实支付、AI 模型和图片真实耗时验证;邮件服务仅用于后续密码找回/通知等邮件能力,不阻塞注册登录。详见 [`docs/current-state.md`](docs/current-state.md)。
+Phase 2 计费核心已完成,Phase 3 对外 API 与充值已完成到 T-306,Phase 4 用户端已完成 T-501~T-505,Phase 5 已完成 T-401 运营后台完善、T-402 MVP 完整验收与 T-403 部署 / 运行文档,Phase 6 已完成 T-601 可用别名发现、T-602/T-603 django-admin 中文化、T-604 中文敏感词本地过滤、T-605 免邮箱验证策略落地与 T-606 公开首页 + 客户端下载入口。用户可通过公开首页进入注册、登录和下载入口;登录后可扫码充值并轮询到账,生成 / 删除(吊销)API Key,查看余额、充值总额、分页充值记录、分页消费记录与可用模型;运营可在 django-admin 检索用户、钱包、API Key、计费规则、汇率、充值订单、点数流水、调用记录和客户端发布版本,并通过计费层带原因手工调点;生产部署按 `docs/deployment.md` 执行。生产侧仍需补真实支付回调到账闭环、配置并发布客户端下载包和图片真实耗时验证;邮件服务仅用于后续密码找回/通知等邮件能力,不阻塞注册登录。详见 [`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) 第四节计费时序。
diff --git a/apps/portal/admin.py b/apps/portal/admin.py
index 8c38f3f..b534bae 100644
--- a/apps/portal/admin.py
+++ b/apps/portal/admin.py
@@ -1,3 +1,64 @@
from django.contrib import admin
-# Register your models here.
+from .models import DownloadRelease
+
+
+@admin.register(DownloadRelease)
+class DownloadReleaseAdmin(admin.ModelAdmin):
+ list_display = (
+ "platform",
+ "version",
+ "is_current",
+ "download_source",
+ "sha256_short",
+ "updated_at",
+ )
+ list_filter = ("platform", "is_current")
+ search_fields = ("version", "sha256", "external_url", "file")
+ readonly_fields = ("created_at", "updated_at")
+ fieldsets = (
+ (
+ "版本信息",
+ {
+ "fields": (
+ "platform",
+ "version",
+ "is_current",
+ "release_notes",
+ )
+ },
+ ),
+ (
+ "下载配置",
+ {
+ "fields": (
+ "file",
+ "external_url",
+ "sha256",
+ )
+ },
+ ),
+ (
+ "时间",
+ {
+ "fields": (
+ "created_at",
+ "updated_at",
+ )
+ },
+ ),
+ )
+
+ @admin.display(description="下载来源")
+ def download_source(self, obj):
+ if obj.external_url:
+ return "外部链接"
+ if obj.file:
+ return "本地文件"
+ return "未配置"
+
+ @admin.display(description="SHA256")
+ def sha256_short(self, obj):
+ if not obj.sha256:
+ return "-"
+ return f"{obj.sha256[:12]}..."
diff --git a/apps/portal/migrations/0001_initial.py b/apps/portal/migrations/0001_initial.py
new file mode 100644
index 0000000..a36e83d
--- /dev/null
+++ b/apps/portal/migrations/0001_initial.py
@@ -0,0 +1,37 @@
+# Generated by Django 5.2.15 on 2026-07-06 09:48
+
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='DownloadRelease',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('platform', models.CharField(choices=[('windows', 'Windows'), ('macos', 'macOS'), ('linux', 'Linux')], max_length=32, verbose_name='平台')),
+ ('version', models.CharField(max_length=64, verbose_name='版本')),
+ ('file', models.FileField(blank=True, upload_to='downloads/', verbose_name='安装包文件')),
+ ('external_url', models.URLField(blank=True, verbose_name='外部下载地址')),
+ ('sha256', models.CharField(blank=True, max_length=64, validators=[django.core.validators.RegexValidator(message='SHA256 必须是 64 位十六进制字符。', regex='^[A-Fa-f0-9]{64}$')], verbose_name='SHA256')),
+ ('is_current', models.BooleanField(default=False, verbose_name='当前版本')),
+ ('release_notes', models.TextField(blank=True, verbose_name='发布说明')),
+ ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+ ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
+ ],
+ options={
+ 'db_table': 'download_release',
+ 'verbose_name': '客户端下载版本',
+ 'verbose_name_plural': '客户端下载版本',
+ 'ordering': ('platform', '-is_current', '-created_at', '-id'),
+ 'indexes': [models.Index(fields=['platform', 'is_current'], name='dl_platform_current_idx')],
+ },
+ ),
+ ]
diff --git a/apps/portal/models.py b/apps/portal/models.py
index 71a8362..a2b50f2 100644
--- a/apps/portal/models.py
+++ b/apps/portal/models.py
@@ -1,3 +1,71 @@
-from django.db import models
+from django.core.exceptions import ValidationError
+from django.core.validators import RegexValidator
+from django.db import models, transaction
-# Create your models here.
+
+class DownloadRelease(models.Model):
+ class Platform(models.TextChoices):
+ WINDOWS = "windows", "Windows"
+ MACOS = "macos", "macOS"
+ LINUX = "linux", "Linux"
+
+ platform = models.CharField("平台", max_length=32, choices=Platform.choices)
+ version = models.CharField("版本", max_length=64)
+ file = models.FileField("安装包文件", upload_to="downloads/", blank=True)
+ external_url = models.URLField("外部下载地址", blank=True)
+ sha256 = models.CharField(
+ "SHA256",
+ max_length=64,
+ blank=True,
+ validators=[
+ RegexValidator(
+ regex=r"^[A-Fa-f0-9]{64}$",
+ message="SHA256 必须是 64 位十六进制字符。",
+ )
+ ],
+ )
+ is_current = models.BooleanField("当前版本", default=False)
+ release_notes = models.TextField("发布说明", blank=True)
+ created_at = models.DateTimeField("创建时间", auto_now_add=True)
+ updated_at = models.DateTimeField("更新时间", auto_now=True)
+
+ class Meta:
+ db_table = "download_release"
+ verbose_name = "客户端下载版本"
+ verbose_name_plural = "客户端下载版本"
+ ordering = ("platform", "-is_current", "-created_at", "-id")
+ indexes = [
+ models.Index(
+ fields=("platform", "is_current"),
+ name="dl_platform_current_idx",
+ ),
+ ]
+
+ def __str__(self):
+ return f"{self.get_platform_display()} {self.version}"
+
+ @property
+ def download_url(self) -> str:
+ if self.external_url:
+ return self.external_url
+ if self.file:
+ return self.file.url
+ return ""
+
+ @property
+ def has_download(self) -> bool:
+ return bool(self.download_url)
+
+ def clean(self):
+ super().clean()
+ if not self.file and not self.external_url:
+ raise ValidationError("安装包文件和外部下载地址至少填写一个。")
+
+ def save(self, *args, **kwargs):
+ with transaction.atomic():
+ super().save(*args, **kwargs)
+ if self.is_current:
+ type(self).objects.filter(
+ platform=self.platform,
+ is_current=True,
+ ).exclude(pk=self.pk).update(is_current=False)
diff --git a/apps/portal/static/portal/brand.css b/apps/portal/static/portal/brand.css
new file mode 100644
index 0000000..0a8717f
--- /dev/null
+++ b/apps/portal/static/portal/brand.css
@@ -0,0 +1,634 @@
+:root {
+ --cm-paper: #F5F7FB;
+ --cm-card: #FFFFFF;
+ --cm-ink-strong: #161C2B;
+ --cm-ink: #1A2030;
+ --cm-ink-soft: #5A6478;
+ --cm-faint: #8A93A6;
+ --cm-line: #DCE1EC;
+ --cm-indigo: #5B4BEF;
+ --cm-indigo-deep: #3A2FB0;
+ --cm-amber: #E8A02D;
+ --cm-amber-ink: #C9821A;
+ --cm-mint: #3FCB7E;
+ --cm-chip-indigo: #ECEBFF;
+ --cm-chip-amber: #FBEFD9;
+ --cm-band-1: #1B1740;
+ --cm-band-2: #2A1E5E;
+ --cm-radius: 8px;
+ --cm-radius-lg: 8px;
+ --cm-font-cjk: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif;
+ --cm-font-latin: "Space Grotesk", system-ui, sans-serif;
+ --cm-font-mono: "JetBrains Mono", "SFMono-Regular", Consolas, monospace;
+ --cm-btn: linear-gradient(#6455F2, #4335CF);
+ --cm-thread: linear-gradient(#5B4BEF, #E8A02D);
+ --cm-shadow: 0 18px 42px rgba(22, 28, 43, 0.09);
+ --cmhub-bg: var(--cm-paper);
+ --cmhub-ink: var(--cm-ink);
+ --cmhub-line: var(--cm-line);
+ --cmhub-accent: var(--cm-indigo);
+ --bs-body-font-family: var(--cm-font-cjk);
+ --bs-body-color: var(--cm-ink);
+ --bs-body-bg: var(--cm-paper);
+ --bs-border-color: var(--cm-line);
+ --bs-link-color: var(--cm-indigo);
+ --bs-link-hover-color: var(--cm-indigo-deep);
+}
+
+body {
+ background: var(--cm-paper);
+ color: var(--cm-ink);
+}
+
+.navbar {
+ border-bottom: 1px solid var(--cm-line);
+ background: rgba(255, 255, 255, 0.92);
+}
+
+.navbar-brand {
+ align-items: center;
+ color: var(--cm-ink-strong);
+ display: inline-flex;
+ gap: 10px;
+}
+
+.navbar-brand::before {
+ background:
+ linear-gradient(to top, var(--cm-indigo) 46%, transparent 46%) 0 100% / 5px 18px no-repeat,
+ linear-gradient(to top, var(--cm-indigo) 68%, transparent 68%) 9px 100% / 5px 18px no-repeat,
+ linear-gradient(to top, var(--cm-amber) 92%, transparent 92%) 18px 100% / 5px 18px no-repeat;
+ border: 1px solid var(--cm-line);
+ border-radius: 7px;
+ content: "";
+ height: 28px;
+ width: 32px;
+}
+
+.cmhub-shell {
+ margin: 0 auto;
+ max-width: 1080px;
+ padding: 40px 16px;
+}
+
+.cmhub-panel {
+ background: var(--cm-card);
+ border: 1px solid var(--cm-line);
+ border-radius: var(--cm-radius);
+ box-shadow: var(--cm-shadow);
+ margin: 0 auto;
+ max-width: 440px;
+ padding: 28px;
+}
+
+.cmhub-panel form p {
+ margin-bottom: 16px;
+}
+
+.cmhub-panel label {
+ color: var(--cm-ink-strong);
+ display: block;
+ font-weight: 600;
+ margin-bottom: 6px;
+}
+
+.cmhub-panel input {
+ border: 1px solid #B9C2CF;
+ border-radius: var(--cm-radius);
+ min-height: 40px;
+ padding: 8px 10px;
+ width: 100%;
+}
+
+.cmhub-panel .errorlist {
+ color: #B42318;
+ margin: 0 0 12px;
+ padding-left: 18px;
+}
+
+.btn-primary {
+ --bs-btn-bg: var(--cm-indigo);
+ --bs-btn-border-color: var(--cm-indigo);
+ --bs-btn-hover-bg: var(--cm-indigo-deep);
+ --bs-btn-hover-border-color: var(--cm-indigo-deep);
+ --bs-btn-active-bg: var(--cm-indigo-deep);
+ --bs-btn-active-border-color: var(--cm-indigo-deep);
+ background-image: var(--cm-btn);
+}
+
+.btn-outline-secondary {
+ --bs-btn-color: var(--cm-ink);
+ --bs-btn-border-color: var(--cm-line);
+ --bs-btn-hover-bg: var(--cm-chip-indigo);
+ --bs-btn-hover-border-color: var(--cm-indigo);
+ --bs-btn-hover-color: var(--cm-indigo-deep);
+}
+
+.metric,
+.cmhub-surface {
+ background: var(--cm-card);
+ border: 1px solid var(--cm-line);
+ border-radius: var(--cm-radius);
+ box-shadow: 0 10px 30px rgba(22, 28, 43, 0.05);
+}
+
+.metric {
+ padding: 18px;
+}
+
+.metric .display-6,
+.metric .h5,
+.cmhub-surface h2 {
+ color: var(--cm-ink-strong);
+}
+
+.metric .display-6 {
+ color: var(--cm-amber-ink);
+ font-family: var(--cm-font-mono);
+}
+
+.cmhub-surface {
+ padding: 20px;
+}
+
+.key-value,
+code,
+.font-monospace {
+ font-family: var(--cm-font-mono);
+ word-break: break-all;
+}
+
+.table {
+ --bs-table-bg: transparent;
+ --bs-table-color: var(--cm-ink);
+ --bs-table-border-color: var(--cm-line);
+}
+
+.badge.text-bg-secondary {
+ background-color: var(--cm-ink-soft) !important;
+}
+
+.badge.text-bg-warning {
+ background-color: var(--cm-chip-amber) !important;
+ color: var(--cm-amber-ink) !important;
+}
+
+.badge.text-bg-success {
+ background-color: var(--cm-mint) !important;
+ color: #0E3B24 !important;
+}
+
+.home-page .navbar {
+ position: sticky;
+ top: 0;
+ z-index: 10;
+}
+
+.home-main {
+ overflow: hidden;
+}
+
+.home-container {
+ margin: 0 auto;
+ max-width: 1180px;
+ padding: 0 20px;
+}
+
+.home-hero {
+ min-height: calc(100vh - 56px);
+ padding: 64px 0 34px;
+}
+
+.home-hero-grid {
+ align-items: center;
+ display: grid;
+ gap: 42px;
+ grid-template-columns: minmax(0, 0.96fr) minmax(360px, 1.04fr);
+}
+
+.home-eyebrow {
+ color: var(--cm-indigo);
+ font-size: 13px;
+ font-weight: 700;
+ letter-spacing: 0;
+ margin-bottom: 14px;
+}
+
+.home-hero h1,
+.home-section-title,
+.home-band-title {
+ color: var(--cm-ink-strong);
+ font-family: var(--cm-font-latin), var(--cm-font-cjk);
+ font-weight: 700;
+ letter-spacing: 0;
+}
+
+.home-hero h1 {
+ font-size: clamp(2.35rem, 5vw, 3.55rem);
+ line-height: 1.05;
+ margin-bottom: 20px;
+}
+
+.home-lead {
+ color: var(--cm-ink-soft);
+ font-size: 1.08rem;
+ line-height: 1.75;
+ max-width: 620px;
+}
+
+.home-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ margin-top: 28px;
+}
+
+.home-proof {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ margin-top: 24px;
+}
+
+.home-chip {
+ align-items: center;
+ background: var(--cm-card);
+ border: 1px solid var(--cm-line);
+ border-radius: 999px;
+ color: var(--cm-ink-soft);
+ display: inline-flex;
+ font-size: 0.88rem;
+ gap: 8px;
+ min-height: 34px;
+ padding: 6px 12px;
+}
+
+.home-chip::before {
+ background: var(--cm-mint);
+ border-radius: 999px;
+ content: "";
+ height: 8px;
+ width: 8px;
+}
+
+.home-stage {
+ background: var(--cm-card);
+ border: 1px solid var(--cm-line);
+ border-radius: var(--cm-radius-lg);
+ box-shadow: var(--cm-shadow);
+ padding: 18px;
+}
+
+.home-product-shot {
+ background:
+ linear-gradient(135deg, rgba(91, 75, 239, 0.08), rgba(232, 160, 45, 0.16)),
+ var(--cm-paper);
+ border: 1px solid var(--cm-line);
+ border-radius: var(--cm-radius);
+ min-height: 160px;
+ padding: 18px;
+}
+
+.home-product-frame {
+ align-items: center;
+ background: #FBFCFF;
+ border: 1px solid var(--cm-line);
+ border-radius: var(--cm-radius);
+ display: flex;
+ gap: 14px;
+ min-height: 120px;
+ padding: 14px;
+}
+
+.home-product-image {
+ align-items: center;
+ background: linear-gradient(145deg, #E8EBF7, #FFFFFF);
+ border: 1px solid var(--cm-line);
+ border-radius: var(--cm-radius);
+ color: var(--cm-indigo);
+ display: flex;
+ flex: 0 0 108px;
+ font-weight: 700;
+ height: 92px;
+ justify-content: center;
+}
+
+.home-product-copy {
+ color: var(--cm-ink-soft);
+ font-size: 0.9rem;
+ line-height: 1.6;
+}
+
+.home-thread {
+ background: var(--cm-thread);
+ border-radius: 999px;
+ height: 56px;
+ margin: 16px auto;
+ position: relative;
+ width: 4px;
+}
+
+.home-thread::before,
+.home-thread::after {
+ background: var(--cm-card);
+ border: 2px solid var(--cm-line);
+ border-radius: 999px;
+ content: "";
+ height: 12px;
+ left: -4px;
+ position: absolute;
+ width: 12px;
+}
+
+.home-thread::before {
+ top: -2px;
+}
+
+.home-thread::after {
+ bottom: -2px;
+}
+
+.home-output-grid {
+ display: grid;
+ gap: 12px;
+ grid-template-columns: 1fr 1fr;
+}
+
+.home-output {
+ border: 1px solid var(--cm-line);
+ border-radius: var(--cm-radius);
+ padding: 14px;
+}
+
+.home-output-title {
+ color: var(--cm-ink-strong);
+ font-weight: 700;
+ margin-bottom: 10px;
+}
+
+.home-output-text {
+ color: var(--cm-ink-soft);
+ font-size: 0.92rem;
+ line-height: 1.6;
+}
+
+.home-point-pill {
+ background: var(--cm-chip-amber);
+ border-radius: 999px;
+ color: var(--cm-amber-ink);
+ display: inline-flex;
+ font-family: var(--cm-font-mono);
+ font-size: 0.84rem;
+ font-weight: 700;
+ margin-top: 12px;
+ padding: 5px 10px;
+}
+
+.home-section {
+ padding: 70px 0;
+}
+
+.home-section-heading {
+ display: flex;
+ justify-content: space-between;
+ align-items: end;
+ gap: 24px;
+ margin-bottom: 28px;
+}
+
+.home-section-title {
+ font-size: clamp(1.9rem, 4vw, 2.4rem);
+ line-height: 1.15;
+ margin: 0;
+}
+
+.home-section-copy {
+ color: var(--cm-ink-soft);
+ line-height: 1.7;
+ margin: 0;
+ max-width: 520px;
+}
+
+.home-steps,
+.home-cards {
+ display: grid;
+ gap: 16px;
+}
+
+.home-steps {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+}
+
+.home-cards {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.home-step,
+.home-card,
+.home-download-box {
+ background: var(--cm-card);
+ border: 1px solid var(--cm-line);
+ border-radius: var(--cm-radius);
+ padding: 22px;
+}
+
+.home-step-num {
+ color: var(--cm-indigo);
+ font-family: var(--cm-font-mono);
+ font-size: 0.86rem;
+ font-weight: 700;
+ margin-bottom: 18px;
+}
+
+.home-step h3,
+.home-card h3,
+.home-download-box h3 {
+ color: var(--cm-ink-strong);
+ font-size: 1.12rem;
+ font-weight: 700;
+ margin-bottom: 10px;
+}
+
+.home-step p,
+.home-card p,
+.home-download-box p {
+ color: var(--cm-ink-soft);
+ line-height: 1.7;
+ margin-bottom: 0;
+}
+
+.home-card {
+ min-height: 210px;
+}
+
+.home-card-icon {
+ align-items: center;
+ background: var(--cm-chip-indigo);
+ border-radius: var(--cm-radius);
+ color: var(--cm-indigo-deep);
+ display: inline-flex;
+ font-weight: 700;
+ height: 44px;
+ justify-content: center;
+ margin-bottom: 22px;
+ width: 44px;
+}
+
+.home-band {
+ background: linear-gradient(135deg, var(--cm-band-1), var(--cm-band-2));
+ color: #FFFFFF;
+ padding: 62px 0;
+}
+
+.home-band-grid {
+ align-items: center;
+ display: grid;
+ gap: 28px;
+ grid-template-columns: 1.1fr 0.9fr;
+}
+
+.home-band-title {
+ color: #FFFFFF;
+ font-size: clamp(1.9rem, 4vw, 2.35rem);
+ line-height: 1.18;
+ margin-bottom: 14px;
+}
+
+.home-band p {
+ color: rgba(255, 255, 255, 0.78);
+ line-height: 1.75;
+ margin-bottom: 0;
+}
+
+.home-band-meter {
+ background: rgba(255, 255, 255, 0.08);
+ border: 1px solid rgba(255, 255, 255, 0.18);
+ border-radius: var(--cm-radius);
+ padding: 22px;
+}
+
+.home-band-meter strong {
+ color: var(--cm-amber);
+ display: block;
+ font-family: var(--cm-font-mono);
+ font-size: 2.2rem;
+ line-height: 1;
+ margin-bottom: 8px;
+}
+
+.home-download-grid {
+ display: grid;
+ gap: 20px;
+ grid-template-columns: minmax(0, 1fr) minmax(320px, 0.74fr);
+}
+
+.home-release-meta {
+ display: grid;
+ gap: 12px;
+ margin-top: 18px;
+}
+
+.home-release-row {
+ align-items: start;
+ display: grid;
+ gap: 10px;
+ grid-template-columns: 96px minmax(0, 1fr);
+}
+
+.home-release-row span {
+ color: var(--cm-faint);
+}
+
+.home-release-row strong,
+.home-release-row code {
+ color: var(--cm-ink);
+ min-width: 0;
+}
+
+.home-notice {
+ background: var(--cm-chip-amber);
+ border: 1px solid rgba(232, 160, 45, 0.34);
+ border-radius: var(--cm-radius);
+ color: #6F4708;
+ line-height: 1.7;
+ margin-top: 18px;
+ padding: 14px 16px;
+}
+
+.home-links {
+ display: grid;
+ gap: 12px;
+}
+
+.home-link-row {
+ align-items: center;
+ border: 1px solid var(--cm-line);
+ border-radius: var(--cm-radius);
+ color: var(--cm-ink);
+ display: flex;
+ justify-content: space-between;
+ min-height: 54px;
+ padding: 12px 14px;
+ text-decoration: none;
+}
+
+.home-link-row:hover {
+ background: var(--cm-chip-indigo);
+ color: var(--cm-indigo-deep);
+}
+
+@media (max-width: 991.98px) {
+ .home-hero-grid,
+ .home-band-grid,
+ .home-download-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .home-steps {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+@media (max-width: 575.98px) {
+ .cmhub-shell {
+ padding: 28px 12px;
+ }
+
+ .home-container {
+ padding: 0 14px;
+ }
+
+ .home-hero {
+ min-height: auto;
+ padding: 44px 0 28px;
+ }
+
+ .home-hero-grid,
+ .home-output-grid,
+ .home-steps,
+ .home-cards {
+ grid-template-columns: 1fr;
+ }
+
+ .home-section-heading {
+ align-items: start;
+ flex-direction: column;
+ }
+
+ .home-actions .btn {
+ width: 100%;
+ }
+
+ .home-product-frame,
+ .home-release-row {
+ grid-template-columns: 1fr;
+ }
+
+ .home-product-frame {
+ align-items: stretch;
+ display: grid;
+ }
+
+ .home-product-image {
+ width: 100%;
+ }
+}
diff --git a/apps/portal/templates/portal/base.html b/apps/portal/templates/portal/base.html
index 9b28dab..24e844f 100644
--- a/apps/portal/templates/portal/base.html
+++ b/apps/portal/templates/portal/base.html
@@ -6,78 +6,9 @@
{% block title %}cmhub{% endblock %}
-
+
-
+
+ {% block body %}
{% if messages %}
@@ -112,5 +44,6 @@
{% endif %}
{% block content %}{% endblock %}
+ {% endblock %}
diff --git a/apps/portal/templates/portal/home.html b/apps/portal/templates/portal/home.html
new file mode 100644
index 0000000..97845c5
--- /dev/null
+++ b/apps/portal/templates/portal/home.html
@@ -0,0 +1,199 @@
+{% extends "portal/base.html" %}
+
+{% block title %}cmhub AI 电商生成台{% endblock %}
+{% block body_class %}home-page{% endblock %}
+
+{% block body %}
+
+
+
+
+
AI 生成 / 点数计费 / 桌面端接入
+
cmhub AI 电商生成台
+
+ 把生成标题、生成主图能力封装成稳定的 HTTP API。桌面端填入 API Key 后按本地点数扣费,
+ 余额不足直接拦截,失败自动退点。
+
+
+ {% if user.is_authenticated %}
+
进入控制台
+ {% else %}
+
免费注册
+
登录
+ {% endif %}
+ {% if current_release and current_release.has_download %}
+
下载客户端
+ {% else %}
+
客户端下载暂未发布
+ {% endif %}
+
+
+ 注册即可用
+ 按点数扣点
+ API Key 接入
+
+
+
+
+
+
+
商品图
+
+ 轻量夹克,春秋外套,通勤穿搭,图片清晰,适合电商标题与主图优化。
+
+
+
+
+
+
+
吸睛标题
+
春秋通勤轻量夹克,防风舒适,男女日常外套
+
-2 点
+
+
+
生成主图
+
保留商品主体,生成干净背景和电商展示构图。
+
-12 点
+
+
+
+
+
+
+
+
+
+
四步接入桌面端
+
从账号到调用链路保持简单:账号、点数、API Key 和客户端配置各司其职。
+
+
+
+
01
+
注册账号
+
邮箱注册后直接登录,系统创建 0 点钱包,不赠点、不写虚假流水。
+
+
+
02
+
充值点数
+
扫码创建充值订单,到账后按汇率入账到本地点数账本。
+
+
+
03
+
生成 API Key
+
在控制台生成 Key,明文只显示一次,后续列表只展示 prefix。
+
+
+
04
+
填入桌面端
+
客户端使用能力别名调用标题或图片接口,按规则扣点。
+
+
+
+
+
+
+
+
+
面向电商流程的两类能力
+
对外只暴露能力别名,不暴露底层供应商 SKU、URL、密钥或 extra_body。
+
+
+
+ 文
+ 生成标题
+ 按商品信息、提示词和能力别名生成标题。敏感词命中会在扣点和上游调用前拦截。
+
+
+ 图
+ 生成主图(改图)
+ 支持需要原图的图片能力,图片输入先做公网地址和大小校验,结果保存后返回 URL。
+
+
+
+
+
+
+
+
+
点数余额是唯一权威账本
+
+ 充值成功后写入本地点数;每次生成先预扣,成功确认,失败退点。调用链路不实时查询支付系统。
+
+
+
+
本地扣点
+
余额不足返回“点数不足,请先充值”,不会继续调用上游模型。
+
+
+
+
+
+
+
+
下载桌面端
+
下载走 HTTPS,页面展示 SHA256 供校验;安装包前期由服务器 media 目录托管,生产 Nginx 直接服务。
+
+
+
+ {% if current_release and current_release.has_download %}
+
Windows 客户端 {{ current_release.version }}
+
当前版本已发布,可下载后填入 cmhub API Key 使用。
+
+
+
+ 首期安装包可能未签名。Windows 若提示未知发布者,请先核对 SHA256,再选择“仍要运行”;正式代码签名后会更新版本说明。
+
+ {% else %}
+
客户端暂未发布
+
后台尚未配置当前 Windows 版本。发布后这里会展示版本号、下载按钮、SHA256 和版本说明。
+
+
暂未发布
+ {% if user.is_authenticated %}
+
进入控制台
+ {% else %}
+
先注册账号
+ {% endif %}
+
+ {% endif %}
+
+
+
+
+
+
+{% endblock %}
diff --git a/apps/portal/tests.py b/apps/portal/tests.py
index 9e29790..37b7378 100644
--- a/apps/portal/tests.py
+++ b/apps/portal/tests.py
@@ -18,6 +18,7 @@ from apps.billing.models import (
PricingRule,
RechargeOrder,
)
+from apps.portal.models import DownloadRelease
from apps.users.models import ApiKey, UserWallet
@@ -110,6 +111,25 @@ class PortalAccountFlowTests(TestCase):
ai_model=ai_model,
)
+ def create_download_release(
+ self,
+ *,
+ version: str = "1.0.0",
+ platform: str = DownloadRelease.Platform.WINDOWS,
+ is_current: bool = True,
+ external_url: str = "https://download.example.com/cmhub-desktop.exe",
+ sha256: str = "a" * 64,
+ release_notes: str = "首版 Windows 客户端",
+ ) -> DownloadRelease:
+ return DownloadRelease.objects.create(
+ platform=platform,
+ version=version,
+ is_current=is_current,
+ external_url=external_url,
+ sha256=sha256,
+ release_notes=release_notes,
+ )
+
def assert_nav_link_active(self, response, *, href: str, label: str):
html = response.content.decode("utf-8")
self.assertIn(
@@ -211,6 +231,56 @@ class PortalAccountFlowTests(TestCase):
self.assertEqual(response.status_code, 403)
+ def test_homepage_is_public_and_shows_anonymous_onboarding_without_release(self):
+ response = self.client.get("/")
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.resolver_match.url_name, "portal-home")
+ self.assertContains(response, "cmhub AI 电商生成台")
+ self.assertContains(response, "免费注册")
+ self.assertContains(response, "登录")
+ self.assertContains(response, "客户端暂未发布")
+ self.assertContains(response, "暂未发布")
+ self.assertNotContains(response, "/login?next=")
+
+ def test_homepage_shows_current_download_release(self):
+ release = self.create_download_release(
+ version="1.2.3",
+ external_url="https://download.example.com/cmhub-1.2.3.exe",
+ sha256="b" * 64,
+ release_notes="修复下载入口并补充 SHA256",
+ )
+
+ response = self.client.get("/")
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context["current_release"], release)
+ self.assertContains(response, "Windows 客户端 1.2.3")
+ self.assertContains(response, "https://download.example.com/cmhub-1.2.3.exe")
+ self.assertContains(response, "b" * 64)
+ self.assertContains(response, "修复下载入口并补充 SHA256")
+ self.assertContains(response, "未知发布者")
+
+ def test_homepage_shows_dashboard_entry_for_authenticated_user(self):
+ user = self.create_verified_user()
+ self.client.force_login(user)
+
+ response = self.client.get("/")
+
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, "进入控制台")
+ self.assertNotContains(response, "免费注册")
+
+ def test_download_release_only_keeps_one_current_per_platform(self):
+ old_release = self.create_download_release(version="1.0.0", sha256="c" * 64)
+ new_release = self.create_download_release(version="1.1.0", sha256="d" * 64)
+
+ old_release.refresh_from_db()
+ new_release.refresh_from_db()
+
+ self.assertFalse(old_release.is_current)
+ self.assertTrue(new_release.is_current)
+
def test_apikeys_requires_session_login(self):
response = self.client.get("/apikeys")
diff --git a/apps/portal/urls.py b/apps/portal/urls.py
index d347caa..ed35bf3 100644
--- a/apps/portal/urls.py
+++ b/apps/portal/urls.py
@@ -1,11 +1,11 @@
from allauth.account.views import LoginView, LogoutView, SignupView
from django.urls import path
-from django.views.generic import RedirectView
from .views import (
ApiKeyDeleteView,
ApiKeyListCreateView,
DashboardView,
+ HomeView,
ModelCatalogView,
RechargePageView,
RechargeRecordListView,
@@ -13,7 +13,7 @@ from .views import (
)
urlpatterns = [
- path("", RedirectView.as_view(pattern_name="portal-dashboard", permanent=False), name="portal-home"),
+ path("", HomeView.as_view(), name="portal-home"),
path("signup", SignupView.as_view(), name="portal-signup"),
path("login", LoginView.as_view(), name="portal-login"),
path("logout", LogoutView.as_view(), name="portal-logout"),
diff --git a/apps/portal/views.py b/apps/portal/views.py
index 1524476..1ab751f 100644
--- a/apps/portal/views.py
+++ b/apps/portal/views.py
@@ -19,6 +19,7 @@ from apps.billing.services import (
from apps.users.models import ApiKey
from .forms import ApiKeyCreateForm, RechargeCreateForm
+from .models import DownloadRelease
NEW_API_KEY_SESSION_KEY = "portal_new_api_key"
@@ -88,6 +89,22 @@ def paginate_records(request, queryset):
}
+class HomeView(TemplateView):
+ template_name = "portal/home.html"
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["current_release"] = (
+ DownloadRelease.objects.filter(
+ platform=DownloadRelease.Platform.WINDOWS,
+ is_current=True,
+ )
+ .order_by("-created_at", "-id")
+ .first()
+ )
+ return context
+
+
class DashboardView(LoginRequiredMixin, TemplateView):
template_name = "portal/dashboard.html"
diff --git a/docs/00-ai-start-here.md b/docs/00-ai-start-here.md
index 0543873..15645a1 100644
--- a/docs/00-ai-start-here.md
+++ b/docs/00-ai-start-here.md
@@ -38,7 +38,7 @@
## 当前阶段
-当前项目处于:**Phase 6 增强任务推进期**。Phase 2 计费核心已完成到 T-204;Phase 3 已完成 T-301 API Key 鉴权、T-302 生成标题 / 图片接口、T-303 余额查询接口、T-304 充值回调、T-305 扫码充值下单 + 轮询与 T-306 对外 API 安全加固;Phase 4 已完成 T-501 注册 / 登录(allauth)、T-502 API Key 自助管理页、T-503 个人中心 / 记录页、T-504 充值页与 T-505 用户端审核优化;Phase 5 已完成 T-401 运营后台完善、T-402 MVP 完整验收与 T-403 部署 / 运行文档;Phase 6 已完成 T-601「可用别名发现」、T-602「django-admin 中文化第 1-3 层」、T-603「django-admin 字段级中文化」、T-604「中文敏感词本地过滤」与 T-605「免邮箱验证策略落地」。下一步按看板进入 T-606「公开首页 + 客户端下载入口」。生产侧仍需补真实支付回调到账闭环和图片生成真实耗时验证;邮件服务仅用于后续密码找回/通知等邮件能力,不阻塞注册登录。
+当前项目处于:**Phase 6 增强任务推进期**。Phase 2 计费核心已完成到 T-204;Phase 3 已完成 T-301 API Key 鉴权、T-302 生成标题 / 图片接口、T-303 余额查询接口、T-304 充值回调、T-305 扫码充值下单 + 轮询与 T-306 对外 API 安全加固;Phase 4 已完成 T-501 注册 / 登录(allauth)、T-502 API Key 自助管理页、T-503 个人中心 / 记录页、T-504 充值页与 T-505 用户端审核优化;Phase 5 已完成 T-401 运营后台完善、T-402 MVP 完整验收与 T-403 部署 / 运行文档;Phase 6 已完成 T-601「可用别名发现」、T-602「django-admin 中文化第 1-3 层」、T-603「django-admin 字段级中文化」、T-604「中文敏感词本地过滤」、T-605「免邮箱验证策略落地」与 T-606「公开首页 + 客户端下载入口」。生产侧仍需补真实支付回调到账闭环、配置并发布客户端下载包和图片生成真实耗时验证;邮件服务仅用于后续密码找回/通知等邮件能力,不阻塞注册登录。
优先路径:
@@ -48,7 +48,7 @@
4. Phase 3:对外 API 与充值 —— T-301 Key 鉴权、T-302 生成接口、T-303 余额查询、T-304 充值回调、T-305 扫码下单与轮询、T-306 安全加固已完成。
5. Phase 4:用户端(Django 模板 SSR)—— T-501 注册登录、T-502 API Key 管理、T-503 个人中心 / 记录页、T-504 充值页与 T-505 用户端审核优化已完成。
6. Phase 5:后台与发布 —— T-401 运营后台完善、T-402 完整验收 MVP、T-403 部署 / 运行文档已完成;计划内 MVP 任务已收尾。
-7. Phase 6:增强(MVP 后)—— T-601 可用别名发现已完成,实现 `/api/v1/models` 与 portal 只读「可用模型」页;T-602 已完成 django-admin 分组/表名中文化;T-603 已完成字段级中文标签代码与 no-op 迁移并人工确认 admin 字段中文化;T-604 已完成中文敏感词本地过滤;T-605 已完成免邮箱验证策略落地;下一步是 T-606 公开首页 + 客户端下载入口。
+7. Phase 6:增强(MVP 后)—— T-601 可用别名发现已完成,实现 `/api/v1/models` 与 portal 只读「可用模型」页;T-602 已完成 django-admin 分组/表名中文化;T-603 已完成字段级中文标签代码与 no-op 迁移并人工确认 admin 字段中文化;T-604 已完成中文敏感词本地过滤;T-605 已完成免邮箱验证策略落地;T-606 已完成公开首页 + 客户端下载入口。下一步需先确认业务优先级或拆新任务。
## 领取任务规则
diff --git a/docs/04-architecture.md b/docs/04-architecture.md
index b4aa34d..0659faa 100644
--- a/docs/04-architecture.md
+++ b/docs/04-architecture.md
@@ -18,7 +18,7 @@
组件落位:
-- **用户端层(Django 模板 SSR)**:注册/登录(Django auth / allauth)、个人中心(余额/充值总额/充值记录/消费记录)、API Key 自助管理、发起扫码充值。入口 `apps/portal/`,用 session 鉴权。T-501 已落地 `/signup`、`/login`、`/logout` 与最小 `/dashboard`;注册成功通过 allauth adapter 创建 0 点 `UserWallet`,不写赠点流水。T-502 已落地 `/apikeys`,用户可自助生成和删除(吊销)自己的 API Key,明文只显示一次,列表只显示 prefix。T-503 已扩展 `/dashboard` 并新增 `/records/recharge`、`/records/usage`,只读展示当前用户余额、充值订单与消费流水。T-504 已落地 `/recharge`,用户可创建 pending 充值订单、查看二维码票据,并轮询订单状态;到账仍以服务端回调或主动查单入账后的本地订单状态为准。T-505 已把 Bootstrap/qrcode.js 改成本地 static 自托管,并把充值/消费记录页从固定切片改为分页。
+- **用户端层(Django 模板 SSR)**:公开首页、注册/登录(Django auth / allauth)、个人中心(余额/充值总额/充值记录/消费记录)、API Key 自助管理、发起扫码充值、模型目录与客户端下载入口。入口 `apps/portal/`,公开首页匿名可访问,其他自助页面用 session 鉴权。T-501 已落地 `/signup`、`/login`、`/logout` 与最小 `/dashboard`;注册成功通过 allauth adapter 创建 0 点 `UserWallet`,不写赠点流水。T-502 已落地 `/apikeys`,用户可自助生成和删除(吊销)自己的 API Key,明文只显示一次,列表只显示 prefix。T-503 已扩展 `/dashboard` 并新增 `/records/recharge`、`/records/usage`,只读展示当前用户余额、充值订单与消费流水。T-504 已落地 `/recharge`,用户可创建 pending 充值订单、查看二维码票据,并轮询订单状态;到账仍以服务端回调或主动查单入账后的本地订单状态为准。T-505 已把 Bootstrap/qrcode.js 改成本地 static 自托管,并把充值/消费记录页从固定切片改为分页。T-606 已把 `/` 改为公开首页,并新增 `DownloadRelease` 下载版本配置用于展示 Windows 客户端版本、下载地址、SHA256 与发布说明。
- **用户与账号层**:注册用户 `User`、点数钱包 `UserWallet`、`ApiKey`(一用户多把、哈希存储)。入口 `apps/users/`。
- **API 层(DRF)**:对外生成接口、余额查询、支付回调接收、扫码下单。入口 `apps/api/`。生成/余额这类对外业务 API **只认 API Key,不接受 Web session**;充值下单/状态查询属于用户端流程,走 Web session + CSRF;支付回调走平台验签。
- **计费层**:点数计算、原子扣减(锁 `UserWallet` 行)、退点、充值入账、流水记账。入口 `apps/billing/`。
@@ -97,6 +97,7 @@ T-306 已实现对外 API 安全加固:`download_image_input()` 在请求前
| PricingRule | 运营配置 | 操作类型 × **能力别名字符串**(+ 可选分辨率)→ 点数单价;不外键到具体 AiModel |
| ExchangeRate | 运营配置 | 金额 → 点数 的汇率(如 1 元 = N 点),按 `currency + effective_from` 取当前有效值 |
| SensitiveWord | 运营配置 | 本地敏感词快筛词库;word、normalized_word、category、action、is_active;T-604 MVP 仅支持 action=block |
+| DownloadRelease | 运营配置 | 客户端下载版本;platform、version、file、external_url、sha256、is_current、release_notes、created_at、updated_at;每平台当前版本由应用层事务保存时互斥 |
关键事实:
@@ -184,9 +185,23 @@ CREATE TABLE exchange_rate (
created_at DATETIME(6) NOT NULL,
updated_at DATETIME(6) NOT NULL
);
+
+-- 客户端下载版本(首页读取当前版本;安装包生产由 Nginx 直接服务 media)
+CREATE TABLE download_release (
+ id BIGINT PRIMARY KEY,
+ platform VARCHAR(32) NOT NULL, -- windows / macos / linux
+ version VARCHAR(64) NOT NULL,
+ file VARCHAR(100) NOT NULL DEFAULT '', -- MEDIA_ROOT/downloads/...
+ external_url VARCHAR(200) NOT NULL DEFAULT '', -- 有则优先,后续切 CDN/对象存储
+ sha256 VARCHAR(64) NOT NULL DEFAULT '',
+ is_current BOOLEAN NOT NULL DEFAULT FALSE,
+ release_notes LONGTEXT NOT NULL,
+ created_at DATETIME(6) NOT NULL,
+ updated_at DATETIME(6) NOT NULL
+);
```
-T-102 已实现 `AiModel` / `ModelAlias` 的 Django models、admin、迁移与别名解析。T-103 已补 `AiConfigAuditLog`,admin 里保存/删除模型配置或能力别名时自动写审计日志。T-202 已实现 `PricingRule` / `ExchangeRate` 与 `apps.billing.pricing` 计算函数:定价按 `operation_type + alias + resolution` 查 active 规则,优先 exact resolution,再回退到空 resolution 默认价;缺规则抛 `NoPricingRuleError(code="no_pricing_rule")`。T-203 已实现 `apps.billing.services`:`precharge_call()` 锁 `UserWallet` 行预扣并写 pending 调用与 consume 流水;`mark_call_success()` 确认成功不再改余额;`refund_call_points()` 锁调用记录并幂等退点,写 refund 流水。T-401 已在同一计费层新增 `adjust_wallet_points()`,供 admin 手工调点使用。默认别名唯一性由 model validation、admin 与导入器保证;MySQL 不支持通用 partial unique index,若后续要强制数据库层默认别名唯一,可另评估触发器或约束表。
+T-102 已实现 `AiModel` / `ModelAlias` 的 Django models、admin、迁移与别名解析。T-103 已补 `AiConfigAuditLog`,admin 里保存/删除模型配置或能力别名时自动写审计日志。T-202 已实现 `PricingRule` / `ExchangeRate` 与 `apps.billing.pricing` 计算函数:定价按 `operation_type + alias + resolution` 查 active 规则,优先 exact resolution,再回退到空 resolution 默认价;缺规则抛 `NoPricingRuleError(code="no_pricing_rule")`。T-203 已实现 `apps.billing.services`:`precharge_call()` 锁 `UserWallet` 行预扣并写 pending 调用与 consume 流水;`mark_call_success()` 确认成功不再改余额;`refund_call_points()` 锁调用记录并幂等退点,写 refund 流水。T-401 已在同一计费层新增 `adjust_wallet_points()`,供 admin 手工调点使用。T-606 已实现 `DownloadRelease`,admin 可上传安装包或填写 `external_url` 并标记当前版本;`external_url` 优先于 `file.url`,每平台仅一个当前版本由模型 `save()` 在事务内把同平台旧 current 置为 false。默认别名唯一性由 model validation、admin 与导入器保证;MySQL 不支持通用 partial unique index,若后续要强制数据库层默认别名唯一,可另评估触发器或约束表。
> 可选增强(接口预留、MVP 不实现):`account_alias_permission`(按账号授权可用别名,防止调用方点用未授权/昂贵模型);别名按比例分流到多个模型(灰度/AB/故障转移)。适配器接口需为此留口子。
@@ -391,7 +406,7 @@ cmhub/
├── config/ # Django 工程:settings / urls / wsgi
├── apps/
│ ├── users/ # 注册用户 User、UserWallet 钱包、ApiKey(模型 + admin)
-│ ├── portal/ # 用户端页面(Django 模板 SSR):注册/登录/个人中心/充值/API Key 管理
+│ ├── portal/ # 用户端页面(Django 模板 SSR):公开首页/注册/登录/个人中心/充值/API Key/下载版本
│ ├── billing/ # 计费规则、汇率、充值订单、点数流水、扣点/入账逻辑(点数唯一写入方)
│ ├── ai/ # 别名解析 + Provider 适配器层
│ │ └── providers/ # 各供应商适配器(chat/gemini/images_edits…),移植自 cmbot
diff --git a/docs/06-tasks.md b/docs/06-tasks.md
index 8b0c0a2..daa4016 100644
--- a/docs/06-tasks.md
+++ b/docs/06-tasks.md
@@ -84,7 +84,7 @@
| 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` 留证据。已人工确认 admin 字段中文化,目标后台回归测试通过 | DONE |
| T-605 | 落实「免邮箱验证」策略 | T-501 | 把 `ACCOUNT_EMAIL_VERIFICATION="none"`(注册即可用、不发验证邮件、邮箱仍必填且唯一)作为**既定策略**清理落地:① `config/settings.py` 把 `ACCOUNT_EMAIL_VERIFICATION = "none"#"mandatory"` 改为干净的 `"none"`(去行内注释),并把免验证下无实际意义的 `ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION` 设为 `False`;② 更新 `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` 通过,`apps.portal` 26 tests OK;全量 `manage.py test` 已尝试,跑到 95/131 后因远程 MySQL 连接超时失败(WinError 10051/10060),非本任务断言失败,详见 `../progress.md` | DONE |
| 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 | DONE |
-| T-606 | 公开首页 + 客户端下载入口 | T-501 | 给网站补「前门」并提供桌面端下载。**路由改造**:`/` 从「重定向到 `/dashboard`」改为**公开首页**(匿名可访问、不跳登录);已登录用户显示「进入控制台」,匿名显示「注册/登录 + 下载客户端」。**首页内容**(SSR 模板):项目一句话介绍(生成标题/图片、按点数计费)+ 三步上手(注册→充值→建 API Key→桌面端填 Key)+ 下载入口 + 文档链接。**`DownloadRelease` 模型**:`platform`(如 windows)、`version`、`file`(FileField,**前期存 `MEDIA_ROOT`、服务器托管**) + `external_url`(URLField 可选,**后续切对象存储/CDN 用,有则优先**)、`sha256`、`is_current`(每平台仅一个当前版本)、`release_notes`、时间戳;admin 可上传安装包并标记当前版本;迁移。**下载区块**:展示当前 release 的版本、下载按钮、**SHA256 校验值**、可选 release notes;无 current release 时优雅提示「暂未发布」。**托管策略**:**前期安装包放本服务器**(生产由 **Nginx 直接服务 media/下载文件、不走 Django**,与 T-403 static/media serving 一致,大文件不占 gunicorn worker);`external_url` 预留,后续切对象存储只改后台链接不改代码。**安全**:下载走 HTTPS,页面展示 SHA256 供校验;**代码签名**作为决策登记——未签名 Windows 安装包会被 SmartScreen 拦「未知发布者」、macOS 被 Gatekeeper 拦,首页先给「如何忽略警告」说明,正式签名后续补(挂 Backlog / deployment)。**含测试**:`/` 匿名 200 不跳登录、下载区展示当前 release、无 current release 优雅处理、已登录用户显示「进入控制台」。**视觉原型(已定 v1)**:按 `prototypes/cmhub-homepage-v1.svg` 落地——「生成台」方向:靛蓝=生成 / 琥珀=点数;Hero 为「商品图 + 一句话 → 吸睛标题 + 生成主图 + 点数计量」转化图;四步上手 01–04、两张能力卡、深色计费 band、下载区(版本/SHA256/未签名提示);配色、间距、结构照此原型转成 Django 模板(Bootstrap + 本地 static)。**视觉一致性(brand token)**:按 [`brand.md`](brand.md) 抽出共享 CSS 变量(如 `apps/portal/static/portal/brand.css`),**首页与现有 portal 页面(dashboard/记录/充值/API Key)一起套用同一套 token**(把配色/字体变量灌进现有 Bootstrap,各页只引用变量不散写 hex),确保落地页与登录后控制台风格一致——**不是只做漂亮首页**。验收 `check`/`test`/`init` 全绿并在 `../progress.md` 留证据 | TODO |
+| T-606 | 公开首页 + 客户端下载入口 | T-501 | 给网站补「前门」并提供桌面端下载。**路由改造**:`/` 从「重定向到 `/dashboard`」改为**公开首页**(匿名可访问、不跳登录);已登录用户显示「进入控制台」,匿名显示「注册/登录 + 下载客户端」。**首页内容**(SSR 模板):项目一句话介绍(生成标题/图片、按点数计费)+ 三步上手(注册→充值→建 API Key→桌面端填 Key)+ 下载入口 + 文档链接。**`DownloadRelease` 模型**:`platform`(如 windows)、`version`、`file`(FileField,**前期存 `MEDIA_ROOT`、服务器托管**) + `external_url`(URLField 可选,**后续切对象存储/CDN 用,有则优先**)、`sha256`、`is_current`(每平台仅一个当前版本)、`release_notes`、时间戳;admin 可上传安装包并标记当前版本;迁移。**下载区块**:展示当前 release 的版本、下载按钮、**SHA256 校验值**、可选 release notes;无 current release 时优雅提示「暂未发布」。**托管策略**:**前期安装包放本服务器**(生产由 **Nginx 直接服务 media/下载文件、不走 Django**,与 T-403 static/media serving 一致,大文件不占 gunicorn worker);`external_url` 预留,后续切对象存储只改后台链接不改代码。**安全**:下载走 HTTPS,页面展示 SHA256 供校验;**代码签名**作为决策登记——未签名 Windows 安装包会被 SmartScreen 拦「未知发布者」、macOS 被 Gatekeeper 拦,首页先给「如何忽略警告」说明,正式签名后续补(挂 Backlog / deployment)。**含测试**:`/` 匿名 200 不跳登录、下载区展示当前 release、无 current release 优雅处理、已登录用户显示「进入控制台」。**视觉原型(已定 v1)**:按 `prototypes/cmhub-homepage-v1.svg` 落地——「生成台」方向:靛蓝=生成 / 琥珀=点数;Hero 为「商品图 + 一句话 → 吸睛标题 + 生成主图 + 点数计量」转化图;四步上手 01–04、两张能力卡、深色计费 band、下载区(版本/SHA256/未签名提示);配色、间距、结构照此原型转成 Django 模板(Bootstrap + 本地 static)。**视觉一致性(brand token)**:按 [`brand.md`](brand.md) 抽出共享 CSS 变量(如 `apps/portal/static/portal/brand.css`),**首页与现有 portal 页面(dashboard/记录/充值/API Key)一起套用同一套 token**(把配色/字体变量灌进现有 Bootstrap,各页只引用变量不散写 hex),确保落地页与登录后控制台风格一致——**不是只做漂亮首页**。验收 `check`/`test`/`init` 全绿并在 `../progress.md` 留证据 | DONE |
## 里程碑
diff --git a/docs/api.md b/docs/api.md
index 6b90add..f640033 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -41,6 +41,8 @@ T-503/T-505 已实现用户端个人中心 / 记录页基线:`/dashboard` 走
T-504/T-505 已实现用户端充值页基线:`/recharge` 走 Django session + CSRF,GET 展示余额、充值表单、当前订单和最近充值;POST 创建 pending 充值订单并用本地 static 自托管的 qrcode.js 展示支付二维码票据;浏览器轮询 `GET /api/v1/recharge/status`,订单 paid 后刷新页面重新读取余额。页面不直接加点,到账仍以支付回调或主动查单入账后的本地订单状态为准。若当前 `PAYMENT_CALLBACK_MODE=mock`,页面必须明确提示二维码为测试票据、不能用微信/支付宝真实支付,避免用户把 mock 二维码当成生产收款码。当前因支付宝可信 IP 未配置,用户端 `/recharge` 表单暂只开放微信支付,底层 JSON API / 回调 / SDK 路径仍保留支付宝。
+T-606 已实现公开首页与客户端下载入口:`GET /` 匿名返回 200,不再重定向到 `/dashboard`;匿名用户看到注册 / 登录 / 下载入口,登录用户看到「进入控制台」。下载区读取 `DownloadRelease(platform=windows, is_current=True)`,展示版本、下载按钮、SHA256 和发布说明;`external_url` 优先于后台上传文件的 `file.url`。`DownloadRelease` 不新增对外 JSON API,只由 SSR 首页和 django-admin 使用;生产本地安装包由 Nginx 直接服务 `MEDIA_ROOT/downloads/`,避免大文件占用 Gunicorn worker。
+
通用错误响应:
```json
diff --git a/docs/current-state.md b/docs/current-state.md
index df6b625..bb7ba93 100644
--- a/docs/current-state.md
+++ b/docs/current-state.md
@@ -12,11 +12,12 @@
## 当前快照
- 日期:2026-07-06
-- 阶段:Phase 6 增强(MVP 后);Phase 3 对外 API 与充值已完成到 T-306,Phase 4 用户端 T-501 注册 / 登录(allauth)、T-502 API Key 自助管理页、T-503 个人中心 / 记录页、T-504 充值页与 T-505 用户端审核优化已完成,Phase 5 T-401 运营后台完善、T-402 MVP 完整验收与 T-403 部署 / 运行文档已完成,Phase 6 T-601 可用别名发现、T-602 django-admin 中文化第 1-3 层、T-603 django-admin 字段级中文化、T-604 中文敏感词本地过滤、T-605 免邮箱验证策略落地已完成;下一步可进入 T-606 公开首页 + 客户端下载入口
+- 阶段:Phase 6 增强(MVP 后);Phase 3 对外 API 与充值已完成到 T-306,Phase 4 用户端 T-501 注册 / 登录(allauth)、T-502 API Key 自助管理页、T-503 个人中心 / 记录页、T-504 充值页与 T-505 用户端审核优化已完成,Phase 5 T-401 运营后台完善、T-402 MVP 完整验收与 T-403 部署 / 运行文档已完成,Phase 6 T-601 可用别名发现、T-602 django-admin 中文化第 1-3 层、T-603 django-admin 字段级中文化、T-604 中文敏感词本地过滤、T-605 免邮箱验证策略落地、T-606 公开首页 + 客户端下载入口已完成;后续可继续处理真实支付回调到账闭环、客户端下载包发布或真实图片耗时验证
- 技术栈:系统 Python 3.12.3 + Django 5.2.15 + DRF 3.16.1 + django-allauth 65.18.0 + PyMySQL 1.1.3 + cryptography 49.0.0 + requests 2.34.2 + ahocorapy 1.6.2 + wechatpayv3 2.0.2 + python-alipay-sdk 3.4.0 + django-admin;MySQL 8.4 已接入 settings,并支持 `MYSQL_CONNECT_TIMEOUT` / `MYSQL_READ_TIMEOUT` / `MYSQL_WRITE_TIMEOUT`;用户端已用 Django 模板 SSR + Bootstrap + allauth 落地注册登录;生产部署口径为 VPS / 宝塔 + Nginx + Gunicorn(gthread) + systemd;详见 `03-tech-stack.md` 与 `deployment.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;T-104/T-105 已完成录制 title/image smoke 与审核修补;T-201 已新增 `UserWallet` / `ApiKey`、`PointsLedger` / `CallRecord`、对应 admin 与迁移;T-202 已新增 `PricingRule` / `ExchangeRate`、`apps.billing.pricing` 计费计算函数、admin 配置页与迁移;T-203 已新增 `apps.billing.services`,实现并发安全预扣、成功确认与幂等失败退点;T-204 已新增 `billing.0003_pointsledger_unique_ledger_change_type_per_call`,用 MySQL 可落地的 `ref_call + change_type` 复合唯一约束兜底防重复 refund;T-301 已新增 `apps.api.authentication.ApiKeyAuthentication` 与 `ExternalApiView`;T-302 已新增生成接口编排、序列化器、图片本地存储和 `/api/v1/generate/title|image` 路由;T-303 已新增 `apps.billing.services.get_balance_snapshot()` 与 `/api/v1/balance` 余额查询接口;T-304 已新增 `RechargeOrder`、充值回调验签适配器、幂等入账服务、微信/支付宝回调路由与迁移 `billing.0004_rechargeorder_and_more`;T-305 已新增 `create_recharge_order()`、微信/支付宝扫码下单 mock/SDK 入口、`/api/v1/recharge/create` 与 `/api/v1/recharge/status`;T-306 已新增 `apps.api.throttles`、`apps.api.exceptions`、`REST_FRAMEWORK` 安全默认认证、生成/认证失败限流、`image_url` SSRF 防护与响应大小上限、充值单笔金额上限;T-501 已接入 allauth,新增 portal 路由、注册适配器、登录/注册/登出模板和最小 dashboard,注册成功创建 0 点钱包且不写赠点流水;T-502 已新增 `/apikeys`、API Key 创建表单、列表页和删除(吊销)动作,生成后明文只显示一次,列表只显示 prefix;T-503 已扩展 `/dashboard` 为个人中心汇总,并新增 `/records/recharge` 充值记录与 `/records/usage` 消费记录,只读展示当前用户数据;T-504 已新增 `/recharge` 页面、`RechargeCreateForm`、充值导航入口和轮询脚本,页面创建 pending 订单、展示二维码票据、轮询 `/api/v1/recharge/status`,订单 paid 后刷新余额;T-505 已把 Bootstrap 5 CSS 与 qrcode.js vendoring 到 `apps/portal/static/portal/vendor/`,页面不再依赖 jsdelivr,并把充值记录 / 消费记录改为 Django `Paginator` 分页;T-401 已新增 `adjust_wallet_points()` 手工调点服务、钱包 admin 专用调点表单与模板,后台可管理/检索用户、钱包、API Key(脱敏)、计费规则、汇率、充值订单、点数流水和调用记录,流水/订单/调用记录保持只读;T-402 已新增 `docs/mvp-acceptance.md`,按 P0 验收矩阵记录 MVP 完整验收结论、测试证据和已知限制;T-403 已新增 `docs/deployment.md` 与 `requirements-production.txt`,并在 settings 中补齐 `STATIC_ROOT`、`CSRF_TRUSTED_ORIGINS`、共享 `CACHES`、HTTPS cookie、proxy SSL 与 HSTS 环境变量支持;T-601 已新增 `apps.ai.catalog.get_public_model_catalog()`、`GET /api/v1/models` 与 portal `/models` 只读页面,只展示 active 可调用别名、能力、是否需要原图和点数单价,不解密 provider key,不暴露底层 SKU / URL / key / `extra_body`;T-604 已新增 `apps.moderation`、`SensitiveWord` 模型/admin/迁移、keyword provider、归一化管线和共享 cache 版本失效,生成接口已改为 prompt 先审再读取图片/计费/扣点/调上游
+- 生产代码:已有最小 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/T-105 已完成录制 title/image smoke 与审核修补;T-201 已新增 `UserWallet` / `ApiKey`、`PointsLedger` / `CallRecord`、对应 admin 与迁移;T-202 已新增 `PricingRule` / `ExchangeRate`、`apps.billing.pricing` 计费计算函数、admin 配置页与迁移;T-203 已新增 `apps.billing.services`,实现并发安全预扣、成功确认与幂等失败退点;T-204 已新增 `billing.0003_pointsledger_unique_ledger_change_type_per_call`,用 MySQL 可落地的 `ref_call + change_type` 复合唯一约束兜底防重复 refund;T-301 已新增 `apps.api.authentication.ApiKeyAuthentication` 与 `ExternalApiView`;T-302 已新增生成接口编排、序列化器、图片本地存储和 `/api/v1/generate/title|image` 路由;T-303 已新增 `apps.billing.services.get_balance_snapshot()` 与 `/api/v1/balance` 余额查询接口;T-304 已新增 `RechargeOrder`、充值回调验签适配器、幂等入账服务、微信/支付宝回调路由与迁移 `billing.0004_rechargeorder_and_more`;T-305 已新增 `create_recharge_order()`、微信/支付宝扫码下单 mock/SDK 入口、`/api/v1/recharge/create` 与 `/api/v1/recharge/status`;T-306 已新增 `apps.api.throttles`、`apps.api.exceptions`、`REST_FRAMEWORK` 安全默认认证、生成/认证失败限流、`image_url` SSRF 防护与响应大小上限、充值单笔金额上限;T-501 已接入 allauth,新增 portal 路由、注册适配器、登录/注册/登出模板和最小 dashboard,注册成功创建 0 点钱包且不写赠点流水;T-502 已新增 `/apikeys`、API Key 创建表单、列表页和删除(吊销)动作,生成后明文只显示一次,列表只显示 prefix;T-503 已扩展 `/dashboard` 为个人中心汇总,并新增 `/records/recharge` 充值记录与 `/records/usage` 消费记录,只读展示当前用户数据;T-504 已新增 `/recharge` 页面、`RechargeCreateForm`、充值导航入口和轮询脚本,页面创建 pending 订单、展示二维码票据、轮询 `/api/v1/recharge/status`,订单 paid 后刷新余额;T-505 已把 Bootstrap 5 CSS 与 qrcode.js vendoring 到 `apps/portal/static/portal/vendor/`,页面不再依赖 jsdelivr,并把充值记录 / 消费记录改为 Django `Paginator` 分页;T-401 已新增 `adjust_wallet_points()` 手工调点服务、钱包 admin 专用调点表单与模板,后台可管理/检索用户、钱包、API Key(脱敏)、计费规则、汇率、充值订单、点数流水和调用记录,流水/订单/调用记录保持只读;T-402 已新增 `docs/mvp-acceptance.md`,按 P0 验收矩阵记录 MVP 完整验收结论、测试证据和已知限制;T-403 已新增 `docs/deployment.md` 与 `requirements-production.txt`,并在 settings 中补齐 `STATIC_ROOT`、`CSRF_TRUSTED_ORIGINS`、共享 `CACHES`、HTTPS cookie、proxy SSL 与 HSTS 环境变量支持;T-601 已新增 `apps.ai.catalog.get_public_model_catalog()`、`GET /api/v1/models` 与 portal `/models` 只读页面,只展示 active 可调用别名、能力、是否需要原图和点数单价,不解密 provider key,不暴露底层 SKU / URL / key / `extra_body`;T-604 已新增 `apps.moderation`、`SensitiveWord` 模型/admin/迁移、keyword provider、归一化管线和共享 cache 版本失效,生成接口已改为 prompt 先审再读取图片/计费/扣点/调上游;T-606 已新增公开首页 `/`、`DownloadRelease` 模型/admin/迁移、首页 SSR 模板、共享 `portal/brand.css`,并把现有 portal 页面套入同一套品牌 token。
- 用户端导航:顶部导航 active 状态已修复,`portal/base.html` 基于 `request.resolver_match.url_name` 高亮当前页面入口,并用 `aria-current="page"` 标记;「充值」不再在非充值页固定深色高亮。
- 测试:T-601 已验证:`.\init.ps1` 通过(Python 3.12.3,依赖已满足,`manage.py check` 0 issues,打印启动命令);`py -3.12 -m py_compile apps\ai\catalog.py apps\api\views.py apps\api\urls.py apps\api\tests.py apps\portal\views.py apps\portal\urls.py apps\portal\tests.py` 通过;`py -3.12 manage.py check` 通过;`py -3.12 manage.py makemigrations --check --dry-run` 无变化;`git diff --check` 通过;`py -3.12 manage.py test apps.api.tests.ModelsCatalogApiTests apps.portal.tests.PortalAccountFlowTests.test_models_page_requires_session_login apps.portal.tests.PortalAccountFlowTests.test_models_page_lists_public_aliases_prices_and_unpriced_state --keepdb --noinput --verbosity 2` 首次运行时 3 条 API 测试已通过,portal `setUpClass` 阶段因远程 MySQL 连接 43.128.3.240 超时中断;随后 `Test-NetConnection 43.128.3.240 -Port 3306` 显示 `TcpTestSucceeded=True`,单独重跑两条 portal 测试通过,2 tests OK。T-403 已验证:`./init.ps1` 开工前通过;`py -3.12 manage.py check` 通过;`py -3.12 manage.py makemigrations --check --dry-run` 无变化;`py -3.12 manage.py findstatic portal/vendor/bootstrap/bootstrap.min.css portal/vendor/qrcode/qrcode.js --verbosity 1` 找到两个本地 static 文件;`py -3.12 manage.py check --deploy` 在当前开发 `.env` 下仅报预期安全配置警告,临时注入生产型安全环境变量(含 HSTS includeSubDomains/preload)后通过,0 issues;`py -3.12 manage.py collectstatic --dry-run --noinput` 通过,预期收集 169 个 static 文件;`py -3.12 manage.py createcachetable --dry-run cmhub_cache` 通过,输出 MySQL cache 表 DDL;`py -3.12 -m compileall config` 通过;尝试 `py -3.12 manage.py test apps.api --noinput --keepdb --verbosity 2` 时 25 条用例已通过,`GenerateApiTests` 14 条因远程 MySQL 连接重置/事务中断被记 ERROR,随后 `Test-NetConnection 43.128.3.240 -Port 3306` 端口可达,单独重跑 `py -3.12 manage.py test apps.api.tests.GenerateApiTests --noinput --keepdb --verbosity 2` 通过,14 tests OK。测试/迁移阶段仍有 allauth `account.EmailAddress` 条件唯一约束在 MySQL 上不可创建的 `models.W036` 警告;本项目用户账本邮箱唯一性由 `user.email` 唯一约束承担。
+- 最新验证:T-606 公开首页 + 客户端下载入口已验证 `.\init.ps1` 通过(Python 3.12.3,依赖已满足,`manage.py check` 0 issues,打印启动命令);`py -3.12 -m py_compile apps\portal\models.py apps\portal\admin.py apps\portal\views.py apps\portal\urls.py apps\portal\tests.py` 通过;`py -3.12 manage.py check` 通过,0 issues;`py -3.12 manage.py makemigrations --check --dry-run` 通过,No changes detected;`py -3.12 manage.py migrate portal` 已应用 `portal.0001_initial` 到当前数据库;`py -3.12 manage.py shell -c "... Client().get('/') ..."` 返回 `STATUS=200`、首页标识和下载区标识均存在;`py -3.12 manage.py findstatic portal/brand.css portal/vendor/bootstrap/bootstrap.min.css portal/vendor/qrcode/qrcode.js --verbosity 1` 均找到;`py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests.test_homepage_is_public_and_shows_anonymous_onboarding_without_release apps.portal.tests.PortalAccountFlowTests.test_homepage_shows_current_download_release apps.portal.tests.PortalAccountFlowTests.test_homepage_shows_dashboard_entry_for_authenticated_user apps.portal.tests.PortalAccountFlowTests.test_download_release_only_keeps_one_current_per_platform apps.portal.tests.PortalAccountFlowTests.test_authenticated_nav_highlights_current_page_only --noinput --verbosity 2` 通过,5 tests OK。`py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests --keepdb --noinput --verbosity 2` 已重跑,因远程 MySQL `43.128.3.240` 在测试库迁移阶段连接超时(2003 / WinError 10060)中断,不是断言失败;数据库恢复后需补跑 portal 整组。
- 最新验证:T-605 免邮箱验证策略已验证 `py -3.12 -m py_compile config\settings.py apps\portal\tests.py` 通过;`py -3.12 manage.py check` 通过,0 issues;`py -3.12 manage.py makemigrations --check --dry-run` 通过,No changes detected;更新后的注册 / 登录目标测试 3 tests OK;`py -3.12 manage.py test apps.portal --keepdb --noinput --verbosity 2` 通过,26 tests OK;`git diff --check` 通过,仅 Windows CRLF 提示。全量 `py -3.12 manage.py test --keepdb --noinput --verbosity 2` 已尝试,发现 131 tests,跑到 95 tests 后远程 MySQL `43.128.3.240` 出现连接超时 / WinError 10051,最终 22 errors;随后 `Test-NetConnection 43.128.3.240 -Port 3306` 显示 `TcpTestSucceeded=True`,单独复跑失败起点的 billing 分组时 17 条业务用例 OK,并发扣点用例本体打印 `ok` 后在测试库 flush 阶段再次因 MySQL 连接超时 / WinError 10060 记 ERROR。结论:T-605 相关测试已通过,全量单次绿受远程 MySQL 稳定性阻塞。
- 最新验证:T-604 中文敏感词本地过滤已验证 `py -3.12 -m py_compile config\settings.py apps\api\generation.py apps\api\tests.py apps\moderation\apps.py apps\moderation\models.py apps\moderation\normalization.py apps\moderation\policy.py apps\moderation\services.py apps\moderation\signals.py apps\moderation\versioning.py apps\moderation\providers\base.py apps\moderation\providers\keyword.py apps\moderation\tests.py` 通过;`py -3.12 manage.py check` 通过,0 issues;`py -3.12 manage.py makemigrations --check --dry-run` 通过,No changes detected;`py -3.12 manage.py test apps.moderation --keepdb --noinput --verbosity 2` 通过,7 tests OK;新增生成接口目标测试 2 tests OK;`py -3.12 manage.py test apps.api.tests.GenerateApiTests --keepdb --noinput --verbosity 2` 通过,16 tests OK。测试期仅保留 allauth 在 MySQL 条件唯一约束上的既有 `models.W036` 警告。
- 最新验证:T-603 django-admin 字段级中文化已收尾:人工确认 `/admin/` 字段标签中文化;`py -3.12 manage.py check` 通过,0 issues;`py -3.12 manage.py makemigrations --check --dry-run` 通过,No changes detected;`py -3.12 manage.py test apps.users.tests apps.billing.tests.BillingAdminTests apps.ai.tests.AiModelAdminTests apps.ai.tests.AiConfigAuditAdminTests --keepdb --noinput --verbosity 2` 通过,15 tests OK。测试期仅保留 allauth 在 MySQL 条件唯一约束上的既有 `models.W036` 警告。
@@ -42,7 +43,7 @@
| `init.sh` / `init.ps1` | 已有 | 启动验证入口,已固定系统 Python 3.12 命令,并校验解释器版本 `>=3.12,<3.14` |
| `requirements.txt` / `requirements-production.txt` / `pyproject.toml` | 已有 | `requirements.txt` 管本地/基础运行依赖;`requirements-production.txt` 追加 Linux 生产 Gunicorn;`pyproject.toml` 落地 `requires-python`;T-101 新增 `requests`;T-102 使用既有 `cryptography` 做 Fernet 加密;T-501 新增 `django-allauth` |
| `config/`(Django 工程) | 已有 | T-001 创建,含 settings / urls / wsgi / asgi |
-| `apps/`(users/portal/billing/ai/api/moderation) | 已有 | 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/T-105 已新增 `smoke_ai_generation` 录制 title/image smoke 命令;T-201 已在 users 落 `UserWallet` / `ApiKey`,在 billing 落 `PointsLedger` / `CallRecord`;T-202 已在 billing 落 `PricingRule` / `ExchangeRate` 与 `pricing.py`;T-203/T-303/T-304/T-305 已在 `apps/billing/services.py` 落扣点/退点、余额快照、充值入账与充值下单;T-401 已在 `apps/billing/services.py` 落 `adjust_wallet_points()`,在 `apps/users/admin.py` 与 `apps/users/templates/admin/users/userwallet/adjust_points.html` 落钱包手工调点入口;T-304/T-305 已在 `apps/billing/payment_gateways.py` 落回调验签、mock 下单与 SDK 入口;T-301~T-306 已在 api 落鉴权、生成接口编排、序列化器、图片存储、余额查询、充值回调、充值下单/状态查询、`image_url` SSRF 防护、生成/认证限流与统一 429 错误响应;T-501 已在 portal 落 allauth 注册/登录/登出路由、模板、adapter 与 dashboard;T-502 已在 portal 落 `/apikeys`、API Key 创建表单、列表模板与删除(吊销)动作;T-503 已在 portal 落个人中心汇总、充值记录和消费记录页;T-504 已在 portal 落 `/recharge` 充值页、充值表单、二维码票据展示和状态轮询;T-505 已在 portal 落本地 vendor static 与记录分页;T-601 已在 `apps/ai/catalog.py` 落公开模型目录,在 api 落 `/api/v1/models`,在 portal 落 `/models` 页面和导航入口;T-604 已在 moderation 落 `SensitiveWord`、keyword matcher、归一化、共享 cache 版本失效与 admin |
+| `apps/`(users/portal/billing/ai/api/moderation) | 已有 | 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/T-105 已新增 `smoke_ai_generation` 录制 title/image smoke 命令;T-201 已在 users 落 `UserWallet` / `ApiKey`,在 billing 落 `PointsLedger` / `CallRecord`;T-202 已在 billing 落 `PricingRule` / `ExchangeRate` 与 `pricing.py`;T-203/T-303/T-304/T-305 已在 `apps/billing/services.py` 落扣点/退点、余额快照、充值入账与充值下单;T-401 已在 `apps/billing/services.py` 落 `adjust_wallet_points()`,在 `apps/users/admin.py` 与 `apps/users/templates/admin/users/userwallet/adjust_points.html` 落钱包手工调点入口;T-304/T-305 已在 `apps/billing/payment_gateways.py` 落回调验签、mock 下单与 SDK 入口;T-301~T-306 已在 api 落鉴权、生成接口编排、序列化器、图片存储、余额查询、充值回调、充值下单/状态查询、`image_url` SSRF 防护、生成/认证限流与统一 429 错误响应;T-501 已在 portal 落 allauth 注册/登录/登出路由、模板、adapter 与 dashboard;T-502 已在 portal 落 `/apikeys`、API Key 创建表单、列表模板与删除(吊销)动作;T-503 已在 portal 落个人中心汇总、充值记录和消费记录页;T-504 已在 portal 落 `/recharge` 充值页、充值表单、二维码票据展示和状态轮询;T-505 已在 portal 落本地 vendor static 与记录分页;T-601 已在 `apps/ai/catalog.py` 落公开模型目录,在 api 落 `/api/v1/models`,在 portal 落 `/models` 页面和导航入口;T-604 已在 moderation 落 `SensitiveWord`、keyword matcher、归一化、共享 cache 版本失效与 admin;T-606 已在 portal 落公开首页、`DownloadRelease`、下载版本 admin、`brand.css` 与首页模板 |
| `manage.py` | 已有 | T-001 创建 |
| `tests/` | 待建 | 随各任务补充 |
@@ -50,11 +51,11 @@
任务状态以 [`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-104 跑通一次录制标题生成;T-105 Phase 1 AI 层审核修补;T-201 User / UserWallet / ApiKey / PointsLedger / CallRecord 模型;T-202 PricingRule / ExchangeRate 模型 + 计费计算;T-203 并发安全扣点 / 退点;T-204 Phase 2 计费核心审核加固;T-301 API Key 鉴权;T-302 生成标题 / 图片接口;T-303 余额查询接口;T-304 充值回调;T-305 扫码充值下单 + 轮询;T-306 Phase 3 对外 API 安全加固;T-501 注册 / 登录(allauth);T-502 API Key 自助管理页;T-503 个人中心 / 记录页;T-504 充值页(扫码 + 轮询到账);T-505 Phase 4 用户端审核优化;T-401 运营后台完善;T-402 完整验收 MVP;T-403 部署 / 运行文档;T-601 可用别名发现;T-602 django-admin 中文化(第 1-3 层);T-603 django-admin 中文化(第 4 层·字段级);T-604 中文敏感词本地过滤;T-605 免邮箱验证策略落地。
+- 已完成: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 跑通一次录制标题生成;T-105 Phase 1 AI 层审核修补;T-201 User / UserWallet / ApiKey / PointsLedger / CallRecord 模型;T-202 PricingRule / ExchangeRate 模型 + 计费计算;T-203 并发安全扣点 / 退点;T-204 Phase 2 计费核心审核加固;T-301 API Key 鉴权;T-302 生成标题 / 图片接口;T-303 余额查询接口;T-304 充值回调;T-305 扫码充值下单 + 轮询;T-306 Phase 3 对外 API 安全加固;T-501 注册 / 登录(allauth);T-502 API Key 自助管理页;T-503 个人中心 / 记录页;T-504 充值页(扫码 + 轮询到账);T-505 Phase 4 用户端审核优化;T-401 运营后台完善;T-402 完整验收 MVP;T-403 部署 / 运行文档;T-601 可用别名发现;T-602 django-admin 中文化(第 1-3 层);T-603 django-admin 中文化(第 4 层·字段级);T-604 中文敏感词本地过滤;T-605 免邮箱验证策略落地;T-606 公开首页 + 客户端下载入口。
- 正在进行:无。
-- 待开始:T-606 公开首页 + 客户端下载入口;之后可继续处理真实支付、AI 模型导入与真实图片耗时验证,或从 Backlog 拆新任务。
+- 待开始:看板当前无新的编号任务;之后可继续处理真实支付回调到账闭环、配置并发布客户端下载包、真实图片耗时验证,或从 Backlog 拆新任务。
- 当前 blocker:支付商户真实密钥/证书与生产 SDK 依赖仍待提供;微信回调到账闭环仍需真实支付验收;真实 AI 标题生成已在线上跑通,图片生成真实耗时仍需补测。
-- 下一个可领取任务:T-606 公开首页 + 客户端下载入口。
+- 下一个可领取任务:看板当前无新的编号任务,需先确认下一项业务优先级或新增任务。
## 当前可运行内容
@@ -99,6 +100,7 @@ gunicorn config.wsgi:application --bind 127.0.0.1:8002 --workers 1 --worker-clas
当前用户端页面:
+- `GET /`
- `GET/POST /signup`
- `GET/POST /login`
- `GET/POST /logout`
@@ -110,15 +112,15 @@ gunicorn config.wsgi:application --bind 127.0.0.1:8002 --workers 1 --worker-clas
- `GET /records/recharge`
- `GET /records/usage`
-当前骨架可运行。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/T-105 已用临时回滚配置跑通录制标题和录制图片生成。T-201 已落地钱包、API Key、点数流水和调用记录:API Key 明文只在创建 helper 返回,库内只存 hash/prefix;CallRecord 只存 `result_ref`/`result_summary`,没有 provider raw 字段。T-202 已落地 `PricingRule` / `ExchangeRate`:计费按 `operation_type + alias + resolution` 查 active 规则,优先精确分辨率,再回退默认价;缺规则抛 `NoPricingRuleError(code="no_pricing_rule")`;金额换点数按当前 active 汇率向下取整。T-203 已落地 `precharge_call()` / `mark_call_success()` / `refund_call_points()`:预扣锁钱包行,余额不足不写调用/流水;失败退点锁调用记录并幂等写 refund 流水。T-204 已完成复合唯一约束加固,并取得一次完整 `manage.py test` 单次全绿。T-301 已落地 `Authorization: Bearer
` 鉴权:成功后 `request.user` 为所属用户、`request.auth` 为 `ApiKey`,缺失/无效 Key 返回 401,用户或 Key 禁用返回 403,外部 API 不接受 Web session。T-302 已落地生成接口:请求别名解析后按规则计费,预扣成功才调用 Provider,成功确认调用记录,`AiProviderError` / `AiCapabilityError` 等失败路径会退点;图片结果保存到本地 media 并返回 URL。T-303 已落地余额查询接口:`GET /api/v1/balance` 继承外部 API Key 鉴权,读取 billing 余额快照并返回兼容字段 `user` / `points_balance`,以及不含邮箱的 `account.username` / `account.display_name`,测试覆盖余额与流水累加一致。T-304 已落地充值回调:`RechargeOrder` 保存下单锁定的金额/汇率/点数,微信/支付宝回调先验签再按订单幂等入账,重复回调不重复加点,金额不一致不入账;主动查单兜底可调用 `query_and_apply_recharge_payment(order_no, query_func)` 复用同一入账路径。T-305 已落地扫码下单与轮询:用户端 session 登录后可 `POST /api/v1/recharge/create` 创建 pending 订单并拿到 mock/SDK 二维码票据,`GET /api/v1/recharge/status` 只返回本人订单并在 pending 时尝试主动查单补入账;API Key 不能调用这两个用户端接口。T-306 已落地对外 API 安全加固:`image_url` 下载在扣点前做协议白名单、公网地址校验、重定向逐跳校验和响应大小上限;DRF 全局默认不再隐式启用 Session/Basic;生成接口按 Key 限流,认证失败按 IP 限流;充值下单有单笔金额上限。T-501/T-605 已落地 allauth 注册 / 登录:`ACCOUNT_EMAIL_VERIFICATION="none"`,免邮箱验证、注册即可用,邮箱仍必填且唯一;注册成功创建 0 点钱包、不写点数流水。T-502 已落地 API Key 自助管理:`/apikeys` 登录访问,生成后完整明文只显示一次,列表只显示 prefix,不显示 hash 或历史明文;删除为吊销 `revoked`,吊销后外部 API 返回 403。T-503 已落地个人中心与记录页:`/dashboard` 展示剩余点数、充值总额、入账点数、净消耗点数和最近记录;`/records/recharge` 展示当前用户充值订单;`/records/usage` 展示当前用户 consume/refund 点数流水并关联调用信息;所有页面均只读且只查本人。T-504 已落地充值页:`/recharge` GET 展示余额、充值表单、当前订单和最近充值,POST 创建 pending 订单并展示二维码票据,浏览器轮询 `/api/v1/recharge/status`,paid 后刷新页面重新读取余额;页面不直接写钱包或流水。T-401 已落地运营后台完善:用户列表显示钱包余额,钱包余额只读且通过专用表单手工调点,调点必须填原因、非 0、不得扣成负数,并经 `adjust_wallet_points()` 锁钱包写 `PointsLedger(adjust)`;API Key admin 只展示 prefix 和 hash 摘要,不回显明文或完整 hash;订单、流水、调用记录继续只读并增强检索。T-402 已完成 MVP P0 验收并新增 `docs/mvp-acceptance.md`。T-403 已完成部署 / 运行文档,生产按 `deployment.md` 执行,并已补 settings 对生产静态目录、共享 cache、CSRF trusted origins、HTTPS cookie/proxy/HSTS 的环境变量支持。T-601 已落地可用别名发现:`GET /api/v1/models` 用 API Key 鉴权返回公开别名目录,`/models` 用 session 展示只读「可用模型」页;两者均不解密 provider key,不输出底层 SKU、URL、key 或 `extra_body`。T-603 已完成 django-admin 字段级中文化并人工确认字段标签中文。T-604 已落地本地 prompt 敏感词过滤:`MODERATION_ENABLED=false` 默认 no-op,启用 `keyword` 后命中返回 `content_blocked`,并在下载 `image_url`、解析别名、计费、预扣点和上游调用前拦截。真实标题上游生成已在线上跑通并验证扣点;图片生成真实耗时仍需补测。
+当前骨架可运行。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/T-105 已用临时回滚配置跑通录制标题和录制图片生成。T-201 已落地钱包、API Key、点数流水和调用记录:API Key 明文只在创建 helper 返回,库内只存 hash/prefix;CallRecord 只存 `result_ref`/`result_summary`,没有 provider raw 字段。T-202 已落地 `PricingRule` / `ExchangeRate`:计费按 `operation_type + alias + resolution` 查 active 规则,优先精确分辨率,再回退默认价;缺规则抛 `NoPricingRuleError(code="no_pricing_rule")`;金额换点数按当前 active 汇率向下取整。T-203 已落地 `precharge_call()` / `mark_call_success()` / `refund_call_points()`:预扣锁钱包行,余额不足不写调用/流水;失败退点锁调用记录并幂等写 refund 流水。T-204 已完成复合唯一约束加固,并取得一次完整 `manage.py test` 单次全绿。T-301 已落地 `Authorization: Bearer ` 鉴权:成功后 `request.user` 为所属用户、`request.auth` 为 `ApiKey`,缺失/无效 Key 返回 401,用户或 Key 禁用返回 403,外部 API 不接受 Web session。T-302 已落地生成接口:请求别名解析后按规则计费,预扣成功才调用 Provider,成功确认调用记录,`AiProviderError` / `AiCapabilityError` 等失败路径会退点;图片结果保存到本地 media 并返回 URL。T-303 已落地余额查询接口:`GET /api/v1/balance` 继承外部 API Key 鉴权,读取 billing 余额快照并返回兼容字段 `user` / `points_balance`,以及不含邮箱的 `account.username` / `account.display_name`,测试覆盖余额与流水累加一致。T-304 已落地充值回调:`RechargeOrder` 保存下单锁定的金额/汇率/点数,微信/支付宝回调先验签再按订单幂等入账,重复回调不重复加点,金额不一致不入账;主动查单兜底可调用 `query_and_apply_recharge_payment(order_no, query_func)` 复用同一入账路径。T-305 已落地扫码下单与轮询:用户端 session 登录后可 `POST /api/v1/recharge/create` 创建 pending 订单并拿到 mock/SDK 二维码票据,`GET /api/v1/recharge/status` 只返回本人订单并在 pending 时尝试主动查单补入账;API Key 不能调用这两个用户端接口。T-306 已落地对外 API 安全加固:`image_url` 下载在扣点前做协议白名单、公网地址校验、重定向逐跳校验和响应大小上限;DRF 全局默认不再隐式启用 Session/Basic;生成接口按 Key 限流,认证失败按 IP 限流;充值下单有单笔金额上限。T-501/T-605 已落地 allauth 注册 / 登录:`ACCOUNT_EMAIL_VERIFICATION="none"`,免邮箱验证、注册即可用,邮箱仍必填且唯一;注册成功创建 0 点钱包、不写点数流水。T-502 已落地 API Key 自助管理:`/apikeys` 登录访问,生成后完整明文只显示一次,列表只显示 prefix,不显示 hash 或历史明文;删除为吊销 `revoked`,吊销后外部 API 返回 403。T-503 已落地个人中心与记录页:`/dashboard` 展示剩余点数、充值总额、入账点数、净消耗点数和最近记录;`/records/recharge` 展示当前用户充值订单;`/records/usage` 展示当前用户 consume/refund 点数流水并关联调用信息;所有页面均只读且只查本人。T-504 已落地充值页:`/recharge` GET 展示余额、充值表单、当前订单和最近充值,POST 创建 pending 订单并展示二维码票据,浏览器轮询 `/api/v1/recharge/status`,paid 后刷新页面重新读取余额;页面不直接写钱包或流水。T-401 已落地运营后台完善:用户列表显示钱包余额,钱包余额只读且通过专用表单手工调点,调点必须填原因、非 0、不得扣成负数,并经 `adjust_wallet_points()` 锁钱包写 `PointsLedger(adjust)`;API Key admin 只展示 prefix 和 hash 摘要,不回显明文或完整 hash;订单、流水、调用记录继续只读并增强检索。T-402 已完成 MVP P0 验收并新增 `docs/mvp-acceptance.md`。T-403 已完成部署 / 运行文档,生产按 `deployment.md` 执行,并已补 settings 对生产静态目录、共享 cache、CSRF trusted origins、HTTPS cookie/proxy/HSTS 的环境变量支持。T-601 已落地可用别名发现:`GET /api/v1/models` 用 API Key 鉴权返回公开别名目录,`/models` 用 session 展示只读「可用模型」页;两者均不解密 provider key,不输出底层 SKU、URL、key 或 `extra_body`。T-603 已完成 django-admin 字段级中文化并人工确认字段标签中文。T-604 已落地本地 prompt 敏感词过滤:`MODERATION_ENABLED=false` 默认 no-op,启用 `keyword` 后命中返回 `content_blocked`,并在下载 `image_url`、解析别名、计费、预扣点和上游调用前拦截。T-606 已落地公开首页:`GET /` 匿名 200、不跳登录;下载区读取 Windows 当前 `DownloadRelease`,优先 `external_url`,无当前版本显示「暂未发布」;现有 portal 页面已共用 `brand.css` 品牌 token。真实标题上游生成已在线上跑通并验证扣点;图片生成真实耗时仍需补测。
## 开始编码前检查
1. 读仓库级 `AGENTS.md` / `CLAUDE.md`。
2. 读 `docs/00-ai-start-here.md`。
3. 读 `docs/05-coding-rules.md`(尤其第 8 节资金安全)。
-4. 在 `docs/06-tasks.md` 领取第一个 `TODO` 且依赖均 `DONE` 的任务;当前 T-606 为 `TODO`。
-5. 当前下一个任务是 T-606:公开首页 + 客户端下载入口。
+4. 在 `docs/06-tasks.md` 领取第一个 `TODO` 且依赖均 `DONE` 的任务;T-606 已完成,看板当前无新的编号任务。
+5. 下一步需先确认业务优先级,或把真实支付回调到账闭环、客户端下载包发布、真实图片耗时验证拆成新任务。
## 维护规则
diff --git a/docs/project-brief.md b/docs/project-brief.md
index 2e538bc..e5d805e 100644
--- a/docs/project-brief.md
+++ b/docs/project-brief.md
@@ -108,8 +108,8 @@
- Phase 5 已完成 T-401:运营后台可管理/检索用户、钱包、API Key(脱敏)、计费规则、汇率、充值订单、点数流水和调用记录;手工调点必须填写原因,并经计费层锁钱包、写 `adjust` 流水。
- Phase 5 已完成 T-402:MVP P0 验收通过,注册/充值/API Key/调用/余额/记录/后台/别名映射均有测试证据,详见 `mvp-acceptance.md`。
- Phase 5 已完成 T-403:已补部署 / 运行文档,明确宝塔/Nginx/Gunicorn、生产静态与媒体文件、共享缓存限流、图片同步超时、真实商户配置和上线检查;当前免邮箱验证,邮件服务仅作为后续密码找回/通知等邮件能力配置项,详见 `deployment.md`。
-- Phase 6 已完成 T-601 可用别名发现、T-602 后台表名/分组中文化、T-603 字段级中文化、T-604 中文敏感词本地过滤与 T-605 免邮箱验证策略落地;线上真实标题生成已恢复并验证扣点。
-- 下一步是 T-606 公开首页 + 客户端下载入口,并继续补跑真实支付回调到账闭环和真实图片耗时验证。
+- Phase 6 已完成 T-601 可用别名发现、T-602 后台表名/分组中文化、T-603 字段级中文化、T-604 中文敏感词本地过滤、T-605 免邮箱验证策略落地与 T-606 公开首页 + 客户端下载入口;线上真实标题生成已恢复并验证扣点。
+- 下一步可继续补跑真实支付回调到账闭环、配置并发布客户端下载包和真实图片耗时验证。
---
*更多细节:愿景 `01-vision.md` | 需求与验收 `02-requirements.md` | 架构 `04-architecture.md` | 任务计划 `06-tasks.md`。*
diff --git a/docs/routes.md b/docs/routes.md
index e3ae7d8..21c954f 100644
--- a/docs/routes.md
+++ b/docs/routes.md
@@ -7,6 +7,7 @@
| 路由 | 方法 | 职责 | 鉴权 |
| --- | --- | --- | --- |
+| `/` | GET | 公开首页:项目介绍、四步上手、客户端下载入口、API/控制台入口 | 公开 |
| `/signup` `/login` `/logout` | GET/POST | 自助注册(**免邮箱验证、注册即可用**)/登录/登出(Django auth/allauth) | 公开 |
| `/dashboard` | GET | 个人中心:剩余点数、充值总额、快捷入口 | session |
| `/recharge` | GET/POST | 发起充值:选金额→展示支付二维码→轮询到账 | session |
@@ -17,6 +18,7 @@
T-501 已落地 `/signup`、`/login`、`/logout` 与最小 `/dashboard`。T-502 已落地 `/apikeys`:登录用户只能管理自己的 Key,生成后明文只显示一次,列表只显示 prefix,删除为吊销 `revoked`。T-503/T-505 已扩展 `/dashboard` 为个人中心汇总,并落地 `/records/recharge` 与 `/records/usage`:充值总额按已支付订单统计,入账 / 消费 / 退款点数按 `PointsLedger` 统计,记录页只查询当前登录用户数据并分页展示。T-504/T-505 已落地 `/recharge`:登录用户可选择金额和支付方式创建 pending 充值订单,页面用本地 static 自托管 qrcode.js 展示二维码票据并轮询 `/api/v1/recharge/status`,到账后刷新余额。
T-601 已落地 `/models`:登录用户可查看当前公开可调用别名、能力、是否需要原图和点数单价;页面不展示底层 SKU、模型 URL、provider key、`api_key_encrypted` 或 `extra_body`。
+T-606 已落地 `/` 公开首页:匿名访问返回 200,不再重定向到 `/dashboard`;匿名用户看到注册 / 登录 / 下载入口,登录用户看到「进入控制台」。首页下载区读取 `DownloadRelease(platform=windows, is_current=True)`,优先使用 `external_url`,否则使用后台上传文件的 `file.url`;无当前版本时显示「暂未发布」。
## API 路由(对外,DRF)
@@ -48,6 +50,7 @@ T-601 已落地 `/models`:登录用户可查看当前公开可调用别名、
| 模型配置 | AiModel | 维护上游模型(url/model/api_key[加密脱敏]/api_type/capabilities/timeout) |
| 能力别名 | ModelAlias | 维护对外别名 → 具体模型的映射;换供应商在此改指向 |
| 配置审计 | AiConfigAuditLog | 只读查看 AiModel / ModelAlias / 密钥变更:谁、何时、改了什么 |
+| 客户端下载版本 | DownloadRelease | 上传安装包或填写外部下载地址,标记每个平台当前版本;展示版本、SHA256 与发布说明 |
## 后台职责约定
diff --git a/progress.md b/progress.md
index 296364e..3492d0b 100644
--- a/progress.md
+++ b/progress.md
@@ -1371,3 +1371,32 @@
- `py -3.12 manage.py test apps.users.tests apps.billing.tests.BillingAdminTests apps.ai.tests.AiModelAdminTests apps.ai.tests.AiConfigAuditAdminTests --keepdb --noinput --verbosity 2`:通过,15 tests OK。
- 测试期仅保留 allauth 在 MySQL 条件唯一约束上的既有 `models.W036` 警告;本项目邮箱唯一性由 `user.email` 承担。
- 下一步:按任务看板进入 T-606 公开首页 + 客户端下载入口;生产侧另需补微信真实回调到账闭环与图片生成真实耗时验证。
+
+## 2026-07-06 T-606 公开首页 + 客户端下载入口
+
+- 状态:DONE。
+- 代码变更:
+ - `apps/portal/models.py` / `apps/portal/migrations/0001_initial.py`:新增 `DownloadRelease`,字段包含 `platform`、`version`、`file`、`external_url`、`sha256`、`is_current`、`release_notes` 与时间戳;表名 `download_release`;`external_url` 优先于 `file.url`;保存时在事务内把同平台其他 current 置为 false,避免使用 MySQL 不支持的条件唯一约束。
+ - `apps/portal/admin.py`:注册客户端下载版本 admin,支持上传本地安装包或填写外部链接,列表展示平台、版本、当前状态、下载来源和 SHA256 摘要。
+ - `apps/portal/views.py` / `urls.py`:`/` 从重定向 `/dashboard` 改为 `HomeView` 公开首页;首页读取 Windows 当前发布版本。
+ - `apps/portal/templates/portal/home.html`:按 v1「生成台」方向落地首页,包含 Hero、四步上手、标题/主图能力卡、点数计费 band、客户端下载区和控制台/API 入口;无 current release 时显示「暂未发布」。
+ - `apps/portal/static/portal/brand.css` / `base.html`:抽共享品牌 token,首页与现有 dashboard / 记录 / 充值 / API Key / 可用模型页面共用同一套颜色、字体、按钮和面板样式;保留现有导航按钮 HTML,避免破坏 active 状态断言。
+ - `apps/portal/tests.py`:新增 `/` 匿名 200、不跳登录、下载当前版本展示、无当前版本提示、登录用户显示「进入控制台」、每平台只保留一个 current release 等测试。
+- 文档变更:
+ - `docs/06-tasks.md`:T-606 标为 DONE。
+ - `docs/routes.md`:新增 `/` 公开首页和 `DownloadRelease` admin 职责。
+ - `docs/04-architecture.md` / `docs/api.md`:同步 `DownloadRelease` schema、首页/下载入口行为和生产媒体文件由 Nginx 直接服务口径。
+ - `docs/current-state.md`、`docs/00-ai-start-here.md`、`docs/project-brief.md`、`README.md`:同步 Phase 6 当前状态,移除“下一步 T-606”旧口径。
+- 验证:
+ - `.\init.ps1`:开工前通过(Python 3.12.3,依赖已满足,`manage.py check` 0 issues,打印启动命令)。
+ - `py -3.12 manage.py makemigrations portal`:生成 `apps/portal/migrations/0001_initial.py`。
+ - `py -3.12 -m py_compile apps\portal\models.py apps\portal\admin.py apps\portal\views.py apps\portal\urls.py apps\portal\tests.py`:通过。
+ - `py -3.12 manage.py check`:通过,0 issues。
+ - `py -3.12 manage.py makemigrations --check --dry-run`:通过,No changes detected。
+ - `py -3.12 manage.py migrate portal`:通过,`portal.0001_initial` 已应用到当前数据库;仅保留 allauth 既有 `models.W036` 警告。
+ - `py -3.12 manage.py shell -c "... Client().get('/') ..."`:通过,返回 `STATUS=200`,首页标识 `cmhub AI 电商生成台` 与下载区标识均存在。
+ - `py -3.12 manage.py findstatic portal/brand.css portal/vendor/bootstrap/bootstrap.min.css portal/vendor/qrcode/qrcode.js --verbosity 1`:通过,均找到本地 static 文件。
+ - `py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests.test_homepage_is_public_and_shows_anonymous_onboarding_without_release apps.portal.tests.PortalAccountFlowTests.test_homepage_shows_current_download_release apps.portal.tests.PortalAccountFlowTests.test_homepage_shows_dashboard_entry_for_authenticated_user apps.portal.tests.PortalAccountFlowTests.test_download_release_only_keeps_one_current_per_platform apps.portal.tests.PortalAccountFlowTests.test_authenticated_nav_highlights_current_page_only --noinput --verbosity 2`:重建测试库后通过,5 tests OK;测试期仅保留 allauth 在 MySQL 条件唯一约束上的既有 `models.W036` 警告。
+ - `.\init.ps1`:收尾验证通过(Python 3.12.3,依赖已满足,`manage.py check` 0 issues,打印启动命令)。
+ - `py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests --keepdb --noinput --verbosity 2`:已尝试,因远程 MySQL `43.128.3.240` 在测试库迁移阶段连接超时(2003 / WinError 10060)中断,不是断言失败;数据库恢复后需补跑 portal 整组。
+- 下一步:看板当前无新的编号任务;建议优先确认真实支付回调到账闭环、上传/发布 Windows 客户端安装包,或补真实图片生成耗时验证并拆成新任务。