docs: package week 4 demo and architecture

This commit is contained in:
2026-04-18 22:14:22 +08:00
parent 70efaf3ccf
commit d5a173aa0d
9 changed files with 317 additions and 5 deletions

View File

@@ -151,9 +151,12 @@ npm run build
- `docs/planning/week-2-execution-backlog.md`:下一阶段执行 backlog
- `docs/planning/week-2-to-4-execution-backlog.md`Week 2 到 Week 4 总执行 backlog
- `docs/planning/demo-checklist.md`:求职演示检查清单
- `docs/planning/demo-package.md`:求职版 Demo 包装页
- `docs/planning/demo-validation-log.md`:本地 Docker 演示验证记录
- `docs/planning/interview-pitch.md`3 分钟项目讲解稿
- `docs/planning/week-1-sprint-review.md`Week 1 复盘总结
- `docs/planning/week-4-sprint-review.md`Week 4 复盘和生产化 backlog
- `docs/technical/architecture.md`:求职版架构说明
- `docs/technical/api-compatibility.md`:旧生成 API 兼容层策略
- `docs/technical/generation-job-state.md`Generation Job 状态落库决策
- `docs/technical/memory-system-dev.md`:记忆系统技术说明

View File

@@ -87,6 +87,9 @@ const currentStoryId = computed(() => {
return Number.isFinite(parsed) ? parsed : null
})
const storybookTraceId = computed(() => storybook.value?.id ?? currentStoryId.value)
const readingPositionKey = computed(() => (
storybookTraceId.value ? `dreamweaver:admin-storybook:${storybookTraceId.value}:page` : null
))
function goHome() {
store.clearStorybook()
@@ -105,6 +108,22 @@ function prevPage() {
}
}
function restoreReadingPosition() {
const key = readingPositionKey.value
if (!key || !totalPages.value) {
currentPageIndex.value = -1
return
}
const savedValue = Number(window.localStorage.getItem(key))
if (!Number.isFinite(savedValue)) {
currentPageIndex.value = -1
return
}
currentPageIndex.value = Math.min(Math.max(savedValue, -1), totalPages.value - 1)
}
async function loadStorybook() {
loading.value = true
error.value = ''
@@ -125,6 +144,7 @@ async function loadStorybook() {
}
if (cachedStorybook?.id === storyId) {
restoreReadingPosition()
loading.value = false
await generationTraceRef.value?.refresh()
return
@@ -155,6 +175,7 @@ async function loadStorybook() {
last_error: detail.last_error,
retryable_assets: detail.retryable_assets,
})
restoreReadingPosition()
await generationTraceRef.value?.refresh()
} catch (e) {
error.value = e instanceof Error ? e.message : '绘本加载失败'
@@ -208,6 +229,13 @@ watch(
},
{ immediate: true },
)
watch(currentPageIndex, (pageIndex) => {
const key = readingPositionKey.value
if (key) {
window.localStorage.setItem(key, String(pageIndex))
}
})
</script>
<template>

View File

@@ -24,6 +24,9 @@
- `planning/demo-checklist.md`
求职演示检查清单。用于演示前确认 Docker 环境、核心链路、话术和风险预案。
- `planning/demo-package.md`
求职版 Demo 包装页。用于面试前快速准备演示路径、讲解锚点和失败预案。
- `planning/demo-validation-log.md`
本地 Docker 演示验证记录。用于说明最近一次构建、smoke 和手动检查结果。
@@ -33,8 +36,14 @@
- `planning/week-1-sprint-review.md`
Week 1 复盘总结。用于沉淀已完成成果、剩余缺口和下一阶段建议。
- `planning/week-4-sprint-review.md`
Week 4 复盘总结。用于说明求职版 Demo 包装、当前状态和生产化 backlog。
## Technical
- `technical/architecture.md`
求职版架构说明。用于解释产品闭环、后端分层、状态模型和可观测性。
- `technical/memory-system-dev.md`
记忆系统技术说明。用于后续继续做孩子档案、故事宇宙和个性化生成。

View File

