Initial commit: img-infinite-canvas AI design workbench MVP

Moteva-style AI design workbench replica. Home prompt creates a project;
the project page provides an infinite canvas with node drag, zoom/pan,
a design chat panel, history replay, image asset upload, and project
save/regenerate.

- Backend: go-zero, DDD layering, sqlc/pgx, optional PostgreSQL; memory
  or Redis cache; asynq job queue; MinIO/S3/R2/OSS object storage;
  sky-valley/pi agent runtime adapter.
- Frontend: Next.js App Router + Vite artifact build, TypeScript, i18n,
  shadcn/ui components, auth (OTP/Turnstile/Google/WeChat).
- Deploy: Docker Compose and k3s manifests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-07 23:15:37 +08:00
commit 44406b72db
349 changed files with 73265 additions and 0 deletions
+163
View File
@@ -0,0 +1,163 @@
---
name: Moteva
colors:
surface: '#fcf8fa'
surface-dim: '#dcd9db'
surface-bright: '#fcf8fa'
surface-container-lowest: '#ffffff'
surface-container-low: '#f6f3f5'
surface-container: '#f0edef'
surface-container-high: '#eae7e9'
surface-container-highest: '#e4e2e4'
on-surface: '#1b1b1d'
on-surface-variant: '#45464d'
inverse-surface: '#303032'
inverse-on-surface: '#f3f0f2'
outline: '#76777d'
outline-variant: '#c6c6cd'
surface-tint: '#565e74'
primary: '#000000'
on-primary: '#ffffff'
primary-container: '#131b2e'
on-primary-container: '#7c839b'
inverse-primary: '#bec6e0'
secondary: '#0058be'
on-secondary: '#ffffff'
secondary-container: '#2170e4'
on-secondary-container: '#fefcff'
tertiary: '#000000'
on-tertiary: '#ffffff'
tertiary-container: '#271901'
on-tertiary-container: '#98805d'
error: '#ba1a1a'
on-error: '#ffffff'
error-container: '#ffdad6'
on-error-container: '#93000a'
primary-fixed: '#dae2fd'
primary-fixed-dim: '#bec6e0'
on-primary-fixed: '#131b2e'
on-primary-fixed-variant: '#3f465c'
secondary-fixed: '#d8e2ff'
secondary-fixed-dim: '#adc6ff'
on-secondary-fixed: '#001a42'
on-secondary-fixed-variant: '#004395'
tertiary-fixed: '#fcdeb5'
tertiary-fixed-dim: '#dec29a'
on-tertiary-fixed: '#271901'
on-tertiary-fixed-variant: '#574425'
background: '#fcf8fa'
on-background: '#1b1b1d'
surface-variant: '#e4e2e4'
typography:
display-lg:
fontFamily: Inter
fontSize: 32px
fontWeight: '700'
lineHeight: 40px
letterSpacing: -0.02em
headline-md:
fontFamily: Inter
fontSize: 24px
fontWeight: '600'
lineHeight: 32px
letterSpacing: -0.01em
title-sm:
fontFamily: Inter
fontSize: 18px
fontWeight: '600'
lineHeight: 24px
body-md:
fontFamily: Inter
fontSize: 16px
fontWeight: '400'
lineHeight: 24px
body-sm:
fontFamily: Inter
fontSize: 14px
fontWeight: '400'
lineHeight: 20px
label-caps:
fontFamily: Inter
fontSize: 12px
fontWeight: '600'
lineHeight: 16px
letterSpacing: 0.05em
label-xs:
fontFamily: Inter
fontSize: 11px
fontWeight: '500'
lineHeight: 14px
display-lg-mobile:
fontFamily: Inter
fontSize: 28px
fontWeight: '700'
lineHeight: 36px
rounded:
sm: 0.25rem
DEFAULT: 0.5rem
md: 0.75rem
lg: 1rem
xl: 1.5rem
full: 9999px
spacing:
unit: 4px
xs: 4px
sm: 8px
md: 16px
lg: 24px
xl: 40px
sidebar_width: 280px
toolbar_height: 56px
---
## Brand & Style
The design system is engineered for high-velocity creative workflows, targeting E-commerce and UI designers who require a tool that recedes into the background while maintaining a premium, authoritative presence.
The aesthetic is **Corporate / Modern** with a focus on **Minimalism**. It prioritizes extreme clarity, reducing visual noise to ensure the users content remains the focal point. The interface utilizes a "Canvas and Chrome" philosophy: the workspace is expansive and neutral, while the surrounding "Chrome" (panels, toolbars, and inspectors) is structured with surgical precision. The emotional response is one of calm control, reliability, and professional-grade efficiency.
## Colors
The palette is anchored by **Deep Navy (#0F172A)** for primary branding and high-level structural elements, providing a grounded, professional foundation. **Electric Blue (#3B82F6)** serves as the functional action color, reserved for primary buttons, active states, and selection highlights to drive focus.
The background strategy employs **Off-white (#F8FAFC)** for the main application canvas to reduce eye strain, while **Pure White (#FFFFFF)** is used for floating panels, cards, and inspector menus to create a clear layered hierarchy. Borders and dividers utilize **Soft Grays (#E2E8F0)** to define space without creating harsh visual breaks.
## Typography
This design system utilizes **Inter** exclusively to leverage its exceptional legibility and systematic optical sizing. The hierarchy is highly structured to facilitate rapid scanning of dense information.
- **Headlines:** Use tighter letter spacing and heavier weights to establish clear section anchors.
- **Labels:** Small-scale labels (11px-12px) are essential for tool property inspectors; use uppercase for category headers to create distinction without increasing size.
- **Body:** Standardized at 14px for utility-heavy areas to maximize information density while maintaining readability.
## Layout & Spacing
The layout follows a **Fixed-Fluid Hybrid** model. Sidebars (Layers and Properties) are fixed at 280px to provide a consistent control surface, while the central viewport (the Canvas) is fluid.
A **4px baseline grid** governs all spatial relationships. High-productivity areas (like property inspectors) should use compact spacing (`sm` / 8px), while marketing or dashboard views should use more generous breathing room (`lg` / 24px). Breakpoints are set at 768px (Tablet) and 1280px (Desktop), with mobile views collapsing sidebars into bottom sheets or hidden drawers.
## Elevation & Depth
Hierarchy is established through **Tonal Layering** and **Ambient Shadows**.
1. **Level 0 (Canvas):** The base layer using the Off-white background.
2. **Level 1 (Sidebars/Panels):** Pure white surfaces with a 1px subtle border (#E2E8F0) to separate them from the canvas.
3. **Level 2 (Popovers/Menus):** Elements that float above the UI use a soft, diffused shadow (0px 4px 12px rgba(15, 23, 42, 0.08)) to indicate interactivity and temporary state.
4. **Active State:** Selection boxes and focused inputs use a 2px Electric Blue stroke, omitting shadows to maintain a "flat" technical feel.
## Shapes
The shape language balances modern approachability with professional precision. A **Rounded (0.5rem / 8px)** corner radius is the standard for cards, buttons, and input fields.
- Use **Sharp (0px)** for main structural dividers or the edge of the screen panels.
- Use **Large (1rem / 16px)** for main dashboard cards or onboarding modals.
- Interactive icons and status chips may use **Pill-shaped** geometry to differentiate them from functional input fields.
## Components
- **Buttons:** Primary buttons use Electric Blue with white text. Secondary buttons use a white fill with a 1px border (#E2E8F0) and Deep Navy text.
- **Input Fields:** Use 14px typography. The default state has a subtle border; the active/focus state uses an Electric Blue ring.
- **Cards:** White background, 8px corner radius, and a very light 1px border. Shadows are only applied if the card is "draggable" or "floating."
- **Chips:** Used for tags or status indicators. Use low-saturation backgrounds (e.g., light blue or light gray) with high-contrast text.
- **Layers List:** High-density list items (32px height) with subtle hover states (#F1F5F9). Active layers use a light blue tint.
- **Segmented Controls:** Used for switching between tools; use a light gray container with a white "slider" for the active selection.
+289
View File
@@ -0,0 +1,289 @@
<!DOCTYPE html><html class="light" lang="zh-CN" style=""><head><meta charset="utf-8"><meta content="width=device-width, initial-scale=1.0" name="viewport"><link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=block" rel="stylesheet"><link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&amp;display=swap" rel="stylesheet"><script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script><script id="tailwind-config">try{
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-error-container": "#93000a",
"surface-container-highest": "#e4e2e4",
"tertiary-fixed-dim": "#dec29a",
"on-background": "#000000",
"on-secondary-fixed-variant": "#004395",
"outline-variant": "#e5e7eb",
"on-tertiary-container": "#98805d",
"tertiary": "#000000",
"on-tertiary-fixed": "#271901",
"surface-container": "#f9fafb",
"tertiary-container": "#271901",
"on-secondary-fixed": "#001a42",
"on-tertiary": "#ffffff",
"inverse-surface": "#111827",
"on-surface-variant": "#6b7280",
"tertiary-fixed": "#fcdeb5",
"primary-fixed": "#dae2fd",
"on-primary-fixed": "#131b2e",
"primary": "#000000",
"surface-container-low": "#ffffff",
"surface-container-lowest": "#ffffff",
"background": "#fafafa",
"on-surface": "#111827",
"on-secondary-container": "#fefcff",
"secondary-fixed-dim": "#adc6ff",
"on-secondary": "#ffffff",
"primary-container": "#131b2e",
"on-primary-fixed-variant": "#3f465c",
"surface-variant": "#f3f4f6",
"error-container": "#ffdad6",
"inverse-on-surface": "#f3f0f2",
"on-tertiary-fixed-variant": "#574425",
"inverse-primary": "#bec6e0",
"on-error": "#ffffff",
"on-primary": "#ffffff",
"secondary-container": "#2563eb",
"surface-bright": "#ffffff",
"surface-dim": "#e5e7eb",
"secondary": "#2563eb",
"surface": "#ffffff",
"primary-fixed-dim": "#bec6e0",
"on-primary-container": "#7c839b",
"surface-container-high": "#f3f4f6",
"error": "#ef4444",
"surface-tint": "#000000",
"outline": "#d1d5db",
"secondary-fixed": "#dbeafe"
},
"borderRadius": {
"DEFAULT": "0.5rem",
"lg": "0.75rem",
"xl": "1rem",
"2xl": "1.5rem",
"3xl": "2rem",
"full": "9999px"
},
"spacing": {
"toolbar_height": "64px",
"xs": "4px",
"unit": "4px",
"md": "16px",
"xl": "48px",
"sm": "8px",
"lg": "24px",
"sidebar_width": "80px"
},
"fontFamily": {
"title-sm": ["Inter", "sans-serif"],
"label-xs": ["Inter", "sans-serif"],
"display-lg-mobile": ["Inter", "sans-serif"],
"body-md": ["Inter", "sans-serif"],
"display-lg": ["Inter", "sans-serif"],
"headline-md": ["Inter", "sans-serif"],
"body-sm": ["Inter", "sans-serif"],
"label-caps": ["Inter", "sans-serif"]
},
"fontSize": {
"label-xs": ["12px", {"lineHeight": "16px", "fontWeight": "500"}],
"body-sm": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}],
"title-sm": ["18px", {"lineHeight": "24px", "fontWeight": "600"}],
"headline-md": ["28px", {"lineHeight": "36px", "letterSpacing": "-0.02em", "fontWeight": "600"}],
"display-lg": ["48px", {"lineHeight": "56px", "letterSpacing": "-0.03em", "fontWeight": "700"}]
}
},
},
}
}catch(_e){}</script></head><body class="bg-background text-on-background selection:bg-secondary-fixed">
<!-- Floating Top Bar -->
<header class="fixed top-0 right-0 left-0 z-40 h-toolbar_height px-xl flex justify-between items-center bg-background/50 backdrop-blur-sm">
<div class="flex items-center gap-md pl-[100px]">
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-black rounded-lg flex items-center justify-center">
<span class="text-white font-bold text-xs">F</span>
</div>
<span class="font-display-lg text-title-sm tracking-tight font-bold">Moteva</span>
</div>
</div>
<div class="flex items-center gap-lg">
<div class="flex items-center gap-md bg-white/80 border border-outline-variant rounded-full py-1.5 pl-4 pr-1.5 shadow-sm">
<span class="text-label-caps text-on-surface-variant">30 Credits</span>
<button class="bg-black text-white px-4 py-1.5 rounded-full text-label-caps font-semibold hover:opacity-80 transition-opacity">Upgrade</button>
</div>
<div class="flex items-center gap-md">
<button class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-surface-variant transition-colors">
</button>
<div class="w-10 h-10 rounded-full overflow-hidden border-2 border-white shadow-sm ring-1 ring-black/5">
<img class="w-full h-full object-cover" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBTmp5nTMXnY0WSYm7jiRppEgqwryIFqcauCfdxXrM-KwgBUbYtfYz52SPV3RQPCJkU9hD24kvcGoG0dwFt_FHutjmhcBXs3e-aglUZNgeUiPlsvclTlX913atn8mB_WiGrasG3_Y-fd3LHiB-w8GfQP22ncoDZd-VYxVUM70gMsxFmKdWSGtTMQ0xHCRP0WZ7vl93Je5TKoRAMRc1S7scg3u1XVmKA1RW4mXZImZb6s-MNaFOqV1iY" alt="Profile">
</div>
</div>
</div>
</header>
<!-- Floating Pill Sidebar Navigation (Based on reference https://lh3.googleusercontent.com/aida-public/AB6AXuCQB7VKJYDi-F7VZfGPRFQxFQ0lx0Gc95xq89IjTPxTjYW4-PVi8nrL_cgna6RmS78wAOLqhXmyw7P5FjjrKX5qy7TC1d4zDOARbyJoNMe0m-VTSMpxv4DGVAvxLn_7Kz01-TOvKlfZ-_kVRg13ojxQIasrmV5beoAJVEC_R5td24eRf3pIxNlopAtOBKXL1kjDEHE5QGGgw64K98mK3XyjS5TnAS_ZhahLS3RiZXI6lmRGFRMf9S8QofX7inFYhhUacA) -->
<aside class="fixed left-6 top-1/2 -translate-y-1/2 z-50 flex flex-col items-center gap-4">
<!-- Top Action Button -->
<button class="w-[56px] h-[56px] rounded-full bg-black text-white flex items-center justify-center shadow-xl hover:scale-105 active:scale-95 transition-all">
<span class="material-symbols-outlined !text-[32px]">add</span>
</button>
<!-- Navigation Pill -->
<nav class="glass w-[56px] py-6 px-1.5 rounded-full flex flex-col items-center gap-6 pill-nav-shadow border border-white/50">
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-surface-variant/80 text-black transition-colors" title="Home">
<span class="material-symbols-outlined !font-light">home</span>
</a>
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full text-on-surface-variant hover:bg-surface-variant transition-colors" title="Projects">
<span class="material-symbols-outlined !font-light">folder</span>
</a>
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full text-on-surface-variant hover:bg-surface-variant transition-colors" title="Assets">
<span class="material-symbols-outlined !font-light">person</span>
</a>
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full text-on-surface-variant hover:bg-surface-variant transition-colors" title="Settings">
<span class="material-symbols-outlined !font-light">info</span>
</a>
</nav>
<!-- Bottom Help/Settings -->
<div class="glass w-[56px] py-4 rounded-full flex flex-col items-center gap-4 pill-nav-shadow border border-white/50">
<a href="#" class="text-on-surface-variant hover:text-black transition-colors">
<span class="material-symbols-outlined">settings</span>
</a>
</div>
</aside>
<!-- Main Canvas -->
<main class="pl-[112px] pt-toolbar_height min-h-screen p-xl max-w-[1440px] mx-auto">
<!-- Hero Section -->
<section class="flex flex-col items-center justify-center py-24 gap-12">
<div class="flex flex-col items-center text-center gap-4">
<h1 class="text-display-lg font-bold tracking-tight text-black max-w-2xl leading-tight">
Moteva 让设计变简单
</h1>
<p class="text-body-md text-on-surface-variant max-w-lg font-medium opacity-70">
懂你的设计 Agent,帮你搞定一切设计需求
</p>
</div>
<!-- AI Prompt Input - Premium Style -->
<div class="w-full max-w-3xl bg-white border border-outline-variant/60 rounded-3xl p-6 shadow-[0_8px_30px_rgb(0,0,0,0.04)] focus-within:shadow-[0_8px_30px_rgb(0,0,0,0.08)] transition-all duration-300">
<textarea class="w-full bg-transparent border-none focus:ring-0 text-body-md resize-none min-h-[100px] placeholder:text-outline font-medium" placeholder="让Moteva Moteva 帮你构思下一个电商爆款..."></textarea>
<div class="flex justify-between items-center mt-4">
<div class="flex gap-2"><button class="w-10 h-10 flex items-center justify-center text-on-surface-variant hover:bg-surface-variant rounded-full transition-colors"><span class="material-symbols-outlined !text-[20px]">attach_file</span></button></div>
<div class="flex gap-2 mr-2 ml-auto"><button class="w-10 h-10 flex items-center justify-center text-on-surface-variant border border-outline-variant/60 hover:bg-surface-variant rounded-full transition-colors"><span class="material-symbols-outlined !text-[20px]">language</span></button><button class="w-10 h-10 flex items-center justify-center text-on-surface-variant border border-outline-variant/60 hover:bg-surface-variant rounded-full transition-colors"><span class="material-symbols-outlined !text-[20px]">view_in_ar</span></button></div><button class="bg-black text-white w-12 h-12 flex items-center justify-center rounded-2xl hover:scale-105 active:scale-95 transition-all shadow-lg">
<span class="material-symbols-outlined">arrow_upward</span>
</button>
</div>
</div>
<!-- Quick Actions Tags -->
<div class="flex flex-wrap justify-center gap-3">
<button class="px-5 py-2.5 bg-white border border-outline-variant/60 rounded-full text-label-caps text-on-surface hover:bg-black hover:text-white transition-all flex items-center gap-2 shadow-sm">
<span class="material-symbols-outlined !text-[18px]">shopping_bag</span> 电商海报
</button>
<button class="px-5 py-2.5 bg-white border border-outline-variant/60 rounded-full text-label-caps text-on-surface hover:bg-black hover:text-white transition-all flex items-center gap-2 shadow-sm">
<span class="material-symbols-outlined !text-[18px]">web</span> UI 界面
</button>
<button class="px-5 py-2.5 bg-white border border-outline-variant/60 rounded-full text-label-caps text-on-surface hover:bg-black hover:text-white transition-all flex items-center gap-2 shadow-sm">
<span class="material-symbols-outlined !text-[18px]">auto_fix_high</span> 产品精修
</button>
<button class="px-5 py-2.5 bg-white border border-outline-variant/60 rounded-full text-label-caps text-on-surface hover:bg-black hover:text-white transition-all flex items-center gap-2 shadow-sm">
<span class="material-symbols-outlined !text-[18px]">style</span> 风格迁移
</button>
</div>
</section>
<!-- Recent Projects Section -->
<section class="flex flex-col gap-8 mb-24">
<div class="flex justify-between items-end border-b border-outline-variant/40 pb-4">
<h2 class="text-headline-md font-bold tracking-tight">最近项目</h2>
<a class="text-on-surface-variant hover:text-black font-semibold text-label-caps transition-colors" href="#">查看全部 →</a>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
<!-- Create New Card -->
<button class="group relative aspect-[4/3] rounded-3xl border-2 border-dashed border-outline hover:border-black transition-all flex flex-col items-center justify-center gap-4 bg-white/50 hover:bg-white">
<div class="w-12 h-12 rounded-full bg-surface-variant flex items-center justify-center group-hover:bg-black group-hover:text-white transition-all">
<span class="material-symbols-outlined !text-2xl">add</span>
</div>
<span class="text-label-caps font-bold">新建项目</span>
</button>
<!-- Project Card 1 -->
<div class="group flex flex-col gap-4">
<div class="relative aspect-[4/3] rounded-3xl overflow-hidden border border-outline-variant/60 bg-white card-shadow">
<img class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBAQZyxZCxIKIioQCDAkqLOc8Sq-gcrSHf5VnaCZTWp19m-P0Psr54Ks3Fd6qklIvl9bxsbrZamLOsCUhSJ6F8SswqUgf_niHqEFZUtz03fkTsaHhga4UrKshzQ4pfwzJ425L2GjzhBQkkSaClYfPQr02uDVHzY0YC7RpP3-AakcF4d5OjK_7TrbnPeYQZH39-swQVz0a9BSkJDq7thTb380rTbs61183-vmTwyepkprJKGio_ebkD_" alt="Project 1">
<div class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center backdrop-blur-[2px]">
<button class="bg-white text-black px-6 py-2.5 rounded-full font-bold text-label-caps transform translate-y-4 group-hover:translate-y-0 transition-transform">继续编辑</button>
</div>
</div>
<div class="flex flex-col px-1">
<span class="text-body-md font-bold text-black">夏日护肤专题海报</span>
<span class="text-label-xs text-on-surface-variant mt-0.5">2024-05-20</span>
</div>
</div>
<!-- Project Card 2 -->
<div class="group flex flex-col gap-4">
<div class="relative aspect-[4/3] rounded-3xl overflow-hidden border border-outline-variant/60 bg-white card-shadow">
<img class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700" src="https://lh3.googleusercontent.com/aida-public/AB6AXuCuj9jFt7Dqr05Md4O4BAnmb8u23kAnxqmktdJECGs_ia0yAvqqvehjRNhAwZVaO6QYKMl-b38N5WGI8oWgSg-EFmLrG2BTKwam7ELYlXjPK7qEqYAKsQtu2x-a8Aq_41ryeL8FAs43M3gr_xDICWKfaOyaqG_2mVsWNi5dWch9wWY6XohkmTRliWV6DYcWFuSUHCYMjUVrQLXTvRlpUG0BLm1H0X_4IPXjyPFIqGYYE71gmDoJgcou" alt="Project 2">
<div class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center backdrop-blur-[2px]">
<button class="bg-white text-black px-6 py-2.5 rounded-full font-bold text-label-caps transform translate-y-4 group-hover:translate-y-0 transition-transform">继续编辑</button>
</div>
</div>
<div class="flex flex-col px-1">
<span class="text-body-md font-bold text-black">智能家居仪表盘</span>
<span class="text-label-xs text-on-surface-variant mt-0.5">2024-05-18</span>
</div>
</div>
<!-- Project Card 3 -->
<div class="group flex flex-col gap-4">
<div class="relative aspect-[4/3] rounded-3xl overflow-hidden border border-outline-variant/60 bg-white card-shadow">
<img class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAP1LSqxG8FZs48N9oogf9Hv5rpvUQ6_O-5cWX3NjBIbXKEgog_-cSN1-zbPQ86PY6cgFWWrmO2y7Izy7PCJKQ4ehAd8sSHrdRwGEFsEkV4ECFjBZ3jBz1pOFqjKf_8g7YleRwxMtrxNyH-IsmYJAafVScvYYz9JEWVxw4-mPhrehkZShuiARwE9Bp6SLKrsGrZtK4FmQ90qSVrzR960GjL4hqnGyKTZyCy_5aGqz1a3KF4w2MWJuGs" alt="Project 3">
<div class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center backdrop-blur-[2px]">
<button class="bg-white text-black px-6 py-2.5 rounded-full font-bold text-label-caps transform translate-y-4 group-hover:translate-y-0 transition-transform">继续编辑</button>
</div>
</div>
<div class="flex flex-col px-1">
<span class="text-body-md font-bold text-black">未命名品牌设计</span>
<span class="text-label-xs text-on-surface-variant mt-0.5">2024-05-15</span>
</div>
</div>
</div>
</section>
<!-- Inspiration Discovery Section (Bento Grid) -->
<section class="flex flex-col gap-8 pb-32">
<div class="flex justify-between items-center border-b border-outline-variant/40 pb-4">
<h2 class="text-headline-md font-bold tracking-tight">灵感发现</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 auto-rows-[220px]">
<!-- Bento Item 1: Large -->
<div class="col-span-1 md:col-span-2 row-span-2 rounded-3xl overflow-hidden relative group cursor-pointer border border-outline-variant/40 shadow-sm">
<img class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-1000" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBcMApNuln2ErI4FTqRz2PEwVG3fd7V4kJfUFoXJpsgFm_-5jrcW3rHz_LnPVeJUvCuWuLbobk3o7nRvCjHaKDInVy5EqQ1A1EMSPT3uqQ0VH_vv957qrEuECwqPdYAJdbLmsUtAwSPoLFvrfFlB9Lb9I8lQJE-QecRcaRIFVNs4couaHkhjcwtqUeyFN4tghBJV2qSuzHyyR7JIPTrtrjjKAAc78YtftSQCDYnX7tTmWe8cWJkVJ52" alt="Fashion Trends">
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent opacity-90"></div>
<div class="absolute inset-x-0 bottom-0 p-8 text-white">
<p class="text-label-caps font-semibold tracking-wider opacity-60 mb-2 uppercase">Luxury Showcase</p>
<h3 class="text-title-sm md:text-headline-md font-bold mb-2 leading-tight">2024 春季系列设计趋势</h3>
<p class="text-body-sm opacity-80 max-w-sm">探索本季最受关注的视觉语言与色彩搭配方案</p>
</div>
</div>
<!-- Bento Item 2: Vertical -->
<div class="row-span-2 rounded-3xl overflow-hidden relative group cursor-pointer border border-outline-variant/40 shadow-sm bg-white">
<img class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-1000" src="https://lh3.googleusercontent.com/aida-public/AB6AXuCVCdEqFYe9Uq8-hJzgx5bpmon0YGrHEO6aylI2LyPJJG9Qn2XQXzS9a43vFCkvDdN0AMWcQwhLOaxxIBo5blTXZy2P87CwU6UXH-bRyoxT3PrF6jIrE5hyuYlBcMHYGcZynVCH5ypT0O55ceC1Nq73LSwl-5-SOAw0V3KAITwuxUJER7tEHKFooGqDdm7IJgnr83wp9T0RO5lzIgZ73QqLuZd_WtjPzSjSJND-dyjOM1DDluEV8eMi" alt="App UI">
<div class="absolute inset-x-0 bottom-0 p-6 bg-gradient-to-t from-black/60 to-transparent text-white">
<h3 class="text-label-caps font-bold">极简自然主义 UI</h3>
</div>
</div>
<!-- Bento Item 3: Square -->
<div class="rounded-3xl overflow-hidden relative group cursor-pointer border border-outline-variant/40 shadow-sm bg-white">
<img class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-1000" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAwAgikDwW7I3qOFd4RobTHTM-in62BWoxaVpc0pZoZRlMC7lc19dlopTZ0q4sR6dg2BFDuXQ8Be06sJQCmuXQ26tWHL16cz5LlYejK8LmMMzSd9kcJ0jX6Za6AREg-O-V0TZuMVfVzgSyD8np4laknsPSz4j2YihRWwTmqj3NRCfg64RoWHilVyL7Ms77GO75nVJD0uePp3ot-3eZwvMTmxcQDZ9_MUb37I7moQ6sJgk2LJJhz5NIl" alt="3D Geometry">
<div class="absolute inset-0 bg-black/10 group-hover:bg-transparent transition-colors"></div>
</div>
<!-- Bento Item 4: Square -->
<div class="rounded-3xl overflow-hidden relative group cursor-pointer border border-outline-variant/40 shadow-sm bg-white">
</div></div></section></main></body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 KiB

+15
View File
@@ -0,0 +1,15 @@
产品对标:Moteva
其中,视频生成暂时不需要对接
1. go-zero + sqlc + pgx + PostgreSQL + 缓存技术(Redis 和 memory 可选,默认 memory) 高性能服务,需要践行 DDD 领域设计
2. pi-agent: https://github.com/sky-valley/pi (go 版本);可参考完美版 typescript 版本( https://github.com/earendil-works/pi Pi Agent Harness
This is the home of the Pi agent harness project including our self extensible coding agent.
@earendil-works/pi-coding-agent: Interactive coding agent CLI
@earendil-works/pi-agent-core: Agent runtime with tool calling and state management
@earendil-works/pi-ai: Unified multi-provider LLM API (OpenAI, Anthropic, Google, …)
1. next.js + pnpm + vite + typesript + gzip + i18n + ShadUI + less + MobX + 组件名是文件夹名,文件里可以放 index.tsx 和 index.less ,需要践行 DDD 领域设计
2. 前端复刻 skills 使用 frontend skills
3. 无线画布使用 https://github.com/basketikun/infinite-canvas 去除 本地 agent 功能,功能模仿 Moteva 来,可通过 mcp 查看 Moteva 实现原理然后抄过来,agent 需要支持 历史聊天记录回放;图片支持 oss 存储(R2, aws oss, aliyun oss 和 私有部署的 minio
4. 页面设计参考 refer/design 文件夹;首页设计 refer/design/home 页面 ;新建项目和打开最近项目页面 refer/design/project 页面 (使用了无限画布)
5. PostgreSQL 可以使用 docker 版本
6. 支持 本机二进制部署, docker compose 部署和 k3s 部署
+611
View File
@@ -0,0 +1,611 @@
# img-infinite-canvas 初期架构与演进方案
## 目标
当前阶段不要直接建设 Kafka、多微服务、多区域部署等终局复杂架构。推荐采用“可演进的模块化单体”:
- 初期保持一个 go-zero API 项目,一个 PostgreSQL,一个 Redis,一个对象存储。
- API 与 worker 可以先共用同一代码仓库,但部署为两个进程。
- 所有关键边界按未来可拆服务设计,避免用户增长后推倒重来。
- 新建项目必须登录,所有项目、资产、消息、任务、账本都必须绑定用户。
初期目标规模:
- 1 万注册用户。
- 数百级并发在线。
- 数十到数百级并发生成任务。
- 支持后续平滑演进到 Kafka 事件总线、独立 Billing/Agent/Realtime 服务。
## 当前主要问题
1. 没有用户维度
当前 `projects` 没有 `user_id`,项目列表和详情天然是全局资源。后续必须改成用户或租户隔离。
2. 新建项目没有登录要求
项目创建、保存、删除、生成、上传都应受登录态保护。未来新建项目必须登录。
3. 画布保存是全量覆盖
当前保存项目时会重写 nodes、connections、messages。用户增长后会造成写放大,并带来并发覆盖风险。
4. 聊天消息和画布保存耦合
`agent_messages` 应该 append-only,不应在保存画布时被删除重插。
5. 异步任务依赖进程内 goroutine
服务重启会丢任务,多实例部署时无法统一调度,也无法做全局并发控制、重试、死信队列。
6. 实时通道轮询数据库
WebSocket/SSE 当前通过高频读取完整项目推送状态。并发连接变多后会放大数据库和 JSON 序列化压力。
7. 点数扣费尚未形成账本
后续需要扣除点数,必须有账户、冻结、扣费、退款、流水和幂等设计。
8. 配置和生产环境边界不足
生产环境必须禁用 memory storage,密钥必须走环境变量或 secret,不应提交明文 API key。
## 初期简化架构
```text
Next.js frontend
-> go-zero API
-> PostgreSQL: 用户、项目、画布、消息、点数账本、任务状态
-> Redis: 缓存、限流、Asynq 队列、短期实时事件
-> Object Storage: MinIO / S3 / R2 / OSS
-> Worker process: 图片生成、图层合并、资产清理、扣费结算
```
初期暂不引入:
- Kafka。
- NATS。
- 微服务拆分。
- 多区域部署。
- 复杂 organization/workspace。
- 数据仓库事件流。
但要预留这些能力:
- `outbox_events` 表,为未来接 Kafka 做准备。
- 模块化目录和接口,未来可拆服务。
- append-only 消息和账本,未来可审计、可回放。
- 可替换 JobQueue 接口,未来可从 Asynq 切到 Kafka command topic。
## 初期技术选型
### API
- 继续使用 go-zero REST API。
- handler 保持薄层,只做请求解析和调用 logic。
- logic 调 application service。
- application service 只依赖领域接口,不依赖具体 Redis/Kafka/Asynq 实现。
### 数据库
- PostgreSQL 作为权威业务数据源。
- 余额、扣点、冻结、退款必须在 PostgreSQL 事务内完成。
- 所有核心数据表增加 `user_id`
### 缓存与任务
- Redis 用于缓存、限流、短期实时事件。
- 使用 Asynq 作为初期任务队列。
### 对象存储
- 本地开发可用 MinIO。
- 生产使用 S3 / R2 / OSS / 私有 MinIO。
- 数据库只保存 object key、public URL、owner、project 归属和元数据。
## 初期模块边界
建议在单体内按未来服务边界拆模块:
```text
server/internal/modules/auth
server/internal/modules/project
server/internal/modules/canvas
server/internal/modules/agent
server/internal/modules/billing
server/internal/modules/asset
server/internal/modules/job
server/internal/modules/realtime
server/internal/shared
```
也可以先不大规模移动现有文件,但新增代码应按这些边界组织。
模块职责:
- `auth`: 登录、注册、JWT、refresh token、用户上下文。
- `project`: 项目元数据、项目列表、项目删除、所有权校验。
- `canvas`: 画布快照、节点保存、版本冲突。
- `agent`: 会话线程、消息、生成编排。
- `billing`: 点数账户、冻结、扣费、释放、退款、流水。
- `asset`: 上传签名、对象存储、资产归属、清理。
- `job`: 任务队列接口、Asynq/Postgres jobs 实现。
- `realtime`: WebSocket/SSE 推送接口。
## 初期数据模型
最小可运营版本需要这些表:
```sql
users (
id TEXT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
projects (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
title TEXT NOT NULL,
brief TEXT NOT NULL,
status TEXT NOT NULL,
thumbnail TEXT NOT NULL DEFAULT '',
current_snapshot_id TEXT NOT NULL DEFAULT '',
version BIGINT NOT NULL DEFAULT 1,
updated_at TIMESTAMPTZ NOT NULL,
deleted_at TIMESTAMPTZ
);
canvas_snapshots (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id),
version BIGINT NOT NULL,
viewport_json JSONB NOT NULL,
nodes_json JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL
);
agent_threads (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
mode TEXT NOT NULL DEFAULT 'design',
status TEXT NOT NULL DEFAULT 'running',
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
agent_messages (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
thread_id TEXT NOT NULL REFERENCES agent_threads(id) ON DELETE CASCADE,
role TEXT NOT NULL,
message_type TEXT NOT NULL DEFAULT '',
content TEXT NOT NULL,
status TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL
);
credit_accounts (
user_id TEXT PRIMARY KEY REFERENCES users(id),
balance BIGINT NOT NULL DEFAULT 0,
reserved BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL
);
credit_reservations (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
project_id TEXT NOT NULL REFERENCES projects(id),
job_id TEXT NOT NULL,
amount BIGINT NOT NULL,
status TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
credit_ledger (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
project_id TEXT,
job_id TEXT,
reservation_id TEXT,
ledger_type TEXT NOT NULL,
amount BIGINT NOT NULL,
balance_after BIGINT NOT NULL,
idempotency_key TEXT NOT NULL UNIQUE,
reason TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL
);
generation_jobs (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
project_id TEXT NOT NULL REFERENCES projects(id),
thread_id TEXT NOT NULL REFERENCES agent_threads(id),
reservation_id TEXT,
job_type TEXT NOT NULL,
status TEXT NOT NULL,
payload_json JSONB NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
idempotency_key TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
assets (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
project_id TEXT REFERENCES projects(id),
object_key TEXT NOT NULL,
public_url TEXT NOT NULL,
content_type TEXT NOT NULL DEFAULT '',
size_bytes BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL,
deleted_at TIMESTAMPTZ
);
outbox_events (
id TEXT PRIMARY KEY,
aggregate_type TEXT NOT NULL,
aggregate_id TEXT NOT NULL,
event_type TEXT NOT NULL,
payload_json JSONB NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
published_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL
);
```
关键索引:
```sql
CREATE INDEX projects_user_updated_idx ON projects(user_id, updated_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX canvas_snapshots_project_version_idx ON canvas_snapshots(project_id, version DESC);
CREATE INDEX agent_threads_project_updated_idx ON agent_threads(project_id, updated_at DESC);
CREATE INDEX agent_messages_thread_created_idx ON agent_messages(thread_id, created_at ASC);
CREATE INDEX generation_jobs_status_updated_idx ON generation_jobs(status, updated_at);
CREATE INDEX assets_user_project_created_idx ON assets(user_id, project_id, created_at DESC);
CREATE INDEX outbox_events_status_created_idx ON outbox_events(status, created_at);
```
## 关键接口约束
所有项目相关 repository 方法必须带 `userID`
```go
GetProject(ctx context.Context, userID string, projectID string) (Project, error)
ListProjects(ctx context.Context, userID string, cursor string, limit int64) (ProjectPage, error)
SaveCanvas(ctx context.Context, userID string, projectID string, req SaveCanvasRequest) (Project, error)
DeleteProject(ctx context.Context, userID string, projectID string) error
```
不要再使用只带 `projectID` 的业务方法。这样后续拆服务时权限边界不需要重做。
任务队列只暴露接口:
```go
type JobQueue interface {
EnqueueGeneration(ctx context.Context, job GenerationJob) error
EnqueueAssetCleanup(ctx context.Context, job AssetCleanupJob) error
}
```
初期实现可以是 Asynq。未来如果切 Kafka,业务层不改,只替换 infrastructure。
计费模块只暴露业务语义:
```go
type BillingService interface {
ReserveCredits(ctx context.Context, req ReserveCreditsRequest) (Reservation, error)
ChargeCredits(ctx context.Context, req ChargeCreditsRequest) error
ReleaseCredits(ctx context.Context, req ReleaseCreditsRequest) error
}
```
生成模块不直接修改余额,只调用 BillingService。
## 登录与项目创建流程
```text
1. 用户登录或注册。
2. API 生成 access token 和 refresh token。
3. 前端调用 POST /api/projects。
4. JWT middleware 解析 user_id。
5. ProjectService.CreateProject(ctx, userID, req)。
6. 创建 project 和初始 canvas_snapshot。
7. 写 outbox_events.ProjectCreated。
8. 返回项目。
```
硬性要求:
- 未登录不能创建项目。
- 未登录不能上传资产。
- 未登录不能触发生成。
- 用户只能访问自己的项目,除非后续增加协作授权。
## 生成与扣点流程
```text
1. 用户点击生成。
2. API 解析 user_id、project_id、prompt、model。
3. ProjectService 校验项目归属。
4. BillingService 计算预估点数。
5. 开启 PostgreSQL 事务。
6. SELECT credit_accounts ... FOR UPDATE 锁定账户。
7. 检查 balance - reserved 是否足够。
8. 创建 credit_reservation,增加 reserved。
9. 创建 generation_job。
10. 写 credit_ledger(reserve)。
11. 写 outbox_events.GenerationRequested 和 CreditsReserved。
12. 提交事务。
13. JobQueue 入队。
14. worker 执行图片生成。
15. 成功后写节点/快照,ChargeCredits,释放多余冻结。
16. 失败后 ReleaseCredits。
17. 写 outbox_events.GenerationCompleted 或 GenerationFailed。
18. Realtime 推送前端。
```
扣点规则:
- 生成前冻结点数,不直接扣除。
- 成功后按实际消耗扣除。
- 失败后释放冻结。
- 所有扣点和释放必须写 `credit_ledger`
- 所有扣点操作必须有 `idempotency_key`
## 画布保存流程
初期先用快照方式,不急着做复杂增量节点表:
```text
1. 前端保存时带 base_version 和 snapshot。
2. API 校验 user_id + project_id。
3. 对 project 做乐观锁检查。
4. 如果 base_version != current version,返回 409。
5. 写入新的 canvas_snapshot。
6. 更新 projects.current_snapshot_id 和 version。
7. 不修改 agent_messages。
8. 写 outbox_events.CanvasSnapshotCreated。
```
这样可以先解决“保存画布重写消息”的问题,同时为未来增量节点保存预留空间。
## 实时推送初期方案
初期可以分两步:
第一步:
- 保留现有 WebSocket/SSE 形态。
- 降低数据库轮询频率。
- 只在生成中开启轮询。
- 项目静止时不轮询。
第二步:
- worker 写 outbox 或 Redis pub/sub(用 outbox 保底、Redis 做实时加速)。
- realtime 模块订阅事件。
- WebSocket/SSE 只推增量事件,不再轮询完整 project。
事件示例:
```json
{
"type": "generation.completed",
"projectId": "project-id",
"threadId": "thread-id",
"snapshotId": "snapshot-id",
"messageId": "message-id"
}
```
## Outbox 设计
初期即使不用 Kafka,也必须先建 `outbox_events`
写业务数据和写 outbox 必须在同一个数据库事务内:
```text
BEGIN;
update credit_accounts;
insert credit_ledger;
insert generation_jobs;
insert outbox_events;
COMMIT;
```
初期可以用 Go poller 读取 `outbox_events where status='pending'`,发布到 Redis 或直接调用 realtime broadcaster。
未来接 Kafka 时:
- 使用 Debezium Outbox,或自研 outbox publisher。
- Kafka topic 可以按 `billing.events``generation.events``project.events` 拆分。
- 业务事务无需重写。
## 架构演进路线
### 阶段 1:可运营单体
目标:能安全上线给真实用户使用。
改造项:
- 新增用户登录。
- 项目创建必须登录。
- 所有核心表加 `user_id`
- 所有项目接口按 `user_id + project_id` 查询。
- 新增点数账户、冻结、流水。
- 生成前冻结点数,成功后扣除,失败后释放。
- 生产禁用 memory storage。
- 明文密钥移出仓库。
部署:
```text
api process
worker process
postgres
redis
object storage
```
### 阶段 2:可扩展单体
目标:API 和 worker 可独立扩容。
改造项:
- 引入 Asynq。
- 所有慢任务进入队列。
- `agent_messages` append-only。
- `canvas_snapshots` 替换全量 project 保存。
- outbox poller 发布内部事件。
- 基础限流:按 user、IP、model。
- 基础指标:QPS、p95、DB pool、队列长度、生成耗时、扣点失败率。
部署:
```text
api replicas
worker-image replicas
worker-cleanup replicas
postgres
redis
object storage
```
### 阶段 3:准终局架构
目标:将增长压力从 API 层剥离。
改造项:
- realtime 独立进程。
- worker 按任务类型拆池。
- Redis pub/sub 或 stream 推送增量事件。
- 画布保存支持乐观锁和冲突处理。
- 历史快照清理或归档。
- 点数账本增加审计后台。
- 资产增加生命周期管理。
部署:
```text
api replicas
realtime replicas
worker-image replicas
worker-billing replicas
worker-asset replicas
postgres primary/replica
redis
object storage + CDN
```
### 阶段 4:终局平滑拆分
触发条件:
- API 单体发布频率互相影响。
- Billing 需要独立合规和权限边界。
- Agent worker 资源消耗显著高于其他模块。
- Realtime 连接规模对 API 进程造成影响。
- 多业务系统需要消费项目、计费、生成事件。
拆分顺序:
1. 拆 Realtime 服务。
2. 拆 Worker 服务。
3. 拆 Billing 服务。
4. 拆 Agent 服务。
5. 接 Kafka 作为跨系统事件总线。
终局形态:
```text
API Gateway
-> Auth Service
-> Project Service
-> Canvas Service
-> Billing Service
-> Agent Service
-> Asset Service
-> Realtime Service
PostgreSQL / Redis / Kafka / Object Storage / CDN / Observability
```
## 现在不要做死的设计
- 不要再出现全局项目列表。
- 不要只用 `project_id` 查询项目。
- 不要把扣点逻辑写在生成逻辑里。
- 不要让生成任务只存在 goroutine 里。
- 不要让保存画布重写消息。
- 不要把实时事件只放内存里。
- 不要让业务层依赖 Asynq/Kafka 具体实现。
- 不要让 assets 只有 URL,没有 owner 和 project 归属。
## 初期实施顺序
1. 清理密钥
移除配置文件中的真实 API key,统一使用环境变量或 secret。
2. 登录鉴权
新增 `users`、登录接口、JWT middleware。项目创建、保存、删除、生成、上传全部要求登录。
3. 数据隔离
`projects``user_id`,所有项目 repository 方法带 `userID`。迁移现有项目到默认管理员用户。
4. 点数账本
新增 `credit_accounts``credit_reservations``credit_ledger`。生成前冻结点数。
5. 任务队列
新增 `generation_jobs`,引入 Asynq worker。替换进程内 `go func()` 生成任务。
6. 消息解耦
`agent_messages` append-only,画布保存不再重写消息。
7. 画布快照
新增 `canvas_snapshots`,保存时使用版本号和 `base_version`
8. Outbox
新增 `outbox_events`,业务事务内写事件,初期用 poller 发布。
9. 实时优化
由轮询完整 project 逐步改为推送增量事件。
10. 压测和运维
建立基础指标、日志、告警、备份恢复和压测脚本。
## 验收标准
上线前必须满足:
- 未登录不能创建项目。
- 用户不能读取、保存、删除别人的项目。
- 生成任务重复提交不会重复扣点。
- worker 重启后任务不会永久丢失。
- 图片生成失败会释放冻结点数。
- 保存画布不会重写聊天消息。
- 项目列表按 `user_id` 分页查询。
- 生产配置不使用 memory repository/cache/object storage。
- 真实密钥不在仓库配置文件中。
增长到 1 万用户前应满足:
- API 和 worker 可独立水平扩容。
- 队列长度和失败率可观测。
- PostgreSQL 慢查询可观测。
- 点数账本可审计。
- 资产可按用户和项目追踪。
- WebSocket/SSE 不再高频拉完整 project。
## 参考方向
- AWS Well-Architected: 持续演进、可靠性、可观测性。
- Strangler Fig Pattern: 从模块化单体平滑演进到微服务。
- Debezium Outbox Pattern: 业务事务与事件发布解耦。
- PostgreSQL row lock: 点数账户冻结与扣费需要数据库事务和行锁。
- OWASP Authentication Cheat Sheet: 登录和会话安全基线。