chore: simplify project entrypoints
This commit is contained in:
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Skill(codex)",
|
||||
"Bash(pip install:*)",
|
||||
"Bash(alembic upgrade:*)",
|
||||
"Bash(uvicorn:*)",
|
||||
"Bash(npm run dev)",
|
||||
"Bash(python:*)",
|
||||
"Bash(ruff check:*)",
|
||||
"Bash(tasklist:*)",
|
||||
"Bash(findstr:*)",
|
||||
"Bash(pushd:*)",
|
||||
"Bash(popd)",
|
||||
"Bash(curl:*)",
|
||||
"WebSearch",
|
||||
"WebFetch(domain:www.novelai.net)",
|
||||
"WebFetch(domain:www.storywizard.ai)",
|
||||
"WebFetch(domain:www.oscarstories.com)",
|
||||
"WebFetch(domain:www.moshi.com)",
|
||||
"WebFetch(domain:www.calm.com)",
|
||||
"WebFetch(domain:www.epic.com)",
|
||||
"WebFetch(domain:www.headspace.com)",
|
||||
"WebFetch(domain:www.getepic.com)",
|
||||
"WebFetch(domain:www.tonies.com)",
|
||||
"Bash(del:*)",
|
||||
"Bash(netstat:*)",
|
||||
"Bash(taskkill:*)",
|
||||
"Bash(codex-wrapper:*)",
|
||||
"Bash(dir /b /s /a-d /o-d)",
|
||||
"Bash(dir:*)",
|
||||
"Bash(.venv/Scripts/python:*)",
|
||||
"Bash(.venv/Scripts/ruff check:*)",
|
||||
"Bash(npm run build:*)",
|
||||
"Bash(test -f \"F:\\\\Code\\\\dreamweaver-python\\\\backend\\\\.env\")",
|
||||
"Bash(pytest:*)",
|
||||
"Bash(npm run type-check:*)",
|
||||
"Bash(npx vue-tsc:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(git init:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git remote add:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(git branch:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,416 +0,0 @@
|
||||
# 代码架构重构 PRD
|
||||
|
||||
> 版本: 1.0
|
||||
> 日期: 2025-01-21
|
||||
> 状态: 待实施
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
### 1.1 现状问题
|
||||
|
||||
经过代码审计,发现以下架构债务:
|
||||
|
||||
| 问题 | 位置 | 影响 |
|
||||
|------|------|------|
|
||||
| 缺少 Schemas 层 | Backend API | 类型不安全,文档生成差 |
|
||||
| Fat Controller | `stories.py` 562行 | 难测试,职责混乱 |
|
||||
| 重复加载逻辑 | Frontend 5+ 组件 | 代码冗余,维护困难 |
|
||||
| 无 Repository 层 | Backend | DB 查询散落,难复用 |
|
||||
|
||||
### 1.2 重构目标
|
||||
|
||||
- **后端**:引入 Pydantic Schemas 层,分离请求/响应模型
|
||||
- **前端**:抽取 Vue Composables,消除重复逻辑
|
||||
- **原则**:渐进式重构,不破坏现有功能
|
||||
|
||||
---
|
||||
|
||||
## 2. 后端重构:Schemas 层
|
||||
|
||||
### 2.1 目标结构
|
||||
|
||||
```
|
||||
backend/app/
|
||||
├── api/ # 路由层(瘦身)
|
||||
│ ├── stories.py
|
||||
│ └── ...
|
||||
├── schemas/ # 新增:Pydantic 模型
|
||||
│ ├── __init__.py
|
||||
│ ├── auth.py
|
||||
│ ├── story.py
|
||||
│ ├── profile.py
|
||||
│ ├── universe.py
|
||||
│ ├── memory.py
|
||||
│ ├── push_config.py
|
||||
│ └── common.py # 分页、错误响应等通用结构
|
||||
├── models/ # SQLAlchemy ORM(原 db/)
|
||||
└── services/ # 业务逻辑
|
||||
```
|
||||
|
||||
### 2.2 Schema 设计规范
|
||||
|
||||
#### 命名约定
|
||||
|
||||
| 类型 | 命名模式 | 示例 |
|
||||
|------|----------|------|
|
||||
| 创建请求 | `{Entity}Create` | `StoryCreate` |
|
||||
| 更新请求 | `{Entity}Update` | `ProfileUpdate` |
|
||||
| 响应模型 | `{Entity}Response` | `StoryResponse` |
|
||||
| 列表响应 | `{Entity}ListResponse` | `StoryListResponse` |
|
||||
|
||||
#### 示例:Story Schema
|
||||
|
||||
```python
|
||||
# schemas/story.py
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class StoryCreate(BaseModel):
|
||||
"""创建故事请求"""
|
||||
type: Literal["keywords", "enhance", "storybook"]
|
||||
data: str = Field(..., min_length=1, max_length=2000)
|
||||
education_theme: str | None = None
|
||||
child_profile_id: str | None = None
|
||||
universe_id: str | None = None
|
||||
|
||||
class StoryResponse(BaseModel):
|
||||
"""故事响应"""
|
||||
id: int
|
||||
title: str
|
||||
story_text: str | None
|
||||
pages: list[dict] | None
|
||||
image_url: str | None
|
||||
mode: str
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True # 支持 ORM 模型转换
|
||||
|
||||
class StoryListResponse(BaseModel):
|
||||
"""故事列表响应"""
|
||||
stories: list[StoryResponse]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
```
|
||||
|
||||
### 2.3 迁移步骤
|
||||
|
||||
| 阶段 | 任务 | 文件 |
|
||||
|------|------|------|
|
||||
| P1 | 创建 `schemas/common.py` | 分页、错误响应 |
|
||||
| P2 | 创建 `schemas/story.py` | 故事相关模型 |
|
||||
| P3 | 创建 `schemas/profile.py` | 档案相关模型 |
|
||||
| P4 | 创建 `schemas/universe.py` | 宇宙相关模型 |
|
||||
| P5 | 创建 `schemas/auth.py` | 认证相关模型 |
|
||||
| P6 | 创建 `schemas/memory.py` | 记忆相关模型 |
|
||||
| P7 | 创建 `schemas/push_config.py` | 推送配置模型 |
|
||||
| P8 | 重构 `api/stories.py` | 使用新 schemas |
|
||||
| P9 | 重构其他 API 文件 | 逐个迁移 |
|
||||
|
||||
### 2.4 验收标准
|
||||
|
||||
- [ ] 所有 API endpoint 使用 `response_model` 参数
|
||||
- [ ] 请求体使用 Pydantic 模型而非 `Body(...)`
|
||||
- [ ] OpenAPI 文档自动生成完整类型
|
||||
- [ ] 现有测试全部通过
|
||||
- [ ] `ruff check` 无错误
|
||||
|
||||
---
|
||||
|
||||
## 3. 前端重构:Composables
|
||||
|
||||
### 3.1 目标结构
|
||||
|
||||
```
|
||||
frontend/src/
|
||||
├── composables/ # 新增:可复用逻辑
|
||||
│ ├── index.ts
|
||||
│ ├── useAsyncData.ts # 异步数据加载
|
||||
│ ├── useFormValidation.ts # 表单验证
|
||||
│ └── useDateFormat.ts # 日期格式化
|
||||
├── components/
|
||||
├── stores/
|
||||
└── views/
|
||||
```
|
||||
|
||||
### 3.2 Composable 设计
|
||||
|
||||
#### 3.2.1 useAsyncData
|
||||
|
||||
**用途**:统一处理 API 数据加载的 loading/error 状态
|
||||
|
||||
```typescript
|
||||
// composables/useAsyncData.ts
|
||||
import { ref, type Ref } from 'vue'
|
||||
|
||||
interface AsyncDataOptions<T> {
|
||||
immediate?: boolean // 立即执行,默认 true
|
||||
initialData?: T // 初始数据
|
||||
onError?: (e: Error) => void
|
||||
}
|
||||
|
||||
interface AsyncDataReturn<T> {
|
||||
data: Ref<T | null>
|
||||
loading: Ref<boolean>
|
||||
error: Ref<string>
|
||||
execute: () => Promise<void>
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
export function useAsyncData<T>(
|
||||
fetcher: () => Promise<T>,
|
||||
options: AsyncDataOptions<T> = {}
|
||||
): AsyncDataReturn<T> {
|
||||
const { immediate = true, initialData = null, onError } = options
|
||||
|
||||
const data = ref<T | null>(initialData) as Ref<T | null>
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
async function execute() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
data.value = await fetcher()
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : '加载失败'
|
||||
error.value = message
|
||||
onError?.(e instanceof Error ? e : new Error(message))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
data.value = initialData
|
||||
loading.value = false
|
||||
error.value = ''
|
||||
}
|
||||
|
||||
if (immediate) {
|
||||
execute()
|
||||
}
|
||||
|
||||
return { data, loading, error, execute, reset }
|
||||
}
|
||||
```
|
||||
|
||||
**使用示例**:
|
||||
|
||||
```typescript
|
||||
// views/MyStories.vue(重构前 40 行 → 重构后 10 行)
|
||||
import { useAsyncData } from '@/composables'
|
||||
import { api } from '@/api/client'
|
||||
|
||||
const { data: response, loading, error } = useAsyncData(
|
||||
() => api.get<StoryListResponse>('/api/stories')
|
||||
)
|
||||
const stories = computed(() => response.value?.stories ?? [])
|
||||
```
|
||||
|
||||
#### 3.2.2 useFormValidation
|
||||
|
||||
**用途**:统一表单验证逻辑
|
||||
|
||||
```typescript
|
||||
// composables/useFormValidation.ts
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
type ValidationRule = (value: unknown) => string | true
|
||||
|
||||
interface FieldRules {
|
||||
[field: string]: ValidationRule[]
|
||||
}
|
||||
|
||||
export function useFormValidation<T extends Record<string, unknown>>(
|
||||
initialData: T,
|
||||
rules: FieldRules
|
||||
) {
|
||||
const form = reactive({ ...initialData })
|
||||
const errors = reactive<Record<string, string>>({})
|
||||
|
||||
function validate(): boolean {
|
||||
let valid = true
|
||||
for (const [field, fieldRules] of Object.entries(rules)) {
|
||||
const value = form[field]
|
||||
for (const rule of fieldRules) {
|
||||
const result = rule(value)
|
||||
if (result !== true) {
|
||||
errors[field] = result
|
||||
valid = false
|
||||
break
|
||||
} else {
|
||||
errors[field] = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
return valid
|
||||
}
|
||||
|
||||
function reset() {
|
||||
Object.assign(form, initialData)
|
||||
Object.keys(errors).forEach(k => errors[k] = '')
|
||||
}
|
||||
|
||||
return { form, errors, validate, reset }
|
||||
}
|
||||
|
||||
// 常用验证规则
|
||||
export const required = (msg = '此字段必填') =>
|
||||
(v: unknown) => (v !== null && v !== undefined && v !== '') || msg
|
||||
|
||||
export const minLength = (min: number, msg?: string) =>
|
||||
(v: unknown) => (typeof v === 'string' && v.length >= min) || msg || `至少 ${min} 个字符`
|
||||
|
||||
export const maxLength = (max: number, msg?: string) =>
|
||||
(v: unknown) => (typeof v === 'string' && v.length <= max) || msg || `最多 ${max} 个字符`
|
||||
```
|
||||
|
||||
#### 3.2.3 useDateFormat
|
||||
|
||||
**用途**:统一日期格式化
|
||||
|
||||
```typescript
|
||||
// composables/useDateFormat.ts
|
||||
export function useDateFormat() {
|
||||
function formatDate(dateStr: string | Date, format: 'date' | 'datetime' | 'relative' = 'date'): string {
|
||||
const date = typeof dateStr === 'string' ? new Date(dateStr) : dateStr
|
||||
|
||||
if (format === 'relative') {
|
||||
return formatRelative(date)
|
||||
}
|
||||
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
|
||||
if (format === 'date') {
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`
|
||||
}
|
||||
|
||||
function formatRelative(date: Date): string {
|
||||
const now = new Date()
|
||||
const diffMs = now.getTime() - date.getTime()
|
||||
const diffMins = Math.floor(diffMs / 60000)
|
||||
const diffHours = Math.floor(diffMs / 3600000)
|
||||
const diffDays = Math.floor(diffMs / 86400000)
|
||||
|
||||
if (diffMins < 1) return '刚刚'
|
||||
if (diffMins < 60) return `${diffMins} 分钟前`
|
||||
if (diffHours < 24) return `${diffHours} 小时前`
|
||||
if (diffDays < 7) return `${diffDays} 天前`
|
||||
return formatDate(date, 'date')
|
||||
}
|
||||
|
||||
return { formatDate, formatRelative }
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 迁移步骤
|
||||
|
||||
| 阶段 | 任务 | 影响文件 |
|
||||
|------|------|----------|
|
||||
| P1 | 创建 `composables/useAsyncData.ts` | 新文件 |
|
||||
| P2 | 创建 `composables/useDateFormat.ts` | 新文件 |
|
||||
| P3 | 创建 `composables/useFormValidation.ts` | 新文件 |
|
||||
| P4 | 创建 `composables/index.ts` | 统一导出 |
|
||||
| P5 | 重构 `views/MyStories.vue` | 使用 useAsyncData |
|
||||
| P6 | 重构 `views/ChildProfiles.vue` | 使用 useAsyncData |
|
||||
| P7 | 重构 `views/Universes.vue` | 使用 useAsyncData |
|
||||
| P8 | 重构 `components/CreateStoryModal.vue` | 使用 useFormValidation |
|
||||
| P9 | 同步重构 `admin-frontend/` | 复制 composables |
|
||||
|
||||
### 3.4 验收标准
|
||||
|
||||
- [ ] `composables/` 目录包含 3 个核心 composable
|
||||
- [ ] 至少 3 个 view 组件使用 `useAsyncData`
|
||||
- [ ] `CreateStoryModal` 使用 `useFormValidation`
|
||||
- [ ] `npm run build` 无类型错误
|
||||
- [ ] 现有功能正常运行
|
||||
|
||||
---
|
||||
|
||||
## 4. 风险与缓解
|
||||
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
|------|------|----------|
|
||||
| 重构引入 bug | 高 | 每个阶段运行测试 |
|
||||
| 类型不兼容 | 中 | 使用 `from_attributes = True` |
|
||||
| 前端构建失败 | 中 | 逐文件迁移,及时 commit |
|
||||
|
||||
---
|
||||
|
||||
## 5. 时间估算
|
||||
|
||||
| 模块 | 工作量 | 预计时间 |
|
||||
|------|--------|----------|
|
||||
| Backend Schemas | 10 个新文件 + 9 个 API 重构 | 3-4 小时 |
|
||||
| Frontend Composables | 4 个新文件 + 6 个组件重构 | 2-3 小时 |
|
||||
| 测试验证 | 全量回归 | 1 小时 |
|
||||
| **总计** | | **6-8 小时** |
|
||||
|
||||
---
|
||||
|
||||
## 6. 后续迭代
|
||||
|
||||
本次重构完成后,可继续:
|
||||
|
||||
1. **Repository 层**:抽象 DB 查询逻辑
|
||||
2. **Service 层瘦身**:拆分 `provider_router.py` (433行)
|
||||
3. **前端共享包**:`frontend` 和 `admin-frontend` 共用 UI 组件
|
||||
4. **API 版本化**:引入 `/api/v1/` 前缀
|
||||
|
||||
---
|
||||
|
||||
## 附录:文件清单
|
||||
|
||||
### 新增文件
|
||||
|
||||
```
|
||||
backend/app/schemas/
|
||||
├── __init__.py
|
||||
├── common.py
|
||||
├── auth.py
|
||||
├── story.py
|
||||
├── profile.py
|
||||
├── universe.py
|
||||
├── memory.py
|
||||
└── push_config.py
|
||||
|
||||
frontend/src/composables/
|
||||
├── index.ts
|
||||
├── useAsyncData.ts
|
||||
├── useFormValidation.ts
|
||||
└── useDateFormat.ts
|
||||
```
|
||||
|
||||
### 修改文件
|
||||
|
||||
```
|
||||
backend/app/api/
|
||||
├── stories.py
|
||||
├── profiles.py
|
||||
├── universes.py
|
||||
├── memories.py
|
||||
├── push_configs.py
|
||||
├── reading_events.py
|
||||
└── auth.py
|
||||
|
||||
frontend/src/views/
|
||||
├── MyStories.vue
|
||||
├── ChildProfiles.vue
|
||||
├── Universes.vue
|
||||
└── ...
|
||||
|
||||
frontend/src/components/
|
||||
└── CreateStoryModal.vue
|
||||
```
|
||||
@@ -1,153 +0,0 @@
|
||||
# DreamWeaver 品牌视觉方向(Web 阶段)
|
||||
|
||||
## 概述
|
||||
|
||||
提供三套高保真视觉方向,用于 Web MVP。三者的 UX 结构一致,仅在色彩、视觉重量与插画风格上不同。
|
||||
|
||||
---
|
||||
|
||||
## 方案 A:Soft Aurora(温暖高级)
|
||||
|
||||
**理由**
|
||||
- 家长信任感强,同时保留童趣与想象力。
|
||||
- 高级但不商业化。
|
||||
|
||||
**配色**
|
||||
- 主色 600: #6C5CE7
|
||||
- 主色 500: #7C69FF
|
||||
- 主色 100: #EAE7FF
|
||||
- 强调粉: #FF8FB1
|
||||
- 强调蓝: #65C3FF
|
||||
- 中性 900: #1F2430
|
||||
- 中性 700: #4B5563
|
||||
- 中性 500: #9AA3B2
|
||||
- 中性 200: #E5E7EB
|
||||
- 中性 100: #F5F7FB
|
||||
- 白色: #FFFFFF
|
||||
|
||||
**渐变**
|
||||
- Hero 背景:linear-gradient(135deg, #EAE7FF 0%, #FDF6FF 40%, #EAF6FF 100%)
|
||||
- CTA 光晕:radial-gradient(circle at 30% 30%, #7C69FF 0%, #6C5CE7 50%, #4C3FCF 100%)
|
||||
|
||||
**字体**
|
||||
- 标题:Noto Sans SC / Inter
|
||||
- 正文:Noto Sans SC / Inter
|
||||
- 数字强调:Inter
|
||||
|
||||
**插画风格**
|
||||
- 柔和、低对比度、轻画笔质感。
|
||||
- 圆润形状、轻高光。
|
||||
- 角色简单轮廓与友好表情。
|
||||
|
||||
**图标风格**
|
||||
- 1.5px 线宽,圆角端点。
|
||||
- 强调色点缀,避免过度饱和。
|
||||
|
||||
**组件建议**
|
||||
- 按钮:主色实心 + 内阴影。
|
||||
- 卡片:大圆角 + 柔和阴影。
|
||||
- 输入:浅底色 + 主色焦点环。
|
||||
|
||||
---
|
||||
|
||||
## 方案 B:Storybook Minimal(极简编辑风)
|
||||
|
||||
**理由**
|
||||
- 强可读性,适合长文本阅读。
|
||||
- 简洁、专业、强调内容。
|
||||
|
||||
**配色**
|
||||
- 主色 600: #3B82F6
|
||||
- 主色 500: #60A5FA
|
||||
- 主色 100: #DBEAFE
|
||||
- 强调金: #F5C542
|
||||
- 强调薄荷: #6EE7B7
|
||||
- 中性 900: #111827
|
||||
- 中性 700: #374151
|
||||
- 中性 500: #9CA3AF
|
||||
- 中性 200: #E5E7EB
|
||||
- 中性 100: #F9FAFB
|
||||
- 白色: #FFFFFF
|
||||
|
||||
**渐变**
|
||||
- Hero 背景:linear-gradient(180deg, #F9FAFB 0%, #EEF2FF 100%)
|
||||
- CTA 光晕:radial-gradient(circle at 40% 30%, #60A5FA 0%, #3B82F6 60%, #1D4ED8 100%)
|
||||
|
||||
**字体**
|
||||
- 标题:Inter / Noto Sans SC
|
||||
- 正文:Inter / Noto Sans SC
|
||||
- 阅读场景可提升行高和对比度。
|
||||
|
||||
**插画风格**
|
||||
- 扁平化、线条干净、留白较多。
|
||||
- 色彩克制、视觉清爽。
|
||||
|
||||
**图标风格**
|
||||
- 2px 线宽,极简。
|
||||
|
||||
**组件建议**
|
||||
- 按钮:纯色、无明显渐变。
|
||||
- 卡片:细边框 + 极轻阴影。
|
||||
- 输入:白底 + 清晰边框。
|
||||
|
||||
---
|
||||
|
||||
## 方案 C:Playful Glow(活力明快)
|
||||
|
||||
**理由**
|
||||
- 视觉更鲜活,记忆点强。
|
||||
- 更偏童趣,但仍保持专业感。
|
||||
|
||||
**配色**
|
||||
- 主色 600: #7C3AED
|
||||
- 主色 500: #8B5CF6
|
||||
- 主色 100: #EDE9FE
|
||||
- 强调珊瑚: #FB7185
|
||||
- 强调青蓝: #22D3EE
|
||||
- 中性 900: #1F2937
|
||||
- 中性 700: #4B5563
|
||||
- 中性 500: #9CA3AF
|
||||
- 中性 200: #E5E7EB
|
||||
- 中性 100: #F5F5F7
|
||||
- 白色: #FFFFFF
|
||||
|
||||
**渐变**
|
||||
- Hero 背景:linear-gradient(135deg, #EDE9FE 0%, #FFE4F3 45%, #E0F7FF 100%)
|
||||
- CTA 光晕:radial-gradient(circle at 30% 30%, #8B5CF6 0%, #7C3AED 60%, #5B21B6 100%)
|
||||
|
||||
**字体**
|
||||
- 标题:Noto Sans SC / Inter
|
||||
- 正文:Noto Sans SC / Inter
|
||||
- 强调色点到为止,避免花哨。
|
||||
|
||||
**插画风格**
|
||||
- 更鲜艳、更活泼。
|
||||
- 大色块 + 轻纹理背景。
|
||||
|
||||
**图标风格**
|
||||
- 1.5px 线宽 + 小实心点装饰。
|
||||
|
||||
**组件建议**
|
||||
- 按钮:渐变或实心 + Hover 发光。
|
||||
- 卡片:更明显阴影 + 彩色边角。
|
||||
- 输入:轻微色彩底。
|
||||
|
||||
---
|
||||
|
||||
## 共享视觉资产
|
||||
|
||||
**封面比例**
|
||||
- 列表卡片:21:9
|
||||
- 详情头图:16:9
|
||||
|
||||
**插画 vs 照片**
|
||||
- 默认使用插画,避免真实儿童照片(隐私与合规)。
|
||||
|
||||
**空态插画**
|
||||
- 统一 1 张主插画,做颜色变体复用。
|
||||
|
||||
---
|
||||
|
||||
## 推荐
|
||||
|
||||
建议 Web MVP 使用方案 A(Soft Aurora),兼顾温暖与信任。方案 B/C 可作为后续主题或 A/B 测试备选。
|
||||
@@ -1,639 +0,0 @@
|
||||
# DreamWeaver 落地页重构规范文档
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
### 1.1 目标
|
||||
将当前简单的 Home.vue 落地页重构为专业级 SaaS 产品落地页,提升品牌形象和用户转化率。
|
||||
|
||||
### 1.2 当前状态
|
||||
- 文件位置: `frontend/src/views/Home.vue`
|
||||
- 问题: 页面结构单一,仅有一个故事生成表单,缺少产品介绍、功能展示、用户信任背书等专业落地页必备元素
|
||||
|
||||
### 1.3 技术栈
|
||||
- Vue 3 + Composition API + TypeScript
|
||||
- Tailwind CSS
|
||||
- vue-i18n 国际化
|
||||
- @heroicons/vue/24/outline 图标库
|
||||
- 现有 UI 组件: BaseButton, BaseCard, BaseInput, BaseSelect, BaseTextarea
|
||||
|
||||
---
|
||||
|
||||
## 2. 页面结构规范
|
||||
|
||||
页面从上到下包含 8 个主要区块(Section),每个区块独立且可复用。
|
||||
|
||||
### 2.1 Hero Section(主视觉区)
|
||||
|
||||
**布局**: 两栏布局,左 60% 右 40%,移动端堆叠
|
||||
|
||||
**左侧内容**:
|
||||
```
|
||||
- 主标题: 使用 gradient-text 样式
|
||||
- 第一行: "为孩子编织" (普通渐变)
|
||||
- 第二行: "专属的童话梦境" (加粗强调)
|
||||
- 副标题: 灰色次要文字,说明产品价值
|
||||
- CTA 按钮组:
|
||||
- 主按钮: "开始创作" (btn-magic 样式,点击打开创作模态框)
|
||||
- 次按钮: "了解更多" (outline 样式,滚动到 Features 区块)
|
||||
```
|
||||
|
||||
**右侧内容**:
|
||||
```
|
||||
- 故事卡片预览 (模拟产品效果)
|
||||
- 卡片使用 glass 样式 + 阴影
|
||||
- 顶部: 模拟封面图区域 (渐变色块 + 星星图标)
|
||||
- 标题: "小兔子的勇气冒险"
|
||||
- 内容预览: 故事开头文字 (截断显示)
|
||||
- 底部: 模拟的播放按钮和图片生成按钮图标
|
||||
- 添加浮动动画 (animate-float)
|
||||
```
|
||||
|
||||
**背景装饰**:
|
||||
```
|
||||
- 左上角: 浮动星星 SVG (absolute, opacity-20)
|
||||
- 右下角: 浮动云朵 SVG (absolute, opacity-15)
|
||||
- 使用 CSS animation 实现缓慢浮动效果
|
||||
```
|
||||
|
||||
**i18n 键**:
|
||||
- `home.heroTitle`, `home.heroTitleHighlight`
|
||||
- `home.heroSubtitle`
|
||||
- `home.heroCta`, `home.heroCtaSecondary`
|
||||
- `home.heroPreviewTitle`, `home.heroPreviewText`
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Trust Bar(信任背书区)
|
||||
|
||||
**布局**: 水平三等分,居中对齐
|
||||
|
||||
**内容**:
|
||||
```
|
||||
| 10,000+ 故事已创作 | 5,000+ 家庭信赖 | 98% 满意度 |
|
||||
```
|
||||
|
||||
**样式**:
|
||||
```
|
||||
- 背景: 浅紫色渐变 (from-purple-50 to-pink-50)
|
||||
- 数字: 大号加粗,渐变色
|
||||
- 文字: 灰色小号
|
||||
- 分隔: 使用竖线或间距分隔
|
||||
```
|
||||
|
||||
**交互**:
|
||||
```
|
||||
- 数字使用计数动画 (从 0 递增到目标值)
|
||||
- 使用 IntersectionObserver 触发动画
|
||||
- 动画时长: 2 秒,使用 easeOutQuart 缓动
|
||||
```
|
||||
|
||||
**实现要点**:
|
||||
```typescript
|
||||
// 计数动画函数
|
||||
function animateCount(target: number, duration: number, callback: (value: number) => void) {
|
||||
const start = performance.now()
|
||||
const step = (timestamp: number) => {
|
||||
const progress = Math.min((timestamp - start) / duration, 1)
|
||||
const eased = 1 - Math.pow(1 - progress, 4) // easeOutQuart
|
||||
callback(Math.floor(eased * target))
|
||||
if (progress < 1) requestAnimationFrame(step)
|
||||
}
|
||||
requestAnimationFrame(step)
|
||||
}
|
||||
```
|
||||
|
||||
**i18n 键**:
|
||||
- `home.trustStoriesCreated`, `home.trustFamilies`, `home.trustSatisfaction`
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Features Section(功能特性区)
|
||||
|
||||
**布局**: 标题 + 副标题 + 6 卡片网格 (3x2,移动端 1 列)
|
||||
|
||||
**标题区**:
|
||||
```
|
||||
- 主标题: "为什么选择梦语织机"
|
||||
- 副标题: "我们用 AI 技术和教育理念,为每个孩子打造独一无二的成长故事"
|
||||
```
|
||||
|
||||
**6 个功能卡片**:
|
||||
|
||||
| # | 图标 | 标题 | 描述 |
|
||||
|---|------|------|------|
|
||||
| 1 | SparklesIcon | AI 智能创作 | 输入几个关键词,AI 即刻为您的孩子创作一个充满想象力的原创故事 |
|
||||
| 2 | UserIcon | 个性化记忆 | 系统记住孩子的喜好和成长轨迹,故事越来越懂 TA |
|
||||
| 3 | PhotoIcon | 精美 AI 插画 | 为每个故事自动生成独特的精美封面插画,让故事更加生动 |
|
||||
| 4 | SpeakerWaveIcon | 温暖语音朗读 | 专业级 AI 配音,温暖的声音陪伴孩子进入甜美梦乡 |
|
||||
| 5 | AcademicCapIcon | 教育主题融入 | 勇气、友谊、分享、诚实...在故事中自然传递正向价值观 |
|
||||
| 6 | GlobeAltIcon | 故事宇宙 | 创建专属世界观,让喜爱的角色在不同故事中持续冒险 |
|
||||
|
||||
**卡片样式**:
|
||||
```
|
||||
- 使用 BaseCard 组件,添加 hover 效果
|
||||
- 图标: 48x48,紫色渐变背景圆形容器
|
||||
- 标题: font-bold text-gray-800
|
||||
- 描述: text-gray-600 text-sm
|
||||
- hover: 上移 + 阴影增强
|
||||
```
|
||||
|
||||
**滚动动画**:
|
||||
```
|
||||
- 使用 IntersectionObserver
|
||||
- 卡片依次渐入 (stagger 100ms)
|
||||
- 动画: opacity 0->1, translateY 20px->0
|
||||
```
|
||||
|
||||
**i18n 键**:
|
||||
- `home.featuresTitle`, `home.featuresSubtitle`
|
||||
- `home.feature1Title` ~ `home.feature6Title`
|
||||
- `home.feature1Desc` ~ `home.feature6Desc`
|
||||
|
||||
---
|
||||
|
||||
### 2.4 How It Works Section(使用流程区)
|
||||
|
||||
**布局**: 标题 + 4 步骤水平排列(移动端垂直)
|
||||
|
||||
**步骤内容**:
|
||||
|
||||
| 步骤 | 图标 | 标题 | 描述 |
|
||||
|------|------|------|------|
|
||||
| 1 | LightBulbIcon | 输入灵感 | 输入关键词、角色或简单想法 |
|
||||
| 2 | CpuChipIcon | AI 创作 | AI 根据输入生成专属故事 |
|
||||
| 3 | PaintBrushIcon | 丰富内容 | 自动生成精美插画和语音 |
|
||||
| 4 | ShareIcon | 分享故事 | 保存收藏,随时为孩子讲述 |
|
||||
|
||||
**样式**:
|
||||
```
|
||||
- 步骤编号: 圆形渐变背景,白色数字
|
||||
- 步骤之间: 虚线连接 (桌面端水平,移动端垂直)
|
||||
- 图标: 在编号下方,较大尺寸
|
||||
- 文字: 居中对齐
|
||||
```
|
||||
|
||||
**连接线实现**:
|
||||
```css
|
||||
/* 桌面端水平连接线 */
|
||||
.step-connector {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
left: 100%;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, #c084fc, #f472b6);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* 移动端隐藏水平线,显示垂直线 */
|
||||
@media (max-width: 768px) {
|
||||
.step-connector { display: none; }
|
||||
.step-connector-vertical { display: block; }
|
||||
}
|
||||
```
|
||||
|
||||
**i18n 键**:
|
||||
- `home.howItWorksTitle`, `home.howItWorksSubtitle`
|
||||
- `home.step1Title` ~ `home.step4Title`
|
||||
- `home.step1Desc` ~ `home.step4Desc`
|
||||
|
||||
---
|
||||
|
||||
### 2.5 Product Showcase Section(产品展示区)
|
||||
|
||||
**布局**: 两栏,左侧功能列表,右侧模拟界面
|
||||
|
||||
**左侧内容**:
|
||||
```
|
||||
- 小标题: "专为家长设计"
|
||||
- 大标题: "简单易用,功能强大"
|
||||
- 功能列表 (带勾选图标):
|
||||
✓ 直观的创作界面,几秒即可生成故事
|
||||
✓ 多孩子档案管理,每个孩子独立记忆
|
||||
✓ 故事历史永久保存,随时回顾美好时光
|
||||
✓ 支持中英双语,培养语言能力
|
||||
```
|
||||
|
||||
**右侧内容**:
|
||||
```
|
||||
- 模拟的产品界面截图 (CSS 绘制)
|
||||
- 包含:
|
||||
- 模拟浏览器顶栏 (三个圆点)
|
||||
- 模拟导航栏
|
||||
- 模拟故事卡片列表
|
||||
- 使用 glass 样式 + 阴影
|
||||
- 轻微倾斜 (transform: perspective + rotateY)
|
||||
```
|
||||
|
||||
**模拟界面 CSS**:
|
||||
```css
|
||||
.mock-browser {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
transform: perspective(1000px) rotateY(-5deg);
|
||||
}
|
||||
|
||||
.mock-browser-bar {
|
||||
height: 32px;
|
||||
background: #f1f5f9;
|
||||
border-radius: 12px 12px 0 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.mock-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.mock-dot-red { background: #ef4444; }
|
||||
.mock-dot-yellow { background: #eab308; }
|
||||
.mock-dot-green { background: #22c55e; }
|
||||
```
|
||||
|
||||
**i18n 键**:
|
||||
- `home.showcaseTitle`, `home.showcaseSubtitle`
|
||||
- `home.showcaseFeature1` ~ `home.showcaseFeature4`
|
||||
|
||||
---
|
||||
|
||||
### 2.6 Testimonials Section(用户评价区)
|
||||
|
||||
**布局**: 标题 + 3 评价卡片水平排列
|
||||
|
||||
**评价内容**:
|
||||
|
||||
| # | 评价 | 用户名 | 身份 |
|
||||
|---|------|--------|------|
|
||||
| 1 | "每晚睡前,女儿都要听一个新故事。梦语织机让我不再为编故事发愁,而且故事质量真的很高!" | 小雨妈妈 | 5岁女孩家长 |
|
||||
| 2 | "最惊喜的是个性化功能,系统记住了儿子喜欢恐龙和太空,每个故事都能戳中他的兴趣点。" | 航航爸爸 | 6岁男孩家长 |
|
||||
| 3 | "语音朗读功能太棒了!出差时也能远程给孩子讲故事,声音温暖自然,孩子很喜欢。" | 朵朵妈妈 | 4岁女孩家长 |
|
||||
|
||||
**卡片样式**:
|
||||
```
|
||||
- 背景: glass 样式
|
||||
- 顶部: 引号图标 (大号,低透明度)
|
||||
- 评价文字: 斜体,灰色
|
||||
- 底部: 头像 + 用户名 + 身份
|
||||
- 头像: 渐变色圆形 + 首字母
|
||||
```
|
||||
|
||||
**头像生成**:
|
||||
```typescript
|
||||
// 根据名字生成渐变色头像
|
||||
function getAvatarStyle(name: string) {
|
||||
const colors = [
|
||||
['#667eea', '#764ba2'],
|
||||
['#f093fb', '#f5576c'],
|
||||
['#4facfe', '#00f2fe'],
|
||||
]
|
||||
const index = name.charCodeAt(0) % colors.length
|
||||
return {
|
||||
background: `linear-gradient(135deg, ${colors[index][0]}, ${colors[index][1]})`,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**i18n 键**:
|
||||
- `home.testimonialsTitle`, `home.testimonialsSubtitle`
|
||||
- `home.testimonial1Text` ~ `home.testimonial3Text`
|
||||
- `home.testimonial1Name` ~ `home.testimonial3Name`
|
||||
- `home.testimonial1Role` ~ `home.testimonial3Role`
|
||||
|
||||
---
|
||||
|
||||
### 2.7 FAQ Section(常见问题区)
|
||||
|
||||
**布局**: 标题 + 手风琴问答列表
|
||||
|
||||
**问答内容**:
|
||||
|
||||
| # | 问题 | 答案 |
|
||||
|---|------|------|
|
||||
| 1 | 梦语织机适合多大的孩子? | 我们专为 3-8 岁儿童设计,故事内容、语言难度和教育主题都针对这个年龄段优化。 |
|
||||
| 2 | 生成的故事安全吗? | 绝对安全。所有故事都经过内容过滤,确保适合儿童阅读,传递积极正向的价值观。 |
|
||||
| 3 | 可以自定义故事角色吗? | 可以!您可以在孩子档案中设置喜好,或在创作时指定角色名称、特点,AI 会将其融入故事。 |
|
||||
| 4 | 故事会重复吗? | 不会。每个故事都是 AI 实时原创生成的,即使使用相同关键词,也会产生不同的故事。 |
|
||||
| 5 | 支持哪些语言? | 目前支持中文和英文,您可以随时切换界面语言,故事也会相应调整。 |
|
||||
|
||||
**手风琴实现**:
|
||||
```typescript
|
||||
const expandedFaq = ref<number | null>(null)
|
||||
|
||||
function toggleFaq(index: number) {
|
||||
expandedFaq.value = expandedFaq.value === index ? null : index
|
||||
}
|
||||
```
|
||||
|
||||
**样式**:
|
||||
```
|
||||
- 问题行: 可点击,右侧箭头图标
|
||||
- 展开时: 箭头旋转 180°,答案滑入显示
|
||||
- 使用 Transition 组件实现平滑动画
|
||||
```
|
||||
|
||||
**i18n 键**:
|
||||
- `home.faqTitle`
|
||||
- `home.faq1Question` ~ `home.faq5Question`
|
||||
- `home.faq1Answer` ~ `home.faq5Answer`
|
||||
|
||||
---
|
||||
|
||||
### 2.8 Final CTA Section(底部转化区)
|
||||
|
||||
**布局**: 居中,渐变背景
|
||||
|
||||
**内容**:
|
||||
```
|
||||
- 大标题: "准备好为孩子创造魔法了吗?"
|
||||
- 副标题: "立即开始,让 AI 为您的孩子编织独一无二的成长故事"
|
||||
- CTA 按钮: "免费开始创作" (大号,btn-magic)
|
||||
- 小字: "无需信用卡,立即体验"
|
||||
```
|
||||
|
||||
**背景**:
|
||||
```css
|
||||
.cta-section {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 装饰性圆形 */
|
||||
.cta-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
top: -200px;
|
||||
right: -100px;
|
||||
}
|
||||
```
|
||||
|
||||
**i18n 键**:
|
||||
- `home.ctaTitle`, `home.ctaSubtitle`
|
||||
- `home.ctaButton`, `home.ctaNote`
|
||||
|
||||
---
|
||||
|
||||
## 3. 创作模态框规范
|
||||
|
||||
### 3.1 触发方式
|
||||
- 点击 Hero 区 "开始创作" 按钮
|
||||
- 点击 Final CTA 区按钮
|
||||
- 已登录用户直接打开模态框
|
||||
- 未登录用户跳转登录流程
|
||||
|
||||
### 3.2 模态框结构
|
||||
```
|
||||
- 遮罩层: 半透明黑色背景
|
||||
- 模态框: 居中,最大宽度 600px
|
||||
- 关闭按钮: 右上角 X 图标
|
||||
- 内容: 复用原有表单逻辑
|
||||
```
|
||||
|
||||
### 3.3 表单内容(保留原有逻辑)
|
||||
```
|
||||
1. 输入类型切换: 关键词创作 / 故事润色
|
||||
2. 孩子档案选择 (可选)
|
||||
3. 故事宇宙选择 (可选,依赖档案)
|
||||
4. 输入区域 (关键词或故事文本)
|
||||
5. 教育主题选择 (可选)
|
||||
6. 提交按钮
|
||||
```
|
||||
|
||||
### 3.4 状态管理
|
||||
```typescript
|
||||
const showCreateModal = ref(false)
|
||||
|
||||
function openCreateModal() {
|
||||
if (!userStore.user) {
|
||||
// 跳转登录
|
||||
return
|
||||
}
|
||||
showCreateModal.value = true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 动画规范
|
||||
|
||||
### 4.1 滚动渐入动画
|
||||
|
||||
**实现方式**: 使用 IntersectionObserver + CSS Transition
|
||||
|
||||
```typescript
|
||||
// composables/useScrollAnimation.ts
|
||||
export function useScrollAnimation() {
|
||||
const observedElements = ref<Set<Element>>(new Set())
|
||||
|
||||
onMounted(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('animate-in')
|
||||
observer.unobserve(entry.target)
|
||||
}
|
||||
})
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
)
|
||||
|
||||
document.querySelectorAll('.scroll-animate').forEach((el) => {
|
||||
observer.observe(el)
|
||||
})
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**CSS**:
|
||||
```css
|
||||
.scroll-animate {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition: opacity 0.6s ease, transform 0.6s ease;
|
||||
}
|
||||
|
||||
.scroll-animate.animate-in {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 延迟类 */
|
||||
.delay-100 { transition-delay: 100ms; }
|
||||
.delay-200 { transition-delay: 200ms; }
|
||||
.delay-300 { transition-delay: 300ms; }
|
||||
```
|
||||
|
||||
### 4.2 浮动动画
|
||||
|
||||
```css
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
.animate-float {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-float-slow {
|
||||
animation: float 5s ease-in-out infinite;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 数字计数动画
|
||||
|
||||
见 2.2 节 Trust Bar 实现要点。
|
||||
|
||||
---
|
||||
|
||||
## 5. 响应式规范
|
||||
|
||||
### 5.1 断点定义
|
||||
```
|
||||
- sm: 640px
|
||||
- md: 768px
|
||||
- lg: 1024px
|
||||
- xl: 1280px
|
||||
```
|
||||
|
||||
### 5.2 各区块响应式行为
|
||||
|
||||
| 区块 | 桌面端 | 平板端 | 移动端 |
|
||||
|------|--------|--------|--------|
|
||||
| Hero | 两栏 60/40 | 两栏 50/50 | 单栏堆叠 |
|
||||
| Trust Bar | 水平三等分 | 水平三等分 | 垂直堆叠 |
|
||||
| Features | 3x2 网格 | 2x3 网格 | 单列 |
|
||||
| How It Works | 水平 4 步 | 水平 4 步 | 垂直 4 步 |
|
||||
| Showcase | 两栏 | 两栏 | 单栏堆叠 |
|
||||
| Testimonials | 水平 3 卡 | 水平 3 卡 | 单列滚动 |
|
||||
| FAQ | 单列 | 单列 | 单列 |
|
||||
| Final CTA | 居中 | 居中 | 居中 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 暗色模式规范
|
||||
|
||||
### 6.1 颜色映射
|
||||
|
||||
| 元素 | 亮色模式 | 暗色模式 |
|
||||
|------|----------|----------|
|
||||
| 背景 | 渐变浅色 | 渐变深色 |
|
||||
| 卡片背景 | rgba(255,255,255,0.7) | rgba(15,23,42,0.6) |
|
||||
| 主文字 | gray-800 | gray-100 |
|
||||
| 次文字 | gray-600 | gray-400 |
|
||||
| 边框 | gray-200 | gray-700 |
|
||||
|
||||
### 6.2 实现方式
|
||||
使用 Tailwind dark: 前缀,配合现有 .dark 类切换。
|
||||
|
||||
---
|
||||
|
||||
## 7. 验收标准
|
||||
|
||||
### 7.1 功能验收
|
||||
- [ ] Hero 区正确显示,CTA 按钮可点击
|
||||
- [ ] Trust Bar 数字动画正常触发
|
||||
- [ ] Features 6 个卡片正确显示
|
||||
- [ ] How It Works 4 步骤正确显示,连接线可见
|
||||
- [ ] Product Showcase 模拟界面正确渲染
|
||||
- [ ] Testimonials 3 个评价卡片正确显示
|
||||
- [ ] FAQ 手风琴展开/收起正常
|
||||
- [ ] Final CTA 按钮可点击
|
||||
- [ ] 创作模态框正常打开/关闭
|
||||
- [ ] 故事生成功能正常(保留原有逻辑)
|
||||
|
||||
### 7.2 样式验收
|
||||
- [ ] 所有文案使用 i18n,中英文切换正常
|
||||
- [ ] 响应式布局在 320px ~ 1920px 宽度下正常
|
||||
- [ ] 暗色模式下所有元素可读
|
||||
- [ ] 滚动动画流畅,无卡顿
|
||||
- [ ] 所有图标正确显示
|
||||
|
||||
### 7.3 性能验收
|
||||
- [ ] 首屏加载时间 < 3s
|
||||
- [ ] Lighthouse Performance 分数 > 80
|
||||
- [ ] 无控制台错误
|
||||
|
||||
---
|
||||
|
||||
## 8. 文件变更清单
|
||||
|
||||
| 文件 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `frontend/src/views/Home.vue` | 重写 | 完整重构落地页 |
|
||||
| `frontend/src/locales/zh.json` | 已更新 | 新增落地页文案 |
|
||||
| `frontend/src/locales/en.json` | 已更新 | 新增落地页文案 |
|
||||
| `frontend/src/style.css` | 修改 | 新增动画和样式类 |
|
||||
| `frontend/src/composables/useScrollAnimation.ts` | 新建 | 滚动动画 composable |
|
||||
|
||||
---
|
||||
|
||||
## 9. 依赖说明
|
||||
|
||||
### 9.1 现有依赖(无需新增)
|
||||
- Vue 3
|
||||
- vue-router
|
||||
- vue-i18n
|
||||
- Pinia
|
||||
- Tailwind CSS
|
||||
- @heroicons/vue
|
||||
|
||||
### 9.2 需要使用的图标
|
||||
```typescript
|
||||
import {
|
||||
SparklesIcon,
|
||||
UserIcon,
|
||||
PhotoIcon,
|
||||
SpeakerWaveIcon,
|
||||
AcademicCapIcon,
|
||||
GlobeAltIcon,
|
||||
LightBulbIcon,
|
||||
CpuChipIcon,
|
||||
PaintBrushIcon,
|
||||
ShareIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
XMarkIcon,
|
||||
ArrowRightIcon,
|
||||
} from '@heroicons/vue/24/outline'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 实现顺序建议
|
||||
|
||||
1. **Phase 1**: 基础结构
|
||||
- 创建页面骨架(8 个 section)
|
||||
- 实现 Hero 区(不含动画)
|
||||
- 实现创作模态框
|
||||
|
||||
2. **Phase 2**: 内容区块
|
||||
- Trust Bar + 计数动画
|
||||
- Features 卡片
|
||||
- How It Works 步骤
|
||||
|
||||
3. **Phase 3**: 展示区块
|
||||
- Product Showcase
|
||||
- Testimonials
|
||||
- FAQ 手风琴
|
||||
|
||||
4. **Phase 4**: 收尾
|
||||
- Final CTA
|
||||
- 滚动动画
|
||||
- 响应式调整
|
||||
- 暗色模式适配
|
||||
|
||||
---
|
||||
|
||||
*文档版本: 1.0*
|
||||
*创建时间: 2025-12-30*
|
||||
*作者: Claude Code*
|
||||
@@ -1,230 +0,0 @@
|
||||
# DreamWeaver 高保真页面布局与组件规格(Web)
|
||||
|
||||
## 范围
|
||||
|
||||
本文将每个页面映射为高保真布局规范:结构、核心组件与状态,便于在 Figma 中快速搭建。
|
||||
|
||||
---
|
||||
|
||||
## 全局布局
|
||||
|
||||
- 画布:1440 x 900
|
||||
- 内容容器:1200px 居中
|
||||
- 栅格:12 列,24px 间距
|
||||
- 基础间距:8pt
|
||||
|
||||
---
|
||||
|
||||
## 全局组件
|
||||
|
||||
**顶部导航**
|
||||
- 左:Logo + 产品名
|
||||
- 中:主导航
|
||||
- 右:搜索、孩子切换器、头像菜单
|
||||
|
||||
**主 CTA**
|
||||
- 主色实心按钮
|
||||
|
||||
**卡片**
|
||||
- 21:9 封面
|
||||
- 标题、标签、元信息、操作
|
||||
|
||||
**表单控件**
|
||||
- 文本输入、选择器、日期、滑块、标签
|
||||
|
||||
**状态**
|
||||
- 空态、加载、错误、离线
|
||||
|
||||
---
|
||||
|
||||
## 1) 登录 / 授权
|
||||
|
||||
**结构**
|
||||
- 渐变背景
|
||||
- 居中卡片(420px 宽)
|
||||
|
||||
**组件**
|
||||
- Logo 组合
|
||||
- 标题 + 副标题
|
||||
- OAuth 按钮(GitHub、Google)
|
||||
- 隐私说明
|
||||
|
||||
**状态**
|
||||
- Loading(按钮 spinner)
|
||||
- Error(行内错误)
|
||||
|
||||
---
|
||||
|
||||
## 2) 首页:生成故事
|
||||
|
||||
**结构**
|
||||
- 双栏布局(左表单、右预览)
|
||||
- 顶部步骤条
|
||||
|
||||
**左侧表单**
|
||||
- 孩子选择器(下拉 + 头像)
|
||||
- 宇宙选择器(延续 / 新建)
|
||||
- 关键词标签输入
|
||||
- 成长主题选择
|
||||
- 长度选择(分段按钮)
|
||||
- 生成按钮
|
||||
|
||||
**右侧预览**
|
||||
- 封面占位
|
||||
- 标题占位
|
||||
- 摘要预览
|
||||
- 进度指示(文本 -> 封面 -> 语音)
|
||||
|
||||
**状态**
|
||||
- 空预览
|
||||
- 生成中(进度)
|
||||
- 封面失败(重试)
|
||||
|
||||
---
|
||||
|
||||
## 3) 我的故事(列表)
|
||||
|
||||
**结构**
|
||||
- 工具条 + 网格列表
|
||||
|
||||
**工具条**
|
||||
- 搜索
|
||||
- 筛选(孩子、标签)
|
||||
- 排序(最新、最早)
|
||||
- 视图切换(网格/列表)
|
||||
|
||||
**网格卡片**
|
||||
- 桌面端 3 列
|
||||
- Hover 操作:阅读、重生成封面、删除
|
||||
|
||||
**状态**
|
||||
- 空列表 + CTA
|
||||
- 骨架屏
|
||||
|
||||
---
|
||||
|
||||
## 4) 故事详情
|
||||
|
||||
**头图**
|
||||
- 16:9 封面
|
||||
- 标题 + 元信息(孩子、宇宙、标签)
|
||||
- 操作按钮:重生成封面、生成语音、分享
|
||||
|
||||
**正文**
|
||||
- 正文阅读区
|
||||
- 成就面板
|
||||
|
||||
**音频**
|
||||
- 底部吸附迷你播放器
|
||||
|
||||
**状态**
|
||||
- 封面失败
|
||||
- 语音未生成
|
||||
- 语音加载中
|
||||
|
||||
---
|
||||
|
||||
## 5) 孩子档案
|
||||
|
||||
**列表视图**
|
||||
- 头像卡片网格
|
||||
- CTA:添加档案
|
||||
|
||||
**详情视图**
|
||||
- 头像头部 + 编辑按钮
|
||||
- Tabs:基础信息 / 兴趣与成长 / 故事宇宙 / 阅读记录
|
||||
|
||||
**编辑弹窗**
|
||||
- 姓名、生日、性别
|
||||
- 兴趣标签
|
||||
- 成长主题
|
||||
|
||||
---
|
||||
|
||||
## 6) 故事宇宙
|
||||
|
||||
**列表视图**
|
||||
- 宇宙卡片 + 摘要
|
||||
- CTA:新建宇宙
|
||||
|
||||
**详情视图**
|
||||
- 摘要区
|
||||
- 分区:主角、角色、世界观、成就
|
||||
|
||||
**创建/编辑**
|
||||
- 结构化表单 + 示例提示
|
||||
|
||||
---
|
||||
|
||||
## 7) 推送设置
|
||||
|
||||
**结构**
|
||||
- 卡片式设置
|
||||
|
||||
**组件**
|
||||
- 主开关
|
||||
- 时间选择 + 周期
|
||||
- 触发开关
|
||||
- 免打扰时段
|
||||
- 文案预览
|
||||
- 测试推送按钮
|
||||
|
||||
---
|
||||
|
||||
## 8) 账户设置
|
||||
|
||||
**组件**
|
||||
- 个人信息
|
||||
- OAuth 连接
|
||||
- 数据导出/删除
|
||||
- 语言(预留)
|
||||
|
||||
---
|
||||
|
||||
## 9) 管理后台:Providers
|
||||
|
||||
**结构**
|
||||
- 表格布局
|
||||
|
||||
**表格列**
|
||||
- Provider 名称、类型、状态、延迟、最近检查
|
||||
|
||||
**操作**
|
||||
- 编辑、禁用、重载
|
||||
- JSON 配置编辑器(弹窗)
|
||||
|
||||
---
|
||||
|
||||
## 10) 404 / 错误 / 空态
|
||||
|
||||
**布局**
|
||||
- 居中插画 + CTA
|
||||
|
||||
---
|
||||
|
||||
## 交互规范
|
||||
|
||||
- 按钮 Hover:轻微放大 1.02
|
||||
- 卡片 Hover:抬升阴影
|
||||
- Toast:右上角,自动消失
|
||||
- 列表使用 Skeleton
|
||||
|
||||
---
|
||||
|
||||
## 响应式规则(移动端阶段)
|
||||
|
||||
- 顶部导航 -> 底部 Tab
|
||||
- 双栏 -> 单栏
|
||||
- 详情页操作 -> 底部吸附按钮
|
||||
|
||||
---
|
||||
|
||||
## Figma 搭建清单
|
||||
|
||||
- 新建 Page:Design System
|
||||
- 新建 Page:Web Screens
|
||||
- 建立颜色与字体样式
|
||||
- 组件做 Variant
|
||||
- 全部使用 Auto Layout
|
||||
- 1440/1200/1024/768 建立栅格
|
||||
- 状态页复制并标注
|
||||
@@ -1,299 +0,0 @@
|
||||
# DreamWeaver Web 高保真原型规范 (v1)
|
||||
|
||||
## 范围与目标
|
||||
|
||||
- 目标:为 DreamWeaver 提供专业、Web 优先的高保真 UI/UX,既温暖有想象力,又让家长感到可信与高品质。
|
||||
- 受众:3-8 岁儿童的家长。
|
||||
- 阶段重点:Web 端(桌面/平板),同时制定响应式规则,方便后续移动端迁移。
|
||||
- 假设:界面语言为简体中文;管理端或系统字段可能含英文。
|
||||
|
||||
---
|
||||
|
||||
## 设计方向
|
||||
|
||||
- 氛围:温柔、治愈、有想象力,但保持简洁与高级感(避免过度幼儿化)。
|
||||
- 视觉风格:柔和渐变、圆润形状、插画风封面、轻量阴影、舒适中性色。
|
||||
- UX 原则:低阻力、流程清晰、反馈及时、错误可恢复、AI 失败时有明确兜底。
|
||||
|
||||
---
|
||||
|
||||
## 设计系统(Web)
|
||||
|
||||
### 栅格与布局
|
||||
|
||||
- 基准:8pt 间距系统。
|
||||
- 容器:1200px 最大宽度,左右 24px 边距,12 列栅格。
|
||||
- 断点:
|
||||
- 1440+(宽屏)
|
||||
- 1200(标准桌面)
|
||||
- 1024(横屏平板)
|
||||
- 768(竖屏平板)
|
||||
|
||||
### 色板
|
||||
|
||||
- 主色 600: #6C5CE7
|
||||
- 主色 500: #7C69FF
|
||||
- 主色 100: #EAE7FF
|
||||
- 强调粉: #FF8FB1
|
||||
- 强调蓝: #65C3FF
|
||||
- 成功: #34C759
|
||||
- 警告: #F6A609
|
||||
- 错误: #FF5A5F
|
||||
|
||||
- 中性 900: #1F2430
|
||||
- 中性 700: #4B5563
|
||||
- 中性 500: #9AA3B2
|
||||
- 中性 200: #E5E7EB
|
||||
- 中性 100: #F5F7FB
|
||||
- 白色: #FFFFFF
|
||||
|
||||
### 字体
|
||||
|
||||
- 主体字体:PingFang SC, Noto Sans SC, Inter, system-ui
|
||||
- H1:32/40,Semibold
|
||||
- H2:24/32,Semibold
|
||||
- H3:20/28,Semibold
|
||||
- Body L:16/24,Regular
|
||||
- Body M:14/22,Regular
|
||||
- Caption:12/18,Regular
|
||||
|
||||
### 圆角与阴影
|
||||
|
||||
- 圆角:12(卡片)、10(输入框)、8(按钮)、24(胶囊标签)
|
||||
- 阴影 S:0 4 16 rgba(31,36,48,0.08)
|
||||
- 阴影 M:0 10 30 rgba(31,36,48,0.12)
|
||||
|
||||
### 核心组件
|
||||
|
||||
- 顶部导航:Logo、主 CTA、搜索、孩子切换器、头像菜单
|
||||
- 侧边导航(可用于设置/管理):图标 + 文案
|
||||
- 按钮:Primary / Secondary / Ghost / Destructive
|
||||
- 输入:文本、文本域、数字、日期、选择、滑块
|
||||
- 标签:兴趣/成长主题(多选)
|
||||
- 卡片:故事、孩子、宇宙、Provider
|
||||
- 弹窗:创建/编辑表单
|
||||
- Toast:成功/错误/提示
|
||||
- Skeleton:列表与故事内容
|
||||
- 音频播放器:播放/暂停、进度、倍速、下载
|
||||
- 空态:插画 + CTA
|
||||
- 错误态:行内错误 + 重试
|
||||
|
||||
---
|
||||
|
||||
## 信息架构(Web)
|
||||
|
||||
顶级导航:
|
||||
|
||||
- 生成故事(Home)
|
||||
- 我的故事
|
||||
- 孩子档案
|
||||
- 故事宇宙
|
||||
- 推送设置
|
||||
- 账户设置
|
||||
- 管理后台(仅开启时显示)
|
||||
|
||||
---
|
||||
|
||||
## 页面规格(高保真)
|
||||
|
||||
### 1) 登录/授权
|
||||
|
||||
**布局**
|
||||
- 渐变背景 + 居中卡片
|
||||
- Logo、Slogan、OAuth 按钮
|
||||
|
||||
**元素**
|
||||
- 标题:“欢迎来到 DreamWeaver”
|
||||
- 副标题:“为孩子生成独一无二的故事”
|
||||
- 按钮:“使用 GitHub 登录”、“使用 Google 登录”
|
||||
- 隐私说明:“我们仅使用公开信息创建账户”
|
||||
|
||||
**状态**
|
||||
- Loading:按钮 spinner
|
||||
- Error:行内错误 + 重试
|
||||
|
||||
---
|
||||
|
||||
### 2) 生成故事(Home)
|
||||
|
||||
**布局**
|
||||
- 左表单 / 右预览双栏
|
||||
- 顶部步骤条
|
||||
|
||||
**主表单**
|
||||
- 孩子选择器:头像 + 姓名 + 年龄,含“新建档案”入口
|
||||
- 宇宙选择器:默认“延续上一次”,可切“新建宇宙”
|
||||
- 关键词输入(标签 + 手输)
|
||||
- 成长主题选择(可选)
|
||||
- 故事长度(短/中/长)
|
||||
- 主要 CTA:“生成故事”
|
||||
|
||||
**预览面板**
|
||||
- 标题占位
|
||||
- 封面占位
|
||||
- 摘要预览
|
||||
- 错误态:封面失败提示 + 重试
|
||||
|
||||
**交互**
|
||||
- Stepper:档案 → 宇宙 → 关键词 → 生成
|
||||
- 生成过程:阶段进度(文本/封面/语音)
|
||||
|
||||
---
|
||||
|
||||
### 3) 我的故事(列表)
|
||||
|
||||
**布局**
|
||||
- 顶部工具条 + 网格列表
|
||||
|
||||
**卡片**
|
||||
- 21:9 封面、标题、标签、所属孩子、更新时间
|
||||
- Hover 操作:继续阅读、重新生成封面、删除
|
||||
|
||||
**筛选**
|
||||
- 孩子
|
||||
- 标签
|
||||
- 时间范围
|
||||
|
||||
**空态**
|
||||
- 插画 + “开始生成第一个故事”
|
||||
|
||||
---
|
||||
|
||||
### 4) 故事详情
|
||||
|
||||
**头图**
|
||||
- 大封面
|
||||
- 标题 + 元信息(孩子、宇宙、标签、日期)
|
||||
- 主操作:生成封面 / 生成语音 / 分享
|
||||
|
||||
**内容区**
|
||||
- 正文排版(舒适行高)
|
||||
- 成就模块(卡片式)
|
||||
|
||||
**音频**
|
||||
- 滚动时底部吸附播放器
|
||||
- 倍速切换(0.8/1.0/1.2)
|
||||
|
||||
**状态**
|
||||
- 封面失败:占位 + 重试
|
||||
- 语音未生成:显示 CTA
|
||||
|
||||
---
|
||||
|
||||
### 5) 孩子档案
|
||||
|
||||
**列表**
|
||||
- 头像卡片 + 基础信息
|
||||
- CTA:“添加档案”
|
||||
|
||||
**详情**
|
||||
- 档案头部 + 编辑
|
||||
- Tabs:基础信息 / 兴趣与成长 / 故事宇宙 / 阅读记录
|
||||
|
||||
**编辑弹窗**
|
||||
- 姓名、生日、性别
|
||||
- 兴趣标签(多选)
|
||||
- 成长主题(单选或多选)
|
||||
|
||||
---
|
||||
|
||||
### 6) 故事宇宙
|
||||
|
||||
**列表**
|
||||
- 宇宙卡片:主角、常驻角色、成就数量
|
||||
- CTA:“新建宇宙”
|
||||
|
||||
**详情**
|
||||
- 宇宙摘要
|
||||
- 可编辑区:主角 / 角色 / 世界观
|
||||
- 成就时间轴
|
||||
|
||||
**创建/编辑**
|
||||
- 结构化表单 + 示例提示
|
||||
|
||||
---
|
||||
|
||||
### 7) 推送设置
|
||||
|
||||
**布局**
|
||||
- 卡片式设置区
|
||||
|
||||
**设置项**
|
||||
- 主开关:开启主动推送
|
||||
- 时间选择 + 周期
|
||||
- 触发类型(复选)
|
||||
- 免打扰时段
|
||||
|
||||
**预览**
|
||||
- 推送文案预览
|
||||
- 测试推送按钮
|
||||
|
||||
---
|
||||
|
||||
### 8) 账户设置
|
||||
|
||||
- 个人信息
|
||||
- OAuth 绑定
|
||||
- 数据隐私(导出/删除)
|
||||
- 语言切换(预留)
|
||||
|
||||
---
|
||||
|
||||
### 9) 管理后台(Provider)
|
||||
|
||||
**表格**
|
||||
- Provider 列表:状态、延迟、最近检查
|
||||
- 操作:编辑、禁用、重载
|
||||
|
||||
**详情**
|
||||
- JSON 配置编辑器(等宽字体)
|
||||
- 健康检查按钮
|
||||
|
||||
---
|
||||
|
||||
### 10) 404 / 错误 / 空态
|
||||
|
||||
- 友好插画 + 返回 CTA
|
||||
|
||||
---
|
||||
|
||||
## 交互与动效
|
||||
|
||||
- 按钮:Hover 轻微放大(1.02)
|
||||
- 卡片:Hover 提升阴影
|
||||
- Loading:列表 Skeleton、生成进度
|
||||
- Toast:右上角,3s 自动消失
|
||||
|
||||
---
|
||||
|
||||
## 可访问性
|
||||
|
||||
- 文本对比度 >= 4.5:1
|
||||
- 输入框/按钮焦点态清晰
|
||||
- 最小触控区域 44px
|
||||
|
||||
---
|
||||
|
||||
## 响应式策略(移动端阶段)
|
||||
|
||||
- 顶部导航改为底部 Tab
|
||||
- 双栏变单栏
|
||||
- 详情页操作改为底部吸附操作条
|
||||
- 卡片 1 列展示,触控面积更大
|
||||
|
||||
---
|
||||
|
||||
## Figma 实现说明
|
||||
|
||||
- 全部使用 Auto Layout
|
||||
- 统一命名:Page/Section/Component/State
|
||||
- 按钮、输入、卡片做 Variant
|
||||
- 颜色/字体/间距作为样式管理
|
||||
|
||||
---
|
||||
|
||||
## 交付物
|
||||
|
||||
- 设计系统库(色板、文字、组件)
|
||||
- 全流程高保真页面
|
||||
- 原型链接(Figma 中生成)
|
||||
Binary file not shown.
@@ -1,59 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>账户设置</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="grid grid-2 section">
|
||||
<div class="card">
|
||||
<h3>个人信息</h3>
|
||||
<div class="row section">
|
||||
<input class="input" placeholder="昵称" value="Dream Parent" />
|
||||
<input class="input" placeholder="邮箱" value="parent@example.com" />
|
||||
</div>
|
||||
<button class="btn btn--primary">保存</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>账号安全</h3>
|
||||
<div class="callout">已绑定 GitHub、Google</div>
|
||||
<button class="btn btn--secondary" style="margin-top: 12px;">管理绑定</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>数据隐私</h3>
|
||||
<button class="btn btn--secondary">导出数据</button>
|
||||
<button class="btn btn--danger" style="margin-top: 12px;">删除账户</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,74 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>管理后台 - Providers</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="toolbar section">
|
||||
<h2>Providers 管理</h2>
|
||||
<button class="btn btn--primary">新增 Provider</button>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>类型</th>
|
||||
<th>状态</th>
|
||||
<th>延迟</th>
|
||||
<th>最近检查</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>text_primary</td>
|
||||
<td>Text</td>
|
||||
<td><span class="badge">健康</span></td>
|
||||
<td>420ms</td>
|
||||
<td>2 分钟前</td>
|
||||
<td><a href="#">编辑</a> · <a href="#">禁用</a> · <a href="#">重载</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>image_primary</td>
|
||||
<td>Image</td>
|
||||
<td><span class="badge">健康</span></td>
|
||||
<td>860ms</td>
|
||||
<td>5 分钟前</td>
|
||||
<td><a href="#">编辑</a> · <a href="#">禁用</a> · <a href="#">重载</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="footer-note">点击编辑后弹出 JSON 配置编辑器。</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,88 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>孩子档案详情</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="card section" style="display:flex; align-items:center; justify-content: space-between;">
|
||||
<div style="display:flex; gap:12px; align-items:center;">
|
||||
<div class="avatar">明</div>
|
||||
<div>
|
||||
<div class="card-title">小明 · 5岁</div>
|
||||
<div class="card-meta">男 · 生日 2020/05/12</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn--secondary">编辑档案</button>
|
||||
</div>
|
||||
|
||||
<div class="tabs section">
|
||||
<div class="tab active">基础信息</div>
|
||||
<div class="tab">兴趣与成长</div>
|
||||
<div class="tab">故事宇宙</div>
|
||||
<div class="tab">阅读记录</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2 section">
|
||||
<div class="card">
|
||||
<h3>兴趣标签</h3>
|
||||
<div class="chips">
|
||||
<span class="chip selected">太空</span>
|
||||
<span class="chip selected">机器人</span>
|
||||
<span class="chip">冒险</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>成长主题</h3>
|
||||
<div class="chips">
|
||||
<span class="chip selected">勇气</span>
|
||||
<span class="chip">分享</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>故事宇宙</h3>
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">星际冒险</div>
|
||||
<div class="card-meta">主角:小明船长 · 成就 3 个</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">梦幻森林</div>
|
||||
<div class="card-meta">主角:森林守护者 · 成就 1 个</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,58 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>孩子档案</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="toolbar section">
|
||||
<h2>我的宝贝</h2>
|
||||
<button class="btn btn--primary">添加档案</button>
|
||||
</div>
|
||||
<div class="grid grid-3 section">
|
||||
<div class="card">
|
||||
<div class="avatar">明</div>
|
||||
<div class="card-title">小明 · 5岁</div>
|
||||
<div class="chips"><span class="chip">太空</span><span class="chip">机器人</span></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="avatar">红</div>
|
||||
<div class="card-title">小红 · 3岁</div>
|
||||
<div class="chips"><span class="chip">公主</span><span class="chip">动物</span></div>
|
||||
</div>
|
||||
<div class="card card--flat">
|
||||
<div class="callout">空态示例:添加一个孩子档案</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,111 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>生成故事</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="stepper section">
|
||||
<span class="step active">档案</span>
|
||||
<span class="step">宇宙</span>
|
||||
<span class="step">关键词</span>
|
||||
<span class="step">生成</span>
|
||||
</div>
|
||||
<div class="split section">
|
||||
<div class="card">
|
||||
<h3>为谁创作故事</h3>
|
||||
<div class="row section">
|
||||
<div>
|
||||
<label>孩子档案</label>
|
||||
<select>
|
||||
<option>小明 · 5岁</option>
|
||||
<option>小红 · 3岁</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>故事宇宙</label>
|
||||
<select>
|
||||
<option>延续上一次(星际冒险)</option>
|
||||
<option>新建宇宙</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<label>关键词</label>
|
||||
<div class="chips section">
|
||||
<span class="chip selected">太空</span>
|
||||
<span class="chip selected">勇气</span>
|
||||
<span class="chip">机器人</span>
|
||||
<span class="chip">探索</span>
|
||||
</div>
|
||||
<input class="input" placeholder="输入更多关键词" />
|
||||
</div>
|
||||
<div class="row section">
|
||||
<div>
|
||||
<label>成长主题</label>
|
||||
<select>
|
||||
<option>勇气</option>
|
||||
<option>分享</option>
|
||||
<option>独立</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>故事长度</label>
|
||||
<div class="chips">
|
||||
<span class="chip selected">短</span>
|
||||
<span class="chip">中</span>
|
||||
<span class="chip">长</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<button class="btn btn--primary" style="width: 100%;">生成故事</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>生成预览</h3>
|
||||
<div class="card-cover"></div>
|
||||
<div class="card-title">故事标题占位</div>
|
||||
<p class="card-meta">故事摘要将显示在这里,支持 2-3 行预览。</p>
|
||||
<div class="section">
|
||||
<div class="callout">生成中:文本 → 封面 → 语音</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="callout" style="border-color: var(--error); color: var(--error);">封面生成失败,稍后重试</div>
|
||||
<button class="btn btn--secondary" style="margin-top: 8px;">重新生成封面</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,33 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>DreamWeaver 原型入口</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="container" style="padding: 48px 0;">
|
||||
<div class="hero">
|
||||
<h1>DreamWeaver HTML 原型入口</h1>
|
||||
<p>请选择页面进行导入或预览(HTML to Figma)。</p>
|
||||
<div class="grid grid-3 section">
|
||||
<div class="card"><a href="login.html">登录 / 授权</a></div>
|
||||
<div class="card"><a href="home.html">生成故事(Home)</a></div>
|
||||
<div class="card"><a href="my-stories.html">我的故事(列表)</a></div>
|
||||
<div class="card"><a href="story-detail.html">故事详情</a></div>
|
||||
<div class="card"><a href="child-profiles.html">孩子档案(列表)</a></div>
|
||||
<div class="card"><a href="child-profile-detail.html">孩子档案(详情)</a></div>
|
||||
<div class="card"><a href="universes.html">故事宇宙(列表)</a></div>
|
||||
<div class="card"><a href="universe-detail.html">故事宇宙(详情)</a></div>
|
||||
<div class="card"><a href="push-settings.html">推送设置</a></div>
|
||||
<div class="card"><a href="account-settings.html">账户设置</a></div>
|
||||
<div class="card"><a href="admin-providers.html">管理后台(Providers)</a></div>
|
||||
<div class="card"><a href="not-found.html">404 / 错误</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,28 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>登录 / 授权</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="container" style="padding: 80px 0;">
|
||||
<div class="hero" style="max-width: 420px; margin: 0 auto; text-align: center;">
|
||||
<div class="nav__logo" style="justify-content: center;">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<h2 style="margin-top: 16px;">欢迎来到 DreamWeaver</h2>
|
||||
<p>为孩子生成独一无二的故事</p>
|
||||
<div class="section" style="display: grid; gap: 12px;">
|
||||
<button class="btn btn--primary">使用 GitHub 登录</button>
|
||||
<button class="btn btn--secondary">使用 Google 登录</button>
|
||||
</div>
|
||||
<div class="footer-note">我们仅使用公开信息创建账户</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,84 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>我的故事</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="toolbar section">
|
||||
<input class="input" style="width: 260px;" placeholder="搜索标题或关键词" />
|
||||
<select style="width: 160px;">
|
||||
<option>孩子:全部</option>
|
||||
<option>小明</option>
|
||||
<option>小红</option>
|
||||
</select>
|
||||
<select style="width: 160px;">
|
||||
<option>排序:最新</option>
|
||||
<option>最早</option>
|
||||
</select>
|
||||
<button class="btn btn--ghost">网格</button>
|
||||
<button class="btn btn--ghost">列表</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-3 section">
|
||||
<div class="card">
|
||||
<div class="card-cover"></div>
|
||||
<div class="card-title">星际冒险 · 第三章</div>
|
||||
<div class="chips">
|
||||
<span class="chip">太空</span><span class="chip">勇气</span>
|
||||
</div>
|
||||
<div class="card-meta">小明 · 更新于 2 天前</div>
|
||||
<div class="section hero-actions">
|
||||
<button class="btn btn--primary">继续阅读</button>
|
||||
<button class="btn btn--secondary">重生成封面</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-cover"></div>
|
||||
<div class="card-title">梦幻森林 · 朋友篇</div>
|
||||
<div class="chips">
|
||||
<span class="chip">友谊</span><span class="chip">动物</span>
|
||||
</div>
|
||||
<div class="card-meta">小红 · 更新于 5 天前</div>
|
||||
<div class="section hero-actions">
|
||||
<button class="btn btn--primary">继续阅读</button>
|
||||
<button class="btn btn--danger">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card card--flat">
|
||||
<div class="callout">空态示例:开始生成第一个故事</div>
|
||||
<button class="btn btn--primary" style="margin-top: 12px;">生成故事</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,20 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>404</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="container" style="padding: 80px 0; text-align:center;">
|
||||
<div class="hero">
|
||||
<h1>404</h1>
|
||||
<p>页面走丢了,回到生成故事开始吧。</p>
|
||||
<button class="btn btn--primary">返回首页</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,78 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>推送设置</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="grid grid-2 section">
|
||||
<div class="card">
|
||||
<h3>主动推送</h3>
|
||||
<div class="row section">
|
||||
<div>
|
||||
<label>主开关</label>
|
||||
<select>
|
||||
<option>开启</option>
|
||||
<option>关闭</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>推送时间</label>
|
||||
<input class="input" placeholder="20:00" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<label>触发类型</label>
|
||||
<div class="chips">
|
||||
<span class="chip selected">时间触发</span>
|
||||
<span class="chip selected">事件触发</span>
|
||||
<span class="chip">行为触发</span>
|
||||
<span class="chip">成长触发</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<label>免打扰</label>
|
||||
<div class="row">
|
||||
<input class="input" placeholder="21:00" />
|
||||
<input class="input" placeholder="09:00" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>推送预览</h3>
|
||||
<div class="callout">“今晚给小明讲一个关于太空的故事,好吗?”</div>
|
||||
<button class="btn btn--secondary" style="margin-top: 12px;">发送测试推送</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,73 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>故事详情</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="section">
|
||||
<div class="cover-hero"></div>
|
||||
<h2 style="margin-top: 16px;">星际冒险 · 勇气的种子</h2>
|
||||
<div class="card-meta">小明 · 星际冒险宇宙 · 2025/01/12</div>
|
||||
<div class="hero-actions section">
|
||||
<button class="btn btn--secondary">重新生成封面</button>
|
||||
<button class="btn btn--primary">生成语音</button>
|
||||
<button class="btn btn--ghost">分享</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="split section">
|
||||
<div class="card">
|
||||
<h3>故事正文</h3>
|
||||
<p>夜空像一条温柔的河流,小明驾驶着飞船穿过星光……</p>
|
||||
<p>他握紧操纵杆,鼓起勇气,向未知的星球靠近。</p>
|
||||
<p>最终,小明发现了新的朋友,也学会了如何面对黑暗。</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>成就</h3>
|
||||
<div class="chips section">
|
||||
<span class="chip selected">勇气</span>
|
||||
<span class="chip selected">友谊</span>
|
||||
</div>
|
||||
<div class="callout section">“克服了黑暗的恐惧”</div>
|
||||
<div class="callout">“帮助了迷路的小伙伴”</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section audio-player">
|
||||
<button class="btn btn--ghost">播放</button>
|
||||
<div class="audio-bar"><div class="audio-progress"></div></div>
|
||||
<button class="btn btn--ghost">1.0x</button>
|
||||
</div>
|
||||
<div class="footer-note">语音未生成时,显示“生成语音”按钮作为主操作。</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,217 +0,0 @@
|
||||
:root {
|
||||
--primary-600: #6C5CE7;
|
||||
--primary-500: #7C69FF;
|
||||
--primary-100: #EAE7FF;
|
||||
--accent-pink: #FF8FB1;
|
||||
--accent-sky: #65C3FF;
|
||||
--success: #34C759;
|
||||
--warning: #F6A609;
|
||||
--error: #FF5A5F;
|
||||
--neutral-900: #1F2430;
|
||||
--neutral-700: #4B5563;
|
||||
--neutral-500: #9AA3B2;
|
||||
--neutral-200: #E5E7EB;
|
||||
--neutral-100: #F5F7FB;
|
||||
--hero-gradient: linear-gradient(135deg, #EAE7FF 0%, #FDF6FF 40%, #EAF6FF 100%);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
:root {
|
||||
--container-width: 1200px;
|
||||
--gutter: 24px;
|
||||
--radius-card: 12px;
|
||||
--radius-input: 10px;
|
||||
--radius-button: 8px;
|
||||
--radius-pill: 24px;
|
||||
--shadow-s: 0 4px 16px rgba(31,36,48,0.08);
|
||||
--shadow-m: 0 10px 30px rgba(31,36,48,0.12);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC", "Noto Sans SC", Inter, system-ui, -apple-system, sans-serif;
|
||||
color: var(--neutral-900);
|
||||
background: var(--neutral-100);
|
||||
}
|
||||
|
||||
a { color: var(--primary-600); text-decoration: none; }
|
||||
|
||||
.page { min-height: 100vh; }
|
||||
.container {
|
||||
width: min(var(--container-width), 100% - 48px);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.nav {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--neutral-200);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.nav__inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 0;
|
||||
gap: 16px;
|
||||
}
|
||||
.nav__left, .nav__center, .nav__right { display: flex; align-items: center; gap: 16px; }
|
||||
.nav__logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--neutral-900);
|
||||
}
|
||||
.nav__logo-badge {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, var(--primary-500), var(--accent-sky));
|
||||
}
|
||||
.nav__item { color: var(--neutral-700); font-weight: 500; }
|
||||
.nav__item.active { color: var(--primary-600); }
|
||||
|
||||
.hero {
|
||||
background: var(--hero-gradient);
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
box-shadow: var(--shadow-s);
|
||||
}
|
||||
|
||||
.section { margin: 28px 0; }
|
||||
.section-title { font-size: 20px; font-weight: 600; margin-bottom: 12px; }
|
||||
|
||||
.grid { display: grid; gap: 16px; }
|
||||
.grid-2 { grid-template-columns: repeat(2, minmax(0,1fr)); }
|
||||
.grid-3 { grid-template-columns: repeat(3, minmax(0,1fr)); }
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: var(--radius-card);
|
||||
padding: 16px;
|
||||
box-shadow: var(--shadow-s);
|
||||
}
|
||||
.card--flat { box-shadow: none; border: 1px solid var(--neutral-200); }
|
||||
.card-cover {
|
||||
width: 100%;
|
||||
aspect-ratio: 21 / 9;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, var(--primary-100), #fff 60%, var(--accent-sky));
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.card-title { font-weight: 600; margin: 6px 0; }
|
||||
.card-meta { color: var(--neutral-500); font-size: 12px; }
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--primary-100);
|
||||
color: var(--primary-600);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.chip {
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius-pill);
|
||||
border: 1px solid var(--neutral-200);
|
||||
background: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
.chip.selected { background: var(--primary-100); border-color: var(--primary-500); color: var(--primary-600); }
|
||||
|
||||
.btn {
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
border-radius: var(--radius-button);
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn--primary { background: var(--primary-600); color: #fff; }
|
||||
.btn--secondary { background: #fff; border-color: var(--primary-600); color: var(--primary-600); }
|
||||
.btn--ghost { background: transparent; color: var(--neutral-700); }
|
||||
.btn--danger { background: #fff; border-color: var(--error); color: var(--error); }
|
||||
|
||||
.input, select, textarea {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-input);
|
||||
border: 1px solid var(--neutral-200);
|
||||
background: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.row { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px; }
|
||||
|
||||
.stepper { display: flex; gap: 10px; align-items: center; }
|
||||
.step {
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--neutral-100);
|
||||
color: var(--neutral-700);
|
||||
font-size: 12px;
|
||||
}
|
||||
.step.active { background: var(--primary-100); color: var(--primary-600); }
|
||||
|
||||
.toolbar { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
||||
|
||||
.table { width: 100%; border-collapse: collapse; }
|
||||
.table th, .table td { border-bottom: 1px solid var(--neutral-200); padding: 12px 8px; text-align: left; font-size: 14px; }
|
||||
|
||||
.avatar {
|
||||
width: 40px; height: 40px; border-radius: 50%;
|
||||
background: var(--primary-100);
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
font-weight: 700; color: var(--primary-600);
|
||||
}
|
||||
|
||||
.cover-hero {
|
||||
aspect-ratio: 16 / 9;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, var(--primary-100), #fff 55%, var(--accent-pink));
|
||||
}
|
||||
|
||||
.audio-player {
|
||||
display: flex; align-items: center; gap: 12px; padding: 12px 16px;
|
||||
border-radius: 12px; border: 1px solid var(--neutral-200); background: #fff;
|
||||
}
|
||||
.audio-bar { height: 6px; background: var(--neutral-200); border-radius: 999px; flex: 1; }
|
||||
.audio-progress { width: 35%; height: 100%; background: var(--primary-600); border-radius: 999px; }
|
||||
|
||||
.tabs { display: flex; gap: 8px; border-bottom: 1px solid var(--neutral-200); }
|
||||
.tab { padding: 10px 12px; color: var(--neutral-700); }
|
||||
.tab.active { color: var(--primary-600); border-bottom: 2px solid var(--primary-600); }
|
||||
|
||||
.callout {
|
||||
background: #fff;
|
||||
border: 1px dashed var(--neutral-200);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
color: var(--neutral-700);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.footer-note { color: var(--neutral-500); font-size: 12px; margin-top: 12px; }
|
||||
|
||||
.hero-actions { display: flex; gap: 12px; flex-wrap: wrap; }
|
||||
|
||||
.split {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.grid-3 { grid-template-columns: repeat(2, minmax(0,1fr)); }
|
||||
.split { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.grid-2 { grid-template-columns: 1fr; }
|
||||
.grid-3 { grid-template-columns: 1fr; }
|
||||
.row { grid-template-columns: 1fr; }
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>宇宙详情</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="card section">
|
||||
<h2>星际冒险</h2>
|
||||
<div class="card-meta">主角:小明船长 · 更新于 2025/01/12</div>
|
||||
</div>
|
||||
<div class="grid grid-2 section">
|
||||
<div class="card">
|
||||
<h3>主角设定</h3>
|
||||
<div class="callout">小明是来自地球的探险家,勇敢且好奇。</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>常驻角色</h3>
|
||||
<div class="callout">机器人小七、外星猫咪星星</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>世界观</h3>
|
||||
<div class="callout">星际学院、彩虹星云、飞船港湾</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>成就</h3>
|
||||
<div class="callout">克服恐惧 · 结交朋友 · 学会独立</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<button class="btn btn--secondary">编辑宇宙</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,64 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>故事宇宙</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="toolbar section">
|
||||
<h2>故事宇宙</h2>
|
||||
<button class="btn btn--primary">新建宇宙</button>
|
||||
</div>
|
||||
<div class="grid grid-3 section">
|
||||
<div class="card">
|
||||
<div class="card-title">星际冒险</div>
|
||||
<div class="card-meta">主角:小明船长</div>
|
||||
<div class="chips section">
|
||||
<span class="chip">伙伴:机器人小七</span>
|
||||
<span class="chip">成就:3</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">梦幻森林</div>
|
||||
<div class="card-meta">主角:森林守护者</div>
|
||||
<div class="chips section">
|
||||
<span class="chip">伙伴:魔法猫咪</span>
|
||||
<span class="chip">成就:1</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card card--flat">
|
||||
<div class="callout">空态示例:创建第一个宇宙</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,59 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>账户设置</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="grid grid-2 section">
|
||||
<div class="card">
|
||||
<h3>个人信息</h3>
|
||||
<div class="row section">
|
||||
<input class="input" placeholder="昵称" value="Dream Parent" />
|
||||
<input class="input" placeholder="邮箱" value="parent@example.com" />
|
||||
</div>
|
||||
<button class="btn btn--primary">保存</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>账号安全</h3>
|
||||
<div class="callout">已绑定 GitHub、Google</div>
|
||||
<button class="btn btn--secondary" style="margin-top: 12px;">管理绑定</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>数据隐私</h3>
|
||||
<button class="btn btn--secondary">导出数据</button>
|
||||
<button class="btn btn--danger" style="margin-top: 12px;">删除账户</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,74 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>管理后台 - Providers</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="toolbar section">
|
||||
<h2>Providers 管理</h2>
|
||||
<button class="btn btn--primary">新增 Provider</button>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>类型</th>
|
||||
<th>状态</th>
|
||||
<th>延迟</th>
|
||||
<th>最近检查</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>text_primary</td>
|
||||
<td>Text</td>
|
||||
<td><span class="badge">健康</span></td>
|
||||
<td>420ms</td>
|
||||
<td>2 分钟前</td>
|
||||
<td><a href="#">编辑</a> · <a href="#">禁用</a> · <a href="#">重载</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>image_primary</td>
|
||||
<td>Image</td>
|
||||
<td><span class="badge">健康</span></td>
|
||||
<td>860ms</td>
|
||||
<td>5 分钟前</td>
|
||||
<td><a href="#">编辑</a> · <a href="#">禁用</a> · <a href="#">重载</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="footer-note">点击编辑后弹出 JSON 配置编辑器。</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,88 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>孩子档案详情</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="card section" style="display:flex; align-items:center; justify-content: space-between;">
|
||||
<div style="display:flex; gap:12px; align-items:center;">
|
||||
<div class="avatar">明</div>
|
||||
<div>
|
||||
<div class="card-title">小明 · 5岁</div>
|
||||
<div class="card-meta">男 · 生日 2020/05/12</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn--secondary">编辑档案</button>
|
||||
</div>
|
||||
|
||||
<div class="tabs section">
|
||||
<div class="tab active">基础信息</div>
|
||||
<div class="tab">兴趣与成长</div>
|
||||
<div class="tab">故事宇宙</div>
|
||||
<div class="tab">阅读记录</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2 section">
|
||||
<div class="card">
|
||||
<h3>兴趣标签</h3>
|
||||
<div class="chips">
|
||||
<span class="chip selected">太空</span>
|
||||
<span class="chip selected">机器人</span>
|
||||
<span class="chip">冒险</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>成长主题</h3>
|
||||
<div class="chips">
|
||||
<span class="chip selected">勇气</span>
|
||||
<span class="chip">分享</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>故事宇宙</h3>
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">星际冒险</div>
|
||||
<div class="card-meta">主角:小明船长 · 成就 3 个</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">梦幻森林</div>
|
||||
<div class="card-meta">主角:森林守护者 · 成就 1 个</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,58 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>孩子档案</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="toolbar section">
|
||||
<h2>我的宝贝</h2>
|
||||
<button class="btn btn--primary">添加档案</button>
|
||||
</div>
|
||||
<div class="grid grid-3 section">
|
||||
<div class="card">
|
||||
<div class="avatar">明</div>
|
||||
<div class="card-title">小明 · 5岁</div>
|
||||
<div class="chips"><span class="chip">太空</span><span class="chip">机器人</span></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="avatar">红</div>
|
||||
<div class="card-title">小红 · 3岁</div>
|
||||
<div class="chips"><span class="chip">公主</span><span class="chip">动物</span></div>
|
||||
</div>
|
||||
<div class="card card--flat">
|
||||
<div class="callout">空态示例:添加一个孩子档案</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,111 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>生成故事</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="stepper section">
|
||||
<span class="step active">档案</span>
|
||||
<span class="step">宇宙</span>
|
||||
<span class="step">关键词</span>
|
||||
<span class="step">生成</span>
|
||||
</div>
|
||||
<div class="split section">
|
||||
<div class="card">
|
||||
<h3>为谁创作故事</h3>
|
||||
<div class="row section">
|
||||
<div>
|
||||
<label>孩子档案</label>
|
||||
<select>
|
||||
<option>小明 · 5岁</option>
|
||||
<option>小红 · 3岁</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>故事宇宙</label>
|
||||
<select>
|
||||
<option>延续上一次(星际冒险)</option>
|
||||
<option>新建宇宙</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<label>关键词</label>
|
||||
<div class="chips section">
|
||||
<span class="chip selected">太空</span>
|
||||
<span class="chip selected">勇气</span>
|
||||
<span class="chip">机器人</span>
|
||||
<span class="chip">探索</span>
|
||||
</div>
|
||||
<input class="input" placeholder="输入更多关键词" />
|
||||
</div>
|
||||
<div class="row section">
|
||||
<div>
|
||||
<label>成长主题</label>
|
||||
<select>
|
||||
<option>勇气</option>
|
||||
<option>分享</option>
|
||||
<option>独立</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>故事长度</label>
|
||||
<div class="chips">
|
||||
<span class="chip selected">短</span>
|
||||
<span class="chip">中</span>
|
||||
<span class="chip">长</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<button class="btn btn--primary" style="width: 100%;">生成故事</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>生成预览</h3>
|
||||
<div class="card-cover"></div>
|
||||
<div class="card-title">故事标题占位</div>
|
||||
<p class="card-meta">故事摘要将显示在这里,支持 2-3 行预览。</p>
|
||||
<div class="section">
|
||||
<div class="callout">生成中:文本 → 封面 → 语音</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="callout" style="border-color: var(--error); color: var(--error);">封面生成失败,稍后重试</div>
|
||||
<button class="btn btn--secondary" style="margin-top: 8px;">重新生成封面</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,33 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>DreamWeaver 原型入口</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="container" style="padding: 48px 0;">
|
||||
<div class="hero">
|
||||
<h1>DreamWeaver HTML 原型入口</h1>
|
||||
<p>请选择页面进行导入或预览(HTML to Figma)。</p>
|
||||
<div class="grid grid-3 section">
|
||||
<div class="card"><a href="login.html">登录 / 授权</a></div>
|
||||
<div class="card"><a href="home.html">生成故事(Home)</a></div>
|
||||
<div class="card"><a href="my-stories.html">我的故事(列表)</a></div>
|
||||
<div class="card"><a href="story-detail.html">故事详情</a></div>
|
||||
<div class="card"><a href="child-profiles.html">孩子档案(列表)</a></div>
|
||||
<div class="card"><a href="child-profile-detail.html">孩子档案(详情)</a></div>
|
||||
<div class="card"><a href="universes.html">故事宇宙(列表)</a></div>
|
||||
<div class="card"><a href="universe-detail.html">故事宇宙(详情)</a></div>
|
||||
<div class="card"><a href="push-settings.html">推送设置</a></div>
|
||||
<div class="card"><a href="account-settings.html">账户设置</a></div>
|
||||
<div class="card"><a href="admin-providers.html">管理后台(Providers)</a></div>
|
||||
<div class="card"><a href="not-found.html">404 / 错误</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,28 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>登录 / 授权</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="container" style="padding: 80px 0;">
|
||||
<div class="hero" style="max-width: 420px; margin: 0 auto; text-align: center;">
|
||||
<div class="nav__logo" style="justify-content: center;">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<h2 style="margin-top: 16px;">欢迎来到 DreamWeaver</h2>
|
||||
<p>为孩子生成独一无二的故事</p>
|
||||
<div class="section" style="display: grid; gap: 12px;">
|
||||
<button class="btn btn--primary">使用 GitHub 登录</button>
|
||||
<button class="btn btn--secondary">使用 Google 登录</button>
|
||||
</div>
|
||||
<div class="footer-note">我们仅使用公开信息创建账户</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,84 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>我的故事</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="toolbar section">
|
||||
<input class="input" style="width: 260px;" placeholder="搜索标题或关键词" />
|
||||
<select style="width: 160px;">
|
||||
<option>孩子:全部</option>
|
||||
<option>小明</option>
|
||||
<option>小红</option>
|
||||
</select>
|
||||
<select style="width: 160px;">
|
||||
<option>排序:最新</option>
|
||||
<option>最早</option>
|
||||
</select>
|
||||
<button class="btn btn--ghost">网格</button>
|
||||
<button class="btn btn--ghost">列表</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-3 section">
|
||||
<div class="card">
|
||||
<div class="card-cover"></div>
|
||||
<div class="card-title">星际冒险 · 第三章</div>
|
||||
<div class="chips">
|
||||
<span class="chip">太空</span><span class="chip">勇气</span>
|
||||
</div>
|
||||
<div class="card-meta">小明 · 更新于 2 天前</div>
|
||||
<div class="section hero-actions">
|
||||
<button class="btn btn--primary">继续阅读</button>
|
||||
<button class="btn btn--secondary">重生成封面</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-cover"></div>
|
||||
<div class="card-title">梦幻森林 · 朋友篇</div>
|
||||
<div class="chips">
|
||||
<span class="chip">友谊</span><span class="chip">动物</span>
|
||||
</div>
|
||||
<div class="card-meta">小红 · 更新于 5 天前</div>
|
||||
<div class="section hero-actions">
|
||||
<button class="btn btn--primary">继续阅读</button>
|
||||
<button class="btn btn--danger">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card card--flat">
|
||||
<div class="callout">空态示例:开始生成第一个故事</div>
|
||||
<button class="btn btn--primary" style="margin-top: 12px;">生成故事</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,20 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>404</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="container" style="padding: 80px 0; text-align:center;">
|
||||
<div class="hero">
|
||||
<h1>404</h1>
|
||||
<p>页面走丢了,回到生成故事开始吧。</p>
|
||||
<button class="btn btn--primary">返回首页</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,78 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>推送设置</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="grid grid-2 section">
|
||||
<div class="card">
|
||||
<h3>主动推送</h3>
|
||||
<div class="row section">
|
||||
<div>
|
||||
<label>主开关</label>
|
||||
<select>
|
||||
<option>开启</option>
|
||||
<option>关闭</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>推送时间</label>
|
||||
<input class="input" placeholder="20:00" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<label>触发类型</label>
|
||||
<div class="chips">
|
||||
<span class="chip selected">时间触发</span>
|
||||
<span class="chip selected">事件触发</span>
|
||||
<span class="chip">行为触发</span>
|
||||
<span class="chip">成长触发</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<label>免打扰</label>
|
||||
<div class="row">
|
||||
<input class="input" placeholder="21:00" />
|
||||
<input class="input" placeholder="09:00" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>推送预览</h3>
|
||||
<div class="callout">“今晚给小明讲一个关于太空的故事,好吗?”</div>
|
||||
<button class="btn btn--secondary" style="margin-top: 12px;">发送测试推送</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,73 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>故事详情</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="section">
|
||||
<div class="cover-hero"></div>
|
||||
<h2 style="margin-top: 16px;">星际冒险 · 勇气的种子</h2>
|
||||
<div class="card-meta">小明 · 星际冒险宇宙 · 2025/01/12</div>
|
||||
<div class="hero-actions section">
|
||||
<button class="btn btn--secondary">重新生成封面</button>
|
||||
<button class="btn btn--primary">生成语音</button>
|
||||
<button class="btn btn--ghost">分享</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="split section">
|
||||
<div class="card">
|
||||
<h3>故事正文</h3>
|
||||
<p>夜空像一条温柔的河流,小明驾驶着飞船穿过星光……</p>
|
||||
<p>他握紧操纵杆,鼓起勇气,向未知的星球靠近。</p>
|
||||
<p>最终,小明发现了新的朋友,也学会了如何面对黑暗。</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>成就</h3>
|
||||
<div class="chips section">
|
||||
<span class="chip selected">勇气</span>
|
||||
<span class="chip selected">友谊</span>
|
||||
</div>
|
||||
<div class="callout section">“克服了黑暗的恐惧”</div>
|
||||
<div class="callout">“帮助了迷路的小伙伴”</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section audio-player">
|
||||
<button class="btn btn--ghost">播放</button>
|
||||
<div class="audio-bar"><div class="audio-progress"></div></div>
|
||||
<button class="btn btn--ghost">1.0x</button>
|
||||
</div>
|
||||
<div class="footer-note">语音未生成时,显示“生成语音”按钮作为主操作。</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,217 +0,0 @@
|
||||
:root {
|
||||
--primary-600: #3B82F6;
|
||||
--primary-500: #60A5FA;
|
||||
--primary-100: #DBEAFE;
|
||||
--accent-pink: #F5C542;
|
||||
--accent-sky: #6EE7B7;
|
||||
--success: #34C759;
|
||||
--warning: #F6A609;
|
||||
--error: #FF5A5F;
|
||||
--neutral-900: #111827;
|
||||
--neutral-700: #374151;
|
||||
--neutral-500: #9CA3AF;
|
||||
--neutral-200: #E5E7EB;
|
||||
--neutral-100: #F9FAFB;
|
||||
--hero-gradient: linear-gradient(180deg, #F9FAFB 0%, #EEF2FF 100%);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
:root {
|
||||
--container-width: 1200px;
|
||||
--gutter: 24px;
|
||||
--radius-card: 12px;
|
||||
--radius-input: 10px;
|
||||
--radius-button: 8px;
|
||||
--radius-pill: 24px;
|
||||
--shadow-s: 0 4px 16px rgba(31,36,48,0.08);
|
||||
--shadow-m: 0 10px 30px rgba(31,36,48,0.12);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC", "Noto Sans SC", Inter, system-ui, -apple-system, sans-serif;
|
||||
color: var(--neutral-900);
|
||||
background: var(--neutral-100);
|
||||
}
|
||||
|
||||
a { color: var(--primary-600); text-decoration: none; }
|
||||
|
||||
.page { min-height: 100vh; }
|
||||
.container {
|
||||
width: min(var(--container-width), 100% - 48px);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.nav {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--neutral-200);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.nav__inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 0;
|
||||
gap: 16px;
|
||||
}
|
||||
.nav__left, .nav__center, .nav__right { display: flex; align-items: center; gap: 16px; }
|
||||
.nav__logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--neutral-900);
|
||||
}
|
||||
.nav__logo-badge {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, var(--primary-500), var(--accent-sky));
|
||||
}
|
||||
.nav__item { color: var(--neutral-700); font-weight: 500; }
|
||||
.nav__item.active { color: var(--primary-600); }
|
||||
|
||||
.hero {
|
||||
background: var(--hero-gradient);
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
box-shadow: var(--shadow-s);
|
||||
}
|
||||
|
||||
.section { margin: 28px 0; }
|
||||
.section-title { font-size: 20px; font-weight: 600; margin-bottom: 12px; }
|
||||
|
||||
.grid { display: grid; gap: 16px; }
|
||||
.grid-2 { grid-template-columns: repeat(2, minmax(0,1fr)); }
|
||||
.grid-3 { grid-template-columns: repeat(3, minmax(0,1fr)); }
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: var(--radius-card);
|
||||
padding: 16px;
|
||||
box-shadow: var(--shadow-s);
|
||||
}
|
||||
.card--flat { box-shadow: none; border: 1px solid var(--neutral-200); }
|
||||
.card-cover {
|
||||
width: 100%;
|
||||
aspect-ratio: 21 / 9;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, var(--primary-100), #fff 60%, var(--accent-sky));
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.card-title { font-weight: 600; margin: 6px 0; }
|
||||
.card-meta { color: var(--neutral-500); font-size: 12px; }
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--primary-100);
|
||||
color: var(--primary-600);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.chip {
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius-pill);
|
||||
border: 1px solid var(--neutral-200);
|
||||
background: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
.chip.selected { background: var(--primary-100); border-color: var(--primary-500); color: var(--primary-600); }
|
||||
|
||||
.btn {
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
border-radius: var(--radius-button);
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn--primary { background: var(--primary-600); color: #fff; }
|
||||
.btn--secondary { background: #fff; border-color: var(--primary-600); color: var(--primary-600); }
|
||||
.btn--ghost { background: transparent; color: var(--neutral-700); }
|
||||
.btn--danger { background: #fff; border-color: var(--error); color: var(--error); }
|
||||
|
||||
.input, select, textarea {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-input);
|
||||
border: 1px solid var(--neutral-200);
|
||||
background: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.row { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px; }
|
||||
|
||||
.stepper { display: flex; gap: 10px; align-items: center; }
|
||||
.step {
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--neutral-100);
|
||||
color: var(--neutral-700);
|
||||
font-size: 12px;
|
||||
}
|
||||
.step.active { background: var(--primary-100); color: var(--primary-600); }
|
||||
|
||||
.toolbar { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
||||
|
||||
.table { width: 100%; border-collapse: collapse; }
|
||||
.table th, .table td { border-bottom: 1px solid var(--neutral-200); padding: 12px 8px; text-align: left; font-size: 14px; }
|
||||
|
||||
.avatar {
|
||||
width: 40px; height: 40px; border-radius: 50%;
|
||||
background: var(--primary-100);
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
font-weight: 700; color: var(--primary-600);
|
||||
}
|
||||
|
||||
.cover-hero {
|
||||
aspect-ratio: 16 / 9;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, var(--primary-100), #fff 55%, var(--accent-pink));
|
||||
}
|
||||
|
||||
.audio-player {
|
||||
display: flex; align-items: center; gap: 12px; padding: 12px 16px;
|
||||
border-radius: 12px; border: 1px solid var(--neutral-200); background: #fff;
|
||||
}
|
||||
.audio-bar { height: 6px; background: var(--neutral-200); border-radius: 999px; flex: 1; }
|
||||
.audio-progress { width: 35%; height: 100%; background: var(--primary-600); border-radius: 999px; }
|
||||
|
||||
.tabs { display: flex; gap: 8px; border-bottom: 1px solid var(--neutral-200); }
|
||||
.tab { padding: 10px 12px; color: var(--neutral-700); }
|
||||
.tab.active { color: var(--primary-600); border-bottom: 2px solid var(--primary-600); }
|
||||
|
||||
.callout {
|
||||
background: #fff;
|
||||
border: 1px dashed var(--neutral-200);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
color: var(--neutral-700);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.footer-note { color: var(--neutral-500); font-size: 12px; margin-top: 12px; }
|
||||
|
||||
.hero-actions { display: flex; gap: 12px; flex-wrap: wrap; }
|
||||
|
||||
.split {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.grid-3 { grid-template-columns: repeat(2, minmax(0,1fr)); }
|
||||
.split { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.grid-2 { grid-template-columns: 1fr; }
|
||||
.grid-3 { grid-template-columns: 1fr; }
|
||||
.row { grid-template-columns: 1fr; }
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>宇宙详情</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="card section">
|
||||
<h2>星际冒险</h2>
|
||||
<div class="card-meta">主角:小明船长 · 更新于 2025/01/12</div>
|
||||
</div>
|
||||
<div class="grid grid-2 section">
|
||||
<div class="card">
|
||||
<h3>主角设定</h3>
|
||||
<div class="callout">小明是来自地球的探险家,勇敢且好奇。</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>常驻角色</h3>
|
||||
<div class="callout">机器人小七、外星猫咪星星</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>世界观</h3>
|
||||
<div class="callout">星际学院、彩虹星云、飞船港湾</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>成就</h3>
|
||||
<div class="callout">克服恐惧 · 结交朋友 · 学会独立</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<button class="btn btn--secondary">编辑宇宙</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,64 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>故事宇宙</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="toolbar section">
|
||||
<h2>故事宇宙</h2>
|
||||
<button class="btn btn--primary">新建宇宙</button>
|
||||
</div>
|
||||
<div class="grid grid-3 section">
|
||||
<div class="card">
|
||||
<div class="card-title">星际冒险</div>
|
||||
<div class="card-meta">主角:小明船长</div>
|
||||
<div class="chips section">
|
||||
<span class="chip">伙伴:机器人小七</span>
|
||||
<span class="chip">成就:3</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">梦幻森林</div>
|
||||
<div class="card-meta">主角:森林守护者</div>
|
||||
<div class="chips section">
|
||||
<span class="chip">伙伴:魔法猫咪</span>
|
||||
<span class="chip">成就:1</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card card--flat">
|
||||
<div class="callout">空态示例:创建第一个宇宙</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,59 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>账户设置</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="grid grid-2 section">
|
||||
<div class="card">
|
||||
<h3>个人信息</h3>
|
||||
<div class="row section">
|
||||
<input class="input" placeholder="昵称" value="Dream Parent" />
|
||||
<input class="input" placeholder="邮箱" value="parent@example.com" />
|
||||
</div>
|
||||
<button class="btn btn--primary">保存</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>账号安全</h3>
|
||||
<div class="callout">已绑定 GitHub、Google</div>
|
||||
<button class="btn btn--secondary" style="margin-top: 12px;">管理绑定</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>数据隐私</h3>
|
||||
<button class="btn btn--secondary">导出数据</button>
|
||||
<button class="btn btn--danger" style="margin-top: 12px;">删除账户</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,74 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>管理后台 - Providers</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="toolbar section">
|
||||
<h2>Providers 管理</h2>
|
||||
<button class="btn btn--primary">新增 Provider</button>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>类型</th>
|
||||
<th>状态</th>
|
||||
<th>延迟</th>
|
||||
<th>最近检查</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>text_primary</td>
|
||||
<td>Text</td>
|
||||
<td><span class="badge">健康</span></td>
|
||||
<td>420ms</td>
|
||||
<td>2 分钟前</td>
|
||||
<td><a href="#">编辑</a> · <a href="#">禁用</a> · <a href="#">重载</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>image_primary</td>
|
||||
<td>Image</td>
|
||||
<td><span class="badge">健康</span></td>
|
||||
<td>860ms</td>
|
||||
<td>5 分钟前</td>
|
||||
<td><a href="#">编辑</a> · <a href="#">禁用</a> · <a href="#">重载</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="footer-note">点击编辑后弹出 JSON 配置编辑器。</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,88 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>孩子档案详情</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="card section" style="display:flex; align-items:center; justify-content: space-between;">
|
||||
<div style="display:flex; gap:12px; align-items:center;">
|
||||
<div class="avatar">明</div>
|
||||
<div>
|
||||
<div class="card-title">小明 · 5岁</div>
|
||||
<div class="card-meta">男 · 生日 2020/05/12</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn--secondary">编辑档案</button>
|
||||
</div>
|
||||
|
||||
<div class="tabs section">
|
||||
<div class="tab active">基础信息</div>
|
||||
<div class="tab">兴趣与成长</div>
|
||||
<div class="tab">故事宇宙</div>
|
||||
<div class="tab">阅读记录</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2 section">
|
||||
<div class="card">
|
||||
<h3>兴趣标签</h3>
|
||||
<div class="chips">
|
||||
<span class="chip selected">太空</span>
|
||||
<span class="chip selected">机器人</span>
|
||||
<span class="chip">冒险</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>成长主题</h3>
|
||||
<div class="chips">
|
||||
<span class="chip selected">勇气</span>
|
||||
<span class="chip">分享</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>故事宇宙</h3>
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-title">星际冒险</div>
|
||||
<div class="card-meta">主角:小明船长 · 成就 3 个</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">梦幻森林</div>
|
||||
<div class="card-meta">主角:森林守护者 · 成就 1 个</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,58 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>孩子档案</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="toolbar section">
|
||||
<h2>我的宝贝</h2>
|
||||
<button class="btn btn--primary">添加档案</button>
|
||||
</div>
|
||||
<div class="grid grid-3 section">
|
||||
<div class="card">
|
||||
<div class="avatar">明</div>
|
||||
<div class="card-title">小明 · 5岁</div>
|
||||
<div class="chips"><span class="chip">太空</span><span class="chip">机器人</span></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="avatar">红</div>
|
||||
<div class="card-title">小红 · 3岁</div>
|
||||
<div class="chips"><span class="chip">公主</span><span class="chip">动物</span></div>
|
||||
</div>
|
||||
<div class="card card--flat">
|
||||
<div class="callout">空态示例:添加一个孩子档案</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,111 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>生成故事</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="stepper section">
|
||||
<span class="step active">档案</span>
|
||||
<span class="step">宇宙</span>
|
||||
<span class="step">关键词</span>
|
||||
<span class="step">生成</span>
|
||||
</div>
|
||||
<div class="split section">
|
||||
<div class="card">
|
||||
<h3>为谁创作故事</h3>
|
||||
<div class="row section">
|
||||
<div>
|
||||
<label>孩子档案</label>
|
||||
<select>
|
||||
<option>小明 · 5岁</option>
|
||||
<option>小红 · 3岁</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>故事宇宙</label>
|
||||
<select>
|
||||
<option>延续上一次(星际冒险)</option>
|
||||
<option>新建宇宙</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<label>关键词</label>
|
||||
<div class="chips section">
|
||||
<span class="chip selected">太空</span>
|
||||
<span class="chip selected">勇气</span>
|
||||
<span class="chip">机器人</span>
|
||||
<span class="chip">探索</span>
|
||||
</div>
|
||||
<input class="input" placeholder="输入更多关键词" />
|
||||
</div>
|
||||
<div class="row section">
|
||||
<div>
|
||||
<label>成长主题</label>
|
||||
<select>
|
||||
<option>勇气</option>
|
||||
<option>分享</option>
|
||||
<option>独立</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>故事长度</label>
|
||||
<div class="chips">
|
||||
<span class="chip selected">短</span>
|
||||
<span class="chip">中</span>
|
||||
<span class="chip">长</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<button class="btn btn--primary" style="width: 100%;">生成故事</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>生成预览</h3>
|
||||
<div class="card-cover"></div>
|
||||
<div class="card-title">故事标题占位</div>
|
||||
<p class="card-meta">故事摘要将显示在这里,支持 2-3 行预览。</p>
|
||||
<div class="section">
|
||||
<div class="callout">生成中:文本 → 封面 → 语音</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="callout" style="border-color: var(--error); color: var(--error);">封面生成失败,稍后重试</div>
|
||||
<button class="btn btn--secondary" style="margin-top: 8px;">重新生成封面</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,33 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>DreamWeaver 原型入口</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="container" style="padding: 48px 0;">
|
||||
<div class="hero">
|
||||
<h1>DreamWeaver HTML 原型入口</h1>
|
||||
<p>请选择页面进行导入或预览(HTML to Figma)。</p>
|
||||
<div class="grid grid-3 section">
|
||||
<div class="card"><a href="login.html">登录 / 授权</a></div>
|
||||
<div class="card"><a href="home.html">生成故事(Home)</a></div>
|
||||
<div class="card"><a href="my-stories.html">我的故事(列表)</a></div>
|
||||
<div class="card"><a href="story-detail.html">故事详情</a></div>
|
||||
<div class="card"><a href="child-profiles.html">孩子档案(列表)</a></div>
|
||||
<div class="card"><a href="child-profile-detail.html">孩子档案(详情)</a></div>
|
||||
<div class="card"><a href="universes.html">故事宇宙(列表)</a></div>
|
||||
<div class="card"><a href="universe-detail.html">故事宇宙(详情)</a></div>
|
||||
<div class="card"><a href="push-settings.html">推送设置</a></div>
|
||||
<div class="card"><a href="account-settings.html">账户设置</a></div>
|
||||
<div class="card"><a href="admin-providers.html">管理后台(Providers)</a></div>
|
||||
<div class="card"><a href="not-found.html">404 / 错误</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,28 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>登录 / 授权</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="container" style="padding: 80px 0;">
|
||||
<div class="hero" style="max-width: 420px; margin: 0 auto; text-align: center;">
|
||||
<div class="nav__logo" style="justify-content: center;">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<h2 style="margin-top: 16px;">欢迎来到 DreamWeaver</h2>
|
||||
<p>为孩子生成独一无二的故事</p>
|
||||
<div class="section" style="display: grid; gap: 12px;">
|
||||
<button class="btn btn--primary">使用 GitHub 登录</button>
|
||||
<button class="btn btn--secondary">使用 Google 登录</button>
|
||||
</div>
|
||||
<div class="footer-note">我们仅使用公开信息创建账户</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,84 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>我的故事</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="toolbar section">
|
||||
<input class="input" style="width: 260px;" placeholder="搜索标题或关键词" />
|
||||
<select style="width: 160px;">
|
||||
<option>孩子:全部</option>
|
||||
<option>小明</option>
|
||||
<option>小红</option>
|
||||
</select>
|
||||
<select style="width: 160px;">
|
||||
<option>排序:最新</option>
|
||||
<option>最早</option>
|
||||
</select>
|
||||
<button class="btn btn--ghost">网格</button>
|
||||
<button class="btn btn--ghost">列表</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-3 section">
|
||||
<div class="card">
|
||||
<div class="card-cover"></div>
|
||||
<div class="card-title">星际冒险 · 第三章</div>
|
||||
<div class="chips">
|
||||
<span class="chip">太空</span><span class="chip">勇气</span>
|
||||
</div>
|
||||
<div class="card-meta">小明 · 更新于 2 天前</div>
|
||||
<div class="section hero-actions">
|
||||
<button class="btn btn--primary">继续阅读</button>
|
||||
<button class="btn btn--secondary">重生成封面</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-cover"></div>
|
||||
<div class="card-title">梦幻森林 · 朋友篇</div>
|
||||
<div class="chips">
|
||||
<span class="chip">友谊</span><span class="chip">动物</span>
|
||||
</div>
|
||||
<div class="card-meta">小红 · 更新于 5 天前</div>
|
||||
<div class="section hero-actions">
|
||||
<button class="btn btn--primary">继续阅读</button>
|
||||
<button class="btn btn--danger">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card card--flat">
|
||||
<div class="callout">空态示例:开始生成第一个故事</div>
|
||||
<button class="btn btn--primary" style="margin-top: 12px;">生成故事</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,20 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>404</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="container" style="padding: 80px 0; text-align:center;">
|
||||
<div class="hero">
|
||||
<h1>404</h1>
|
||||
<p>页面走丢了,回到生成故事开始吧。</p>
|
||||
<button class="btn btn--primary">返回首页</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,78 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>推送设置</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="grid grid-2 section">
|
||||
<div class="card">
|
||||
<h3>主动推送</h3>
|
||||
<div class="row section">
|
||||
<div>
|
||||
<label>主开关</label>
|
||||
<select>
|
||||
<option>开启</option>
|
||||
<option>关闭</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>推送时间</label>
|
||||
<input class="input" placeholder="20:00" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<label>触发类型</label>
|
||||
<div class="chips">
|
||||
<span class="chip selected">时间触发</span>
|
||||
<span class="chip selected">事件触发</span>
|
||||
<span class="chip">行为触发</span>
|
||||
<span class="chip">成长触发</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<label>免打扰</label>
|
||||
<div class="row">
|
||||
<input class="input" placeholder="21:00" />
|
||||
<input class="input" placeholder="09:00" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>推送预览</h3>
|
||||
<div class="callout">“今晚给小明讲一个关于太空的故事,好吗?”</div>
|
||||
<button class="btn btn--secondary" style="margin-top: 12px;">发送测试推送</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,73 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>故事详情</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="section">
|
||||
<div class="cover-hero"></div>
|
||||
<h2 style="margin-top: 16px;">星际冒险 · 勇气的种子</h2>
|
||||
<div class="card-meta">小明 · 星际冒险宇宙 · 2025/01/12</div>
|
||||
<div class="hero-actions section">
|
||||
<button class="btn btn--secondary">重新生成封面</button>
|
||||
<button class="btn btn--primary">生成语音</button>
|
||||
<button class="btn btn--ghost">分享</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="split section">
|
||||
<div class="card">
|
||||
<h3>故事正文</h3>
|
||||
<p>夜空像一条温柔的河流,小明驾驶着飞船穿过星光……</p>
|
||||
<p>他握紧操纵杆,鼓起勇气,向未知的星球靠近。</p>
|
||||
<p>最终,小明发现了新的朋友,也学会了如何面对黑暗。</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>成就</h3>
|
||||
<div class="chips section">
|
||||
<span class="chip selected">勇气</span>
|
||||
<span class="chip selected">友谊</span>
|
||||
</div>
|
||||
<div class="callout section">“克服了黑暗的恐惧”</div>
|
||||
<div class="callout">“帮助了迷路的小伙伴”</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section audio-player">
|
||||
<button class="btn btn--ghost">播放</button>
|
||||
<div class="audio-bar"><div class="audio-progress"></div></div>
|
||||
<button class="btn btn--ghost">1.0x</button>
|
||||
</div>
|
||||
<div class="footer-note">语音未生成时,显示“生成语音”按钮作为主操作。</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,217 +0,0 @@
|
||||
:root {
|
||||
--primary-600: #7C3AED;
|
||||
--primary-500: #8B5CF6;
|
||||
--primary-100: #EDE9FE;
|
||||
--accent-pink: #FB7185;
|
||||
--accent-sky: #22D3EE;
|
||||
--success: #34C759;
|
||||
--warning: #F6A609;
|
||||
--error: #FF5A5F;
|
||||
--neutral-900: #1F2937;
|
||||
--neutral-700: #4B5563;
|
||||
--neutral-500: #9CA3AF;
|
||||
--neutral-200: #E5E7EB;
|
||||
--neutral-100: #F5F5F7;
|
||||
--hero-gradient: linear-gradient(135deg, #EDE9FE 0%, #FFE4F3 45%, #E0F7FF 100%);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
:root {
|
||||
--container-width: 1200px;
|
||||
--gutter: 24px;
|
||||
--radius-card: 12px;
|
||||
--radius-input: 10px;
|
||||
--radius-button: 8px;
|
||||
--radius-pill: 24px;
|
||||
--shadow-s: 0 4px 16px rgba(31,36,48,0.08);
|
||||
--shadow-m: 0 10px 30px rgba(31,36,48,0.12);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC", "Noto Sans SC", Inter, system-ui, -apple-system, sans-serif;
|
||||
color: var(--neutral-900);
|
||||
background: var(--neutral-100);
|
||||
}
|
||||
|
||||
a { color: var(--primary-600); text-decoration: none; }
|
||||
|
||||
.page { min-height: 100vh; }
|
||||
.container {
|
||||
width: min(var(--container-width), 100% - 48px);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.nav {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--neutral-200);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.nav__inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 0;
|
||||
gap: 16px;
|
||||
}
|
||||
.nav__left, .nav__center, .nav__right { display: flex; align-items: center; gap: 16px; }
|
||||
.nav__logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--neutral-900);
|
||||
}
|
||||
.nav__logo-badge {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, var(--primary-500), var(--accent-sky));
|
||||
}
|
||||
.nav__item { color: var(--neutral-700); font-weight: 500; }
|
||||
.nav__item.active { color: var(--primary-600); }
|
||||
|
||||
.hero {
|
||||
background: var(--hero-gradient);
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
box-shadow: var(--shadow-s);
|
||||
}
|
||||
|
||||
.section { margin: 28px 0; }
|
||||
.section-title { font-size: 20px; font-weight: 600; margin-bottom: 12px; }
|
||||
|
||||
.grid { display: grid; gap: 16px; }
|
||||
.grid-2 { grid-template-columns: repeat(2, minmax(0,1fr)); }
|
||||
.grid-3 { grid-template-columns: repeat(3, minmax(0,1fr)); }
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: var(--radius-card);
|
||||
padding: 16px;
|
||||
box-shadow: var(--shadow-s);
|
||||
}
|
||||
.card--flat { box-shadow: none; border: 1px solid var(--neutral-200); }
|
||||
.card-cover {
|
||||
width: 100%;
|
||||
aspect-ratio: 21 / 9;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, var(--primary-100), #fff 60%, var(--accent-sky));
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.card-title { font-weight: 600; margin: 6px 0; }
|
||||
.card-meta { color: var(--neutral-500); font-size: 12px; }
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--primary-100);
|
||||
color: var(--primary-600);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.chip {
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius-pill);
|
||||
border: 1px solid var(--neutral-200);
|
||||
background: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
.chip.selected { background: var(--primary-100); border-color: var(--primary-500); color: var(--primary-600); }
|
||||
|
||||
.btn {
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
border-radius: var(--radius-button);
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn--primary { background: var(--primary-600); color: #fff; }
|
||||
.btn--secondary { background: #fff; border-color: var(--primary-600); color: var(--primary-600); }
|
||||
.btn--ghost { background: transparent; color: var(--neutral-700); }
|
||||
.btn--danger { background: #fff; border-color: var(--error); color: var(--error); }
|
||||
|
||||
.input, select, textarea {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-input);
|
||||
border: 1px solid var(--neutral-200);
|
||||
background: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.row { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px; }
|
||||
|
||||
.stepper { display: flex; gap: 10px; align-items: center; }
|
||||
.step {
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--neutral-100);
|
||||
color: var(--neutral-700);
|
||||
font-size: 12px;
|
||||
}
|
||||
.step.active { background: var(--primary-100); color: var(--primary-600); }
|
||||
|
||||
.toolbar { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
||||
|
||||
.table { width: 100%; border-collapse: collapse; }
|
||||
.table th, .table td { border-bottom: 1px solid var(--neutral-200); padding: 12px 8px; text-align: left; font-size: 14px; }
|
||||
|
||||
.avatar {
|
||||
width: 40px; height: 40px; border-radius: 50%;
|
||||
background: var(--primary-100);
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
font-weight: 700; color: var(--primary-600);
|
||||
}
|
||||
|
||||
.cover-hero {
|
||||
aspect-ratio: 16 / 9;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, var(--primary-100), #fff 55%, var(--accent-pink));
|
||||
}
|
||||
|
||||
.audio-player {
|
||||
display: flex; align-items: center; gap: 12px; padding: 12px 16px;
|
||||
border-radius: 12px; border: 1px solid var(--neutral-200); background: #fff;
|
||||
}
|
||||
.audio-bar { height: 6px; background: var(--neutral-200); border-radius: 999px; flex: 1; }
|
||||
.audio-progress { width: 35%; height: 100%; background: var(--primary-600); border-radius: 999px; }
|
||||
|
||||
.tabs { display: flex; gap: 8px; border-bottom: 1px solid var(--neutral-200); }
|
||||
.tab { padding: 10px 12px; color: var(--neutral-700); }
|
||||
.tab.active { color: var(--primary-600); border-bottom: 2px solid var(--primary-600); }
|
||||
|
||||
.callout {
|
||||
background: #fff;
|
||||
border: 1px dashed var(--neutral-200);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
color: var(--neutral-700);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.footer-note { color: var(--neutral-500); font-size: 12px; margin-top: 12px; }
|
||||
|
||||
.hero-actions { display: flex; gap: 12px; flex-wrap: wrap; }
|
||||
|
||||
.split {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.grid-3 { grid-template-columns: repeat(2, minmax(0,1fr)); }
|
||||
.split { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.grid-2 { grid-template-columns: 1fr; }
|
||||
.grid-3 { grid-template-columns: 1fr; }
|
||||
.row { grid-template-columns: 1fr; }
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>宇宙详情</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="card section">
|
||||
<h2>星际冒险</h2>
|
||||
<div class="card-meta">主角:小明船长 · 更新于 2025/01/12</div>
|
||||
</div>
|
||||
<div class="grid grid-2 section">
|
||||
<div class="card">
|
||||
<h3>主角设定</h3>
|
||||
<div class="callout">小明是来自地球的探险家,勇敢且好奇。</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>常驻角色</h3>
|
||||
<div class="callout">机器人小七、外星猫咪星星</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>世界观</h3>
|
||||
<div class="callout">星际学院、彩虹星云、飞船港湾</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>成就</h3>
|
||||
<div class="callout">克服恐惧 · 结交朋友 · 学会独立</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<button class="btn btn--secondary">编辑宇宙</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,64 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>故事宇宙</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="nav">
|
||||
<div class="container nav__inner">
|
||||
<div class="nav__left">
|
||||
<div class="nav__logo">
|
||||
<span class="nav__logo-badge"></span>
|
||||
DreamWeaver
|
||||
</div>
|
||||
<span class="badge">Web 原型</span>
|
||||
</div>
|
||||
<div class="nav__center">
|
||||
<a class="nav__item active" href="home.html">生成故事</a>
|
||||
<a class="nav__item" href="my-stories.html">我的故事</a>
|
||||
<a class="nav__item" href="child-profiles.html">孩子档案</a>
|
||||
<a class="nav__item" href="universes.html">故事宇宙</a>
|
||||
<a class="nav__item" href="push-settings.html">推送设置</a>
|
||||
<a class="nav__item" href="account-settings.html">账户设置</a>
|
||||
<a class="nav__item" href="admin-providers.html">管理后台</a>
|
||||
</div>
|
||||
<div class="nav__right">
|
||||
<input class="input" style="width: 200px;" placeholder="搜索故事" />
|
||||
<div class="avatar">家</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container" style="padding: 28px 0 60px;">
|
||||
<div class="toolbar section">
|
||||
<h2>故事宇宙</h2>
|
||||
<button class="btn btn--primary">新建宇宙</button>
|
||||
</div>
|
||||
<div class="grid grid-3 section">
|
||||
<div class="card">
|
||||
<div class="card-title">星际冒险</div>
|
||||
<div class="card-meta">主角:小明船长</div>
|
||||
<div class="chips section">
|
||||
<span class="chip">伙伴:机器人小七</span>
|
||||
<span class="chip">成就:3</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">梦幻森林</div>
|
||||
<div class="card-meta">主角:森林守护者</div>
|
||||
<div class="chips section">
|
||||
<span class="chip">伙伴:魔法猫咪</span>
|
||||
<span class="chip">成就:1</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card card--flat">
|
||||
<div class="callout">空态示例:创建第一个宇宙</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,429 +0,0 @@
|
||||
# 孩子档案数据模型
|
||||
|
||||
## 概述
|
||||
|
||||
孩子档案是记忆智能系统的核心,存储孩子的基础信息、兴趣偏好和阅读行为数据。
|
||||
|
||||
---
|
||||
|
||||
## 一、数据库模型
|
||||
|
||||
### 1.1 主表: child_profiles
|
||||
|
||||
```sql
|
||||
CREATE TABLE child_profiles (
|
||||
-- 主键
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- 外键: 所属用户
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
|
||||
-- 基础信息
|
||||
name VARCHAR(50) NOT NULL,
|
||||
avatar_url VARCHAR(500),
|
||||
birth_date DATE,
|
||||
gender VARCHAR(10) CHECK (gender IN ('male', 'female', 'other')),
|
||||
|
||||
-- 显式偏好 (家长填写)
|
||||
interests JSONB DEFAULT '[]',
|
||||
-- 示例: ["恐龙", "太空", "公主", "动物"]
|
||||
|
||||
growth_themes JSONB DEFAULT '[]',
|
||||
-- 示例: ["勇气", "分享"]
|
||||
|
||||
-- 隐式偏好 (系统学习)
|
||||
reading_preferences JSONB DEFAULT '{}',
|
||||
-- 示例: {
|
||||
-- "preferred_length": "medium", -- short/medium/long
|
||||
-- "preferred_style": "adventure", -- adventure/fairy/educational
|
||||
-- "tag_weights": {"恐龙": 5, "公主": 2, "太空": 3}
|
||||
-- }
|
||||
|
||||
-- 统计数据
|
||||
stories_count INTEGER DEFAULT 0,
|
||||
total_reading_time INTEGER DEFAULT 0, -- 秒
|
||||
|
||||
-- 时间戳
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
-- 约束
|
||||
CONSTRAINT unique_child_per_user UNIQUE (user_id, name)
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX idx_child_profiles_user_id ON child_profiles(user_id);
|
||||
```
|
||||
|
||||
### 1.2 兴趣标签枚举
|
||||
|
||||
预定义的兴趣标签,前端展示用:
|
||||
|
||||
```python
|
||||
INTEREST_TAGS = {
|
||||
"animals": {
|
||||
"zh": "动物",
|
||||
"icon": "🐾",
|
||||
"subtags": ["恐龙", "猫咪", "狗狗", "兔子", "海洋动物"]
|
||||
},
|
||||
"fantasy": {
|
||||
"zh": "奇幻",
|
||||
"icon": "✨",
|
||||
"subtags": ["公主", "王子", "魔法", "精灵", "龙"]
|
||||
},
|
||||
"adventure": {
|
||||
"zh": "冒险",
|
||||
"icon": "🗺️",
|
||||
"subtags": ["太空", "海盗", "探险", "寻宝"]
|
||||
},
|
||||
"vehicles": {
|
||||
"zh": "交通工具",
|
||||
"icon": "🚗",
|
||||
"subtags": ["汽车", "火车", "飞机", "火箭"]
|
||||
},
|
||||
"nature": {
|
||||
"zh": "自然",
|
||||
"icon": "🌳",
|
||||
"subtags": ["森林", "海洋", "山川", "四季"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 成长主题枚举
|
||||
|
||||
```python
|
||||
GROWTH_THEMES = [
|
||||
{"key": "courage", "zh": "勇气", "description": "克服恐惧,勇敢面对"},
|
||||
{"key": "sharing", "zh": "分享", "description": "学会与他人分享"},
|
||||
{"key": "friendship", "zh": "友谊", "description": "交朋友,珍惜友情"},
|
||||
{"key": "honesty", "zh": "诚实", "description": "说真话,不撒谎"},
|
||||
{"key": "independence", "zh": "独立", "description": "自己的事情自己做"},
|
||||
{"key": "kindness", "zh": "善良", "description": "帮助他人,关爱弱小"},
|
||||
{"key": "patience", "zh": "耐心", "description": "学会等待,不急躁"},
|
||||
{"key": "curiosity", "zh": "好奇", "description": "探索未知,爱问为什么"}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、SQLAlchemy 模型
|
||||
|
||||
```python
|
||||
# backend/app/db/models.py
|
||||
|
||||
from sqlalchemy import Column, String, Date, Integer, ForeignKey, JSON
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
import uuid
|
||||
|
||||
class ChildProfile(Base):
|
||||
__tablename__ = "child_profiles"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
||||
|
||||
# 基础信息
|
||||
name = Column(String(50), nullable=False)
|
||||
avatar_url = Column(String(500))
|
||||
birth_date = Column(Date)
|
||||
gender = Column(String(10))
|
||||
|
||||
# 偏好
|
||||
interests = Column(JSON, default=list)
|
||||
growth_themes = Column(JSON, default=list)
|
||||
reading_preferences = Column(JSON, default=dict)
|
||||
|
||||
# 统计
|
||||
stories_count = Column(Integer, default=0)
|
||||
total_reading_time = Column(Integer, default=0)
|
||||
|
||||
# 时间戳
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
# 关系
|
||||
user = relationship("User", back_populates="child_profiles")
|
||||
story_universes = relationship("StoryUniverse", back_populates="child_profile", cascade="all, delete-orphan")
|
||||
|
||||
@property
|
||||
def age(self) -> int | None:
|
||||
"""计算年龄"""
|
||||
if not self.birth_date:
|
||||
return None
|
||||
today = date.today()
|
||||
return today.year - self.birth_date.year - (
|
||||
(today.month, today.day) < (self.birth_date.month, self.birth_date.day)
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、Pydantic Schema
|
||||
|
||||
```python
|
||||
# backend/app/schemas/child_profile.py
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import date
|
||||
from uuid import UUID
|
||||
|
||||
class ChildProfileCreate(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=50)
|
||||
birth_date: date | None = None
|
||||
gender: str | None = Field(None, pattern="^(male|female|other)$")
|
||||
interests: list[str] = Field(default_factory=list)
|
||||
growth_themes: list[str] = Field(default_factory=list)
|
||||
|
||||
class ChildProfileUpdate(BaseModel):
|
||||
name: str | None = Field(None, min_length=1, max_length=50)
|
||||
birth_date: date | None = None
|
||||
gender: str | None = Field(None, pattern="^(male|female|other)$")
|
||||
interests: list[str] | None = None
|
||||
growth_themes: list[str] | None = None
|
||||
avatar_url: str | None = None
|
||||
|
||||
class ChildProfileResponse(BaseModel):
|
||||
id: UUID
|
||||
name: str
|
||||
avatar_url: str | None
|
||||
birth_date: date | None
|
||||
gender: str | None
|
||||
age: int | None
|
||||
interests: list[str]
|
||||
growth_themes: list[str]
|
||||
stories_count: int
|
||||
total_reading_time: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class ChildProfileListResponse(BaseModel):
|
||||
profiles: list[ChildProfileResponse]
|
||||
total: int
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、API 实现
|
||||
|
||||
```python
|
||||
# backend/app/api/profiles.py
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from uuid import UUID
|
||||
|
||||
router = APIRouter(prefix="/api/profiles", tags=["profiles"])
|
||||
|
||||
@router.get("", response_model=ChildProfileListResponse)
|
||||
async def list_profiles(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取当前用户的所有孩子档案"""
|
||||
profiles = await db.execute(
|
||||
select(ChildProfile)
|
||||
.where(ChildProfile.user_id == current_user.id)
|
||||
.order_by(ChildProfile.created_at)
|
||||
)
|
||||
profiles = profiles.scalars().all()
|
||||
return ChildProfileListResponse(profiles=profiles, total=len(profiles))
|
||||
|
||||
@router.post("", response_model=ChildProfileResponse, status_code=201)
|
||||
async def create_profile(
|
||||
data: ChildProfileCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""创建孩子档案"""
|
||||
# 检查是否超过限制 (每用户最多5个孩子档案)
|
||||
count = await db.scalar(
|
||||
select(func.count(ChildProfile.id))
|
||||
.where(ChildProfile.user_id == current_user.id)
|
||||
)
|
||||
if count >= 5:
|
||||
raise HTTPException(400, "最多只能创建5个孩子档案")
|
||||
|
||||
profile = ChildProfile(user_id=current_user.id, **data.model_dump())
|
||||
db.add(profile)
|
||||
await db.commit()
|
||||
await db.refresh(profile)
|
||||
return profile
|
||||
|
||||
@router.get("/{profile_id}", response_model=ChildProfileResponse)
|
||||
async def get_profile(
|
||||
profile_id: UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取单个孩子档案"""
|
||||
profile = await db.get(ChildProfile, profile_id)
|
||||
if not profile or profile.user_id != current_user.id:
|
||||
raise HTTPException(404, "档案不存在")
|
||||
return profile
|
||||
|
||||
@router.put("/{profile_id}", response_model=ChildProfileResponse)
|
||||
async def update_profile(
|
||||
profile_id: UUID,
|
||||
data: ChildProfileUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""更新孩子档案"""
|
||||
profile = await db.get(ChildProfile, profile_id)
|
||||
if not profile or profile.user_id != current_user.id:
|
||||
raise HTTPException(404, "档案不存在")
|
||||
|
||||
for key, value in data.model_dump(exclude_unset=True).items():
|
||||
setattr(profile, key, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(profile)
|
||||
return profile
|
||||
|
||||
@router.delete("/{profile_id}", status_code=204)
|
||||
async def delete_profile(
|
||||
profile_id: UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""删除孩子档案"""
|
||||
profile = await db.get(ChildProfile, profile_id)
|
||||
if not profile or profile.user_id != current_user.id:
|
||||
raise HTTPException(404, "档案不存在")
|
||||
|
||||
await db.delete(profile)
|
||||
await db.commit()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、隐式偏好学习
|
||||
|
||||
### 5.1 行为事件
|
||||
|
||||
```python
|
||||
class ReadingEvent(BaseModel):
|
||||
"""阅读行为事件"""
|
||||
profile_id: UUID
|
||||
story_id: UUID
|
||||
event_type: Literal["started", "completed", "skipped", "replayed"]
|
||||
reading_time: int # 秒
|
||||
timestamp: datetime
|
||||
```
|
||||
|
||||
### 5.2 偏好更新算法
|
||||
|
||||
```python
|
||||
async def update_reading_preferences(
|
||||
db: AsyncSession,
|
||||
profile_id: UUID,
|
||||
story: Story,
|
||||
event: ReadingEvent
|
||||
):
|
||||
"""根据阅读行为更新隐式偏好"""
|
||||
profile = await db.get(ChildProfile, profile_id)
|
||||
prefs = profile.reading_preferences or {}
|
||||
tag_weights = prefs.get("tag_weights", {})
|
||||
|
||||
# 权重调整
|
||||
weight_delta = {
|
||||
"completed": 1.0, # 完整阅读,正向
|
||||
"replayed": 1.5, # 重复播放,强正向
|
||||
"skipped": -0.5, # 跳过,负向
|
||||
"started": 0.1 # 开始阅读,弱正向
|
||||
}
|
||||
|
||||
delta = weight_delta.get(event.event_type, 0)
|
||||
|
||||
for tag in story.tags:
|
||||
current = tag_weights.get(tag, 0)
|
||||
tag_weights[tag] = max(0, current + delta) # 不低于0
|
||||
|
||||
# 更新阅读长度偏好
|
||||
if event.event_type == "completed":
|
||||
word_count = len(story.content)
|
||||
if word_count < 300:
|
||||
length_pref = "short"
|
||||
elif word_count < 600:
|
||||
length_pref = "medium"
|
||||
else:
|
||||
length_pref = "long"
|
||||
|
||||
# 简单的移动平均
|
||||
prefs["preferred_length"] = length_pref
|
||||
|
||||
prefs["tag_weights"] = tag_weights
|
||||
profile.reading_preferences = prefs
|
||||
await db.commit()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、数据迁移
|
||||
|
||||
```python
|
||||
# backend/alembic/versions/xxx_add_child_profiles.py
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'child_profiles',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('user_id', sa.UUID(), nullable=False),
|
||||
sa.Column('name', sa.String(50), nullable=False),
|
||||
sa.Column('avatar_url', sa.String(500)),
|
||||
sa.Column('birth_date', sa.Date()),
|
||||
sa.Column('gender', sa.String(10)),
|
||||
sa.Column('interests', sa.JSON(), server_default='[]'),
|
||||
sa.Column('growth_themes', sa.JSON(), server_default='[]'),
|
||||
sa.Column('reading_preferences', sa.JSON(), server_default='{}'),
|
||||
sa.Column('stories_count', sa.Integer(), server_default='0'),
|
||||
sa.Column('total_reading_time', sa.Integer(), server_default='0'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('idx_child_profiles_user_id', 'child_profiles', ['user_id'])
|
||||
|
||||
def downgrade():
|
||||
op.drop_index('idx_child_profiles_user_id')
|
||||
op.drop_table('child_profiles')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、隐私与安全
|
||||
|
||||
### 7.1 数据加密
|
||||
|
||||
敏感字段(姓名、出生日期)在存储时加密:
|
||||
|
||||
```python
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
class EncryptedChildProfile:
|
||||
"""加密存储的孩子档案"""
|
||||
|
||||
@staticmethod
|
||||
def encrypt_name(name: str, key: bytes) -> str:
|
||||
f = Fernet(key)
|
||||
return f.encrypt(name.encode()).decode()
|
||||
|
||||
@staticmethod
|
||||
def decrypt_name(encrypted: str, key: bytes) -> str:
|
||||
f = Fernet(key)
|
||||
return f.decrypt(encrypted.encode()).decode()
|
||||
```
|
||||
|
||||
### 7.2 访问控制
|
||||
|
||||
- 孩子档案只能被创建者访问
|
||||
- 删除用户时级联删除所有孩子档案
|
||||
- API 层强制校验 `user_id` 归属
|
||||
|
||||
### 7.3 数据保留
|
||||
|
||||
- 用户可随时删除孩子档案
|
||||
- 删除后 30 天内可恢复(软删除)
|
||||
- 30 天后永久删除
|
||||
@@ -1,541 +0,0 @@
|
||||
# 记忆智能系统 PRD
|
||||
|
||||
## 概述
|
||||
|
||||
**功能名称**: 记忆智能 (Memory Intelligence)
|
||||
**版本**: v1.1
|
||||
**优先级**: Phase 2.5 (体验增强后、社区化前)
|
||||
**目标用户**: 家长 + 3-8 岁儿童
|
||||
**更新记录**: 2025-01-22 合并 `backend/docs/memory_system_prd.md`
|
||||
|
||||
### 核心愿景
|
||||
|
||||
将当前的"数据存储"升级为有温度的**"情感连接系统"**。
|
||||
我们不只是在记住数据,而是在**维护孩子与故事世界的关系**。让每一个故事不再是孤立的碎片,而是构建孩子专属"故事宇宙"的砖瓦。
|
||||
|
||||
### 核心价值
|
||||
|
||||
让 DreamWeaver 从"故事生成工具"进化为"懂孩子的故事伙伴":
|
||||
- **记住孩子**: 偏好、成长阶段、兴趣变化
|
||||
- **延续故事**: 角色、世界观跨故事延续
|
||||
- **主动关怀**: 适时推送个性化故事建议
|
||||
|
||||
### 产品痛点与解决方案
|
||||
|
||||
| 用户角色 | 核心痛点 | 解决方案 | 预期价值 |
|
||||
|---------|---------|---------|---------|
|
||||
| **孩子** | "上次的小兔子怎么不认识我了?" 故事之间缺乏连续性。 | **角色一致性与记忆注入** 故事开头主动提及往事,角色性格延续。 | 建立情感依恋,提升沉浸感。 |
|
||||
| **家长** | "这App除了生成故事还能干嘛?" 无法感知产品的长期教育价值。 | **显性化成长轨迹** 词汇量统计、主题变化、成就徽章可视化。 | 提高付费意愿,提供社交货币。 |
|
||||
| **平台** | 用户用完即走,缺乏留存壁垒。 | **沉没成本与情感资产** 积累的记忆越多,越舍不得离开。 | 提升长期留存率 (LTV)。 |
|
||||
|
||||
---
|
||||
|
||||
## 一、功能模块
|
||||
|
||||
### 1.0 记忆分层模型
|
||||
|
||||
#### 层级 1: 核心档案 (Identity Layer)
|
||||
*性质:永久、静态、显性*
|
||||
- **数据**: 姓名、年龄、性别
|
||||
- **输入**: 家长在 Onboarding 阶段手动输入
|
||||
- **作用**: 决定故事的基础适龄性和称呼
|
||||
|
||||
#### 层级 2: 故事宇宙 (Universe Layer)
|
||||
*性质:长期、动态积累、半显性*
|
||||
- **主角设定**: 姓名、性格特征(勇敢/害羞)、外貌特征(戴眼镜/卷发)
|
||||
- **常驻配角**: 从随机故事中涌现出的固定伙伴(如"爱吃胡萝卜的松鼠奇奇")
|
||||
- **世界观**: 故事发生的背景(魔法森林、未来城市、海底世界)
|
||||
- **成就系统**: 孩子获得的虚拟奖励(勇气勋章、小小探险家)
|
||||
|
||||
#### 层级 3: 工作记忆 (Working Memory)
|
||||
*性质:短期、自动衰减、隐性*
|
||||
- **关键情节**: 最近 3 个故事的结局和核心冲突
|
||||
- **情感标记**: 孩子对特定内容的反应(根据"重播"、"跳过"推断)
|
||||
- **新学词汇**: 故事中出现的高级词汇
|
||||
|
||||
### 1.1 孩子档案系统 (Child Profile)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| 基础信息 | 显式 | 姓名、年龄、性别 |
|
||||
| 兴趣标签 | 显式+隐式 | 恐龙、公主、太空、动物等 |
|
||||
| 成长主题 | 显式 | 当前关注:勇气/分享/独立等 |
|
||||
| 阅读偏好 | 隐式 | 故事长度、风格、复杂度 |
|
||||
| 互动历史 | 隐式 | 喜欢的故事、跳过的故事 |
|
||||
|
||||
**数据来源**:
|
||||
- 显式: 家长主动填写
|
||||
- 隐式: 系统从使用行为中学习
|
||||
|
||||
### 1.2 故事宇宙记忆 (Story Universe)
|
||||
|
||||
跨故事保持连续性的元素:
|
||||
|
||||
| 元素 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| 主角设定 | 孩子的故事化身 | "小明是个爱冒险的男孩" |
|
||||
| 常驻角色 | 反复出现的配角 | 魔法猫咪"星星"、智慧老树 |
|
||||
| 世界观 | 故事发生的宇宙 | 梦幻森林、星际学院 |
|
||||
| 成就系统 | 主角的成长轨迹 | "学会了勇敢"、"交到新朋友" |
|
||||
|
||||
**记忆结构字段**:
|
||||
- `protagonist` / `recurring_characters` / `world_settings` / `achievements`(JSON 结构)
|
||||
- “延续上一个故事”默认选最近更新的宇宙(按 `updated_at` 倒序)
|
||||
|
||||
### 1.3 主动推送系统 (Proactive Push)
|
||||
|
||||
| 触发类型 | 条件 | 推送内容 |
|
||||
|----------|------|----------|
|
||||
| 时间触发 | 睡前时段 (19:00-21:00) | "今晚想听什么故事?" |
|
||||
| 事件触发 | 节日/生日 | 主题故事推荐 |
|
||||
| 行为触发 | 3天未使用 | 召回提醒 |
|
||||
| 成长触发 | 年龄变化 | 难度升级建议 |
|
||||
|
||||
**优先级与抑制**:
|
||||
- 优先级:事件 > 成长 > 行为 > 时间
|
||||
- 抑制:当天已推送不再触发;静默时段(21:00-09:00)延迟;用户关闭推送则不触发
|
||||
|
||||
---
|
||||
|
||||
## 二、用户故事
|
||||
|
||||
### US-1: 创建孩子档案
|
||||
```
|
||||
作为家长
|
||||
我想要创建孩子的专属档案
|
||||
以便系统生成更适合孩子的故事
|
||||
```
|
||||
|
||||
**验收标准**:
|
||||
- [ ] 可填写孩子基础信息(姓名、年龄、性别)
|
||||
- [ ] 可选择兴趣标签(多选)
|
||||
- [ ] 可设置当前成长主题
|
||||
- [ ] 支持多个孩子档案切换
|
||||
|
||||
### US-2: 故事角色延续
|
||||
```
|
||||
作为家长
|
||||
我想要故事中的角色能在新故事中再次出现
|
||||
以便孩子感受到故事的连续性
|
||||
```
|
||||
|
||||
**验收标准**:
|
||||
- [ ] 生成故事时可选择"延续上一个故事"
|
||||
- [ ] 系统自动带入主角设定和常驻角色
|
||||
- [ ] 新故事引用之前的"成就"
|
||||
|
||||
### US-3: 睡前故事提醒
|
||||
```
|
||||
作为家长
|
||||
我想要在睡前时段收到故事推荐
|
||||
以便养成固定的亲子阅读习惯
|
||||
```
|
||||
|
||||
**验收标准**:
|
||||
- [ ] 可设置提醒时间
|
||||
- [ ] 推送包含个性化故事建议
|
||||
- [ ] 可一键进入故事生成
|
||||
|
||||
---
|
||||
|
||||
## 三、数据模型
|
||||
|
||||
### 3.1 孩子档案表 (child_profiles)
|
||||
|
||||
```sql
|
||||
CREATE TABLE child_profiles (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID REFERENCES users(id),
|
||||
name VARCHAR(50) NOT NULL,
|
||||
birth_date DATE,
|
||||
gender VARCHAR(10),
|
||||
interests JSONB DEFAULT '[]',
|
||||
growth_themes JSONB DEFAULT '[]',
|
||||
reading_preferences JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
### 3.2 故事宇宙表 (story_universes)
|
||||
|
||||
```sql
|
||||
CREATE TABLE story_universes (
|
||||
id UUID PRIMARY KEY,
|
||||
child_profile_id UUID REFERENCES child_profiles(id),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
protagonist JSONB NOT NULL,
|
||||
recurring_characters JSONB DEFAULT '[]',
|
||||
world_settings JSONB DEFAULT '{}',
|
||||
achievements JSONB DEFAULT '[]',
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
### 3.3 推送配置表 (push_configs)
|
||||
|
||||
```sql
|
||||
CREATE TABLE push_configs (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID REFERENCES users(id),
|
||||
child_profile_id UUID REFERENCES child_profiles(id),
|
||||
push_time TIME,
|
||||
push_days INTEGER[], -- 0-6 表示周日到周六
|
||||
enabled BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
### 3.4 推送事件表 (push_events)
|
||||
|
||||
```sql
|
||||
CREATE TABLE push_events (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID NOT NULL,
|
||||
child_profile_id UUID NOT NULL,
|
||||
trigger_type VARCHAR(20) NOT NULL, -- time/event/behavior/growth
|
||||
sent_at TIMESTAMP NOT NULL,
|
||||
status VARCHAR(20) NOT NULL, -- sent/failed/suppressed
|
||||
reason TEXT
|
||||
);
|
||||
```
|
||||
|
||||
### 3.5 记忆条目表 (memory_items)
|
||||
|
||||
用于存储“可解释、可控”的记忆条目(兴趣偏好、成长主题、常驻角色、关键事件等),并支持时序衰减。
|
||||
|
||||
```sql
|
||||
CREATE TABLE memory_items (
|
||||
id UUID PRIMARY KEY,
|
||||
child_profile_id UUID NOT NULL,
|
||||
universe_id UUID,
|
||||
type VARCHAR(50) NOT NULL, -- interest/growth/character/event等
|
||||
value JSONB NOT NULL, -- 结构化内容
|
||||
base_weight FLOAT DEFAULT 1.0, -- 初始权重
|
||||
last_used_at TIMESTAMP, -- 最近使用时间
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
ttl_days INTEGER -- 可选:过期天数
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、API 设计
|
||||
|
||||
### 4.1 孩子档案 API
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/profiles` | 获取当前用户的所有孩子档案 |
|
||||
| POST | `/api/profiles` | 创建孩子档案 |
|
||||
| GET | `/api/profiles/{id}` | 获取单个档案详情 |
|
||||
| PUT | `/api/profiles/{id}` | 更新档案 |
|
||||
| DELETE | `/api/profiles/{id}` | 删除档案 |
|
||||
|
||||
### 4.2 故事宇宙 API
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/profiles/{id}/universes` | 获取孩子的故事宇宙列表 |
|
||||
| POST | `/api/profiles/{id}/universes` | 创建新宇宙 |
|
||||
| GET | `/api/universes/{id}` | 获取宇宙详情 |
|
||||
| PUT | `/api/universes/{id}` | 更新宇宙设定 |
|
||||
| POST | `/api/universes/{id}/achievements` | 添加成就 |
|
||||
|
||||
### 4.3 推送配置 API
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/push-configs` | 获取推送配置 |
|
||||
| PUT | `/api/push-configs` | 更新推送配置 |
|
||||
|
||||
---
|
||||
|
||||
## 五、Prompt 工程
|
||||
|
||||
### 5.1 带记忆的故事生成 Prompt
|
||||
|
||||
```
|
||||
你是一个专业的儿童故事作家。请为以下孩子创作一个故事:
|
||||
|
||||
【孩子档案】
|
||||
- 姓名: {child_name}
|
||||
- 年龄: {age}岁
|
||||
- 兴趣: {interests}
|
||||
- 当前成长主题: {growth_theme}
|
||||
|
||||
【故事宇宙】
|
||||
- 主角设定: {protagonist}
|
||||
- 常驻角色: {recurring_characters}
|
||||
- 世界观: {world_settings}
|
||||
- 已获成就: {achievements}
|
||||
|
||||
【本次创作要求】
|
||||
- 关键词: {keywords}
|
||||
- 延续之前的故事世界观
|
||||
- 让主角在故事中有新的成长
|
||||
|
||||
请创作一个适合{age}岁儿童的故事,约{word_count}字。
|
||||
```
|
||||
|
||||
### 5.2 智能开场白 (Memory Injection)
|
||||
|
||||
在生成新故事时,Prompt 必须包含一段"记忆唤醒"指令:
|
||||
- **示例**: "小明,还记得上周我们帮小松鼠找回了松果吗?今天,小松鼠带来了一位新朋友..."
|
||||
- **策略**: 提取权重最高的 Top 3 记忆注入 Prompt
|
||||
|
||||
### 5.3 成就提取 Prompt
|
||||
|
||||
```
|
||||
请分析以下故事,提取主角获得的成长/成就:
|
||||
|
||||
【故事内容】
|
||||
{story_content}
|
||||
|
||||
请以JSON格式返回:
|
||||
{
|
||||
"achievements": [
|
||||
{"type": "勇气", "description": "克服了对黑暗的恐惧"},
|
||||
{"type": "友谊", "description": "帮助了迷路的小兔子"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、前端设计
|
||||
|
||||
### 6.1 孩子档案页面
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ 我的宝贝 [+添加] │
|
||||
├─────────────────────────────────────┤
|
||||
│ ┌─────┐ ┌─────┐ ┌─────┐ │
|
||||
│ │ 👦 │ │ 👧 │ │ + │ │
|
||||
│ │小明 │ │小红 │ │添加 │ │
|
||||
│ │5岁 │ │3岁 │ │ │ │
|
||||
│ └─────┘ └─────┘ └─────┘ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 6.2 档案详情页
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ ← 小明的档案 [编辑] │
|
||||
├─────────────────────────────────────┤
|
||||
│ 基础信息 │
|
||||
│ 姓名: 小明 年龄: 5岁 性别: 男 │
|
||||
├─────────────────────────────────────┤
|
||||
│ 兴趣爱好 │
|
||||
│ [恐龙] [太空] [机器人] │
|
||||
├─────────────────────────────────────┤
|
||||
│ 成长主题 │
|
||||
│ ○ 勇气 ● 分享 ○ 独立 ○ 友谊 │
|
||||
├─────────────────────────────────────┤
|
||||
│ 故事宇宙 │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ 🌟 星际冒险 │ │
|
||||
│ │ 主角: 小明船长 │ │
|
||||
│ │ 伙伴: 机器人小七、外星猫咪 │ │
|
||||
│ │ 成就: 3个 │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 6.3 故事生成时选择档案
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ 为谁创作故事? │
|
||||
├─────────────────────────────────────┤
|
||||
│ ● 小明 (5岁) │
|
||||
│ ○ 小红 (3岁) │
|
||||
│ ○ 不使用档案 │
|
||||
├─────────────────────────────────────┤
|
||||
│ 选择故事宇宙 │
|
||||
│ ● 星际冒险 (延续上次) │
|
||||
│ ○ 创建新宇宙 │
|
||||
├─────────────────────────────────────┤
|
||||
│ [下一步: 输入关键词] │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、技术实现要点
|
||||
|
||||
### 7.1 隐式偏好学习
|
||||
|
||||
```python
|
||||
# 基于用户行为更新偏好
|
||||
async def update_implicit_preferences(
|
||||
child_id: UUID,
|
||||
story: Story,
|
||||
interaction: Interaction # 完整阅读/跳过/重复播放
|
||||
):
|
||||
profile = await get_child_profile(child_id)
|
||||
|
||||
if interaction == "completed":
|
||||
# 增加相关标签权重
|
||||
for tag in story.tags:
|
||||
profile.reading_preferences[tag] = \
|
||||
profile.reading_preferences.get(tag, 0) + 1
|
||||
elif interaction == "skipped":
|
||||
# 降低相关标签权重
|
||||
for tag in story.tags:
|
||||
profile.reading_preferences[tag] = \
|
||||
profile.reading_preferences.get(tag, 0) - 0.5
|
||||
```
|
||||
|
||||
### 7.2 成就自动提取
|
||||
|
||||
故事生成完成后,异步调用 LLM 提取成就(以 `type + description` 去重):
|
||||
|
||||
```python
|
||||
@celery.task
|
||||
async def extract_achievements(story_id: UUID, universe_id: UUID):
|
||||
story = await get_story(story_id)
|
||||
universe = await get_universe(universe_id)
|
||||
|
||||
achievements = await llm.extract_achievements(story.content)
|
||||
|
||||
universe.achievements.extend(achievements)
|
||||
await save_universe(universe)
|
||||
```
|
||||
|
||||
### 7.3 推送调度
|
||||
|
||||
使用 Celery Beat 定时检查推送:
|
||||
|
||||
```python
|
||||
@celery.task
|
||||
def check_push_notifications():
|
||||
current_time = datetime.now().time()
|
||||
current_day = datetime.now().weekday()
|
||||
|
||||
configs = PushConfig.query.filter(
|
||||
PushConfig.enabled == True,
|
||||
PushConfig.push_time <= current_time,
|
||||
current_day.in_(PushConfig.push_days)
|
||||
).all()
|
||||
|
||||
for config in configs:
|
||||
send_push_notification.delay(config.user_id, config.child_profile_id)
|
||||
```
|
||||
|
||||
**执行约束**:
|
||||
- 同一孩子每天最多 1 次推送
|
||||
- 推送前查询 `push_events` 去重,成功/抑制均需记录
|
||||
|
||||
### 7.4 时序衰减与记忆评分
|
||||
|
||||
**目标**:让“越新的记忆影响越大”,避免旧偏好长期干扰。
|
||||
|
||||
**默认实现(推荐)**:查询时动态计算分数,不直接修改数据库。
|
||||
- 记忆分数:`score = base_weight × decay(Δt)`
|
||||
- 衰减示例(分段):0-7 天 1.0,8-30 天 0.7,31-90 天 0.4,90 天后 0.2
|
||||
- 读取时按 `score` 排序,选 Top N 进入 Prompt
|
||||
|
||||
**可选实现**:定期批处理降权
|
||||
- 每日/每周批量更新 `base_weight`
|
||||
- 适合数据量大、读多写少的场景
|
||||
|
||||
**RAG 场景的衰减用法**:
|
||||
- 语义相似度分数 × 时间衰减
|
||||
- 可加时间窗口过滤(如仅取最近 90 天)
|
||||
|
||||
**删除策略(默认不删)**:
|
||||
- 默认只降权,不主动删除
|
||||
- 可选:对低权重且 180 天未使用的条目执行 TTL 清理
|
||||
|
||||
---
|
||||
|
||||
## 八、关键功能特性
|
||||
|
||||
### 8.1 成长时间轴 (Growth Timeline)
|
||||
|
||||
一个可视化的 H5 页面或 App 模块,以时间轴形式展示里程碑:
|
||||
- 🌟 **初次相遇**: 创建角色的第一天
|
||||
- 📖 **阅读打卡**: 累计阅读 10/50/100 本
|
||||
- 🏅 **获得成就**: 获得"诚实勋章"
|
||||
- 🧠 **能力解锁**: 第一次阅读"科幻"题材
|
||||
|
||||
### 8.2 成就仪式感 (Achievement Ceremony)
|
||||
|
||||
- **触发**: 故事生成并分析后,如果获得新成就
|
||||
- **表现**: 弹窗动画 + 音效 + "恭喜获得 [勇气] 徽章"
|
||||
- **分享**: 允许生成带二维码的成就海报
|
||||
|
||||
---
|
||||
|
||||
## 九、记忆类型扩展
|
||||
|
||||
| 类型 Key | 描述 | 来源 | 过期策略 |
|
||||
|---------|------|------|---------|
|
||||
| `recent_story` | 最近读过的故事梗概 | 阅读事件 | 30天衰减 |
|
||||
| `favorite_character` | 孩子喜欢的角色 | 重播/高评分 | 长期有效 |
|
||||
| `scary_element` | 孩子害怕/不喜欢的元素 | 跳过/负反馈 | 长期有效 (避雷) |
|
||||
| `vocabulary_growth` | 新掌握的词汇 | 故事分析 | 90天衰减 |
|
||||
| `emotional_highlight` | 高光时刻 (如: 特别开心的情节) | 互动数据 | 60天衰减 |
|
||||
|
||||
---
|
||||
|
||||
## 十、里程碑
|
||||
|
||||
### Phase 1: 基础建设 (v0.3.0)
|
||||
- [x] 数据库 `MemoryItem` 表 (已存在)
|
||||
- [ ] 扩展 `MemoryItem` 类型字段,支持更多维度
|
||||
- [ ] 优化 `_build_memory_context`,支持更自然的 Prompt 注入
|
||||
- [ ] 前端:简单的"近期回忆"展示列表
|
||||
|
||||
### M1: 孩子档案基础
|
||||
- [ ] 数据库模型
|
||||
- [ ] CRUD API
|
||||
- [ ] 前端档案管理页面
|
||||
- [ ] 故事生成时选择档案
|
||||
|
||||
### M2: 故事宇宙
|
||||
- [ ] 宇宙数据模型
|
||||
- [ ] Prompt 集成
|
||||
- [ ] 成就自动提取
|
||||
- [ ] 前端宇宙管理
|
||||
|
||||
### M3: 主动推送
|
||||
- [ ] 推送配置 API
|
||||
- [ ] Celery Beat 调度
|
||||
- [ ] 推送通知集成 (Web Push / 微信)
|
||||
|
||||
### M4: 隐式学习
|
||||
- [ ] 行为埋点
|
||||
- [ ] 偏好学习算法
|
||||
- [ ] 推荐优化
|
||||
|
||||
### Phase 2: 可视化与成就 (v0.4.0)
|
||||
- [ ] 实现"成就提取器" (Achievement Extractor) 的闭环通知
|
||||
- [ ] 前端:开发"我的成就"和"成长时间轴"页面
|
||||
- [ ] 增加故事开场白的动态生成逻辑
|
||||
|
||||
### Phase 3: 深度智能 (v0.5.0+)
|
||||
- [ ] 引入向量数据库,实现基于语义的记忆检索 (不仅是时间最近)
|
||||
- [ ] 情感分析模型:分析用户行为推断情感倾向
|
||||
|
||||
---
|
||||
|
||||
## 十一、风险与应对
|
||||
|
||||
| 风险 | 影响 | 应对 |
|
||||
|------|------|------|
|
||||
| 隐私合规 | 高 | 儿童数据加密存储,家长授权机制 |
|
||||
| 推送骚扰 | 中 | 默认关闭,用户主动开启 |
|
||||
| 记忆膨胀 | 低 | 定期清理旧数据,限制宇宙数量 |
|
||||
|
||||
---
|
||||
|
||||
## 十二、相关文档
|
||||
|
||||
- [孩子档案数据模型](./CHILD-PROFILE-MODEL.md)
|
||||
- [故事宇宙记忆结构](./STORY-UNIVERSE-MODEL.md)
|
||||
- [主动推送触发规则](./PUSH-TRIGGER-RULES.md)
|
||||
@@ -1,177 +0,0 @@
|
||||
# 记忆与个性化技术方案建议(PRD 讨论稿)
|
||||
|
||||
> 目标:给 DreamWeaver 的“记忆与个性化”提供可落地的技术路径与产品取舍依据,用于 PRD 细化。
|
||||
|
||||
---
|
||||
|
||||
## 1. 总体结论(推荐方案)
|
||||
|
||||
**v1 推荐:混合方案(结构化 DB + 轻量语义检索)**
|
||||
|
||||
- **DB** 作为权威事实与可解释记忆(孩子档案、宇宙设定、成就、偏好权重)。
|
||||
- **RAG** 用于非结构化内容(故事摘要、互动摘要、近期期望),辅助个性化提示词。
|
||||
|
||||
**原因**
|
||||
- 纯 DB 可控但缺乏语义弹性;纯 RAG 难以稳定控制与审计。
|
||||
- 混合方案能在“可解释 + 个性化”之间取到最佳平衡。
|
||||
|
||||
---
|
||||
|
||||
## 2. DB vs RAG:技术与产品对比
|
||||
|
||||
### 2.1 DB(结构化记忆)
|
||||
|
||||
**适用内容**
|
||||
- 孩子档案(基础信息)
|
||||
- 兴趣标签与成长主题
|
||||
- 故事宇宙设定(主角、世界观、常驻角色)
|
||||
- 成就(可审核、可追溯)
|
||||
|
||||
**优点**
|
||||
- 高可解释性
|
||||
- 变更可追踪、可回滚
|
||||
- 便于用户管理(家长可编辑)
|
||||
|
||||
**缺点**
|
||||
- 灵活性不足
|
||||
- 难以覆盖“隐性偏好”(比如叙事风格喜好)
|
||||
|
||||
### 2.2 RAG(语义记忆)
|
||||
|
||||
**适用内容**
|
||||
- 故事摘要
|
||||
- 互动摘要(“最近更喜欢冒险故事”)
|
||||
- 非结构化日志
|
||||
|
||||
**优点**
|
||||
- 具备语义召回能力
|
||||
- 适合挖掘“隐含偏好”
|
||||
|
||||
**缺点**
|
||||
- 可解释性弱
|
||||
- 成本与性能压力大
|
||||
- 隐私风险更高
|
||||
|
||||
---
|
||||
|
||||
## 3. 时序性与记忆衰减(建议必须有)
|
||||
|
||||
**核心观点**:孩子兴趣会随时间变化,必须引入时间衰减。
|
||||
|
||||
**做法建议**
|
||||
- 所有记忆项带 `created_at` / `last_used_at`
|
||||
- 引入权重衰减模型:
|
||||
- 近 7 天:高权重
|
||||
- 30 天:中权重
|
||||
- 90 天:低权重
|
||||
- 超过 90 天:降权或淘汰
|
||||
|
||||
**价值**
|
||||
- 避免旧偏好过度影响新故事
|
||||
- 体现成长与兴趣演变
|
||||
|
||||
---
|
||||
|
||||
## 4. 分层记忆(建议引入)
|
||||
|
||||
建议采用三层结构:
|
||||
|
||||
### 4.1 短期记忆(Session)
|
||||
- 当前生成上下文(关键词、选定档案/宇宙)
|
||||
- 生命周期:仅本次请求有效
|
||||
|
||||
### 4.2 中期记忆(近期偏好)
|
||||
- 最近 5-10 次故事生成/阅读偏好
|
||||
- 生命周期:30-60 天
|
||||
|
||||
### 4.3 长期记忆(稳定事实)
|
||||
- 档案、宇宙、核心兴趣
|
||||
- 生命周期:长期可编辑
|
||||
|
||||
**价值**
|
||||
- 既保留稳定设定,又能捕捉近期变化
|
||||
|
||||
---
|
||||
|
||||
## 5. Agent 动态判断是否写入记忆
|
||||
|
||||
**建议:规则优先 + 模型辅助**
|
||||
|
||||
流程示例:
|
||||
1. 命中规则(如完整阅读/重复播放)→ 进入候选
|
||||
2. LLM 抽取结构化信息 + 置信度
|
||||
3. 置信度不足 → 不写入
|
||||
|
||||
**优点**
|
||||
- 避免模型“乱记忆”
|
||||
- 降低噪声,提高记忆质量
|
||||
|
||||
---
|
||||
|
||||
## 6. 推荐的记忆数据结构
|
||||
|
||||
### 6.1 结构化表(DB)
|
||||
|
||||
- `child_profiles`:基础信息、兴趣、成长主题
|
||||
- `story_universes`:主角、角色、世界观、成就
|
||||
- `reading_events`:阅读/跳过/重播行为日志
|
||||
- `memory_items`:抽象记忆表(type, value, confidence, ttl)
|
||||
|
||||
### 6.2 语义检索(RAG)
|
||||
|
||||
- 存储内容:故事摘要、成就摘要、行为总结
|
||||
- 向量库:**pgvector**(成本低、易部署)
|
||||
- 检索过滤:`child_id` / `universe_id` / 时间窗口
|
||||
|
||||
---
|
||||
|
||||
## 7. 关键产品问题(需明确)
|
||||
|
||||
1) **记忆是否可编辑**
|
||||
- 家长是否能查看、修改、删除系统记忆?
|
||||
|
||||
2) **跨孩子隔离**
|
||||
- 同账号多孩子的记忆是否完全隔离(推荐隔离)
|
||||
|
||||
3) **隐私与合规**
|
||||
- 哪些数据进入记忆?是否脱敏?是否加密?
|
||||
|
||||
4) **性能与成本**
|
||||
- RAG 查询是否影响生成时延?
|
||||
- 是否需要缓存与批量检索?
|
||||
|
||||
5) **效果评估**
|
||||
- 记忆是否提高故事满意度?
|
||||
- 需要 A/B 或指标体系吗?
|
||||
|
||||
---
|
||||
|
||||
## 8. 推荐实施路线
|
||||
|
||||
### v1(1-2 个月)
|
||||
- DB 记忆为主,RAG 只做轻量补充
|
||||
- 引入时序衰减
|
||||
- 记忆来源:用户显式输入 + 行为日志
|
||||
|
||||
### v2(2-3 个月)
|
||||
- 引入 Agent 记忆抽取与置信度
|
||||
- 记忆管理界面(家长可编辑)
|
||||
- 更精细的个性化推荐
|
||||
|
||||
---
|
||||
|
||||
## 9. 需要确认的决定点
|
||||
|
||||
- 是否采用混合方案(DB + RAG)
|
||||
- RAG 的检索范围(故事摘要 / 行为摘要 / 成就)
|
||||
- 记忆分层与衰减规则
|
||||
- Agent 记忆写入规则与阈值
|
||||
- 家长可见/可控的记忆管理策略
|
||||
|
||||
---
|
||||
|
||||
如确认以上方向,我可以进一步输出:
|
||||
- PRD 里的“记忆系统”完整章节
|
||||
- 数据模型(含字段 + 时序衰减)
|
||||
- 交互与界面草案
|
||||
- 后端实现拆解(任务清单 + 里程碑)
|
||||
@@ -1,129 +0,0 @@
|
||||
# 主动推送触发规则
|
||||
|
||||
## 概述
|
||||
|
||||
主动推送用于在合适的时间为家长提供个性化故事建议,提升使用频次与亲子阅读习惯。推送默认关闭,需家长开启并配置时间。
|
||||
|
||||
---
|
||||
|
||||
## 一、数据输入
|
||||
|
||||
- **孩子档案**: `child_profiles`(年龄、兴趣、成长主题)
|
||||
- **故事数据**: `stories`(最近生成/阅读时间、主题标签)
|
||||
- **推送配置**: `push_configs`(时间、周期、开关)
|
||||
- **节日与生日**: 预置日历 + `birth_date`
|
||||
- **行为事件**: 阅读/播放/跳过等行为埋点
|
||||
|
||||
---
|
||||
|
||||
## 二、触发类型与规则
|
||||
|
||||
### 2.1 时间触发(睡前)
|
||||
- 条件:当前时间落在用户设定 `push_time` 附近(建议 ±30 分钟)。
|
||||
- 频率:同一孩子每天最多 1 次。
|
||||
- 示例:19:00-21:00 之间推送“今晚想听什么故事?”
|
||||
|
||||
### 2.2 事件触发(节日/生日)
|
||||
- 条件:
|
||||
- 生日:`birth_date` 月日与当天一致。
|
||||
- 节日:命中节日清单(如儿童节、中秋节等)。
|
||||
- 频率:当天仅推送 1 次,优先级高于时间触发。
|
||||
|
||||
### 2.3 行为触发(召回)
|
||||
- 条件:最近 3 天无故事生成或阅读行为。
|
||||
- 频率:每 3 天最多 1 次,避免频繁打扰。
|
||||
|
||||
### 2.4 成长触发(年龄变化)
|
||||
- 条件:年龄跨越关键节点(如 4→5 岁)。
|
||||
- 频率:每次年龄变化仅触发一次。
|
||||
- 目的:推荐难度升级或新的成长主题。
|
||||
|
||||
---
|
||||
|
||||
## 三、优先级与抑制规则
|
||||
|
||||
**优先级顺序**(从高到低):
|
||||
1. 事件触发
|
||||
2. 成长触发
|
||||
3. 行为触发
|
||||
4. 时间触发
|
||||
|
||||
**抑制规则**:
|
||||
- 当天已推送则不再触发其他类型。
|
||||
- 若在静默时间(21:00-09:00)触发,则延迟至下一个允许窗口。
|
||||
- 用户关闭推送或未配置推送时间时,不触发。
|
||||
|
||||
---
|
||||
|
||||
## 四、个性化内容策略
|
||||
|
||||
- **兴趣标签**: 引用孩子的兴趣标签生成主题。
|
||||
- **成长主题**: 优先匹配当前成长主题。
|
||||
- **历史偏好**: 参考最近故事的标签与完成度。
|
||||
|
||||
**示例模板**:
|
||||
- “今晚给{child_name}讲一个关于{interest}的故事,好吗?”
|
||||
- “{child_name}最近在学习{growth_theme},我准备了一个新故事。”
|
||||
|
||||
---
|
||||
|
||||
## 五、调度实现建议
|
||||
|
||||
使用 Celery Beat 每 5-10 分钟执行一次规则检查:
|
||||
|
||||
```python
|
||||
@celery.task
|
||||
def check_push_notifications():
|
||||
now = datetime.now(local_tz)
|
||||
configs = get_enabled_configs(now)
|
||||
|
||||
for config in configs:
|
||||
if has_sent_today(config.child_profile_id):
|
||||
continue
|
||||
|
||||
trigger = select_trigger(config, now)
|
||||
if trigger:
|
||||
send_push_notification(config.user_id, config.child_profile_id, trigger)
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
- 需要记录每日推送日志用于去重。
|
||||
- 优先级触发时应立即标记已发送。
|
||||
|
||||
---
|
||||
|
||||
## 六、日志与度量
|
||||
|
||||
建议增加 `push_events` 事件表用于统计与去重:
|
||||
|
||||
```sql
|
||||
CREATE TABLE push_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL,
|
||||
child_profile_id UUID NOT NULL,
|
||||
trigger_type VARCHAR(20) NOT NULL, -- time/event/behavior/growth
|
||||
sent_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
status VARCHAR(20) NOT NULL, -- sent/failed/suppressed
|
||||
reason TEXT
|
||||
);
|
||||
```
|
||||
|
||||
核心指标:
|
||||
- Push 发送成功率
|
||||
- 打开率(CTA 点击)
|
||||
- 触发分布占比
|
||||
|
||||
---
|
||||
|
||||
## 七、安全与合规
|
||||
|
||||
- **默认关闭**,需家长显式开启。
|
||||
- 支持一键关闭或设定免打扰时段。
|
||||
- 遵循儿童隐私合规要求,最小化推送内容敏感信息。
|
||||
|
||||
---
|
||||
|
||||
## 八、相关文档
|
||||
|
||||
- [记忆智能系统 PRD](./MEMORY-INTELLIGENCE-PRD.md)
|
||||
- [孩子档案数据模型](./CHILD-PROFILE-MODEL.md)
|
||||
@@ -1,231 +0,0 @@
|
||||
# 故事宇宙记忆结构
|
||||
|
||||
## 概述
|
||||
|
||||
故事宇宙用于在多次故事生成中保持角色、世界观与成长成就的连续性。每个孩子档案可以拥有多个宇宙,故事生成时可选择“延续上一个故事”,系统自动带入宇宙设定。
|
||||
|
||||
---
|
||||
|
||||
## 一、数据库模型
|
||||
|
||||
### 1.1 主表: story_universes
|
||||
|
||||
```sql
|
||||
CREATE TABLE story_universes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
child_profile_id UUID NOT NULL REFERENCES child_profiles(id) ON DELETE CASCADE,
|
||||
|
||||
-- 宇宙基础
|
||||
name VARCHAR(100) NOT NULL,
|
||||
|
||||
-- 记忆结构
|
||||
protagonist JSONB NOT NULL, -- 主角设定
|
||||
recurring_characters JSONB DEFAULT '[]',
|
||||
world_settings JSONB DEFAULT '{}',
|
||||
achievements JSONB DEFAULT '[]',
|
||||
|
||||
-- 时间戳
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_story_universes_child_id ON story_universes(child_profile_id);
|
||||
CREATE INDEX idx_story_universes_updated_at ON story_universes(updated_at);
|
||||
```
|
||||
|
||||
### 1.2 JSON 结构示例
|
||||
|
||||
**protagonist**
|
||||
```json
|
||||
{
|
||||
"name": "小明",
|
||||
"role": "星际船长",
|
||||
"traits": ["勇敢", "好奇"],
|
||||
"goal": "寻找失落的星球",
|
||||
"backstory": "来自地球的探险家"
|
||||
}
|
||||
```
|
||||
|
||||
**recurring_characters**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "星星",
|
||||
"role": "魔法猫咪",
|
||||
"traits": ["聪明", "调皮"],
|
||||
"relation": "伙伴",
|
||||
"first_story_id": "story-uuid"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**world_settings**
|
||||
```json
|
||||
{
|
||||
"world_name": "梦幻森林",
|
||||
"era": "童话时代",
|
||||
"locations": ["彩虹河", "月光山"],
|
||||
"rules": ["动物会说话", "星星会指路"],
|
||||
"tone": "温暖治愈"
|
||||
}
|
||||
```
|
||||
|
||||
**achievements**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"type": "勇气",
|
||||
"description": "克服了对黑暗的恐惧",
|
||||
"story_id": "story-uuid",
|
||||
"achieved_at": "2025-01-10T12:00:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、SQLAlchemy 模型
|
||||
|
||||
```python
|
||||
# backend/app/db/models.py
|
||||
|
||||
from sqlalchemy import Column, String, ForeignKey, JSON
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
import uuid
|
||||
|
||||
class StoryUniverse(Base):
|
||||
__tablename__ = "story_universes"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
child_profile_id = Column(UUID(as_uuid=True), ForeignKey("child_profiles.id", ondelete="CASCADE"), nullable=False)
|
||||
|
||||
name = Column(String(100), nullable=False)
|
||||
protagonist = Column(JSON, nullable=False)
|
||||
recurring_characters = Column(JSON, default=list)
|
||||
world_settings = Column(JSON, default=dict)
|
||||
achievements = Column(JSON, default=list)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
child_profile = relationship("ChildProfile", back_populates="story_universes")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、Pydantic Schema
|
||||
|
||||
```python
|
||||
# backend/app/schemas/story_universe.py
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
class StoryUniverseCreate(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=100)
|
||||
protagonist: dict[str, Any]
|
||||
recurring_characters: list[dict[str, Any]] = Field(default_factory=list)
|
||||
world_settings: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
class StoryUniverseUpdate(BaseModel):
|
||||
name: str | None = Field(None, min_length=1, max_length=100)
|
||||
protagonist: dict[str, Any] | None = None
|
||||
recurring_characters: list[dict[str, Any]] | None = None
|
||||
world_settings: dict[str, Any] | None = None
|
||||
|
||||
class StoryUniverseResponse(BaseModel):
|
||||
id: UUID
|
||||
child_profile_id: UUID
|
||||
name: str
|
||||
protagonist: dict[str, Any]
|
||||
recurring_characters: list[dict[str, Any]]
|
||||
world_settings: dict[str, Any]
|
||||
achievements: list[dict[str, Any]]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、API 约定
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/profiles/{id}/universes` | 获取孩子的故事宇宙列表 |
|
||||
| POST | `/api/profiles/{id}/universes` | 创建新宇宙 |
|
||||
| GET | `/api/universes/{id}` | 获取宇宙详情 |
|
||||
| PUT | `/api/universes/{id}` | 更新宇宙设定 |
|
||||
| POST | `/api/universes/{id}/achievements` | 添加成就 |
|
||||
|
||||
---
|
||||
|
||||
## 五、业务规则
|
||||
|
||||
- **延续故事**: “延续上一个故事”默认选最近更新的宇宙(按 `updated_at` 倒序)。
|
||||
- **成就追加**: 新成就追加到 `achievements`,以 `type + description` 去重。
|
||||
- **成长轨迹**: 成就保留顺序,优先展示最新项。
|
||||
|
||||
---
|
||||
|
||||
## 六、Prompt 集成
|
||||
|
||||
当选择宇宙时,生成 Prompt 需带入宇宙记忆:
|
||||
|
||||
```
|
||||
【故事宇宙】
|
||||
- 主角设定: {protagonist}
|
||||
- 常驻角色: {recurring_characters}
|
||||
- 世界观: {world_settings}
|
||||
- 已获成就: {achievements}
|
||||
```
|
||||
|
||||
未选择宇宙时,提示词忽略该块,避免混淆。
|
||||
|
||||
---
|
||||
|
||||
## 七、数据迁移示例
|
||||
|
||||
```python
|
||||
# backend/alembic/versions/xxx_add_story_universes.py
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
"story_universes",
|
||||
sa.Column("id", sa.UUID(), nullable=False),
|
||||
sa.Column("child_profile_id", sa.UUID(), nullable=False),
|
||||
sa.Column("name", sa.String(100), nullable=False),
|
||||
sa.Column("protagonist", sa.JSON(), nullable=False),
|
||||
sa.Column("recurring_characters", sa.JSON(), server_default='[]'),
|
||||
sa.Column("world_settings", sa.JSON(), server_default='{}'),
|
||||
sa.Column("achievements", sa.JSON(), server_default='[]'),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
sa.ForeignKeyConstraint(["child_profile_id"], ["child_profiles.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("idx_story_universes_child_id", "story_universes", ["child_profile_id"])
|
||||
op.create_index("idx_story_universes_updated_at", "story_universes", ["updated_at"])
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_index("idx_story_universes_updated_at")
|
||||
op.drop_index("idx_story_universes_child_id")
|
||||
op.drop_table("story_universes")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、权限与安全
|
||||
|
||||
- 宇宙数据必须通过 `child_profile_id` 归属校验,确保仅拥有者可访问。
|
||||
- 删除用户或档案时,级联删除所有宇宙数据。
|
||||
|
||||
---
|
||||
|
||||
## 九、相关文档
|
||||
|
||||
- [孩子档案数据模型](./CHILD-PROFILE-MODEL.md)
|
||||
- [记忆智能系统 PRD](./MEMORY-INTELLIGENCE-PRD.md)
|
||||
@@ -1,130 +0,0 @@
|
||||
# DreamWeaver 产品愿景与全流程规划
|
||||
|
||||
## 一、产品定位
|
||||
|
||||
### 1.1 愿景
|
||||
**梦语织机** - 为 3-8 岁儿童打造的 AI 故事创作平台,让每个孩子都能拥有专属的成长故事。
|
||||
|
||||
### 1.2 核心价值
|
||||
| 维度 | 价值主张 |
|
||||
|------|----------|
|
||||
| 个性化 | 基于关键词/主角定制,每个故事独一无二 |
|
||||
| 教育性 | 融入成长主题(勇气、友谊、诚实等) |
|
||||
| 沉浸感 | AI 封面 + 语音朗读,多感官体验 |
|
||||
| 亲子互动 | 家长参与创作,增进亲子关系 |
|
||||
|
||||
### 1.3 目标用户
|
||||
**主要用户:家长(25-40岁)**
|
||||
- 需求:为孩子找到有教育意义的睡前故事
|
||||
- 痛点:市面故事千篇一律,缺乏个性化
|
||||
- 场景:睡前、旅途、周末亲子时光
|
||||
|
||||
**次要用户:幼儿园/早教机构**
|
||||
- 需求:批量生成教学故事素材
|
||||
- 痛点:内容制作成本高
|
||||
|
||||
---
|
||||
|
||||
## 二、竞品分析
|
||||
|
||||
| 产品 | 优势 | 劣势 | 我们的差异化 |
|
||||
|------|------|------|--------------|
|
||||
| 凯叔讲故事 | 内容丰富、品牌知名 | 无个性化、订阅贵 | AI 个性化生成 |
|
||||
| 喜马拉雅儿童 | 海量音频、多平台 | 内容同质化 | 用户参与创作 |
|
||||
| ChatGPT | AI 能力强 | 非儿童专属、无配套 | 垂直场景优化 |
|
||||
| Midjourney | 图像质量高 | 无故事整合 | 故事+图像+音频一体 |
|
||||
|
||||
---
|
||||
|
||||
## 三、产品路线图
|
||||
|
||||
### Phase 1: MVP 完善 ✅ 已完成
|
||||
- [x] 关键词生成故事
|
||||
- [x] 故事润色增强
|
||||
- [x] AI 封面生成
|
||||
- [x] 语音朗读
|
||||
- [x] 故事收藏管理
|
||||
- [x] OAuth 登录
|
||||
- [x] 工程鲁棒性改进
|
||||
|
||||
### Phase 2: 体验增强
|
||||
| 功能 | 优先级 | 用户价值 |
|
||||
|------|--------|----------|
|
||||
| 故事编辑 | P0 | 用户可修改 AI 生成内容 |
|
||||
| 角色定制 | P0 | 孩子成为故事主角 |
|
||||
| 故事续写 | P1 | 形成系列故事 |
|
||||
| 多语言支持 | P1 | 英文故事学习 |
|
||||
| 故事分享 | P1 | 社交传播 |
|
||||
|
||||
### Phase 3: 供应商平台化
|
||||
| 功能 | 优先级 | 技术价值 |
|
||||
|------|--------|----------|
|
||||
| 供应商管理后台 | P0 | 可视化配置 AI 供应商 |
|
||||
| 适配器插件化 | P0 | 新供应商零代码接入 |
|
||||
| 供应商健康监控 | P1 | 自动故障转移 |
|
||||
| A/B 测试框架 | P1 | 供应商效果对比 |
|
||||
| 成本分析面板 | P2 | API 调用成本追踪 |
|
||||
|
||||
### Phase 4: 社区与增长
|
||||
| 功能 | 优先级 | 增长价值 |
|
||||
|------|--------|----------|
|
||||
| 故事广场 | P0 | 内容发现 |
|
||||
| 点赞/收藏 | P0 | 社区互动 |
|
||||
| 创作者主页 | P1 | 用户留存 |
|
||||
| 故事模板 | P1 | 降低创作门槛 |
|
||||
|
||||
### Phase 5: 商业化
|
||||
| 功能 | 优先级 | 商业价值 |
|
||||
|------|--------|----------|
|
||||
| 会员订阅 | P0 | 核心收入 |
|
||||
| 故事导出 | P0 | 增值服务 |
|
||||
| 实体书打印 | P1 | 高客单价 |
|
||||
| API 开放 | P2 | B 端收入 |
|
||||
|
||||
---
|
||||
|
||||
## 四、核心指标 (KPIs)
|
||||
|
||||
### 4.1 用户指标
|
||||
| 指标 | 定义 | 目标 |
|
||||
|------|------|------|
|
||||
| DAU | 日活跃用户 | Phase 2: 1000+ |
|
||||
| 留存率 | 次日/7日/30日 | 40%/25%/15% |
|
||||
| 创作转化率 | 访问→创作 | 30%+ |
|
||||
|
||||
### 4.2 业务指标
|
||||
| 指标 | 定义 | 目标 |
|
||||
|------|------|------|
|
||||
| 故事生成量 | 日均生成数 | 5000+ |
|
||||
| 分享率 | 故事被分享比例 | 10%+ |
|
||||
| 付费转化率 | 免费→付费 | 5%+ |
|
||||
|
||||
### 4.3 技术指标
|
||||
| 指标 | 定义 | 目标 |
|
||||
|------|------|------|
|
||||
| API 成功率 | 供应商调用成功率 | 99%+ |
|
||||
| 响应时间 | 故事生成 P95 | <30s |
|
||||
| 成本/故事 | 单个故事 API 成本 | <$0.05 |
|
||||
|
||||
---
|
||||
|
||||
## 五、风险与应对
|
||||
|
||||
| 风险 | 影响 | 概率 | 应对策略 |
|
||||
|------|------|------|----------|
|
||||
| AI 生成内容不当 | 高 | 中 | 内容审核 + 家长控制 + 敏感词过滤 |
|
||||
| API 成本过高 | 高 | 中 | 多供应商比价 + 缓存优化 + 分级限流 |
|
||||
| 供应商服务中断 | 高 | 低 | 多供应商冗余 + 自动故障转移 |
|
||||
| 用户增长缓慢 | 中 | 中 | 社区运营 + 分享裂变 + SEO |
|
||||
| 竞品模仿 | 低 | 高 | 快速迭代 + 深耕垂直 + 数据壁垒 |
|
||||
|
||||
---
|
||||
|
||||
## 六、下一步讨论议题
|
||||
|
||||
1. **供应商平台化架构** - 如何设计插件化的适配器系统?
|
||||
2. **Phase 2 功能优先级** - 先做哪个功能?
|
||||
3. **技术选型** - nanobanana vs flux vs 其他图像供应商?
|
||||
4. **商业模式** - 免费/付费边界在哪里?
|
||||
|
||||
请确认以上产品愿景是否符合预期,我们再深入讨论供应商平台化的技术架构。
|
||||
@@ -1,677 +0,0 @@
|
||||
# RFC: 供应商平台化架构设计
|
||||
|
||||
## 背景
|
||||
|
||||
### 当前问题
|
||||
1. **硬编码适配器**: `gemini`, `flux`, `minimax` 写死在代码中
|
||||
2. **新供应商需改代码**: 接入 nanobanana 等新供应商需要修改 `provider_router.py`
|
||||
3. **无法动态切换**: 供应商故障时需要重启服务
|
||||
4. **缺乏监控**: 不知道哪个供应商更快、更便宜、更稳定
|
||||
|
||||
### 目标
|
||||
- **零代码接入**: 通过后台配置即可接入新供应商
|
||||
- **动态切换**: 运行时切换供应商,无需重启
|
||||
- **智能路由**: 基于成本、延迟、成功率自动选择最优供应商
|
||||
- **可观测性**: 供应商健康状态、成本、性能一目了然
|
||||
|
||||
---
|
||||
|
||||
## 架构设计
|
||||
|
||||
### 1. 整体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Admin Dashboard │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
|
||||
│ │ 供应商管理 │ │ 健康监控 │ │ 成本分析 │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Provider Router │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 路由策略: Priority → Weight → Health → Cost │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌───────────┬───────────┬───────────┬───────────────────┐ │
|
||||
│ │ Adapter │ Adapter │ Adapter │ Adapter │ │
|
||||
│ │ Registry │ Factory │ Health │ Metrics │ │
|
||||
│ └───────────┴───────────┴───────────┴───────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────┼─────────────────────┐
|
||||
▼ ▼ ▼
|
||||
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
||||
│ Text Adapters │ │ Image Adapters│ │ TTS Adapters │
|
||||
├───────────────┤ ├───────────────┤ ├───────────────┤
|
||||
│ • Gemini │ │ • Flux │ │ • Minimax │
|
||||
│ • OpenAI │ │ • Nanobanana │ │ • ElevenLabs │
|
||||
│ • Claude │ │ • DALL-E │ │ • Azure TTS │
|
||||
│ • Qwen │ │ • Midjourney │ │ • Google TTS │
|
||||
└───────────────┘ └───────────────┘ └───────────────┘
|
||||
```
|
||||
|
||||
### 2. 核心组件
|
||||
|
||||
#### 2.1 Adapter 接口定义
|
||||
|
||||
```python
|
||||
# 统一适配器接口
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TypeVar, Generic
|
||||
from pydantic import BaseModel
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class AdapterConfig(BaseModel):
|
||||
"""适配器配置基类"""
|
||||
api_key: str
|
||||
api_base: str | None = None
|
||||
model: str | None = None
|
||||
timeout_ms: int = 60000
|
||||
max_retries: int = 3
|
||||
|
||||
class BaseAdapter(ABC, Generic[T]):
|
||||
"""适配器基类"""
|
||||
|
||||
# 适配器元信息
|
||||
adapter_type: str # text / image / tts
|
||||
adapter_name: str # gemini / flux / minimax
|
||||
|
||||
def __init__(self, config: AdapterConfig):
|
||||
self.config = config
|
||||
|
||||
@abstractmethod
|
||||
async def execute(self, **kwargs) -> T:
|
||||
"""执行适配器逻辑"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def health_check(self) -> bool:
|
||||
"""健康检查"""
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def estimated_cost(self) -> float:
|
||||
"""预估单次调用成本 (USD)"""
|
||||
pass
|
||||
```
|
||||
|
||||
#### 2.2 适配器注册表
|
||||
|
||||
```python
|
||||
# 适配器注册表 - 支持动态注册
|
||||
class AdapterRegistry:
|
||||
"""适配器注册表"""
|
||||
|
||||
_adapters: dict[str, type[BaseAdapter]] = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, adapter_type: str, adapter_name: str):
|
||||
"""装饰器: 注册适配器"""
|
||||
def decorator(adapter_class: type[BaseAdapter]):
|
||||
key = f"{adapter_type}:{adapter_name}"
|
||||
cls._adapters[key] = adapter_class
|
||||
return adapter_class
|
||||
return decorator
|
||||
|
||||
@classmethod
|
||||
def get(cls, adapter_type: str, adapter_name: str) -> type[BaseAdapter] | None:
|
||||
key = f"{adapter_type}:{adapter_name}"
|
||||
return cls._adapters.get(key)
|
||||
|
||||
@classmethod
|
||||
def list_adapters(cls, adapter_type: str | None = None) -> list[str]:
|
||||
"""列出所有已注册的适配器"""
|
||||
if adapter_type:
|
||||
return [k for k in cls._adapters if k.startswith(f"{adapter_type}:")]
|
||||
return list(cls._adapters.keys())
|
||||
```
|
||||
|
||||
#### 2.3 适配器实现示例
|
||||
|
||||
```python
|
||||
# 图像适配器示例: Nanobanana
|
||||
@AdapterRegistry.register("image", "nanobanana")
|
||||
class NanobananapAdapter(BaseAdapter[str]):
|
||||
adapter_type = "image"
|
||||
adapter_name = "nanobanana"
|
||||
|
||||
async def execute(self, prompt: str, **kwargs) -> str:
|
||||
"""生成图片,返回 URL"""
|
||||
async with httpx.AsyncClient(timeout=self.config.timeout_ms / 1000) as client:
|
||||
response = await client.post(
|
||||
f"{self.config.api_base}/generate",
|
||||
json={"prompt": prompt, "model": self.config.model},
|
||||
headers={"Authorization": f"Bearer {self.config.api_key}"},
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()["image_url"]
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
# 简单的健康检查
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5) as client:
|
||||
response = await client.get(f"{self.config.api_base}/health")
|
||||
return response.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@property
|
||||
def estimated_cost(self) -> float:
|
||||
return 0.02 # $0.02 per image
|
||||
```
|
||||
|
||||
#### 2.4 智能路由器
|
||||
|
||||
```python
|
||||
class ProviderRouter:
|
||||
"""智能供应商路由器"""
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
self._health_cache: dict[str, tuple[bool, float]] = {} # adapter_key -> (healthy, last_check)
|
||||
|
||||
async def route(
|
||||
self,
|
||||
provider_type: str,
|
||||
strategy: str = "priority", # priority / cost / latency / round_robin
|
||||
**kwargs
|
||||
):
|
||||
"""路由到最优供应商"""
|
||||
providers = await self._get_enabled_providers(provider_type)
|
||||
|
||||
if not providers:
|
||||
raise ValueError(f"No {provider_type} providers configured")
|
||||
|
||||
# 按策略排序
|
||||
sorted_providers = self._sort_by_strategy(providers, strategy)
|
||||
|
||||
errors = []
|
||||
for provider in sorted_providers:
|
||||
# 检查健康状态
|
||||
if not await self._is_healthy(provider):
|
||||
continue
|
||||
|
||||
try:
|
||||
adapter = self._create_adapter(provider)
|
||||
result = await adapter.execute(**kwargs)
|
||||
|
||||
# 记录成功指标
|
||||
await self._record_metrics(provider, success=True)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"{provider.name}: {e}")
|
||||
await self._record_metrics(provider, success=False, error=str(e))
|
||||
continue
|
||||
|
||||
raise ValueError(f"All providers failed: {' | '.join(errors)}")
|
||||
|
||||
def _sort_by_strategy(self, providers: list[Provider], strategy: str) -> list[Provider]:
|
||||
if strategy == "priority":
|
||||
return sorted(providers, key=lambda p: (-p.priority, -p.weight))
|
||||
elif strategy == "cost":
|
||||
return sorted(providers, key=lambda p: self._get_estimated_cost(p))
|
||||
elif strategy == "latency":
|
||||
return sorted(providers, key=lambda p: self._get_avg_latency(p))
|
||||
else:
|
||||
return providers
|
||||
```
|
||||
|
||||
### 3. 数据模型扩展
|
||||
|
||||
```sql
|
||||
-- 供应商表 (已有,需扩展)
|
||||
ALTER TABLE providers ADD COLUMN api_key_ref VARCHAR(100); -- 密钥引用 (从 secrets 表获取)
|
||||
ALTER TABLE providers ADD COLUMN request_schema JSONB; -- 请求参数 schema
|
||||
ALTER TABLE providers ADD COLUMN response_parser VARCHAR(200); -- 响应解析规则
|
||||
|
||||
-- 供应商指标表 (新增)
|
||||
CREATE TABLE provider_metrics (
|
||||
id SERIAL PRIMARY KEY,
|
||||
provider_id VARCHAR(36) REFERENCES providers(id),
|
||||
timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
success BOOLEAN,
|
||||
latency_ms INTEGER,
|
||||
cost_usd DECIMAL(10, 6),
|
||||
error_message TEXT,
|
||||
request_id VARCHAR(100)
|
||||
);
|
||||
|
||||
-- 供应商健康状态表 (新增)
|
||||
CREATE TABLE provider_health (
|
||||
provider_id VARCHAR(36) PRIMARY KEY REFERENCES providers(id),
|
||||
is_healthy BOOLEAN DEFAULT TRUE,
|
||||
last_check TIMESTAMP WITH TIME ZONE,
|
||||
consecutive_failures INTEGER DEFAULT 0,
|
||||
last_error TEXT
|
||||
);
|
||||
|
||||
-- 密钥管理表 (新增)
|
||||
CREATE TABLE provider_secrets (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
encrypted_value TEXT NOT NULL, -- 加密存储
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
### 4. Admin Dashboard 功能
|
||||
|
||||
#### 4.1 供应商管理
|
||||
- 供应商列表 (启用/禁用/删除)
|
||||
- 新增供应商 (选择适配器类型 + 配置参数)
|
||||
- 编辑供应商 (修改优先级/权重/超时等)
|
||||
- 测试连接 (验证 API Key 有效性)
|
||||
|
||||
#### 4.2 健康监控
|
||||
- 实时健康状态 (绿/黄/红)
|
||||
- 成功率趋势图
|
||||
- 延迟分布图
|
||||
- 故障告警配置
|
||||
|
||||
#### 4.3 成本分析
|
||||
- 按供应商统计调用量
|
||||
- 按供应商统计成本
|
||||
- 成本趋势图
|
||||
- 预算告警
|
||||
|
||||
#### 4.4 A/B 测试
|
||||
- 创建实验 (供应商 A vs B)
|
||||
- 流量分配 (50/50 或自定义)
|
||||
- 效果对比 (成功率/延迟/成本)
|
||||
|
||||
---
|
||||
|
||||
## 实现路径
|
||||
|
||||
### 阶段 1: 适配器抽象 (基础) - ✅ 已完成
|
||||
|
||||
| 任务 | 状态 | 文件 |
|
||||
|------|------|------|
|
||||
| 定义 `BaseAdapter` 接口 | ✅ | `services/adapters/base.py` |
|
||||
| 实现 `AdapterRegistry` 注册表 | ✅ | `services/adapters/registry.py` |
|
||||
| 重构 GeminiAdapter | ✅ | `services/adapters/text/gemini.py` |
|
||||
| 重构 FluxAdapter | ✅ | `services/adapters/image/flux.py` |
|
||||
| 重构 MinimaxAdapter | ✅ | `services/adapters/tts/minimax.py` |
|
||||
| 重构 `ProviderRouter` 使用新接口 | ✅ | `services/provider_router.py` |
|
||||
|
||||
### 阶段 2: 新供应商接入 (扩展) - 待开始
|
||||
1. 实现 Nanobanana 适配器
|
||||
2. 实现 OpenAI/Claude 文本适配器
|
||||
3. 实现 ElevenLabs TTS 适配器
|
||||
4. 验证零代码接入流程
|
||||
|
||||
### 阶段 3: 监控与分析 (可观测) - 待开始
|
||||
1. 实现指标收集
|
||||
2. 实现健康检查
|
||||
3. 实现成本追踪
|
||||
4. Admin Dashboard 开发
|
||||
|
||||
### 阶段 4: 智能路由 (优化) - 待开始
|
||||
1. 实现多种路由策略
|
||||
2. 实现自动故障转移
|
||||
3. 实现 A/B 测试框架
|
||||
|
||||
---
|
||||
|
||||
## 并行执行与容错设计
|
||||
|
||||
### 问题
|
||||
|
||||
当前串行流程存在两个问题:
|
||||
1. **等待时间长**: 故事(3-5s) → 封面(5-10s) → 音频(3-5s) = 总计 11-20s
|
||||
2. **单点失败**: 某一步502/超时导致整个流程失败
|
||||
|
||||
### 方案 1: 并行执行
|
||||
|
||||
```python
|
||||
async def generate_story_full(keywords: list[str]) -> StoryResult:
|
||||
# Step 1: 故事生成(必须先完成,后续依赖它)
|
||||
story = await generate_story_content(keywords)
|
||||
|
||||
# Step 2: 图片和音频并行执行
|
||||
image_task = asyncio.create_task(generate_image(story.summary))
|
||||
audio_task = asyncio.create_task(text_to_speech(story.content))
|
||||
|
||||
# 等待两者完成,互不阻塞
|
||||
image_result, audio_result = await asyncio.gather(
|
||||
image_task, audio_task,
|
||||
return_exceptions=True # 一个<E4B880><E4B8AA><EFBFBD>败不影响另一个
|
||||
)
|
||||
|
||||
return StoryResult(
|
||||
story=story,
|
||||
image_url=image_result if not isinstance(image_result, Exception) else None,
|
||||
audio_url=audio_result if not isinstance(audio_result, Exception) else None,
|
||||
errors={
|
||||
"image": str(image_result) if isinstance(image_result, Exception) else None,
|
||||
"audio": str(audio_result) if isinstance(audio_result, Exception) else None,
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
**时间对比:**
|
||||
```
|
||||
串行: 3s + 8s + 4s = 15s
|
||||
并行: 3s + max(8s, 4s) = 11s (节省 27%)
|
||||
```
|
||||
|
||||
### 方案 2: 部分成功处理
|
||||
|
||||
**核心原则: 部分成功 > 全部失败**
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class StoryResult:
|
||||
story: Story # 核心,必须成功
|
||||
image_url: str | None = None # 增强,可降级
|
||||
audio_url: str | None = None # 增强,可降级
|
||||
errors: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def is_complete(self) -> bool:
|
||||
return self.image_url is not None and self.audio_url is not None
|
||||
|
||||
@property
|
||||
def failed_components(self) -> list[str]:
|
||||
return [k for k, v in self.errors.items() if v is not None]
|
||||
```
|
||||
|
||||
**降级策略:**
|
||||
|
||||
| 组件 | 失败时降级方案 | 用户体验 |
|
||||
|------|---------------|---------|
|
||||
| 故事 | 无降级,整体失败 | 显示错误,提示重试 |
|
||||
| 封面 | 使用默认封面图 | 显示占位图 + "重新生成"按钮 |
|
||||
| 音频 | 不生成音频 | 隐藏播放按钮 + "生成语音"按钮 |
|
||||
|
||||
### 方案 3: 流式返回 (SSE)
|
||||
|
||||
**为什么用 SSE:**
|
||||
- 用户无需等待全部完成
|
||||
- 每完成一步立即展示
|
||||
- 比 WebSocket 简单,HTTP 兼容性好
|
||||
|
||||
**后端实现:**
|
||||
|
||||
```python
|
||||
from fastapi import APIRouter
|
||||
from sse_starlette.sse import EventSourceResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/api/generate/stream")
|
||||
async def generate_story_stream(
|
||||
request: GenerateRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
async def event_generator():
|
||||
# 1. 立即返回任务ID
|
||||
story_id = str(uuid.uuid4())
|
||||
yield {"event": "started", "data": json.dumps({"story_id": story_id})}
|
||||
|
||||
# 2. 生成故事
|
||||
try:
|
||||
story = await generate_story_content(request.keywords)
|
||||
yield {"event": "story_ready", "data": json.dumps({
|
||||
"title": story.title,
|
||||
"content": story.content,
|
||||
})}
|
||||
except Exception as e:
|
||||
yield {"event": "story_failed", "data": json.dumps({"error": str(e)})}
|
||||
return
|
||||
|
||||
# 3. 并行生成图片和音频
|
||||
async def gen_image():
|
||||
try:
|
||||
url = await generate_image(story.summary)
|
||||
yield {"event": "image_ready", "data": json.dumps({"image_url": url})}
|
||||
except Exception as e:
|
||||
yield {"event": "image_failed", "data": json.dumps({"error": str(e)})}
|
||||
|
||||
async def gen_audio():
|
||||
try:
|
||||
url = await text_to_speech(story.content)
|
||||
yield {"event": "audio_ready", "data": json.dumps({"audio_url": url})}
|
||||
except Exception as e:
|
||||
yield {"event": "audio_failed", "data": json.dumps({"error": str(e)})}
|
||||
|
||||
# 并行执行,逐个yield结果
|
||||
tasks = [gen_image(), gen_audio()]
|
||||
for coro in asyncio.as_completed([t.__anext__() for t in tasks]):
|
||||
result = await coro
|
||||
yield result
|
||||
|
||||
yield {"event": "complete", "data": json.dumps({"story_id": story_id})}
|
||||
|
||||
return EventSourceResponse(event_generator())
|
||||
```
|
||||
|
||||
**前端实现:**
|
||||
|
||||
```typescript
|
||||
const eventSource = new EventSource('/api/generate/stream', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ keywords }),
|
||||
});
|
||||
|
||||
eventSource.addEventListener('started', (e) => {
|
||||
const { story_id } = JSON.parse(e.data);
|
||||
showLoading('正在创作故事...');
|
||||
});
|
||||
|
||||
eventSource.addEventListener('story_ready', (e) => {
|
||||
const { title, content } = JSON.parse(e.data);
|
||||
renderStory(title, content);
|
||||
showLoading('正在生成封面和语音...');
|
||||
});
|
||||
|
||||
eventSource.addEventListener('image_ready', (e) => {
|
||||
const { image_url } = JSON.parse(e.data);
|
||||
renderCover(image_url);
|
||||
});
|
||||
|
||||
eventSource.addEventListener('image_failed', (e) => {
|
||||
showRetryButton('image');
|
||||
});
|
||||
|
||||
eventSource.addEventListener('audio_ready', (e) => {
|
||||
const { audio_url } = JSON.parse(e.data);
|
||||
enablePlayButton(audio_url);
|
||||
});
|
||||
|
||||
eventSource.addEventListener('complete', () => {
|
||||
eventSource.close();
|
||||
hideLoading();
|
||||
});
|
||||
```
|
||||
|
||||
**用户体验时间线:**
|
||||
```
|
||||
0s → 显示"正在创作..."
|
||||
3s → 故事文本渲染,显示"正在生成封面和语音..."
|
||||
3-7s → 音频就绪,播放按钮可用
|
||||
3-11s → 封面就绪,图片显示
|
||||
11s → 完成
|
||||
```
|
||||
|
||||
### 方案 4: 断点续传 (可选)
|
||||
|
||||
适用于网络不稳定场景,支持刷新页面后继续:
|
||||
|
||||
```python
|
||||
class StoryWorkflowState(Base):
|
||||
__tablename__ = "story_workflow_states"
|
||||
|
||||
story_id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
||||
status: Mapped[str] = mapped_column(String(20)) # pending/story_done/image_done/audio_done/complete
|
||||
story_content: Mapped[str | None] = mapped_column(Text)
|
||||
image_url: Mapped[str | None] = mapped_column(String(500))
|
||||
audio_url: Mapped[str | None] = mapped_column(String(500))
|
||||
last_error: Mapped[str | None] = mapped_column(Text)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, onupdate=datetime.utcnow)
|
||||
|
||||
async def resume_workflow(story_id: str) -> StoryResult:
|
||||
state = await get_workflow_state(story_id)
|
||||
|
||||
if state.status == "story_done":
|
||||
# 从图片+音频生成继续
|
||||
return await generate_image_and_audio(state)
|
||||
elif state.status == "image_done":
|
||||
# 只需要生成音频
|
||||
return await generate_audio_only(state)
|
||||
elif state.status == "audio_done":
|
||||
# 只需要生成图片
|
||||
return await generate_image_only(state)
|
||||
else:
|
||||
return StoryResult.from_state(state)
|
||||
```
|
||||
|
||||
### 推荐实现顺序
|
||||
|
||||
| 优先级 | 方案 | 收益 | 复杂度 | 状态 |
|
||||
|--------|------|------|--------|------|
|
||||
| P0 | 并行执行 | 节省 27% 时间 | 低 | ✅ 已完成 |
|
||||
| P0 | 部分成功 | 提升容错性 | 低 | ✅ 已完成 |
|
||||
| P1 | SSE 流式返回 | 体验大幅提升 | 中 | 待开始 |
|
||||
| P2 | 断点续传 | 极端场景保障 | 高 | 待开始 |
|
||||
|
||||
**P0 实现详情:**
|
||||
- 新增 API: `POST /api/generate/full`
|
||||
- 文件: `api/stories.py:113-189`
|
||||
- 响应模型: `FullStoryResponse` (含 `errors` 字段标识失败组件)
|
||||
|
||||
---
|
||||
|
||||
## 待决策清单
|
||||
|
||||
> **使用说明**: 在每个决策的 `[ ]` 中填入你的选择(如 `[x]` 或 `[B]`),确认后删除未选中的选项。
|
||||
|
||||
---
|
||||
|
||||
### 决策 1: 适配器配置存储
|
||||
|
||||
**问题**: 适配器的配置信息(API地址、模型名、超时等)存在哪里?
|
||||
|
||||
| 选项 | 方案 | 优点 | 缺点 |
|
||||
|------|------|------|------|
|
||||
| [ ] A | 全部存数据库 | 完全动态,运行时可改 | 需要管理界面,初始化复杂 |
|
||||
| [ ] B | 代码定义 + DB配置 | 平衡,核心逻辑在代码,参数可调 | 新适配器仍需改代码 |
|
||||
| [ ] C | 配置文件 (YAML/JSON) | 简单,版本控制友好 | 改配置需重启 |
|
||||
|
||||
**推荐**: B(代码定义适配器类,DB存储启用状态/优先级/API Key引用)
|
||||
|
||||
---
|
||||
|
||||
### 决策 2: 密钥管理
|
||||
|
||||
**问题**: API Key 等敏感信息如何存储?
|
||||
|
||||
| 选项 | 方案 | 优点 | 缺点 |
|
||||
|------|------|------|------|
|
||||
| [ ] A | 环境变量 | 简单,当前方式 | 多供应商时env膨胀,改key需重启 |
|
||||
| [ ] B | 数据库加密存储 | 动态管理,支持多key | 需要加密方案,安全风险 |
|
||||
| [ ] C | 外部密钥服务 (Vault/AWS Secrets) | 企业级安全 | 复杂,增加依赖 |
|
||||
|
||||
**推荐**: A(当前阶段),后期可迁移到B
|
||||
|
||||
---
|
||||
|
||||
### 决策 3: 图像供应商优先级
|
||||
|
||||
**问题**: 接入多个图像供应商后,默认使用哪个?
|
||||
|
||||
| 选项 | 供应商 | 特点 | 预估成本 |
|
||||
|------|--------|------|----------|
|
||||
| [ ] 1 | Nanobanana | 新兴,据说效果好 | 待调研 |
|
||||
| [ ] 2 | Flux (当前) | 稳定,已接入 | ~$0.03/张 |
|
||||
| [ ] 3 | DALL-E 3 | OpenAI出品,质量高 | ~$0.04/张 |
|
||||
| [ ] 4 | Midjourney | 艺术风格强 | API受限 |
|
||||
|
||||
**推荐**: 先调研Nanobanana,效果好则替换Flux
|
||||
|
||||
---
|
||||
|
||||
### 决策 4: 文本供应商优先级
|
||||
|
||||
**问题**: 故事生成使用哪个LLM?
|
||||
|
||||
| 选项 | 供应商 | 特点 | 预估成本 |
|
||||
|------|--------|------|----------|
|
||||
| [ ] 1 | Gemini (当前) | 免费额度大,中文好 | 免费/低成本 |
|
||||
| [ ] 2 | OpenAI GPT-4o | 质量稳定 | ~$0.01/1K tokens |
|
||||
| [ ] 3 | Claude | 创意写作强 | ~$0.015/1K tokens |
|
||||
| [ ] 4 | Qwen (通义千问) | 国内,中文优化 | 待调研 |
|
||||
|
||||
**推荐**: Gemini为主,OpenAI备用
|
||||
|
||||
---
|
||||
|
||||
### 决策 5: TTS供应商优先级
|
||||
|
||||
**问题**: 语音合成使用哪个服务?
|
||||
|
||||
| 选项 | 供应商 | 特点 | 预估成本 |
|
||||
|------|--------|------|----------|
|
||||
| [ ] 1 | Minimax (当前) | 中文效果好,已接入 | ~$0.01/1K字符 |
|
||||
| [ ] 2 | ElevenLabs | 英文最佳,多语言 | ~$0.03/1K字符 |
|
||||
| [ ] 3 | Azure TTS | 稳定,多语言 | ~$0.016/1K字符 |
|
||||
| [ ] 4 | Google TTS | 便宜 | ~$0.004/1K字符 |
|
||||
|
||||
**推荐**: Minimax为主(中文场景)
|
||||
|
||||
---
|
||||
|
||||
### 决策 6: Admin Dashboard 技术栈
|
||||
|
||||
**问题**: 供应商管理后台用什么技术?
|
||||
|
||||
| 选项 | 方案 | 优点 | 缺点 |
|
||||
|------|------|------|------|
|
||||
| [ ] A | 复用 Vue 前端 | 技术栈统一,复用组件 | 需要自己写UI |
|
||||
| [ ] B | React Admin | 成熟的Admin框架 | 引入新技术栈 |
|
||||
| [ ] C | 现成方案 (AdminJS/Retool) | 开发快 | 定制性差,可能收费 |
|
||||
|
||||
**推荐**: A(在现有Vue项目中加 `/admin` 路由)
|
||||
|
||||
---
|
||||
|
||||
### 决策 7: Phase 2 功能优先级
|
||||
|
||||
**问题**: 体验增强阶段先做哪个功能?
|
||||
|
||||
| 选项 | 功能 | 用户价值 | 开发复杂度 |
|
||||
|------|------|----------|------------|
|
||||
| [ ] 1 | 故事编辑 | 高(用户可修改AI内容) | 中 |
|
||||
| [ ] 2 | 角色定制 | 高(孩子成为主角) | 低 |
|
||||
| [ ] 3 | 故事分享 | 高(增长引擎) | 中 |
|
||||
| [ ] 4 | 故事续写 | 中(延长使用时长) | 中 |
|
||||
|
||||
**推荐**: 2 → 1 → 3 → 4(角色定制最快出效果)
|
||||
|
||||
---
|
||||
|
||||
### 决策 8: 并行与容错实现顺序
|
||||
|
||||
**问题**: 并行执行、部分成功、SSE、断点续传,先做哪些?
|
||||
|
||||
| 选项 | 方案 | 说明 |
|
||||
|------|------|------|
|
||||
| [ ] A | P0先做 | 先实现并行+部分成功,快速见效 |
|
||||
| [ ] B | P0+P1一起 | 并行+部分成功+SSE,体验完整 |
|
||||
| [ ] C | 只做SSE | 跳过简单方案,直接上流式 |
|
||||
|
||||
**推荐**: A(先P0,验证后再做SSE)
|
||||
|
||||
---
|
||||
|
||||
## 确认后删除此区块
|
||||
|
||||
确认所有决策后,可以删除未选中的选项,保留最终方案作为实现依据。
|
||||
@@ -1,169 +0,0 @@
|
||||
# DreamWeaver 产品路线图
|
||||
|
||||
## 产品愿景
|
||||
|
||||
**梦语织机** - 为 3-8 岁儿童打造的 AI 故事创作平台,让每个孩子都能拥有专属的成长故事。
|
||||
|
||||
### 核心价值主张
|
||||
- **个性化**: 基于关键词生成独一无二的故事
|
||||
- **教育性**: 融入成长主题(勇气、友谊、诚实等)
|
||||
- **沉浸感**: AI 封面 + 语音朗读,多感官体验
|
||||
- **亲子互动**: 家长参与创作,增进亲子关系
|
||||
|
||||
---
|
||||
|
||||
## 用户画像
|
||||
|
||||
### 主要用户:家长(25-40岁)
|
||||
- **需求**: 为孩子找到有教育意义的睡前故事
|
||||
- **痛点**: 市面故事千篇一律,缺乏个性化
|
||||
- **场景**: 睡前、旅途、周末亲子时光
|
||||
|
||||
### 次要用户:幼儿园/早教机构
|
||||
- **需求**: 批量生成教学故事素材
|
||||
- **痛点**: 内容制作成本高
|
||||
- **场景**: 课堂教学、活动策划
|
||||
|
||||
---
|
||||
|
||||
## 功能规划
|
||||
|
||||
### Phase 1: MVP 完善(当前)
|
||||
> 目标:核心体验闭环,用户可完整使用
|
||||
|
||||
| 功能 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| 关键词生成故事 | ✅ 已完成 | 输入关键词,AI 生成故事 |
|
||||
| 故事润色增强 | ✅ 已完成 | 用户提供草稿,AI 润色 |
|
||||
| AI 封面生成 | ✅ 已完成 | 根据故事生成插画 |
|
||||
| 语音朗读 | ✅ 已完成 | TTS 朗读故事 |
|
||||
| 故事收藏管理 | ✅ 已完成 | 保存、查看、删除 |
|
||||
| OAuth 登录 | ✅ 已完成 | GitHub/Google 登录 |
|
||||
|
||||
### Phase 2: 体验增强
|
||||
> 目标:提升用户粘性,增加互动性
|
||||
|
||||
| 功能 | 优先级 | 说明 |
|
||||
|------|--------|------|
|
||||
| **故事编辑** | P0 | 用户可修改 AI 生成的故事内容 |
|
||||
| **角色定制** | P0 | 输入孩子姓名/性别,成为故事主角 |
|
||||
| **故事续写** | P1 | 基于已有故事继续创作下一章 |
|
||||
| **多语言支持** | P1 | 英文故事生成(已有 i18n 基础) |
|
||||
| **故事分享** | P1 | 生成分享图片/链接 |
|
||||
| **收藏夹/标签** | P2 | 故事分类管理 |
|
||||
|
||||
### Phase 3: 社区与增长
|
||||
> 目标:构建用户社区,实现自然增长
|
||||
|
||||
| 功能 | 优先级 | 说明 |
|
||||
|------|--------|------|
|
||||
| **故事广场** | P0 | 公开优质故事,用户可浏览 |
|
||||
| **点赞/收藏** | P0 | 社区互动基础 |
|
||||
| **故事模板** | P1 | 预设故事框架(冒险/友谊/成长) |
|
||||
| **创作者主页** | P1 | 展示用户创作的故事集 |
|
||||
| **评论系统** | P2 | 用户交流反馈 |
|
||||
|
||||
### Phase 4: 商业化
|
||||
> 目标:建立可持续商业模式
|
||||
|
||||
| 功能 | 优先级 | 说明 |
|
||||
|------|--------|------|
|
||||
| **会员订阅** | P0 | 免费/基础/高级三档 |
|
||||
| **故事导出** | P0 | PDF/电子书格式导出 |
|
||||
| **实体书打印** | P1 | 对接印刷服务,生成实体绘本 |
|
||||
| **API 开放** | P2 | 为 B 端客户提供 API |
|
||||
| **企业版** | P2 | 幼儿园/早教机构定制 |
|
||||
|
||||
---
|
||||
|
||||
## 技术架构演进
|
||||
|
||||
### 当前架构 (Phase 1)
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Vue 3 │────▶│ FastAPI │────▶│ PostgreSQL │
|
||||
│ Frontend │ │ Backend │ │ (Neon) │
|
||||
└─────────────┘ └──────┬──────┘ └─────────────┘
|
||||
│
|
||||
┌────────────┼────────────┐
|
||||
▼ ▼ ▼
|
||||
┌────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ Gemini │ │ Minimax │ │ Flux │
|
||||
│ (Text) │ │ (TTS) │ │ (Image) │
|
||||
└────────┘ └─────────┘ └─────────┘
|
||||
```
|
||||
|
||||
### Phase 2 架构演进
|
||||
```
|
||||
新增组件:
|
||||
- Redis: 缓存 + 会话 + Rate Limit
|
||||
- Celery: 异步任务队列(图片/音频生成)
|
||||
- S3/OSS: 静态资源存储
|
||||
```
|
||||
|
||||
### Phase 3 架构演进
|
||||
```
|
||||
新增组件:
|
||||
- Elasticsearch: 故事全文搜索
|
||||
- CDN: 静态资源加速
|
||||
- 消息队列: 社区通知推送
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 里程碑规划
|
||||
|
||||
### M1: MVP 完善 ✅
|
||||
- [x] 核心功能闭环
|
||||
- [x] 工程鲁棒性改进
|
||||
- [x] 测试覆盖
|
||||
|
||||
### M2: 体验增强
|
||||
- [ ] 故事编辑功能
|
||||
- [ ] 角色定制(孩子成为主角)
|
||||
- [ ] 故事续写
|
||||
- [ ] 多语言支持
|
||||
- [ ] 分享功能
|
||||
|
||||
### M3: 社区上线
|
||||
- [ ] 故事广场
|
||||
- [ ] 用户互动(点赞/收藏)
|
||||
- [ ] 创作者主页
|
||||
|
||||
### M4: 商业化
|
||||
- [ ] 会员体系
|
||||
- [ ] 故事导出
|
||||
- [ ] 实体书打印
|
||||
|
||||
---
|
||||
|
||||
## 竞品分析
|
||||
|
||||
| 产品 | 优势 | 劣势 | 我们的差异化 |
|
||||
|------|------|------|--------------|
|
||||
| 凯叔讲故事 | 内容丰富、品牌知名 | 无个性化、订阅贵 | AI 个性化生成 |
|
||||
| 喜马拉雅儿童 | 海量音频、多平台 | 内容同质化 | 用户参与创作 |
|
||||
| ChatGPT | AI 能力强 | 非儿童专属、无配套 | 垂直场景优化 |
|
||||
|
||||
---
|
||||
|
||||
## 风险与应对
|
||||
|
||||
| 风险 | 影响 | 应对策略 |
|
||||
|------|------|----------|
|
||||
| AI 生成内容不当 | 高 | 内容审核 + 家长控制 |
|
||||
| API 成本过高 | 中 | 缓存优化 + 分级限流 |
|
||||
| 用户增长缓慢 | 中 | 社区运营 + 分享裂变 |
|
||||
| 竞品模仿 | 低 | 快速迭代 + 深耕垂直 |
|
||||
|
||||
---
|
||||
|
||||
## 下一步行动
|
||||
|
||||
**Phase 2 优先实现功能:**
|
||||
|
||||
1. **故事编辑** - 用户体验核心痛点
|
||||
2. **角色定制** - 差异化竞争力
|
||||
3. **故事分享** - 自然增长引擎
|
||||
|
||||
是否需要我为这些功能生成详细的技术规格文档?
|
||||
@@ -1,49 +0,0 @@
|
||||
# DreamWeaver 工程鲁棒性改进计划
|
||||
|
||||
## 概述
|
||||
本计划旨在提升 DreamWeaver 项目的工程质量,包括测试覆盖、稳定性、可观测性等方面。
|
||||
|
||||
## 任务列表
|
||||
|
||||
### P0 - 关键问题修复
|
||||
|
||||
#### Task-1: 修复 Rate Limit 内存泄漏 ✅
|
||||
- **文件**: `backend/app/api/stories.py`
|
||||
- **方案**: 已迁移至 Redis 分布式限流,内存泄漏问题不再存在
|
||||
|
||||
#### Task-2: 添加核心 API 测试 ✅
|
||||
- **文件**: `backend/tests/`
|
||||
- **范围**: test_auth, test_stories, test_profiles, test_universes, test_push_configs, test_reading_events, test_provider_router
|
||||
|
||||
### P1 - 稳定性提升
|
||||
|
||||
#### Task-3: 添加 API 重试机制 ✅
|
||||
- **方案**: 所有适配器已使用 `tenacity` 指数退避重试 (gemini, openai, cqtai, antigravity, minimax, elevenlabs)
|
||||
|
||||
#### Task-4: 添加结构化日志 ✅
|
||||
- **文件**: `backend/app/core/logging.py`
|
||||
- **方案**: structlog JSON/Console 双模式,所有适配器和 provider_router 已集成
|
||||
|
||||
### P2 - 代码优化
|
||||
|
||||
#### Task-5: 重构 Provider Router ✅
|
||||
- **文件**: `backend/app/services/provider_router.py`
|
||||
- **方案**: 已实现统一 `_route_with_failover` 函数
|
||||
|
||||
#### Task-6: 配置外部化 ✅
|
||||
- **文件**: `backend/app/core/config.py`, `backend/app/services/provider_router.py`
|
||||
- **方案**: 所有模型名已移至 Settings,支持环境变量覆盖
|
||||
|
||||
#### Task-7: 修复脆弱的 URL 解析 ✅
|
||||
- **状态**: `drawing.py` 已被适配器系统取代,不再存在
|
||||
|
||||
## 新增依赖 (已添加)
|
||||
```toml
|
||||
# pyproject.toml [project.dependencies]
|
||||
cachetools>=5.0.0 # Task-1: TTL cache
|
||||
tenacity>=8.0.0 # Task-3: 重试机制
|
||||
structlog>=24.0.0 # Task-4: 结构化日志
|
||||
|
||||
# [project.optional-dependencies.dev]
|
||||
pytest-cov>=4.0.0 # Task-2: 覆盖率报告
|
||||
```
|
||||
@@ -1,303 +0,0 @@
|
||||
DreamWeaver 前端 UI 重构任务列表
|
||||
|
||||
阶段一:基础设施(必须先完成)
|
||||
|
||||
TASK-001 [x]: 安装图标库
|
||||
|
||||
文件: frontend/package.json
|
||||
操作: 安装 @heroicons/vue 图标库
|
||||
命令: npm install @heroicons/vue
|
||||
验收: 能在 Vue 组件中 import { SparklesIcon } from '@heroicons/vue/24/outline'
|
||||
|
||||
TASK-002 [x]: 扩展 Tailwind 配置
|
||||
|
||||
文件: frontend/tailwind.config.js
|
||||
操作: 添加完整的设计系统配置
|
||||
内容:
|
||||
|
||||
- 扩展 fontFamily 添加 sans: ['Noto Sans SC', ...]
|
||||
- 扩展 borderRadius 添加 '2xl': '1rem', '3xl': '1.5rem'
|
||||
- 扩展 boxShadow 添加 'glass': '0 8px 32px rgba(0,0,0,0.08)'
|
||||
- 扩展 animation 添加 'float': 'float 3s ease-in-out infinite'
|
||||
- 扩展 keyframes 添加 float 动画定义
|
||||
验收: Tailwind 类 font-sans, rounded-3xl, shadow-glass, animate-float 可用
|
||||
|
||||
TASK-003 [x]: 精简全局样式
|
||||
|
||||
文件: frontend/src/style.css
|
||||
操作:
|
||||
|
||||
1. 删除 .animate-float (移至 Tailwind)
|
||||
2. 删除 .stars::before/after (移除 emoji 装饰)
|
||||
3. 保留 .glass, .btn-magic, .input-magic, .card-hover, .gradient-text
|
||||
4. 删除 --gradient-magic 变量(过于花哨)
|
||||
验收: 文件行数减少约 30%,无 emoji 相关 CSS
|
||||
|
||||
---
|
||||
|
||||
阶段二:创建可复用组件
|
||||
|
||||
TASK-004 [x]: 创建 BaseButton 组件
|
||||
|
||||
文件: frontend/src/components/ui/BaseButton.vue
|
||||
操作: 创建统一按钮组件
|
||||
Props:
|
||||
|
||||
- variant: 'primary' | 'secondary' | 'danger' | 'ghost'
|
||||
- size: 'sm' | 'md' | 'lg'
|
||||
- loading: boolean
|
||||
- disabled: boolean
|
||||
- icon: Component (可选,Heroicon 组件)
|
||||
样式规范:
|
||||
- primary: 使用 .btn-magic 渐变
|
||||
- secondary: bg-white border border-gray-200
|
||||
- danger: bg-red-500 text-white
|
||||
- ghost: bg-transparent hover:bg-gray-100
|
||||
验收: 导出组件,支持 slot 内容和所有 props
|
||||
|
||||
TASK-005 [x]: 创建 BaseCard 组件
|
||||
|
||||
文件: frontend/src/components/ui/BaseCard.vue
|
||||
操作: 创建统一卡片组件
|
||||
Props:
|
||||
|
||||
- hover: boolean (是否启用悬浮效果)
|
||||
- padding: 'none' | 'sm' | 'md' | 'lg'
|
||||
样式: 使用 .glass + rounded-2xl + 可选 .card-hover
|
||||
验收: 导出组件,支持默认 slot
|
||||
|
||||
TASK-006 [x]: 创建 BaseInput 组件
|
||||
|
||||
文件: frontend/src/components/ui/BaseInput.vue
|
||||
操作: 创建统一输入框组件
|
||||
Props:
|
||||
|
||||
- modelValue: string
|
||||
- type: 'text' | 'password' | 'email' | 'number'
|
||||
- placeholder: string
|
||||
- label: string (可选)
|
||||
- error: string (可选)
|
||||
- disabled: boolean
|
||||
样式: 使用 .input-magic + 错误状态红色边框
|
||||
验收: 支持 v-model,显示 label 和 error
|
||||
|
||||
TASK-007 [x]: 创建 BaseSelect 组件
|
||||
|
||||
文件: frontend/src/components/ui/BaseSelect.vue
|
||||
操作: 创建统一下拉选择组件
|
||||
Props:
|
||||
|
||||
- modelValue: string | number
|
||||
- options: Array<{ value: string | number, label: string }>
|
||||
- label: string (可选)
|
||||
- placeholder: string
|
||||
- disabled: boolean
|
||||
样式: 与 BaseInput 保持一致
|
||||
验收: 支持 v-model,正确渲染 options
|
||||
|
||||
TASK-008 [x]: 创建 BaseTextarea 组件
|
||||
|
||||
文件: frontend/src/components/ui/BaseTextarea.vue
|
||||
操作: 创建统一文本域组件
|
||||
Props:
|
||||
|
||||
- modelValue: string
|
||||
- placeholder: string
|
||||
- rows: number
|
||||
- maxLength: number (可选,显示字数统计)
|
||||
- label: string (可选)
|
||||
样式: 使用 .input-magic,右下角显示字数
|
||||
验收: 支持 v-model,字数统计正确
|
||||
|
||||
TASK-009 [x]: 创建 LoadingSpinner 组件
|
||||
|
||||
文件: frontend/src/components/ui/LoadingSpinner.vue
|
||||
操作: 创建统一加载动画组件
|
||||
Props:
|
||||
|
||||
- size: 'sm' | 'md' | 'lg'
|
||||
- text: string (可选,加载提示文字)
|
||||
样式: 紫色渐变圆环旋转动画,无 emoji
|
||||
验收: 三种尺寸正确渲染
|
||||
|
||||
TASK-010 [x]: 创建 EmptyState 组件
|
||||
|
||||
文件: frontend/src/components/ui/EmptyState.vue
|
||||
操作: 创建统一空状态组件
|
||||
Props:
|
||||
|
||||
- icon: Component (Heroicon)
|
||||
- title: string
|
||||
- description: string
|
||||
- actionText: string (可选)
|
||||
- actionTo: string (可选,路由路径)
|
||||
样式: 居中布局,图标使用 Heroicon 而非 emoji
|
||||
验收: 点击按钮正确跳转
|
||||
|
||||
TASK-011 [x]: 创建 ConfirmModal 组件
|
||||
|
||||
文件: frontend/src/components/ui/ConfirmModal.vue
|
||||
操作: 创建统一确认弹窗组件
|
||||
Props:
|
||||
|
||||
- show: boolean
|
||||
- title: string
|
||||
- message: string
|
||||
- confirmText: string
|
||||
- cancelText: string
|
||||
- variant: 'danger' | 'warning' | 'info'
|
||||
Emits: confirm, cancel
|
||||
样式: 使用 .glass 背景,Transition 动画
|
||||
验收: 显示/隐藏动画流畅,事件正确触发
|
||||
|
||||
TASK-012 [x]: 创建组件导出索引
|
||||
|
||||
文件: frontend/src/components/ui/index.ts
|
||||
操作: 统一导出所有 UI 组件
|
||||
内容:
|
||||
export { default as BaseButton } from './BaseButton.vue'
|
||||
export { default as BaseCard } from './BaseCard.vue'
|
||||
// ... 其他组件
|
||||
验收: 可以 import { BaseButton, BaseCard } from '@/components/ui'
|
||||
|
||||
---
|
||||
|
||||
阶段三:重构现有页面
|
||||
|
||||
TASK-013 [x]: 重构 NavBar 组件
|
||||
|
||||
文件: frontend/src/components/NavBar.vue
|
||||
操作:
|
||||
|
||||
1. 将 emoji ✨🌟📚🛠️🚪 替换为 Heroicons (SparklesIcon, StarIcon, BookOpenIcon, Cog6ToothIcon, ArrowRightOnRectangleIcon)
|
||||
2. 将 ?? 占位符替换为正确图标 (UserGroupIcon, GlobeAltIcon)
|
||||
3. 使用 BaseButton 替换登录按钮
|
||||
4. 移除 animate-float 和 animate-pulse 装饰动画
|
||||
验收: 无 emoji,图标统一为 Heroicons,视觉更专业
|
||||
|
||||
TASK-014 [x]: 重构 Home.vue 页面
|
||||
|
||||
文件: frontend/src/views/Home.vue
|
||||
操作:
|
||||
|
||||
1. 删除 Hero 区域的浮动 emoji 装饰 (🌙⭐✨🌟)
|
||||
2. 将模式切换按钮的 emoji (✨📝) 替换为 Heroicons
|
||||
3. 将教育主题按钮的 emoji 替换为 Heroicons 或移除
|
||||
4. 使用 BaseButton 替换提交按钮
|
||||
5. 使用 BaseTextarea 替换文本输入区
|
||||
6. 使用 BaseSelect 替换档案/宇宙选择器
|
||||
7. 将 Features 区域的 emoji (🎨🔊📚) 替换为 Heroicons
|
||||
验收: 页面无 emoji,使用统一组件,视觉简洁专业
|
||||
|
||||
TASK-015 [x]: 重构 MyStories.vue 页面
|
||||
|
||||
文件: frontend/src/views/MyStories.vue
|
||||
操作:
|
||||
|
||||
1. 使用 BaseButton 替换"创作新故事"按钮
|
||||
2. 使用 LoadingSpinner 替换自定义加载动画
|
||||
3. 使用 EmptyState 替换空状态区域(移除 📚✨🪄 emoji)
|
||||
4. 将错误状态的 😢 替换为 Heroicon ExclamationCircleIcon
|
||||
5. 将统计区域的 📖 替换为 Heroicon
|
||||
6. 使用 BaseCard 包装故事卡片
|
||||
验收: 页面无 emoji,组件统一
|
||||
|
||||
TASK-016 [x]: 重构 StoryDetail.vue 页面
|
||||
|
||||
文件: frontend/src/views/StoryDetail.vue
|
||||
操作:
|
||||
|
||||
1. 使用 LoadingSpinner 替换加载动画
|
||||
2. 将 🎨 替换为 Heroicon PhotoIcon
|
||||
3. 将 🔊 替换为 Heroicon SpeakerWaveIcon
|
||||
4. 将 ✨ 替换为 Heroicon SparklesIcon
|
||||
5. 将 🗑️ 替换为 Heroicon TrashIcon
|
||||
6. 将 ⚠️ 替换为 Heroicon ExclamationTriangleIcon
|
||||
7. 使用 BaseButton 替换所有按钮
|
||||
8. 使用 ConfirmModal 替换删除确认弹窗
|
||||
验收: 页面无 emoji,弹窗使用统一组件
|
||||
|
||||
TASK-017 [x]: 重构 AdminProviders.vue 页面
|
||||
|
||||
文件: frontend/src/views/AdminProviders.vue
|
||||
操作:
|
||||
|
||||
1. 移除 <style scoped> 中的所有自定义样式
|
||||
2. 登录表单使用 BaseCard + BaseInput + BaseButton
|
||||
3. Provider 表单使用 BaseCard + BaseInput + BaseSelect + BaseButton
|
||||
4. 表格使用 Tailwind 样式:divide-y divide-gray-200,hover:bg-gray-50
|
||||
5. 操作按钮使用 BaseButton variant="ghost"
|
||||
6. 整体布局使用 .glass 背景
|
||||
7. 添加页面标题使用 .gradient-text
|
||||
验收: 与主应用风格一致,无原生 HTML 样式
|
||||
|
||||
TASK-018 [x]: 重构 ChildProfiles.vue 页面
|
||||
|
||||
文件: frontend/src/views/ChildProfiles.vue
|
||||
操作:
|
||||
|
||||
1. 检查并替换所有 emoji 为 Heroicons
|
||||
2. 使用 BaseButton, BaseCard, BaseInput 等统一组件
|
||||
3. 使用 EmptyState 处理空状态
|
||||
4. 使用 LoadingSpinner 处理加载状态
|
||||
验收: 页面无 emoji,组件统一
|
||||
|
||||
TASK-019 [x]: 重构 ChildProfileDetail.vue 页面
|
||||
|
||||
文件: frontend/src/views/ChildProfileDetail.vue
|
||||
操作: 同 TASK-018
|
||||
验收: 页面无 emoji,组件统一
|
||||
|
||||
TASK-020 [x]: 重构 Universes.vue 页面
|
||||
|
||||
文件: frontend/src/views/Universes.vue
|
||||
操作: 同 TASK-018
|
||||
验收: 页面无 emoji,组件统一
|
||||
|
||||
TASK-021 [x]: 重构 UniverseDetail.vue 页面
|
||||
|
||||
文件: frontend/src/views/UniverseDetail.vue
|
||||
操作: 同 TASK-018
|
||||
验收: 页面无 emoji,组件统一
|
||||
|
||||
---
|
||||
|
||||
阶段四:优化与收尾
|
||||
|
||||
TASK-022 [x]: 添加深色模式支持(可选)
|
||||
|
||||
文件: frontend/tailwind.config.js, frontend/src/style.css
|
||||
操作:
|
||||
|
||||
1. 在 tailwind.config.js 添加 darkMode: 'class'
|
||||
2. 为 .glass, .btn-magic 等添加 dark: 变体
|
||||
3. 在 NavBar 添加主题切换按钮
|
||||
验收: 点击切换按钮,整体配色切换
|
||||
|
||||
TASK-023 [x]: 添加 prefers-reduced-motion 支持
|
||||
|
||||
文件: frontend/src/style.css
|
||||
操作: 为所有动画添加媒体查询
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.animate-float, .card-hover, .btn-magic { animation: none; transition: none; }
|
||||
}
|
||||
验收: 系统设置"减少动态效果"时,动画停止
|
||||
|
||||
TASK-024 [x]: 性能优化 - 减少 backdrop-filter
|
||||
|
||||
文件: frontend/src/style.css
|
||||
操作:
|
||||
|
||||
1. 将 .glass 的 backdrop-filter: blur(20px) 改为 blur(10px)
|
||||
2. 移除嵌套 .glass 元素的 backdrop-filter
|
||||
验收: 页面滚动更流畅,尤其在移动端
|
||||
|
||||
---
|
||||
|
||||
执行顺序建议
|
||||
|
||||
阶段一 (TASK-001 ~ 003) → 阶段二 (TASK-004 ~ 012) → 阶段三 (TASK-013 ~ 021) → 阶段四 (TASK-022 ~ 024)
|
||||
|
||||
每个任务完成后运行 npm run build 确保无类型错误。
|
||||
|
||||
|
||||
189
.github/workflows/build.yml
vendored
189
.github/workflows/build.yml
vendored
@@ -1,189 +0,0 @@
|
||||
# .github/workflows/build.yml
|
||||
# 构建并推送 Docker 镜像到 GitHub Container Registry
|
||||
#
|
||||
# 触发条件:
|
||||
# - push 到 main 分支
|
||||
# - 手动触发 (workflow_dispatch)
|
||||
# - 创建版本标签 (v*)
|
||||
#
|
||||
# 镜像命名:
|
||||
# ghcr.io/<owner>/dreamweaver-backend:latest
|
||||
# ghcr.io/<owner>/dreamweaver-frontend:latest
|
||||
# ghcr.io/<owner>/dreamweaver-admin-frontend:latest
|
||||
|
||||
name: Build and Push Docker Images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'frontend/**'
|
||||
- 'admin-frontend/**'
|
||||
- '.github/workflows/build.yml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
force_build:
|
||||
description: 'Force rebuild all images'
|
||||
required: false
|
||||
default: 'false'
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_PREFIX: ${{ github.repository_owner }}/dreamweaver
|
||||
|
||||
jobs:
|
||||
# ==============================================
|
||||
# 检测变更的目录
|
||||
# ==============================================
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
backend: ${{ steps.filter.outputs.backend }}
|
||||
frontend: ${{ steps.filter.outputs.frontend }}
|
||||
admin-frontend: ${{ steps.filter.outputs.admin-frontend }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
backend:
|
||||
- 'backend/**'
|
||||
frontend:
|
||||
- 'frontend/**'
|
||||
admin-frontend:
|
||||
- 'admin-frontend/**'
|
||||
|
||||
# ==============================================
|
||||
# 构建后端镜像
|
||||
# ==============================================
|
||||
build-backend:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.backend == 'true' || github.event.inputs.force_build == 'true' || startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-backend
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=sha,prefix=
|
||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./backend
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
# ==============================================
|
||||
# 构建前端镜像
|
||||
# ==============================================
|
||||
build-frontend:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.frontend == 'true' || github.event.inputs.force_build == 'true' || startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-frontend
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=sha,prefix=
|
||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./frontend
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
# ==============================================
|
||||
# 构建管理后台前端镜像
|
||||
# ==============================================
|
||||
build-admin-frontend:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.admin-frontend == 'true' || github.event.inputs.force_build == 'true' || startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-admin-frontend
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=sha,prefix=
|
||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./admin-frontend
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -16,8 +16,8 @@ dist/
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# Claude Code local settings
|
||||
.claude/settings.local.json
|
||||
# Agent scratch files
|
||||
.claude/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
|
||||
29
AGENTS.md
29
AGENTS.md
@@ -18,6 +18,11 @@ DreamWeaver (梦语织机) - AI-powered children's story generation app for ages
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Docker demo stack
|
||||
cp backend/.env.example backend/.env
|
||||
docker compose up -d --build
|
||||
docker compose ps
|
||||
|
||||
# Backend
|
||||
cd backend
|
||||
pip install -e . # Install dependencies
|
||||
@@ -25,7 +30,8 @@ pip install -e ".[dev]" # With dev tools (pytest, ruff)
|
||||
uvicorn app.main:app --reload --port 8000 # Start dev server
|
||||
|
||||
# Celery worker (requires Redis)
|
||||
celery -A app.tasks worker --loglevel=info
|
||||
celery -A app.core.celery_app worker --loglevel=info
|
||||
celery -A app.core.celery_app beat --loglevel=info
|
||||
|
||||
# Database migrations
|
||||
alembic upgrade head # Run migrations
|
||||
@@ -45,6 +51,12 @@ cd frontend
|
||||
npm install
|
||||
npm run dev # Start dev server (port 5173)
|
||||
npm run build # Type-check + build
|
||||
|
||||
# Admin frontend
|
||||
cd admin-frontend
|
||||
npm install
|
||||
npm run dev
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Architecture
|
||||
@@ -97,11 +109,12 @@ frontend/src/
|
||||
- **JWT auth:** Stored in httpOnly cookie, validated via `get_current_user` dependency
|
||||
- **Provider routing:** `provider_router.py` tries providers in order, auto-failover on error
|
||||
- **Background tasks:** Celery workers handle achievements and push notifications
|
||||
- **Proxy in dev:** Vite proxies `/api`, `/auth`, `/admin` to backend (see `vite.config.ts`)
|
||||
- **Docker proxy:** Frontend Nginx proxies `/api`, `/auth`, `/admin`, `/static` to backend services
|
||||
- **Local demo:** `ENABLE_DEMO_PROVIDERS=true` enables deterministic text/image/storybook demo adapters
|
||||
|
||||
## Provider System
|
||||
|
||||
AI providers are configured via env vars (`TEXT_PROVIDERS`, `IMAGE_PROVIDERS`, `TTS_PROVIDERS`) as JSON arrays. The router tries each in order and fails over automatically.
|
||||
AI providers are configured via env vars (`TEXT_PROVIDERS`, `IMAGE_PROVIDERS`, `TTS_PROVIDERS`, `STORYBOOK_PROVIDERS`) as JSON arrays. The router tries each in order and fails over automatically.
|
||||
|
||||
Admin console (disabled by default): Set `ENABLE_ADMIN_CONSOLE=true` to enable `/admin/providers` CRUD endpoints with Basic Auth (`ADMIN_USERNAME`/`ADMIN_PASSWORD`).
|
||||
|
||||
@@ -112,7 +125,8 @@ See `backend/.env.example` for required variables:
|
||||
- `DATABASE_URL`, `SECRET_KEY` (required)
|
||||
- OAuth: `GITHUB_CLIENT_ID/SECRET`, `GOOGLE_CLIENT_ID/SECRET`
|
||||
- AI: `TEXT_API_KEY`, `TTS_API_BASE`, `TTS_API_KEY`, `IMAGE_API_KEY`
|
||||
- Providers: `TEXT_PROVIDERS`, `IMAGE_PROVIDERS`, `TTS_PROVIDERS` (JSON arrays)
|
||||
- Providers: `TEXT_PROVIDERS`, `IMAGE_PROVIDERS`, `TTS_PROVIDERS`, `STORYBOOK_PROVIDERS` (JSON arrays)
|
||||
- Demo mode: `ENABLE_DEMO_PROVIDERS`
|
||||
- Celery: `CELERY_BROKER_URL`, `CELERY_RESULT_BACKEND` (Redis URLs)
|
||||
- Admin: `ENABLE_ADMIN_CONSOLE`, `ADMIN_USERNAME`, `ADMIN_PASSWORD`
|
||||
|
||||
@@ -121,10 +135,11 @@ See `backend/.env.example` for required variables:
|
||||
| Method | Route | Description |
|
||||
| ------------------- | -------------------------- | --------------------------- |
|
||||
| GET | `/auth/{provider}/signin` | OAuth login |
|
||||
| GET | `/auth/dev/signin` | Local dev login |
|
||||
| GET | `/auth/session` | Get current user |
|
||||
| POST | `/api/generate` | Generate/enhance story |
|
||||
| POST | `/api/image/generate/{id}` | Generate cover image |
|
||||
| GET | `/api/audio/{id}` | Get TTS audio |
|
||||
| POST | `/api/stories/generate/full` | Generate story + assets |
|
||||
| POST | `/api/storybook/generate` | Generate storybook |
|
||||
| POST | `/api/stories/{id}/assets/retry` | Retry cover/audio assets |
|
||||
| GET | `/api/stories` | List stories (paginated) |
|
||||
| GET/DELETE | `/api/stories/{id}` | Story CRUD |
|
||||
| CRUD | `/api/profiles` | User profiles |
|
||||
|
||||
134
CLAUDE.md
134
CLAUDE.md
@@ -1,134 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
DreamWeaver (梦语织机) - AI-powered children's story generation app for ages 3-8. Features story generation from keywords, story enhancement, AI-generated cover images, and text-to-speech narration.
|
||||
|
||||
**Language:** Chinese (Simplified) for UI and comments.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Backend:** FastAPI + PostgreSQL (Neon) + SQLAlchemy (async) + Celery/Redis | **Python 3.11+**
|
||||
- **Frontend:** Vue 3 + TypeScript + Pinia + Tailwind CSS + vue-i18n
|
||||
- **Auth:** OAuth 2.0 (GitHub/Google) with JWT in httpOnly cookie
|
||||
- **AI Services:** Text generation, image generation, TTS (pluggable adapters)
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Backend
|
||||
cd backend
|
||||
pip install -e . # Install dependencies
|
||||
pip install -e ".[dev]" # With dev tools (pytest, ruff)
|
||||
uvicorn app.main:app --reload --port 8000 # Start dev server
|
||||
|
||||
# Celery worker (requires Redis)
|
||||
celery -A app.tasks worker --loglevel=info
|
||||
|
||||
# Database migrations
|
||||
alembic upgrade head # Run migrations
|
||||
alembic revision -m "message" --autogenerate # Generate new migration
|
||||
|
||||
# Linting
|
||||
ruff check app/ # Check code
|
||||
ruff check app/ --fix # Auto-fix
|
||||
|
||||
# Testing
|
||||
pytest # Run all tests
|
||||
pytest tests/test_auth.py -v # Single test file
|
||||
pytest -k "test_name" # Match pattern
|
||||
|
||||
# Frontend
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev # Start dev server (port 5173)
|
||||
npm run build # Type-check + build
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
backend/app/
|
||||
├── main.py # FastAPI app entry, routes registration
|
||||
├── api/
|
||||
│ ├── auth.py # OAuth routes (GitHub/Google)
|
||||
│ ├── stories.py # Story CRUD and AI generation endpoints
|
||||
│ ├── profiles.py # User profile management
|
||||
│ ├── universes.py # Story universe/world management
|
||||
│ ├── reading_events.py # Reading progress tracking
|
||||
│ ├── push_configs.py # Push notification settings
|
||||
│ ├── admin_providers.py # Provider CRUD (admin)
|
||||
│ └── admin_reload.py # Hot-reload providers (admin)
|
||||
├── core/
|
||||
│ ├── config.py # Pydantic settings from env
|
||||
│ ├── deps.py # Dependency injection (auth, db session)
|
||||
│ ├── security.py # JWT token create/verify
|
||||
│ ├── prompts.py # AI prompt templates
|
||||
│ └── admin_auth.py # Basic Auth for admin routes
|
||||
├── db/
|
||||
│ ├── database.py # SQLAlchemy async engine + session
|
||||
│ ├── models.py # User, Story models
|
||||
│ └── admin_models.py # Provider model
|
||||
├── services/
|
||||
│ ├── adapters/ # Capability adapters (text, image, tts)
|
||||
│ ├── provider_router.py # Failover routing across providers
|
||||
│ ├── provider_cache.py # In-memory provider config cache
|
||||
│ ├── provider_metrics.py # Provider performance metrics
|
||||
│ └── achievement_extractor.py # Extract achievements from stories
|
||||
└── tasks/
|
||||
├── achievements.py # Celery task: achievement processing
|
||||
└── push_notifications.py # Celery task: push notifications
|
||||
|
||||
frontend/src/
|
||||
├── api/client.ts # Axios wrapper with auth interceptors
|
||||
├── stores/user.ts # Pinia user state
|
||||
├── router.ts # Vue Router config
|
||||
├── i18n.ts + locales/ # vue-i18n setup
|
||||
├── components/ # Reusable Vue components
|
||||
└── views/ # Page components
|
||||
```
|
||||
|
||||
## Key Patterns
|
||||
|
||||
- **Async everywhere:** All database and API calls use async/await
|
||||
- **Dependency injection:** FastAPI `Depends()` for auth and db session
|
||||
- **JWT auth:** Stored in httpOnly cookie, validated via `get_current_user` dependency
|
||||
- **Provider routing:** `provider_router.py` tries providers in order, auto-failover on error
|
||||
- **Background tasks:** Celery workers handle achievements and push notifications
|
||||
- **Proxy in dev:** Vite proxies `/api`, `/auth`, `/admin` to backend (see `vite.config.ts`)
|
||||
|
||||
## Provider System
|
||||
|
||||
AI providers are configured via env vars (`TEXT_PROVIDERS`, `IMAGE_PROVIDERS`, `TTS_PROVIDERS`) as JSON arrays. The router tries each in order and fails over automatically.
|
||||
|
||||
Admin console (disabled by default): Set `ENABLE_ADMIN_CONSOLE=true` to enable `/admin/providers` CRUD endpoints with Basic Auth (`ADMIN_USERNAME`/`ADMIN_PASSWORD`).
|
||||
|
||||
## Environment Variables
|
||||
|
||||
See `backend/.env.example` for required variables:
|
||||
|
||||
- `DATABASE_URL`, `SECRET_KEY` (required)
|
||||
- OAuth: `GITHUB_CLIENT_ID/SECRET`, `GOOGLE_CLIENT_ID/SECRET`
|
||||
- AI: `TEXT_API_KEY`, `TTS_API_BASE`, `TTS_API_KEY`, `IMAGE_API_KEY`
|
||||
- Providers: `TEXT_PROVIDERS`, `IMAGE_PROVIDERS`, `TTS_PROVIDERS` (JSON arrays)
|
||||
- Celery: `CELERY_BROKER_URL`, `CELERY_RESULT_BACKEND` (Redis URLs)
|
||||
- Admin: `ENABLE_ADMIN_CONSOLE`, `ADMIN_USERNAME`, `ADMIN_PASSWORD`
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Method | Route | Description |
|
||||
| ------------------- | -------------------------- | --------------------------- |
|
||||
| GET | `/auth/{provider}/signin` | OAuth login |
|
||||
| GET | `/auth/session` | Get current user |
|
||||
| POST | `/api/generate` | Generate/enhance story |
|
||||
| POST | `/api/image/generate/{id}` | Generate cover image |
|
||||
| GET | `/api/audio/{id}` | Get TTS audio |
|
||||
| GET | `/api/stories` | List stories (paginated) |
|
||||
| GET/DELETE | `/api/stories/{id}` | Story CRUD |
|
||||
| CRUD | `/api/profiles` | User profiles |
|
||||
| CRUD | `/api/universes` | Story universes |
|
||||
| CRUD | `/api/reading-events` | Reading progress |
|
||||
| CRUD | `/api/push-configs` | Push notification settings |
|
||||
| GET/POST/PUT/DELETE | `/admin/providers` | Provider management (admin) |
|
||||
288
README.md
288
README.md
@@ -1,208 +1,144 @@
|
||||
# DreamWeaver
|
||||
# DreamWeaver 梦语织机
|
||||
|
||||
AI 驱动的儿童故事生成应用,面向 3-8 岁儿童提供个性化童话创作。
|
||||
AI 儿童故事生成应用,面向 3-8 岁儿童与家长,支持关键词生成故事、绘本页生成、封面图生成、语音朗读、孩子档案、故事宇宙与 Provider 管理。
|
||||
|
||||
当前仓库优先服务一个目标:本地 Docker 环境中稳定跑通完整产品闭环,便于求职演示和后续迭代。
|
||||
|
||||
## 技术栈
|
||||
- 后端:FastAPI、SQLAlchemy (async)、PostgreSQL,OAuth (GitHub/Google)
|
||||
- AI:文本生成、图像生成、语音合成(可替换适配器)
|
||||
- 前端:Vue 3、TypeScript、Pinia、Vue Router、Vite
|
||||
|
||||
- 后端:FastAPI、SQLAlchemy async、PostgreSQL、Celery、Redis
|
||||
- 前端:Vue 3、TypeScript、Pinia、Vue Router、Tailwind CSS、Vite
|
||||
- 认证:OAuth 2.0 + JWT httpOnly cookie,本地演示支持 dev login
|
||||
- AI:文本、图片、TTS、绘本结构生成均通过 adapter/provider router 接入
|
||||
|
||||
## 仓库结构
|
||||
```
|
||||
backend/
|
||||
app/ # 后端代码
|
||||
alembic/ # 数据库迁移
|
||||
pyproject.toml
|
||||
.env.example
|
||||
frontend/
|
||||
src/ # 前端代码
|
||||
package.json
|
||||
|
||||
```text
|
||||
backend/ FastAPI 后端、Celery 任务、数据库迁移
|
||||
frontend/ 用户端 Vue 应用
|
||||
admin-frontend/ 管理端 Vue 应用
|
||||
docs/ 当前产品、规划与技术文档
|
||||
docker-compose.yml
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
### 后端
|
||||
## 本地 Docker 演示
|
||||
|
||||
1. 准备环境文件:
|
||||
|
||||
```bash
|
||||
cp backend/.env.example backend/.env
|
||||
```
|
||||
|
||||
本地求职演示建议在 `backend/.env` 中使用:
|
||||
|
||||
```env
|
||||
DEBUG=true
|
||||
ENABLE_DEMO_PROVIDERS=true
|
||||
TEXT_PROVIDERS=["demo", "gemini", "openai"]
|
||||
IMAGE_PROVIDERS=["demo", "cqtai"]
|
||||
TTS_PROVIDERS=["edge_tts", "minimax", "elevenlabs"]
|
||||
STORYBOOK_PROVIDERS=["demo", "storybook_primary"]
|
||||
```
|
||||
|
||||
`SECRET_KEY` 必须设置为强随机值。`backend/.env` 已被 git 忽略,不要提交真实密钥。
|
||||
|
||||
2. 启动完整本地栈:
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
3. 访问入口:
|
||||
|
||||
- 用户端:http://localhost:52080
|
||||
- 本地开发登录:http://localhost:52080/auth/dev/signin
|
||||
- 管理端:http://localhost:52888
|
||||
- 后端健康检查:http://localhost:52000/health
|
||||
- 管理后端健康检查:http://localhost:52800/health
|
||||
|
||||
4. 常用命令:
|
||||
|
||||
```bash
|
||||
docker compose ps
|
||||
docker compose logs -f backend
|
||||
docker compose down
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
## 手动开发
|
||||
|
||||
后端:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
python -m venv .venv
|
||||
.\.venv\Scripts\activate # Linux/Mac: source .venv/bin/activate
|
||||
pip install -e .
|
||||
|
||||
cp .env.example .env # 填写 SECRET_KEY、DATABASE_URL、各 API Key
|
||||
|
||||
# 运行数据库迁移(先配置好 DATABASE_URL)
|
||||
pip install -e ".[dev]"
|
||||
alembic upgrade head
|
||||
|
||||
# 启动
|
||||
uvicorn app.main:app --reload --port 8000
|
||||
```
|
||||
|
||||
### 前端
|
||||
Celery:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
celery -A app.core.celery_app worker --loglevel=info
|
||||
celery -A app.core.celery_app beat --loglevel=info
|
||||
```
|
||||
|
||||
用户前端:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
前端常用环境变量(可放 `.env.development`):
|
||||
- `VITE_API_BASE`:后端地址,例如 `http://localhost:8000`
|
||||
- `VITE_ADMIN_USER` / `VITE_ADMIN_PASS`:管理后台 Basic Auth 账号(仅后台开启时需要)
|
||||
|
||||
### 访问
|
||||
- 前端:http://localhost:5173
|
||||
- 后端 API:http://localhost:8000
|
||||
- Swagger 文档:http://localhost:8000/docs
|
||||
管理前端:
|
||||
|
||||
## Docker Compose 使用说明
|
||||
本项目包含 3 个 compose 文件:
|
||||
|
||||
- `docker-compose.yml`:开发基线,包含本地构建(`build`)配置,适合日常开发调试。
|
||||
- `docker-compose.prod.yml`:生产基线,使用预构建镜像(不本地构建),适合部署环境。
|
||||
- `docker-compose.ha.yml`:HA 覆盖层,提供 PostgreSQL 主从、Redis 主从 + Sentinel、备份任务。
|
||||
|
||||
### 使用选择
|
||||
- 本地开发:使用 `docker-compose.yml`
|
||||
- 生产部署:使用 `docker-compose.prod.yml`
|
||||
- 需要高可用:在上面任一基线上叠加 `docker-compose.ha.yml`
|
||||
|
||||
> 注意:`docker-compose.ha.yml` 是覆盖文件,不能单独使用。
|
||||
|
||||
### 常用命令
|
||||
|
||||
#### 开发模式(单机)
|
||||
```bash
|
||||
docker compose -f docker-compose.yml up -d
|
||||
cd admin-frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
#### 开发 + HA(主从/哨兵演练)
|
||||
## 质量检查
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.ha.yml up -d
|
||||
cd backend
|
||||
pytest
|
||||
ruff check app tests
|
||||
|
||||
cd ../frontend
|
||||
npm run build
|
||||
|
||||
cd ../admin-frontend
|
||||
npm run build
|
||||
```
|
||||
|
||||
#### 生产模式(预构建镜像)
|
||||
```bash
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
当前已知情况:完整后端测试可通过;全量 ruff 仍有少量历史 lint 债,优先处理核心演示链路与新增代码。
|
||||
|
||||
#### 生产 + HA
|
||||
```bash
|
||||
docker compose -f docker-compose.prod.yml -f docker-compose.ha.yml up -d
|
||||
```
|
||||
## 核心接口
|
||||
|
||||
#### 查看状态 / 日志
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.ha.yml ps
|
||||
docker compose -f docker-compose.yml -f docker-compose.ha.yml logs -f backend
|
||||
```
|
||||
|
||||
#### 停止并清理(含卷)
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.ha.yml down -v
|
||||
```
|
||||
|
||||
### `docker-compose.prod.yml` 镜像标签
|
||||
`docker-compose.prod.yml` 使用以下镜像格式:
|
||||
- `${REGISTRY:-}dreamweaver-backend:${TAG:-latest}`
|
||||
- `${REGISTRY:-}dreamweaver-frontend:${TAG:-latest}`
|
||||
- `${REGISTRY:-}dreamweaver-admin-frontend:${TAG:-latest}`
|
||||
|
||||
Linux 部署示例(推荐):
|
||||
```bash
|
||||
export REGISTRY=my-registry.example.com/
|
||||
export TAG=2026.02.12
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
Windows PowerShell 示例:
|
||||
```powershell
|
||||
$env:REGISTRY="my-registry.example.com/"
|
||||
$env:TAG="2026.02.12"
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### Linux 服务器部署流程(推荐)
|
||||
以下流程适用于 Ubuntu/CentOS 等 Linux 服务器:
|
||||
|
||||
#### 1) 准备配置
|
||||
```bash
|
||||
cp backend/.env.example backend/.env
|
||||
# 编辑 backend/.env,至少配置 SECRET_KEY、OAuth/API Key 等
|
||||
```
|
||||
|
||||
#### 2) 启动(非 HA)
|
||||
```bash
|
||||
export REGISTRY=my-registry.example.com/
|
||||
export TAG=2026.02.12
|
||||
docker compose -f docker-compose.prod.yml pull
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
#### 3) 启动(HA)
|
||||
```bash
|
||||
export REGISTRY=my-registry.example.com/
|
||||
export TAG=2026.02.12
|
||||
docker compose -f docker-compose.prod.yml -f docker-compose.ha.yml pull
|
||||
docker compose -f docker-compose.prod.yml -f docker-compose.ha.yml up -d
|
||||
```
|
||||
|
||||
#### 4) 运行状态检查
|
||||
```bash
|
||||
docker compose -f docker-compose.prod.yml ps
|
||||
docker compose -f docker-compose.prod.yml logs -f backend
|
||||
```
|
||||
HA 场景使用:
|
||||
```bash
|
||||
docker compose -f docker-compose.prod.yml -f docker-compose.ha.yml ps
|
||||
docker compose -f docker-compose.prod.yml -f docker-compose.ha.yml logs -f backend
|
||||
```
|
||||
|
||||
#### 5) 版本升级
|
||||
```bash
|
||||
export TAG=2026.02.13
|
||||
docker compose -f docker-compose.prod.yml pull
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
HA 场景同理,在命令中额外叠加 `-f docker-compose.ha.yml`。
|
||||
|
||||
#### 6) 版本回滚
|
||||
```bash
|
||||
export TAG=2026.02.12
|
||||
docker compose -f docker-compose.prod.yml pull
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
## 供应商路由与管理后台
|
||||
- 路由按配置顺序尝试:`TEXT_PROVIDERS`(默认 `text_primary`)、`IMAGE_PROVIDERS`(默认 `image_primary`)、`TTS_PROVIDERS`(默认 `tts_primary`)。失败会自动切换下一个。
|
||||
- 管理后台(默认关闭):`ENABLE_ADMIN_CONSOLE=true` 时启用,接口在 `/admin/providers`(CRUD)和 `/admin/providers/reload`。鉴权使用 Basic Auth,账号密码由 `ADMIN_USERNAME`/`ADMIN_PASSWORD` 设置(请覆盖默认值)。
|
||||
- 建议后台放在受保护子域并在反代层加 Basic Auth/IP 白名单;生产环境默认关闭。
|
||||
- 前端最小管理页:`/admin/providers`,仅后台开启时可用,使用上面的 Basic Auth 调用。
|
||||
|
||||
## 数据库迁移(Alembic)
|
||||
- 运行迁移:`alembic upgrade head`
|
||||
- 生成新迁移:`alembic revision -m "message" --autogenerate`
|
||||
迁移脚本位于 `backend/alembic/versions/`,包含 `providers` 表和 `stories.mode` 字段。
|
||||
|
||||
## 主要 API
|
||||
| 方法 | 路径 | 说明 |
|
||||
| ---- | ---- | ---- |
|
||||
| GET | `/auth/github/signin` | GitHub 登录 |
|
||||
| GET | `/auth/google/signin` | Google 登录 |
|
||||
| --- | --- | --- |
|
||||
| GET | `/auth/dev/signin` | 本地开发登录,仅 `DEBUG=true` 可用 |
|
||||
| GET | `/auth/github/signin` | GitHub OAuth 登录 |
|
||||
| GET | `/auth/google/signin` | Google OAuth 登录 |
|
||||
| GET | `/auth/session` | 当前会话 |
|
||||
| POST | `/api/generate` | 生成/润色故事 |
|
||||
| POST | `/api/image/generate/{id}` | 生成封面图 |
|
||||
| GET | `/api/audio/{id}` | 获取 TTS 音频 |
|
||||
| GET | `/api/stories` | 获取故事列表(分页) |
|
||||
| GET | `/api/stories/{id}` | 获取故事详情 |
|
||||
| DELETE | `/api/stories/{id}` | 删除故事 |
|
||||
| GET | `/admin/providers` | Provider 列表(需后台开启 + 管理员) |
|
||||
| POST | `/api/stories/generate/full` | 生成故事并尝试生成封面 |
|
||||
| POST | `/api/storybook/generate` | 生成绘本 |
|
||||
| POST | `/api/stories/{story_id}/assets/retry` | 统一重试封面/语音资源 |
|
||||
| GET | `/api/stories` | 故事列表 |
|
||||
| GET | `/api/stories/{story_id}` | 故事详情 |
|
||||
| DELETE | `/api/stories/{story_id}` | 删除故事 |
|
||||
| GET/POST/PUT/DELETE | `/admin/providers` | Provider 管理,需开启管理后台 |
|
||||
|
||||
## 环境变量
|
||||
常用项(详见 `backend/.env.example`):
|
||||
- `SECRET_KEY`(必填):JWT 签名密钥
|
||||
- `DATABASE_URL`(必填):PostgreSQL 连接串
|
||||
- OAuth:`GITHUB_CLIENT_ID/SECRET`,`GOOGLE_CLIENT_ID/SECRET`
|
||||
- AI Keys:`TEXT_API_KEY`,`TTS_API_BASE`,`TTS_API_KEY`,`IMAGE_API_KEY`
|
||||
- Provider 列表:`TEXT_PROVIDERS`,`IMAGE_PROVIDERS`,`TTS_PROVIDERS`
|
||||
- 管理后台:`ENABLE_ADMIN_CONSOLE`,`ADMIN_USERNAME`,`ADMIN_PASSWORD`
|
||||
## 文档入口
|
||||
|
||||
## 注意
|
||||
- 生产务必使用强随机 `SECRET_KEY`,并关闭 `ENABLE_ADMIN_CONSOLE`(或放在受保护子域/内网)。
|
||||
- 路由会按配置/DB 顺序尝试供应商并失败切换,请确保各 Key 有效且配额充足。
|
||||
- `docs/product/job-search-relaunch-prd.md`:求职版产品重启 PRD
|
||||
- `docs/product/unified-generation-workflow-prd.md`:统一生成工作流 PRD
|
||||
- `docs/planning/week-1-execution-backlog.md`:短期执行 backlog
|
||||
- `docs/technical/memory-system-dev.md`:记忆系统技术说明
|
||||
|
||||
## 当前取舍
|
||||
|
||||
仓库只保留一个 Docker Compose 入口:`docker-compose.yml`。生产部署、HA 演练、旧 Claude 原型和历史归档已从主仓库移除,避免干扰当前求职演示主线。
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
# docker-compose.ha.yml
|
||||
# HA 覆盖配置(建议与 docker-compose.yml 叠加使用)
|
||||
#
|
||||
# 启动示例:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.ha.yml up -d
|
||||
|
||||
services:
|
||||
# ==============================================
|
||||
# 应用服务 Sentinel 配置覆盖
|
||||
# ==============================================
|
||||
backend:
|
||||
environment:
|
||||
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
|
||||
- REDIS_SENTINEL_ENABLED=true
|
||||
- REDIS_SENTINEL_NODES=redis-sentinel-1:26379,redis-sentinel-2:26379,redis-sentinel-3:26379
|
||||
- REDIS_SENTINEL_MASTER_NAME=mymaster
|
||||
- REDIS_SENTINEL_DB=0
|
||||
- REDIS_SENTINEL_SOCKET_TIMEOUT=0.5
|
||||
|
||||
backend-admin:
|
||||
environment:
|
||||
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
|
||||
- REDIS_SENTINEL_ENABLED=true
|
||||
- REDIS_SENTINEL_NODES=redis-sentinel-1:26379,redis-sentinel-2:26379,redis-sentinel-3:26379
|
||||
- REDIS_SENTINEL_MASTER_NAME=mymaster
|
||||
- REDIS_SENTINEL_DB=0
|
||||
- REDIS_SENTINEL_SOCKET_TIMEOUT=0.5
|
||||
|
||||
worker:
|
||||
environment:
|
||||
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
|
||||
- REDIS_SENTINEL_ENABLED=true
|
||||
- REDIS_SENTINEL_NODES=redis-sentinel-1:26379,redis-sentinel-2:26379,redis-sentinel-3:26379
|
||||
- REDIS_SENTINEL_MASTER_NAME=mymaster
|
||||
- REDIS_SENTINEL_DB=0
|
||||
- REDIS_SENTINEL_SOCKET_TIMEOUT=0.5
|
||||
|
||||
celery-beat:
|
||||
environment:
|
||||
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
|
||||
- REDIS_SENTINEL_ENABLED=true
|
||||
- REDIS_SENTINEL_NODES=redis-sentinel-1:26379,redis-sentinel-2:26379,redis-sentinel-3:26379
|
||||
- REDIS_SENTINEL_MASTER_NAME=mymaster
|
||||
- REDIS_SENTINEL_DB=0
|
||||
- REDIS_SENTINEL_SOCKET_TIMEOUT=0.5
|
||||
|
||||
# ==============================================
|
||||
# PostgreSQL 主库(覆盖默认 db)
|
||||
# ==============================================
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
container_name: dreamweaver_db_primary
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-dreamweaver}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-dreamweaver_password}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-dreamweaver_db}
|
||||
command:
|
||||
- postgres
|
||||
- -c
|
||||
- wal_level=replica
|
||||
- -c
|
||||
- max_wal_senders=10
|
||||
- -c
|
||||
- max_replication_slots=10
|
||||
- -c
|
||||
- hot_standby=on
|
||||
- -c
|
||||
- hba_file=/etc/postgresql/pg_hba.conf
|
||||
ports:
|
||||
- "52432:5432"
|
||||
volumes:
|
||||
- postgres_primary_data:/var/lib/postgresql/data
|
||||
- ./ops/postgres-ha/pg_hba.conf:/etc/postgresql/pg_hba.conf:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-dreamweaver} -d ${POSTGRES_DB:-dreamweaver_db}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
# ==============================================
|
||||
# PostgreSQL 从库(基于 pg_basebackup 初始化)
|
||||
# ==============================================
|
||||
db-replica:
|
||||
image: postgres:15-alpine
|
||||
container_name: dreamweaver_db_replica
|
||||
restart: unless-stopped
|
||||
user: "postgres"
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-dreamweaver}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-dreamweaver_password}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-dreamweaver_db}
|
||||
PGDATA: /var/lib/postgresql/data
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- postgres_replica_data:/var/lib/postgresql/data
|
||||
command:
|
||||
- /bin/sh
|
||||
- -ec
|
||||
- |
|
||||
if [ ! -s "$$PGDATA/PG_VERSION" ]; then
|
||||
echo "Initializing replica from primary..."
|
||||
until pg_isready -h db -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"; do sleep 2; done
|
||||
export PGPASSWORD="$$POSTGRES_PASSWORD"
|
||||
rm -rf "$$PGDATA"/*
|
||||
pg_basebackup -h db -D "$$PGDATA" -U "$$POSTGRES_USER" -Fp -Xs -P -R
|
||||
fi
|
||||
chmod 700 "$$PGDATA"
|
||||
exec postgres -c hot_standby=on
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"pg_isready -U ${POSTGRES_USER:-dreamweaver} -d ${POSTGRES_DB:-dreamweaver_db} && psql -U ${POSTGRES_USER:-dreamweaver} -d ${POSTGRES_DB:-dreamweaver_db} -tAc 'select pg_is_in_recovery();' | grep -q t",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
# ==============================================
|
||||
# PostgreSQL 备份任务(每日一次,保留 7 天)
|
||||
# ==============================================
|
||||
postgres-backup:
|
||||
image: postgres:15-alpine
|
||||
container_name: dreamweaver_postgres_backup
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-dreamweaver}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-dreamweaver_db}
|
||||
PGPASSWORD: ${POSTGRES_PASSWORD:-dreamweaver_password}
|
||||
BACKUP_INTERVAL_SECONDS: ${BACKUP_INTERVAL_SECONDS:-86400}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- postgres_backups:/backups
|
||||
command:
|
||||
- /bin/sh
|
||||
- -ec
|
||||
- |
|
||||
while true; do
|
||||
ts=$$(date +%Y%m%d_%H%M%S);
|
||||
pg_dump -h db -U "$$POSTGRES_USER" -d "$$POSTGRES_DB" -F c -f "/backups/dreamweaver_$${ts}.dump";
|
||||
find /backups -type f -name '*.dump' -mtime +7 -delete;
|
||||
sleep "$$BACKUP_INTERVAL_SECONDS";
|
||||
done
|
||||
|
||||
# ==============================================
|
||||
# Redis 主库(覆盖默认 redis)
|
||||
# ==============================================
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: dreamweaver_redis_master
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "52379:6379"
|
||||
volumes:
|
||||
- redis_master_data:/data
|
||||
command: ["redis-server", "--appendonly", "yes", "--protected-mode", "no"]
|
||||
networks:
|
||||
default:
|
||||
ipv4_address: 172.29.0.10
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
# ==============================================
|
||||
# Redis 从库
|
||||
# ==============================================
|
||||
redis-replica:
|
||||
image: redis:7-alpine
|
||||
container_name: dreamweaver_redis_replica
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- redis_replica_data:/data
|
||||
command:
|
||||
[
|
||||
"redis-server",
|
||||
"--appendonly",
|
||||
"yes",
|
||||
"--protected-mode",
|
||||
"no",
|
||||
"--replicaof",
|
||||
"172.29.0.10",
|
||||
"6379",
|
||||
]
|
||||
networks:
|
||||
default:
|
||||
ipv4_address: 172.29.0.11
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
# ==============================================
|
||||
# Redis Sentinel (3 节点)
|
||||
# ==============================================
|
||||
redis-sentinel-1:
|
||||
image: redis:7-alpine
|
||||
container_name: dreamweaver_redis_sentinel_1
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "52631:26379"
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
redis-replica:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
default:
|
||||
ipv4_address: 172.29.0.21
|
||||
command:
|
||||
- /bin/sh
|
||||
- -ec
|
||||
- |
|
||||
cat > /tmp/sentinel.conf <<EOF
|
||||
port 26379
|
||||
sentinel resolve-hostnames yes
|
||||
sentinel announce-hostnames yes
|
||||
sentinel monitor mymaster 172.29.0.10 6379 2
|
||||
sentinel down-after-milliseconds mymaster 5000
|
||||
sentinel failover-timeout mymaster 10000
|
||||
sentinel parallel-syncs mymaster 1
|
||||
protected-mode no
|
||||
dir /tmp
|
||||
EOF
|
||||
exec redis-sentinel /tmp/sentinel.conf
|
||||
|
||||
redis-sentinel-2:
|
||||
image: redis:7-alpine
|
||||
container_name: dreamweaver_redis_sentinel_2
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "52632:26379"
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
redis-replica:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
default:
|
||||
ipv4_address: 172.29.0.22
|
||||
command:
|
||||
- /bin/sh
|
||||
- -ec
|
||||
- |
|
||||
cat > /tmp/sentinel.conf <<EOF
|
||||
port 26379
|
||||
sentinel resolve-hostnames yes
|
||||
sentinel announce-hostnames yes
|
||||
sentinel monitor mymaster 172.29.0.10 6379 2
|
||||
sentinel down-after-milliseconds mymaster 5000
|
||||
sentinel failover-timeout mymaster 10000
|
||||
sentinel parallel-syncs mymaster 1
|
||||
protected-mode no
|
||||
dir /tmp
|
||||
EOF
|
||||
exec redis-sentinel /tmp/sentinel.conf
|
||||
|
||||
redis-sentinel-3:
|
||||
image: redis:7-alpine
|
||||
container_name: dreamweaver_redis_sentinel_3
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "52633:26379"
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
redis-replica:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
default:
|
||||
ipv4_address: 172.29.0.23
|
||||
command:
|
||||
- /bin/sh
|
||||
- -ec
|
||||
- |
|
||||
cat > /tmp/sentinel.conf <<EOF
|
||||
port 26379
|
||||
sentinel resolve-hostnames yes
|
||||
sentinel announce-hostnames yes
|
||||
sentinel monitor mymaster 172.29.0.10 6379 2
|
||||
sentinel down-after-milliseconds mymaster 5000
|
||||
sentinel failover-timeout mymaster 10000
|
||||
sentinel parallel-syncs mymaster 1
|
||||
protected-mode no
|
||||
dir /tmp
|
||||
EOF
|
||||
exec redis-sentinel /tmp/sentinel.conf
|
||||
|
||||
volumes:
|
||||
postgres_primary_data:
|
||||
postgres_replica_data:
|
||||
postgres_backups:
|
||||
redis_master_data:
|
||||
redis_replica_data:
|
||||
|
||||
networks:
|
||||
default:
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.29.0.0/24
|
||||
@@ -1,158 +0,0 @@
|
||||
# docker-compose.prod.yml
|
||||
# 生产部署配置 - 使用预构建镜像,不包含 build 指令
|
||||
# 镜像通过 GitHub Actions 或本地 docker build 预先构建
|
||||
#
|
||||
# 使用方式:
|
||||
# docker compose -f docker-compose.prod.yml up -d
|
||||
#
|
||||
# 镜像构建 (手动):
|
||||
# docker build -t dreamweaver-backend:latest ./backend
|
||||
# docker build -t dreamweaver-frontend:latest ./frontend
|
||||
# docker build -t dreamweaver-admin-frontend:latest ./admin-frontend
|
||||
|
||||
services:
|
||||
# ==============================================
|
||||
# 前端服务 (C端用户 App)
|
||||
# ==============================================
|
||||
frontend:
|
||||
image: ${REGISTRY:-}dreamweaver-frontend:${TAG:-latest}
|
||||
container_name: dreamweaver_frontend
|
||||
restart: always
|
||||
ports:
|
||||
- "52080:80"
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
# ==============================================
|
||||
# 管理后台前端 (Admin Console)
|
||||
# ==============================================
|
||||
frontend-admin:
|
||||
image: ${REGISTRY:-}dreamweaver-admin-frontend:${TAG:-latest}
|
||||
container_name: dreamweaver_frontend_admin
|
||||
restart: always
|
||||
ports:
|
||||
- "52888:80"
|
||||
depends_on:
|
||||
- backend-admin
|
||||
|
||||
# ==============================================
|
||||
# 后端服务 (FastAPI)
|
||||
# ==============================================
|
||||
backend:
|
||||
image: ${REGISTRY:-}dreamweaver-backend:${TAG:-latest}
|
||||
container_name: dreamweaver_backend
|
||||
restart: always
|
||||
ports:
|
||||
- "52000:8000"
|
||||
env_file:
|
||||
- ./backend/.env
|
||||
environment:
|
||||
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
|
||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||
volumes:
|
||||
- backend_static:/app/static
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_started
|
||||
|
||||
# ==============================================
|
||||
# 管理后台后端 (Admin Backend)
|
||||
# ==============================================
|
||||
backend-admin:
|
||||
image: ${REGISTRY:-}dreamweaver-backend:${TAG:-latest}
|
||||
container_name: dreamweaver_backend_admin
|
||||
restart: always
|
||||
ports:
|
||||
- "52800:8001"
|
||||
command: ["uvicorn", "app.admin_main:app", "--host", "0.0.0.0", "--port", "8001"]
|
||||
env_file:
|
||||
- ./backend/.env
|
||||
environment:
|
||||
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
|
||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||
volumes:
|
||||
- backend_static:/app/static
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_started
|
||||
|
||||
# ==============================================
|
||||
# 工作节点 (Celery Worker)
|
||||
# ==============================================
|
||||
worker:
|
||||
image: ${REGISTRY:-}dreamweaver-backend:${TAG:-latest}
|
||||
container_name: dreamweaver_worker
|
||||
command: celery -A app.core.celery_app worker --loglevel=info
|
||||
restart: always
|
||||
env_file: ./backend/.env
|
||||
environment:
|
||||
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
|
||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_started
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
# ==============================================
|
||||
# 调度节点 (Celery Beat)
|
||||
# ==============================================
|
||||
celery-beat:
|
||||
image: ${REGISTRY:-}dreamweaver-backend:${TAG:-latest}
|
||||
container_name: dreamweaver_beat
|
||||
command: celery -A app.core.celery_app beat --loglevel=info
|
||||
restart: always
|
||||
env_file: ./backend/.env
|
||||
environment:
|
||||
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
|
||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_started
|
||||
|
||||
# ==============================================
|
||||
# 数据库 (PostgreSQL)
|
||||
# ==============================================
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
container_name: dreamweaver_db
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-dreamweaver}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-dreamweaver_password}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-dreamweaver_db}
|
||||
ports:
|
||||
- "52432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-dreamweaver}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# ==============================================
|
||||
# 缓存 (Redis)
|
||||
# ==============================================
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: dreamweaver_redis
|
||||
restart: always
|
||||
ports:
|
||||
- "52379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
command: redis-server --appendonly yes
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
backend_static:
|
||||
@@ -1,183 +1,127 @@
|
||||
# docker-compose.yml
|
||||
# 开发环境配置 - 支持本地构建和快速迭代
|
||||
#
|
||||
# 使用方式:
|
||||
# docker compose up -d # 启动所有服务
|
||||
# docker compose up -d --build # 重新构建并启动
|
||||
# docker compose logs -f backend # 查看日志
|
||||
#
|
||||
# 生产部署请使用: docker-compose.prod.yml
|
||||
|
||||
services:
|
||||
# ==============================================
|
||||
# 前端服务 (C端用户 App)
|
||||
# ==============================================
|
||||
frontend:
|
||||
build: ./frontend
|
||||
image: dreamweaver-frontend:dev
|
||||
container_name: dreamweaver_frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "52080:80"
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
# ==============================================
|
||||
# 管理后台前端 (Admin Console)
|
||||
# ==============================================
|
||||
frontend-admin:
|
||||
build: ./admin-frontend
|
||||
image: dreamweaver-admin-frontend:dev
|
||||
container_name: dreamweaver_frontend_admin
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "52888:80"
|
||||
depends_on:
|
||||
- backend-admin
|
||||
|
||||
# ==============================================
|
||||
# 后端服务 (FastAPI)
|
||||
# ==============================================
|
||||
backend:
|
||||
build: ./backend
|
||||
image: dreamweaver-backend:dev
|
||||
container_name: dreamweaver_backend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "52000:8000"
|
||||
env_file:
|
||||
- ./backend/.env
|
||||
environment:
|
||||
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
|
||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
volumes:
|
||||
- backend_static:/app/static
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_started
|
||||
|
||||
# ==============================================
|
||||
# 管理后台后端 (Admin Backend) - 复用 backend 镜像
|
||||
# ==============================================
|
||||
backend-admin:
|
||||
image: dreamweaver-backend:dev
|
||||
container_name: dreamweaver_backend_admin
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "52800:8001"
|
||||
command: ["uvicorn", "app.admin_main:app", "--host", "0.0.0.0", "--port", "8001"]
|
||||
env_file:
|
||||
- ./backend/.env
|
||||
environment:
|
||||
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
|
||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
volumes:
|
||||
- backend_static:/app/static
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_started
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_started
|
||||
|
||||
# ==============================================
|
||||
# 工作节点 (Celery Worker) - 复用 backend 镜像
|
||||
# ==============================================
|
||||
worker:
|
||||
image: dreamweaver-backend:dev
|
||||
container_name: dreamweaver_worker
|
||||
command: celery -A app.core.celery_app worker --loglevel=info
|
||||
restart: unless-stopped
|
||||
env_file: ./backend/.env
|
||||
environment:
|
||||
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
|
||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_started
|
||||
redis:
|
||||
condition: service_started
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
# ==============================================
|
||||
# 调度节点 (Celery Beat) - 复用 backend 镜像
|
||||
# ==============================================
|
||||
celery-beat:
|
||||
image: dreamweaver-backend:dev
|
||||
container_name: dreamweaver_beat
|
||||
command: celery -A app.core.celery_app beat --loglevel=info
|
||||
restart: unless-stopped
|
||||
env_file: ./backend/.env
|
||||
environment:
|
||||
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
|
||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_started
|
||||
redis:
|
||||
condition: service_started
|
||||
|
||||
# ==============================================
|
||||
# 数据库 (PostgreSQL)
|
||||
# ==============================================
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
container_name: dreamweaver_db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-dreamweaver}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-dreamweaver_password}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-dreamweaver_db}
|
||||
ports:
|
||||
- "52432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-dreamweaver}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# ==============================================
|
||||
# 缓存 (Redis)
|
||||
# ==============================================
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: dreamweaver_redis
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "52379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
command: redis-server --appendonly yes
|
||||
|
||||
# ==============================================
|
||||
# 数据库管理 (Adminer) - 仅开发环境
|
||||
# ==============================================
|
||||
adminer:
|
||||
image: adminer
|
||||
container_name: dreamweaver_adminer
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "52999:8080"
|
||||
depends_on:
|
||||
- db
|
||||
profiles:
|
||||
- dev # 仅在 --profile dev 时启动
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
backend_static:
|
||||
name: dreamweaver
|
||||
|
||||
x-backend-env: &backend-env
|
||||
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
|
||||
CELERY_BROKER_URL: redis://redis:6379/0
|
||||
CELERY_RESULT_BACKEND: redis://redis:6379/0
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
|
||||
services:
|
||||
frontend:
|
||||
build: ./frontend
|
||||
image: dreamweaver-frontend:dev
|
||||
container_name: dreamweaver_frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "52080:80"
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_started
|
||||
|
||||
frontend-admin:
|
||||
build: ./admin-frontend
|
||||
image: dreamweaver-admin-frontend:dev
|
||||
container_name: dreamweaver_frontend_admin
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "52888:80"
|
||||
depends_on:
|
||||
backend-admin:
|
||||
condition: service_started
|
||||
|
||||
backend:
|
||||
build: ./backend
|
||||
image: dreamweaver-backend:dev
|
||||
container_name: dreamweaver_backend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "52000:8000"
|
||||
env_file: ./backend/.env
|
||||
environment: *backend-env
|
||||
volumes:
|
||||
- backend_static:/app/static
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_started
|
||||
|
||||
backend-admin:
|
||||
image: dreamweaver-backend:dev
|
||||
container_name: dreamweaver_backend_admin
|
||||
restart: unless-stopped
|
||||
command: ["uvicorn", "app.admin_main:app", "--host", "0.0.0.0", "--port", "8001"]
|
||||
ports:
|
||||
- "52800:8001"
|
||||
env_file: ./backend/.env
|
||||
environment: *backend-env
|
||||
volumes:
|
||||
- backend_static:/app/static
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_started
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_started
|
||||
|
||||
worker:
|
||||
image: dreamweaver-backend:dev
|
||||
container_name: dreamweaver_worker
|
||||
restart: unless-stopped
|
||||
command: celery -A app.core.celery_app worker --loglevel=info
|
||||
env_file: ./backend/.env
|
||||
environment: *backend-env
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_started
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_started
|
||||
|
||||
celery-beat:
|
||||
image: dreamweaver-backend:dev
|
||||
container_name: dreamweaver_beat
|
||||
restart: unless-stopped
|
||||
command: celery -A app.core.celery_app beat --loglevel=info
|
||||
env_file: ./backend/.env
|
||||
environment: *backend-env
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_started
|
||||
redis:
|
||||
condition: service_started
|
||||
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
container_name: dreamweaver_db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-dreamweaver}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-dreamweaver_password}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-dreamweaver_db}
|
||||
ports:
|
||||
- "52432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-dreamweaver} -d ${POSTGRES_DB:-dreamweaver_db}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: dreamweaver_redis
|
||||
restart: unless-stopped
|
||||
command: redis-server --appendonly yes
|
||||
ports:
|
||||
- "52379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
|
||||
volumes:
|
||||
backend_static:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
|
||||
175
docs/README.md
175
docs/README.md
@@ -1,166 +1,29 @@
|
||||
# Documentation Index
|
||||
# 文档索引
|
||||
|
||||
This repository now uses a simple documentation taxonomy so you can quickly tell:
|
||||
当前文档只保留对求职演示和短期迭代有用的内容,避免旧方案、临时交接和原型素材混在主线里。
|
||||
|
||||
- what is current and actionable
|
||||
- what is product-facing vs. technical
|
||||
- what is historical and should not be treated as source of truth
|
||||
## Product
|
||||
|
||||
---
|
||||
- `product/job-search-relaunch-prd.md`
|
||||
求职版产品重启 PRD。用于说明产品定位、用户价值、MVP 范围和面试表达主线。
|
||||
|
||||
## Folder Structure
|
||||
- `product/unified-generation-workflow-prd.md`
|
||||
统一生成工作流 PRD。用于说明故事、绘本、封面、语音如何收敛成一条清晰体验。
|
||||
|
||||
### `docs/product/`
|
||||
## Planning
|
||||
|
||||
Current product documents. These are the best starting point when you want to understand:
|
||||
- `planning/week-1-execution-backlog.md`
|
||||
当前短期执行 backlog。用于决定下一步先做什么,以及如何拆成可交付任务。
|
||||
|
||||
- product positioning
|
||||
- MVP scope
|
||||
- feature requirements
|
||||
- portfolio/presentation story
|
||||
## Technical
|
||||
|
||||
Files:
|
||||
- `technical/memory-system-dev.md`
|
||||
记忆系统技术说明。用于后续继续做孩子档案、故事宇宙和个性化生成。
|
||||
|
||||
- `job-search-relaunch-prd.md`
|
||||
Status: Active
|
||||
Type: Product strategy / reboot PRD
|
||||
Use when: you want the current product direction and prioritization.
|
||||
## 维护规则
|
||||
|
||||
- `unified-generation-workflow-prd.md`
|
||||
Status: Active
|
||||
Type: Feature-level PRD
|
||||
Use when: you want the target design for the core generation workflow.
|
||||
|
||||
### `docs/planning/`
|
||||
|
||||
Execution-oriented documents. These are for sprint planning, backlog breakdown, and short-term delivery.
|
||||
|
||||
Files:
|
||||
|
||||
- `document-status-inventory.md`
|
||||
Status: Active
|
||||
Type: Documentation audit / implementation mapping
|
||||
Use when: you want to know which docs are current, which capabilities are really implemented, and where coding should restart.
|
||||
|
||||
- `week-1-execution-backlog.md`
|
||||
Status: Active
|
||||
Type: Sprint / execution planning
|
||||
Use when: you want to know what to do first and how to break work into tasks.
|
||||
|
||||
- `weekend-handoff-2026-04-17.md`
|
||||
Status: Active
|
||||
Type: Progress handoff / execution snapshot
|
||||
Use when: you want to continue work from another machine without reconstructing the latest checkpoint from chat history.
|
||||
|
||||
### `docs/technical/`
|
||||
|
||||
Technical reference documents. These are implementation-oriented and may include design guidance or development notes.
|
||||
|
||||
Files:
|
||||
|
||||
- `memory-system-dev.md`
|
||||
Status: Reference
|
||||
Type: Technical design / development guide
|
||||
Use when: you work on the memory system or want to study one style of technical design note.
|
||||
Note: parts of this document are forward-looking and should be validated against the current codebase before implementation.
|
||||
|
||||
### `docs/operations/`
|
||||
|
||||
Runbooks and environment/operations documentation.
|
||||
|
||||
Files:
|
||||
|
||||
- `ha-runbook.md`
|
||||
Status: Reference
|
||||
Type: Operations runbook
|
||||
Use when: you work on Docker-based HA deployment, Redis Sentinel, PostgreSQL replication, or backup verification.
|
||||
|
||||
### `docs/archive/`
|
||||
|
||||
Historical documents. Keep these for learning or project history, but do not treat them as the current source of truth.
|
||||
|
||||
Files:
|
||||
|
||||
- `provider-system-legacy.md`
|
||||
Status: Archived
|
||||
Type: Historical technical plan
|
||||
Why archived: partially outdated; references earlier provider architecture and older app entry naming.
|
||||
|
||||
- `refactoring-plan-legacy.md`
|
||||
Status: Archived
|
||||
Type: Historical implementation plan
|
||||
Why archived: reflects an earlier refactor phase; some items are completed, some are no longer current priorities.
|
||||
|
||||
- `stories-split-analysis-legacy.md`
|
||||
Status: Archived
|
||||
Type: Historical code analysis
|
||||
Why archived: tied to a past `stories.py` split effort and no longer represents the current structure.
|
||||
|
||||
---
|
||||
|
||||
## Deleted Document
|
||||
|
||||
The following document was removed instead of archived:
|
||||
|
||||
- `backend/docs/code_review_report.md`
|
||||
Reason: it was a one-off review artifact, not a durable project document, and its main issue was already resolved by the later `0002_add_api_key_to_providers.py` migration.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Reading Order
|
||||
|
||||
If you want to understand the project as a product manager:
|
||||
|
||||
1. `docs/product/job-search-relaunch-prd.md`
|
||||
2. `docs/product/unified-generation-workflow-prd.md`
|
||||
3. `docs/planning/document-status-inventory.md`
|
||||
4. `docs/planning/week-1-execution-backlog.md`
|
||||
5. `docs/planning/weekend-handoff-2026-04-17.md`
|
||||
6. `docs/technical/memory-system-dev.md`
|
||||
7. `docs/operations/ha-runbook.md`
|
||||
|
||||
If you want to understand the project as an engineer:
|
||||
|
||||
1. `docs/planning/document-status-inventory.md`
|
||||
2. `docs/product/unified-generation-workflow-prd.md`
|
||||
3. `docs/technical/memory-system-dev.md`
|
||||
4. `docs/operations/ha-runbook.md`
|
||||
5. `docs/archive/*` only when you need historical context
|
||||
|
||||
---
|
||||
|
||||
## Documentation Rules Going Forward
|
||||
|
||||
When adding a new document, place it using these rules:
|
||||
|
||||
- Put it in `docs/product/` if it explains what should be built and why.
|
||||
- Put it in `docs/planning/` if it explains when or in what order work should happen.
|
||||
- Put it in `docs/technical/` if it explains how something works or should be implemented.
|
||||
- Put it in `docs/operations/` if it is about deployment, environments, runbooks, or maintenance.
|
||||
- Put it in `docs/archive/` if it is historically useful but no longer current.
|
||||
|
||||
Delete a document only when all three are true:
|
||||
|
||||
- it is a one-off artifact
|
||||
- it is not a reusable reference
|
||||
- its key information is either obsolete or already captured elsewhere
|
||||
|
||||
Archive instead of deleting when:
|
||||
|
||||
- the document shows project history
|
||||
- the document may still help future debugging or learning
|
||||
- you are not fully sure whether it is still valuable
|
||||
|
||||
---
|
||||
|
||||
## PM Learning Note
|
||||
|
||||
A good documentation system helps you think clearly:
|
||||
|
||||
- `product` tells you what problem you are solving
|
||||
- `planning` tells you what to do next
|
||||
- `technical` tells you how the system works
|
||||
- `operations` tells you how to run it
|
||||
- `archive` tells you what used to be true
|
||||
|
||||
That separation is useful not only for this repo, but also as a general PM habit. Many product documents become confusing because they mix all five at once.
|
||||
- 新 PRD 放到 `docs/product/`
|
||||
- 新执行计划放到 `docs/planning/`
|
||||
- 新技术说明放到 `docs/technical/`
|
||||
- 一次性记录、过期交接、旧原型、未验证部署实验不再放进主仓库
|
||||
- 如果某份文档不能帮助“演示产品、解释决策、指导下一步编码”,优先删除而不是继续堆叠
|
||||
|
||||
@@ -1,246 +0,0 @@
|
||||
# Provider 系统开发文档
|
||||
|
||||
## 当前版本功能 (v0.2.0)
|
||||
|
||||
### 已完成功能
|
||||
|
||||
1. **CQTAI nano 图像适配器** (`app/services/adapters/image/cqtai.py`)
|
||||
- 异步生成 + 轮询获取结果
|
||||
- 支持 nano-banana / nano-banana-pro 模型
|
||||
- 支持多种分辨率和画面比例
|
||||
- 支持图生图 (filesUrl)
|
||||
|
||||
2. **密钥加密存储** (`app/services/secret_service.py`)
|
||||
- Fernet 对称加密,密钥从 SECRET_KEY 派生
|
||||
- Provider API Key 自动加密存储
|
||||
- 密钥管理 API (CRUD)
|
||||
|
||||
3. **指标收集系统** (`app/services/provider_metrics.py`)
|
||||
- 调用成功率、延迟、成本统计
|
||||
- 时间窗口聚合查询
|
||||
- 已集成到 provider_router
|
||||
|
||||
4. **熔断器功能** (`app/services/provider_metrics.py`)
|
||||
- 连续失败 3 次触发熔断
|
||||
- 60 秒后自动恢复尝试
|
||||
- 健康状态持久化到数据库
|
||||
|
||||
5. **管理后台前端** (`app/admin_app.py`)
|
||||
- 独立端口部署 (8001)
|
||||
- Vue 3 + Tailwind CSS 单页应用
|
||||
- Provider CRUD 管理
|
||||
- 密钥管理界面
|
||||
- Basic Auth 认证
|
||||
|
||||
### 配置说明
|
||||
|
||||
```bash
|
||||
# 启动主应用
|
||||
uvicorn app.main:app --port 8000
|
||||
|
||||
# 启动管理后台 (独立端口)
|
||||
uvicorn app.admin_app:app --port 8001
|
||||
```
|
||||
|
||||
环境变量:
|
||||
```
|
||||
CQTAI_API_KEY=your-cqtai-api-key
|
||||
ENABLE_ADMIN_CONSOLE=true
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=your-secure-password
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 下一版本优化计划 (v0.3.0)
|
||||
|
||||
### 高优先级
|
||||
|
||||
#### 1. 智能负载分流 (方案 B)
|
||||
**目标**: 主渠道压力大时自动分流到后备渠道
|
||||
|
||||
**实现方案**:
|
||||
- 监控指标: 并发数、响应延迟、错误率
|
||||
- 分流阈值配置:
|
||||
```python
|
||||
class LoadBalanceConfig:
|
||||
max_concurrent: int = 10 # 并发超过此值时分流
|
||||
max_latency_ms: int = 5000 # 延迟超过此值时分流
|
||||
max_error_rate: float = 0.1 # 错误率超过 10% 时分流
|
||||
```
|
||||
- 分流策略: 加权轮询,根据健康度动态调整权重
|
||||
|
||||
**涉及文件**:
|
||||
- `app/services/provider_router.py` - 添加负载均衡逻辑
|
||||
- `app/services/provider_metrics.py` - 添加并发计数器
|
||||
- `app/db/admin_models.py` - 添加 LoadBalanceConfig 模型
|
||||
|
||||
#### 2. Storybook 适配器
|
||||
**目标**: 生成可翻页的分页故事书
|
||||
|
||||
**实现方案**:
|
||||
- 参考 Gemini AI Story Generator 格式
|
||||
- 输出结构:
|
||||
```python
|
||||
class StorybookPage:
|
||||
page_number: int
|
||||
text: str
|
||||
image_prompt: str
|
||||
image_url: str | None
|
||||
|
||||
class Storybook:
|
||||
title: str
|
||||
pages: list[StorybookPage]
|
||||
cover_url: str | None
|
||||
```
|
||||
- 集成文本 + 图像生成流水线
|
||||
|
||||
**涉及文件**:
|
||||
- `app/services/adapters/storybook/` - 新建目录
|
||||
- `app/api/stories.py` - 添加 storybook 生成端点
|
||||
|
||||
### 中优先级
|
||||
|
||||
#### 3. 成本追踪系统
|
||||
**目标**: 记录实际消费,支持预算控制
|
||||
|
||||
**实现方案**:
|
||||
- 成本记录表:
|
||||
```python
|
||||
class CostRecord:
|
||||
user_id: str
|
||||
provider_id: str
|
||||
capability: str # text/image/tts
|
||||
estimated_cost: Decimal
|
||||
actual_cost: Decimal | None
|
||||
timestamp: datetime
|
||||
```
|
||||
- 预算配置:
|
||||
```python
|
||||
class BudgetConfig:
|
||||
user_id: str
|
||||
daily_limit: Decimal
|
||||
monthly_limit: Decimal
|
||||
alert_threshold: float = 0.8 # 80% 时告警
|
||||
```
|
||||
- 超预算处理: 拒绝请求 / 降级到低成本 provider
|
||||
|
||||
**涉及文件**:
|
||||
- `app/db/admin_models.py` - 添加 CostRecord, BudgetConfig
|
||||
- `app/services/cost_tracker.py` - 新建
|
||||
- `app/api/admin_providers.py` - 添加成本查询 API
|
||||
|
||||
#### 4. 指标可视化
|
||||
**目标**: 管理后台展示供应商指标图表
|
||||
|
||||
**实现方案**:
|
||||
- 添加指标查询 API:
|
||||
- GET /admin/metrics/summary - 汇总统计
|
||||
- GET /admin/metrics/timeline - 时间线数据
|
||||
- GET /admin/metrics/providers/{id} - 单个供应商详情
|
||||
- 前端使用 Chart.js 或 ECharts 展示
|
||||
|
||||
### 低优先级
|
||||
|
||||
#### 5. 多租户 Provider 配置
|
||||
**目标**: 每个租户可配置独立 provider 列表和 API Key
|
||||
|
||||
**实现方案**:
|
||||
- 租户配置表:
|
||||
```python
|
||||
class TenantProviderConfig:
|
||||
tenant_id: str
|
||||
provider_type: str
|
||||
provider_ids: list[str] # 按优先级排序
|
||||
api_key_override: str | None # 加密存储
|
||||
```
|
||||
- 路由时优先使用租户配置,回退到全局配置
|
||||
|
||||
#### 6. Provider 健康检查调度器
|
||||
**目标**: 定期主动检查 provider 健康状态
|
||||
|
||||
**实现方案**:
|
||||
- Celery Beat 定时任务
|
||||
- 每 5 分钟检查一次所有启用的 provider
|
||||
- 更新 ProviderHealth 表
|
||||
|
||||
#### 7. 适配器热加载
|
||||
**目标**: 支持运行时动态加载新适配器
|
||||
|
||||
**实现方案**:
|
||||
- 适配器插件目录: `app/services/adapters/plugins/`
|
||||
- 启动时扫描并注册
|
||||
- 提供 API 触发重新扫描
|
||||
|
||||
---
|
||||
|
||||
## API 变更记录
|
||||
|
||||
### v0.2.0 新增
|
||||
|
||||
| Method | Route | Description |
|
||||
|--------|-------|-------------|
|
||||
| GET | `/admin/secrets` | 列出所有密钥名称 |
|
||||
| POST | `/admin/secrets` | 创建/更新密钥 |
|
||||
| DELETE | `/admin/secrets/{name}` | 删除密钥 |
|
||||
| GET | `/admin/secrets/{name}/verify` | 验证密钥有效性 |
|
||||
|
||||
### 计划中 (v0.3.0)
|
||||
|
||||
| Method | Route | Description |
|
||||
|--------|-------|-------------|
|
||||
| GET | `/admin/metrics/summary` | 指标汇总 |
|
||||
| GET | `/admin/metrics/timeline` | 时间线数据 |
|
||||
| POST | `/api/storybook/generate` | 生成分页故事书 |
|
||||
| GET | `/admin/costs` | 成本统计 |
|
||||
| POST | `/admin/budgets` | 设置预算 |
|
||||
|
||||
---
|
||||
|
||||
## 适配器开发指南
|
||||
|
||||
### 添加新适配器
|
||||
|
||||
1. 创建适配器文件:
|
||||
```python
|
||||
# app/services/adapters/image/new_provider.py
|
||||
from app.services.adapters.base import AdapterConfig, BaseAdapter
|
||||
from app.services.adapters.registry import AdapterRegistry
|
||||
|
||||
@AdapterRegistry.register("image", "new_provider")
|
||||
class NewProviderAdapter(BaseAdapter[str]):
|
||||
adapter_type = "image"
|
||||
adapter_name = "new_provider"
|
||||
|
||||
async def execute(self, prompt: str, **kwargs) -> str:
|
||||
# 实现生成逻辑
|
||||
pass
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
# 实现健康检查
|
||||
pass
|
||||
|
||||
@property
|
||||
def estimated_cost(self) -> float:
|
||||
return 0.01 # USD
|
||||
```
|
||||
|
||||
2. 在 `__init__.py` 中导入:
|
||||
```python
|
||||
# app/services/adapters/__init__.py
|
||||
from app.services.adapters.image import new_provider as _new_provider # noqa: F401
|
||||
```
|
||||
|
||||
3. 添加配置:
|
||||
```python
|
||||
# app/core/config.py
|
||||
new_provider_api_key: str = ""
|
||||
|
||||
# app/services/provider_router.py
|
||||
API_KEY_MAP["new_provider"] = "new_provider_api_key"
|
||||
```
|
||||
|
||||
4. 更新 `.env.example`:
|
||||
```
|
||||
NEW_PROVIDER_API_KEY=
|
||||
```
|
||||
@@ -1,109 +0,0 @@
|
||||
# DreamWeaver 重构实施计划
|
||||
|
||||
## 1. 概述
|
||||
|
||||
本文档基于对当前架构的深入分析,制定了从稳定性、可维护性到可扩展性的分阶段重构计划。
|
||||
|
||||
**目标**:
|
||||
- **短期**:解决单点故障风险,优化开发体验,清理关键技术债。
|
||||
- **中期**:提升系统高可用能力,增强监控与可观测性。
|
||||
- **长期**:架构演进,支持大规模并发与复杂业务场景。
|
||||
|
||||
---
|
||||
|
||||
## 2. 短期优化计划 (1-2周)
|
||||
|
||||
**重点**:消除即时风险,提升部署效率。
|
||||
|
||||
### 2.1 统一镜像构建 (High Priority)
|
||||
目前 `backend`, `backend-admin`, `worker`, `celery-beat` 重复构建 4 次,浪费资源且镜像版本可能不一致。
|
||||
|
||||
- **Action Items**:
|
||||
- [x] 修改 `backend/Dockerfile` 为通用基础镜像。
|
||||
- [x] 更新 `docker-compose.yml`,定义 `backend-base` 服务或使用 `image` 标签共享镜像。
|
||||
- [x] 确保所有 Python 服务共用同一构建产物,仅启动命令不同。
|
||||
|
||||
### 2.2 修复 Provider 缓存与限流 (High Priority)
|
||||
内存缓存 (`TTLCache`, `_latency_cache`) 在多进程/多实例下失效。
|
||||
|
||||
- **Action Items**:
|
||||
- [x] 引入 Redis 作为共享缓存后端。
|
||||
- [x] 重构 `_load_provider_cache`,将 Provider 配置缓存至 Redis。
|
||||
- [x] 重构 `stories.py` 中的限流逻辑,使用 `redis-cell` 或简单的 Redis 计数器替代 `TTLCache`。
|
||||
|
||||
### 2.3 拆分 `stories.py` (Medium Priority)
|
||||
`app/api/stories.py` 超过 600 行,包含 API 定义、业务逻辑、验证逻辑,维护困难。
|
||||
|
||||
- **Action Items**:
|
||||
- [x] 创建 `app/services/story_service.py`,迁移生成、润色、PDF生成等核心逻辑。
|
||||
- [x] 创建 `app/schemas/story_schemas.py`,迁移 Pydantic 模型(`GenerateRequest`, `StoryResponse` 等)。
|
||||
- [x] API 层 `stories.py` 仅保留路由定义和依赖注入,调用 Service 层。
|
||||
|
||||
---
|
||||
|
||||
## 3. 中期优化计划 (1-2月)
|
||||
|
||||
**重点**:高可用 (HA) 与系统韧性。
|
||||
|
||||
### 3.1 数据库高可用 (Critical)
|
||||
当前 PostgreSQL 为单点,且 Admin/User 混合使用。
|
||||
|
||||
- **Action Items**:
|
||||
- [ ] 部署 PostgreSQL 主从复制 (Master-Slave)。
|
||||
- [ ] 配置 `PgBouncer` 或 SQLAlchemy 读写分离,减轻主库压力。
|
||||
- [ ] 实施数据库自动备份策略 (如 `pg_dump` 定时上传 S3)。
|
||||
|
||||
### 3.2 消息队列高可用 (Critical)
|
||||
Redis 单点故障将导致 Celery 任务全盘停摆。
|
||||
|
||||
- **Action Items**:
|
||||
- [ ] 迁移至 Redis Sentinel 或 Redis Cluster 模式。
|
||||
- [ ] 更新 Celery 配置以支持 Sentinel/Cluster 连接串。
|
||||
|
||||
### 3.3 增强可观测性 (Important)
|
||||
目前仅有简单的日志,缺乏系统级指标。
|
||||
|
||||
- **Action Items**:
|
||||
- [ ] 集成 Prometheus Client,暴露 `/metrics` 端点。
|
||||
- [ ] 部署 Grafana + Prometheus,监控 API 延迟、QPS、Celery 队列积压情况。
|
||||
- [ ] 完善 `ProviderMetrics`,增加可视化大盘,实时监控 AI 供应商的成本与成功率。
|
||||
|
||||
### 3.4 Phase 3 最小可执行任务清单 (MVP)
|
||||
|
||||
目标:在不大改业务代码的前提下,于一个迭代内完成高可用基础设施闭环。
|
||||
|
||||
- [x] PostgreSQL 主从:新增 `docker-compose.ha.yml`,包含 1 主 1 从与健康检查。
|
||||
- [x] PostgreSQL 备份:新增每日备份任务(`pg_dump`)与 7 天保留策略。
|
||||
- [x] Redis Sentinel:新增 1 主 2 哨兵最小拓扑,并验证故障切换。
|
||||
- [x] Celery 连接:更新 Celery broker/result backend 配置,支持 Sentinel 连接串。
|
||||
- [x] 回归验证:执行一次故事生成 + 异步任务链路(worker/beat)冒烟测试。
|
||||
- [x] 运行手册:补充故障切换与恢复步骤文档(PostgreSQL/Redis/Celery)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 长期架构演进 (季度规划)
|
||||
|
||||
**重点**:业务解耦与规模化。
|
||||
|
||||
### 4.1 统一 API 网关
|
||||
- **当前**:前端直连后端端口,CORS 配置分散。
|
||||
- **演进**:引入 Traefik 或 Nginx 作为统一网关,管理路由、SSL、全局限流、统一鉴权。
|
||||
|
||||
### 4.2 前端工程合并
|
||||
- **当前**:User App 和 Admin Console 是完全独立的两个项目,但在组件和工具链上高度重复。
|
||||
- **演进**:使用一种 Monorepo 策略或基于路由的单一应用策略,共享组件库和类型定义,减少维护成本。
|
||||
|
||||
### 4.3 事件驱动架构完善
|
||||
- **当前**:部分业务逻辑耦合在 API 中。
|
||||
- **演进**:扩展事件总线,将“阅读记录”、“成就解锁”、“通知推送”等非核心链路完全异步化,通过 Domain Events 解耦。
|
||||
|
||||
---
|
||||
|
||||
## 5. 实施路线图
|
||||
|
||||
| 阶段 | 时间估算 | 关键里程碑 |
|
||||
| :--- | :--- | :--- |
|
||||
| **Phase 1: 基础夯实** | Week 1-2 | Docker 构建优化上线,Redis 替代内存缓存。 |
|
||||
| **Phase 2: 代码重构** | Week 3-4 | `stories.py` 拆分完成,Service 层建立。 |
|
||||
| **Phase 3: 高可用建设** | Month 2 | 数据库与 Redis 实现主备/集群模式。 |
|
||||
| **Phase 4: 监控体系** | Month 2 | Grafana 监控大盘上线,关键指标报警配置完毕。 |
|
||||
@@ -1,52 +0,0 @@
|
||||
# `stories.py` 拆分分析 (Phase 2 准备)
|
||||
|
||||
## 当前职责
|
||||
|
||||
`app/api/stories.py` (591 行) 承担了以下职责:
|
||||
|
||||
| 职责 | 行数 | 描述 |
|
||||
|---|---|---|
|
||||
| Pydantic 模型 | ~50 行 | `GenerateRequest`, `StoryResponse`, `FullStoryResponse` 等 |
|
||||
| 验证逻辑 | ~40 行 | `_validate_profile_and_universe` |
|
||||
| 路由 + 业务 | ~300 行 | `generate_story`, `generate_story_full`, `generate_story_stream` |
|
||||
| 绘本逻辑 | ~170 行 | `generate_storybook_api` (含并行图片生成) |
|
||||
| 成就查询 | ~30 行 | `get_story_achievements` |
|
||||
|
||||
## 缺失端点
|
||||
|
||||
测试中引用但 **未实现** 的端点(这些应在拆分时一并补充):
|
||||
|
||||
- `GET /api/stories` — 故事列表 (分页)
|
||||
- `GET /api/stories/{id}` — 故事详情
|
||||
- `DELETE /api/stories/{id}` — 故事删除
|
||||
- `POST /api/image/generate/{id}` — 封面图片生成
|
||||
- `GET /api/audio/{id}` — 语音朗读
|
||||
|
||||
## 建议拆分结构
|
||||
|
||||
```
|
||||
app/
|
||||
├── schemas/
|
||||
│ └── story_schemas.py # [NEW] Pydantic 模型
|
||||
├── services/
|
||||
│ └── story_service.py # [NEW] 核心业务逻辑
|
||||
└── api/
|
||||
├── stories.py # [SLIM] 路由定义 + 依赖注入
|
||||
└── stories_storybook.py # [NEW] 绘本相关端点 (可选)
|
||||
```
|
||||
|
||||
### `story_schemas.py`
|
||||
- 迁移所有 Pydantic 模型
|
||||
- 包括 `GenerateRequest`, `StoryResponse`, `FullStoryResponse`, `StorybookRequest`, `StorybookResponse` 等
|
||||
|
||||
### `story_service.py`
|
||||
- `validate_profile_and_universe()` — 验证逻辑
|
||||
- `create_story()` — 故事入库
|
||||
- `generate_and_save_story()` — 生成 + 保存联合操作
|
||||
- `generate_storybook_with_images()` — 绘本并行图片生成
|
||||
- 补充: `list_stories()`, `get_story()`, `delete_story()`
|
||||
|
||||
### `stories.py` (瘦路由层)
|
||||
- 仅保留 `@router` 装饰器和依赖注入
|
||||
- 调用 service 层完成业务逻辑
|
||||
- 预计 150-200 行
|
||||
@@ -1,89 +0,0 @@
|
||||
# HA 部署与验证 Runbook(Phase 3 MVP)
|
||||
|
||||
本文档对应 `docker-compose.ha.yml`,用于本地/测试环境验证高可用基础能力。
|
||||
|
||||
## 1. 启动方式
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.ha.yml up -d
|
||||
```
|
||||
|
||||
说明:
|
||||
- 基础业务服务仍来自 `docker-compose.yml`。
|
||||
- `docker-compose.ha.yml` 覆盖了 `db`、`redis`,并新增 `db-replica`、`postgres-backup`、`redis-replica`、`redis-sentinel-*`。
|
||||
|
||||
## 2. 核心环境变量建议
|
||||
|
||||
在 `backend/.env`(或 shell 环境)中至少配置:
|
||||
|
||||
```env
|
||||
# PostgreSQL
|
||||
POSTGRES_USER=dreamweaver
|
||||
POSTGRES_PASSWORD=dreamweaver_password
|
||||
POSTGRES_DB=dreamweaver_db
|
||||
POSTGRES_REPMGR_PASSWORD=repmgr_password
|
||||
|
||||
# Redis Sentinel
|
||||
REDIS_SENTINEL_ENABLED=true
|
||||
REDIS_SENTINEL_NODES=redis-sentinel-1:26379,redis-sentinel-2:26379,redis-sentinel-3:26379
|
||||
REDIS_SENTINEL_MASTER_NAME=mymaster
|
||||
REDIS_SENTINEL_DB=0
|
||||
REDIS_SENTINEL_SOCKET_TIMEOUT=0.5
|
||||
|
||||
# 可选:若 Sentinel/Redis 设置了密码
|
||||
REDIS_SENTINEL_PASSWORD=
|
||||
|
||||
# 备份周期,默认 86400 秒(1 天)
|
||||
BACKUP_INTERVAL_SECONDS=86400
|
||||
```
|
||||
|
||||
## 3. 健康检查
|
||||
|
||||
### 3.1 PostgreSQL 主从
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.ha.yml ps
|
||||
docker exec -it dreamweaver_db_primary psql -U dreamweaver -d dreamweaver_db -c "select now();"
|
||||
docker exec -it dreamweaver_db_replica psql -U dreamweaver -d dreamweaver_db -c "select pg_is_in_recovery();"
|
||||
```
|
||||
|
||||
期望:
|
||||
- 主库可读写;
|
||||
- 从库 `pg_is_in_recovery()` 返回 `t`。
|
||||
|
||||
### 3.2 Redis Sentinel
|
||||
|
||||
```bash
|
||||
docker exec -it dreamweaver_redis_sentinel_1 redis-cli -p 26379 sentinel masters
|
||||
docker exec -it dreamweaver_redis_sentinel_1 redis-cli -p 26379 sentinel replicas mymaster
|
||||
```
|
||||
|
||||
期望:
|
||||
- `mymaster` 存在;
|
||||
- 至少 1 个 replica 被发现。
|
||||
|
||||
### 3.3 备份任务
|
||||
|
||||
```bash
|
||||
docker exec -it dreamweaver_postgres_backup sh -c "ls -lh /backups"
|
||||
```
|
||||
|
||||
期望:
|
||||
- `/backups` 下出现 `.dump` 文件;
|
||||
- 旧于 7 天的备份会被自动清理。
|
||||
|
||||
## 4. 故障切换演练(最小)
|
||||
|
||||
```bash
|
||||
# 模拟 Redis 主节点故障
|
||||
docker stop dreamweaver_redis_master
|
||||
|
||||
# 等待 Sentinel 选主后查看
|
||||
docker exec -it dreamweaver_redis_sentinel_1 redis-cli -p 26379 sentinel get-master-addr-by-name mymaster
|
||||
```
|
||||
|
||||
提示:应用与 Celery 已支持 Sentinel 配置。若未启用 Sentinel,仍可回退到 `REDIS_URL` / `CELERY_BROKER_URL` / `CELERY_RESULT_BACKEND` 直连模式。
|
||||
|
||||
## 5. 当前已知限制(下一步)
|
||||
|
||||
- PostgreSQL 侧当前仅完成主从拓扑,读写分离(PgBouncer/路由)待后续迭代。
|
||||
@@ -1,450 +0,0 @@
|
||||
# DreamWeaver 文档状态盘点表
|
||||
|
||||
**Version**: 1.0
|
||||
**Date**: 2026-04-17
|
||||
**Author**: Sarah (Product Owner) / Codex
|
||||
**Document Type**: Documentation Audit / Source-of-Truth Inventory
|
||||
|
||||
---
|
||||
|
||||
## 1. 盘点目的
|
||||
|
||||
这份文档不是新的 PRD,也不是新的技术方案,而是一份“项目资产盘点文档”。它解决三个问题:
|
||||
|
||||
1. 让团队快速分清楚 `docs/` 里哪些文件是当前有效文档,哪些只是历史材料。
|
||||
2. 让产品文档与代码现实建立映射,避免“文档看起来很完整,但代码并没有落地”的错觉。
|
||||
3. 在重新启动项目时,为后续改代码提供明确起点,减少无效重构和重复讨论。
|
||||
|
||||
对于求职版 DreamWeaver,这份盘点文档的价值在于:它帮助你把“会写需求文档”进一步提升为“会管理文档体系、会判断 source of truth、会做项目现状诊断”。
|
||||
|
||||
---
|
||||
|
||||
## 2. 盘点范围与判定口径
|
||||
|
||||
### 2.1 盘点范围
|
||||
|
||||
本次盘点覆盖以下对象:
|
||||
|
||||
- `docs/` 当前全部文档
|
||||
- 后端核心实现:`backend/app/api/`、`backend/app/services/`、`backend/app/db/`
|
||||
- 前端关键体验:`frontend/src/components/`、`frontend/src/views/`、`frontend/src/stores/`
|
||||
- 运维相关配置:`docker-compose.ha.yml`、`backend/app/core/`
|
||||
- 构建与验证结果:后端测试、后端 lint、主前端构建、管理端构建
|
||||
|
||||
### 2.2 文档状态定义
|
||||
|
||||
| 文档状态 | 含义 |
|
||||
| --- | --- |
|
||||
| Active | 当前有效,应作为近期工作的参考依据 |
|
||||
| Reference | 有参考价值,但不能直接视为最新实现说明 |
|
||||
| Archived | 保留历史价值,但不再作为现行 source of truth |
|
||||
|
||||
### 2.3 代码落地状态定义
|
||||
|
||||
| 落地状态 | 含义 |
|
||||
| --- | --- |
|
||||
| 非实现类文档 | 文档本身是产品/规划/治理文档,不直接对应“已实现/未实现” |
|
||||
| 已实现 | 文档描述的主体能力已在代码中形成闭环,且验证结果基本通过 |
|
||||
| 部分实现 | 已有主干能力,但关键路径、恢复能力、状态模型或工程质量仍未闭环 |
|
||||
| 未实现 | 文档描述的主体仍是目标态,当前代码尚未形成有效落地 |
|
||||
| 历史文档 | 文档描述对应的是过去的设计/阶段,部分内容已落地,但已不适合作为现行依据 |
|
||||
|
||||
### 2.4 本次验证快照
|
||||
|
||||
截至 2026-04-17 evening,本次盘点同步得到以下验证结果:
|
||||
|
||||
- 后端测试通过:`backend/` 下 `pytest -q` 结果为 `53 passed`
|
||||
- 主前端类型检查通过:`frontend/` 下 `vue-tsc --noEmit` 成功
|
||||
- 主前端完整构建在当前环境受 Rollup 可选原生包缺失影响,属于环境依赖问题,不是本轮状态模型改动直接引起
|
||||
- 管理端范围仍未明确,不适合作为当前求职版稳定演示链路
|
||||
- 后端 lint 仍有历史债务,尚未完成最后一轮收尾
|
||||
|
||||
这意味着:项目并不是“不能运行”,而是“核心主链路可用,但工程完备度和演示稳定性还没到求职成品状态”。
|
||||
|
||||
---
|
||||
|
||||
## 3. 文档状态总表
|
||||
|
||||
| 文档 | 分类 | 文档状态 | 代码落地状态 | 盘点结论 | 建议动作 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `docs/README.md` | 文档治理 | Active | 非实现类文档 | 当前 docs 分类规则清晰,已成为文档入口页 | 保留并持续维护 |
|
||||
| `docs/product/job-search-relaunch-prd.md` | 产品 PRD | Active | 非实现类文档 | 是当前“求职版产品重启”的核心 source of truth | 保留,作为产品总纲 |
|
||||
| `docs/product/unified-generation-workflow-prd.md` | 功能 PRD | Active | 部分实现 | 目标方向明确,但“统一工作流”目标态尚未真正落地 | 保留,作为改代码主依据 |
|
||||
| `docs/planning/week-1-execution-backlog.md` | 执行规划 | Active | 非实现类文档 | 是执行计划,不应用它判断是否“已经做完” | 保留,并按完成情况更新 |
|
||||
| `docs/planning/document-status-inventory.md` | 项目盘点 | Active | 非实现类文档 | 当前文档体系与代码现实的映射表 | 保留,后续按阶段更新 |
|
||||
| `docs/technical/memory-system-dev.md` | 技术设计 | Reference | 部分实现 | 记忆系统主干已存在,但文档中不少内容仍是增强设计 | 保留,开发前逐条核验 |
|
||||
| `docs/operations/ha-runbook.md` | 运维 Runbook | Reference | 部分实现 | Docker HA、Redis Sentinel、备份与 Celery Sentinel 支持已存在,但仍属基础版 | 保留,按真实环境演练继续校正 |
|
||||
| `docs/archive/provider-system-legacy.md` | 历史技术文档 | Archived | 历史文档 | 部分设计已落地,但命名与架构描述已过时 | 继续归档,不再扩写 |
|
||||
| `docs/archive/refactoring-plan-legacy.md` | 历史实施计划 | Archived | 历史文档 | 反映旧阶段重构过程,部分 checklist 已完成 | 继续归档,仅供回溯 |
|
||||
| `docs/archive/stories-split-analysis-legacy.md` | 历史分析 | Archived | 历史文档 | 拆分分析对应的主要重构已发生 | 继续归档,仅供理解演进过程 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 逐份文档判定说明
|
||||
|
||||
### 4.1 `docs/README.md`
|
||||
|
||||
**判定**
|
||||
当前有效,属于“文档治理入口”。
|
||||
|
||||
**证据**
|
||||
|
||||
- 已明确区分 `product / planning / technical / operations / archive`
|
||||
- 已写清删除与归档规则
|
||||
- 已能帮助团队快速识别 source of truth
|
||||
|
||||
**结论**
|
||||
这份文档不是实现说明,但它已经承担“文档信息架构”角色,应继续保留并作为 docs 首页。
|
||||
|
||||
### 4.2 `docs/product/job-search-relaunch-prd.md`
|
||||
|
||||
**判定**
|
||||
当前有效,属于产品总纲文档。
|
||||
|
||||
**证据**
|
||||
|
||||
- 文档明确提出求职版产品定位、成功指标、P0/P1/P2 取舍
|
||||
- 文档中的问题诊断与当前代码现实一致,包括:
|
||||
- Storybook 恢复能力不足
|
||||
- Provider 体系职责混杂
|
||||
- Admin 构建问题影响演示
|
||||
- 前端状态设计薄弱
|
||||
|
||||
**结论**
|
||||
这份 PRD 不用于判断“是否已实现”,而用于回答“现在应该把项目做成什么样”。它是当前最重要的产品 source of truth。
|
||||
|
||||
### 4.3 `docs/product/unified-generation-workflow-prd.md`
|
||||
|
||||
**判定**
|
||||
当前有效,但对应能力仅部分实现。
|
||||
|
||||
**证据**
|
||||
|
||||
- 当前后端仍保留多条生成路径:
|
||||
- `POST /api/stories/generate`
|
||||
- `POST /api/stories/generate/full`
|
||||
- `POST /api/storybook/generate`
|
||||
- 相关实现仍分别落在 `backend/app/api/stories.py` 与 `backend/app/services/story_service.py`
|
||||
- 当前 `Story` 模型已具备统一主记录的基础字段:
|
||||
- `story_text`
|
||||
- `pages`
|
||||
- `cover_prompt`
|
||||
- `image_url`
|
||||
- `mode`
|
||||
- 当前已经落地的统一状态模型包括:
|
||||
- `generation_status`
|
||||
- `image_status`
|
||||
- `audio_status`
|
||||
- `last_error`
|
||||
- `degraded_completed`
|
||||
- 但更完整的工作流目标仍未完全实现,例如:
|
||||
- `partial_ready`
|
||||
- `retryable_assets`
|
||||
- 统一资产重试入口
|
||||
- 单一 generation service workflow
|
||||
|
||||
**结论**
|
||||
这份文档对应的是“当前核心改造主线”。它已经不再只是方向性文档,因为统一状态模型和恢复能力已经开始落地;但它仍不是“已完成实现说明”,因为统一工作流入口和统一资产补全过程还未真正收束。
|
||||
|
||||
### 4.4 `docs/planning/week-1-execution-backlog.md`
|
||||
|
||||
**判定**
|
||||
当前有效,属于执行计划文档。
|
||||
|
||||
**证据**
|
||||
|
||||
- 文档将工作拆成产品聚焦、工作流定义、Storybook 恢复、Admin 处理、Provider 边界梳理等任务
|
||||
- 这些任务与盘点出的真实缺口一致
|
||||
- 但多数事项仍未被代码完成,因此不能把这份文档当作“实现说明”
|
||||
|
||||
**结论**
|
||||
这是一份“该做什么”的文档,不是“已经做了什么”的文档。后续应在每个任务完成后更新状态,而不是继续长期停留在初始 backlog。
|
||||
|
||||
### 4.5 `docs/technical/memory-system-dev.md`
|
||||
|
||||
**判定**
|
||||
技术参考文档,部分实现。
|
||||
|
||||
**已落地部分证据**
|
||||
|
||||
- `backend/app/db/models.py` 中存在 `MemoryItem`、`ChildProfile`、`StoryUniverse`
|
||||
- `backend/app/services/memory_service.py` 中已实现:
|
||||
- 记忆类型定义
|
||||
- 时效衰减评分
|
||||
- Prompt 注入格式化
|
||||
- TTL 清理
|
||||
- recent story / favorite character / scary element 创建
|
||||
- `backend/app/api/memories.py` 已提供记忆查询、创建、删除相关接口
|
||||
- `backend/app/api/profiles.py` 已提供 `GET /profiles/{profile_id}/timeline`
|
||||
- `backend/app/tasks/memory.py` 和 `backend/app/core/celery_app.py` 已接入每日清理任务
|
||||
|
||||
**未完全落地部分**
|
||||
|
||||
- 文档规划的反馈接口 `POST /api/memories/{id}/feedback` 当前不存在
|
||||
- 更复杂的“长期印象总结”“通知机制”“更丰富的结构化 schema”尚未形成闭环
|
||||
- 时间线目前主要由档案创建、故事记录、宇宙成就拼装而成,还不是完整的成长操作系统
|
||||
|
||||
**结论**
|
||||
记忆系统不是“没做”,而是“已经有主干,但还停在可用原型阶段”。这份文档应该被保留为技术参考,但开发时必须逐条核验,不可直接按文档默认其已落地。
|
||||
|
||||
### 4.6 `docs/operations/ha-runbook.md`
|
||||
|
||||
**判定**
|
||||
运维参考文档,部分实现。
|
||||
|
||||
**已落地部分证据**
|
||||
|
||||
- `docker-compose.ha.yml` 已提供:
|
||||
- PostgreSQL 主库
|
||||
- PostgreSQL 从库
|
||||
- 定时备份容器
|
||||
- Redis 主从
|
||||
- 3 个 Sentinel 节点
|
||||
- `backend/app/core/config.py` 已支持 Sentinel 相关配置解析
|
||||
- `backend/app/core/redis.py` 已支持通过 Sentinel 获取 Redis master
|
||||
- `backend/app/core/celery_app.py` 已支持 Celery broker/result backend 走 Sentinel
|
||||
|
||||
**未完全落地部分**
|
||||
|
||||
- 仍停留在 Docker Compose 层的基础 HA 演练,不是成熟生产级方案
|
||||
- 尚未看到读写分离、连接池代理、监控告警等更完整设施
|
||||
- 这份 runbook 更适合作为“基础 HA 实验手册”,而不是正式生产运维规范
|
||||
|
||||
**结论**
|
||||
该文档不应删除,因为它对应的基础设施确实存在;但也不能对外表述成“完整 HA 能力已成熟上线”。
|
||||
|
||||
### 4.7 `docs/archive/provider-system-legacy.md`
|
||||
|
||||
**判定**
|
||||
历史文档,部分内容已落地,但整体已过时。
|
||||
|
||||
**证据**
|
||||
|
||||
- 文档提到的 provider failover、metrics、secret management、admin console 等能力,在代码中能找到对应实现:
|
||||
- `backend/app/services/provider_router.py`
|
||||
- `backend/app/services/provider_metrics.py`
|
||||
- `backend/app/services/secret_service.py`
|
||||
- `backend/app/api/admin_providers.py`
|
||||
- 但文档中的部分命名与现状不一致,例如仍提到 `app/admin_app.py`,而当前入口为 `backend/app/admin_main.py`
|
||||
- 当前 provider router 同时承担默认配置、凭据映射、路由策略、熔断、成本记录等多项职责,说明体系已继续演化,不再等同于这份旧文档
|
||||
|
||||
**结论**
|
||||
这份文档值得保留用于理解历史,但不能作为现行 provider 体系说明书。
|
||||
|
||||
### 4.8 `docs/archive/refactoring-plan-legacy.md`
|
||||
|
||||
**判定**
|
||||
历史计划文档,部分任务已完成。
|
||||
|
||||
**证据**
|
||||
|
||||
- 文档中提到的 `stories.py` 拆分,目前已经有明显落地:
|
||||
- `backend/app/services/story_service.py`
|
||||
- `backend/app/schemas/story_schemas.py`
|
||||
- `backend/app/api/stories.py`
|
||||
- 文档中提到的 Redis / HA 方向也已有基础实现
|
||||
- 但它描述的是更早阶段的改造路线,与当前“求职版重启”的产品目标已不是同一语境
|
||||
|
||||
**结论**
|
||||
保留在 `archive/` 是合理的。它是“项目曾经怎么想”的材料,不是“现在应该怎么做”的材料。
|
||||
|
||||
### 4.9 `docs/archive/stories-split-analysis-legacy.md`
|
||||
|
||||
**判定**
|
||||
历史分析文档,核心分析目的已经完成。
|
||||
|
||||
**证据**
|
||||
|
||||
- 文档聚焦 `stories.py` 过重的问题
|
||||
- 当前已形成更合理的拆分:
|
||||
- API 层保留路由
|
||||
- schema 独立
|
||||
- service 独立
|
||||
- 说明它的主要使命已经完成
|
||||
|
||||
**结论**
|
||||
应继续归档,用于未来解释“为什么会有现在的结构”,但不再参与当前需求决策。
|
||||
|
||||
---
|
||||
|
||||
## 5. 当前已落地的核心能力
|
||||
|
||||
以下能力已经具备“代码存在且主链路可验证”的基础:
|
||||
|
||||
### 5.1 内容生成基础能力
|
||||
|
||||
- 普通故事生成、完整故事生成、绘本生成均存在可调用接口
|
||||
- `Story` 模型已能同时承载文本故事与分页绘本
|
||||
- 封面图生成与成就提取已接入后处理链路
|
||||
|
||||
### 5.2 个性化上下文基础能力
|
||||
|
||||
- 孩子档案、故事宇宙、记忆系统、成长时间线均已有基础模型和接口
|
||||
- Prompt 侧已接入记忆上下文构建
|
||||
- 成就可回写到 `StoryUniverse.achievements`
|
||||
|
||||
### 5.3 Provider 管理基础能力
|
||||
|
||||
- Provider Router 已支持 failover
|
||||
- Provider 管理、密钥管理、成本汇总等管理 API 已存在
|
||||
- 默认 provider 列表与数据库 provider 配置可共存
|
||||
|
||||
### 5.4 运维与异步基础能力
|
||||
|
||||
- Celery + Redis 已接入
|
||||
- Redis Sentinel 与 Celery Sentinel 配置已实现
|
||||
- PostgreSQL 主从与备份的 Compose 级实验环境已存在
|
||||
|
||||
### 5.5 工程可运行性
|
||||
|
||||
- 后端测试通过:`53 passed`
|
||||
- 主前端构建通过
|
||||
|
||||
---
|
||||
|
||||
## 6. 当前“部分实现 / 未实现”的关键缺口
|
||||
|
||||
这些缺口正是接下来改代码最应该优先处理的地方。
|
||||
|
||||
### 6.1 统一生成工作流尚未真正落地
|
||||
|
||||
虽然 PRD 已经明确目标,但当前系统仍是多入口、多响应模型、多处理路径并存。它们共享了一些底层能力,但还没有收束为统一 workflow。
|
||||
|
||||
### 6.2 Storybook 恢复能力不完整
|
||||
|
||||
当前前端仍依赖 `frontend/src/stores/storybook.ts` 暂存数据,`frontend/src/views/StorybookViewer.vue` 在刷新或直接访问时无法按 ID 恢复。这是最明显的“演示链路不稳”问题之一。
|
||||
|
||||
### 6.3 音频体验未形成闭环
|
||||
|
||||
当前 `GET /api/audio/{id}` 会在请求时即时生成音频,但没有持久化缓存与复用策略,既影响用户体验,也影响成本控制。
|
||||
|
||||
### 6.4 Provider 体系职责边界仍然混杂
|
||||
|
||||
当前 `provider_router.py` 既负责默认 provider、凭据映射、策略排序,又承担 metrics、熔断、成本记录等职责。功能虽强,但不利于后续持续演进,也不利于你在面试中清晰讲解。
|
||||
|
||||
### 6.5 管理端尚未达到“可展示成品”标准
|
||||
|
||||
`admin-frontend` 当前构建失败,说明管理端虽然概念上存在,但还不适合作为稳定演示链路的一部分。
|
||||
|
||||
### 6.6 工程质量信号还不统一
|
||||
|
||||
后端测试是加分项,但 lint 未通过会削弱成熟度观感。对于求职版项目,测试通过但 lint 大量报错,会让项目显得“能跑,但还没收尾”。
|
||||
|
||||
---
|
||||
|
||||
## 7. 推荐下一步编码切入点
|
||||
|
||||
如果目标是“尽快把项目恢复到可演示、可讲清、可继续迭代”的状态,建议按以下顺序推进。
|
||||
|
||||
### 7.1 第一优先级:补齐 Storybook 按 ID 恢复
|
||||
|
||||
**为什么先做**
|
||||
|
||||
- 改动范围相对可控
|
||||
- 用户价值直观
|
||||
- 修完后演示稳定性立刻提升
|
||||
- 很适合作为“重新启动项目后的第一场胜仗”
|
||||
|
||||
**目标**
|
||||
|
||||
- `StorybookViewer` 不再只依赖 Pinia
|
||||
- 支持通过 `story_id` 拉取 `Story.pages`
|
||||
- 刷新页面后仍能继续阅读
|
||||
|
||||
### 7.2 第二优先级:抽出统一生成状态模型
|
||||
|
||||
**为什么第二个做**
|
||||
|
||||
- 这是“统一工作流”真正开始落地的最小切口
|
||||
- 它能先统一语言,再统一代码
|
||||
- 前端状态设计、后端任务编排、部分完成/降级完成,都会以它为中心展开
|
||||
|
||||
**目标**
|
||||
|
||||
- 先在服务层定义统一状态
|
||||
- 再决定是否扩展数据库字段
|
||||
- 让故事、绘本、图片、音频都能共享一套状态表达
|
||||
|
||||
### 7.3 第三优先级:清理 Provider 边界并决定 Admin 范围
|
||||
|
||||
**为什么第三个做**
|
||||
|
||||
- 这是系统长期可解释性的关键
|
||||
- 但它比 Storybook 恢复和状态模型更抽象,适合在主链路稳定后推进
|
||||
|
||||
**目标**
|
||||
|
||||
- 先梳理 Capability / Provider / Routing Policy 三层概念
|
||||
- 再判断管理端是修复、降级,还是缩小到只保留必要接口
|
||||
|
||||
---
|
||||
|
||||
## 8. 建议保留、更新、删除动作汇总
|
||||
|
||||
### 8.1 建议保留
|
||||
|
||||
- `docs/README.md`
|
||||
- `docs/product/job-search-relaunch-prd.md`
|
||||
- `docs/product/unified-generation-workflow-prd.md`
|
||||
- `docs/planning/week-1-execution-backlog.md`
|
||||
- `docs/technical/memory-system-dev.md`
|
||||
- `docs/operations/ha-runbook.md`
|
||||
- `docs/archive/*`
|
||||
|
||||
### 8.2 建议更新
|
||||
|
||||
- `docs/planning/week-1-execution-backlog.md`
|
||||
- 需要随着任务推进更新完成状态,不应长期停留在纯规划状态
|
||||
- `docs/technical/memory-system-dev.md`
|
||||
- 后续开发时应补充“已实现”和“待实现”标记,减少误读
|
||||
- `docs/operations/ha-runbook.md`
|
||||
- 后续若做真实演练,应把演练结果写回文档
|
||||
|
||||
### 8.3 当前不建议再删除
|
||||
|
||||
本轮分类整理后,`docs/` 目录中没有新的“应该直接删除”的文档。剩余历史文件都具备学习价值或项目演进价值,适合继续保留在 `archive/`。
|
||||
|
||||
---
|
||||
|
||||
## 9. PM 学习笔记:为什么要写这种盘点文档
|
||||
|
||||
很多初级产品文档只会写“要做什么”,但不会回答:
|
||||
|
||||
- 现在手里的文档哪些是真的有效
|
||||
- 哪些是目标态,哪些是现状
|
||||
- 哪些能力已经能演示,哪些只是概念
|
||||
- 哪些问题适合现在改,哪些问题应该晚一点改
|
||||
|
||||
“文档状态盘点表”就是用来解决这些问题的。它本质上是产品管理中的三项能力训练:
|
||||
|
||||
1. **Source of Truth 管理**
|
||||
你要知道团队现在到底该信哪份文档。
|
||||
2. **现状诊断能力**
|
||||
你要把 PRD、代码、构建结果、运维配置放在一起看,而不是只看其中一边。
|
||||
3. **优先级判断能力**
|
||||
你要判断什么是“现在最值得做的第一件事”。
|
||||
|
||||
以后你在写自己的项目盘点时,可以直接复用这套结构:
|
||||
|
||||
1. 盘点目的
|
||||
2. 判定口径
|
||||
3. 状态总表
|
||||
4. 逐项证据
|
||||
5. 已落地能力
|
||||
6. 关键缺口
|
||||
7. 下一步建议
|
||||
|
||||
---
|
||||
|
||||
## 10. 本次盘点结论
|
||||
|
||||
DreamWeaver 当前不是“半成品废案”,而是“有明显实现基础、但还缺一轮产品收束与关键链路补完”的项目。
|
||||
|
||||
更准确地说:
|
||||
|
||||
- 产品层面,方向已经比以前清楚,现有 PRD 可以继续作为重启依据。
|
||||
- 技术层面,后端主能力、记忆系统、Provider 管理、异步任务和基础 HA 都不是空白。
|
||||
- 体验层面,Storybook 恢复、音频闭环、前端状态设计已明显推进,但统一工作流与统一重试入口仍是关键缺口。
|
||||
- 工程层面,主前端与后端可用,但 admin-frontend 与 lint 问题说明项目还没完成最后一轮收尾。
|
||||
|
||||
因此,文档已经足够清晰,可以进入下一阶段:按优先级开始改代码,而不是继续扩写更多概念文档。
|
||||
@@ -1,139 +0,0 @@
|
||||
# DreamWeaver Weekend Handoff - 2026-04-17
|
||||
|
||||
## Purpose
|
||||
|
||||
这份文档用于周末在另一台电脑上继续推进 DreamWeaver 时快速接手,不需要先重新阅读大量聊天记录或从工作区猜测上下文。
|
||||
|
||||
---
|
||||
|
||||
## What Is Already On Remote
|
||||
|
||||
当前远端 `main` 已经包含两个连续 checkpoint:
|
||||
|
||||
- Commit: `a97a2fe`
|
||||
- Message: `feat: persist story generation states and cache audio`
|
||||
|
||||
这个 checkpoint 覆盖的主线如下:
|
||||
|
||||
- 新增并落地统一生成状态字段:
|
||||
- `generation_status`
|
||||
- `image_status`
|
||||
- `audio_status`
|
||||
- `last_error`
|
||||
- Storybook 阅读页支持按 ID 恢复
|
||||
- 故事列表页、故事详情页、绘本阅读页接入统一状态展示
|
||||
- 音频首次生成后缓存落盘并可复用
|
||||
- 统一状态语义中 `degraded_completed` 已和错误展示保持一致
|
||||
|
||||
- Commit: `b8d3cb4`
|
||||
- Message: `wip: snapshot full local workspace state`
|
||||
|
||||
这个 checkpoint 已把 2026-04-17 晚间的工作区快照同步到远端,包括:
|
||||
|
||||
- 新增 `AGENTS.md`
|
||||
- 整理 `docs/` 文档信息架构
|
||||
- 新增求职版 PRD、统一生成工作流 PRD、Week 1 backlog 与文档盘点
|
||||
- 归档旧 backend docs 到 `docs/archive/`、`docs/technical/`、`docs/operations/`
|
||||
- 补齐 Storybook 带 ID 路由恢复相关前端改动
|
||||
|
||||
注意:`b8d3cb4` 是一次 WIP 快照提交,原始 diff 中包含大量行尾/格式噪音。继续开发时应以当前 `main` 代码与 `docs/` 中 Active 文档为准,不需要再回到 `a97a2fe` 重新整理。
|
||||
|
||||
---
|
||||
|
||||
## Current Local Status On 2026-04-18
|
||||
|
||||
2026-04-18 在个人电脑接手后已确认:
|
||||
|
||||
- 本地 `main` 与 `origin/main` 对齐到 `b8d3cb4`
|
||||
- 工作树起始状态干净
|
||||
- 已配置 Gitea HTTPS 推送凭据,并通过 `git push --dry-run origin HEAD:main`
|
||||
- 后端本地虚拟环境可用:`backend/.venv`
|
||||
- 主前端与管理端依赖已安装到各自 `node_modules`
|
||||
|
||||
---
|
||||
|
||||
## Recommended Reading Order
|
||||
|
||||
周末继续前,建议先阅读:
|
||||
|
||||
1. `docs/product/job-search-relaunch-prd.md`
|
||||
2. `docs/product/unified-generation-workflow-prd.md`
|
||||
3. `docs/planning/week-1-execution-backlog.md`
|
||||
4. `docs/planning/document-status-inventory.md`
|
||||
|
||||
---
|
||||
|
||||
## Environment Setup On The Next Machine
|
||||
|
||||
建议接手后先完成以下动作:
|
||||
|
||||
1. `git pull`
|
||||
2. `cd backend && alembic upgrade head`
|
||||
3. `cd backend && .venv/bin/python -m pytest -q`(macOS/Linux)
|
||||
4. `cd frontend && npm install`
|
||||
5. `cd frontend && ./node_modules/.bin/vue-tsc --noEmit`
|
||||
|
||||
如果主前端完整构建失败,优先检查 Rollup 可选原生包是否正常安装,而不是先怀疑本轮代码逻辑。
|
||||
|
||||
---
|
||||
|
||||
## Current Product / Engineering Position
|
||||
|
||||
当前阶段不是“继续加功能”,而是把 DreamWeaver 收敛成可讲述、可演示、可恢复的求职版产品。
|
||||
|
||||
已经完成的关键支点:
|
||||
|
||||
- 状态模型已落地,不再只是文档概念
|
||||
- Storybook 恢复能力已补上
|
||||
- 音频体验开始形成闭环
|
||||
|
||||
还没完成的关键工作:
|
||||
|
||||
- 普通故事、完整生成、绘本生成仍是多条 service 路径
|
||||
- 缺少统一资产重试入口
|
||||
- 缺少更清晰的统一 workflow 编排边界
|
||||
- admin-frontend 范围和 Provider 边界仍未收束
|
||||
|
||||
---
|
||||
|
||||
## Best Next Step
|
||||
|
||||
周末最值得继续做的第一优先级:
|
||||
|
||||
### P0: 统一资产补全过程
|
||||
|
||||
目标:
|
||||
|
||||
- 抽出封面生成和音频生成的共同步骤
|
||||
- 让图片 / 音频共享一套资产状态回写逻辑
|
||||
- 为后续“统一重试入口”打基础
|
||||
|
||||
为什么先做:
|
||||
|
||||
- 它直接承接已经落地的状态模型
|
||||
- 它比继续加页面更能体现系统设计能力
|
||||
- 它能把当前三条生成路径往统一 workflow 再推近一步
|
||||
|
||||
### P1: 统一重试入口
|
||||
|
||||
目标:
|
||||
|
||||
- 至少设计出一个清晰的 retry API 方向
|
||||
- 即使不一次性重命名为 `/api/generations/...`,也先做到内部统一
|
||||
|
||||
### P1: 收敛 service workflow
|
||||
|
||||
目标:
|
||||
|
||||
- 梳理普通故事 / 完整生成 / 绘本生成的共同步骤
|
||||
- 把“验证上下文 -> 生成主内容 -> 保存主记录 -> 补全资产 -> 状态回写”整理成更明确的共享流程
|
||||
|
||||
---
|
||||
|
||||
## Important Reminder
|
||||
|
||||
如果周末是在另一台电脑上继续,不要默认“今天下午所有本地修改”都已经上远端。当前最可靠的 source of truth 是:
|
||||
|
||||
- 远端代码:以 commit `b8d3cb4` 为准
|
||||
- 产品目标:以 `docs/product/job-search-relaunch-prd.md` 为准
|
||||
- 当前执行主线:以 `docs/product/unified-generation-workflow-prd.md` 与 `docs/planning/week-1-execution-backlog.md` 为准
|
||||
216
install.cmd
216
install.cmd
@@ -1,216 +0,0 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM Claude Code Windows CMD Bootstrap Script
|
||||
REM Installs Claude Code for environments where PowerShell is not available
|
||||
|
||||
REM Parse command line argument
|
||||
set "TARGET=%~1"
|
||||
if "!TARGET!"=="" set "TARGET=latest"
|
||||
|
||||
REM Validate target parameter
|
||||
if /i "!TARGET!"=="stable" goto :target_valid
|
||||
if /i "!TARGET!"=="latest" goto :target_valid
|
||||
echo !TARGET! | findstr /r "^[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*" >nul
|
||||
if !ERRORLEVEL! equ 0 goto :target_valid
|
||||
|
||||
echo Usage: %0 [stable^|latest^|VERSION] >&2
|
||||
echo Example: %0 1.0.58 >&2
|
||||
exit /b 1
|
||||
|
||||
:target_valid
|
||||
|
||||
REM Check for 64-bit Windows
|
||||
if /i "%PROCESSOR_ARCHITECTURE%"=="AMD64" goto :arch_valid
|
||||
if /i "%PROCESSOR_ARCHITECTURE%"=="ARM64" goto :arch_valid
|
||||
if /i "%PROCESSOR_ARCHITEW6432%"=="AMD64" goto :arch_valid
|
||||
if /i "%PROCESSOR_ARCHITEW6432%"=="ARM64" goto :arch_valid
|
||||
|
||||
echo Claude Code does not support 32-bit Windows. Please use a 64-bit version of Windows. >&2
|
||||
exit /b 1
|
||||
|
||||
:arch_valid
|
||||
|
||||
REM Set constants
|
||||
set "GCS_BUCKET=https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases"
|
||||
set "DOWNLOAD_DIR=%USERPROFILE%\.claude\downloads"
|
||||
set "PLATFORM=win32-x64"
|
||||
|
||||
REM Create download directory
|
||||
if not exist "!DOWNLOAD_DIR!" mkdir "!DOWNLOAD_DIR!"
|
||||
|
||||
REM Check for curl availability
|
||||
curl --version >nul 2>&1
|
||||
if !ERRORLEVEL! neq 0 (
|
||||
echo curl is required but not available. Please install curl or use PowerShell installer. >&2
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Always download latest version (which has the most up-to-date installer)
|
||||
call :download_file "!GCS_BUCKET!/latest" "!DOWNLOAD_DIR!\latest"
|
||||
if !ERRORLEVEL! neq 0 (
|
||||
echo Failed to get latest version >&2
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Read version from file
|
||||
set /p VERSION=<"!DOWNLOAD_DIR!\latest"
|
||||
del "!DOWNLOAD_DIR!\latest"
|
||||
|
||||
REM Download manifest
|
||||
call :download_file "!GCS_BUCKET!/!VERSION!/manifest.json" "!DOWNLOAD_DIR!\manifest.json"
|
||||
if !ERRORLEVEL! neq 0 (
|
||||
echo Failed to get manifest >&2
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Extract checksum from manifest
|
||||
call :parse_manifest "!DOWNLOAD_DIR!\manifest.json" "!PLATFORM!"
|
||||
if !ERRORLEVEL! neq 0 (
|
||||
echo Platform !PLATFORM! not found in manifest >&2
|
||||
del "!DOWNLOAD_DIR!\manifest.json" 2>nul
|
||||
exit /b 1
|
||||
)
|
||||
del "!DOWNLOAD_DIR!\manifest.json"
|
||||
|
||||
REM Download binary
|
||||
set "BINARY_PATH=!DOWNLOAD_DIR!\claude-!VERSION!-!PLATFORM!.exe"
|
||||
call :download_file "!GCS_BUCKET!/!VERSION!/!PLATFORM!/claude.exe" "!BINARY_PATH!"
|
||||
if !ERRORLEVEL! neq 0 (
|
||||
echo Failed to download binary >&2
|
||||
if exist "!BINARY_PATH!" del "!BINARY_PATH!"
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Verify checksum
|
||||
call :verify_checksum "!BINARY_PATH!" "!EXPECTED_CHECKSUM!"
|
||||
if !ERRORLEVEL! neq 0 (
|
||||
echo Checksum verification failed >&2
|
||||
del "!BINARY_PATH!"
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Run claude install to set up launcher and shell integration
|
||||
echo Setting up Claude Code...
|
||||
"!BINARY_PATH!" install "!TARGET!"
|
||||
set "INSTALL_RESULT=!ERRORLEVEL!"
|
||||
|
||||
REM Clean up downloaded file
|
||||
REM Wait a moment for any file handles to be released
|
||||
timeout /t 1 /nobreak >nul 2>&1
|
||||
del /f "!BINARY_PATH!" >nul 2>&1
|
||||
if exist "!BINARY_PATH!" (
|
||||
echo Warning: Could not remove temporary file: !BINARY_PATH!
|
||||
)
|
||||
|
||||
if !INSTALL_RESULT! neq 0 (
|
||||
echo Installation failed >&2
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Installation complete^^!
|
||||
echo.
|
||||
exit /b 0
|
||||
|
||||
REM ============================================================================
|
||||
REM SUBROUTINES
|
||||
REM ============================================================================
|
||||
|
||||
:download_file
|
||||
REM Downloads a file using curl
|
||||
REM Args: %1=URL, %2=OutputPath
|
||||
set "URL=%~1"
|
||||
set "OUTPUT=%~2"
|
||||
|
||||
curl -fsSL "!URL!" -o "!OUTPUT!"
|
||||
exit /b !ERRORLEVEL!
|
||||
|
||||
:parse_manifest
|
||||
REM Parse JSON manifest to extract checksum for platform
|
||||
REM Args: %1=ManifestPath, %2=Platform
|
||||
set "MANIFEST_PATH=%~1"
|
||||
set "PLATFORM_NAME=%~2"
|
||||
set "EXPECTED_CHECKSUM="
|
||||
|
||||
REM Use findstr to find platform section, then look for checksum
|
||||
set "FOUND_PLATFORM="
|
||||
set "IN_PLATFORM_SECTION="
|
||||
|
||||
REM Read the manifest line by line
|
||||
for /f "usebackq tokens=*" %%i in ("!MANIFEST_PATH!") do (
|
||||
set "LINE=%%i"
|
||||
|
||||
REM Check if this line contains our platform
|
||||
echo !LINE! | findstr /c:"\"%PLATFORM_NAME%\":" >nul
|
||||
if !ERRORLEVEL! equ 0 (
|
||||
set "IN_PLATFORM_SECTION=1"
|
||||
)
|
||||
|
||||
REM If we're in the platform section, look for checksum
|
||||
if defined IN_PLATFORM_SECTION (
|
||||
echo !LINE! | findstr /c:"\"checksum\":" >nul
|
||||
if !ERRORLEVEL! equ 0 (
|
||||
REM Extract checksum value
|
||||
for /f "tokens=2 delims=:" %%j in ("!LINE!") do (
|
||||
set "CHECKSUM_PART=%%j"
|
||||
REM Remove quotes, whitespace, and comma
|
||||
set "CHECKSUM_PART=!CHECKSUM_PART: =!"
|
||||
set "CHECKSUM_PART=!CHECKSUM_PART:"=!"
|
||||
set "CHECKSUM_PART=!CHECKSUM_PART:,=!"
|
||||
|
||||
REM Check if it looks like a SHA256 (64 hex chars)
|
||||
if not "!CHECKSUM_PART!"=="" (
|
||||
call :check_length "!CHECKSUM_PART!" 64
|
||||
if !ERRORLEVEL! equ 0 (
|
||||
set "EXPECTED_CHECKSUM=!CHECKSUM_PART!"
|
||||
exit /b 0
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
REM Check if we've left the platform section (closing brace)
|
||||
echo !LINE! | findstr /c:"}" >nul
|
||||
if !ERRORLEVEL! equ 0 set "IN_PLATFORM_SECTION="
|
||||
)
|
||||
)
|
||||
|
||||
if "!EXPECTED_CHECKSUM!"=="" exit /b 1
|
||||
exit /b 0
|
||||
|
||||
:check_length
|
||||
REM Check if string length equals expected length
|
||||
REM Args: %1=String, %2=ExpectedLength
|
||||
set "STR=%~1"
|
||||
set "EXPECTED_LEN=%~2"
|
||||
set "LEN=0"
|
||||
:count_loop
|
||||
if "!STR:~%LEN%,1!"=="" goto :count_done
|
||||
set /a LEN+=1
|
||||
goto :count_loop
|
||||
:count_done
|
||||
if %LEN%==%EXPECTED_LEN% exit /b 0
|
||||
exit /b 1
|
||||
|
||||
:verify_checksum
|
||||
REM Verify file checksum using certutil
|
||||
REM Args: %1=FilePath, %2=ExpectedChecksum
|
||||
set "FILE_PATH=%~1"
|
||||
set "EXPECTED=%~2"
|
||||
|
||||
for /f "skip=1 tokens=*" %%i in ('certutil -hashfile "!FILE_PATH!" SHA256') do (
|
||||
set "ACTUAL=%%i"
|
||||
set "ACTUAL=!ACTUAL: =!"
|
||||
if "!ACTUAL!"=="CertUtil:Thecommandcompletedsuccessfully." goto :verify_done
|
||||
if "!ACTUAL!" neq "" (
|
||||
if /i "!ACTUAL!"=="!EXPECTED!" (
|
||||
exit /b 0
|
||||
) else (
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
:verify_done
|
||||
exit /b 1
|
||||
@@ -1,10 +0,0 @@
|
||||
# Allow local socket access
|
||||
local all all trust
|
||||
|
||||
# Allow all IPv4/IPv6 client access in local docker network
|
||||
host all all 0.0.0.0/0 trust
|
||||
host all all ::/0 trust
|
||||
|
||||
# Allow streaming replication connections
|
||||
host replication all 0.0.0.0/0 trust
|
||||
host replication all ::/0 trust
|
||||
Reference in New Issue
Block a user