@@ -0,0 +1,66 @@
# DreamWeaver 求职版 Demo 包装
这份文档用于演示前 5 分钟快速准备,也可以作为面试官追问时的项目导航。
---
## 1. 一句话定位
DreamWeaver 是面向 3-8 岁亲子场景的个性化 AI 绘本与陪伴式讲述产品。它把孩子档案、故事宇宙、故事生成、绘本插图、语音缓存、阅读事件和成长记忆串成一个可恢复的阅读闭环。
---
## 2. 演示前检查
```bash
docker compose up -d --build
./scripts/demo_smoke.sh
```
需要验证语音链路时:
```bash
SMOKE_AUDIO=1 ./scripts/demo_smoke.sh
```
演示入口:
- 用户端:`http://localhost:52080`
- 本地登录:`http://localhost:52080/auth/dev/signin`
- 管理端:`http://localhost:52888`
- 后端健康:`http://localhost:52000/health`
---
## 3. 主演示路径
1. 使用本地登录进入用户端。
2. 创建普通故事,说明主内容优先可读。
3. 打开故事详情页,展示 `partial_ready`、封面补全、音频缓存状态和生成轨迹。
4. 补全封面或音频,说明资产失败不会覆盖正文。
5. 创建绘本,进入绘本阅读器。
6. 刷新页面或重新进入绘本,说明按 ID 恢复和阅读位置恢复。
7. 回到故事库,展示跨故事 Provider 运营摘要。
8. 打开孩子时间线,展示阅读事件和记忆沉淀。
---
## 4. 面试讲解锚点
- **产品判断**:求职版不追求功能越多,而是围绕亲子阅读闭环收敛。
- **AI 不确定性处理**:主内容和资产拆开,图片/音频失败不阻塞阅读。
- **Provider 产品化**:用户看到稳定能力,系统内部用 Capability / Provider / Adapter / Routing Policy 管供应链。
- **可观测性**generation job/event 让生成过程、失败恢复和 Provider 成本可解释。
- **可继续生产化**:前端已有轮询形态,后端已有任务事件模型,下一步可以迁移到 worker。
---
## 5. 失败预案
| 风险 | 现场处理 |
| --- | --- |
| TTS 网络失败 | 说明音频是可恢复资产,展示缓存状态或跳过语音 |
| 图片生成失败 | 展示 `degraded_completed` 与资源重试 |
| Docker 冷启动慢 | 演示前先跑 smoke 并保持容器运行 |
| Provider 追问过深 | 回到 Capability / Provider / Adapter / Routing Policy 四层解释 |
| 生产化追问 | 说明下一步是 worker 化、监控告警、密钥治理和 Provider analytics 扩展 |

View File

@@ -14,6 +14,8 @@
- 音频缓存治理首版已验证:`GET /api/audio/{story_id}/status` 查询状态不触发生成,`DELETE /api/audio/{story_id}/cache` 可清理缓存并让音频重新进入可补全状态。
- 时间线联动已验证:阅读事件会生成更完整的 recent_story 记忆,孩子时间线会展示阅读记录和记忆沉淀。
- `./scripts/demo_smoke.sh` 已覆盖音频缓存状态查询。
- Week 4 Demo 包装已完成新增架构说明、Demo 包装文档、Week 4 sprint review用户端和管理端绘本阅读器支持阅读位置恢复。
- Week 4 最终回归通过:后端全量测试 85 passedruff 通过,用户端/管理端构建通过,`docker compose up -d --build``./scripts/demo_smoke.sh` 通过。
- 后端新增 `partial_ready``text_status` 与迁移 `0012_story_text_status` 后,`backend/.venv/bin/python -m pytest backend/tests -q` 通过82 个测试通过。
- `backend/.venv/bin/python -m ruff check backend/app backend/tests backend/alembic/versions/0012_add_story_text_status_and_partial_ready.py` 通过。
- 用户端与管理端 `npm run build` 均通过。

View File

@@ -61,11 +61,11 @@ Week 2 已完成演示闭环、统一生成工作流、generation job/event、
| ID | Workstream | Task | Output | Priority | Status |
| --- | --- | --- | --- | --- | --- |
| W4-01 | Frontend | 结果页与阅读页体验收尾 | 故事详情、绘本阅读、故事库状态一致 | P0 | Pending |
| W4-02 | Docs | 架构图与系统说明 | `docs/technical/architecture.md` 或 Mermaid 图 | P0 | Pending |
| W4-03 | Demo | 求职版 Demo 包装 | 演示路径、风险预案、检查命令一页化 | P0 | Pending |
| W4-04 | QA | 全量回归与验证记录 | pytest、ruff、前端 build、Docker smoke | P0 | Pending |
| W4-05 | Product | 项目复盘与下一阶段路线 | Week 4 review + production backlog | P1 | Pending |
| W4-01 | Frontend | 结果页与阅读页体验收尾 | 故事详情音频缓存、绘本阅读位置恢复、故事库状态一致 | P0 | Done |
| W4-02 | Docs | 架构图与系统说明 | `docs/technical/architecture.md` | P0 | Done |
| W4-03 | Demo | 求职版 Demo 包装 | `docs/planning/demo-package.md` | P0 | Done |
| W4-04 | QA | 全量回归与验证记录 | pytest、ruff、前端 build、Docker smoke | P0 | Done |
| W4-05 | Product | 项目复盘与下一阶段路线 | `docs/planning/week-4-sprint-review.md` | P1 | Done |
---

