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:
@@ -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 user’s 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.
|
||||
@@ -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&display=block" rel="stylesheet"><link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&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 |
@@ -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
@@ -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: 登录和会话安全基线。
|
||||
Reference in New Issue
Block a user