# 美图 Agent — 视频创作可视化界面 实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 构建基于 pi-agent 的视频创作 Web 可视化界面,三栏布局,支持账户管理、提示词编辑、聊天驱动 Pipeline、资产画廊。 **Architecture:** React SPA (Vite + Tailwind + shadcn/ui) 通过 REST + WebSocket 与 Express 后端通信。后端集成 pi-agent-core SDK 编排对话,直接调用 pipeline.js 执行视频创作流程。SQLite 存储会话、资产和配置。 **Tech Stack:** TypeScript, Vite, React 18, Tailwind CSS, shadcn/ui, Express, ws, better-sqlite3, pi-agent-core, Zustand --- ## 文件结构(全量) ``` web/ ├── package.json ├── tsconfig.base.json ├── server/ │ ├── tsconfig.json │ ├── index.ts │ ├── db/ │ │ ├── index.ts │ │ └── schema.ts │ ├── routes/ │ │ ├── accounts.ts │ │ ├── prompts.ts │ │ ├── pipeline.ts │ │ ├── assets.ts │ │ └── configs.ts │ ├── agent/ │ │ ├── index.ts │ │ └── tools.ts │ └── ws/ │ └── chat.ts ├── client/ │ ├── tsconfig.json │ ├── index.html │ ├── vite.config.ts │ ├── tailwind.config.ts │ ├── postcss.config.js │ └── src/ │ ├── main.tsx │ ├── App.tsx │ ├── index.css │ ├── lib/ │ │ ├── api.ts │ │ └── websocket.ts │ ├── hooks/ │ │ ├── useAccounts.ts │ │ ├── usePrompts.ts │ │ ├── useAssets.ts │ │ └── useChat.ts │ ├── types/ │ │ └── index.ts │ ├── components/ │ │ ├── ui/ # shadcn/ui (generated) │ │ ├── layout/ │ │ │ ├── AppLayout.tsx │ │ │ ├── Sidebar.tsx │ │ │ └── MiddlePanel.tsx │ │ ├── chat/ │ │ │ ├── ChatView.tsx │ │ │ ├── ChatMessage.tsx │ │ │ ├── ChatInput.tsx │ │ │ └── PipelineProgress.tsx │ │ ├── accounts/ │ │ │ ├── AccountList.tsx │ │ │ └── AccountForm.tsx │ │ ├── prompts/ │ │ │ └── PromptEditor.tsx │ │ ├── assets/ │ │ │ ├── AssetGallery.tsx │ │ │ └── AssetPreview.tsx │ │ └── config/ │ │ └── ConfigForm.tsx │ └── store/ │ └── index.ts ``` --- ## P0: 项目脚手架 ### Task 0.1: 初始化项目与 package.json **Files:** - Create: `web/package.json` - Create: `web/tsconfig.base.json` - Create: `web/server/tsconfig.json` - Create: `web/client/tsconfig.json` - [ ] **Step 1: 创建 web/package.json** ```bash mkdir -p web/server web/client/src ``` ```json { "name": "meitu-agent", "version": "0.1.0", "private": true, "scripts": { "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"", "dev:server": "tsx watch server/index.ts", "dev:client": "vite --config client/vite.config.ts", "build": "vite build --config client/vite.config.ts", "db:init": "tsx server/db/schema.ts" }, "dependencies": { "express": "^4.21.0", "ws": "^8.18.0", "better-sqlite3": "^11.6.0", "zod": "^3.23.0", "cors": "^2.8.5", "zustand": "^5.0.0", "react": "^18.3.0", "react-dom": "^18.3.0", "lucide-react": "^0.460.0", "clsx": "^2.1.0", "tailwind-merge": "^2.6.0" }, "devDependencies": { "@types/express": "^5.0.0", "@types/ws": "^8.5.0", "@types/better-sqlite3": "^7.6.0", "@types/cors": "^2.8.0", "@types/react": "^18.3.0", "@types/react-dom": "^18.3.0", "typescript": "^5.6.0", "tsx": "^4.19.0", "vite": "^5.4.0", "@vitejs/plugin-react": "^4.3.0", "tailwindcss": "^3.4.0", "postcss": "^8.4.0", "autoprefixer": "^10.4.0", "concurrently": "^9.1.0" } } ``` - [ ] **Step 2: 创建 tsconfig.base.json** ```json { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "declaration": true, "declarationMap": true, "sourceMap": true } } ``` - [ ] **Step 3: 创建 server/tsconfig.json** ```json { "extends": "../tsconfig.base.json", "compilerOptions": { "module": "ESNext", "outDir": "./dist", "rootDir": "." }, "include": ["./**/*.ts"] } ``` - [ ] **Step 4: 创建 client/tsconfig.json** ```json { "extends": "../tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "outDir": "./dist", "rootDir": "./src", "baseUrl": ".", "paths": { "@/*": ["./src/*"] } }, "include": ["./src/**/*.ts", "./src/**/*.tsx"] } ``` - [ ] **Step 5: 安装依赖** ```bash cd web && npm install ``` - [ ] **Step 6: Commit** ```bash git add web/package.json web/tsconfig.base.json web/server/tsconfig.json web/client/tsconfig.json web/package-lock.json git commit -m "feat(web): init project scaffold with package.json and tsconfig" ``` --- ### Task 0.2: 初始化 Vite + React + Tailwind + shadcn/ui **Files:** - Create: `web/client/index.html` - Create: `web/client/vite.config.ts` - Create: `web/client/tailwind.config.ts` - Create: `web/client/postcss.config.js` - Create: `web/client/src/main.tsx` - Create: `web/client/src/index.css` - Create: `web/client/src/lib/utils.ts` - [ ] **Step 1: 创建 client/index.html** ```html 美图 Agent
``` - [ ] **Step 2: 创建 client/vite.config.ts** ```typescript import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import path from 'path'; export default defineConfig({ plugins: [react()], root: path.resolve(__dirname), resolve: { alias: { '@': path.resolve(__dirname, 'src'), }, }, server: { port: 5173, proxy: { '/api': 'http://localhost:3001', '/ws': { target: 'ws://localhost:3001', ws: true, }, }, }, }); ``` - [ ] **Step 3: 创建 client/tailwind.config.ts** ```typescript import type { Config } from 'tailwindcss'; export default { content: ['./client/src/**/*.{ts,tsx}'], darkMode: 'class', theme: { extend: { colors: { border: 'hsl(240 3.7% 15.9%)', input: 'hsl(240 3.7% 15.9%)', ring: 'hsl(240 4.9% 83.9%)', background: 'hsl(240 10% 3.9%)', foreground: 'hsl(0 0% 98%)', primary: { DEFAULT: 'hsl(0 0% 98%)', foreground: 'hsl(240 5.9% 10%)', }, secondary: { DEFAULT: 'hsl(240 3.7% 15.9%)', foreground: 'hsl(0 0% 98%)', }, muted: { DEFAULT: 'hsl(240 3.7% 15.9%)', foreground: 'hsl(240 5% 64.9%)', }, accent: { DEFAULT: 'hsl(240 3.7% 15.9%)', foreground: 'hsl(0 0% 98%)', }, destructive: { DEFAULT: 'hsl(0 62.8% 30.6%)', foreground: 'hsl(0 0% 98%)', }, }, borderRadius: { lg: '0.5rem', md: 'calc(0.5rem - 2px)', sm: 'calc(0.5rem - 4px)', }, }, }, plugins: [], } satisfies Config; ``` - [ ] **Step 4: 创建 client/postcss.config.js** ```javascript export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ``` - [ ] **Step 5: 创建 client/src/index.css** ```css @tailwind base; @tailwind components; @tailwind utilities; @layer base { * { @apply border-border; } body { @apply bg-zinc-950 text-zinc-50; } } ``` - [ ] **Step 6: 创建 client/src/lib/utils.ts** ```typescript import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } ``` - [ ] **Step 7: 创建 client/src/main.tsx** ```typescript import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import './index.css'; ReactDOM.createRoot(document.getElementById('root')!).render( ); ``` - [ ] **Step 8: Commit** ```bash git add web/client/ git commit -m "feat(web): init Vite + React + Tailwind client scaffold" ``` --- ### Task 0.3: 初始化 shadcn/ui 基础组件 **Files:** - Create: `web/components.json` - Create: `web/client/src/components/ui/button.tsx` - Create: `web/client/src/components/ui/input.tsx` - Create: `web/client/src/components/ui/scroll-area.tsx` - [ ] **Step 1: 创建 components.json** ```json { "$schema": "https://ui.shadcn.com/schema.json", "style": "default", "rsc": false, "tsx": true, "tailwind": { "config": "client/tailwind.config.ts", "css": "client/src/index.css", "baseColor": "zinc", "cssVariables": true }, "aliases": { "components": "@/components", "utils": "@/lib/utils" } } ``` - [ ] **Step 2: 创建 button.tsx** ```typescript import * as React from 'react'; import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils'; const buttonVariants = cva( 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', { variants: { variant: { default: 'bg-primary text-primary-foreground hover:bg-primary/90', destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', link: 'text-primary underline-offset-4 hover:underline', }, size: { default: 'h-10 px-4 py-2', sm: 'h-9 rounded-md px-3', lg: 'h-11 rounded-md px-8', icon: 'h-10 w-10', }, }, defaultVariants: { variant: 'default', size: 'default' }, } ); export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps {} const Button = React.forwardRef( ({ className, variant, size, ...props }, ref) => ( ))} ); } ``` - [ ] **Step 5: 创建 MiddlePanel.tsx(对话列表占位)** ```typescript import { Plus } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { ScrollArea } from '@/components/ui/scroll-area'; import { useAppStore } from '@/store'; export function MiddlePanel() { const { conversations, activeConversationId, setActiveConversationId } = useAppStore(); return ( ); } ``` - [ ] **Step 6: 创建 App.tsx** ```typescript import { AppLayout } from '@/components/layout/AppLayout'; import { ChatView } from '@/components/chat/ChatView'; import { AccountList } from '@/components/accounts/AccountList'; import { AssetGallery } from '@/components/assets/AssetGallery'; import { ConfigForm } from '@/components/config/ConfigForm'; import { useAppStore } from '@/store'; import { useEffect } from 'react'; function MainContent() { const view = useAppStore((s) => s.activeView); switch (view) { case 'chat': return ; case 'accounts': return ; case 'assets': return ; case 'config': return ; } } export default function App() { useEffect(() => { fetch('/api/configs') .then((r) => r.json()) .catch(() => {}); }, []); return ( ); } ``` - [ ] **Step 7: 创建占位组件(空壳,后续任务填充)** 创建以下占位文件,每个只返回一个空 div: `chat/ChatView.tsx`: ```typescript export function ChatView() { return
选择或开始新对话
; } ``` `accounts/AccountList.tsx`: ```typescript export function AccountList() { return
账户管理
; } ``` `assets/AssetGallery.tsx`: ```typescript export function AssetGallery() { return
资产画廊
; } ``` `config/ConfigForm.tsx`: ```typescript export function ConfigForm() { return
设置
; } ``` - [ ] **Step 8: 验证: 启动 dev server** ```bash cd web && npm run dev ``` 打开 http://localhost:5173,确认三栏布局渲染正常(左侧图标栏 56px + 中间列表 240px + 右侧主区域)。 - [ ] **Step 9: Commit** ```bash git add web/client/src/ git commit -m "feat(web): add three-column layout shell with placeholder views" ``` --- ## P1: 账户管理 + 配置 ### Task 1.1: 类型定义 + API Client **Files:** - Create: `web/client/src/lib/api.ts` - [ ] **Step 1: 创建 lib/api.ts** ```typescript import type { Account, Asset, Conversation, Message, ConfigItem } from '@/types'; const BASE = '/api'; async function request(path: string, options?: RequestInit): Promise { const res = await fetch(`${BASE}${path}`, { headers: { 'Content-Type': 'application/json' }, ...options, }); if (!res.ok) throw new Error(await res.text()); return res.json(); } export const api = { // Accounts listAccounts: () => request('/accounts'), getAccount: (id: string) => request(`/accounts/${id}`), createAccount: (data: Partial) => request('/accounts', { method: 'POST', body: JSON.stringify(data) }), updateAccount: (id: string, data: Partial) => request(`/accounts/${id}`, { method: 'PUT', body: JSON.stringify(data) }), deleteAccount: (id: string) => request(`/accounts/${id}`, { method: 'DELETE' }), // Prompts getPrompt: (accountId: string, type: string) => request<{ path: string; content: string }>(`/prompts/${accountId}/${type}`), savePrompt: (accountId: string, type: string, content: string) => request(`/prompts/${accountId}/${type}`, { method: 'PUT', body: JSON.stringify({ content }) }), // Conversations listConversations: () => request('/pipeline/conversations'), getMessages: (convId: string) => request(`/pipeline/conversations/${convId}/messages`), // Assets listAssets: (params?: { accountId?: string; type?: string }) => { const qs = new URLSearchParams(); if (params?.accountId) qs.set('accountId', params.accountId); if (params?.type) qs.set('type', params.type); return request(`/assets?${qs}`); }, deleteAsset: (id: string) => request(`/assets/${id}`, { method: 'DELETE' }), // Configs getConfigs: () => request('/configs'), saveConfig: (key: string, value: Record) => request(`/configs/${key}`, { method: 'PUT', body: JSON.stringify({ value }) }), }; ``` - [ ] **Step 2: Commit** ```bash git add web/client/src/lib/api.ts git commit -m "feat(web): add typed API client" ``` --- ### Task 1.2: 账户 CRUD 后端路由 **Files:** - Create: `web/server/routes/accounts.ts` - [ ] **Step 1: 创建 server/routes/accounts.ts** ```typescript import { Router } from 'express'; import fs from 'fs'; import path from 'path'; const ACCOUNTS_DIR = path.resolve(__dirname, '..', '..', '..', 'accounts'); export const accountsRouter = Router(); function readAccountJson(id: string): Record | null { const p = path.join(ACCOUNTS_DIR, id, 'account.json'); if (!fs.existsSync(p)) return null; return JSON.parse(fs.readFileSync(p, 'utf-8')); } function writeAccountJson(id: string, data: Record): void { const dir = path.join(ACCOUNTS_DIR, id); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(path.join(dir, 'account.json'), JSON.stringify(data, null, 2), 'utf-8'); } // List accountsRouter.get('/', (_req, res) => { const dirs = fs.readdirSync(ACCOUNTS_DIR, { withFileTypes: true }) .filter((d) => d.isDirectory() && !d.name.startsWith('_') && !d.name.startsWith('.')) .map((d) => d.name); const accounts = dirs .map((id) => readAccountJson(id)) .filter(Boolean); res.json(accounts); }); // Get accountsRouter.get('/:id', (req, res) => { const data = readAccountJson(req.params.id); if (!data) return res.status(404).json({ error: 'Account not found' }); res.json(data); }); // Create accountsRouter.post('/', (req, res) => { const { id, ...rest } = req.body; if (!id) return res.status(400).json({ error: 'id is required' }); const dir = path.join(ACCOUNTS_DIR, id); if (fs.existsSync(dir)) return res.status(409).json({ error: 'Account already exists' }); const data = { id, name: rest.name || id, description: rest.description || '', defaultFormat: rest.defaultFormat || '9:16', imageModel: rest.imageModel || 'gemini', videoModel: rest.videoModel || 'veo3-fast', batchSize: rest.batchSize || 30, ttsVoice: rest.ttsVoice || '', ttsInstruction: rest.ttsInstruction || '', storyboardPrompt: rest.storyboardPrompt || 'prompts/分镜.md', imageStylePrompt: rest.imageStylePrompt || 'prompts/图片提示词.md', videoStylePrompt: rest.videoStylePrompt || 'prompts/视频提示词.md', references: rest.references || [], capcut: rest.capcut || {}, }; writeAccountJson(id, data); res.status(201).json(data); }); // Update accountsRouter.put('/:id', (req, res) => { const existing = readAccountJson(req.params.id); if (!existing) return res.status(404).json({ error: 'Account not found' }); const merged = { ...existing, ...req.body, id: req.params.id }; writeAccountJson(req.params.id, merged); res.json(merged); }); // Delete accountsRouter.delete('/:id', (req, res) => { const dir = path.join(ACCOUNTS_DIR, req.params.id); if (!fs.existsSync(dir)) return res.status(404).json({ error: 'Account not found' }); fs.rmSync(dir, { recursive: true }); res.status(204).send(); }); ``` - [ ] **Step 2: Commit** ```bash git add web/server/routes/accounts.ts git commit -m "feat(web): add account CRUD API routes" ``` --- ### Task 1.3: 配置 CRUD 后端路由 **Files:** - Create: `web/server/routes/configs.ts` - [ ] **Step 1: 创建 server/routes/configs.ts** ```typescript import { Router } from 'express'; import { getDb } from '../db'; export const configsRouter = Router(); configsRouter.get('/', (_req, res) => { const rows = getDb().prepare('SELECT * FROM configs ORDER BY key').all(); res.json(rows); }); configsRouter.get('/:key', (req, res) => { const row = getDb().prepare('SELECT * FROM configs WHERE key = ?').get(req.params.key); if (!row) return res.status(404).json({ error: 'Config not found' }); res.json(row); }); configsRouter.put('/:key', (req, res) => { const { value } = req.body; getDb().prepare(` INSERT INTO configs (id, key, value, updated_at) VALUES (?, ?, ?, datetime('now')) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at `).run(crypto.randomUUID(), req.params.key, JSON.stringify(value)); res.json({ key: req.params.key, ok: true }); }); ``` - [ ] **Step 2: Commit** ```bash git add web/server/routes/configs.ts git commit -m "feat(web): add config CRUD API routes" ``` --- ### Task 1.4: 账户列表 + 表单前端组件 **Files:** - Replace: `web/client/src/components/accounts/AccountList.tsx` - Create: `web/client/src/components/accounts/AccountForm.tsx` - Create: `web/client/src/hooks/useAccounts.ts` - [ ] **Step 1: 创建 hooks/useAccounts.ts** ```typescript import { useState, useEffect, useCallback } from 'react'; import { api } from '@/lib/api'; import type { Account } from '@/types'; export function useAccounts() { const [accounts, setAccounts] = useState([]); const [loading, setLoading] = useState(true); const refresh = useCallback(() => { api.listAccounts().then(setAccounts).finally(() => setLoading(false)); }, []); useEffect(() => { refresh(); }, [refresh]); const create = (data: Partial) => api.createAccount(data).then(refresh); const update = (id: string, data: Partial) => api.updateAccount(id, data).then(refresh); const remove = (id: string) => api.deleteAccount(id).then(refresh); return { accounts, loading, refresh, create, update, remove }; } ``` - [ ] **Step 2: 重写 AccountList.tsx** ```typescript import { Plus } from 'lucide-react'; import { useState } from 'react'; import { useAccounts } from '@/hooks/useAccounts'; import { AccountForm } from './AccountForm'; import { Button } from '@/components/ui/button'; import { ScrollArea } from '@/components/ui/scroll-area'; import type { Account } from '@/types'; export function AccountList() { const { accounts, create, update, remove } = useAccounts(); const [editing, setEditing] = useState(null); const [creating, setCreating] = useState(false); return (

账户列表

{accounts.map((a) => ( ))}
{(editing || creating) ? ( { if (creating) { create(data).then(() => setCreating(false)); } else { update(editing!.id, data).then(() => setEditing(null)); } }} onDelete={editing ? () => { if (confirm(`确定删除账户「${editing.name}」?`)) { remove(editing.id).then(() => setEditing(null)); } } : undefined} onCancel={() => { setEditing(null); setCreating(false); }} /> ) : (
选择一个账户或创建新账户
)}
); } ``` - [ ] **Step 3: 创建 AccountForm.tsx** ```typescript import { useState } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import type { Account } from '@/types'; interface Props { account?: Account; onSave: (data: Partial) => void; onDelete?: () => void; onCancel: () => void; } export function AccountForm({ account, onSave, onDelete, onCancel }: Props) { const [form, setForm] = useState({ id: account?.id || '', name: account?.name || '', description: account?.description || '', defaultFormat: account?.defaultFormat || '9:16', imageModel: account?.imageModel || 'gemini', videoModel: account?.videoModel || 'veo3-fast', ttsVoice: account?.ttsVoice || '', ttsInstruction: account?.ttsInstruction || '', }); const handleChange = (key: string, value: string) => setForm((f) => ({ ...f, [key]: value })); return (

{account ? `编辑账户: ${account.name}` : '创建新账户'}

handleChange('id', e.target.value)} disabled={!!account} placeholder="my-account" className="mt-1 bg-zinc-900 border-zinc-800" />
handleChange('name', e.target.value)} placeholder="账户名称" className="mt-1 bg-zinc-900 border-zinc-800" />
handleChange('description', e.target.value)} placeholder="简短描述..." className="mt-1 bg-zinc-900 border-zinc-800" />
handleChange('ttsVoice', e.target.value)} placeholder="cosyvoice-xxx" className="mt-1 bg-zinc-900 border-zinc-800 font-mono text-xs" />