View File

@@ -0,0 +1,69 @@
# DreamWeaver Week 4 Sprint Review
**Date**: 2026-04-18
**Theme**: 求职版 Demo 包装、结果体验收尾与生产化路线
---
## 1. 本阶段完成
- Week 2-4 总 backlog 已固化Week 2 全部完成。
- Week 3 已补齐音频缓存治理首版:
- 音频缓存状态查询
- 音频缓存清理
- 故事详情页展示缓存大小和更新时间
- Week 3 已补齐时间线与记忆联动:
- 阅读事件进入孩子成长时间线
- 阅读事件生成的 `recent_story` 记忆带上故事模式、封面、阅读时长和来源
- 时间线能展示阅读记录与记忆沉淀
- Week 4 已补齐绘本阅读位置恢复。
- Week 4 已输出架构说明和 Demo 包装文档。
---
## 2. 当前项目状态
DreamWeaver 已经具备求职演示所需的完整闭环:
`孩子档案 -> 输入主题 -> 生成故事/绘本 -> 资产补全 -> 语音缓存 -> 阅读记录 -> 记忆沉淀 -> 成长时间线 -> 复用上下文生成新故事`
同时具备可解释的系统设计:
- 统一生成入口
- 统一状态模型
- generation job/event
- Provider failover 和聚合指标
- 跨故事 Provider analytics
- 前端生成轨迹和自动轮询形态
---
## 3. 验证状态
最近一轮验证包括:
- 后端全量测试85 passed
- 后端 ruff通过
- 用户端生产构建:通过
- 管理端生产构建:通过
- Docker 全栈重建:通过
- demo smoke通过
---
## 4. 生产化 Backlog
| Priority | Task | Why |
| --- | --- | --- |
| P0 | 将同步生成迁移到 Celery worker | 支持真实长任务、断点恢复和后台进度 |
| P0 | Provider analytics 加入时间窗口和失败原因 | 让运营分析可用于成本与稳定性决策 |
| P1 | 音频缓存过期策略与后台清理 | 控制磁盘占用和缓存生命周期 |
| P1 | 生成任务取消与重试队列 | 防止重复任务和用户误触造成浪费 |
| P1 | 监控告警与结构化 dashboard | 上线前需要可观测性闭环 |
| P2 | 更细粒度叙事风格与音色策略 | 扩展体验,但不影响当前求职版主线 |
---
## 5. 面试表达
这个项目最重要的不是“接了几个 AI API”而是把模型的不确定性变成了可恢复、可解释、可追踪的产品体验。主内容优先保存资产独立补全状态明确可读Provider 调用可观测,阅读行为还能反哺记忆和下一次生成。

View File

@@ -0,0 +1,107 @@
# DreamWeaver 求职版架构说明
这份说明用于面试和复盘场景,帮助快速解释 DreamWeaver 如何把多模型能力收敛成稳定的亲子阅读产品闭环。
---
## 1. 产品闭环
```mermaid
flowchart LR
A["孩子档案 / 故事宇宙"] --> B["输入主题或教育目标"]
B --> C["统一生成入口 /api/generations"]
C --> D["文本故事或绘本结构"]
D --> E["主记录保存到 stories"]
E --> F["封面 / 插图 / 音频资产补全"]
F --> G["故事详情 / 绘本阅读器"]
G --> H["阅读事件"]
H --> I["记忆沉淀与成长时间线"]
I --> A
```
核心取舍是主内容优先可读,封面、插图和音频作为可恢复资产补全。这样 AI 资产失败不会摧毁用户已经得到的故事。
---
## 2. 后端分层
```mermaid
flowchart TB
FE["Vue 用户端 / 管理端"] --> API["FastAPI API 层"]
API --> WF["story_service 统一工作流"]
WF --> JOB["generation_jobs / events"]
WF --> ST["story_status 状态推导"]
WF --> MEM["memory_service 记忆上下文"]
WF --> ROUTER["provider_router"]
ROUTER --> POLICY["provider_policy"]
ROUTER --> ADAPTER["text / image / tts / storybook adapters"]
WF --> DB["PostgreSQL: stories, profiles, universes, reading_events, memory_items"]
WF --> CACHE["音频文件缓存"]
```
职责边界:
- API 层负责认证、限流、兼容 header 和响应模型。
- `story_service` 负责产品工作流:上下文准备、主记录保存、资产补全、状态回写。
- `story_status` 负责把文本、图片、音频状态推导为统一 `generation_status`
- `generation_jobs` 负责生成任务、事件流、进度摘要和 Provider 聚合。
- `provider_router` 负责运行时调用、failover、指标和成本记录。
- 前端只消费标准状态和 `retryable_assets`,不自行猜测重试逻辑。
---
## 3. 状态模型
```mermaid
stateDiagram-v2
[*] --> narrative_ready
narrative_ready --> partial_ready: 主内容可读且资产待补全
partial_ready --> assets_generating: 发起封面 / 插图 / 音频
assets_generating --> completed: 所需资产完成
assets_generating --> degraded_completed: 资产失败但主内容可读
degraded_completed --> assets_generating: 用户重试失败资产
completed --> assets_generating: 用户清理或重做资产
narrative_ready --> failed: 主内容生成失败
```
关键语义:
- `text_status` 只表达主文本或绘本结构是否可读。
- `partial_ready` 表示主内容可读,资产仍可补全。
- `degraded_completed` 表示主内容可读,但至少一个资产失败。
- `retryable_assets` 由后端标准返回,前端按字段展示 CTA。
---
## 4. 可观测性
```mermaid
flowchart LR
G["生成 / 重试请求"] --> J["generation_jobs"]
J --> E["generation_job_events"]
E --> T["任务详情时间线"]
E --> S["单故事 Provider stats"]
E --> A["跨故事 Provider analytics"]
T --> UI["故事详情 / 绘本阅读"]
S --> UI
A --> LIB["故事库运营摘要"]
```
当前已经能说明:
- 某个故事来自哪次生成任务。
- 每次任务执行到哪一步。
- Provider 是否成功、耗时多少、预估成本多少。
- 失败资源是否可以单独重试。
---
## 5. 当前边界与下一步
当前仍是求职版 MVP不引入复杂工作流引擎。下一步生产化优先级
1. 把同步生成迁移到后台 worker。
2. 基于现有 job 查询和前端轮询展示真实异步进度。
3. 扩展 Provider analytics 的时间窗口、失败原因和跨用户维度。
4. 为音频缓存增加过期策略和后台清理任务。
5. 补充部署、监控告警和密钥治理策略。

View File

@@ -87,6 +87,9 @@ const currentStoryId = computed(() => {
return Number.isFinite(parsed) ? parsed : null
})
const storybookTraceId = computed(() => storybook.value?.id ?? currentStoryId.value)
const readingPositionKey = computed(() => (
storybookTraceId.value ? `dreamweaver:storybook:${storybookTraceId.value}:page` : null
))
function goHome() {
store.clearStorybook()
@@ -105,6 +108,22 @@ function prevPage() {
}
}
function restoreReadingPosition() {
const key = readingPositionKey.value
if (!key || !totalPages.value) {
currentPageIndex.value = -1
return
}
const savedValue = Number(window.localStorage.getItem(key))
if (!Number.isFinite(savedValue)) {
currentPageIndex.value = -1
return
}
currentPageIndex.value = Math.min(Math.max(savedValue, -1), totalPages.value - 1)
}
async function loadStorybook() {
loading.value = true
error.value = ''
@@ -125,6 +144,7 @@ async function loadStorybook() {
}
if (cachedStorybook?.id === storyId) {
restoreReadingPosition()
loading.value = false
await generationTraceRef.value?.refresh()
return
@@ -155,6 +175,7 @@ async function loadStorybook() {
last_error: detail.last_error,
retryable_assets: detail.retryable_assets,
})
restoreReadingPosition()
await generationTraceRef.value?.refresh()
} catch (e) {
error.value = e instanceof Error ? e.message : '绘本加载失败'
@@ -208,6 +229,13 @@ watch(
},
{ immediate: true },
)
watch(currentPageIndex, (pageIndex) => {
const key = readingPositionKey.value
if (key) {
window.localStorage.setItem(key, String(pageIndex))
}
})
</script>
<template>